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] diff --git a/Cargo.toml b/Cargo.toml index ce68bdf..d8e567d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,10 @@ serde_derive = "1.0" serde_json = "1.0" thiserror = "1.0.64" +[dev-dependencies] +url = "2.2.2" + + [profile.release] strip = true # Strip symbols from the output binary. lto = true # Enable link-time optimization. diff --git a/include/c2pa.h b/include/c2pa.h index 715afea..e41b695 100644 --- a/include/c2pa.h +++ b/include/c2pa.h @@ -370,6 +370,34 @@ IMPORT extern struct C2paBuilder *c2pa_builder_from_archive(struct CStream *stre */ IMPORT extern void c2pa_builder_free(struct C2paBuilder *builder_ptr); +/** + * Sets the no-embed flag on the Builder. + * When set, the builder will not embed a C2PA manifest store into the asset when signing. + * This is useful when creating cloud or sidecar manifests. + * # Parameters + * * builder_ptr: pointer to a Builder. + * # Safety + * builder_ptr must be a valid pointer to a Builder. + */ +IMPORT extern void c2pa_builder_set_no_embed(struct C2paBuilder *builder_ptr); + +/** + * 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. + */ +IMPORT extern +int c2pa_builder_set_remote_url(struct C2paBuilder *builder_ptr, + const char *remote_url); + /** * Adds a resource to the C2paBuilder. * @@ -408,10 +436,10 @@ int c2pa_builder_add_resource(struct C2paBuilder *builder_ptr, * Reads from NULL-terminated C strings. */ IMPORT extern -int c2pa_builder_add_ingredient(struct C2paBuilder *builder_ptr, - const char *ingredient_json, - const char *format, - struct CStream *source); +int c2pa_builder_add_ingredient_from_stream(struct C2paBuilder *builder_ptr, + const char *ingredient_json, + const char *format, + struct CStream *source); /** * Writes an Archive of the Builder to the destination stream. @@ -454,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 @@ -473,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. * @@ -506,6 +587,21 @@ 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. + * + * # Safety + * The signer_ptr must be a valid pointer to a C2paSigner. + */ +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 8ba02b0..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(); }; @@ -198,6 +202,14 @@ namespace c2pa ~Builder(); + /// @brief Set the no embed flag. + void set_no_embed(); + + /// @brief Set the remote URL. + /// @param remote_url The remote URL to set. + /// @throws C2pa::Exception for errors encountered by the C2PA library. + void set_remote_url(const string &remote_url); + /// @brief Add a resource to the builder. /// @param uri The uri of the resource. /// @param source The input stream to read the resource from. @@ -260,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 df31e7e..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,6 +606,20 @@ namespace c2pa c2pa_builder_free(builder); } + void Builder::set_no_embed() + { + c2pa_builder_set_no_embed(builder); + } + + void Builder::set_remote_url(const string &remote_url) + { + int result = c2pa_builder_set_remote_url(builder, remote_url.c_str()); + if (result < 0) + { + throw Exception(); + } + } + void Builder::add_resource(const string &uri, istream &source) { CppIStream c_source = CppIStream(source); @@ -623,7 +643,7 @@ namespace c2pa void Builder::add_ingredient(const string &ingredient_json, const string &format, istream &source) { CppIStream c_source = CppIStream(source); - int result = c2pa_builder_add_ingredient(builder, ingredient_json.c_str(), format.c_str(), c_source.c_stream); + int result = c2pa_builder_add_ingredient_from_stream(builder, ingredient_json.c_str(), format.c_str(), c_source.c_stream); if (result < 0) { throw Exception(); @@ -649,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) { @@ -746,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 456ee46..4fd7aa7 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 { @@ -515,6 +537,43 @@ pub unsafe extern "C" fn c2pa_builder_free(builder_ptr: *mut C2paBuilder) { } } +/// Sets the no-embed flag on the Builder. +/// When set, the builder will not embed a C2PA manifest store into the asset when signing. +/// This is useful when creating cloud or sidecar manifests. +/// # Parameters +/// * builder_ptr: pointer to a Builder. +/// # Safety +/// builder_ptr must be a valid pointer to a Builder. +#[no_mangle] +pub unsafe extern "C" fn c2pa_builder_set_no_embed(builder_ptr: *mut C2paBuilder) { + let mut builder: Box = 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 +624,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, @@ -642,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( @@ -693,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 @@ -759,6 +921,29 @@ 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. +/// +/// # 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() { + 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); + size +} + /// Frees a C2paSigner allocated by Rust. /// /// # Safety 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..b3046d7 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,7 +106,103 @@ 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; + } + }; +} + +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; }; 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/fixtures/training.json b/tests/fixtures/training.json index 1093019..1f4f211 100644 --- a/tests/fixtures/training.json +++ b/tests/fixtures/training.json @@ -1,26 +1,25 @@ { - "claim_generator": "c2pa-c_test/0.1", "claim_generator_info": [ { "name": "c2pa-c test", - "version": "0.1" + "version": "0.2" } ], "assertions": [ { - "label": "c2pa.training-mining", + "label": "cawg.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" } } 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"); + } +}