Skip to content

Commit

Permalink
feat: c2patool to read cawg data (#904)
Browse files Browse the repository at this point in the history
* ci: Add debug statements

* Omit additional paddings

* feat: Omit more padding tags

* ci: add explanation

* ci: Catch result strings before they get returned

* ci: sdk catch result string before returning them

* ci: look into parsing stuff

* ci: WIP for cawg display

* ci: Replace signature with hash

* ci: Replace signature with hash

* ci: Clean up leftover debug code

* ci: Parse cawg data in cli, introduce tokio

* ci: WIP

* ci: WIP

* ci: Up some deps

* ci: WIP

* ci: WIP

* feat: Add methods to reader to decorate the json

* ci: Got the cleaned content signature

* ci: Some clean up

* ci: Some clean up

* ci: WIP

* feat: cawg parsing hack

* feat: move display

* feat: Refactor

* feat: Refactor

* feat: Refactor

* ci: Tmp check all other tests

* ci: Tmp check all other tests

* Debug

* fix: fix mistake of returning too early

* fix: Remove debug file

* fix: Remove debug file

* fix: Change error handling
  • Loading branch information
tmathern authored Feb 6, 2025
1 parent 4cb20e3 commit b1990a3
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 14 deletions.
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.1", 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
155 changes: 150 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,144 @@ fn verify_fragmented(init_pattern: &Path, frag_pattern: &Path) -> Result<Vec<Rea
Ok(readers)
}

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 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 manifests with CAWG details
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: &c2pa::Manifest = reader.get_manifest(key).unwrap();

// Replace the signature content of the assertion with parsed details by CAWG
let assertions = value.get_mut("assertions").unwrap();
let assertions_array = assertions.as_array_mut().unwrap();
for assertion in assertions_array {
let label = assertion.get("label").unwrap().to_string();

// for CAWG assertions, further parse the signature
if label.contains("cawg.identity") {
let parsed_cawg_json_string =
match get_cawg_details_for_manifest(current_manifest, tokio_runtime) {
Some(parsed_cawg_json_string) => parsed_cawg_json_string,
None => {
println!("Could not parse CAWG details for manifest");
continue;
}
};

let assertion_data: &mut serde_json::Value = assertion.get_mut("data").unwrap();
assertion_data["signature"] =
serde_json::from_str(&parsed_cawg_json_string).unwrap();
}
}
}
} else {
println!("Could not parse manifest store JSON content");
}

match serde_json::to_string_pretty(&reader_content) {
Ok(decorated_result) => decorated_result,
Err(err) => {
println!(
"Could not parse manifest store JSON content with additional CAWG details: {:?}",
err
);
String::new()
}
}
}

/// 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 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 identity assertion: {:?}", err);
return;
}
};

parsed_cawg_json = serde_json::to_string(&ica).unwrap();
});

// Get the JSON as mutable, so we can further parse and format
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 CAWG details for manifest: {:?}", err);
return None;
}
};

// Get the credentials subject information...
let credentials_subject = map.get_mut("credentialSubject");
let credentials_subject = match credentials_subject {
Some(credentials_subject) => credentials_subject,
None => {
println!("Could not find credentialSubject 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 +605,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 +818,9 @@ 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)?
)
println!("## TMN-Debug ~ cli#main ~ Here we read the detailed edition");
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 +832,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.
2 changes: 1 addition & 1 deletion sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ serde_bytes = "0.11.5"
serde_cbor = "0.11.1"
serde_derive = "1.0.197"
serde_json = { version = "1.0.117", features = ["preserve_order"] }
serde_with = "3.11.0"
serde_with = { version = "3.12.0" }
serde-transcode = "1.1.1"
sha1 = "0.10.6"
sha2 = "0.10.6"
Expand Down
3 changes: 3 additions & 0 deletions sdk/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ pub enum Error {
#[error("The Verifiable Content structure is not valid")]
VerifiableCredentialInvalid,

#[error("error while serializing to JSON: {0}")]
JsonSerializationError(String),

/// Could not parse ECDSA signature. (Only appears when using WASM web crypto.)
#[error("could not parse ECDSA signature")]
InvalidEcdsaSignature,
Expand Down
6 changes: 3 additions & 3 deletions sdk/src/hashed_uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ mod tests {
fn impl_clone() {
let h = HashedUri::new(
"self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(),
Some("sha256".to_owned()),
Some("sha256".to_owned()),
&hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"),
);

Expand All @@ -124,7 +124,7 @@ mod tests {
fn impl_debug() {
let h = HashedUri::new(
"self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(),
Some("sha256".to_owned()),
Some("sha256".to_owned()),
&hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"),
);

Expand All @@ -135,7 +135,7 @@ mod tests {
fn impl_display() {
let h = HashedUri::new(
"self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(),
Some("sha256".to_owned()),
Some("sha256".to_owned()),
&hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"),
);

Expand Down
6 changes: 6 additions & 0 deletions sdk/src/manifest_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,13 @@ impl std::fmt::Display for ManifestStore {
}

json = b64_tag(json, "hash");

// list of tags to omit (padding tags)
// Reason of padding, see note at:
// https://c2pa.org/specifications/specifications/2.1/specs/C2PA_Specification.html#_going_back_and_filling_in
json = omit_tag(json, "pad");
json = omit_tag(json, "pad1");
json = omit_tag(json, "pad2");

f.write_str(&json)
}
Expand Down
14 changes: 13 additions & 1 deletion sdk/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use c2pa_status_tracker::DetailedStatusTracker;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

#[cfg(feature = "file_io")]
use crate::error::Error;
Expand Down Expand Up @@ -241,11 +242,22 @@ impl Reader {
})
}

/// Get the manifest store as a JSON string
/// Get the manifest store as a JSON string.
pub fn json(&self) -> String {
self.manifest_store.to_string()
}

/// Get the manifest store as a serde serialized JSON value map.
pub fn json_value_map(&self) -> Result<Map<String, Value>> {
let reader_as_json = self.json();
let reader_as_json_str = reader_as_json.as_str();
let mapped_json = serde_json::from_str(reader_as_json_str);
match mapped_json {
Ok(mapped_json) => Ok(mapped_json),
Err(err) => Err(crate::Error::JsonSerializationError(err.to_string())),
}
}

/// Get the [`ValidationStatus`] array of the manifest store if it exists.
/// Call this method to check for validation errors.
///
Expand Down

0 comments on commit b1990a3

Please sign in to comment.