use async_trait::async_trait;
use serde_json::Value;
use crate::{
context::Context,
dynamic_assertion::{AsyncPostValidator, PartialClaim},
identity::IdentityAssertion,
status_tracker::StatusTracker,
ManifestAssertion,
};
pub struct CawgValidator<'a> {
context: &'a Context,
}
impl<'a> CawgValidator<'a> {
pub fn new(context: &'a Context) -> Self {
Self { context }
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl AsyncPostValidator for CawgValidator<'_> {
async fn validate(
&self,
label: &str,
assertion: &ManifestAssertion,
uri: &str,
partial_claim: &PartialClaim,
tracker: &mut StatusTracker,
) -> crate::Result<Option<Value>> {
if label == "cawg.identity" || label.starts_with("cawg.identity__") {
let identity_assertion: IdentityAssertion = assertion.to_assertion()?;
tracker.push_current_uri(uri.to_string());
let result = identity_assertion
.validate_partial_claim_async(partial_claim, tracker, self.context)
.await
.ok();
tracker.pop_current_uri();
return Ok(result);
};
Ok(None)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use std::io::{Cursor, Read};
use async_trait::async_trait;
use c2pa_macros::c2pa_test_async;
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use crate::{
http::{AsyncHttpResolver, HttpResolverError},
Context, Reader, ValidationState,
};
const CONNECTED_IDENTITIES_VALID: &[u8] =
include_bytes!("tests/fixtures/claim_aggregation/adobe_connected_identities.jpg");
const NO_HARD_BINDING: &[u8] =
include_bytes!("tests/fixtures/validation_method/no_hard_binding.jpg");
const MULTIPLE_IDENTITIES_VALID: &[u8] =
include_bytes!("tests/fixtures/claim_aggregation/ims_multiple_manifests.jpg");
const CONNECTED_IDENTITIES_DID: &str =
include_str!("tests/fixtures/claim_aggregation/connected_identities_did.json");
struct MockDidResolver;
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl AsyncHttpResolver for MockDidResolver {
async fn http_resolve_async(
&self,
_request: http::Request<Vec<u8>>,
) -> Result<http::Response<Box<dyn Read>>, HttpResolverError> {
Ok(http::Response::builder()
.status(200)
.header("content-type", "application/did+json")
.body(
Box::new(Cursor::new(CONNECTED_IDENTITIES_DID.as_bytes().to_vec()))
as Box<dyn Read>,
)
.unwrap())
}
}
#[cfg(not(target_arch = "wasm32"))]
fn mock_connected_identities_did() -> httpmock::MockServer {
use httpmock::prelude::*;
let server = MockServer::start();
server.mock(|when, then| {
when.method(GET).path("/.well-known/did.json");
then.status(200)
.header("content-type", "application/did+json")
.body(CONNECTED_IDENTITIES_DID);
});
crate::identity::claim_aggregation::w3c_vc::did_web::set_proxy(
"connected-identities.identity-stage.adobe.com",
&server.url("/"),
);
server
}
#[c2pa_test_async]
async fn test_connected_identities_valid() {
crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
crate::settings::set_settings_value(
"soft_binding.soft_binding_algorithms",
[
"com.adobe.trustmark.P".to_string(),
"com.adobe.icn.dense".to_string(),
],
)
.unwrap();
let mut settings = crate::settings::get_thread_local_settings();
let anchors = &mut settings.trust.anchors.unwrap();
let mut cawg_anchor: crate::settings::TrustAnchor = anchors[0].clone();
cawg_anchor.trust_kind = crate::settings::TrustListKind::CAWG;
cawg_anchor.trust_uri = Some("cawg_trust".to_string());
cawg_anchor.trusted_ica_issuers = Some(vec![
"did:jwk:eyJhbGciOiJFZERTQSIsImt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiTXA1LTBlODNuTmdRaGRoQlc4UnNoa2p5OTBzYTFBOUpJemtJdGNEcUN1SSJ9".to_string(),
"did:web:connected-identities.identity-stage.adobe.com".to_string(),
]);
anchors.push(cawg_anchor);
settings.trust.anchors = Some(anchors.clone());
let context = Context::default()
.with_settings(settings)
.unwrap()
.with_resolver_async(MockDidResolver);
let mut stream = Cursor::new(CONNECTED_IDENTITIES_VALID);
let reader = Reader::from_context(context)
.with_stream_async("image/jpeg", &mut stream)
.await
.unwrap();
println!("validation results: {}", reader);
assert!(reader
.validation_results()
.unwrap()
.active_manifest()
.unwrap()
.success()
.iter()
.any(|s| s.code() == "cawg.ica.credential_valid"));
}
#[c2pa_test_async]
async fn test_multiple_identities_valid() {
crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
crate::settings::set_settings_value(
"soft_binding.soft_binding_algorithms",
[
"com.adobe.trustmark.P".to_string(),
"com.adobe.icn.dense".to_string(),
],
)
.unwrap();
let settings = crate::settings::get_thread_local_settings();
let context = Context::new()
.with_settings(settings)
.unwrap()
.with_resolver_async(MockDidResolver);
let mut stream = Cursor::new(MULTIPLE_IDENTITIES_VALID);
let reader = Reader::from_context(context)
.with_stream_async("image/jpeg", &mut stream)
.await
.unwrap();
println!("validation results: {reader}");
assert_eq!(
reader
.validation_results()
.unwrap()
.ingredient_deltas()
.unwrap()
.len(),
1
);
assert_eq!(reader.validation_state(), ValidationState::Valid);
}
#[cfg(not(target_arch = "wasm32"))]
#[c2pa_test_async]
async fn ica_issuer_untrusted_does_not_affect_manifest_state() {
use crate::Settings;
let _did_server = mock_connected_identities_did();
let settings = Settings::new()
.with_value("verify.verify_trust", false)
.unwrap()
.with_value("core.decode_identity_assertions", false)
.unwrap()
.with_value("cawg_trust.trusted_ica_issuers", Vec::<String>::new())
.unwrap()
.with_value(
"soft_binding.soft_binding_algorithms",
[
"com.adobe.trustmark.P".to_string(),
"com.adobe.icn.dense".to_string(),
],
)
.unwrap();
let context = Context::new()
.with_settings(settings)
.unwrap()
.into_shared();
let validator = super::CawgValidator::new(&context);
let mut stream = Cursor::new(CONNECTED_IDENTITIES_VALID);
let mut reader = Reader::from_shared_context(&context)
.with_stream_async("image/jpeg", &mut stream)
.await
.unwrap();
reader.post_validate_async(&validator).await.unwrap();
let results = reader.validation_results().unwrap();
let active = results.active_manifest().unwrap();
assert!(active
.informational()
.iter()
.any(|s| s.code() == "cawg.ica.untrusted_issuer"));
assert!(!active
.failure()
.iter()
.any(|s| s.code() == "cawg.ica.untrusted_issuer"));
assert!(!active
.success()
.iter()
.any(|s| s.code() == "cawg.ica.credential_valid"));
assert_eq!(reader.validation_state(), ValidationState::Valid);
}
#[c2pa_test_async]
async fn test_cawg_validate_with_hard_binding_missing() {
let mut stream = Cursor::new(NO_HARD_BINDING);
let reader = Reader::default()
.with_stream_async("image/jpeg", &mut stream)
.await
.unwrap();
assert_eq!(
reader
.validation_results()
.unwrap()
.active_manifest()
.unwrap()
.failure()[0]
.code(),
"cawg.identity.hard_binding_missing"
);
}
}