From 6cf774ce95d16fd57676cf744debfb5a7438f301 Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Sun, 12 Jan 2025 18:24:09 -0800 Subject: [PATCH 1/3] feat(cawg_identity): Implement identity assertion validation --- Cargo.lock | 1 + cawg_identity/Cargo.toml | 1 + .../src/identity_assertion/assertion.rs | 59 ++++++++++++- .../src/identity_assertion/signer_payload.rs | 88 ++++++++++++++++++- .../identity_assertion/validation_error.rs | 2 +- .../src/tests/builder/simple_case.rs | 21 +++-- cawg_identity/src/tests/fixtures/mod.rs | 2 +- .../tests/fixtures/naive_credential_holder.rs | 27 +++++- 8 files changed, 188 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec4ac2be2..a04e56c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -829,6 +829,7 @@ dependencies = [ "c2pa-crypto", "ciborium", "hex-literal", + "regex", "serde", "serde_bytes", "serde_json", diff --git a/cawg_identity/Cargo.toml b/cawg_identity/Cargo.toml index d233736c2..c426bbebd 100644 --- a/cawg_identity/Cargo.toml +++ b/cawg_identity/Cargo.toml @@ -30,6 +30,7 @@ c2pa = { path = "../sdk", version = "0.40.0", features = ["openssl", "unstable_a c2pa-crypto = { path = "../internal/crypto", version = "0.2.0" } ciborium = "0.2.2" hex-literal = "0.4.1" +regex = "1.11" serde = { version = "1.0.197", features = ["derive"] } serde_bytes = "0.11.14" thiserror = "1.0.61" diff --git a/cawg_identity/src/identity_assertion/assertion.rs b/cawg_identity/src/identity_assertion/assertion.rs index a6867dde9..cea6f0436 100644 --- a/cawg_identity/src/identity_assertion/assertion.rs +++ b/cawg_identity/src/identity_assertion/assertion.rs @@ -13,18 +13,20 @@ use std::fmt::{Debug, Formatter}; +use c2pa::Manifest; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use crate::{ identity_assertion::signer_payload::SignerPayload, internal::debug_byte_slice::DebugByteSlice, + SignatureVerifier, ValidationError, }; /// This struct represents the raw content of the identity assertion. /// /// Use [`IdentityAssertionBuilder`] and -- at your option, -/// [`IdentityAssertionSigner`] to ensure correct construction of a new identity -/// assertion. +/// [`IdentityAssertionSigner`] -- to ensure correct construction of a new +/// identity assertion. /// /// [`IdentityAssertionBuilder`]: crate::builder::IdentityAssertionBuilder /// [`IdentityAssertionSigner`]: crate::builder::IdentityAssertionSigner @@ -44,6 +46,59 @@ pub struct IdentityAssertion { pub(crate) pad2: Option, } +impl IdentityAssertion { + /// Find the `IdentityAssertion`s that may be present in a given + /// [`Manifest`]. + /// + /// Iterator returns a [`Result`] because each assertion may fail to parse. + /// + /// Aside from CBOR parsing, no further validation is performed. + pub fn from_manifest( + manifest: &Manifest, + ) -> impl Iterator> + use<'_> { + manifest + .assertions() + .iter() + .filter(|a| a.label().starts_with("cawg.identity")) + .map(|a| a.to_assertion()) + } + + /// Using the provided [`SignatureVerifier`], check the validity of this + /// identity assertion. + /// + /// If successful, returns the credential-type specific information that can + /// be derived from the signature. This is the [`SignatureVerifier::Output`] + /// type which typically describes the named actor, but may also contain + /// information about the time of signing or the credential's source. + pub async fn validate( + &self, + manifest: &Manifest, + verifier: &SV, + ) -> Result> { + self.check_padding()?; + + self.signer_payload.check_against_manifest(manifest)?; + + verifier + .check_signature(&self.signer_payload, &self.signature) + .await + } + + fn check_padding(&self) -> Result<(), ValidationError> { + if !self.pad1.iter().all(|b| *b == 0) { + return Err(ValidationError::InvalidPadding); + } + + if let Some(pad2) = self.pad2.as_ref() { + if !pad2.iter().all(|b| *b == 0) { + return Err(ValidationError::InvalidPadding); + } + } + + Ok(()) + } +} + impl Debug for IdentityAssertion { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { f.debug_struct("IdentityAssertion") diff --git a/cawg_identity/src/identity_assertion/signer_payload.rs b/cawg_identity/src/identity_assertion/signer_payload.rs index 075943630..f94340fa2 100644 --- a/cawg_identity/src/identity_assertion/signer_payload.rs +++ b/cawg_identity/src/identity_assertion/signer_payload.rs @@ -11,11 +11,14 @@ // specific language governing permissions and limitations under // each license. -use std::fmt::Debug; +use std::{collections::HashSet, fmt::Debug, sync::LazyLock}; -use c2pa::HashedUri; +use c2pa::{HashedUri, Manifest}; +use regex::Regex; use serde::{Deserialize, Serialize}; +use crate::ValidationError; + /// A set of _referenced assertions_ and other related data, known overall as /// the **signer payload.** This binding **SHOULD** generally be construed as /// authorization of or participation in the creation of the statements @@ -36,3 +39,84 @@ pub struct SignerPayload { // TO DO: Add role and expected_* fields. // (https://github.com/contentauth/c2pa-rs/issues/816) } + +impl SignerPayload { + pub(super) fn check_against_manifest( + &self, + manifest: &Manifest, + ) -> Result<(), ValidationError> { + // All assertions mentioned in referenced_assertions also need to be referenced + // in the claim. + for ref_assertion in self.referenced_assertions.iter() { + if let Some(claim_assertion) = manifest.assertion_references().find(|a| { + // HACKY workaround for absolute assertion URLs as of c2pa-rs 0.36.0. + // See https://github.com/contentauth/c2pa-rs/pull/603. + let url = a.url(); + if url == ref_assertion.url() { + return true; + } + let url = ABSOLUTE_URL_PREFIX.replace(&url, ""); + url == ref_assertion.url() + }) { + if claim_assertion.hash() != ref_assertion.hash() { + return Err(ValidationError::AssertionMismatch( + ref_assertion.url().to_owned(), + )); + } + + // TO REVIEW WITH GAVIN: I'm getting different value for + // assertion.alg (None) via the DynamicAssertion API than what + // I'm getting when I read the claim back on validation + // (Some("ps256")). + + // if let Some(alg) = claim_assertion.alg().as_ref() { + // if Some(alg) != ref_assertion.alg().as_ref() { + // return Err(ValidationError::AssertionMismatch( + // ref_assertion.url().to_owned(), + // )); + // } + // } else { + // return Err(ValidationError::AssertionMismatch( + // ref_assertion.url().to_owned(), + // )); + // } + } else { + return Err(ValidationError::AssertionNotInClaim( + ref_assertion.url().to_owned(), + )); + } + } + + // Ensure that a hard binding assertion is present. + let ref_assertion_labels: Vec = self + .referenced_assertions + .iter() + .map(|ra| ra.url().to_owned()) + .collect(); + + if !ref_assertion_labels.iter().any(|ra| { + if let Some((_jumbf_prefix, label)) = ra.rsplit_once('/') { + label.starts_with("c2pa.hash.") + } else { + false + } + }) { + return Err(ValidationError::NoHardBindingAssertion); + } + + // Make sure no assertion references are duplicated. + let mut labels = HashSet::::new(); + + for label in &ref_assertion_labels { + let label = label.clone(); + if labels.contains(&label) { + return Err(ValidationError::DuplicateAssertionReference(label)); + } + labels.insert(label); + } + + Ok(()) + } +} + +static ABSOLUTE_URL_PREFIX: LazyLock = LazyLock::new(|| Regex::new("/c2pa/[^/]+/").unwrap()); diff --git a/cawg_identity/src/identity_assertion/validation_error.rs b/cawg_identity/src/identity_assertion/validation_error.rs index 598c1dcc2..2c5997f7e 100644 --- a/cawg_identity/src/identity_assertion/validation_error.rs +++ b/cawg_identity/src/identity_assertion/validation_error.rs @@ -38,7 +38,7 @@ pub enum ValidationError { /// The referenced assertion was referenced more than once by the identity /// assertion. #[error("the named with the label {0:#?} is referenced multiple times")] - DuplicateAssertionReferenced(String), + DuplicateAssertionReference(String), /// No hard-binding assertion was referenced by the identity assertion. #[error("no hard binding assertion is referenced")] diff --git a/cawg_identity/src/tests/builder/simple_case.rs b/cawg_identity/src/tests/builder/simple_case.rs index 1fc4d8740..325883d0c 100644 --- a/cawg_identity/src/tests/builder/simple_case.rs +++ b/cawg_identity/src/tests/builder/simple_case.rs @@ -18,7 +18,8 @@ use serde_json::json; use crate::{ builder::{IdentityAssertionBuilder, IdentityAssertionSigner}, - tests::fixtures::NaiveCredentialHolder, + tests::fixtures::{NaiveCredentialHolder, NaiveSignatureVerifier}, + IdentityAssertion, }; const TEST_IMAGE: &[u8] = include_bytes!("../../../../sdk/tests/fixtures/CA.jpg"); @@ -53,14 +54,22 @@ async fn simple_case() { // Read back the Manifest that was generated. dest.rewind().unwrap(); - let manifest_store = Reader::from_stream(format, &mut dest).unwrap(); + let manifest_store = Reader::from_stream(format, &mut dest).unwrap(); assert_eq!(manifest_store.validation_status(), None); - assert_eq!( - manifest_store.active_manifest().unwrap().title().unwrap(), - "Test_Manifest" - ); + 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(); + dbg!(&ia); + + assert!(ia_iter.next().is_none()); + + // And that identity assertion should be valid for this manifest. + let nsv = NaiveSignatureVerifier {}; + ia.validate(&manifest, &nsv).await.unwrap(); } fn manifest_json() -> String { diff --git a/cawg_identity/src/tests/fixtures/mod.rs b/cawg_identity/src/tests/fixtures/mod.rs index d35702298..f29a93559 100644 --- a/cawg_identity/src/tests/fixtures/mod.rs +++ b/cawg_identity/src/tests/fixtures/mod.rs @@ -14,7 +14,7 @@ #![allow(unused)] mod naive_credential_holder; -pub(crate) use naive_credential_holder::NaiveCredentialHolder; +pub(crate) use naive_credential_holder::{NaiveCredentialHolder, NaiveSignatureVerifier}; mod test_credentials; pub(crate) use test_credentials::cert_chain_and_private_key_for_alg; diff --git a/cawg_identity/src/tests/fixtures/naive_credential_holder.rs b/cawg_identity/src/tests/fixtures/naive_credential_holder.rs index f12f50411..8019f3cce 100644 --- a/cawg_identity/src/tests/fixtures/naive_credential_holder.rs +++ b/cawg_identity/src/tests/fixtures/naive_credential_holder.rs @@ -26,7 +26,7 @@ use async_trait::async_trait; use crate::{ builder::{CredentialHolder, IdentityBuilderError}, - SignerPayload, + SignatureVerifier, SignerPayload, ValidationError, }; #[derive(Debug)] @@ -51,3 +51,28 @@ impl CredentialHolder for NaiveCredentialHolder { Ok(result) } } + +pub(crate) struct NaiveSignatureVerifier {} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl SignatureVerifier for NaiveSignatureVerifier { + type Error = (); + type Output = (); + + async fn check_signature( + &self, + signer_payload: &SignerPayload, + signature: &[u8], + ) -> Result> { + let mut signer_payload_cbor: Vec = vec![]; + ciborium::into_writer(signer_payload, &mut signer_payload_cbor) + .map_err(|_| ValidationError::InternalError("CBOR serialization error".to_string()))?; + + if signer_payload_cbor != signature { + Err(ValidationError::InvalidSignature) + } else { + Ok(()) + } + } +} From 183a0e596bf7f9b8d8141587eb75c1bbffd75b2a Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Sun, 12 Jan 2025 19:24:21 -0800 Subject: [PATCH 2/3] Clippy --- cawg_identity/src/identity_assertion/signer_payload.rs | 1 + cawg_identity/src/tests/builder/simple_case.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cawg_identity/src/identity_assertion/signer_payload.rs b/cawg_identity/src/identity_assertion/signer_payload.rs index f94340fa2..585536347 100644 --- a/cawg_identity/src/identity_assertion/signer_payload.rs +++ b/cawg_identity/src/identity_assertion/signer_payload.rs @@ -119,4 +119,5 @@ impl SignerPayload { } } +#[allow(clippy::unwrap_used)] static ABSOLUTE_URL_PREFIX: LazyLock = LazyLock::new(|| Regex::new("/c2pa/[^/]+/").unwrap()); diff --git a/cawg_identity/src/tests/builder/simple_case.rs b/cawg_identity/src/tests/builder/simple_case.rs index 325883d0c..e017aa5a1 100644 --- a/cawg_identity/src/tests/builder/simple_case.rs +++ b/cawg_identity/src/tests/builder/simple_case.rs @@ -59,7 +59,7 @@ async fn simple_case() { assert_eq!(manifest_store.validation_status(), None); let manifest = manifest_store.active_manifest().unwrap(); - let mut ia_iter = IdentityAssertion::from_manifest(&manifest); + let mut ia_iter = IdentityAssertion::from_manifest(manifest); // Should find exactly one identity assertion. let ia = ia_iter.next().unwrap().unwrap(); @@ -69,7 +69,7 @@ async fn simple_case() { // And that identity assertion should be valid for this manifest. let nsv = NaiveSignatureVerifier {}; - ia.validate(&manifest, &nsv).await.unwrap(); + ia.validate(manifest, &nsv).await.unwrap(); } fn manifest_json() -> String { From ab74ecb86606fdf27318cc6ee8cb29e471e1d34c Mon Sep 17 00:00:00 2001 From: Eric Scouten Date: Sun, 12 Jan 2025 19:28:30 -0800 Subject: [PATCH 3/3] Bump MSRV to 1.82.0 --- .github/workflows/ci.yml | 4 ++-- README.md | 2 +- cawg_identity/Cargo.toml | 2 +- cawg_identity/README.md | 2 +- docs/usage.md | 2 +- export_schema/Cargo.toml | 2 +- internal/crypto/Cargo.toml | 2 +- internal/status-tracker/Cargo.toml | 2 +- make_test_images/Cargo.toml | 2 +- sdk/Cargo.toml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15933064c..01d827e61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] - rust_version: [stable, 1.81.0] + rust_version: [stable, 1.82.0] steps: - name: Checkout repository @@ -225,7 +225,7 @@ jobs: fail-fast: false matrix: target: [aarch64-unknown-linux-gnu] - rust_version: [stable, 1.81.0] + rust_version: [stable, 1.82.0] steps: - name: Checkout repository diff --git a/README.md b/README.md index d39218f24..89a4e2f55 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ NOTE: The current release includes a new API that replaces old methods of readin ### Rust language requirement (MSRV) -The `c2pa` crate requires Rust version 1.81.0 or newer. When a newer version of Rust becomes required, a new minor (0.x.0) version of this crate will be released. +The `c2pa` crate requires Rust version 1.82.0 or newer. When a newer version of Rust becomes required, a new minor (0.x.0) version of this crate will be released. ### Contributions and feedback diff --git a/cawg_identity/Cargo.toml b/cawg_identity/Cargo.toml index c426bbebd..762628730 100644 --- a/cawg_identity/Cargo.toml +++ b/cawg_identity/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" keywords = ["identity"] categories = ["api-bindings"] edition = "2021" -rust-version = "1.81.0" +rust-version = "1.82.0" exclude = ["tests/fixtures"] [lints.rust] diff --git a/cawg_identity/README.md b/cawg_identity/README.md index 055ebddbb..52303b990 100644 --- a/cawg_identity/README.md +++ b/cawg_identity/README.md @@ -14,7 +14,7 @@ This is very early days for this crate. Many things are subject to change at thi ## Requirements -The toolkit requires **Rust version 1.81.0** or newer. When a newer version of Rust becomes required, a new minor (0.x.0) version of this crate will be released. +The toolkit requires **Rust version 1.82.0** or newer. When a newer version of Rust becomes required, a new minor (0.x.0) version of this crate will be released. ### Supported platforms diff --git a/docs/usage.md b/docs/usage.md index be2a0a177..525292fb1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -11,7 +11,7 @@ The C2PA Rust library has been tested on the following operating systems: ## Requirements -The C2PA Rust library requires **Rust version 1.81.0** or newer. +The C2PA Rust library requires **Rust version 1.82.0** or newer. To use the library, add this to your `Cargo.toml`: diff --git a/export_schema/Cargo.toml b/export_schema/Cargo.toml index 2246a394c..cd28e4ed0 100644 --- a/export_schema/Cargo.toml +++ b/export_schema/Cargo.toml @@ -4,7 +4,7 @@ version = "0.36.1" authors = ["Dave Kozma "] license = "MIT OR Apache-2.0" edition = "2018" -rust-version = "1.81.0" +rust-version = "1.82.0" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] } diff --git a/internal/crypto/Cargo.toml b/internal/crypto/Cargo.toml index bc86ca640..3d7540a26 100644 --- a/internal/crypto/Cargo.toml +++ b/internal/crypto/Cargo.toml @@ -18,7 +18,7 @@ readme = "README.md" keywords = ["metadata"] categories = ["api-bindings"] edition = "2021" -rust-version = "1.81.0" +rust-version = "1.82.0" exclude = ["tests/fixtures"] [lints.rust] diff --git a/internal/status-tracker/Cargo.toml b/internal/status-tracker/Cargo.toml index ee1c2e0d1..bc52987bb 100644 --- a/internal/status-tracker/Cargo.toml +++ b/internal/status-tracker/Cargo.toml @@ -13,7 +13,7 @@ homepage = "https://contentauthenticity.org" repository = "https://github.com/contentauth/c2pa-rs" readme = "README.md" edition = "2021" -rust-version = "1.81.0" +rust-version = "1.82.0" exclude = ["tests/fixtures"] [lints.rust] diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml index 68acd974b..ca4b88640 100644 --- a/make_test_images/Cargo.toml +++ b/make_test_images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.36.1" authors = ["Gavin Peacock "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.81.0" +rust-version = "1.82.0" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] } diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 245ae62a5..d218e9fe7 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -18,7 +18,7 @@ readme = "../README.md" keywords = ["xmp", "metadata"] categories = ["api-bindings"] edition = "2021" -rust-version = "1.81.0" +rust-version = "1.82.0" exclude = ["tests/fixtures"] [package.metadata.docs.rs]