simple-c2pa

This is a fork of https://gitlab.com/guardianproject/proofmode/simple-c2pa
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/simple-c2pa.git
Log | Files | Refs | README | LICENSE

commit c939223b2e0d4999a0f3d31c6205dc7e9294d99a
parent 47d4afc16a6c5b90b8aac874f1f6ce39003ac077
Author: Darren Clarke <darren@redaranj.com>
Date:   Wed,  6 Dec 2023 09:17:10 +0100

Add certificate convenience functions, examples

Diffstat:
MMakefile.toml | 5++++-
Msrc/assertions.rs | 51+++++++++++++++++----------------------------------
Msrc/certificates.rs | 55++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/content_credentials.rs | 4++--
Msrc/lib.rs | 4++--
Atests/examples.rs | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtests/main.rs | 87-------------------------------------------------------------------------------
Atests/media/test-1.jpg | 0
Atests/media/test-1.png | 0
9 files changed, 159 insertions(+), 129 deletions(-)

diff --git a/Makefile.toml b/Makefile.toml @@ -74,7 +74,10 @@ cross build --target aarch64-linux-android --release ''' [tasks.android-build-bindings] -script = 'cargo run --bin uniffi-bindgen generate --language kotlin --library ./target/x86_64-linux-android/release/libsimple_c2pa.so --out-dir ${BINDINGS_DIR}/kotlin' +script = ''' +cargo build --target x86_64-linux-android +cargo run --bin uniffi-bindgen generate --language kotlin --library ./target/x86_64-linux-android/debug/libsimple_c2pa.so --out-dir ${BINDINGS_DIR}/kotlin +''' [tasks.android-assemble-aar-files] env = { NAME = "simple_c2pa", LIB_NAME = "lib${NAME}.so", OUT_DIR = "out/android", JNILIBS_DIR = "${OUT_DIR}/jniLibs", ANDROID_BINDINGS_DIR = "kotlin/info/guardianproject/simple_c2pa" } diff --git a/src/assertions.rs b/src/assertions.rs @@ -3,9 +3,8 @@ use crate::content_credentials::ContentCredentials; use c2pa::assertions::{c2pa_action, labels, Action, Actions, Exif, SchemaDotOrg}; use serde::{Deserialize, Serialize}; use std::result::Result; -use std::sync::Arc; -#[derive(uniffi::Object)] +#[derive(uniffi::Record)] pub struct ExifData { pub gps_version_id: Option<String>, pub latitude: Option<String>, @@ -31,37 +30,6 @@ pub struct ExifData { pub lens_specification: Option<Vec<f64>>, } -#[uniffi::export] -impl ExifData { - #[uniffi::constructor] - pub fn new() -> Arc<Self> { - Arc::new(ExifData { - gps_version_id: None, - latitude: None, - longitude: None, - altitude_ref: None, - altitude: None, - timestamp: None, - speed_ref: None, - speed: None, - direction_ref: None, - direction: None, - destination_bearing_ref: None, - destination_bearing: None, - positioning_error: None, - exposure_time: None, - f_number: None, - color_space: None, - digital_zoom_ratio: None, - make: None, - model: None, - lens_make: None, - lens_model: None, - lens_specification: None, - }) - } -} - #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] struct AIDataMiningUsageJSON { @@ -195,7 +163,7 @@ impl ContentCredentials { Ok(()) } - pub fn add_exif_assertion(&self, exif_data: Arc<ExifData>) -> Result<(), SimpleC2PAError> { + pub fn add_exif_assertion(&self, exif_data: ExifData) -> Result<(), SimpleC2PAError> { let mut exif = Exif::new(); if let Some(gps_version_id) = exif_data.gps_version_id.clone() { exif = exif.insert("exif:GPSVersionID", gps_version_id)?; @@ -309,3 +277,18 @@ impl ContentCredentials { Ok(()) } } +/* +#[cfg(test)] +pub mod tests { + #[test] + fn get_creative_work_assertion() { + let work = crate::assertions::get_creative_work_assertion( + "John Doe".to_owned(), + "johndoe".to_owned(), + "https://instagram.com".to_owned(), + ) + .unwrap(); + assert_eq!("X", "CreativeWork"); + } +} +*/ diff --git a/src/certificates.rs b/src/certificates.rs @@ -136,9 +136,9 @@ impl CertificateOptions { #[derive(Clone, Debug, uniffi::Object)] pub struct Certificate { - pub certificate_data: Arc<FileData>, - pub private_key_data: Arc<FileData>, - pub parent_certificate: Option<Arc<Certificate>>, + certificate_data: Arc<FileData>, + private_key_data: Arc<FileData>, + parent_certificate: Option<Arc<Certificate>>, } #[uniffi::export] @@ -155,6 +155,14 @@ impl Certificate { parent_certificate, }) } + + pub fn get_certificate_bytes(&self) -> Result<Vec<u8>, SimpleC2PAError> { + return self.certificate_data.get_bytes(); + } + + pub fn get_private_key_bytes(&self) -> Result<Vec<u8>, SimpleC2PAError> { + return self.private_key_data.get_bytes(); + } } fn generate_serial_number() -> Result<Asn1Integer, SimpleC2PAError> { let random = ring::rand::SystemRandom::new(); @@ -261,6 +269,47 @@ pub fn create_certificate( } #[uniffi::export] +pub fn create_root_certificate( + organization: Option<String>, + validity_days: Option<u32>, +) -> Result<Arc<Certificate>, SimpleC2PAError> { + let key = create_private_key().unwrap(); + let options = CertificateOptions::new( + key.clone(), + CertificateType::OfflineRoot { + organization, + validity_days, + }, + None, + None, + None, + ); + let certificate = create_certificate(options)?; + Ok(certificate) +} + +#[uniffi::export] +pub fn create_content_credentials_certificate( + root_certificate: Option<Arc<Certificate>>, + organization: Option<String>, + validity_days: Option<u32>, +) -> Result<Arc<Certificate>, SimpleC2PAError> { + let key = create_private_key().unwrap(); + let options = CertificateOptions::new( + key.clone(), + CertificateType::ContentCredentials { + organization, + validity_days, + }, + root_certificate, + None, + None, + ); + let certificate = create_certificate(options)?; + Ok(certificate) +} + +#[uniffi::export] pub fn request_signed_certificate( _options: Arc<CertificateOptions>, ) -> Result<String, SimpleC2PAError> { diff --git a/src/content_credentials.rs b/src/content_credentials.rs @@ -80,8 +80,8 @@ impl ContentCredentials { certificate: Arc<Certificate>, output_file: Arc<FileData>, ) -> Result<(), SimpleC2PAError> { - let cert = certificate.certificate_data.get_bytes()?; - let pkey = certificate.private_key_data.get_bytes()?; + let cert = certificate.get_certificate_bytes()?; + let pkey = certificate.get_private_key_bytes()?; let algorithms = vec![ SigningAlg::Es256, SigningAlg::Es384, diff --git a/src/lib.rs b/src/lib.rs @@ -5,8 +5,8 @@ pub use common::FileData; mod certificates; pub use certificates::{ - create_certificate, create_private_key, request_signed_certificate, CertificateOptions, - CertificateType, + create_certificate, create_content_credentials_certificate, create_private_key, + create_root_certificate, request_signed_certificate, CertificateOptions, CertificateType, }; mod content_credentials; diff --git a/tests/examples.rs b/tests/examples.rs @@ -0,0 +1,82 @@ +#[cfg(test)] +pub mod tests { + use simple_c2pa::{ + create_content_credentials_certificate, create_root_certificate, ApplicationInfo, + ContentCredentials, ExifData, FileData, + }; + use std::fs; + + #[test] + fn basic_example() { + let image_path = "tests/media/test-1.jpg"; + let file_name = image_path.split("/").last().unwrap().to_string(); + let file_data = fs::read(image_path).expect("Can't read image"); + + let root_certificate = create_root_certificate(None, None).unwrap(); + let content_credentials_certificate = + create_content_credentials_certificate(Some(root_certificate.clone()), None, None) + .unwrap(); + + let file = FileData::new(None, Some(file_data), Some(file_name.clone())); + let cc = ContentCredentials::new(content_credentials_certificate, file, None); + cc.add_created_assertion().unwrap(); + + let output_path = format!("outputs/c2pa-basic-{}", file_name); + let file_data = cc.embed_manifest(Some(output_path.clone())).unwrap(); + fs::write(output_path, file_data.get_bytes().unwrap()).expect("Can't write file"); + } + + #[test] + fn complex_example() { + let image_path = "tests/media/test-1.jpg"; + let file_name = image_path.split("/").last().unwrap().to_string(); + let file_data = fs::read(image_path).expect("Can't read image"); + let fingerprint = "BA08 71E8 0200 B95D 8297  7ED0 4D1E C37F 88A7 FDCE".to_string(); + + let root_certificate = create_root_certificate(None, None).unwrap(); + let content_credentials_certificate = + create_content_credentials_certificate(Some(root_certificate.clone()), None, None) + .unwrap(); + + let file = FileData::new(None, Some(file_data), Some(file_name.clone())); + let app_info = ApplicationInfo::new("SampleApp".to_string(), "1.0.0".to_string(), None); + let cc = ContentCredentials::new(content_credentials_certificate, file, Some(app_info)); + println!("{:?}", cc); + cc.add_created_assertion().unwrap(); + + let exif_data = ExifData { + gps_version_id: Some("2.2.0.0".to_string()), + latitude: Some("39,21.102N".to_string()), + longitude: Some("74,26.5737W".to_string()), + altitude_ref: Some(0), + altitude: Some("100963/29890".to_string()), + timestamp: Some("2019-09-22T18:22:57Z".to_string()), + speed_ref: Some("K".to_string()), + speed: Some("4009/161323".to_string()), + direction_ref: Some("T".to_string()), + direction: Some("296140/911".to_string()), + destination_bearing_ref: Some("T".to_string()), + destination_bearing: Some("296140/911".to_string()), + positioning_error: Some("13244/2207".to_string()), + exposure_time: Some("1/100".to_string()), + f_number: Some(4.0), + color_space: Some(1), + digital_zoom_ratio: Some(2.0), + make: Some("ProofMode".to_string()), + model: Some("ProofMode In-App Camera v2.0".to_string()), + lens_make: Some("CameraCompany".to_string()), + lens_model: Some("17.0-35.0 mm".to_string()), + lens_specification: Some(vec![1.55, 4.2, 1.6, 2.4]), + }; + cc.add_exif_assertion(exif_data).unwrap(); + cc.add_instagram_assertion("johndoe".to_string(), "John Doe".to_string()) + .unwrap(); + cc.add_pgp_assertion(fingerprint, "88A7FDCE".to_string()) + .unwrap(); + cc.add_website_assertion("https://redaranj.com".to_string()) + .unwrap(); + let output_path = format!("outputs/c2pa-complex-{}", file_name); + let file_data = cc.embed_manifest(Some(output_path.clone())).unwrap(); + fs::write(output_path, file_data.get_bytes().unwrap()).expect("Can't write file"); + } +} diff --git a/tests/main.rs b/tests/main.rs @@ -1,87 +0,0 @@ -use simple_c2pa::{create_certificate, create_private_key, CertificateOptions, CertificateType}; -use simple_c2pa::{ApplicationInfo, ContentCredentials, ExifData, FileData}; -use std::env; -use std::fs; - -fn main() { - let args: Vec<String> = env::args().collect(); - - if args.len() != 2 { - eprintln!("Usage: command image.jpg data.json"); - std::process::exit(1); - } - - let image_path = &args[1]; - let file_name = image_path.split("/").last().unwrap().to_string(); - let file_data = fs::read(image_path).expect("Can't read image"); - let fingerprint = "BA08 71E8 0200 B95D 8297  7ED0 4D1E C37F 88A7 FDCE".to_string(); - - let root_key = create_private_key().unwrap(); - let root_options = CertificateOptions::new( - root_key.clone(), - CertificateType::OfflineRoot { - organization: None, - validity_days: None, - }, - None, - None, - None, - ); - let root_cert = create_certificate(root_options).unwrap(); - - let content_credentials_key = create_private_key().unwrap(); - let content_credentials_options = CertificateOptions::new( - content_credentials_key.clone(), - CertificateType::ContentCredentials { - organization: None, - validity_days: None, - }, - Some(root_cert.clone()), - None, - None, - ); - let content_credentials_certificate = create_certificate(content_credentials_options).unwrap(); - - let certificate = content_credentials_certificate.clone(); - let file = FileData::new(None, Some(file_data), Some(file_name.clone())); - let app_info = ApplicationInfo::new("SampleApp".to_string(), "1.0.0".to_string(), None); - let cc = ContentCredentials::new(certificate, file, Some(app_info)); - println!("{:?}", cc); - cc.add_created_assertion().unwrap(); - /* - let exif_data = ExifData::new(); - exif_data.gps_version_id = Some("2.2.0.0".to_string()); - exif_data.latitude = Some("39,21.102N".to_string()); - exif_data.longitude = Some("74,26.5737W".to_string()); - exif_data.altitude_ref = Some(0); - exif_data.altitude = Some("100963/29890".to_string()); - exif_data.timestamp = Some("2019-09-22T18:22:57Z".to_string()); - exif_data.speed_ref = Some("K".to_string()); - exif_data.speed = Some("4009/161323".to_string()); - exif_data.direction_ref = Some("T".to_string()); - exif_data.direction = Some("296140/911".to_string()); - exif_data.destination_bearing_ref = Some("T".to_string()); - exif_data.destination_bearing = Some("296140/911".to_string()); - exif_data.positioning_error = Some("13244/2207".to_string()); - exif_data.exposure_time = Some("1/100".to_string()); - exif_data.f_number = Some(4.0); - exif_data.color_space = Some(1); - exif_data.digital_zoom_ratio = Some(2.0); - exif_data.make = Some("ProofMode".to_string()); - exif_data.model = Some("ProofMode In-App Camera v2.0".to_string()); - exif_data.lens_make = Some("CameraCompany".to_string()); - exif_data.lens_model = Some("17.0-35.0 mm".to_string()); - exif_data.lens_specification = Some(vec![1.55, 4.2, 1.6, 2.4]); - - cc.add_exif_assertion(exif_data).unwrap(); - */ - cc.add_instagram_assertion("darrenjclarke".to_string(), "Darren Clarke".to_string()) - .unwrap(); - cc.add_pgp_assertion(fingerprint, "88A7FDCE".to_string()) - .unwrap(); - cc.add_website_assertion("https://redaranj.com".to_string()) - .unwrap(); - let output_path = format!("outputs/c2pa-{}", file_name); - let file_data = cc.embed_manifest(Some(output_path.clone())).unwrap(); - fs::write(output_path, file_data.get_bytes().unwrap()).expect("Can't write file"); -} diff --git a/tests/media/test-1.jpg b/tests/media/test-1.jpg Binary files differ. diff --git a/tests/media/test-1.png b/tests/media/test-1.png Binary files differ.