From 994934454e68637e6fd5333f6ae4553584bf6cfd Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Wed, 13 Nov 2024 00:28:23 -0800 Subject: [PATCH 1/5] feat: Adds Builder set_no_embed, and set_remote_url options. chore: update to c2pa_rs v0.38.0 Adds a rust level Builder test. --- Cargo.toml | 6 +++- Makefile | 3 +- include/c2pa.h | 36 +++++++++++++++++++--- include/c2pa.hpp | 8 +++++ src/c2pa.cpp | 15 +++++++++- src/c_api.rs | 39 +++++++++++++++++++++++- src/json_api.rs | 16 +++++----- tests/builder.test.cpp | 61 +++++++++++++++++++++++++++++++++++--- tests/fixtures/ed25519.pem | 3 ++ tests/fixtures/ed25519.pub | 30 +++++++++++++++++++ tests/rust.rs | 32 ++++++++++++++++++++ 11 files changed, 229 insertions(+), 20 deletions(-) create mode 100644 tests/fixtures/ed25519.pem create mode 100644 tests/fixtures/ed25519.pub diff --git a/Cargo.toml b/Cargo.toml index 9cec286..d8e567d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ authors = ["Gavin Peacock = Box::from_raw(builder_ptr); + builder.set_no_embed(true); + let _ = Box::into_raw(builder); +} + +/// Sets the remote URL on the Builder. +/// When set, the builder will embed a remote URL into the asset when signing. +/// This is useful when creating cloud based Manifests. +/// # Parameters +/// * builder_ptr: pointer to a Builder. +/// * remote_url: pointer to a C string with the remote URL. +/// # Errors +/// Returns -1 if there were errors, otherwise returns 0. +/// The error string can be retrieved by calling c2pa_error. +/// # Safety +/// Reads from NULL-terminated C strings. +#[no_mangle] +pub unsafe extern "C" fn c2pa_builder_set_remote_url( + builder_ptr: *mut C2paBuilder, + remote_url: *const c_char, +) -> c_int { + let mut builder: Box = Box::from_raw(builder_ptr); + let remote_url = from_cstr_null_check_int!(remote_url); + builder.set_remote_url(&remote_url); + let _ = Box::into_raw(builder); + 0 as c_int +} + /// Adds a resource to the C2paBuilder. /// /// The resource uri should match an identifier in the manifest definition. @@ -565,7 +602,7 @@ pub unsafe extern "C" fn c2pa_builder_add_resource( /// # Safety /// Reads from NULL-terminated C strings. #[no_mangle] -pub unsafe extern "C" fn c2pa_builder_add_ingredient( +pub unsafe extern "C" fn c2pa_builder_add_ingredient_from_stream( builder_ptr: *mut C2paBuilder, ingredient_json: *const c_char, format: *const c_char, diff --git a/src/json_api.rs b/src/json_api.rs index 587ad83..6b13671 100644 --- a/src/json_api.rs +++ b/src/json_api.rs @@ -10,7 +10,7 @@ // specific language governing permissions and limitations under // each license. -use c2pa::{Ingredient, Manifest, ManifestStore}; +use c2pa::{Ingredient, Manifest, Reader}; use crate::{Error, Result, SignerInfo}; @@ -25,12 +25,14 @@ pub fn sdk_version() -> String { /// Any Validation errors will be reported in the validation_status field. /// pub fn read_file(path: &str, data_dir: Option) -> Result { - Ok(match data_dir { - Some(dir) => ManifestStore::from_file_with_resources(path, &dir), - None => ManifestStore::from_file(path), - } - .map_err(Error::from_c2pa_error)? - .to_string()) + let reader = Reader::from_file(path).map_err(Error::from_c2pa_error)?; + Ok(if let Some(dir) = data_dir { + let json = reader.to_string(); + reader.to_folder(&dir).map_err(Error::from_c2pa_error)?; + json + } else { + reader.to_string() + }) } /// Returns an Ingredient JSON string from a file path. diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index 2408b23..0919cee 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -62,14 +62,15 @@ TEST(Builder, SignFile) auto json = reader.json(); ASSERT_TRUE(std::filesystem::exists(output_path)); } - catch (c2pa::Exception const& e) + catch (c2pa::Exception const &e) { FAIL() << "Failed: C2pa::Builder: " << e.what() << endl; }; }; -TEST(Builder, SignStream) { - try +TEST(Builder, SignStream) +{ + try { fs::path current_dir = fs::path(__FILE__).parent_path(); @@ -105,8 +106,60 @@ TEST(Builder, SignStream) { auto json = reader.json(); ASSERT_TRUE(json.find("c2pa.training-mining") != std::string::npos); } - catch (c2pa::Exception const& e) + catch (c2pa::Exception const &e) { FAIL() << "Failed: C2pa::Builder: " << e.what() << endl; }; +} + +TEST(Builder, SignStreamCloudUrl) +{ + try + { + fs::path current_dir = fs::path(__FILE__).parent_path(); + + // Construct the paths relative to the current directory + fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; + fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; + fs::path signed_image_path = current_dir / "../tests/fixtures/A.jpg"; + + auto manifest = read_text_file(manifest_path); + auto certs = read_text_file(certs_path); + + // create a signer + c2pa::Signer signer = c2pa::Signer(&test_signer, Es256, certs, "http://timestamp.digicert.com"); + + auto builder = c2pa::Builder(manifest); + + // very important to use a URL that does not exist, otherwise you may get a JumbfParseError or JumbfNotFound + builder.set_remote_url("http://this_does_not_exist/foo.jpg"); + builder.set_no_embed(); + + //auto manifest_data = builder.sign(signed_image_path, "target/dest.jpg", signer); + std::ifstream source(signed_image_path, std::ios::binary); + if (!source) + { + FAIL() << "Failed to open file: " << signed_image_path << std::endl; + } + + // Create a memory buffer + std::stringstream memory_buffer(std::ios::in | std::ios::out | std::ios::binary); + std::iostream &dest = memory_buffer; + auto manifest_data = builder.sign("image/jpeg", source, dest, signer); + source.close(); + + // Rewind dest to the start + dest.flush(); + dest.seekp(0, std::ios::beg); + auto reader = c2pa::Reader("image/jpeg", dest); + } + catch (c2pa::Exception const &e) + { + std::string error_message = e.what(); + if (error_message.rfind("Remote ", 0) == 0) { + SUCCEED(); + } else { + FAIL() << "Failed: C2pa::Builder: " << e.what() << endl; + } + }; } \ No newline at end of file diff --git a/tests/fixtures/ed25519.pem b/tests/fixtures/ed25519.pem new file mode 100644 index 0000000..fda14a8 --- /dev/null +++ b/tests/fixtures/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIL2+9INLPNSLH3STzKQJ3Wen9R6uPbIYOIKA2574YQ4O +-----END PRIVATE KEY----- diff --git a/tests/fixtures/ed25519.pub b/tests/fixtures/ed25519.pub new file mode 100644 index 0000000..030a689 --- /dev/null +++ b/tests/fixtures/ed25519.pub @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIICSDCCAfqgAwIBAgIUb+aBTX1CsjJ1iuMJ9kRudz/7qEcwBQYDK2VwMIGMMQsw +CQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3aGVyZTEnMCUG +A1UECgweQzJQQSBUZXN0IEludGVybWVkaWF0ZSBSb290IENBMRkwFwYDVQQLDBBG +T1IgVEVTVElOR19PTkxZMRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUgQ0EwHhcNMjIw +NjEwMTg0NjQxWhcNMzAwODI2MTg0NjQxWjCBgDELMAkGA1UEBhMCVVMxCzAJBgNV +BAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUxHzAdBgNVBAoMFkMyUEEgVGVzdCBT +aWduaW5nIENlcnQxGTAXBgNVBAsMEEZPUiBURVNUSU5HX09OTFkxFDASBgNVBAMM +C0MyUEEgU2lnbmVyMCowBQYDK2VwAyEAMp5+0e83nNgQhdhBW8Rshkjy90sa1A9J +IzkItcDqCuKjeDB2MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH +AwQwDgYDVR0PAQH/BAQDAgbAMB0GA1UdDgQWBBTuLrYRqW4wu6yjIK1/iW8ud7dm +kTAfBgNVHSMEGDAWgBRXTAfC/JxQvRlk/bCbdPMDbsSfqTAFBgMrZXADQQB2R6vb +I+X8CTRC54j3NTvsUj454G1/bdzbiHVgl3n+ShOAJ85FJigE7Eoav7SeXeVnNjc8 +QZ1UrJGwgBBEP84G +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICKTCCAdugAwIBAgIUWqg4SaUDuPVy671PLGgfzcIsHMwwBQYDK2VwMHcxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29tZXdoZXJlMRowGAYD +VQQKDBFDMlBBIFRlc3QgUm9vdCBDQTEZMBcGA1UECwwQRk9SIFRFU1RJTkdfT05M +WTEQMA4GA1UEAwwHUm9vdCBDQTAeFw0yMjA2MTAxODQ2NDFaFw0zMDA4MjcxODQ2 +NDFaMIGMMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3 +aGVyZTEnMCUGA1UECgweQzJQQSBUZXN0IEludGVybWVkaWF0ZSBSb290IENBMRkw +FwYDVQQLDBBGT1IgVEVTVElOR19PTkxZMRgwFgYDVQQDDA9JbnRlcm1lZGlhdGUg +Q0EwKjAFBgMrZXADIQAkzdYBZtpdWfp03GLOb1lmIr/0COsfUa3b8ebt90DorqNj +MGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFFdM +B8L8nFC9GWT9sJt08wNuxJ+pMB8GA1UdIwQYMBaAFF7mVgKz9Y4kTH4Mnumakchr +qU6MMAUGAytlcANBABXakz5vcdftU8Pbe8JDEcFSc+rTnopT8DXYwhtOd3Hvo7Zv +v0fTuwOYImxLdmu0J1u+ULxNqK+jRO7/jSncvwA= +-----END CERTIFICATE----- + diff --git a/tests/rust.rs b/tests/rust.rs index de190e0..5307283 100644 --- a/tests/rust.rs +++ b/tests/rust.rs @@ -13,6 +13,14 @@ // This shows that the c2pa_c library can export the Rust API // could reexport the c_api and add other rust specific features. +use core::panic; +use std::io::Cursor; + +use c2pa::{CallbackSigner, SigningAlg}; + +const CERTS: &[u8] = include_bytes!("../tests/fixtures/ed25519.pub"); +const PRIVATE_KEY: &[u8] = include_bytes!("../tests/fixtures/ed25519.pem"); + #[test] fn test_reader() { let mut stream = std::fs::File::open("tests/fixtures/C.jpg").unwrap(); @@ -20,3 +28,27 @@ fn test_reader() { println!("{}", reader.json()); assert!(reader.validation_status().is_none()) } + +#[test] +fn test_builder_remote_url() { + let ed_signer = + |_context: *const (), data: &[u8]| CallbackSigner::ed25519_sign(data, PRIVATE_KEY); + let signer = CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS); + let mut source = std::fs::File::open("tests/fixtures/A.jpg").unwrap(); + let manifest_json = std::fs::read_to_string("tests/fixtures/training.json").unwrap(); + let mut builder = c2pa::Builder::from_json(&manifest_json).unwrap(); + // very important to use a URL that does not exist, otherwise we may get a JumbfParseError or JumbfNotFound + builder.set_remote_url("http://this_does_not_exist/foo.jpg"); + builder.set_no_embed(true); + let mut output = Cursor::new(Vec::new()); + let _c2pa_data = builder + .sign(&signer, "image/jpeg", &mut source, &mut output) + .unwrap(); + output.set_position(0); + let result = c2pa_c::Reader::from_stream("image/jpeg", &mut output); + if let Err(c2pa::Error::RemoteManifestFetch(url)) = result { + assert_eq!(url, "http://this_does_not_exist/foo.jpg"); + } else { + panic!("Expected RemoteManifestFetch error"); + } +} From 65408885f183e0223af88bd0b9dddf3625b05dcd Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Wed, 13 Nov 2024 15:42:09 -0800 Subject: [PATCH 2/5] feat: Adds data_hashed_embeddable support. Builder.data_hashed_placeholder Builder.sign_data_hashed_embeddable Adds Signer.reserve_size method. Update training.json to use cawg values. --- include/c2pa.h | 67 +++++++++++++++- include/c2pa.hpp | 21 ++++- src/c2pa.cpp | 50 +++++++++++- src/c_api.rs | 151 ++++++++++++++++++++++++++++++++++- tests/builder.test.cpp | 52 +++++++++++- tests/fixtures/training.json | 11 ++- 6 files changed, 334 insertions(+), 18 deletions(-) diff --git a/include/c2pa.h b/include/c2pa.h index 3f1061e..89491a2 100644 --- a/include/c2pa.h +++ b/include/c2pa.h @@ -482,7 +482,7 @@ IMPORT extern int c2pa_builder_to_archive(struct C2paBuilder *builder_ptr, struc * * # Safety * Reads from NULL-terminated C strings - * If c2pa_data_ptr is not NULL, the returned value MUST be released by calling c2pa_release_string + * If manifest_bytes_ptr is not NULL, the returned value MUST be released by calling c2pa_manifest_bytes_free * and it is no longer valid after that call. */ IMPORT extern @@ -501,6 +501,59 @@ int c2pa_builder_sign(struct C2paBuilder *builder_ptr, */ IMPORT extern void c2pa_manifest_bytes_free(const unsigned char *manifest_bytes_ptr); +/** + * Creates a hashed placeholder from a Builder. + * The placeholder is used to reserve size in an asset for later signing. + * + * # Parameters + * * builder_ptr: pointer to a Builder. + * * reserved_size: the size required for a signature from the intended signer. + * * format: pointer to a C string with the mime type or extension. + * * manifest_bytes_ptr: pointer to a pointer to a c_uchar to return manifest_bytes. + * + * # Errors + * Returns -1 if there were errors, otherwise returns the size of the manifest_bytes. + * The error string can be retrieved by calling c2pa_error. + * + * # Safety + * Reads from NULL-terminated C strings. + * If manifest_bytes_ptr is not NULL, the returned value MUST be released by calling c2pa_manifest_bytes_free + * and it is no longer valid after that call. + */ +IMPORT extern +int c2pa_builder_data_hashed_placeholder(struct C2paBuilder *builder_ptr, + uintptr_t reserved_size, + const char *format, + const unsigned char **manifest_bytes_ptr); + +/** + * Sign a Builder using the specified signer and data hash. + * The data hash is a JSON string containing DataHash information for the asset. + * This is a low-level method for advanced use cases where the caller handles embedding the manifest. + * + * # Parameters + * * builder_ptr: pointer to a Builder. + * * signer: pointer to a C2paSigner. + * * data_hash: pointer to a C string with the JSON data hash. + * * format: pointer to a C string with the mime type or extension. + * * manifest_bytes_ptr: pointer to a pointer to a c_uchar to return manifest_bytes (optional, can be NULL). + * + * # Errors + * Returns -1 if there were errors, otherwise returns the size of the manifest_bytes. + * The error string can be retrieved by calling c2pa_error. + * + * # Safety + * Reads from NULL-terminated C strings. + * If manifest_bytes_ptr is not NULL, the returned value MUST be released by calling c2pa_manifest_bytes_free + * and it is no longer valid after that call. + */ +IMPORT extern +int c2pa_builder_sign_data_hashed_embeddable(struct C2paBuilder *builder_ptr, + struct C2paSigner *signer, + const char *data_hash, + const char *format, + const unsigned char **manifest_bytes_ptr); + /** * Creates a C2paSigner from a callback and configuration. * @@ -534,6 +587,18 @@ struct C2paSigner *c2pa_signer_create(const void *context, const char *certs, const char *tsa_url); +/** + * Returns the size to reserve for the signature for this signer. + * + * # Parameters + * * signer_ptr: pointer to a C2paSigner. + * + * # Errors + * Returns -1 if there were errors, otherwise returns the size to reserve. + * The error string can be retrieved by calling c2pa_error. + */ +IMPORT extern int64_t c2pa_signer_reserve_size(struct C2paSigner *signer_ptr); + /** * Frees a C2paSigner allocated by Rust. * diff --git a/include/c2pa.hpp b/include/c2pa.hpp index a5d6af0..edbe267 100644 --- a/include/c2pa.hpp +++ b/include/c2pa.hpp @@ -176,9 +176,13 @@ namespace c2pa Signer(SignerFunc *callback, C2paSigningAlg alg, const string &sign_cert, const string &tsa_uri); Signer(C2paSigner *signer) : signer(signer) {} - + ~Signer(); + /// @brief Get the size to reserve for a signature for this signer. + /// @return Reserved size for the signature. + uintptr_t reserve_size(); + /// @brief Get the C2paSigner C2paSigner *c2pa_signer(); }; @@ -268,6 +272,21 @@ namespace c2pa /// @throws C2pa::Exception for errors encountered by the C2PA library. void to_archive(const path &dest_path); + /// @brief Create a hashed placeholder from the builder. + /// @param reserved_size The size required for a signature from the intended signer. + /// @param format The format of the mime type or extension. + /// @return A vector containing the hashed placeholder. + /// @throws C2pa::Exception for errors encountered by the C2PA library. + std::unique_ptr> data_hashed_placeholder(uintptr_t reserved_size, const string &format); + + /// @brief Sign a Builder using the specified signer and data hash. + /// @param signer The signer to use for signing. + /// @param data_hash The data hash to sign. + /// @param format The format of the data hash. + /// @return A vector containing the signed data. + /// @throws C2pa::Exception for errors encountered by the C2PA library. + std::unique_ptr> sign_data_hashed_embeddable(Signer &signer, const string &data_hash, const string &format); + private: // Private constructor for Builder from an archive (todo: find a better way to handle this) Builder(istream &archive); diff --git a/src/c2pa.cpp b/src/c2pa.cpp index 989018a..41e88b5 100644 --- a/src/c2pa.cpp +++ b/src/c2pa.cpp @@ -572,6 +572,12 @@ namespace c2pa return signer; } + /// @brief Get the size to reserve for a signature for this signer. + uintptr_t Signer::reserve_size() + { + return c2pa_signer_reserve_size(signer); + } + /// @brief Builder class for creating a manifest implementation. Builder::Builder(const string &manifest_json) { @@ -600,7 +606,8 @@ namespace c2pa c2pa_builder_free(builder); } - void Builder::set_no_embed() { + void Builder::set_no_embed() + { c2pa_builder_set_no_embed(builder); } @@ -662,7 +669,7 @@ namespace c2pa { CppIStream c_source = CppIStream(source); CppOStream c_dest = CppOStream(dest); - const unsigned char *c2pa_manifest_bytes = NULL; // TODO: Make returning manifest bytes optional. + const unsigned char *c2pa_manifest_bytes = NULL; auto result = c2pa_builder_sign(builder, format.c_str(), c_source.c_stream, c_dest.c_stream, signer.c2pa_signer(), &c2pa_manifest_bytes); if (result < 0) { @@ -759,4 +766,41 @@ namespace c2pa to_archive(dest); } -} \ No newline at end of file + std::unique_ptr> Builder::data_hashed_placeholder(uintptr_t reserve_size, const string &format) + { + const unsigned char *c2pa_manifest_bytes = NULL; + auto result = c2pa_builder_data_hashed_placeholder(builder, reserve_size, format.c_str(), &c2pa_manifest_bytes); + if (result < 0) + { + throw Exception(); + } + if (c2pa_manifest_bytes != NULL) + { + // Allocate a new vector on the heap and fill it with the data. + auto data = std::make_unique>(c2pa_manifest_bytes, c2pa_manifest_bytes + result); + + c2pa_manifest_bytes_free(c2pa_manifest_bytes); + return data; + } + throw(c2pa::Exception("Failed to create data hashed placeholder")); + } + + std::unique_ptr> Builder::sign_data_hashed_embeddable(Signer &signer, const string &data_hash, const string &format) + { + const unsigned char *c2pa_manifest_bytes = NULL; + auto result = c2pa_builder_sign_data_hashed_embeddable(builder, signer.c2pa_signer(), data_hash.c_str(), format.c_str(), &c2pa_manifest_bytes); + if (result < 0) + { + throw Exception(); + } + if (c2pa_manifest_bytes != NULL) + { + // Allocate a new vector on the heap and fill it with the data. + auto data = std::make_unique>(c2pa_manifest_bytes, c2pa_manifest_bytes + result); + + c2pa_manifest_bytes_free(c2pa_manifest_bytes); + return data; + } + throw(c2pa::Exception("Failed to create data hashed placeholder")); + } +} // namespace c2pa \ No newline at end of file diff --git a/src/c_api.rs b/src/c_api.rs index 67904a0..ecb656f 100644 --- a/src/c_api.rs +++ b/src/c_api.rs @@ -18,8 +18,8 @@ use std::{ // C has no namespace so we prefix things with C2PA to make them unique use c2pa::{ - settings::load_settings_from_str, Builder as C2paBuilder, CallbackSigner, Reader as C2paReader, - SigningAlg, + assertions::DataHash, settings::load_settings_from_str, Builder as C2paBuilder, CallbackSigner, + Reader as C2paReader, SigningAlg, }; use crate::{ @@ -71,6 +71,28 @@ pub struct C2paSigner { pub signer: Box, } +// Internal routine to test for null and return null error +#[macro_export] +macro_rules! null_check { + ($ptr : expr) => { + if $ptr.is_null() { + Error::set_last(Error::NullParameter(stringify!($ptr).to_string())); + return std::ptr::null_mut(); + } + }; +} + +// Internal test for null and return -1 error +#[macro_export] +macro_rules! null_check_int { + ($ptr : expr) => { + if $ptr.is_null() { + Error::set_last(Error::NullParameter(stringify!($ptr).to_string())); + return -1; + } + }; +} + // Internal routine to convert a *const c_char to a rust String or return a NULL error. #[macro_export] macro_rules! from_cstr_null_check { @@ -679,7 +701,7 @@ pub unsafe extern "C" fn c2pa_builder_to_archive( /// /// # Safety /// Reads from NULL-terminated C strings -/// If c2pa_data_ptr is not NULL, the returned value MUST be released by calling c2pa_release_string +/// If manifest_bytes_ptr is not NULL, the returned value MUST be released by calling c2pa_manifest_bytes_free /// and it is no longer valid after that call. #[no_mangle] pub unsafe extern "C" fn c2pa_builder_sign( @@ -730,6 +752,109 @@ pub unsafe extern "C" fn c2pa_manifest_bytes_free(manifest_bytes_ptr: *const c_u } } +/// Creates a hashed placeholder from a Builder. +/// The placeholder is used to reserve size in an asset for later signing. +/// +/// # Parameters +/// * builder_ptr: pointer to a Builder. +/// * reserved_size: the size required for a signature from the intended signer. +/// * format: pointer to a C string with the mime type or extension. +/// * manifest_bytes_ptr: pointer to a pointer to a c_uchar to return manifest_bytes. +/// +/// # Errors +/// Returns -1 if there were errors, otherwise returns the size of the manifest_bytes. +/// The error string can be retrieved by calling c2pa_error. +/// +/// # Safety +/// Reads from NULL-terminated C strings. +/// If manifest_bytes_ptr is not NULL, the returned value MUST be released by calling c2pa_manifest_bytes_free +/// and it is no longer valid after that call. +#[no_mangle] +pub unsafe extern "C" fn c2pa_builder_data_hashed_placeholder( + builder_ptr: *mut C2paBuilder, + reserved_size: usize, + format: *const c_char, + manifest_bytes_ptr: *mut *const c_uchar, +) -> c_int { + null_check_int!(builder_ptr); + null_check_int!(manifest_bytes_ptr); + let mut builder: Box = Box::from_raw(builder_ptr); + let format = from_cstr_null_check_int!(format); + let result = builder.data_hashed_placeholder(reserved_size, &format); + let _ = Box::into_raw(builder); + match result { + Ok(manifest_bytes) => { + let len = manifest_bytes.len() as c_int; + *manifest_bytes_ptr = + Box::into_raw(manifest_bytes.into_boxed_slice()) as *const c_uchar; + len + } + Err(err) => { + Error::from_c2pa_error(err).set_last(); + -1 + } + } +} + +/// Sign a Builder using the specified signer and data hash. +/// The data hash is a JSON string containing DataHash information for the asset. +/// This is a low-level method for advanced use cases where the caller handles embedding the manifest. +/// +/// # Parameters +/// * builder_ptr: pointer to a Builder. +/// * signer: pointer to a C2paSigner. +/// * data_hash: pointer to a C string with the JSON data hash. +/// * format: pointer to a C string with the mime type or extension. +/// * manifest_bytes_ptr: pointer to a pointer to a c_uchar to return manifest_bytes (optional, can be NULL). +/// +/// # Errors +/// Returns -1 if there were errors, otherwise returns the size of the manifest_bytes. +/// The error string can be retrieved by calling c2pa_error. +/// +/// # Safety +/// Reads from NULL-terminated C strings. +/// If manifest_bytes_ptr is not NULL, the returned value MUST be released by calling c2pa_manifest_bytes_free +/// and it is no longer valid after that call. +#[no_mangle] +pub unsafe extern "C" fn c2pa_builder_sign_data_hashed_embeddable( + builder_ptr: *mut C2paBuilder, + signer: *mut C2paSigner, + data_hash: *const c_char, + format: *const c_char, + manifest_bytes_ptr: *mut *const c_uchar, +) -> c_int { + null_check_int!(builder_ptr); + null_check_int!(manifest_bytes_ptr); + + let mut builder: Box = Box::from_raw(builder_ptr); + let c2pa_signer = Box::from_raw(signer); + let data_hash_json = from_cstr_null_check_int!(data_hash); + let data_hash: DataHash = match serde_json::from_str(&data_hash_json) { + Ok(data_hash) => data_hash, + Err(err) => { + Error::from_c2pa_error(c2pa::Error::JsonError(err)).set_last(); + return -1; + } + }; + let format = from_cstr_null_check_int!(format); + let result = + builder.sign_data_hashed_embeddable(c2pa_signer.signer.as_ref(), &data_hash, &format); + let _ = Box::into_raw(c2pa_signer); + let _ = Box::into_raw(builder); + match result { + Ok(manifest_bytes) => { + let len = manifest_bytes.len() as c_int; + *manifest_bytes_ptr = + Box::into_raw(manifest_bytes.into_boxed_slice()) as *const c_uchar; + len + } + Err(err) => { + Error::from_c2pa_error(err).set_last(); + -1 + } + } +} + /// Creates a C2paSigner from a callback and configuration. /// /// # Parameters @@ -796,6 +921,26 @@ pub unsafe extern "C" fn c2pa_signer_create( })) } +/// Returns the size to reserve for the signature for this signer. +/// +/// # Parameters +/// * signer_ptr: pointer to a C2paSigner. +/// +/// # Errors +/// Returns -1 if there were errors, otherwise returns the size to reserve. +/// The error string can be retrieved by calling c2pa_error. +#[no_mangle] +pub unsafe extern "C" fn c2pa_signer_reserve_size(signer_ptr: *mut C2paSigner) -> i64 { + if signer_ptr.is_null() { + Error::set_last(Error::NullParameter(stringify!($ptr).to_string())); + return -1; + } + let c2pa_signer: Box = Box::from_raw(signer_ptr); + let size = c2pa_signer.signer.reserve_size() as i64; + let _ = Box::into_raw(c2pa_signer); + return size; +} + /// Frees a C2paSigner allocated by Rust. /// /// # Safety diff --git a/tests/builder.test.cpp b/tests/builder.test.cpp index 0919cee..b3046d7 100644 --- a/tests/builder.test.cpp +++ b/tests/builder.test.cpp @@ -131,11 +131,11 @@ TEST(Builder, SignStreamCloudUrl) auto builder = c2pa::Builder(manifest); - // very important to use a URL that does not exist, otherwise you may get a JumbfParseError or JumbfNotFound + // very important to use a URL that does not exist, otherwise you may get a JumbfParseError or JumbfNotFound builder.set_remote_url("http://this_does_not_exist/foo.jpg"); builder.set_no_embed(); - //auto manifest_data = builder.sign(signed_image_path, "target/dest.jpg", signer); + // auto manifest_data = builder.sign(signed_image_path, "target/dest.jpg", signer); std::ifstream source(signed_image_path, std::ios::binary); if (!source) { @@ -156,10 +156,54 @@ TEST(Builder, SignStreamCloudUrl) catch (c2pa::Exception const &e) { std::string error_message = e.what(); - if (error_message.rfind("Remote ", 0) == 0) { + if (error_message.rfind("Remote ", 0) == 0) + { SUCCEED(); - } else { + } + else + { FAIL() << "Failed: C2pa::Builder: " << e.what() << endl; } }; +} + +TEST(Builder, SignDataHashedEmbedded) +{ + try + { + fs::path current_dir = fs::path(__FILE__).parent_path(); + + // Construct the paths relative to the current directory + fs::path manifest_path = current_dir / "../tests/fixtures/training.json"; + fs::path certs_path = current_dir / "../tests/fixtures/es256_certs.pem"; + // fs::path signed_image_path = current_dir / "../tests/fixtures/A.jpg"; + + auto manifest = read_text_file(manifest_path); + auto certs = read_text_file(certs_path); + + // create a signer + c2pa::Signer signer = c2pa::Signer(&test_signer, Es256, certs, "http://timestamp.digicert.com"); + + auto builder = c2pa::Builder(manifest); + + auto placeholder = builder.data_hashed_placeholder(signer.reserve_size(), "image/jpeg"); + + std::string data_hash = R"({ + "exclusions": [ + { + "start": 20, + "length": 45884 + } + ], + "name": "jumbf manifest", + "alg": "sha256", + "hash": "gWZNEOMHQNiULfA/tO5HD2awOwYMA3tnfUPApIr9csk=", + "pad": " " + })"; + auto manifest_data = builder.sign_data_hashed_embeddable(signer, data_hash, "image/jpeg"); + } + catch (c2pa::Exception const &e) + { + FAIL() << "Failed: C2pa::Builder: " << e.what() << endl; + }; } \ No newline at end of file diff --git a/tests/fixtures/training.json b/tests/fixtures/training.json index 1093019..3373ff9 100644 --- a/tests/fixtures/training.json +++ b/tests/fixtures/training.json @@ -1,9 +1,8 @@ { - "claim_generator": "c2pa-c_test/0.1", "claim_generator_info": [ { "name": "c2pa-c test", - "version": "0.1" + "version": "0.2" } ], "assertions": [ @@ -11,16 +10,16 @@ "label": "c2pa.training-mining", "data": { "entries": { - "c2pa.ai_generative_training": { + "cawg.ai_generative_training": { "use": "notAllowed" }, - "c2pa.ai_inference": { + "cawg.ai_inference": { "use": "notAllowed" }, - "c2pa.ai_training": { + "cawg.ai_training": { "use": "notAllowed" }, - "c2pa.data_mining": { + "cawg.data_mining": { "use": "notAllowed" } } From e93919037344d0268d58df71236dc88136e4f7c5 Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Thu, 14 Nov 2024 09:30:40 -0800 Subject: [PATCH 3/5] fix clippy issues --- include/c2pa.h | 3 +++ src/c_api.rs | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/include/c2pa.h b/include/c2pa.h index 89491a2..4e7b49d 100644 --- a/include/c2pa.h +++ b/include/c2pa.h @@ -596,6 +596,9 @@ struct C2paSigner *c2pa_signer_create(const void *context, * # Errors * Returns -1 if there were errors, otherwise returns the size to reserve. * The error string can be retrieved by calling c2pa_error. + * + * # Safety + * The signer_ptr must be a valid pointer to a C2paSigner. */ IMPORT extern int64_t c2pa_signer_reserve_size(struct C2paSigner *signer_ptr); diff --git a/src/c_api.rs b/src/c_api.rs index ecb656f..4fd7aa7 100644 --- a/src/c_api.rs +++ b/src/c_api.rs @@ -929,6 +929,9 @@ pub unsafe extern "C" fn c2pa_signer_create( /// # Errors /// Returns -1 if there were errors, otherwise returns the size to reserve. /// The error string can be retrieved by calling c2pa_error. +/// +/// # Safety +/// The signer_ptr must be a valid pointer to a C2paSigner. #[no_mangle] pub unsafe extern "C" fn c2pa_signer_reserve_size(signer_ptr: *mut C2paSigner) -> i64 { if signer_ptr.is_null() { @@ -938,7 +941,7 @@ pub unsafe extern "C" fn c2pa_signer_reserve_size(signer_ptr: *mut C2paSigner) - let c2pa_signer: Box = Box::from_raw(signer_ptr); let size = c2pa_signer.signer.reserve_size() as i64; let _ = Box::into_raw(c2pa_signer); - return size; + size } /// Frees a C2paSigner allocated by Rust. From db511bd95eb5e75f5aed52355ee47c44dc5ee1da Mon Sep 17 00:00:00 2001 From: Gavin Peacock Date: Thu, 14 Nov 2024 16:09:55 -0800 Subject: [PATCH 4/5] chore: update to cawg.training-mining label --- tests/fixtures/training.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/training.json b/tests/fixtures/training.json index 3373ff9..1f4f211 100644 --- a/tests/fixtures/training.json +++ b/tests/fixtures/training.json @@ -7,7 +7,7 @@ ], "assertions": [ { - "label": "c2pa.training-mining", + "label": "cawg.training-mining", "data": { "entries": { "cawg.ai_generative_training": { From 5a312db07cc5b3be83c89580b59aca3b29b16619 Mon Sep 17 00:00:00 2001 From: Dave Stein Date: Fri, 15 Nov 2024 10:43:29 -0500 Subject: [PATCH 5/5] chore: Updating ticket status based on labels --- .../{closing-ticket.yml => closing_ticket.yml} | 2 +- .github/workflows/labeling_ticket_done.yml | 17 +++++++++++++++++ .github/workflows/labeling_ticket_todo.yml | 17 +++++++++++++++++ ...eopening-ticket.yml => reopening_ticket.yml} | 2 +- 4 files changed, 36 insertions(+), 2 deletions(-) rename .github/workflows/{closing-ticket.yml => closing_ticket.yml} (96%) create mode 100644 .github/workflows/labeling_ticket_done.yml create mode 100644 .github/workflows/labeling_ticket_todo.yml rename .github/workflows/{reopening-ticket.yml => reopening_ticket.yml} (96%) diff --git a/.github/workflows/closing-ticket.yml b/.github/workflows/closing_ticket.yml similarity index 96% rename from .github/workflows/closing-ticket.yml rename to .github/workflows/closing_ticket.yml index 1a24b0a..ef8f6dc 100644 --- a/.github/workflows/closing-ticket.yml +++ b/.github/workflows/closing_ticket.yml @@ -1,4 +1,4 @@ -name: closing-ticket +name: Closing ticket on: issues: types: [closed] diff --git a/.github/workflows/labeling_ticket_done.yml b/.github/workflows/labeling_ticket_done.yml new file mode 100644 index 0000000..3337393 --- /dev/null +++ b/.github/workflows/labeling_ticket_done.yml @@ -0,0 +1,17 @@ +name: Labeling ticket "Done" +on: + issues: + types: [labeled] +jobs: + label_issues: + runs-on: ubuntu-latest + if: | + contains(github.event.issue.labels.*.name, 'status: done') + permissions: + issues: write + steps: + - run: 'gh issue close "$NUMBER"' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} diff --git a/.github/workflows/labeling_ticket_todo.yml b/.github/workflows/labeling_ticket_todo.yml new file mode 100644 index 0000000..982b27f --- /dev/null +++ b/.github/workflows/labeling_ticket_todo.yml @@ -0,0 +1,17 @@ +name: Labeling ticket "To Do" +on: + issues: + types: [labeled] +jobs: + label_issues: + runs-on: ubuntu-latest + if: | + !contains(github.event.issue.labels.*.name, 'status: done') + permissions: + issues: write + steps: + - run: 'gh issue reopen "$NUMBER"' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} diff --git a/.github/workflows/reopening-ticket.yml b/.github/workflows/reopening_ticket.yml similarity index 96% rename from .github/workflows/reopening-ticket.yml rename to .github/workflows/reopening_ticket.yml index 0cd3763..d64e991 100644 --- a/.github/workflows/reopening-ticket.yml +++ b/.github/workflows/reopening_ticket.yml @@ -1,4 +1,4 @@ -name: reopening-ticket +name: Reopening ticket on: issues: types: [reopened]