Skip to main content

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(); 
  1. 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.

  2. 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").

  3. 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:

    1. Provenance assertion: to describe the origin of the asset.

    2. Ingredient assertion: to specify source material included in a composite.

    3. Custom JSON assertion: for your own metadata.

  4. Edit manifest

    To customize the manifest:

    1. Modify the JSON payload passed into Exif::from_json_str. Example: update GPS, timestamp, or remove fields.

    2. Replace the EXIF assertion with a different assertion type. Example: provenance, thumbnail, or custom JSON.

    3. Add multiple assertions by calling manifest.add_assertion(...) more than once.

  5. Embed manifest

    The manifest is embedded into the image file when this line executes:

    manifest.embed_stream(&format, &mut stream, &signer) 

Run the application

  1. Go to the base path of the project and run the below commands in sequence:

    1. cargo clean 

    2. cargo generate-lockfile 

    3. cargo update 

    4. cargo build

  2. 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.

  3. To compile the Java program, see the quick start guide.

  4. Check the credentials of a signed image using the Verify Content feature in Content Trust Manager or this C2PA tool.