Skip to content
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
2 changes: 1 addition & 1 deletion Cargo.lock

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

63 changes: 62 additions & 1 deletion cawg_identity/src/claim_aggregation/ica_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,32 @@
// specific language governing permissions and limitations under
// each license.

use std::collections::BTreeMap;

use chrono::{DateTime, FixedOffset};
use iref::{Iri, UriBuf};
use iref::{Iri, IriBuf, UriBuf};
use non_empty_string::NonEmptyString;
use nonempty_collections::NEVec;
use serde::{Deserialize, Serialize};

use crate::{
claim_aggregation::w3c_vc::credential::{CredentialV2, VerifiableCredentialSubtype},
identity_assertion::signature_verifier::ToCredentialSummary,
SignerPayload,
};

/// TO DO: Doc -- looks like CredentialV2 for our specific use
/// case.
pub type IcaCredential = CredentialV2<IdentityClaimsAggregationVc>;

impl ToCredentialSummary for IcaCredential {
type CredentialSummary = IcaCredentialSummary;

fn to_summary(&self) -> Self::CredentialSummary {
IcaCredentialSummary::from_credential(self)
}
}

/// Identity claims aggregation context IRI.
pub const IDENTITY_CLAIMS_AGGREGATION_CONTEXT_IRI: &Iri =
static_iref::iri!("https://creator-assertions.github.io/tbd/tbd");
Expand Down Expand Up @@ -175,3 +186,53 @@ pub struct IdentityProvider {
/// is the user-visible name of the _identity provider._
pub name: NonEmptyString,
}

#[doc(hidden)]
#[derive(Serialize)]
pub struct IcaCredentialSummary {
#[serde(rename = "@context")]
contexts: NEVec<IriBuf>,

#[serde(
default,
deserialize_with = "not_null",
skip_serializing_if = "Option::is_none"
)]
id: Option<UriBuf>,

#[serde(rename = "type")]
types: NEVec<String>,

issuer: UriBuf,

#[serde(rename = "validFrom")]
#[serde(default, skip_serializing_if = "Option::is_none")]
valid_from: Option<DateTime<FixedOffset>>,

#[serde(rename = "validUntil")]
#[serde(default, skip_serializing_if = "Option::is_none")]
valid_until: Option<DateTime<FixedOffset>>,

#[serde(rename = "verifiedIdentities")]
verified_identities: NEVec<VerifiedIdentity>,

#[serde(flatten)]
extra_properties: BTreeMap<String, serde_json::Value>,
}

impl IcaCredentialSummary {
fn from_credential(ica: &IcaCredential) -> Self {
let subject = ica.credential_subjects.first();

Self {
contexts: ica.contexts.clone(),
id: ica.id.clone(),
issuer: ica.issuer.clone(),
types: ica.types.clone(),
valid_from: ica.valid_from,
valid_until: ica.valid_until,
verified_identities: subject.verified_identities.clone(),
extra_properties: ica.extra_properties.clone(),
}
}
}
2 changes: 0 additions & 2 deletions cawg_identity/src/claim_aggregation/w3c_vc/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ pub(crate) mod one_or_many {
where
M: de::MapAccess<'de>,
{
eprintln!("Yo!");

let one = Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?;

Ok(nev!(one))
Expand Down
83 changes: 81 additions & 2 deletions cawg_identity/src/identity_assertion/assertion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@
use serde_bytes::ByteBuf;

use crate::{
identity_assertion::signer_payload::SignerPayload, internal::debug_byte_slice::DebugByteSlice,
SignatureVerifier, ValidationError,
identity_assertion::{
report::{IdentityAssertionReport, IdentityAssertionsForManifest, SignerPayloadReport},
signer_payload::SignerPayload,
},
internal::debug_byte_slice::DebugByteSlice,
SignatureVerifier, ToCredentialSummary, ValidationError,
};

/// This struct represents the raw content of the identity assertion.
Expand Down Expand Up @@ -63,6 +67,81 @@
.map(|a| a.to_assertion())
}

/// Create a summary report from this `IdentityAssertion`.
///
/// This will [`validate`] the assertion and then render the result as
/// an opaque [`Serialize`]-able struct that describes the decoded content
/// of the identity assertion.
///
/// [`validate`]: Self::validate
pub async fn to_summary<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
verifier: &SV,
) -> impl Serialize
where
<SV as SignatureVerifier>::Output: 'static,
{
self.to_summary_impl(manifest, verifier).await
}

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

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/assertion.rs#L77-L86

Added lines #L77 - L86 were not covered by tests

pub(crate) async fn to_summary_impl<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
verifier: &SV,
) -> IdentityAssertionReport<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
>
where
<SV as SignatureVerifier>::Output: 'static,
{
match self.validate(manifest, verifier).await {
Ok(named_actor) => {
let summary = named_actor.to_summary();

IdentityAssertionReport {
signer_payload: SignerPayloadReport::from_signer_payload(&self.signer_payload),
named_actor: Some(summary),
}
}

Err(_err) => {
todo!("Handle summary report for failure case");

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

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/assertion.rs#L108-L109

Added lines #L108 - L109 were not covered by tests
}
}
}

/// Summarize all of the identity assertions found for a [`Manifest`].
pub async fn summarize_all<SV: SignatureVerifier>(
manifest: &Manifest,
verifier: &SV,
) -> impl Serialize {
// NOTE: We can't write this using .map(...).collect() because there are async
// calls.
let mut reports: Vec<
IdentityAssertionReport<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
>,
> = vec![];

for assertion in Self::from_manifest(manifest) {
let report = match assertion {
Ok(assertion) => assertion.to_summary_impl(manifest, verifier).await,
Err(_) => {
todo!("Handle assertion failed to parse case");

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

View check run for this annotation

Codecov / codecov/patch

cawg_identity/src/identity_assertion/assertion.rs#L131

Added line #L131 was not covered by tests
}
};

reports.push(report);
}

IdentityAssertionsForManifest::<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
> {
assertion_reports: reports,
}
}

/// Using the provided [`SignatureVerifier`], check the validity of this
/// identity assertion.
///
Expand Down
1 change: 1 addition & 0 deletions cawg_identity/src/identity_assertion/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// each license.

pub(crate) mod assertion;
pub(crate) mod report;
pub(crate) mod signature_verifier;
pub(crate) mod signer_payload;
pub(crate) mod validation_error;
65 changes: 65 additions & 0 deletions cawg_identity/src/identity_assertion/report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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 serde::{ser::SerializeSeq, Serialize};

use crate::identity_assertion::signer_payload::SignerPayload;

#[doc(hidden)]
pub struct IdentityAssertionsForManifest<IAR: Serialize> {
pub(crate) assertion_reports: Vec<IdentityAssertionReport<IAR>>,
}

impl<IAR: Serialize> Serialize for IdentityAssertionsForManifest<IAR> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.assertion_reports.len()))?;
for report in self.assertion_reports.iter() {
seq.serialize_element(report)?;
}
seq.end()
}
}

#[doc(hidden)]
#[derive(Serialize)]
pub struct IdentityAssertionReport<T: Serialize> {
#[serde(flatten)]
pub(crate) signer_payload: SignerPayloadReport,

#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) named_actor: Option<T>,
}

#[derive(Serialize)]
pub(crate) struct SignerPayloadReport {
sig_type: String,
referenced_assertions: Vec<String>,
// TO DO: Add role and expected_* fields.
// (https://github.com/contentauth/c2pa-rs/issues/816)
}

impl SignerPayloadReport {
pub(crate) fn from_signer_payload(sp: &SignerPayload) -> Self {
Self {
referenced_assertions: sp
.referenced_assertions
.iter()
.map(|a| a.url().replace("self#jumbf=c2pa.assertions/", ""))
.collect(),
sig_type: sp.sig_type.clone(),
}
}
}
25 changes: 23 additions & 2 deletions cawg_identity/src/identity_assertion/signature_verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// each license.

use async_trait::async_trait;
use serde::Serialize;

use crate::{SignerPayload, ValidationError};

Expand All @@ -28,7 +29,7 @@ pub trait SignatureVerifier: Sync {
/// derived from the signature. Typically, this describes the named actor,
/// but may also contain information about the time of signing or the
/// credential's source.
type Output;
type Output: ToCredentialSummary + 'static;

/// The `Error` type provides a credential-specific explanation for why an
/// identity assertion signature could not be accepted. This value may be
Expand Down Expand Up @@ -61,7 +62,7 @@ pub trait SignatureVerifier {
/// derived from the signature. Typically, this describes the named actor,
/// but may also contain information about the time of signing or the
/// credential's source.
type Output;
type Output: ToCredentialSummary + 'static;

/// The `Error` type provides a credential-specific explanation for why an
/// identity assertion signature could not be accepted. This value may be
Expand All @@ -80,3 +81,23 @@ pub trait SignatureVerifier {
signature: &[u8],
) -> Result<Self::Output, ValidationError<Self::Error>>;
}

/// The `Output` result type from [`SignatureVerifier::check_signature`] may be
/// called upon to summarize its contents in a form suitable for JSON or similar
/// serialization.
///
/// This report is kept separate from any [`Serialize`] implementation
/// because that original credential type may have a native serialization that
/// is not suitable for summarizaton.
///
/// This trait allows the credential type to reshape its output into a suitable
/// summary form.
pub trait ToCredentialSummary {
/// A `CredentialSummary` is a serializable type that describes the
/// credential in JSON or similar format.
type CredentialSummary: Serialize;

/// Convert the underlying credential type into a summary type which can be
/// serialized for JSON or similar reporting format.
fn to_summary(&self) -> Self::CredentialSummary;
}
6 changes: 4 additions & 2 deletions cawg_identity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ pub mod claim_aggregation;

mod identity_assertion;
pub use identity_assertion::{
assertion::IdentityAssertion, signature_verifier::SignatureVerifier,
signer_payload::SignerPayload, validation_error::ValidationError,
assertion::IdentityAssertion,
signature_verifier::{SignatureVerifier, ToCredentialSummary},
signer_payload::SignerPayload,
validation_error::ValidationError,
};

pub(crate) mod internal;
Expand Down
14 changes: 11 additions & 3 deletions cawg_identity/src/tests/builder/simple_case.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
IdentityAssertionSigner,
},
tests::fixtures::{NaiveAsyncCredentialHolder, NaiveCredentialHolder, NaiveSignatureVerifier},
IdentityAssertion,
IdentityAssertion, ToCredentialSummary,
};

const TEST_IMAGE: &[u8] = include_bytes!("../../../../sdk/tests/fixtures/CA.jpg");
Expand Down Expand Up @@ -76,7 +76,11 @@ async fn simple_case() {

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

let nc_summary = naive_credential.to_summary();
let nc_json = serde_json::to_string(&nc_summary).unwrap();
assert_eq!(nc_json, "{}");
}

#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
Expand Down Expand Up @@ -123,7 +127,11 @@ async fn simple_case_async() {

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

let nc_summary = naive_credential.to_summary();
let nc_json = serde_json::to_string(&nc_summary).unwrap();
assert_eq!(nc_json, "{}");
}

fn manifest_json() -> String {
Expand Down
9 changes: 9 additions & 0 deletions cawg_identity/src/tests/claim_aggregation/interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,13 @@ async fn adobe_connected_identities() {
sig_type: "cawg.identity_claims_aggregation".to_owned(),
}
);

// Check the summary report for this manifest.
let ia_summary = IdentityAssertion::summarize_all(manifest, &isv).await;
let ia_json = serde_json::to_string(&ia_summary).unwrap();

assert_eq!(
ia_json,
r#"[{"sig_type":"cawg.identity_claims_aggregation","referenced_assertions":["c2pa.hash.data"],"named_actor":{"@context":["https://www.w3.org/ns/credentials/v2","https://creator-assertions.github.io/tbd/tbd"],"type":["VerifiableCredential","IdentityClaimsAggregationCredential"],"issuer":"did:web:connected-identities.identity-stage.adobe.com","validFrom":"2024-10-03T21:47:02Z","verifiedIdentities":[{"type":"cawg.social_media","username":"Robert Tiles","uri":"https://net.s2stagehance.com/roberttiles","verifiedAt":"2024-09-24T18:15:11Z","provider":{"id":"https://behance.net","name":"behance"}}],"credentialSchema":[{"id":"https://creator-assertions.github.io/schemas/v1/creator-identity-assertion.json","type":"JSONSchema"}]}}]"#
);
}
Loading