Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 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
229 changes: 71 additions & 158 deletions sdk/examples/data_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,207 +11,120 @@
// specific language governing permissions and limitations under
// each license.

// Example code (in unit test) for how you might use client DataHash values. This allows clients
// to perform the manifest embedding and optionally the hashing
// Example demonstrating the placeholder/sign workflow for data-hashed embeddable manifests.
//
// This workflow allows clients to:
// 1. Create a placeholder manifest with a pre-sized DataHash assertion
// 2. Embed the placeholder into their asset
// 3. Calculate the hash of the asset (excluding the placeholder)
// 4. Update the DataHash in the Builder with the calculated hash
// 5. Sign the placeholder to create the final manifest
//
// This approach supports dynamic assertions (e.g., CAWG) and gives clients
// full control over the embedding and hashing process.

#[cfg(feature = "file_io")]
use std::{
io::{Cursor, Read, Seek, Write},
path::{Path, PathBuf},
io::{Cursor, Seek, Write},
path::PathBuf,
};

#[cfg(feature = "file_io")]
use c2pa::crypto::raw_signature::SigningAlg;
#[cfg(feature = "file_io")]
#[allow(deprecated)]
use c2pa::{
assertions::{
c2pa_action, labels::*, Action, Actions, CreativeWork, DataHash, Exif, SchemaDotOrgPerson,
},
create_signer, hash_stream_by_alg, Builder, ClaimGeneratorInfo, HashRange, Ingredient, Reader,
Relationship, Result,
assertions::{c2pa_action, Action, DataHash},
hash_stream_by_alg, Builder, ClaimGeneratorInfo, HashRange, Reader, Result,
};

use serde_json::json;
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
println!("DataHash demo");

#[cfg(feature = "file_io")]
user_data_hash_with_sdk_hashing()?;
println!("Done with SDK hashing1");
#[cfg(feature = "file_io")]
user_data_hash_with_user_hashing()?;
println!("Done with SDK hashing2");
user_data_hash_with_placeholder_api()?;
println!("Done with placeholder API");
Ok(())
}

#[cfg(feature = "file_io")]
fn builder_from_source<S: AsRef<Path>>(source: S) -> Result<Builder> {
let mut parent = Ingredient::from_file(source.as_ref())?;
parent.set_relationship(Relationship::ParentOf);
// create an action assertion stating that we imported this file
let actions = Actions::new().add_action(
Action::new(c2pa_action::PLACED)
.set_parameter("ingredients", [parent.instance_id().to_owned()])?,
);

// build a creative work assertion
// TO DO: Remove this example.
#[allow(deprecated)]
let creative_work =
CreativeWork::new().add_author(SchemaDotOrgPerson::new().set_name("me")?)?;

let exif = Exif::from_json_str(
r#"{
"@context" : {
"exif": "http://ns.adobe.com/exif/1.0/"
},
"exif:GPSVersionID": "2.2.0.0",
"exif:GPSLatitude": "39,21.102N",
"exif:GPSLongitude": "74,26.5737W",
"exif:GPSAltitudeRef": 0,
"exif:GPSAltitude": "100963/29890",
"exif:GPSTimeStamp": "2019-09-22T18:22:57Z"
}"#,
)?;

let mut builder = Builder::default();
fn user_data_hash_with_placeholder_api() -> Result<()> {
use c2pa::{Context, Settings};

let mut claim_generator = ClaimGeneratorInfo::new("test_app".to_string());
claim_generator.set_version("0.1");

builder
.set_claim_generator_info(claim_generator)
.add_ingredient(parent)
.add_assertion(ACTIONS, &actions)?
.add_assertion_json(CREATIVE_WORK, &creative_work)?
.add_assertion_json(EXIF, &exif)?;
// Use Settings to configure signer with CAWG support
let settings =
Settings::new().with_toml(include_str!("../tests/fixtures/test_settings_with_cawg_signing.toml"))?;

Ok(builder)
}
let src = "sdk/tests/fixtures/earth_apollo17.jpg";
let format = "image/jpeg";
let source = PathBuf::from(src);

#[cfg(feature = "file_io")]
fn user_data_hash_with_sdk_hashing() -> Result<()> {
// You will often implement your own Signer trait to perform on device signing
let signcert_path = "sdk/tests/fixtures/certs/es256.pub";
let pkey_path = "sdk/tests/fixtures/certs/es256.pem";
let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None)?;
// Create a Builder with Context from Settings
let context = Context::new().with_settings(settings)?.into_shared();
let mut builder = Builder::from_shared_context(&context);

let src = "sdk/tests/fixtures/earth_apollo17.jpg";
let parent_json = json!({"relationship": "parentOf", "label": "parent_label"});
builder.add_ingredient_from_stream(
parent_json.to_string(),
format,
&mut std::fs::File::open(&source)?,
)?;
builder.add_action(
Action::new(c2pa_action::OPENED).set_parameter("ingredientIds", ["parent_label"])?,
)?;

let source = PathBuf::from(src);
// Add a placeholder DataHash with enough space for the exclusion we'll need
// The hash value doesn't need to be final, but the structure should be sized correctly
let mut placeholder_dh = DataHash::new("jumbf manifest", "sha256");
// Add a placeholder exclusion for where the manifest will be embedded
// We don't know the exact size yet, but we'll update it later
placeholder_dh.add_exclusion(HashRange::new(0, 1000)); // Placeholder range
// Set a dummy hash (will be replaced with actual hash later)
let dummy_hash = vec![0u8; 32]; // 32 bytes for SHA-256
placeholder_dh.set_hash(dummy_hash);
builder.add_assertion(DataHash::LABEL, &placeholder_dh)?;

let mut builder = builder_from_source(&source)?; // c2pa::Builder::from_manifest_definition(manifest_definition(&source)?);
// Create the placeholder manifest (supports dynamic assertions)
let placeholder = builder.placeholder("image/jpeg")?;

let placeholder_manifest =
builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg")?;
// Compose the manifest for the target format (JPEG)
let jpeg_placeholder = Builder::composed_manifest(&placeholder, "image/jpeg")?;

let bytes = std::fs::read(&source)?;
let mut output: Vec<u8> = Vec::with_capacity(bytes.len() + placeholder_manifest.len());
let mut output: Vec<u8> = Vec::with_capacity(bytes.len() + jpeg_placeholder.len());

// Generate new file inserting unfinished manifest into file.
// Figure out where you want to put the manifest.
// Here we put it at the beginning of the JPEG as first segment after the 2 byte SOI marker.
// Insert placeholder at beginning of JPEG (after SOI marker)
let manifest_pos = 2;
output.extend_from_slice(&bytes[0..manifest_pos]);
output.extend_from_slice(&placeholder_manifest);
output.extend_from_slice(&jpeg_placeholder);
output.extend_from_slice(&bytes[manifest_pos..]);

// make a stream from the output bytes
let mut output_stream = Cursor::new(output);

// we need to add a data hash that excludes the manifest
let mut dh = DataHash::new("my_manifest", "sha265");
let hr = HashRange::new(manifest_pos as u64, placeholder_manifest.len() as u64);
// Now create the final DataHash with the actual exclusion range
let mut dh = DataHash::new("jumbf manifest", "sha256");
let hr = HashRange::new(manifest_pos as u64, jpeg_placeholder.len() as u64);
dh.add_exclusion(hr.clone());

// Hash the bytes excluding the manifest we inserted
// Hash the bytes excluding the manifest
let hash = hash_stream_by_alg("sha256", &mut output_stream, Some([hr].to_vec()), true)?;
dh.set_hash(hash);

// tell SDK to fill in the hash and sign to complete the manifest
let final_manifest = builder.sign_data_hashed_embeddable(signer.as_ref(), &dh, "image/jpeg")?;
// Remove the old placeholder DataHash and add the updated one
builder
.definition
.assertions
.retain(|a| !a.label.starts_with(DataHash::LABEL));
builder.add_assertion(DataHash::LABEL, &dh)?;

// replace temporary manifest with final signed manifest
// move to location where we inserted manifest,
// note: temporary manifest and final manifest will be the same size
output_stream.seek(std::io::SeekFrom::Start(2))?;
// Sign the placeholder with the updated hash from the Builder
// The signer is obtained from the Builder's context
let final_manifest = builder.sign_placeholder(&placeholder, "image/jpeg")?;

// write completed final manifest bytes over temporary bytes
// Replace placeholder with final signed manifest
output_stream.seek(std::io::SeekFrom::Start(manifest_pos as u64))?;
output_stream.write_all(&final_manifest)?;

output_stream.rewind()?;
// make sure the output stream is correct
let reader = Reader::from_stream("image/jpeg", &mut output_stream)?;

// example of how to print out the whole manifest as json
println!("{reader}\n");

Ok(())
}

#[cfg(feature = "file_io")]
fn user_data_hash_with_user_hashing() -> Result<()> {
// You will often implement your own Signer trait to perform on device signing
let signcert_path = "sdk/tests/fixtures/certs/es256.pub";
let pkey_path = "sdk/tests/fixtures/certs/es256.pem";
let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Es256, None)?;

let src = "sdk/tests/fixtures/earth_apollo17.jpg";
let dst = "target/tmp/output_hashed.jpg";

let source = PathBuf::from(src);
let dest = PathBuf::from(dst);

let mut input_file = std::fs::OpenOptions::new().read(true).open(&source)?;

let mut output_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(dest)?;

let mut builder = builder_from_source(&source)?;
// get the composed manifest ready to insert into a file (returns manifest of same length as finished manifest)
let placeholder_manifest =
builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg")?;

// Figure out where you want to put the manifest, let's put it at the beginning of the JPEG as first segment
// we will need to add a data hash that excludes the manifest
let mut dh = DataHash::new("my_manifest", "sha265");
let hr = HashRange::new(2, placeholder_manifest.len() as u64);
dh.add_exclusion(hr);

// since the only thing we are excluding in this example is the manifest we can just hash all the bytes
// if you have additional exclusions you can add them to the DataHash and pass them to this function to be '
// excluded from the hash generation
let hash = hash_stream_by_alg("sha256", &mut input_file, None, true)?;
dh.set_hash(hash);

// tell SDK to fill in the hash and sign to complete the manifest
let final_manifest: Vec<u8> =
builder.sign_data_hashed_embeddable(signer.as_ref(), &dh, "image/jpeg")?;

// generate new file inserting final manifest into file
input_file.rewind().unwrap();
let mut before = vec![0u8; 2];
input_file.read_exact(before.as_mut_slice()).unwrap();

output_file.write_all(&before).unwrap();

// write completed final manifest
output_file.write_all(&final_manifest).unwrap();

// write bytes after
let mut after_buf = Vec::new();
input_file.read_to_end(&mut after_buf).unwrap();
output_file.write_all(&after_buf).unwrap();

// make sure the output file is correct
output_file.rewind()?;
let reader = Reader::from_stream("image/jpeg", output_file)?;

// example of how to print out the whole manifest as json
println!("Manifest with placeholder API (supports dynamic assertions):");
println!("{reader}\n");

Ok(())
Expand Down
90 changes: 90 additions & 0 deletions sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,8 @@ impl Builder {
/// Create a placeholder for a hashed data manifest.
///
/// This is only used for applications doing their own data_hashed asset management.
/// This function does not support dynamic assertions (e.g., CAWG identity).
/// Use [`Builder::placeholder`] if you need dynamic assertion support.
///
/// # Arguments
/// * `reserve_size` - The size to reserve for the signature (taken from the signer).
Expand All @@ -1920,6 +1922,7 @@ impl Builder {
reserve_size: usize,
format: &str,
) -> Result<Vec<u8>> {
// Add DataHash to builder's definition if not present, so it persists for sign_data_hashed_embeddable
let dh: Result<DataHash> = self.find_assertion(DataHash::LABEL);
if dh.is_err() {
let mut ph = DataHash::new("jumbf manifest", "sha256");
Expand All @@ -1935,6 +1938,93 @@ impl Builder {
Ok(placeholder)
}

/// Create a placeholder manifest with dynamic assertion support.
///
/// This returns raw JUMBF bytes for the placeholder manifest.
/// Use [`Builder::composed_manifest`] to compose for a specific format if needed.
/// The signer is obtained from the Builder's context.
///
/// **Important:** You must add a hash assertion (DataHash or BmffHash) to the Builder
/// before calling this method. The hash doesn't need final values, but should be sized
/// large enough to hold the final hash (e.g., with enough exclusions).
///
/// # Workflow
/// 1. Add a placeholder hash assertion to the Builder with proper sizing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we don't need to do this for them right now, but it would be very helpful if we had methods on our hash bindings that can calculate the placeholder hash sizes given a stream.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we could, but that means another pass through the asset

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move to the asset_io model, we can preserve the asset structure and reuse it, avoiding extra passes. This is more or less a stopgap until we get there.

/// 2. Call this method to create a placeholder manifest
/// 3. Embed the placeholder into your asset
/// 4. Calculate the hash of the asset (excluding the placeholder)
/// 5. Update the hash assertion in the Builder: `builder.add_assertion(DataHash::LABEL, &updated_hash)`
/// 6. Call [`Builder::sign_placeholder`] to sign the manifest
///
/// # Returns
/// * The raw JUMBF bytes of the `c2pa_manifest` placeholder.
///
/// # Errors
/// * Returns an [`Error`] if the placeholder cannot be created or if no hash assertion exists.
pub fn placeholder(&self, format: &str) -> Result<Vec<u8>> {
// Check that a hash assertion exists before proceeding
let has_data_hash = self.find_assertion::<DataHash>(DataHash::LABEL).is_ok();
let has_bmff_hash = self.find_assertion::<BmffHash>(BmffHash::LABEL).is_ok();

Copy link
Contributor

@ok-nick ok-nick Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this function take a placeholder hash binding assertion as a parameter so we force them to think about it at compile time?

Copy link
Collaborator Author

@gpeacock gpeacock Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require the user to know how to generate a hash binding placeholder, and know which one to create. That's most of what this method is for.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this implementation, doesn't the user create the hash binding placeholder anyways and add it to the builder?

if !has_data_hash && !has_bmff_hash {
return Err(Error::BadParam(
"Builder must have a hash assertion (DataHash or BmffHash) before calling placeholder()".to_string()
));
}

let mut store = self.to_store()?;
store.get_placeholder(format, self.context())
}

/// Sign a placeholder manifest with updated hash assertions from the Builder.
///
/// This method takes a placeholder JUMBF created by [`Builder::placeholder`], updates
/// any hash assertions (DataHash, BmffHash) from the Builder's current state, and signs
/// the manifest. This supports dynamic assertions if configured in the signer.
///
/// # Workflow
/// 1. Call [`Builder::placeholder`] to create a placeholder manifest
/// 2. Embed the placeholder into your asset
/// 3. Calculate the hash of the asset (excluding the placeholder)
/// 4. Update the hash assertion in the Builder: `builder.add_assertion(DataHash::LABEL, &updated_hash)`
/// 5. Call this method to sign: `builder.sign_placeholder(&placeholder, signer, format)`
Copy link
Contributor

@ok-nick ok-nick Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this approach and maybe we can change the API to "force" users into doing the right thing at compile time.

Here's what I'm thinking: what if we made Builder::placeholder return a Placeholder struct and the context gets passed along to it from the builder. Then, we can have a method like Placeholder::sign(&self, format, updated_hard_binding_assertion). In this approach, users are forced to do the correct thing and there isn't much room for mistakes.

There can also be a method such as Placeholder::as_bytes so it can be embedded in the asset for calculating offsets. Although if it's a box hash, you can completely skip that step.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what you mean. Let me think about that. I'm not eager to introduce more structures to the C_api but this may make more sense than extending the builder.

///
/// # Arguments
/// * `placeholder_jumbf` - The placeholder JUMBF bytes from [`Builder::placeholder`]
///
/// # Returns
/// * The signed manifest bytes ready for embedding
///
/// # Errors
/// * Returns an [`Error`] if the placeholder cannot be deserialized or signed
pub fn sign_placeholder(&mut self, placeholder_jumbf: &[u8], format: &str) -> Result<Vec<u8>> {
// Deserialize placeholder without validation (it has a placeholder signature)
let mut validation_log = crate::status_tracker::StatusTracker::default();
let mut no_verify_settings = self.context.settings().clone();
no_verify_settings.verify.verify_after_reading = false;
let temp_context = Context::new().with_settings(no_verify_settings)?;

let mut store =
Store::from_jumbf_with_context(placeholder_jumbf, &mut validation_log, &temp_context)?;

// Update hash assertions from Builder into the Store
let pc = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;

if let Ok(data_hash) = self.find_assertion::<DataHash>(DataHash::LABEL) {
pc.update_data_hash(data_hash)?;
}

if let Ok(bmff_hash) = self.find_assertion::<BmffHash>(BmffHash::LABEL) {
pc.update_bmff_hash(bmff_hash)?;
}

let signer = self.context().signer()?;
let jumbf = store.sign_manifest(signer, self.context().settings())?;

// Compose for the target format
Store::get_composed_manifest(&jumbf, format)
}

/// Create a signed data hashed embeddable manifest using a supplied signer.
///
/// This is used to create a manifest that can be embedded into a stream.
Expand Down
Loading
Loading