Skip to content

Commit

Permalink
feat: X.509 support for CAWG identity SDK (#880)
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten-adobe authored Jan 25, 2025
1 parent 0fccae9 commit 3a6a26b
Show file tree
Hide file tree
Showing 10 changed files with 370 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cawg_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async-trait = "0.1.78"
base64 = "0.22.1"
c2pa = { path = "../sdk", version = "0.43.0", features = ["openssl", "unstable_api"] }
c2pa-crypto = { path = "../internal/crypto", version = "0.5.0" }
c2pa-status-tracker = { path = "../internal/status-tracker", version = "0.3.0" }
chrono = { version = "0.4.38", features = ["serde"] }
ciborium = "0.2.2"
coset = "0.3.8"
Expand Down
11 changes: 7 additions & 4 deletions cawg_identity/src/builder/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

use std::fmt::Debug;

use c2pa_crypto::raw_signature::RawSignerError;
use thiserror::Error;

/// Describes errors that can occur when building a CAWG identity assertion.
Expand All @@ -27,9 +26,13 @@ pub enum IdentityBuilderError {
#[error("error while generating CBOR ({0})")]
CborGenerationError(String),

/// An error occurred when generating the underlying raw signature.
#[error(transparent)]
RawSignerError(#[from] RawSignerError),
/// The credentials provided could not be used.
#[error("credential-related error ({0})")]
CredentialError(String),

/// An error occurred when generating the underlying signature.
#[error("error while generating signature ({0})")]
SignerError(String),

/// An unexpected internal error occured while requesting the time stamp
/// response.
Expand Down
2 changes: 2 additions & 0 deletions cawg_identity/src/claim_aggregation/ica_signature_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ impl SignatureVerifier for IcaSignatureVerifier {
));
};

// TO DO: Enforce signer_payload matches what was stated outside the signature.

// TO DO: Enforce validity window as compared to sig time (or now if no TSA
// time).

Expand Down
2 changes: 2 additions & 0 deletions cawg_identity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ pub(crate) mod internal;

#[cfg(test)]
pub(crate) mod tests;

pub mod x509;
1 change: 1 addition & 0 deletions cawg_identity/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod claim_aggregation;
pub(crate) mod fixtures;
mod identity_assertion;
mod internal;
mod x509;

#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
145 changes: 145 additions & 0 deletions cawg_identity/src/tests/x509.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright 2025 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.

// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
// specific language governing permissions and limitations under
// each license.

use std::io::{Cursor, Seek};

use c2pa::{Builder, Reader, SigningAlg};
use c2pa_crypto::raw_signature;
use serde_json::json;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test;

use crate::{
builder::{IdentityAssertionBuilder, IdentityAssertionSigner},
tests::fixtures::cert_chain_and_private_key_for_alg,
x509::{X509CredentialHolder, X509SignatureVerifier},
IdentityAssertion,
};

const TEST_IMAGE: &[u8] = include_bytes!("../../../sdk/tests/fixtures/CA.jpg");
const TEST_THUMBNAIL: &[u8] = include_bytes!("../../../sdk/tests/fixtures/thumbnail.jpg");

#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
async fn simple_case() {
let format = "image/jpeg";
let mut source = Cursor::new(TEST_IMAGE);
let mut dest = Cursor::new(Vec::new());

let mut builder = Builder::from_json(&manifest_json()).unwrap();
builder
.add_ingredient_from_stream(parent_json(), format, &mut source)
.unwrap();

builder
.add_resource("thumbnail.jpg", Cursor::new(TEST_THUMBNAIL))
.unwrap();

let mut c2pa_signer = IdentityAssertionSigner::from_test_credentials(SigningAlg::Ps256);

let (cawg_cert_chain, cawg_private_key) =
cert_chain_and_private_key_for_alg(SigningAlg::Ed25519);

let cawg_raw_signer = raw_signature::async_signer_from_cert_chain_and_private_key(
&cawg_cert_chain,
&cawg_private_key,
SigningAlg::Ed25519,
None,
)
.unwrap();

let x509_holder = X509CredentialHolder::from_async_raw_signer(cawg_raw_signer);
let iab = IdentityAssertionBuilder::for_credential_holder(x509_holder);
c2pa_signer.add_identity_assertion(iab);

builder
.sign_async(&c2pa_signer, format, &mut source, &mut dest)
.await
.unwrap();

// Read back the Manifest that was generated.
dest.rewind().unwrap();

let manifest_store = Reader::from_stream(format, &mut dest).unwrap();
assert_eq!(manifest_store.validation_status(), None);

let manifest = manifest_store.active_manifest().unwrap();
let mut ia_iter = IdentityAssertion::from_manifest(manifest);

// Should find exactly one identity assertion.
let ia = ia_iter.next().unwrap().unwrap();
assert!(ia_iter.next().is_none());

// And that identity assertion should be valid for this manifest.
let x509_verifier = X509SignatureVerifier {};
let sig_info = ia.validate(manifest, &x509_verifier).await.unwrap();

let cert_info = &sig_info.cert_info;
assert_eq!(cert_info.alg.unwrap(), SigningAlg::Ed25519);
assert_eq!(
cert_info.issuer_org.as_ref().unwrap(),
"C2PA Test Signing Cert"
);

// TO DO: Not sure what to check from COSE_Sign1.
}

fn manifest_json() -> String {
json!({
"vendor": "test",
"claim_generator_info": [
{
"name": "c2pa_test",
"version": "1.0.0"
}
],
"metadata": [
{
"dateTime": "1985-04-12T23:20:50.52Z",
"my_custom_metadata": "my custom metatdata value"
}
],
"title": "Test_Manifest",
"format": "image/tiff",
"instance_id": "1234",
"thumbnail": {
"format": "image/jpeg",
"identifier": "thumbnail.jpg"
},
"ingredients": [
{
"title": "Test",
"format": "image/jpeg",
"instance_id": "12345",
"relationship": "componentOf"
}
],
"assertions": [
{
"label": "org.test.assertion",
"data": "assertion"
}
]
})
.to_string()
}

fn parent_json() -> String {
json!({
"title": "Parent Test",
"format": "image/jpeg",
"instance_id": "12345",
"relationship": "parentOf"
})
.to_string()
}
28 changes: 28 additions & 0 deletions cawg_identity/src/x509/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2025 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.

// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
// specific language governing permissions and limitations under
// each license.

//! Contains implementations of [`CredentialHolder`] and [`SignatureVerifier`]
//! for the X.509 certificates credential type described as specified in
//! [§8.2, X.509 certificates and COSE signatures].
//!
//! [`CredentialHolder`]: crate::builder::CredentialHolder
//! [`SignatureVerifier`]: crate::SignatureVerifier
//! [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures
mod x509_credential_holder;
pub use x509_credential_holder::X509CredentialHolder;

mod x509_signature_verifier;
pub use x509_signature_verifier::X509SignatureVerifier;

const CAWG_X509_SIG_TYPE: &str = "cawg.x509.cose";
96 changes: 96 additions & 0 deletions cawg_identity/src/x509/x509_credential_holder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2025 Adobe. All rights reserved.
// This file is licensed to you under the Apache License,
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// or the MIT license (http://opensource.org/licenses/MIT),
// at your option.

// Unless required by applicable law or agreed to in writing,
// this software is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
// specific language governing permissions and limitations under
// each license.

use async_trait::async_trait;
use c2pa_crypto::{
cose::{sign_async, TimeStampStorage},
raw_signature::AsyncRawSigner,
};

use crate::{
builder::{CredentialHolder, IdentityBuilderError},
SignerPayload,
};

/// An implementation of [`CredentialHolder`] that generates COSE signatures
/// using X.509 credentials as specified in [§8.2, X.509 certificates and COSE
/// signatures].
///
/// [`SignatureVerifier`]: crate::SignatureVerifier
/// [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures
#[cfg(not(target_arch = "wasm32"))]
pub struct X509CredentialHolder(Box<dyn AsyncRawSigner + Send + Sync + 'static>);

/// An implementation of [`CredentialHolder`] that generates COSE signatures
/// using X.509 credentials as specified in [§8.2, X.509 certificates and COSE
/// signatures].
///
/// [`CredentialHolder`]: crate::builder::CredentialHolder
/// [§8.2, X.509 certificates and COSE signatures]: https://cawg.io/identity/1.1-draft/#_x_509_certificates_and_cose_signatures
#[cfg(target_arch = "wasm32")]
pub struct X509CredentialHolder(Box<dyn AsyncRawSigner + 'static>);

impl X509CredentialHolder {
/// Create an `X509CredentialHolder` instance by wrapping an instance of
/// [`AsyncRawSigner`].
///
/// The [`AsyncRawSigner`] implementation actually holds (or has access to)
/// the relevant certificates and private key material.
///
/// [`AsyncRawSigner`]: c2pa_crypto::raw_signature::AsyncRawSigner
#[cfg(not(target_arch = "wasm32"))]
pub fn from_async_raw_signer(signer: Box<dyn AsyncRawSigner + Send + Sync + 'static>) -> Self {
Self(signer)
}

/// Create an `X509CredentialHolder` instance by wrapping an instance of
/// [`AsyncRawSigner`].
///
/// The [`AsyncRawSigner`] implementation actually holds (or has access to)
/// the relevant certificates and private key material.
///
/// [`AsyncRawSigner`]: c2pa_crypto::raw_signature::AsyncRawSigner
#[cfg(target_arch = "wasm32")]
pub fn from_async_raw_signer(signer: Box<dyn AsyncRawSigner + 'static>) -> Self {
Self(signer)
}
}

#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl CredentialHolder for X509CredentialHolder {
fn sig_type(&self) -> &'static str {
super::CAWG_X509_SIG_TYPE
}

fn reserve_size(&self) -> usize {
self.0.reserve_size()
}

async fn sign(&self, signer_payload: &SignerPayload) -> Result<Vec<u8>, IdentityBuilderError> {
// TO DO: Check signing cert (see signing_cert_valid in c2pa-rs's cose_sign).

let mut sp_cbor: Vec<u8> = vec![];
ciborium::into_writer(signer_payload, &mut sp_cbor)
.map_err(|e| IdentityBuilderError::CborGenerationError(e.to_string()))?;

Ok(sign_async(
self.0.as_ref(),
&sp_cbor,
None,
TimeStampStorage::V2_sigTst2_CTT,
)
.await
.map_err(|e| IdentityBuilderError::SignerError(e.to_string()))?)
}
}
Loading

0 comments on commit 3a6a26b

Please sign in to comment.