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: Update c2patool to read CAWG data #907

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b1990a3
feat: c2patool to read cawg data (#904)
tmathern Feb 6, 2025
3521730
ci: Refactor
tmathern Feb 6, 2025
9af655d
ci: Refactor
tmathern Feb 6, 2025
7ed719c
ci: Refactor
tmathern Feb 6, 2025
a18e1f9
ci: Refactor and rerun tests
tmathern Feb 6, 2025
cb389d5
ci: Refactor
tmathern Feb 6, 2025
e1b268e
ci: Solidify error handling
tmathern Feb 6, 2025
cc6e7bd
ci: Add test
tmathern Feb 6, 2025
aba1ef6
ci: Merge branch 'main' into mathern/cawg-reading-trois
tmathern Feb 6, 2025
8a2ead2
ci: Prepare for detailed view
tmathern Feb 6, 2025
dd70c83
ci: Retrigger
tmathern Feb 6, 2025
a77004e
ci: Remove debug files
tmathern Feb 6, 2025
13875f4
ci: Review comment 1
tmathern Feb 7, 2025
3b45088
ci: Review comment 2
tmathern Feb 7, 2025
49adfd2
ci: Retrigger checks
tmathern Feb 7, 2025
151e312
ci: Retrigger checks
tmathern Feb 7, 2025
928b07b
ci: Merge branch 'main' into mathern/cawg-reading-trois
tmathern Feb 7, 2025
153be98
ci: Cargo lock too
tmathern Feb 7, 2025
c32ec31
Errors go to stdout
tmathern Feb 7, 2025
aba8668
feat: c2patool to read cawg data with `--detailed` flag too (#909)
tmathern Feb 7, 2025
85201a7
feat: Move cawg parsing for c2patool into own file
tmathern Feb 7, 2025
077a285
fix: Overhaul of error handling
tmathern Feb 7, 2025
b9c5c7a
fix: Update error handling for non detailed view too
tmathern Feb 7, 2025
5cffc27
fix: Format
tmathern Feb 7, 2025
ea92320
ci: Clarify comment
tmathern Feb 7, 2025
b8ad835
fix: Refactor
tmathern Feb 7, 2025
346f040
ci: Format
tmathern Feb 7, 2025
dc48058
fix: Add test
tmathern Feb 7, 2025
2a7d66d
fix: Add test
tmathern Feb 7, 2025
7d2418a
ci: Some more refactor
tmathern Feb 7, 2025
1c262b2
fix: Clarify comments
tmathern Feb 7, 2025
772570a
ci: Refactor
tmathern Feb 7, 2025
18f1a0d
ci: Refactor
tmathern Feb 7, 2025
7b74080
fix: No need to process stuff we don't use
tmathern Feb 7, 2025
9a12f0f
fix: Remove debug code
tmathern Feb 7, 2025
2b7e13d
fix: Remove some more stuff
tmathern Feb 7, 2025
fdd1906
ci: Undo new error
tmathern Feb 7, 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
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ test-wasm:
test-wasm-web:
cd sdk && wasm-pack test --chrome --headless -- --features="serialize_thumbnails"

test-no-wasm: check-format check-docs clippy test-local

# Full local validation, build and test all features including wasm
# Run this before pushing a PR to pre-validate
test: check-format check-docs clippy test-local test-wasm-web
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl SignatureVerifier for IcaSignatureVerifier {
));
};

dbg!(&jwk_prop);
// dbg!(&jwk_prop);

// OMG SO HACKY!
let Ok(jwk_json) = serde_json::to_string_pretty(jwk_prop) else {
Expand Down
2 changes: 1 addition & 1 deletion cawg_identity/src/claim_aggregation/w3c_vc/did_web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub(crate) async fn resolve(did: &Did<'_>) -> Result<DidDocument, DidWebError> {

let method_specific_id = did.method_specific_id();

dbg!(method_specific_id);
// dbg!(method_specific_id);

let url = to_url(method_specific_id)?;
// TODO: https://w3c-ccg.github.io/did-method-web/#in-transit-security
Expand Down
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
2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)'] }
[dependencies]
anyhow = "1.0"
atree = "0.5.2"
cawg-identity = { path = "../cawg_identity"}
c2pa = { path = "../sdk", version = "0.45.2", features = [
"fetch_remote_manifests",
"file_io",
Expand All @@ -37,6 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0"
serde_json = "1.0"
tempfile = "3.3"
tokio = { version = "1.42", features = ["full"] }
treeline = "0.1.0"
pem = "3.0.3"
openssl = { version = "0.10.61", features = ["vendored"] }
Expand Down
255 changes: 250 additions & 5 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ use std::{

use anyhow::{anyhow, bail, Context, Result};
use c2pa::{Builder, ClaimGeneratorInfo, Error, Ingredient, ManifestDefinition, Reader, Signer};
use cawg_identity::{claim_aggregation::IcaSignatureVerifier, IdentityAssertion};
use clap::{Parser, Subcommand};
use log::debug;
use serde::Deserialize;
use serde_json::{Map, Value};
use signer::SignConfig;
use tokio::runtime::Runtime;
use url::Url;

use crate::{
Expand Down Expand Up @@ -424,6 +427,245 @@ fn verify_fragmented(init_pattern: &Path, frag_pattern: &Path) -> Result<Vec<Rea
Ok(readers)
}

/// Update/decorate the displayed JSON string for a more human-readable JSON output.
fn decorate_json_display(reader: Reader, tokio_runtime: &Runtime) -> String {
let mut reader_content = match reader.json_value_map() {
Ok(mapped_json) => mapped_json,
Err(_) => {
println!("Could not parse manifest store JSON content");
return String::new();
}
};

let manifests_json_content = match reader_content.get_mut("manifests") {
Some(json) => json,
None => {
println!("No JSON to parse in manifest store (key: manifests)");
return String::new();
}
};

// Update assertion with more details, eg. for CAWG
match decorate_json_assertions(reader, manifests_json_content, tokio_runtime) {
Ok(_) => (),
Err(err) => {
println!("Could not decorate JSON assertions for display: {:?}", err);
}
};
match serde_json::to_string_pretty(&reader_content) {
Ok(decorated_result) => decorated_result,
Err(err) => {
println!(
"Could not decorate displayed JSON with additional details: {:?}",
err
);
String::new()
}
}
}

/// Update/decorate the displayed JSON assertions for a more human-readable JSON output.
fn decorate_json_assertions(
reader: Reader,
json_content: &mut Value,
tokio_runtime: &Runtime,
) -> Result<(), Error> {
if let Value::Object(map) = json_content {
// Iterate over the key-value pairs
for (key, value) in &mut *map {
// Get additional CAWG details
let current_manifest = reader.get_manifest(key);
let current_manifest = match current_manifest {
Some(current_manifest) => current_manifest,
None => {
return Err(crate::Error::JsonSerializationError(
"Could not get current manifest".to_string(),
));
}
};

// Get the assertions as array from the JSON
let assertions = match value.get_mut("assertions") {
Some(assertions) => assertions,
None => {
return Err(crate::Error::JsonSerializationError(
"Could not parse JSON assertions as object".to_string(),
));
}
};
let assertions_array = match assertions.as_array_mut() {
Some(assertions_array) => assertions_array,
None => {
return Err(crate::Error::JsonSerializationError(
"Could not parse JSON assertions as array".to_string(),
));
}
};

// Loop over the assertions to process those of interest
for assertion in assertions_array {
let label = match assertion.get("label") {
Some(label) => label.to_string(),
None => {
return Err(crate::Error::JsonSerializationError(
"Could not parse assertion label".to_string(),
));
}
};

// for CAWG assertions, further parse the signature
if label.contains("cawg.identity") {
decorate_json_cawg_assertions(current_manifest, assertion, tokio_runtime)?;
}
}
}
}

Ok(())
}

/// Update/decorate the displayed CAWG assertion for a more human-readable JSON output.
fn decorate_json_cawg_assertions(
holding_manifest: &c2pa::Manifest,
assertion: &mut Value,
tokio_runtime: &Runtime,
) -> Result<(), Error> {
let parsed_cawg_json_string =
match get_cawg_details_for_manifest(holding_manifest, tokio_runtime) {
Some(parsed_cawg_json_string) => parsed_cawg_json_string,
None => {
println!(
"Could not parse CAWG details for manifest (leaving original raw data unformatted)"
);
return Ok(());
}
};

// Let's look at the assertion data
let assertion_data = match assertion.get_mut("data") {
Some(assertion_data) => assertion_data,
None => {
return Err(crate::Error::JsonSerializationError(
"Could not parse CAWG assertion data".to_string(),
));
}
};

// Update signature with parsed content
let parsed_signature = match serde_json::from_str(&parsed_cawg_json_string) {
Ok(parsed_signature) => parsed_signature,
Err(err) => {
return Err(crate::Error::JsonSerializationError(err.to_string()));
}
};
assertion_data["signature"] = parsed_signature;

// We don't need to show the padding fields either
let assertion_data_map = match assertion_data.as_object_mut() {
Some(assertion_data_map) => assertion_data_map,
None => {
return Err(crate::Error::JsonSerializationError(
"Could not parse CAWG assertion data as object".to_string(),
));
}
};
assertion_data_map.remove("pad1");
assertion_data_map.remove("pad2");

Ok(())
}

/// Parse additional CAWG details from the manifest store to update displayed results.
/// As CAWG mostly async, this will block on network requests for checks using a tokio runtime.
fn get_cawg_details_for_manifest(
manifest: &c2pa::Manifest,
tokio_runtime: &Runtime,
) -> Option<String> {
let ia_iter = IdentityAssertion::from_manifest(manifest);

// TODO: Determine what should happen when multiple identities are reported (currently only 1 is supported)
let mut parsed_cawg_json = String::new();

ia_iter.for_each(|ia| {
let identity_assertion = match ia {
Ok(ia) => ia,
Err(err) => {
println!("Could not parse CAWG identity assertion: {:?}", err);
return;
}
};

let isv = IcaSignatureVerifier {};
let ica_validated = tokio_runtime.block_on(identity_assertion.validate(manifest, &isv));
let ica = match ica_validated {
Ok(ica) => ica,
Err(err) => {
println!("Could not validate CAWG identity assertion: {:?}", err);
return;
}
};

parsed_cawg_json = match serde_json::to_string(&ica) {
Ok(parsed_cawg_json) => parsed_cawg_json,
Err(err) => {
println!(
"Could not parse CAWG identity claims aggregation details for manifest: {:?}",
err
);
return;
}
};
});

if parsed_cawg_json.is_empty() {
return None;
}

// Get the JSON as mutable, so we can further parse and format CAWG data
let maybe_map = serde_json::from_str(parsed_cawg_json.as_str());
let mut map: Map<String, Value> = match maybe_map {
Ok(map) => map,
Err(err) => {
println!(
"Could not parse convert CAWG identity claims details to JSON string map: {:?}",
err
);
return None;
}
};

// Get the credentials subject information...
let credentials_subject_maybe = map.get_mut("credentialSubject");
let credentials_subject = match credentials_subject_maybe {
Some(credentials_subject) => credentials_subject,
None => {
println!("Could not find credential subject in CAWG details for manifest");
return None;
}
};
let credentials_subject_as_obj = credentials_subject.as_object_mut();
let credential_subject_details = match credentials_subject_as_obj {
Some(credentials_subject) => credentials_subject,
None => {
println!("Could not parse credential subject as object in CAWG details for manifest");
return None;
}
};
// As per design CAWG has some repetition between assertion an signature (c2paAsset field)
// so we remove the c2paAsset field from the credential subject details too
credential_subject_details.remove("c2paAsset");

// return the for-display json-formatted string
let serialized_content = serde_json::to_string(&map);
match serialized_content {
Ok(serialized_content) => Some(serialized_content),
Err(err) => {
println!("Could not parse CAWG details for manifest: {:?}", err);
None
}
}
}

fn main() -> Result<()> {
let args = CliArgs::parse();

Expand Down Expand Up @@ -464,6 +706,9 @@ fn main() -> Result<()> {
// configure the SDK
configure_sdk(&args).context("Could not configure c2pa-rs")?;

// configure tokio runtime for blocking operations
let tokio_runtime: Runtime = Runtime::new()?;

// Remove manifest needs to also remove XMP provenance
// if args.remove_manifest {
// match args.output {
Expand Down Expand Up @@ -674,10 +919,8 @@ fn main() -> Result<()> {
Ingredient::from_file(&args.path).map_err(special_errs)?
)
} else if args.detailed {
println!(
"{:#?}",
Reader::from_file(&args.path).map_err(special_errs)?
)
let reader = Reader::from_file(&args.path).map_err(special_errs)?;
println!("{:#?}", reader);
} else if let Some(Commands::Fragment {
fragments_glob: Some(fg),
}) = &args.command
Expand All @@ -689,7 +932,9 @@ fn main() -> Result<()> {
println!("{} Init manifests validated", stores.len());
}
} else {
println!("{}", Reader::from_file(&args.path).map_err(special_errs)?)
let reader: Reader = Reader::from_file(&args.path).map_err(special_errs)?;
let stringified_decorated_json = decorate_json_display(reader, &tokio_runtime);
println!("{}", stringified_decorated_json);
}

Ok(())
Expand Down
Binary file added cli/tests/fixtures/C_with_CAWG_data.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading