-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: X.509 support for CAWG identity SDK (#880)
- Loading branch information
1 parent
0fccae9
commit 3a6a26b
Showing
10 changed files
with
370 additions
and
4 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,3 +31,5 @@ pub(crate) mod internal; | |
|
||
#[cfg(test)] | ||
pub(crate) mod tests; | ||
|
||
pub mod x509; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()))?) | ||
} | ||
} |
Oops, something went wrong.