Skip to content
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

feat(cawg_identity): Implement identity assertion validation #843

Merged
merged 3 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
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.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion cawg_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion cawg_identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
59 changes: 57 additions & 2 deletions cawg_identity/src/identity_assertion/assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,6 +46,59 @@
pub(crate) pad2: Option<ByteBuf>,
}

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<Item = Result<Self, c2pa::Error>> + 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<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
verifier: &SV,
) -> Result<SV::Output, ValidationError<SV::Error>> {
self.check_padding()?;

self.signer_payload.check_against_manifest(manifest)?;

verifier
.check_signature(&self.signer_payload, &self.signature)
.await
}

fn check_padding<E>(&self) -> Result<(), ValidationError<E>> {
if !self.pad1.iter().all(|b| *b == 0) {
return Err(ValidationError::InvalidPadding);

Check warning on line 89 in cawg_identity/src/identity_assertion/assertion.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/assertion.rs#L89

Added line #L89 was not covered by tests
}

if let Some(pad2) = self.pad2.as_ref() {
if !pad2.iter().all(|b| *b == 0) {
return Err(ValidationError::InvalidPadding);

Check warning on line 94 in cawg_identity/src/identity_assertion/assertion.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/assertion.rs#L94

Added line #L94 was not covered by tests
}
}

Check warning on line 96 in cawg_identity/src/identity_assertion/assertion.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/assertion.rs#L96

Added line #L96 was not covered by tests

Ok(())
}
}

impl Debug for IdentityAssertion {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("IdentityAssertion")
Expand Down
89 changes: 87 additions & 2 deletions cawg_identity/src/identity_assertion/signer_payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,3 +39,85 @@
// TO DO: Add role and expected_* fields.
// (https://github.com/contentauth/c2pa-rs/issues/816)
}

impl SignerPayload {
pub(super) fn check_against_manifest<E>(
&self,
manifest: &Manifest,
) -> Result<(), ValidationError<E>> {
// 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;

Check warning on line 56 in cawg_identity/src/identity_assertion/signer_payload.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/signer_payload.rs#L56

Added line #L56 was not covered by tests
}
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(),
));

Check warning on line 64 in cawg_identity/src/identity_assertion/signer_payload.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/signer_payload.rs#L62-L64

Added lines #L62 - L64 were not covered by tests
}

// 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(),
));

Check warning on line 86 in cawg_identity/src/identity_assertion/signer_payload.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/signer_payload.rs#L84-L86

Added lines #L84 - L86 were not covered by tests
}
}

// Ensure that a hard binding assertion is present.
let ref_assertion_labels: Vec<String> = 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

Check warning on line 101 in cawg_identity/src/identity_assertion/signer_payload.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/signer_payload.rs#L101

Added line #L101 was not covered by tests
}
}) {
return Err(ValidationError::NoHardBindingAssertion);

Check warning on line 104 in cawg_identity/src/identity_assertion/signer_payload.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/signer_payload.rs#L104

Added line #L104 was not covered by tests
}

// Make sure no assertion references are duplicated.
let mut labels = HashSet::<String>::new();

for label in &ref_assertion_labels {
let label = label.clone();
if labels.contains(&label) {
return Err(ValidationError::DuplicateAssertionReference(label));

Check warning on line 113 in cawg_identity/src/identity_assertion/signer_payload.rs

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/signer_payload.rs#L113

Added line #L113 was not covered by tests
}
labels.insert(label);
}

Ok(())
}
}

#[allow(clippy::unwrap_used)]
static ABSOLUTE_URL_PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new("/c2pa/[^/]+/").unwrap());
2 changes: 1 addition & 1 deletion cawg_identity/src/identity_assertion/validation_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub enum ValidationError<SignatureError> {
/// 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")]
Expand Down
21 changes: 15 additions & 6 deletions cawg_identity/src/tests/builder/simple_case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion cawg_identity/src/tests/fixtures/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
27 changes: 26 additions & 1 deletion cawg_identity/src/tests/fixtures/naive_credential_holder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use async_trait::async_trait;

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

#[derive(Debug)]
Expand All @@ -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<Self::Output, ValidationError<Self::Error>> {
let mut signer_payload_cbor: Vec<u8> = 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(())
}
}
}
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
2 changes: 1 addition & 1 deletion export_schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.36.1"
authors = ["Dave Kozma <[email protected]>"]
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)'] }
Expand Down
2 changes: 1 addition & 1 deletion internal/crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion internal/status-tracker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion make_test_images/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.36.1"
authors = ["Gavin Peacock <[email protected]>"]
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)'] }
Expand Down
2 changes: 1 addition & 1 deletion sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading