Skip to content

DO NOT MERGE: Hack the CAWG SDK to create error test case files #1127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
134848d
Add code to write the test case file to fixtures directory
scouten-adobe May 22, 2025
8ab9731
WRONG: Generate an identity assertion that contains a field that's no…
scouten-adobe May 22, 2025
0b101e8
WRONG: Generate an identity assertion with a referenced assertion tha…
scouten-adobe May 22, 2025
0bd11af
WRONG: Reference the same assertion twice
scouten-adobe May 22, 2025
48db254
WRONG: Remove the hard binding assertion reference
scouten-adobe May 22, 2025
1409384
WRONG: Non-standard label for X509+COSE signature type
scouten-adobe May 22, 2025
586a0ec
WRONG: Non-zero value in pad1 field
scouten-adobe May 23, 2025
671e97b
WRONG: Non-zero value in pad2
scouten-adobe May 23, 2025
7bd51b1
We're done with X.509 test cases; back out and start again
scouten-adobe May 23, 2025
366298e
Create a test-quality credential holder for identity claims aggregati…
scouten-adobe May 23, 2025
536fb50
WRONG: Write bad COSE signature data
scouten-adobe May 23, 2025
2968e6f
WRONG: Specify unsupported COSE signature algorithm
scouten-adobe May 23, 2025
eb8f6db
WRONG: Don't specify a COSE signature algorithm
scouten-adobe May 23, 2025
a007e49
WRONG: Incorrect content-type header
scouten-adobe May 23, 2025
dba3176
WRONG: Omit content-type header
scouten-adobe May 23, 2025
d772da9
WRONG: Incorrect (assigned value) content type header
scouten-adobe May 23, 2025
db4eec1
WRONG: Create a VC that isn't valid JSON
scouten-adobe May 23, 2025
db01ba8
WRONG: Don't include the VC in the COSE envelope payload
scouten-adobe May 23, 2025
986fb5c
WRONG: Generate an issuer DID that isn't actually a DID
scouten-adobe May 23, 2025
4b11e33
WRONG: Generate issuer DID using an unsupported DID method
scouten-adobe May 23, 2025
7e064ae
WRONG: Generate an ICA with an unresolvable did:web URI
scouten-adobe May 23, 2025
08b7b8a
WRONG: Generate an ICA VC whose DID document lacks an assertionMethod…
scouten-adobe May 23, 2025
942add3
WRONG: Generate a signature mismatch
scouten-adobe May 23, 2025
d26f4db
VALID: Generate an ICA credential with a valid time stamp
scouten-adobe May 23, 2025
d987547
WRONG: Make time stamp invalid by signing an incorrect payload
scouten-adobe May 23, 2025
ce63474
WRONG: Set `valid_from` to far in the future
scouten-adobe May 23, 2025
4bcf27e
WRONG: Omit the `valid_from` field
scouten-adobe May 23, 2025
0ca502e
WRONG: Generate a `validFrom` that is after the CAWG signer time stamp
scouten-adobe May 23, 2025
db8e253
VALID: Generate a validUntil that is far in the future
scouten-adobe May 23, 2025
92d94ad
WRONG: Set valid_until to a date far in the past
scouten-adobe May 23, 2025
721314d
WRONG: Tamper with the signer_payload so it doesn't match what's in t…
scouten-adobe May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// 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 async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
use coset::{iana::OkpKeyParameter, RegisteredLabel};
use iref::UriBuf;
use nonempty_collections::{nev, NEVec};
use x509_parser::pem::Pem;

use super::ica_credential_example::ica_example_identities;
use crate::{
crypto::{
cose::{sign_async, sign_v2_embedded_async, CosePayload, TimeStampStorage},
raw_signature::{self, AsyncRawSigner},
},
identity::{
builder::{
AsyncCredentialHolder, AsyncIdentityAssertionBuilder, AsyncIdentityAssertionSigner,
IdentityBuilderError,
},
claim_aggregation::{
w3c_vc::jwk::{Algorithm, Base64urlUInt, Jwk, OctetParams, Params},
IcaCredential, IcaSignatureVerifier, IdentityClaimsAggregationVc, VerifiedIdentity,
},
tests::fixtures::{cert_chain_and_private_key_for_alg, manifest_json, parent_json},
IdentityAssertion, SignerPayload,
},
status_tracker::StatusTracker,
Builder, HashedUri, Reader, SigningAlg,
};

/// An implementation of [`AsyncCredentialHolder`] that generates an identity
/// claims aggregation credential.
///
/// This is not intended for production use; it has only been used so far to
/// generate error test cases.
pub struct IcaExampleCredentialHolder {
/// Verified identities to be used for this named actor.
pub verified_identities: NEVec<VerifiedIdentity>,

/// Signer for the COSE envelope (i.e. the credential of the example
/// identity claims aggregator).
pub ica_signer: Box<dyn AsyncRawSigner + Send + Sync + 'static>,

/// DID for the simulated identity claims aggregator.
pub issuer_did: String,
}

impl IcaExampleCredentialHolder {
/// Create an `IcaExampleCredentialHolder` instance by wrapping an instance
/// of [`AsyncRawSigner`].
///
/// The [`AsyncRawSigner`] implementation actually holds (or has access to)
/// the relevant certificates and private key material.
///
/// This will generate a sample set of verified identities to match the
/// example used in the CAWG specification.
///
/// [`AsyncRawSigner`]: c2pa_crypto::raw_signature::AsyncRawSigner
pub fn from_async_raw_signer(
ica_signer: Box<dyn AsyncRawSigner + Send + Sync + 'static>,
issuer_did: String,
) -> Self {
Self {
verified_identities: ica_example_identities(),
ica_signer,
issuer_did,
}
}
}

#[async_trait]
impl AsyncCredentialHolder for IcaExampleCredentialHolder {
fn sig_type(&self) -> &'static str {
crate::identity::claim_aggregation::CAWG_ICA_SIG_TYPE
}

fn reserve_size(&self) -> usize {
// TO DO: Refine the guessing mechanism. Should also account for the size of
// verified_identities.
self.ica_signer.reserve_size() + 1500
}

async fn sign(&self, signer_payload: &SignerPayload) -> Result<Vec<u8>, IdentityBuilderError> {
// IMPORTANT: Since this is test-quality code, I am using .unwrap() liberally
// here. These would need to be replaced with proper error handling in order to
// make this into production-level code.

// Pre-process signer_payload to base64 encode the hash references.

let mut signer_payload = signer_payload.clone();

let encoded_assertions = signer_payload
.referenced_assertions
.iter()
.map(|a| {
let encoded_hash = crate::crypto::base64::encode(&a.hash());
HashedUri::new(a.url(), a.alg(), encoded_hash.as_bytes())
})
.collect();

signer_payload.referenced_assertions = encoded_assertions;

// WRONG: Tamper with the signer_payload so it doesn't match what's in the outer
// wrapper of the identity assertion.

let mut signer_payload = signer_payload.clone();

let ref_0 = signer_payload.referenced_assertions[0].clone();
let mut wrong_hash = ref_0.hash();
wrong_hash[0] = 42;
wrong_hash[4] = 98;

signer_payload.referenced_assertions[0] =
HashedUri::new(ref_0.url(), ref_0.alg(), &wrong_hash);

// Generate VC to embed.
let ica_subject = IdentityClaimsAggregationVc {
c2pa_asset: signer_payload.clone(),
verified_identities: self.verified_identities.clone(),
time_stamp: None,
};

let issuer_did = UriBuf::new(self.issuer_did.as_bytes().to_vec()).unwrap();
let mut ica_vc = IcaCredential::new(None, issuer_did, nev![ica_subject]);

// TO DO: Bring in substitute for now() on Wasm.
#[cfg(not(target_arch = "wasm32"))]
{
ica_vc.valid_from = Some(Utc::now().fixed_offset());
}

let ica_json = serde_json::to_string(&ica_vc).unwrap();

// TO DO: Check signing cert validity. (See signing_cert_valid in c2pa-rs's
// cose_sign.)

// TO DO: Switch to new v2_embedded API.
Ok(sign_v2_embedded_async(
self.ica_signer.as_ref(),
ica_json.as_bytes(),
None,
CosePayload::Embedded,
Some(RegisteredLabel::Text("application/vc".to_string())),
TimeStampStorage::V2_sigTst2_CTT,
)
.await
.map_err(|e| IdentityBuilderError::SignerError(e.to_string()))?)
}
}

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

#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn ica_signing() {
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 = AsyncIdentityAssertionSigner::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();

// HACK: Parse end-entity cert and find public key so we can build a did:jwk for
// it.
let first_pem = Pem::iter_from_buffer(&cawg_cert_chain)
.next()
.unwrap()
.unwrap();
let cert = first_pem.parse_x509().unwrap();
let spki = &cert.tbs_certificate.subject_pki;
let public_key = spki.subject_public_key.as_ref();

let jwk = Jwk {
public_key_use: None,
key_operations: None,
algorithm: Some(Algorithm::EdDsa),
key_id: None, // Maybe we need this?
x509_url: None,
x509_certificate_chain: None,
x509_thumbprint_sha1: None,
x509_thumbprint_sha256: None,
params: Params::Okp(OctetParams {
curve: "Ed25519".to_owned(),
public_key: Base64urlUInt(public_key.to_vec()),
private_key: None,
}),
};

let jwk_id = serde_json::to_string(&jwk).unwrap();
let jwk_base64 = crate::crypto::base64::encode(jwk_id.as_bytes());
let issuer_did = format!("did:jwk:{jwk_base64}");

let ica_holder = IcaExampleCredentialHolder::from_async_raw_signer(cawg_raw_signer, issuer_did);
let iab = AsyncIdentityAssertionBuilder::for_credential_holder(ica_holder);
c2pa_signer.add_identity_assertion(iab);

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

// Write error test case file.
// HINT: To cut to the chase and only run this test, run the following
// from the command line:
//
// ```
// cargo test -p c2pa --lib ica_signing
// ```
std::fs::create_dir_all("src/identity/tests/fixtures/claim_aggregation/ica_validation")
.unwrap();

std::fs::write(
"src/identity/tests/fixtures/claim_aggregation/ica_validation/signer_payload_mismatch.jpg",
dest.get_ref(),
)
.unwrap();

// --- THE REST OF THIS EXAMPLE IS TEST CODE ONLY. ---
//
// The following code reads back the content from the file that was just
// generated and verifies that it is valid.
//
// In a normal scenario when generating an asset with a CAWG identity assertion,
// you could stop at this point.

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 st = StatusTracker::default();
let mut ia_iter = IdentityAssertion::from_manifest(manifest, &mut st);

let ia = ia_iter.next().unwrap().unwrap();
assert!(ia_iter.next().is_none());
drop(ia_iter);

let ica_verifier = IcaSignatureVerifier {};
let ica_vc = ia.validate(manifest, &mut st, &ica_verifier).await.unwrap();

dbg!(ica_vc);
panic!("Now what?");
}
3 changes: 3 additions & 0 deletions sdk/src/identity/tests/fixtures/claim_aggregation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@
// each license.

pub(crate) mod ica_credential_example;

#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod ica_example_credential_holder;
Loading