Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8af63c7
feat: adds Ingredient_assertion::from_stream and claim::thumbnaiI()
gpeacock Aug 23, 2025
df816c0
feat: content_credential concept
gpeacock Aug 23, 2025
e965682
Merge branch 'main' into gpeacock/content_credential
gpeacock Sep 1, 2025
3d0a53c
chore: cleanup for unit tests
gpeacock Sep 3, 2025
eb2fce7
Merge branch 'main' into gpeacock/content_credential
gpeacock Sep 3, 2025
203b9fe
feat: remove validation_results and get it from the parent_ingredient.
gpeacock Sep 3, 2025
a4a6161
chore more validation_status removal
gpeacock Sep 3, 2025
6e6bccf
Merge branch 'main' into gpeacock/content_credential
gpeacock Sep 28, 2025
d0ece81
fix: Reader to_folder now writes c2pa_data.c2pa and c2patool doesn't …
gpeacock Sep 28, 2025
05f5d0c
feat: add cc from_stream add_assertion and validation_results to repo…
gpeacock Sep 28, 2025
ae1a4bd
feat: Adds claim.add_action and action, add_action_checked()
gpeacock Sep 29, 2025
e4577a9
Merge branch 'main' into gpeacock/content_credential
gpeacock Oct 17, 2025
c1238e5
feat: add create and add_action method
gpeacock Oct 20, 2025
afd0c25
Merge branch 'main' into gpeacock/content_credential
gpeacock Oct 31, 2025
951d0c9
fmt
gpeacock Oct 31, 2025
1027277
asset.rs early work
gpeacock Nov 1, 2025
18c0627
allow claim thumbnail for ingredient if valid or trusted.
gpeacock Nov 1, 2025
a9565b8
use shared b64 hash function
gpeacock Nov 4, 2025
0df42b2
fmt
gpeacock Nov 4, 2025
46435bc
Merge branch 'main' into gpeacock/content_credential
gpeacock Nov 13, 2025
fceb94c
wip with Context to contain settings and resolvers.
gpeacock Nov 14, 2025
5b06a02
use context in most of the sdk, instead of settings and resolvers.
gpeacock Nov 15, 2025
cc52157
Builder can be created with context and will use it internally
gpeacock Nov 15, 2025
ebf8a59
Add Signer support in Context
gpeacock Nov 15, 2025
fecfbe8
add content_credential open_stream
gpeacock Nov 15, 2025
d5ab217
add update_from_str to settings, so that you can overlay settings on …
gpeacock Nov 17, 2025
0b9e6b4
add try_from for &ClaimGeneratorInfoSettings
gpeacock Nov 17, 2025
d110624
context.with_settings() support IntoSettings trait
gpeacock Nov 17, 2025
aa93819
Add context to Reader
gpeacock Nov 18, 2025
441522d
remove cr:from_stream
gpeacock Nov 20, 2025
e3de303
formatting
gpeacock Nov 21, 2025
98d6ded
Merge branch 'main' into gpeacock/content_credential
gpeacock Nov 25, 2025
007e8f5
fmt
gpeacock Nov 25, 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
3 changes: 1 addition & 2 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,14 +870,13 @@ fn main() -> Result<()> {
let mut reader = Reader::from_file(&args.path).map_err(special_errs)?;
validate_cawg(&mut reader)?;
reader.to_folder(&output)?;
let report = reader.to_string();
let _report = reader.to_string();
if args.detailed {
// for a detailed report first call the above to generate the thumbnails
// then call this to add the detailed report
let detailed = format!("{reader:#?}");
File::create(output.join("detailed.json"))?.write_all(&detailed.into_bytes())?;
}
File::create(output.join("manifest_store.json"))?.write_all(&report.into_bytes())?;
println!("Manifest report written to the directory {:?}", &output);
}
} else if args.ingredient {
Expand Down
10 changes: 1 addition & 9 deletions docs/content_credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,6 @@ For Builder archives - Sign and save a manifest, either embedded or sidecar. If
- Add a captured .c2pa archived ingredient using add_ingredient_from_stream. This will use the parent ingredient in the archive as the ingredient added.




### Questions

- Should we allow any Reader to be converted to a Builder, or only those with the same claim_generator? Maybe there is some other flag.

- Can we save a .c2pa file that does not need to validate against an asset? This should probably be an option, but lets think about it.

### Test cases

1) Validate an ingredient without a manifest, store in Builder and save.
Expand All @@ -78,7 +70,7 @@ A future lower level API will wrap the Claim and Store structures providing a si

## Asset objects (not directly related to the above)

An Asset object creates a persistent layer over the asset_io traits. Currently we parse entire asset every time we need to access information about it. We have separate passes for XMP, JUMBF, Offset/box generation & etc..
An Asset object creates a semi-persistent layer over the asset_io traits. Currently we parse entire asset every time we need to access information about it. We have separate passes for XMP, JUMBF, Offset/box generation & etc..

- The details of file i/o, in memory, streamed, or remote web access are handled here.
- This will parse the asset, extract XMP, and C2PA data and allow
Expand Down
81 changes: 81 additions & 0 deletions sdk/src/assertion_input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// In assertion_input.rs
use crate::Result;
//use serde::{Serialize};
use serde_json::Value as JsonValue;
use serde_cbor::Value as CborValue;
use crate::assertion::AssertionBase;

pub trait AssertionInput {
fn to_assertion_with_label(self, label: &str) -> Result<Box<dyn AssertionBase>>;
}

// Implementation for JSON values
impl AssertionInput for JsonValue {
fn to_assertion_with_label(self, label: &str) -> Result<Box<dyn AssertionBase>> {
try_deserialize_known_json_assertion(label, &self)
}
}

// Implementation for CBOR values
impl AssertionInput for CborValue {
fn to_assertion_with_label(self, label: &str) -> Result<Box<dyn AssertionBase>> {
try_deserialize_known_cbor_assertion(label, &self)
}
}

// Implementation for strings
impl AssertionInput for &str {
fn to_assertion_with_label(self, label: &str) -> Result<Box<dyn AssertionBase>> {
let json_value: JsonValue = serde_json::from_str(self)?;
json_value.to_assertion_with_label(label)
}
}

impl AssertionInput for String {
fn to_assertion_with_label(self, label: &str) -> Result<Box<dyn AssertionBase>> {
self.as_str().to_assertion_with_label(label)
}
}


// Helper functions - return concrete Assertion
fn try_deserialize_known_json_assertion(label: &str, value: &JsonValue) -> Result<Box<dyn AssertionBase>> {
match label {
crate::assertions::Actions::LABEL => {
let actions: crate::assertions::Actions = serde_json::from_value(value.clone())?;
Ok(Box::new(actions)) // Convert to concrete Assertion
},
// crate::assertions::Ingredient::LABEL => {
// let ingredient: crate::assertions::Ingredient = serde_json::from_value(value.clone())?;
// ingredient.to_assertion() // Convert to concrete Assertion
// },
// Add more known assertion types here as needed
_ => {
// Create a User assertion for unknown types
let json_str = serde_json::to_string(value)?;
let user_json = crate::assertions::User::new(label, &json_str);
Ok(Box::new(user_json))
}
}
}

fn try_deserialize_known_cbor_assertion(label: &str, value: &CborValue) -> Result<Box<dyn AssertionBase>> {
let cbor_bytes = serde_cbor::to_vec(value)?;

match label {
crate::assertions::Actions::LABEL => {
let actions: crate::assertions::Actions = serde_cbor::from_slice(&cbor_bytes)?;
Ok(Box::new(actions)) // Convert to concrete Assertion
},
// crate::assertions::Ingredient::LABEL => {
// let ingredient: crate::assertions::Ingredient = serde_cbor::from_slice(&cbor_bytes)?;
// ingredient.to_assertion() // Convert to concrete Assertion
// },
// Add more known assertion types here as needed
_ => {
// Create a UserCbor assertion for unknown types
let user_cbor = crate::assertions::UserCbor::new(label, cbor_bytes);
Ok(Box::new(user_cbor))
}
}
}
54 changes: 52 additions & 2 deletions sdk/src/assertions/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use serde_cbor::Value;
use crate::{
assertion::{Assertion, AssertionBase, AssertionCbor},
assertions::{labels, region_of_interest::RegionOfInterest, Actor, AssertionMetadata},
error::Result,
error::{Error, Result},
resource_store::UriOrResource,
utils::cbor_types::DateT,
ClaimGeneratorInfo, HashedUri,
Expand Down Expand Up @@ -660,6 +660,27 @@ impl Action {
Ok(self)
}

/// Adds an ingredient HashedUri to the action.
pub(crate) fn add_ingredient(mut self, ingredient: HashedUri) -> Result<Self> {
match &mut self.parameters {
Some(params) => match &mut params.ingredients {
Some(ingredients) => {
ingredients.push(ingredient);
}
None => {
params.ingredients = Some(vec![ingredient]);
}
},
None => {
self.parameters = Some(ActionParameters {
ingredients: Some(vec![ingredient]),
..Default::default()
});
}
}
Ok(self)
}

/// Extracts ingredient IDs from the action
/// There are many deprecated ways to specify ingredient IDs
/// priority: parameters.ingredientIds, parameters.org.cai.ingredientIds, parameters.instanceId, instanceId.
Expand Down Expand Up @@ -775,6 +796,7 @@ impl Actions {
///
/// See <https://c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_actions>.
pub const LABEL: &'static str = labels::ACTIONS;
pub const LABEL_VERSIONED: &'static str = "c2pa.actions.v2";
pub const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION);

/// Creates a new [`Actions`] assertion struct.
Expand Down Expand Up @@ -850,6 +872,34 @@ impl Actions {
self
}

/// Adds an [`Action`] to this assertion's list of actions.
pub fn add_action_checked(mut self, action: Action) -> Result<Self> {
let action_name = action.action();
if action_name.is_empty() {
return Err(Error::AssertionSpecificError(
"Action must have a non-empty action label".to_string(),
));
}
if V2_DEPRECATED_ACTIONS.contains(&action_name) {
return Err(Error::VersionCompatibility(format!(
"Action '{action_name}' is deprecated in C2PA v2"
)));
}
if action_name == c2pa_action::OPENED || action_name == c2pa_action::CREATED {
let existing_action = self.actions.iter().find(|a| a.action() == action_name);
if existing_action.is_some() {
return Err(Error::AssertionSpecificError(format!(
"Only one '{action_name}' action is allowed"
)));
}
// always insert as first action
self.actions.insert(0, action);
return Ok(self);
}
self.actions.push(action);
Ok(self)
}

/// Sets [`AssertionMetadata`] for the action.
pub fn add_metadata(mut self, metadata: AssertionMetadata) -> Self {
self.metadata = Some(metadata);
Expand Down Expand Up @@ -882,7 +932,7 @@ impl AssertionBase for Actions {
}

fn label(&self) -> &str {
"c2pa.actions.v2"
Self::LABEL_VERSIONED
}

fn to_assertion(&self) -> Result<Assertion> {
Expand Down
79 changes: 79 additions & 0 deletions sdk/src/assertions/ingredient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// specific language governing permissions and limitations under
// each license.

use std::io::{Read, Seek};

#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
Expand All @@ -20,8 +22,12 @@ use crate::{
assertion::{Assertion, AssertionBase, AssertionDecodeError, AssertionDecodeErrorCause},
assertions::{labels, AssertionMetadata, ReviewRating},
cbor_types::map_cbor_to_type,
context::Context,
error::Result,
hashed_uri::HashedUri,
jumbf::labels::{to_manifest_uri, to_signature_uri},
status_tracker::StatusTracker,
store::Store,
validation_results::ValidationResults,
validation_status::ValidationStatus,
Error,
Expand Down Expand Up @@ -520,6 +526,79 @@ impl Ingredient {

ingredient_map.end()
}

/// Create a new Ingredient assertion from a stream
/// You must specify the relationship and format.
/// This will return both the new Ingredient and the associated Store.
pub(crate) fn from_stream(
relationship: Relationship,
format: &str,
mut stream: impl Read + Seek + Send,
context: &Context,
) -> Result<(Self, Store)> {
let mut validation_log = StatusTracker::default();

// // Try to get xmp info, if this fails all XmpInfo fields will be None.
// let xmp_info = XmpInfo::from_source(stream, &format);

// let id = if let Some(id) = xmp_info.instance_id {
// id
// } else {
// default_instance_id()
// };

// let mut ingredient = Self::new(title.into(), format, id);

// ingredient.document_id = xmp_info.document_id; // use document id if one exists
// ingredient.provenance = xmp_info.provenance;
let store: Store = Store::from_stream(format, &mut stream, &mut validation_log, context)?;
let validation_results = ValidationResults::from_store(&store, &validation_log);
let ingredient =
Self::from_store_and_validation_results(relationship, &store, &validation_results)?;
Ok((ingredient, store))
}

/// Create a new Ingredient assertion from a Store and ValidationResults.
/// You must specify the relationship.
pub(crate) fn from_store_and_validation_results(
relationship: Relationship,
store: &Store,
validation_results: &ValidationResults,
) -> Result<Self> {
if let Some(claim) = store.provenance_claim() {
let mut ingredient = Self::new_v3(relationship);

ingredient.title = claim.title().cloned();
ingredient.format = claim.format().map(|f| f.to_string());
ingredient.instance_id = Some(claim.instance_id().to_string());

let hashes = store.get_manifest_box_hashes(claim);

ingredient.active_manifest = Some(HashedUri::new(
to_manifest_uri(claim.label()),
Some(claim.alg().to_owned()),
hashes.manifest_box_hash.as_ref(),
));
ingredient.claim_signature = Some(HashedUri::new(
to_signature_uri(claim.label()),
Some(claim.alg().to_owned()),
hashes.signature_box_hash.as_ref(),
));

ingredient.validation_results = Some(validation_results.clone());

if ingredient
.validation_results
.as_ref()
.map(|r| r.validation_state())
!= Some(crate::ValidationState::Invalid)
{
ingredient.thumbnail = claim.thumbnail();
}
return Ok(ingredient);
}
Ok(Self::new_v3(relationship))
}
}

fn to_decoding_err(label: &str, version: usize, field: &str) -> Error {
Expand Down
Loading
Loading