Signing mechanism and sample code
Dependencies
Cargo.toml [package]
[package] name = "c2pa_rs" version = "0.1.0" edition = "2024" [lib] name = "c2pa_rs" crate-type = ["cdylib","staticlib"] [dependencies] c2pa = { version = "0.18.1", default-features = false, features = ["sign"] } request = { version = "0.12.20", features = ["blocking", "json", "native-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" base64 = "0.22.1" log = "0.4" env_logger = "0.11.8" sha2 = "0.10" uuid = { version = "1.12.0", features = ["v4"] } once_cell = "1.19"
FFI signature
#[no_mangle] pub extern "C" fn verify_bytes(format: *const c_char, bytes: *const u8, length: usize) { #[no_mangle] pub extern "C" fn sign_bytes( format: *const c_char, source_bytes: *const u8, length: usize, dest_bytes: *mut u8, dest_size: usize, ) -> i64 {
Define API constants
const SAD_API_URL: &str = "https://clientauth.xxxxxxxxxxxx/documentmanager/csc/v1/credentials/authorize"; const SIGNING_API_URL: &str = "https://clientauth.xxxxxxxxxxxx/documentmanager/csc/v1/signatures/signHash"; const CLIENT_CERT_PATH: &str = "Your Client Certificate Path"; const CLIENT_CERT_PASS: &str = "Your Pass Key";
Struct definitions for API requests and responses
#[derive(Serialize)] struct SADRequest { credentialID: String, numSignatures: u8, hash: Vec<String>, PIN: String, } #[derive(Deserialize)] struct SADResponse { pub expiresIn: u64, SAD: String, } #[derive(Serialize)] struct SignRequest { credentialID: String, SAD: String, hash: Vec<String>, signAlgo: String, signAlgoParams: String, } #[derive(Deserialize)] struct SignResponse { signatures: Vec<String>, }
Implement RemoteSigner struct
struct RemoteSigner { signature: Vec<u8>, certificates: Vec<Vec<u8>>, } impl Signer for RemoteSigner { fn sign(&self, _data: &[u8]) -> c2pa::Result<Vec<u8>> { Ok(self.signature.clone()) } fn alg(&self) -> SigningAlg { SigningAlg::Ps256 } fn certs(&self) -> c2pa::Result<Vec<Vec<u8>>> { // Return the entire certificate chain Ok(self.certificates.clone()) } fn reserve_size(&self) -> usize { // Calculate total size needed for signature and certificates let signature_size = self.signature.len(); let certs_size: usize = self.certificates.iter().map(|cert| cert.len()).sum(); // Add some padding for COSE structure overhead let total_size = signature_size + certs_size + 1024; info!("Reserve size calculation:"); info!(" Signature size: {} bytes", signature_size); info!(" Certificates total size: {} bytes", certs_size); info!(" Total reserve size: {} bytes", total_size); total_size } }
Implement verify_bytes function
#[no_mangle] pub extern "C" fn verify_bytes(format: *const c_char, bytes: *const u8, length: usize) { // convert C pointers into Rust let format = unsafe { CStr::from_ptr(format).to_string_lossy().into_owned() }; let bytes: &[u8] = unsafe { slice::from_raw_parts(bytes, length as usize) }; // Verify the manifests match ManifestStore::from_bytes(&format, bytes, true) { Ok(manifest_store) => println!("{}", manifest_store), Err(e) => eprintln!("Error {:?}", e), } }
Call CSC APIs to get signature from hashed data
Fetch SAD from DigiCert API
fn build_client_with_pkcs12() -> Result<Client, Box<dyn Error>> { use reqwest::tls::Identity; use std::fs::File; use std::io::Read; let mut buf = Vec::new(); File::open(CLIENT_CERT_PATH)?.read_to_end(&mut buf)?; let pkcs12 = Identity::from_pkcs12_der(&buf, CLIENT_CERT_PASS)?; let client = Client::builder() .identity(pkcs12) .danger_accept_invalid_certs(true) .build()?; Ok(client) } fn fetch_sad(hash: &str) -> Result<String, Box<dyn Error>> { let client = build_client_with_pkcs12()?; println!("Fetching sad hash {}", hash); let mut headers = HeaderMap::new(); headers.insert("Content-Type", HeaderValue::from_static("application/json")); let request_body = SADRequest { credentialID: "Cred-With-Email-EKU".to_string(), numSignatures: 1, hash: vec![hash.to_string()], PIN: "$zGpbJ".to_string(), }; info!("Fetching SAD from {}", SAD_API_URL); debug!( "SAD Request Body: {:?}", serde_json::to_string(&request_body)? ); let response: SADResponse = client .post(SAD_API_URL) .headers(headers) .json(&request_body) .send()? .error_for_status()? .json()?; info!("Received SAD: {}", response.SAD); Ok(response.SAD) }
Send signing request to DigiCert API
fn sign_via_api(hash: &str, sad: &str) -> Result<String, Box<dyn Error>> { let client = build_client_with_pkcs12()?; let mut headers = HeaderMap::new(); headers.insert("Content-Type", HeaderValue::from_static("application/json")); let request_body = SignRequest { credentialID: "Cred-With-Email-EKU".to_string(), SAD: sad.to_string(), hash: vec![hash.to_string()], signAlgo: "1.2.840.113549.1.1.10".to_string(), signAlgoParams: "MDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEg" .to_string(), }; info!("Sending signing request to {}", SIGNING_API_URL); debug!( "Sign Request Body: {:?}", serde_json::to_string(&request_body)? ); let response: SignResponse = client .post(SIGNING_API_URL) .headers(headers) .json(&request_body) .send()? .error_for_status()? .json()?; info!("Received signature response: {:?}", response.signatures); Ok(response.signatures.first().cloned().ok_or_else(|| { Box::new(std::io::Error::new( std::io::ErrorKind::Other, "Empty signature response", )) })?) } fn read_certificate_chain() -> Result<Vec<Vec<u8>>, Box<dyn Error>> { info!("Using embedded certificate"); // Use the certificate content directly as a string let cert_content = "-----BEGIN CERTIFICATE----- MIIE/jCCA7KgAwIBAgIUU7QZgXiH4LLhdbLnExQihpej68wwQQ -----END CERTIFICATE-----"; // Extract the base64-encoded certificate content let cert_data = cert_content .lines() .filter(|line| !line.contains("BEGIN CERTIFICATE") && !line.contains("END CERTIFICATE")) .collect::<Vec<&str>>() .join(""); info!("Extracted certificate data length: {} chars", cert_data.len()); // Decode the base64 certificate data let cert_binary = BASE64_STANDARD.decode(cert_data)?; info!("Decoded certificate length: {} bytes", cert_binary.len()); // For this implementation, we're just using the single certificate from the PEM file let certs = vec![cert_binary]; info!("Certificate chain loaded with {} certificates", certs.len()); for (i, cert) in certs.iter().enumerate() { info!("Certificate {} length: {} bytes", i, cert.len()); } Ok(certs) }
Expose sign_image function via FFI
//#[cfg(feature = "sign")] #[no_mangle] pub extern "C" fn sign_bytes( format: *const c_char, source_bytes: *const u8, length: usize, dest_bytes: *mut u8, dest_size: usize, ) -> i64 { // Initialize logging env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); // convert C pointers into Rust let format = unsafe { CStr::from_ptr(format).to_string_lossy().into_owned() }; let source_bytes: &[u8] = unsafe { slice::from_raw_parts(source_bytes, length as usize) }; let dest_bytes: &mut [u8] = unsafe { slice::from_raw_parts_mut(dest_bytes, dest_size as usize) }; // Create manifest let mut manifest = c2pa::Manifest::new("my_app".to_owned()); manifest.set_title("EmbedStream"); 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" }"#, ) .unwrap(); manifest.add_assertion(&exif).unwrap(); // Get manifest bytes and compute hash let manifest_bytes = serde_json::to_vec(&manifest).unwrap(); let mut hasher = Sha256::new(); hasher.update(&manifest_bytes); let hashed_data = hasher.finalize(); let base64_hash = BASE64_STANDARD.encode(hashed_data); // Get SAD and signature let sad = match fetch_sad(&base64_hash) { Ok(sad) => sad, Err(e) => { error!("Failed to fetch SAD: {}", e); return -1; } }; let signed_signature = match sign_via_api(&base64_hash, &sad) { Ok(signature) => signature, Err(e) => { error!("Failed to get signature: {}", e); return -1; } }; // Decode the signed signature let decoded_signature = match BASE64_STANDARD.decode(&signed_signature) { Ok(sig) => sig, Err(e) => { error!("Failed to decode signature: {}", e); return -1; } }; // Read certificate chain from file let cert_chain = match read_certificate_chain() { Ok(chain) => { info!("Certificate chain loaded with {} certificates", chain.len()); for (i, cert) in chain.iter().enumerate() { info!("Certificate {} length: {} bytes", i, cert.len()); } chain } Err(e) => { error!("Failed to read certificate chain: {}", e); return -1; } }; // Create a custom signer that uses the decoded signature and certificate chain let signer = RemoteSigner { signature: decoded_signature.clone(), certificates: cert_chain, }; info!("Signature length: {} bytes", decoded_signature.len()); // Convert buffer to cursor let mut stream = Cursor::new(source_bytes.to_vec()); // Embed the manifest let _manifest_bytes = match manifest.embed_stream(&format, &mut stream, &signer) { Ok(manifest) => manifest, Err(e) => { error!("Embed Stream Error: {}", e); return -1; } }; // Get the updated asset let bytes = stream.into_inner(); // Copy the signed asset into the output buffer if dest_size >= bytes.len() { dest_bytes[..bytes.len()].clone_from_slice(&bytes); } else { error!("dest_size too small, {} bytes required", bytes.len()); return -1; } // Verify the manifests match ManifestStore::from_bytes(&format, &bytes, true) { Ok(manifest_store) => println!("{}", manifest_store), Err(e) => { error!("Manifest from bytes Error {:?}", e); return -1; } }; bytes.len() as i64 }
Create and customize manifest
In this implementation, the manifest is created and embedded into the image during the signing process. You can find this logic in the sign_bytes function in lib.rs.
// Create manifest let mut manifest = c2pa::Manifest::new("my_app".to_owned()); manifest.set_title("EmbedStream"); // Example: add EXIF metadata assertion 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" }"#).unwrap(); manifest.add_assertion(&exif).unwrap();
Initialize manifest
c2pa::Manifest::new("my_app") creates a new manifest. The string argument my_app is an identifier that you can change to suit your application.
Set manifest title
manifest.set_title("EmbedStream") gives a name to the manifest. Change this to reflect the purpose of the signed asset (Example: "MyCompanyImageSignature").
Add assertions
In the sample code, an EXIF assertion is added using JSON metadata. This defines GPS coordinates, altitude, and timestamp.
You can replace this with other assertion types supported by C2PA, such as:
Provenance assertion: to describe the origin of the asset.
Ingredient assertion: to specify source material included in a composite.
Custom JSON assertion: for your own metadata.
Edit manifest
To customize the manifest:
Modify the JSON payload passed into Exif::from_json_str. Example: update GPS, timestamp, or remove fields.
Replace the EXIF assertion with a different assertion type. Example: provenance, thumbnail, or custom JSON.
Add multiple assertions by calling manifest.add_assertion(...) more than once.
Embed manifest
The manifest is embedded into the image file when this line executes:
manifest.embed_stream(&format, &mut stream, &signer)
Run the application
Go to the base path of the project and run the below commands in sequence:
cargo clean
cargo generate-lockfile
cargo update
cargo build
This generates libc2pa_rs.dylib under the target folder. As part of JNA, use the name c2pa_rs. JNA will automatically prepend the library to it.
To compile the Java program, see the quick start guide.
Check the credentials of a signed image using the Verify Content feature in Content Trust Manager or this C2PA tool.