commit 1a2711a3adaa648c2a595b1d6a1885ee4f9f40d5
parent b5ce33ffa5b9d65205868b3763815e20497059d9
Author: Gavin Peacock <gpeacock@adobe.com>
Date: Mon, 22 Apr 2024 22:15:27 -0700
(MINOR) Initial V2 API work (#437)
* Adds ManifestStoreBuilder as a simplified way to construct manifest stores
adds preserve_order feature to serde_json
adds serde_with
* Add make_testImages ClaimGeneratorInfo
* CAIRead/Write blanket Impl
Also Send for Wasm
* ResourceStore ResourceResolver open
returns cursor
* Adds Ingredient from_json and add_stream
* ManifestStoreBuilder zip/unzip and ingredient stuff
* resource cursor fix
* Adds Stream read/write of resources
Adds v2api.rs example
adds write_stream to ResourceStore
add get_resource to ManifestStore
add add_resource to ManifestStoreBuilder
add ingredient folders to ManifestStoreBuilder zips
* Added api_notes
* Adds add_assertion to ManifestStoreBuilder
* update make_test_images with v2 API
* Added manifest comparison to make_test_images
added json_manifests folder with known good json manifests
Fixed exposed issues:
default claim_generator to c2pa-rs instead of c2pa
add default ingredient instance_id
ManifestStore is now deserializable
* Adds a sign_remote method
not tested for wasm yet
* Allow file comparison
make_test_images can take two parameters to just compare
* some cleanup in make_test_images
and v2api
* Added goals to V2_API_NOTES
* remove wrap_openssl from ed_signer
* Adds create_callback_signer
supports signers that don't need to know about private keys
* Adds format_to_mime function
format can be an extension or a mime type
* fix default mime type in get_path_info
it was incorrectly using application/binary
* Add ManifestStore::from_stream_async
* Remove automatic validation when signing
* V2 API update
split out a v2_api folder/subcrate
rename ManifestStoreBuilder to Builder
Add Reader for ManifestStore
Add C2pa struct
update make_test_images to v2
update v2api example
add v2_api integration with ed signing (works in wasm)
* Adds openssl dependency to help vscode testing
* add short "tif" extension to extension_to_mime and format_to_extension
* v2 api updates
Builder has a manifest definition
signer callback is now a function trait instead of a struct trait
converts format extensions to mime types on sign
Work in progress on tiff (streams don't work yet)
adds c2pa type stream write
Reader::from_stream function added
* adds Reader:from_c2pa_data_and_stream
adds display/to_string support for Reader
adds new() to Reader
adds format_from_path function
adds v2_show example
* Fix extension missing bug in make_test_images
* support stand alone openssl
without open_ssl sign and without file_io
build make_test_images without file_io feature
* Builder/Reader model share ingredient resources with manifest
clippy and build fixes
* export load_settings_from_str
* Add unit test for wav streams
* riff_io clippy things
* add "sign" recipe to make_test_images
allows signing any supported file type
* types script to test supported types
* add test_builder_sign_assets
attempts to sing supported types with v2
* remove C2pa struct from v2 integration test
* introduces a v1_api feature required to export v1
apis. This is a breaking change to the sdk since the feature is required
for backward compatibility
* add Builder.sign_file
wasm builds with v1
* update create manifest example
* Build v2 only library that can generate docs
* remote wrap_openssl_error
* wasm build working,
Reader.active_manifest -> active, resource -> resource_to_stream
* integrate with main 0.31.1
* Start of an integration testing framework with manifest comparison
* Refactor for integration into main
v1_api set to default for now
use no_default_features to test v2 only build
Eventually v1_apis will be deprecated
* Some cleanup work
* Remove CAIRead and CAIReadWrite from the public interface
now uses mut impl WRITE + READ + SEEK + SEND.
* export CallbackSigner, remove create_callback_signer
add a context parameter to the callback
add back Ingredient DefaultOptions and IngredientOptions (for now)
manifestAssertion::from_assertion made public for now
* Add a set_thumbnail method to Builder
* More signer work and external thumbnail verification
Add SignerContext.
add Ingredient returns ingredient
make_test_images generates it's own thumbnails
* callback context and v1 api alignment
* PR feedback and update json_manifests
* fix build by removing experimental make_test_images formats
* CI cargo udeps fixes
* update tempfile version
* fix the case of Sample1.png
* fix case of TUSCANY.TIF test file
* Some comment cleanup
* PR Feedback changes
Cleaned up docs with Sentence style.
removed v2_api folder.
renamed reader
.get to get_manifest
zip to to_archive
unzip tp from_archive
active to active_manifest
Added
Reader::from_stream_async
Builder.sign_async
Singer. direct_cose_handling() - allows remote signer
TestAsyncRemoteSigner() to test above.
Removed
Reader:new()
Builder.sign_remote()
* remove commented out code
* remove debug print
* more PR feedback cleanup
* box_hash test nit
* box_hash test fix
* This puts the Reader and Builder behind an unstable_api feature.
* Add comment about using the unstable_api feature.
* cleanup V2_API_NOTES, remove Signing Scenarios
* fmt
* PR fixes
Diffstat:
83 files changed, 7532 insertions(+), 542 deletions(-)
diff --git a/V2_API_NOTES.md b/V2_API_NOTES.md
@@ -0,0 +1,70 @@
+## V2 API Notes
+
+### Goals
+Provide a consistent flexible well tested API focusing on core functionality.
+
+- Move toward a JSON + binary resources model that ports well to multiple languages.
+- Eliminate multiple variations of functions for file/memory/stream, sync/async & etc.
+- Have one stream based version of each function that works sync and async.
+- Design APIs keeping in mind support for multiple language bindings.
+- Enable sign only/verify only and no openssl configuration.
+- Support Box Hash and Data Hashed signing models.
+- Enable builds for cameras and other embedded environments.
+- Provide a consistent model for setting runtime options.
+- Write unit tests, integration tests and documentation for all the v2 APIs.
+- Keep v1 to v2 porting as simple as possible.
+
+
+### Resource References
+A resource reference in the API is associated with a HashedUri as a superset.
+- The c2pa spec refers to both a hashed-uri-map and a hashed-ext-uri-map
+- In some cases either one can be used.
+- The resource reference is a superset of both.
+- It also adds local references to things like the filesystem or any abstracted storage.
+I've been using the identifier field to distinguish from the url field, but they are really the same. However, the spec will only allow for JUMBF and http/https references, so if the external identifier is not http/https, it must be converted to
+a JUMBF reference before embedding into a manifest.
+
+When defining a resource for the ManifestStoreBuilder, existing resources in other manifests may be identified via JUMBF urls. This allows a new manifest to inherit an existing thumbnail and is also used to reference parent ingredients. The API will generally do this resolution as needed so users do not need to know about JUMBF URL referencing on manifest creation.
+
+The spec will often require adding a hashed-uri to an assertion. Since the JUMBF uris for a new manifest are not known when defining the manifest, this creates a chicken and egg scenario. We resolve this with local resource references. When constructing the JUMBF for a manifest, the api will convert all local uri references into JUMBF references and fixup the associated cross references.
+
+URI schemes in a resource reference could take the following forms:
+- self#jumbf= an internal JUMBF reference
+- file:/// a local file reference
+- app://contentauth/ a working store reference
+- http:// remote uri
+- https:// remote secure uri
+
+Note that the file: and app: schemes are only used in the context of ManifestStoreBuilder and will never be in JUMBF data. This is proposal, currently there is no implementation for file or app schemes and we do not yet handle http/https schemes this way.
+
+Lack of a scheme will be interpreted as a file:/// reference when file_io is enabled, otherwise as an app: reference.
+
+### Source asset vs Parent asset
+- The source asset isn't always the parent asset.
+The source asset is the asset that we will hash and sign. It can the output from an editing application that has not preserved the manifest store from the parent. In that case the application should have extracted a parent ingredient from the parent asset and added that to the manifest definition.
+
+- Parent asset: with a manifest store.
+- Parent ingredient: generated from that parent asset (hashed and validated)
+- Source asset: may be a generated rendition after edits from the parent asset (manifest?)
+- Signed output which will include the source asset, and the new manifest store with parent ingredient.
+
+If there is no parent ingredient defined, and the source has a manifest store, the sdk will generate a parent ingredient from the parent.
+
+## Testing
+We need a more comprehensive set of tests for the rust codebase.
+
+The plan is to build a solid set of tests on the new streams based API.
+Then we will build everything else on top of that as stable base.
+The current set of unit tests are helpful but many are out of date.
+I've had a long standing issue to generate the test images from clean non-c2pa images.
+When we check in images with manifests, they rapidly get out of date.
+We do need some set of older manifests and third party images to test with
+but I'm not sure if those need to be in the SDK.
+
+- A test assets folder with one public domain image in each asset format we support.
+- A tool, like make_test_images, to generate different kinds of manifests for testing.
+We should maintain an archive of the manifest_store json generated by the previous build
+and compare the old build with the new ones for any significant deltas.
+The tool needs to ignore changes due to new GUIDs, dates, and json object field order.
+
+The make_test_images crate has been updated to do this by default. We may make a policy to run the test comparison nightly.
diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml
@@ -8,14 +8,13 @@ rust-version = "1.73.0"
[dependencies]
anyhow = "1.0.40"
-blake3 = "1.5.0"
-c2pa = { path = "../sdk", features = [
- "add_thumbnails",
- "file_io",
+c2pa = { path = "../sdk", default-features = false, features = [
+ "openssl",
"xmp_write",
+ "unstable_api",
] }
env_logger = "0.10"
-log = "0.4.8"
+log = "0.4.8"
image = { version = "0.24.7", default-features = false, features = [
"jpeg",
"png",
@@ -24,4 +23,6 @@ memchr = "2.7.1"
nom = "7.1.3"
regex = "1.5.6"
serde = "1.0.197"
-serde_json = "1.0.114"
+serde_json = { version = "1.0.114", features = ["preserve_order"] }
+tempfile = "3.10.1"
+
diff --git a/make_test_images/json_manifests/C.json b/make_test_images/json_manifests/C.json
@@ -0,0 +1,64 @@
+{
+ "active_manifest": "contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b",
+ "manifests": {
+ "contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "C.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:8280ad85-e3d5-4954-b2f2-3f62de10072f",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:38+00:00"
+ },
+ "label": "contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CA.json b/make_test_images/json_manifests/CA.json
@@ -0,0 +1,84 @@
+{
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CACA.json b/make_test_images/json_manifests/CACA.json
@@ -0,0 +1,163 @@
+{
+ "active_manifest": "contentauth:urn:uuid:64bf5b80-9b85-4143-a6df-95247e04ed1f",
+ "manifests": {
+ "contentauth:urn:uuid:64bf5b80-9b85-4143-a6df-95247e04ed1f": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CACA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:af80d89c-6cf5-4294-87a3-ecb920e40e28",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:6dfba703-cbd9-4ed9-aa58-4a45e2969888",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "parentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp:iid:6dfba703-cbd9-4ed9-aa58-4a45e2969888",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "XA79Bd5PN1RbcVRajJpOirW/8l9YqK0WectohsIzUYk="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:64bf5b80-9b85-4143-a6df-95247e04ed1f"
+ },
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CACAICAICICA.json b/make_test_images/json_manifests/CACAICAICICA.json
@@ -0,0 +1,374 @@
+{
+ "active_manifest": "contentauth:urn:uuid:6d9a42d6-00b3-478e-9ae4-3104b0ee0683",
+ "manifests": {
+ "contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:f030e95f-c7c0-451f-b5ce-15bcfe104e49",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:347fe660-8c73-4af8-b05f-497ab7fc52b5",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:347fe660-8c73-4af8-b05f-497ab7fc52b5",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "TFKjel4QMJfdash9gAQD7c7qd8s60EawLHtO/TEmQWo="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87"
+ },
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:1475b2b2-3f08-413b-bec6-23af4efa3d9a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:cc7c1620-c296-474c-a766-ab068820f612",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:cc7c1620-c296-474c-a766-ab068820f612",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "1YwxyCCm/HtPiFFJGfcdzaSEXIuQjxpfGydtq2nfk6w="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:41+00:00"
+ },
+ "label": "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55"
+ },
+ "contentauth:urn:uuid:6d9a42d6-00b3-478e-9ae4-3104b0ee0683": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CACAICAICICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:b98ac89f-8cdd-40ac-9620-1822844aba76",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "CAICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:6857372a-729e-405c-bb91-045b07cc7c9f",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "parentOf",
+ "active_manifest": "contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87"
+ },
+ {
+ "title": "CICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:1e5ac050-daeb-4a1d-80a6-0ee2bac7707f",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp:iid:6857372a-729e-405c-bb91-045b07cc7c9f",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "lSOHeAfGe5YskunRUaZlu1GIHFYMPp6/tJIAcA59K9s="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:1e5ac050-daeb-4a1d-80a6-0ee2bac7707f",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "6VU/JgsRsJ45WYkSsmFRWDUU5PiOg1EZ4QMnmzcApGU="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:41+00:00"
+ },
+ "label": "contentauth:urn:uuid:6d9a42d6-00b3-478e-9ae4-3104b0ee0683"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CAI.json b/make_test_images/json_manifests/CAI.json
@@ -0,0 +1,108 @@
+{
+ "active_manifest": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911",
+ "manifests": {
+ "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:07d45e4a-7dff-4494-8282-44eac8eeeba6",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient__1.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "EMeeY5a+lvy1msl+9i5DOcOoeQowrqD7NyV0d8fwAX0="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CAIAIIICAICIICAIICICA.json b/make_test_images/json_manifests/CAIAIIICAICIICAIICICA.json
@@ -0,0 +1,632 @@
+{
+ "active_manifest": "contentauth:urn:uuid:fc5ec1b6-5578-430b-844e-fdec568f5e5f",
+ "manifests": {
+ "contentauth:urn:uuid:fc5ec1b6-5578-430b-844e-fdec568f5e5f": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAIAIIICAICIICAIICICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:eaaf5034-dd49-452a-a2b9-b2a22cf7a1ac",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "C.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:c14601cb-bc15-4f86-9537-77f2efa1f45b",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b"
+ },
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient__1.jpeg"
+ },
+ "relationship": "componentOf"
+ },
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:e88bb690-796c-410f-825d-f334d5f2ec81",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ {
+ "title": "CI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:bffa9fda-119d-4f57-8b1c-2ebdc3fcd0fb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b"
+ },
+ {
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:2f9fd118-8833-4541-9d98-9358a16682e6",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ },
+ {
+ "title": "CICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:9f89f2c9-abf9-4311-890b-dc981453dedf",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:c14601cb-bc15-4f86-9537-77f2efa1f45b",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "z/iTDs+JHYijf953d4KY7auLJR8ecnWJJobOXsMIDUE="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__2",
+ "hash": "lI6ts4SctCpnRKCaW7SyS+vQTai1kq3EsRQejzvtvTE="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:e88bb690-796c-410f-825d-f334d5f2ec81",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__3",
+ "hash": "xp0PWyJld12ya4nSi87pKfPEKbVXRKyNdZ0OSZP/R6s="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:bffa9fda-119d-4f57-8b1c-2ebdc3fcd0fb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__4",
+ "hash": "aX4lDofivAyDiRaGufNFUPVX8DYQBOVe0yMTduSwc08="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:2f9fd118-8833-4541-9d98-9358a16682e6",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__5",
+ "hash": "X6Ot3qiCwK2p25FQfWQe5pxthCWMreRj9DJLt/pI6qw="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:9f89f2c9-abf9-4311-890b-dc981453dedf",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__6",
+ "hash": "7vkiAIPmzTyOYRlGE2x3Omgm3SO2kLnNJj8HjsCVpg4="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:42+00:00"
+ },
+ "label": "contentauth:urn:uuid:fc5ec1b6-5578-430b-844e-fdec568f5e5f"
+ },
+ "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:1475b2b2-3f08-413b-bec6-23af4efa3d9a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:cc7c1620-c296-474c-a766-ab068820f612",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:cc7c1620-c296-474c-a766-ab068820f612",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "1YwxyCCm/HtPiFFJGfcdzaSEXIuQjxpfGydtq2nfk6w="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:41+00:00"
+ },
+ "label": "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55"
+ },
+ "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:7a242815-d261-4e69-87b7-34e51a662d32",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "LhHr79j69sagdiQpq9AfBwH+rzMmkO8LM3tC5IRAXaI="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b"
+ },
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ "contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "C.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:8280ad85-e3d5-4954-b2f2-3f62de10072f",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:38+00:00"
+ },
+ "label": "contentauth:urn:uuid:12a34759-5309-45b6-b4c8-068d341adf2b"
+ },
+ "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:07d45e4a-7dff-4494-8282-44eac8eeeba6",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient__1.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "EMeeY5a+lvy1msl+9i5DOcOoeQowrqD7NyV0d8fwAX0="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CAICA.json b/make_test_images/json_manifests/CAICA.json
@@ -0,0 +1,187 @@
+{
+ "active_manifest": "contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ "contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:f030e95f-c7c0-451f-b5ce-15bcfe104e49",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:347fe660-8c73-4af8-b05f-497ab7fc52b5",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:347fe660-8c73-4af8-b05f-497ab7fc52b5",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "TFKjel4QMJfdash9gAQD7c7qd8s60EawLHtO/TEmQWo="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:76ab5e58-6cef-48e1-9739-03040de0ff87"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CAICAI.json b/make_test_images/json_manifests/CAICAI.json
@@ -0,0 +1,211 @@
+{
+ "active_manifest": "contentauth:urn:uuid:3d91b8cf-72eb-40da-b479-7e97483fcc9f",
+ "manifests": {
+ "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:07d45e4a-7dff-4494-8282-44eac8eeeba6",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient__1.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "EMeeY5a+lvy1msl+9i5DOcOoeQowrqD7NyV0d8fwAX0="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ },
+ "contentauth:urn:uuid:3d91b8cf-72eb-40da-b479-7e97483fcc9f": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAICAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:ee4737e3-3a4e-4b64-b14f-e7bcd6710ea4",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:baccbd98-0680-480a-b9e3-61dfa725aebc",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:baccbd98-0680-480a-b9e3-61dfa725aebc",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "WdHBvgu1goVcT7CAL3KpY++gLtQ0Rq32x1JqErpJw9c="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:41+00:00"
+ },
+ "label": "contentauth:urn:uuid:3d91b8cf-72eb-40da-b479-7e97483fcc9f"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CI.json b/make_test_images/json_manifests/CI.json
@@ -0,0 +1,89 @@
+{
+ "active_manifest": "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b",
+ "manifests": {
+ "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:7a242815-d261-4e69-87b7-34e51a662d32",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "LhHr79j69sagdiQpq9AfBwH+rzMmkO8LM3tC5IRAXaI="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CICA.json b/make_test_images/json_manifests/CICA.json
@@ -0,0 +1,168 @@
+{
+ "active_manifest": "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55",
+ "manifests": {
+ "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CICA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:1475b2b2-3f08-413b-bec6-23af4efa3d9a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:cc7c1620-c296-474c-a766-ab068820f612",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:cc7c1620-c296-474c-a766-ab068820f612",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "1YwxyCCm/HtPiFFJGfcdzaSEXIuQjxpfGydtq2nfk6w="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:41+00:00"
+ },
+ "label": "contentauth:urn:uuid:14792ae1-5bce-45c2-ba4a-ba122865cd55"
+ },
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CICACACA.json b/make_test_images/json_manifests/CICACACA.json
@@ -0,0 +1,188 @@
+{
+ "active_manifest": "contentauth:urn:uuid:1f2bfd18-0b07-4778-b50c-477dce7c7cc5",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ "contentauth:urn:uuid:1f2bfd18-0b07-4778-b50c-477dce7c7cc5": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CICACACA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:9c6442ea-ec6b-42e4-99bd-4b9e68e1a392",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:8c0ff60b-90a3-4f3a-84b9-9662c3826964",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:8c0ff60b-90a3-4f3a-84b9-9662c3826964",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "nI04fnFvqGDGW/M44bWaHX9ZwnFeHpAHOatwxb3yW3o="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:8c0ff60b-90a3-4f3a-84b9-9662c3826964",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "nI04fnFvqGDGW/M44bWaHX9ZwnFeHpAHOatwxb3yW3o="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:8c0ff60b-90a3-4f3a-84b9-9662c3826964",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "nI04fnFvqGDGW/M44bWaHX9ZwnFeHpAHOatwxb3yW3o="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:42+00:00"
+ },
+ "label": "contentauth:urn:uuid:1f2bfd18-0b07-4778-b50c-477dce7c7cc5"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CIE-sig-CA.json b/make_test_images/json_manifests/CIE-sig-CA.json
@@ -0,0 +1,179 @@
+{
+ "active_manifest": "contentauth:urn:uuid:e806386e-a44f-4a7c-8518-23885c307560",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_xxxxxx/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ "contentauth:urn:uuid:e806386e-a44f-4a7c-8518-23885c307560": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CIE-sig-CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:4dc278df-414b-43bf-880d-43c7c52d2605",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "E-sig-CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:6f5bc756-9cc7-4b7a-89d9-602ea6bbe665",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "validation_status": [
+ {
+ "code": "timeStamp.mismatch",
+ "url": "Cose_Sign1",
+ "explanation": "timestamp message imprint did not match"
+ },
+ {
+ "code": "claimSignature.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.signature",
+ "explanation": "claim signature is not valid"
+ }
+ ]
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:6f5bc756-9cc7-4b7a-89d9-602ea6bbe665",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "hh9ensrjGUtbGScwKSk7ll9U1fDmM2PEe3UIQhyvrL4="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:42+00:00"
+ },
+ "label": "contentauth:urn:uuid:e806386e-a44f-4a7c-8518-23885c307560"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/CII.json b/make_test_images/json_manifests/CII.json
@@ -0,0 +1,99 @@
+{
+ "active_manifest": "contentauth:urn:uuid:ac2f5ad8-c031-4c57-86aa-03f53ae021c9",
+ "manifests": {
+ "contentauth:urn:uuid:ac2f5ad8-c031-4c57-86aa-03f53ae021c9": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CII.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:3d6ce559-af88-444c-808a-1e3ece74d175",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "LhHr79j69sagdiQpq9AfBwH+rzMmkO8LM3tC5IRAXaI="
+ }
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "LhHr79j69sagdiQpq9AfBwH+rzMmkO8LM3tC5IRAXaI="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:ac2f5ad8-c031-4c57-86aa-03f53ae021c9"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/E-clm-CAICAI.json b/make_test_images/json_manifests/E-clm-CAICAI.json
@@ -0,0 +1,223 @@
+{
+ "active_manifest": "contentauth:urn:uuid:3d91b8cf-72eb-40da-b479-7e97483fcc9f",
+ "manifests": {
+ "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:07d45e4a-7dff-4494-8282-44eac8eeeba6",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient__1.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "EMeeY5a+lvy1msl+9i5DOcOoeQowrqD7NyV0d8fwAX0="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:40+00:00"
+ },
+ "label": "contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ },
+ "contentauth:urn:uuid:3d91b8cf-72eb-40da-b479-7e97483fcc9f": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CAICAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:ee4737e3-3a4e-4b64-b14f-e7bcd6710ea4",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ },
+ {
+ "title": "CAI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:baccbd98-0680-480a-b9e3-61dfa725aebc",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=/c2pa/contentauth:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911/c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentbeef:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:baccbd98-0680-480a-b9e3-61dfa725aebc",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "hash": "WdHBvgu1goVcT7CAL3KpY++gLtQ0Rq32x1JqErpJw9c="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:41+00:00"
+ },
+ "label": "contentauth:urn:uuid:3d91b8cf-72eb-40da-b479-7e97483fcc9f"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "assertion.hashedURI.mismatch",
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
+ "explanation": "hash does not match assertion data: self#jumbf=c2pa.assertions/c2pa.ingredient__1"
+ },
+ {
+ "code": "claim.missing",
+ "url": "self#jumbf=/c2pa/contentbeef:urn:uuid:5bc1a923-cdfc-4c73-8135-a1e5f7aed911",
+ "explanation": "ingredient not found"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/E-sig-CA.json b/make_test_images/json_manifests/E-sig-CA.json
@@ -0,0 +1,95 @@
+{
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_xxxxxx/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "timeStamp.mismatch",
+ "url": "Cose_Sign1",
+ "explanation": "timestamp message imprint did not match"
+ },
+ {
+ "code": "claimSignature.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.signature",
+ "explanation": "claim signature is not valid"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/E-uri-CA.json b/make_test_images/json_manifests/E-uri-CA.json
@@ -0,0 +1,91 @@
+{
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnessdeadbeef"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "assertion.hashedURI.mismatch",
+ "url": "self#jumbf=c2pa.assertions/c2pa.actions",
+ "explanation": "hash does not match assertion data: self#jumbf=c2pa.assertions/c2pa.actions"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/E-uri-CIE-sig-CA.json b/make_test_images/json_manifests/E-uri-CIE-sig-CA.json
@@ -0,0 +1,191 @@
+{
+ "active_manifest": "contentauth:urn:uuid:e806386e-a44f-4a7c-8518-23885c307560",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_xxxxxx/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnessdeadbeef"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ },
+ "contentauth:urn:uuid:e806386e-a44f-4a7c-8518-23885c307560": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CIE-sig-CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:4dc278df-414b-43bf-880d-43c7c52d2605",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "E-sig-CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:6f5bc756-9cc7-4b7a-89d9-602ea6bbe665",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "componentOf",
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "validation_status": [
+ {
+ "code": "timeStamp.mismatch",
+ "url": "Cose_Sign1",
+ "explanation": "timestamp message imprint did not match"
+ },
+ {
+ "code": "claimSignature.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.signature",
+ "explanation": "claim signature is not valid"
+ }
+ ]
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp:iid:6f5bc756-9cc7-4b7a-89d9-602ea6bbe665",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "hh9ensrjGUtbGScwKSk7ll9U1fDmM2PEe3UIQhyvrL4="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:42+00:00"
+ },
+ "label": "contentauth:urn:uuid:e806386e-a44f-4a7c-8518-23885c307560"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "ingredient.hashedURI.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "explanation": "ingredient hash incorrect"
+ },
+ {
+ "code": "assertion.hashedURI.mismatch",
+ "url": "self#jumbf=c2pa.assertions/c2pa.actions",
+ "explanation": "hash does not match assertion data: self#jumbf=c2pa.assertions/c2pa.actions"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/XCA.json b/make_test_images/json_manifests/XCA.json
@@ -0,0 +1,91 @@
+{
+ "active_manifest": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7",
+ "manifests": {
+ "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:548e96a5-1674-4bd7-9331-d5ea3d4367ee",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "assertion.dataHash.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:55ec3f39-910a-4f5c-b6c0-2ecc5487c1d7/c2pa.assertions/c2pa.hash.data",
+ "explanation": "asset hash error, name: jumbf manifest, error: hash verification( Hashes do not match )"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/json_manifests/XCI.json b/make_test_images/json_manifests/XCI.json
@@ -0,0 +1,96 @@
+{
+ "active_manifest": "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b",
+ "manifests": {
+ "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b": {
+ "claim_generator": "make_test_images/0.31.3 c2pa-rs/0.31.3",
+ "claim_generator_info": [
+ {
+ "name": "make_test_images",
+ "version": "0.31.3"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.3"
+ }
+ ],
+ "title": "CI.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:7a242815-d261-4e69-87b7-34e51a662d32",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "I.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "instance_id": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "John Doe"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.31.3",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ },
+ {
+ "action": "c2pa.placed",
+ "instanceId": "xmp.iid:8a00de7a-e694-43b2-a7e6-ed950421a21a",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "LhHr79j69sagdiQpq9AfBwH+rzMmkO8LM3tC5IRAXaI="
+ }
+ }
+ },
+ {
+ "action": "c2pa.resized"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2024-04-10T16:33:39+00:00"
+ },
+ "label": "contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "assertion.dataHash.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:7cef87aa-9b17-41e1-ab0b-03815f78764b/c2pa.assertions/c2pa.hash.data",
+ "explanation": "asset hash error, name: jumbf manifest, error: hash verification( Hashes do not match )"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/src/compare_manifests.rs b/make_test_images/src/compare_manifests.rs
@@ -0,0 +1,199 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// Compares two manifest stores and prints out the differences.
+use std::collections::HashMap;
+use std::{fs, path::Path};
+
+use c2pa::{Error, Reader as ManifestStore, Result};
+
+/// Compares all the files in two directories and returns a list of issues
+pub fn compare_folders<P: AsRef<Path>, Q: AsRef<Path>>(folder1: P, folder2: Q) -> Result<()> {
+ let folder1 = folder1.as_ref();
+ let folder2 = folder2.as_ref();
+
+ // handle the case we have files instead of folders
+ if folder1.is_file() && folder2.is_file() {
+ let issues = compare_image_manifests(folder1, folder2)?;
+ if !issues.is_empty() {
+ eprintln!("Failed {:?}", folder1);
+ for issue in issues {
+ eprintln!(" {}", issue);
+ }
+ } else {
+ println!("Passed {:?}", folder1);
+ }
+ return Ok(());
+ } else if !(folder1.is_dir() && folder2.is_dir()) {
+ eprintln!("must be two folders or two files");
+ return Err(Error::BadParam(
+ "must be two folders or two files".to_string(),
+ ));
+ }
+
+ for entry in fs::read_dir(folder1)? {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_file() {
+ let relative_path = path.strip_prefix(folder1).unwrap();
+ let other_path = folder2.join(relative_path);
+ //println!("Comparing {:?} to {:?}", path, other_path);
+ let mut issues = Vec::new();
+ if other_path.exists() {
+ let result = compare_image_manifests(&path, &other_path)?;
+ issues.extend(result);
+ } else {
+ issues.push(format!(
+ "File {} does not exist in {}",
+ relative_path.display(),
+ folder2.display()
+ ));
+ }
+ if !issues.is_empty() {
+ eprintln!("Failed {:?}", relative_path);
+ for issue in issues {
+ eprintln!(" {}", issue);
+ }
+ } else {
+ println!("Passed {:?}", relative_path);
+ }
+ }
+ }
+ Ok(())
+}
+
+/// Compares files with manifest stores and returns a list of issues
+pub fn compare_image_manifests<P: AsRef<Path>, Q: AsRef<Path>>(
+ m1: P,
+ m2: Q,
+) -> Result<Vec<String>> {
+ let manifest_store1 = match m1.as_ref().extension() {
+ Some(ext) if ext == "json" => {
+ ManifestStore::from_json(&fs::read_to_string(m1)?)
+ //serde_json::from_str(&fs::read_to_string(m1)?).map_err(Error::JsonError)
+ }
+ _ => ManifestStore::from_file(m1.as_ref()),
+ };
+ let manifest_store2 = match m2.as_ref().extension() {
+ Some(ext) if ext == "json" => ManifestStore::from_json(&fs::read_to_string(m2)?),
+ _ => ManifestStore::from_file(m2.as_ref()),
+ };
+ // let manifest_store1 = ManifestStore::from_file(m1);
+ // let manifest_store2 = ManifestStore::from_file(m2);
+ match (manifest_store1, manifest_store2) {
+ (Ok(manifest_store1), Ok(manifest_store2)) => {
+ compare_manifests(&manifest_store1, &manifest_store2)
+ }
+ (Err(Error::JumbfNotFound), Err(Error::JumbfNotFound)) => Ok(Vec::new()),
+ (_, Err(err)) => Err(err),
+ (Err(err), _) => Err(err),
+ }
+}
+
+/// Compares two manifest stores and returns a list of issues.
+pub fn compare_manifests(
+ manifest_store1: &ManifestStore,
+ manifest_store2: &ManifestStore,
+) -> Result<Vec<String>> {
+ // first we need to gather all the manifests in the order they are first seen recursively
+ let mut labels1 = Vec::new();
+ if let Some(label) = manifest_store1.active_label() {
+ gather_manifests(manifest_store1, label, &mut labels1);
+ }
+ let mut labels2 = Vec::new();
+ if let Some(label) = manifest_store2.active_label() {
+ gather_manifests(manifest_store2, label, &mut labels2);
+ }
+ // now we have two lists of manifests, we need to match them up
+ let manifest_map: HashMap<_, _> = labels1.into_iter().zip(labels2).collect();
+
+ // now we can compare the manifests
+ let mut issues = Vec::new();
+ for (label1, label2) in manifest_map.iter() {
+ // let foo = serde_json::to_string(&manifest_store1.get(label1))?;
+ // let foo = serde_json::from_str(&foo)?;
+ // convert manifests into json values and compare them
+ let value1 = serde_json::to_value(manifest_store1.get_manifest(label1))?;
+ let value2 = serde_json::to_value(manifest_store2.get_manifest(label2))?;
+ compare_json_values(
+ &format!("manifests.{}", label1),
+ &value1,
+ &value2,
+ &mut issues,
+ );
+ }
+ Ok(issues)
+}
+
+// creates list of manifests in the order they are first seen from the active manifest
+fn gather_manifests(
+ manifest_store: &ManifestStore,
+ manifest_label: &str,
+ labels: &mut Vec<String>,
+) {
+ if !labels.contains(&manifest_label.to_string()) {
+ labels.push(manifest_label.to_string());
+ }
+ if let Some(manifest) = manifest_store.get_manifest(manifest_label) {
+ for ingredient in manifest.ingredients() {
+ if let Some(label) = ingredient.active_manifest() {
+ gather_manifests(manifest_store, label, labels);
+ }
+ }
+ }
+}
+
+/// Recursively compare two ManifestStore JSON values
+fn compare_json_values(
+ path: &str,
+ val1: &serde_json::Value,
+ val2: &serde_json::Value,
+ issues: &mut Vec<String>,
+) {
+ match (val1, val2) {
+ (serde_json::Value::Object(map1), serde_json::Value::Object(map2)) => {
+ for (key, val1) in map1 {
+ let val2 = map2.get(key).unwrap_or(&serde_json::Value::Null);
+ compare_json_values(&format!("{}.{}", path, key), val1, val2, issues);
+ }
+
+ for (key, value) in map2 {
+ if map1.get(key).is_none() {
+ issues.push(format!("Added {}.{}: {}", path, key, value));
+ }
+ }
+ }
+ (serde_json::Value::Array(arr1), serde_json::Value::Array(arr2)) => {
+ for (i, (val1, val2)) in arr1.iter().zip(arr2.iter()).enumerate() {
+ compare_json_values(&format!("{}[{}]", path, i), val1, val2, issues);
+ }
+ }
+ (val1, val2) if val1 != val2 => {
+ if !(path.ends_with(".instance_id")
+ || path.ends_with(".instanceId")
+ || path.ends_with(".time")
+ || path.contains(".hash")
+ || val1.is_string() && val2.is_string() && val1.to_string().contains(":urn:uuid:"))
+ {
+ if val2.is_null() {
+ issues.push(format!("Missing {}: {}", path, val1));
+ } else if val2.is_null() {
+ issues.push(format!("Added {}: {}", path, val2));
+ } else {
+ issues.push(format!("Changed {}: {} vs {}", path, val1, val2));
+ }
+ }
+ }
+ _ => (),
+ }
+}
diff --git a/make_test_images/src/main.rs b/make_test_images/src/main.rs
@@ -14,19 +14,34 @@
//! This generates a set of test images with a wide variety of configurations
//! To run this, use the following command in a terminal
//! cargo run --release --bin make_test_images
+mod compare_manifests;
mod make_test_images;
+mod make_thumbnail;
use anyhow::{Context, Result};
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
- let path = if args.len() > 1 {
- args[1].as_ref()
+ let config = if args.len() > 2 {
+ make_test_images::Config {
+ alg: "ps256".to_owned(),
+ tsa_url: None,
+ output_path: "target/images".to_owned(),
+ default_ext: "jpg".to_owned(),
+ author: None,
+ recipes: Vec::new(),
+ compare_folders: Some([args[1].clone(), args[2].clone()]),
+ }
} else {
- "make_test_images/tests.json"
+ let path = if args.len() > 1 {
+ args[1].as_ref()
+ } else {
+ "make_test_images/tests.json"
+ };
+ let buf = std::fs::read_to_string(path).context(format!("Reading {path}"))?;
+ let config: make_test_images::Config =
+ serde_json::from_str(&buf).context("Config file format error")?;
+ config
};
- let buf = std::fs::read_to_string(path).context(format!("Reading {path}"))?;
- let config: make_test_images::Config =
- serde_json::from_str(&buf).context("Config file format")?;
// set RUST_LOG=debug to get detailed debug logging
env_logger::init();
diff --git a/make_test_images/src/make_test_images.rs b/make_test_images/src/make_test_images.rs
@@ -15,18 +15,22 @@
use std::{
collections::HashMap,
fs,
+ io::{Cursor, Seek},
path::{Path, PathBuf},
};
use anyhow::{Context, Result};
use c2pa::{
- assertions::{c2pa_action, Action, Actions, CreativeWork, SchemaDotOrgPerson},
- create_signer, jumbf_io, Error, Ingredient, IngredientOptions, Manifest, ManifestStore, Signer,
- SigningAlg,
+ create_signer,
+ jumbf_io::{get_supported_types, load_jumbf_from_stream, save_jumbf_to_stream},
+ Builder, Error, Reader, Signer, SigningAlg,
};
use memchr::memmem;
use nom::AsBytes;
use serde::Deserialize;
+use serde_json::json;
+
+use crate::{compare_manifests::compare_folders, make_thumbnail::make_thumbnail_from_stream};
const IMAGE_WIDTH: u32 = 2048;
const IMAGE_HEIGHT: u32 = 1365;
@@ -69,6 +73,8 @@ pub struct Config {
pub author: Option<String>,
/// A list of recipes for test files
pub recipes: Vec<Recipe>,
+ /// A folder to compare the output to
+ pub compare_folders: Option<[String; 2]>,
}
impl Config {
@@ -94,27 +100,48 @@ impl Default for Config {
default_ext: "jpg".to_owned(),
author: None,
recipes: Vec::new(),
+ compare_folders: None,
}
}
}
-/// Generate a blake3 hash over the image in path using a fixed buffer
-fn blake3_hash(path: &Path) -> Result<String> {
- use std::{fs::File, io::Read};
- // Hash an input incrementally.
- let mut hasher = blake3::Hasher::new();
- const BUFFER_LEN: usize = 1024 * 1024;
- let mut buffer = [0u8; BUFFER_LEN];
- let mut file = File::open(path)?;
- loop {
- let read_count = file.read(&mut buffer)?;
- hasher.update(&buffer[..read_count]);
- if read_count != BUFFER_LEN {
- break;
- }
- }
- let hash = hasher.finalize();
- Ok(hash.to_hex().as_str().to_owned())
+/// Converts a file extension to a MIME type
+fn extension_to_mime(extension: &str) -> Option<&'static str> {
+ Some(match extension.to_lowercase().as_str() {
+ "jpg" | "jpeg" => "image/jpeg",
+ "png" => "image/png",
+ "gif" => "image/gif",
+ "psd" => "image/vnd.adobe.photoshop",
+ "tiff" | "tif" => "image/tiff",
+ "svg" => "image/svg+xml",
+ "ico" => "image/x-icon",
+ "bmp" => "image/bmp",
+ "webp" => "image/webp",
+ "dng" => "image/dng",
+ "heic" => "image/heic",
+ "heif" => "image/heif",
+ "mp2" | "mpa" | "mpe" | "mpeg" | "mpg" | "mpv2" => "video/mpeg",
+ "mp4" => "video/mp4",
+ "avif" => "image/avif",
+ "mov" | "qt" => "video/quicktime",
+ "m4a" => "audio/mp4",
+ "mid" | "rmi" => "audio/mid",
+ "mp3" => "audio/mpeg",
+ "wav" => "audio/vnd.wav",
+ "aif" | "aifc" | "aiff" => "audio/aiff",
+ "ogg" => "audio/ogg",
+ "pdf" => "application/pdf",
+ "ai" => "application/postscript",
+ _ => return None,
+ })
+}
+
+fn extension(path: &Path) -> Option<&str> {
+ path.extension().and_then(std::ffi::OsStr::to_str)
+}
+
+fn file_name(path: &Path) -> Option<&str> {
+ path.file_name().and_then(std::ffi::OsStr::to_str)
}
/// Tool for building test case images for C2PA
@@ -178,59 +205,112 @@ impl MakeTestImages {
Ok(())
}
- /// Creates a test image with optional source and ingredients, out to dest
+ fn add_ingredient_from_file(
+ builder: &mut Builder,
+ path: &Path,
+ relationship: &str,
+ ) -> Result<String> {
+ let mut source = fs::File::open(path).context("opening ingredient")?;
+ let name = path
+ .file_name()
+ .ok_or(Error::BadParam("no filename".to_string()))?
+ .to_string_lossy();
+ let extension = path
+ .extension()
+ .ok_or(Error::BadParam("no extension".to_owned()))?
+ .to_string_lossy()
+ .into_owned();
+ let format = extension_to_mime(&extension).unwrap_or("image/jpeg");
+
+ let json = json!({
+ "title": name,
+ "relationship": relationship,
+ })
+ .to_string();
+
+ let ingredient = builder.add_ingredient(&json, format, &mut source)?;
+ if ingredient.thumbnail_ref().is_none() {
+ source.rewind()?;
+ let (format, thumbnail) =
+ make_thumbnail_from_stream(format, &mut source).context("making thumbnail")?;
+ ingredient.set_thumbnail(format, thumbnail)?;
+ }
+
+ Ok(
+ builder.definition.ingredients[builder.definition.ingredients.len() - 1]
+ .instance_id()
+ .to_string(),
+ )
+ }
+
fn make_image(&self, recipe: &Recipe) -> Result<PathBuf> {
let src = recipe.parent.as_deref();
- let dst_path = self.make_path(&recipe.output);
+ let dst = recipe.output.as_str();
+ let dst_path = self.make_path(dst);
println!("Creating {dst_path:?}");
- // keep track of all actions here
- let mut actions = Actions::new();
-
- struct ImageOptions {}
- impl ImageOptions {
- fn new() -> Self {
- ImageOptions {}
- }
- }
-
- impl IngredientOptions for ImageOptions {
- fn hash(&self, path: &Path) -> Option<String> {
- blake3_hash(path).ok()
- }
- }
- let generator = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
let software_agent = format!("{} {}", "Make Test Images", env!("CARGO_PKG_VERSION"));
+ // let software_agent = json!({
+ // "name": "Make Test Images",
+ // "version": env!("CARGO_PKG_VERSION")
+ // });
+ let name = file_name(&dst_path).ok_or(Error::BadParam("no filename".to_string()))?;
+ let extension = extension(&dst_path).unwrap_or("jpg");
+
+ let format = extension_to_mime(extension).unwrap_or("image/jpeg");
+
+ let manifest_def = json!({
+ "vendor": "contentauth",
+ "title": name,
+ "format": &format,
+ "claim_generator_info": [
+ {
+ "name": env!("CARGO_PKG_NAME"),
+ "version": env!("CARGO_PKG_VERSION")
+ }
+ ]
+ })
+ .to_string();
- let mut manifest = Manifest::new(generator);
- manifest.set_vendor("contentauth".to_owned()); // needed for generating error cases below
-
- if let Some(user) = self.config.author.as_ref() {
- let creative_work = CreativeWork::new()
- .add_author(SchemaDotOrgPerson::new().set_name(user.to_owned())?)?;
-
- manifest.add_assertion(&creative_work)?;
- }
+ let mut builder = Builder::from_json(&manifest_def)?;
// keep track of ingredient instances so we don't duplicate them
let mut ingredient_table = HashMap::new();
+ let mut actions = Vec::new();
+ if let Some(author) = &self.config.author {
+ builder.add_assertion(
+ "stds.schema-org.CreativeWork",
+ &json!({
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": author
+ }
+ ]
+ }),
+ )?;
+ };
+
// process parent first
let mut img = match src {
Some(src) => {
let src_path = &self.make_path(src);
- let parent = Ingredient::from_file_with_options(src_path, &ImageOptions::new())?;
-
- let instance_id = parent.instance_id().to_string();
+ let instance_id =
+ Self::add_ingredient_from_file(&mut builder, src_path, "parentOf")?;
- actions = actions.add_action(
- Action::new(c2pa_action::OPENED).set_instance_id(parent.instance_id()),
- );
- manifest.set_parent(parent)?;
+ actions.push(json!(
+ {
+ "action": "c2pa.opened",
+ "instanceId": &instance_id,
+ }
+ ));
// keep track of all ingredients we add via the instance Id
- ingredient_table.insert(src, instance_id);
+ ingredient_table.insert(src, instance_id.to_owned());
// load the image for editing
let mut img =
@@ -238,10 +318,14 @@ impl MakeTestImages {
// adjust brightness to show we made an edit
img = img.brighten(30);
- actions = actions.add_action(
- Action::new(c2pa_action::COLOR_ADJUSTMENTS)
- .set_parameter("name".to_owned(), "brightnesscontrast")?,
- );
+ actions.push(json!(
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ));
img
}
None => {
@@ -255,14 +339,16 @@ impl MakeTestImages {
*pixel = image::Rgb([r, 100, b]);
}
}
- actions = actions.add_action(
- Action::new(c2pa_action::CREATED)
- .set_source_type(
- "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia",
- )
- .set_software_agent(software_agent.as_str())
- .set_parameter("name".to_owned(), "gradient")?,
- );
+ actions.push(json!(
+ {
+ "action": "c2pa.created",
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia",
+ "softwareAgent": software_agent,
+ "parameters": {
+ "name": "gradient"
+ }
+ }
+ ));
img
}
};
@@ -288,36 +374,137 @@ impl MakeTestImages {
// if we have already created an ingredient, get the instanceId, otherwise create a new one
let instance_id = match ingredient_table.get(ing.as_str()) {
- Some(id) => id.to_owned(),
+ Some(id) => id.to_string(),
None => {
- let ingredient =
- Ingredient::from_file_with_options(ing_path, &ImageOptions::new())?;
- let instance_id = ingredient.instance_id().to_string();
+ let instance_id =
+ Self::add_ingredient_from_file(&mut builder, ing_path, "componentOf")?;
ingredient_table.insert(ing, instance_id.clone());
- manifest.add_ingredient(ingredient);
instance_id
}
};
-
- actions = actions
- .add_action(Action::new(c2pa_action::PLACED).set_instance_id(instance_id));
-
+ actions.push(json!(
+ {
+ "action": "c2pa.placed",
+ "instanceId": instance_id,
+ }
+ ));
x += width as i64;
}
// record what we did as an action (only need to record this once)
- actions = actions.add_action(Action::new(c2pa_action::RESIZED));
+ actions.push(json!(
+ {
+ "action": "c2pa.resized",
+ }
+ ));
}
+ let mut temp = tempfile::tempfile()?;
+
+ use image::ImageFormat;
+ let image_format = ImageFormat::from_extension(extension)
+ .ok_or(Error::BadParam("extension not supported".to_owned()))?;
// save the changes to the image as our target file
- img.save(&dst_path)?;
+ img.write_to(&mut temp, image_format)?;
+ temp.rewind()?;
// add all our actions as an assertion now.
- manifest.add_assertion(&actions)?; // extra get required here, since actions is an array
+ builder.add_assertion(
+ "c2pa.actions",
+ &json!(
+ {
+ "actions": actions
+ }
+ ),
+ )?;
+
+ // generate a thumbnail and set it in the image
+ // make sure do do this last,on the generated image so that it reflects the output
+ let (thumb_format, image) =
+ make_thumbnail_from_stream(format, &mut temp).context("making thumbnail")?;
+ builder.set_thumbnail(&thumb_format, &mut Cursor::new(image))?;
+
+ temp.rewind()?;
// now sign manifest and embed in target
let signer = self.config.get_signer()?;
- manifest.embed(&dst_path, &dst_path, signer.as_ref())?;
+ let mut dest = fs::File::create(&dst_path)?;
+ builder
+ .sign(format, &mut temp, &mut dest, signer.as_ref())
+ .context("signing")?;
+
+ Ok(dst_path)
+ }
+
+ fn manifest_def(title: &str, format: &str) -> String {
+ json!({
+ "title": title,
+ "format": format,
+ "claim_generator_info": [
+ {
+ "name": "Make Test Images",
+ "version": env!("CARGO_PKG_VERSION")
+ }
+ ],
+ "assertions": [
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.edited",
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
+ "softwareAgent": {
+ "name": "My AI Tool",
+ "version": "0.1.0"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }).to_string()
+ }
+
+ fn sign_image(&self, recipe: &Recipe) -> Result<PathBuf> {
+ let src = recipe.parent.as_deref();
+ let dst = recipe.output.as_str();
+ let dst_path = self.make_path(dst);
+ println!("Signing {dst_path:?}");
+
+ let src = match src {
+ Some(src) => src,
+ None => return Err(Error::BadParam("no parent".to_string()).into()),
+ };
+
+ let name = file_name(&dst_path).ok_or(Error::BadParam("no filename".to_string()))?;
+ let extension = extension(&dst_path).unwrap_or("jpg");
+
+ let format = extension_to_mime(extension).unwrap_or("image/jpeg");
+
+ let json = Self::manifest_def(name, format);
+
+ let src_path = &self.make_path(src);
+ let mut source = fs::File::open(src_path).context("opening ingredient")?;
+
+ let mut builder = Builder::from_json(&json)?;
+
+ let parent_name = file_name(&dst_path).ok_or(Error::BadParam("no filename".to_string()))?;
+ builder.add_ingredient(
+ json!({
+ "title": parent_name,
+ "relationship": "parentOf"
+ })
+ .to_string(),
+ extension,
+ &mut source,
+ )?;
+
+ let mut dest = fs::File::create(&dst_path)?;
+ let signer = self.config.get_signer()?;
+ builder
+ .sign(format, &mut source, &mut dest, signer.as_ref())
+ .context("signing")?;
Ok(dst_path)
}
@@ -327,9 +514,16 @@ impl MakeTestImages {
let src = recipe.parent.as_deref().unwrap_or_default();
let src_path = &self.make_path(src);
let dst_path = self.make_path(recipe.output.as_str());
- println!("Creating OGP {dst_path:?}");
-
- let jumbf = jumbf_io::load_jumbf_from_file(&PathBuf::from(src_path))
+ println!("Creating {dst_path:?}");
+ let format = src_path
+ .extension()
+ .ok_or(Error::BadParam("no extension".to_owned()))?
+ .to_string_lossy()
+ .into_owned();
+
+ let mut source = std::fs::File::open(src_path).context("opening OGP source")?;
+ let jumbf = load_jumbf_from_stream(&format, &mut source)
+ .context("loading OGP")
.context(format!("loading OGP {src_path:?}"))?;
// save the edited image to our destination file
let mut img =
@@ -337,8 +531,10 @@ impl MakeTestImages {
img = img.grayscale();
img.save(&dst_path)
.context(format!("saving OGP image{:?}", &dst_path))?;
+ let image = std::fs::read(&dst_path).context("reading OGP image")?;
+ let mut dest = std::fs::File::create(&dst_path).context("creating OGP image")?;
// write the original claim data to the edited image
- jumbf_io::save_jumbf_to_file(&jumbf, &PathBuf::from(&dst_path), Some(&dst_path))
+ save_jumbf_to_stream(&format, &mut Cursor::new(image), &mut dest, &jumbf)
.context(format!("OGP save_jumbf_to_file {:?}", &dst_path))?;
// The image library does not preserve any metadata so we have to write it ourselves.
// todo: should preserve all metadata and update instanceId.
@@ -350,7 +546,7 @@ impl MakeTestImages {
let op = recipe.op.as_str();
let src = recipe.parent.as_deref().unwrap_or_default();
let dst_path = self.make_path(recipe.output.as_str());
- println!("Creating Error op={op} {dst_path:?}");
+ println!("Creating {dst_path:?}");
let (search_bytes, replace_bytes) = match op {
// modify the XMP (change xmp magic id value) - this should cause a data hash mismatch (OTGP)
@@ -391,35 +587,65 @@ impl MakeTestImages {
/// copies a file from the parent to the output
fn make_copy(&self, recipe: &Recipe) -> Result<PathBuf> {
- let dst_path = self.make_path(recipe.output.as_str());
+ let src = recipe.parent.as_deref().unwrap_or_default();
+ let dst = recipe.output.as_str();
+ let src_path = &self.make_path(src);
+ let dst_path = self.make_path(dst);
println!("Copying {dst_path:?}");
let src = recipe.parent.as_deref().unwrap_or_default();
let dst = recipe.output.as_str();
- std::fs::copy(src, &dst_path).context(format!("copying {src} to {dst}"))?;
+ if extension(&PathBuf::from(src)) != extension(&PathBuf::from(dst)) {
+ let img = image::open(src_path).context(format!("copying {src} to {dst}"))?;
+ img.save(&dst_path)
+ .context(format!("copying {src} to {dst}"))?;
+ } else {
+ std::fs::copy(src, &dst_path).context(format!("copying {src} to {dst}"))?;
+ }
Ok(dst_path)
}
/// Runs a list of recipes
pub fn run(&self) -> Result<()> {
+ let supported = get_supported_types();
+ println!("Supported types: {:#?}", supported);
if !self.output_dir.exists() {
std::fs::create_dir_all(&self.output_dir).context("Can't create output folder")?;
};
+ let json_dir = self.output_dir.join("json");
+ if !json_dir.exists() {
+ std::fs::create_dir_all(&json_dir)?;
+ }
let recipes = &self.config.recipes;
for recipe in recipes {
let dst_path = match recipe.op.as_str() {
"make" => self.make_image(recipe)?,
+ "sign" => self.sign_image(recipe)?,
"ogp" => self.make_ogp(recipe)?,
"dat" | "sig" | "uri" | "clm" | "prv" => self.make_err(recipe)?,
"copy" => self.make_copy(recipe)?,
_ => return Err(Error::BadParam(recipe.op.to_string()).into()),
};
- let manifest_store = ManifestStore::from_file(dst_path);
if recipe.op.as_str() != "copy" {
- println!("{}", manifest_store?);
+ let mut file = std::fs::File::open(&dst_path)?;
+ let format = dst_path
+ .extension()
+ .and_then(|s| s.to_str())
+ .unwrap_or("jpg");
+ let reader = Reader::from_stream(format, &mut file)?;
+ let json = reader.json();
+
+ let json_path = json_dir
+ .join(dst_path.file_name().unwrap())
+ .with_extension("json");
+ std::fs::write(&json_path, json)?;
}
}
+ //println!("Comparing to {:#?}", self.config.compare_folder);
+ if let Some(folders) = &self.config.compare_folders {
+ compare_folders(&folders[0], &folders[1])?;
+ }
Ok(())
}
}
diff --git a/make_test_images/src/make_thumbnail.rs b/make_test_images/src/make_thumbnail.rs
@@ -0,0 +1,58 @@
+// Copyright 2022 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::io::{Read, Seek};
+
+use anyhow::{Error, Result};
+use image::{io::Reader, ImageFormat};
+
+// max edge size allowed in pixels for thumbnail creation
+const THUMBNAIL_LONGEST_EDGE: u32 = 1024;
+const THUMBNAIL_JPEG_QUALITY: u8 = 80;
+
+/// utility to generate a thumbnail from a stream
+/// returns Result (format, image_bits) if successful, otherwise Error
+pub fn make_thumbnail_from_stream<R: Read + Seek + ?Sized>(
+ format: &str,
+ stream: &mut R,
+) -> Result<(String, Vec<u8>)> {
+ let format = ImageFormat::from_extension(format)
+ .or_else(|| ImageFormat::from_mime_type(format))
+ .ok_or(Error::msg(format!("format not supported {format}")))?;
+
+ let reader = Reader::with_format(std::io::BufReader::new(stream), format);
+ let mut img = reader.decode()?;
+
+ let longest_edge = THUMBNAIL_LONGEST_EDGE;
+
+ // generate a thumbnail image scaled down and in jpeg format
+ if img.width() > longest_edge || img.height() > longest_edge {
+ img = img.thumbnail(longest_edge, longest_edge);
+ }
+
+ // for png files, use png thumbnails for transparency
+ // for other supported types try a jpeg thumbnail
+ let (output_format, format) = match format {
+ ImageFormat::Png => (image::ImageOutputFormat::Png, "image/png"),
+ _ => (
+ image::ImageOutputFormat::Jpeg(THUMBNAIL_JPEG_QUALITY),
+ "image/jpeg",
+ ),
+ };
+ let thumbnail_bits = Vec::new();
+ let mut cursor = std::io::Cursor::new(thumbnail_bits);
+ img.write_to(&mut cursor, output_format)?;
+
+ let format = format.to_owned();
+ Ok((format, cursor.into_inner()))
+}
diff --git a/make_test_images/src/types.json b/make_test_images/src/types.json
@@ -0,0 +1,18 @@
+{
+ "alg": "ps256",
+ "tsa_url": "http://timestamp.digicert.com",
+ "output_path": "target/alltypes",
+ "default_ext": "jpg",
+ "author": "John Doe",
+ "recipes": [
+ { "op": "sign", "parent": "sdk/tests/fixtures/IMG_0003.jpg", "output": "IMG_0003_c.jpg" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/sample1.png", "output": "sample1_c.png" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/sample1.webp", "output": "sample1_c.webp" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/TUSCANY.TIF", "output": "tuscany_c.tif" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/sample1.svg", "output": "sample1_c.svg" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/sample1.dng", "output": "sample1_c.dng" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/test.avi", "output": "test_c.avi" },
+ { "op": "sign", "parent": "sdk/tests/fixtures/sample1.wav", "output": "sample1_c.wav" }
+
+ ]
+}
+\ No newline at end of file
diff --git a/make_test_images/tests.json b/make_test_images/tests.json
@@ -3,7 +3,8 @@
"tsa_url": "http://timestamp.digicert.com",
"output_path": "target/images",
"default_ext": "jpg",
- "author": "Adobe make_test",
+ "author": "John Doe",
+ "compare_folders": ["make_test_images/json_manifests","target/images/json"],
"recipes": [
{ "op": "copy", "parent": "sdk/tests/fixtures/IMG_0003.jpg", "output": "A.jpg" },
{ "op": "copy", "parent": "sdk/tests/fixtures/P1000827.jpg", "output": "I.jpg" },
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -25,7 +25,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
-default = []
+default = ["v1_api"]
add_thumbnails = ["image"]
psxxx_ocsp_stapling_experimental = []
file_io = ["openssl_sign"]
@@ -36,12 +36,13 @@ fetch_remote_manifests = []
openssl_sign = ["openssl"]
json_schema = ["dep:schemars"]
pdf = ["dep:lopdf"]
+v1_api = []
+unstable_api = []
# The diagnostics feature is unsupported and might be removed.
# It enables some low-overhead timing features used in our development cycle.
diagnostics = []
-#[cfg(not(target_arch = "wasm32"))]
[[example]]
name = "client"
required-features = ["file_io"]
@@ -51,7 +52,14 @@ name = "show"
required-features = ["file_io"]
[[example]]
-name = "custom_assertion"
+name = "v2show"
+required-features = ["unstable_api"]
+
+[[example]]
+name = "v2api"
+required-features = ["unstable_api"]
+
+
[lib]
crate-type = ["lib"]
@@ -94,6 +102,7 @@ memchr = "2.7.1"
multibase = "0.9.0"
multihash = "0.11.4"
mp4 = "0.13.0"
+pem = "3.0.2"
png_pong = "0.9.1"
rand = "0.8.5"
rand_chacha = "0.3.1"
@@ -107,16 +116,18 @@ serde = { version = "1.0.197", features = ["derive"] }
serde_bytes = "0.11.5"
serde_cbor = "0.11.1"
serde_derive = "1.0.197"
-serde_json = "1.0.114"
+serde_json = { version = "1.0.114", features = ["preserve_order"] }
+serde_with = "3.4.0"
serde-transcode = "1.1.1"
sha2 = "0.10.2"
-tempfile = "3.1.0"
+tempfile = "3.10.1"
thiserror = "1.0.40"
treeline = "0.1.0"
url = "2.2.2"
uuid = { version = "1.3.1", features = ["serde", "v4", "wasm-bindgen"] }
x509-parser = "0.15.1"
x509-certificate = "0.19.0"
+zip = { version = "0.6.6", default-features = false }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ureq = "2.4.0"
@@ -152,9 +163,12 @@ web-sys = { version = "0.3.58", features = [
[dev-dependencies]
anyhow = "1.0.40"
mockall = "0.11.2"
+c2pa = { path = ".", features = ["unstable_api"] } # allow integration tests to use the new API
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.31"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
actix = "0.13.1"
+ed25519-dalek = "2.1.1"
+tokio = { version = "1.36.0", features = ["full"] }
diff --git a/sdk/examples/client/client.rs b/sdk/examples/client/client.rs
@@ -18,7 +18,7 @@ use std::path::PathBuf;
use anyhow::Result;
use c2pa::{
assertions::{c2pa_action, labels, Action, Actions, CreativeWork, Exif, SchemaDotOrgPerson},
- create_signer, Ingredient, Manifest, ManifestStore, SigningAlg,
+ create_signer, Ingredient, Manifest, Reader as ManifestStore, SigningAlg,
};
const GENERATOR: &str = "test_app/0.1";
@@ -29,7 +29,7 @@ fn show_manifest(manifest_store: &ManifestStore, manifest_label: &str, level: us
let indent = " ".repeat(level * INDENT_SPACE);
println!("{indent}manifest_label: {manifest_label}");
- if let Some(manifest) = manifest_store.get(manifest_label) {
+ if let Some(manifest) = manifest_store.get_manifest(manifest_label) {
println!(
"{}title: {} , format: {}, instance_id: {}",
indent,
@@ -136,6 +136,7 @@ pub fn main() -> Result<()> {
println!("{manifest_store}\n");
// walk through the manifest and access data.
+
if let Some(manifest_label) = manifest_store.active_label() {
show_manifest(&manifest_store, manifest_label, 0)?;
}
diff --git a/sdk/examples/custom_assertion.rs b/sdk/examples/custom_assertion.rs
@@ -1,89 +0,0 @@
-// Copyright 2022 Adobe. All rights reserved.
-// This file is licensed to you under the Apache License,
-// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
-// or the MIT license (http://opensource.org/licenses/MIT),
-// at your option.
-
-// Unless required by applicable law or agreed to in writing,
-// this software is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
-// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
-// specific language governing permissions and limitations under
-// each license.
-
-//! Example: Creating a custom assertion
-use c2pa::{Assertion, AssertionBase, AssertionCbor, Manifest, Result};
-use serde::{Deserialize, Serialize};
-
-/// Defines a Custom assertion
-/// This can be any Rust structure
-/// It must support serde Serialize and Deserialize
-/// In this example the assertion contains a version of this sdk
-#[derive(Serialize, Deserialize)]
-pub struct Custom {
- /// Records the version of this c2pa library
- pub version: String,
-}
-
-impl Custom {
- pub fn new() -> Self {
- Self {
- version: c2pa::VERSION.to_owned(),
- }
- }
-}
-
-// Implementing default is a good idea
-impl Default for Custom {
- fn default() -> Self {
- Self::new()
- }
-}
-
-// Implement either AssertionCbor or AssertionJson
-impl AssertionCbor for Custom {}
-
-// Always implement AssertionBase by copying this template
-// If you chose AssertionJson, use to_json_assertion and from_json_assertion instead
-impl AssertionBase for Custom {
- // A label for our assertion, use reverse domain name syntax
- const LABEL: &'static str = "org.contentauth.custom";
-
- fn to_assertion(&self) -> c2pa::Result<Assertion> {
- Self::to_cbor_assertion(self)
- }
-
- fn from_assertion(assertion: &Assertion) -> Result<Self> {
- Self::from_cbor_assertion(assertion)
- }
-}
-
-fn main() -> Result<()> {
- let mut manifest = Manifest::new("c2pa-rs".to_owned());
- let original = Custom::new();
- manifest.add_assertion(&original)?;
- let result: Custom = manifest.find_assertion(Custom::LABEL)?;
- println!("{manifest}\n");
- println!("c2pa sdk version = {}", result.version);
-
- Ok(())
-}
-
-#[cfg(test)]
-pub mod tests {
- #![allow(clippy::expect_used)]
-
- use super::*;
-
- #[test]
- fn assertion_custom() {
- let mut manifest = Manifest::new("my_app".to_owned());
- let original = Custom::new();
- manifest.add_assertion(&original).expect("adding assertion");
- println!("{}", manifest);
- let result: Custom = manifest
- .find_assertion(Custom::LABEL)
- .expect("find_assertion");
- assert_eq!(original.version, result.version);
- }
-}
diff --git a/sdk/examples/show.rs b/sdk/examples/show.rs
@@ -13,13 +13,13 @@
//! Example App that generates a manifest store listing for a given file
use anyhow::Result;
-use c2pa::ManifestStore;
+use c2pa::Reader;
#[cfg(not(target_arch = "wasm32"))]
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
- let ms = ManifestStore::from_file(&args[1])?;
+ let ms = Reader::from_file(&args[1])?;
println!("{ms}");
} else {
println!("Prints a manifest report (requires a file path argument)")
diff --git a/sdk/examples/v2api.rs b/sdk/examples/v2api.rs
@@ -0,0 +1,196 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! Example App showing how to use the new v2 API
+use std::io::{Cursor, Seek};
+
+use anyhow::Result;
+use c2pa::{settings::load_settings_from_str, Builder, CallbackSigner, Reader, SigningAlg};
+use serde_json::json;
+
+const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
+const CERTS: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pub");
+const PRIVATE_KEY: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pem");
+
+fn manifest_def(title: &str, format: &str) -> String {
+ json!({
+ "title": title,
+ "format": format,
+ "claim_generator_info": [
+ {
+ "name": "c2pa test",
+ "version": env!("CARGO_PKG_VERSION")
+ }
+ ],
+ "thumbnail": {
+ "format": format,
+ "identifier": "manifest_thumbnail.jpg"
+ },
+ "ingredients": [
+ {
+ "title": "Test",
+ "format": "image/jpeg",
+ "instance_id": "12345",
+ "relationship": "inputTo"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.edited",
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
+ "softwareAgent": {
+ "name": "My AI Tool",
+ "version": "0.1.0"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }).to_string()
+}
+
+/// This example demonstrates how to use the new v2 API to create a manifest store
+/// It uses only streaming apis, showing how to avoid file i/o
+/// This example uses the `ed25519` signing algorithm
+fn main() -> Result<()> {
+ let title = "v2_edited.jpg";
+ let format = "image/jpeg";
+ let parent_name = "CA.jpg";
+ let mut source = Cursor::new(TEST_IMAGE);
+
+ let modified_core = json!({
+ "core": {
+ "debug": true,
+ "hash_alg": "sha512",
+ "max_memory_usage": 123456
+ }
+ })
+ .to_string();
+
+ load_settings_from_str(&modified_core, "json")?;
+
+ let json = manifest_def(title, format);
+
+ let mut builder = Builder::from_json(&json)?;
+ builder.add_ingredient(
+ json!({
+ "title": parent_name,
+ "relationship": "parentOf"
+ })
+ .to_string(),
+ format,
+ &mut source,
+ )?;
+
+ let thumb_uri = builder
+ .definition
+ .thumbnail
+ .as_ref()
+ .map(|t| t.identifier.clone());
+
+ // add a manifest thumbnail ( just reuse the image for now )
+ if let Some(uri) = thumb_uri {
+ if !uri.starts_with("self#jumbf") {
+ source.rewind()?;
+ builder.add_resource(&uri, &mut source)?;
+ }
+ }
+
+ // write the manifest builder to a zipped stream
+ let mut zipped = Cursor::new(Vec::new());
+ builder.to_archive(&mut zipped)?;
+
+ // write the zipped stream to a file for debugging
+ //let debug_path = format!("{}/../target/test.zip", env!("CARGO_MANIFEST_DIR"));
+ // std::fs::write(debug_path, zipped.get_ref())?;
+
+ // unzip the manifest builder from the zipped stream
+ zipped.rewind()?;
+
+ let ed_signer = |_context: *const (), data: &[u8]| ed_sign(data, PRIVATE_KEY);
+ let signer = CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS);
+
+ let mut builder = Builder::from_archive(&mut zipped)?;
+ // sign the ManifestStoreBuilder and write it to the output stream
+ let mut dest = Cursor::new(Vec::new());
+ builder.sign(format, &mut source, &mut dest, &signer)?;
+
+ // read and validate the signed manifest store
+ dest.rewind()?;
+
+ let reader = Reader::from_stream(format, &mut dest)?;
+
+ // extract a thumbnail image from the ManifestStore
+ let mut thumbnail = Cursor::new(Vec::new());
+ if let Some(manifest) = reader.active_manifest() {
+ if let Some(thumbnail_ref) = manifest.thumbnail_ref() {
+ reader.resource_to_stream(&thumbnail_ref.identifier, &mut thumbnail)?;
+ println!(
+ "wrote thumbnail {} of size {}",
+ thumbnail_ref.format,
+ thumbnail.get_ref().len()
+ );
+ }
+ }
+
+ println!("{}", reader.json());
+ assert!(reader.validation_status().is_none());
+ assert_eq!(reader.active_manifest().unwrap().title().unwrap(), title);
+
+ Ok(())
+}
+
+// Sign the data using the Ed25519 algorithm
+fn ed_sign(data: &[u8], private_key: &[u8]) -> c2pa::Result<Vec<u8>> {
+ use ed25519_dalek::{Signature, Signer, SigningKey};
+ use pem::parse;
+
+ // Parse the PEM data to get the private key
+ let pem = parse(private_key).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
+ // For Ed25519, the key is 32 bytes long, so we skip the first 16 bytes of the PEM data
+ let key_bytes = &pem.contents()[16..];
+ let signing_key =
+ SigningKey::try_from(key_bytes).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
+ // Sign the data
+ let signature: Signature = signing_key.sign(data);
+
+ Ok(signature.to_bytes().to_vec())
+}
+
+// #[cfg(feature = "openssl")]
+// use openssl::{error::ErrorStack, pkey::PKey};
+// #[cfg(feature = "openssl")]
+// fn ed_sign(data: &[u8], pkey: &[u8]) -> std::result::Result<Vec<u8>, ErrorStack> {
+// let pkey = PKey::private_key_from_pem(pkey)?;
+// let mut signer = openssl::sign::Signer::new_without_digest(&pkey)?;
+// signer.sign_oneshot_to_vec(data)
+// }
+
+#[cfg(test)]
+mod tests {
+ #[cfg(target_arch = "wasm32")]
+ use wasm_bindgen_test::*;
+
+ use super::*;
+
+ #[cfg_attr(not(target_arch = "wasm32"), actix::test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ async fn test_v2_api() -> Result<()> {
+ main()
+ }
+}
diff --git a/sdk/examples/v2show.rs b/sdk/examples/v2show.rs
@@ -0,0 +1,64 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! Example App that generates a manifest store listing for a given file
+
+use anyhow::Result;
+#[cfg(target_arch = "wasm32")]
+fn main() -> Result<()> {
+ Ok(())
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn main() -> Result<()> {
+ use std::io::Read;
+
+ use c2pa::{format_from_path, Error, Reader};
+
+ let args: Vec<String> = std::env::args().collect();
+ if args.len() > 1 {
+ let path = std::path::PathBuf::from(&args[1]);
+ let format = format_from_path(&path).ok_or(Error::UnsupportedType)?;
+ let mut file = std::fs::File::open(&path)?;
+
+ let reader = match Reader::from_stream(&format, &mut file) {
+ Ok(reader) => Ok(reader),
+ Err(Error::RemoteManifestUrl(url)) => {
+ println!("Fetching remote manifest from {}", url);
+ let mut c2pa_data = Vec::new();
+ let resp = ureq::get(&url).call()?;
+ resp.into_reader().read_to_end(&mut c2pa_data)?;
+ Reader::from_manifest_data_and_stream(&c2pa_data, &format, &mut file)
+ }
+ Err(Error::JumbfNotFound) => {
+ // if not embedded or cloud, check for sidecar first and load if it exists
+ let potential_sidecar_path = path.with_extension("c2pa");
+ if potential_sidecar_path.exists() {
+ let manifest_data = std::fs::read(potential_sidecar_path)?;
+ Ok(Reader::from_manifest_data_and_stream(
+ &manifest_data,
+ &format,
+ &mut file,
+ )?)
+ } else {
+ Err(Error::JumbfNotFound)
+ }
+ }
+ Err(e) => Err(e),
+ }?;
+ println!("{reader}");
+ } else {
+ println!("Prints a manifest report (requires a file path argument)")
+ }
+ Ok(())
+}
diff --git a/sdk/src/assertion.rs b/sdk/src/assertion.rs
@@ -562,6 +562,21 @@ impl AssertionDecodeError {
source: source.into(),
}
}
+
+ #[cfg(feature = "unstable_api")]
+ pub(crate) fn from_err<S: Into<AssertionDecodeErrorCause>>(
+ label: String,
+ version: Option<usize>,
+ content_type: String,
+ source: S,
+ ) -> Self {
+ Self {
+ label,
+ version,
+ content_type,
+ source: source.into(),
+ }
+ }
}
impl std::fmt::Debug for AssertionDecodeError {
diff --git a/sdk/src/assertions/box_hash.rs b/sdk/src/assertions/box_hash.rs
@@ -301,11 +301,13 @@ impl AssertionBase for BoxHash {
}
}
+#[cfg(feature = "file_io")]
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
+ #[cfg(test)]
use crate::{jumbf_io::get_assetio_handler_from_path, utils::test::fixture_path};
#[test]
diff --git a/sdk/src/assertions/exif.rs b/sdk/src/assertions/exif.rs
@@ -17,7 +17,11 @@ use std::collections::HashMap;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{json, Value};
-use crate::{assertions::labels, Assertion, AssertionBase, AssertionJson, Error, Result};
+use crate::{
+ assertion::{Assertion, AssertionBase, AssertionJson},
+ assertions::labels,
+ Error, Result,
+};
/// The EXIF assertion as defined in the C2PA spec section 17.13
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_exif_information>
diff --git a/sdk/src/assertions/metadata.rs b/sdk/src/assertions/metadata.rs
@@ -248,7 +248,7 @@ pub enum ReviewCode {
Other(String),
}
-/// A rating on an [`Assertion`].
+/// A rating on an Assertion.
///
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review>.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
diff --git a/sdk/src/asset_handlers/c2pa_io.rs b/sdk/src/asset_handlers/c2pa_io.rs
@@ -15,7 +15,8 @@ use std::{fs::File, path::Path};
use crate::{
asset_io::{
- AssetIO, CAIRead, CAIReader, ComposedManifestRef, HashBlockObjectType, HashObjectPositions,
+ AssetIO, CAIRead, CAIReadWrite, CAIReader, CAIWriter, ComposedManifestRef,
+ HashBlockObjectType, HashObjectPositions,
},
error::{Error, Result},
};
@@ -43,6 +44,36 @@ impl CAIReader for C2paIO {
}
}
+impl CAIWriter for C2paIO {
+ fn write_cai(
+ &self,
+ _input_stream: &mut dyn CAIRead,
+ output_stream: &mut dyn CAIReadWrite,
+ store_bytes: &[u8],
+ ) -> Result<()> {
+ // just write the store bytes and ingore the input stream
+ output_stream.write_all(store_bytes)?;
+ Ok(())
+ }
+
+ fn get_object_locations_from_stream(
+ &self,
+ __input_stream: &mut dyn CAIRead,
+ ) -> Result<Vec<HashObjectPositions>> {
+ // there is no data to hash
+ Ok(vec![])
+ }
+
+ fn remove_cai_store_from_stream(
+ &self,
+ _input_stream: &mut dyn CAIRead,
+ _output_stream: &mut dyn CAIReadWrite,
+ ) -> Result<()> {
+ // nothing to do here, just return Ok
+ Ok(())
+ }
+}
+
impl AssetIO for C2paIO {
fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> {
let mut f = File::open(asset_path)?;
@@ -113,7 +144,7 @@ pub mod tests {
use tempfile::tempdir;
- use super::{AssetIO, C2paIO};
+ use super::{AssetIO, C2paIO, CAIReader, CAIWriter};
use crate::{
status_tracker::OneShotStatusTracker,
store::Store,
@@ -141,4 +172,23 @@ pub mod tests {
let manifest2 = store.to_jumbf(signer.as_ref()).expect("to_jumbf");
assert_eq!(&manifest, &manifest2);
}
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn c2pa_stream_io() {
+ use std::io::{empty, Cursor};
+ let path = fixture_path("C.jpg");
+
+ let c2pa_io = C2paIO {};
+ let manifest = crate::jumbf_io::load_jumbf_from_file(&path).expect("load_jumbf_from_file");
+ let mut output_stream = Cursor::new(Vec::new());
+ c2pa_io
+ .write_cai(&mut empty(), &mut output_stream, &manifest)
+ .expect("write_cai");
+
+ output_stream.set_position(0);
+ let manifest2 = c2pa_io.read_cai(&mut output_stream).expect("read_cai");
+
+ assert_eq!(&manifest, &manifest2);
+ }
}
diff --git a/sdk/src/asset_handlers/pdf_io.rs b/sdk/src/asset_handlers/pdf_io.rs
@@ -15,8 +15,8 @@ use std::{fs::File, path::Path};
use crate::{
asset_handlers::pdf::{C2paPdf, Pdf},
- asset_io::{AssetIO, CAIReader, CAIWriter, ComposedManifestRef, HashObjectPositions},
- CAIRead, Error,
+ asset_io::{AssetIO, CAIRead, CAIReader, CAIWriter, ComposedManifestRef, HashObjectPositions},
+ Error,
Error::{JumbfNotFound, NotImplemented, PdfReadError},
};
diff --git a/sdk/src/asset_handlers/riff_io.rs b/sdk/src/asset_handlers/riff_io.rs
@@ -673,6 +673,27 @@ pub mod tests {
}
#[test]
+ fn test_write_wav_stream() {
+ let more_data = "some more test data".as_bytes();
+ let mut source = File::open(fixture_path("sample1.wav")).unwrap();
+
+ let riff_io = RiffIO::new("wav");
+ if let Ok(temp_dir) = tempdir() {
+ let output = temp_dir_path(&temp_dir, "sample1-wav.wav");
+
+ let mut output_stream = File::create(&output).unwrap();
+
+ riff_io
+ .write_cai(&mut source, &mut output_stream, more_data)
+ .unwrap();
+
+ let mut source = File::open(output).unwrap();
+ let read_test_data = riff_io.read_cai(&mut source).unwrap();
+ assert!(vec_compare(more_data, &read_test_data));
+ }
+ }
+
+ #[test]
fn test_patch_write_wav() {
let test_data = "some test data".as_bytes();
let source = fixture_path("sample1.wav");
diff --git a/sdk/src/asset_handlers/tiff_io.rs b/sdk/src/asset_handlers/tiff_io.rs
@@ -52,11 +52,12 @@ const TILEOFFSETS: u16 = 324;
const SUBFILES: [u16; 3] = [SUBFILE_TAG, EXIFIFD_TAG, GPSIFD_TAG];
-static SUPPORTED_TYPES: [&str; 9] = [
+static SUPPORTED_TYPES: [&str; 10] = [
"tif",
"tiff",
"image/tiff",
"dng",
+ "image/dng",
"image/x-adobe-dng",
"arw",
"image/x-sony-arw",
@@ -1014,7 +1015,7 @@ impl<T: Read + Write + Seek> TiffCloner<T> {
self.writer.seek(SeekFrom::Start(curr_pos))?;
self.writer.write_u32(0)?;
}
-
+ self.writer.flush()?;
Ok(())
}
diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs
@@ -13,7 +13,7 @@
use std::{
fmt,
- io::{Read, Seek, Write},
+ io::{Cursor, Read, Seek, Write},
path::Path,
};
@@ -40,17 +40,15 @@ pub struct HashObjectPositions {
pub htype: HashBlockObjectType, // type of hash block object
}
-// Disable `Send` for wasm32 since we are not sending data across threads
-#[cfg(target_arch = "wasm32")]
-pub trait CAIRead: Read + Seek {}
-#[cfg(not(target_arch = "wasm32"))]
pub trait CAIRead: Read + Seek + Send {}
-impl CAIRead for std::fs::File {}
-impl CAIRead for std::io::Cursor<&[u8]> {}
-impl CAIRead for std::io::Cursor<&mut [u8]> {}
-impl CAIRead for std::io::Cursor<Vec<u8>> {}
-impl CAIRead for NamedTempFile {}
+impl<T> CAIRead for T where T: Read + Seek + Send {}
+
+impl From<String> for Box<dyn CAIRead> {
+ fn from(val: String) -> Self {
+ Box::new(Cursor::new(val))
+ }
+}
// Helper struct to create a concrete type for CAIRead when
// that is required. For example a function defined like this
@@ -75,10 +73,7 @@ impl Seek for CAIReadWrapper<'_> {
pub trait CAIReadWrite: CAIRead + Write {}
-impl CAIReadWrite for std::fs::File {}
-impl CAIReadWrite for std::io::Cursor<&mut [u8]> {}
-impl CAIReadWrite for std::io::Cursor<Vec<u8>> {}
-impl CAIReadWrite for NamedTempFile {}
+impl<T> CAIReadWrite for T where T: CAIRead + Write {}
// Helper struct to create a concrete type for CAIReadWrite when
// that is required. For example a function defined like this
@@ -164,6 +159,7 @@ pub trait AssetIO: Sync + Send {
}
// Return entire CAI block as Vec<u8>
+ #[allow(dead_code)]
fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>>;
// Write the CAI block to an asset
@@ -227,7 +223,7 @@ pub trait AssetBoxHash {
// Type of remote reference to embed. Some of the listed
// emums are for future uses and experiments.
-#[allow(unused_variables)]
+#[allow(dead_code)]
pub enum RemoteRefEmbedType {
Xmp(String),
StegoS(String),
diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs
@@ -0,0 +1,1105 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::{
+ collections::HashMap,
+ io::{Read, Seek, Write},
+};
+
+use async_generic::async_generic;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use uuid::Uuid;
+use zip::{write::FileOptions, ZipArchive, ZipWriter};
+
+use crate::{
+ assertion::AssertionBase,
+ assertions::{labels, Actions, CreativeWork, Exif, SoftwareAgent, Thumbnail, User, UserCbor},
+ claim::Claim,
+ error::{Error, Result},
+ ingredient::Ingredient,
+ resource_store::{ResourceRef, ResourceResolver, ResourceStore},
+ salt::DefaultSalt,
+ store::Store,
+ utils::mime::format_to_mime,
+ AsyncSigner, ClaimGeneratorInfo, Signer,
+};
+
+/// A Manifest Definition
+/// This is used to define a manifest and is used to build a ManifestStore
+/// A Manifest is a collection of ingredients and assertions
+/// It is used to define a claim that can be signed and embedded into a file
+#[skip_serializing_none]
+#[derive(Debug, Default, Deserialize, Serialize)]
+#[non_exhaustive]
+pub struct ManifestDefinition {
+ /// Optional prefix added to the generated Manifest Label
+ /// This is typically Internet domain name for the vendor (i.e. `adobe`)
+ pub vendor: Option<String>,
+
+ /// Clam Generator Info is always required with at least one entry
+ #[serde(default = "default_claim_generator_info")]
+ pub claim_generator_info: Vec<ClaimGeneratorInfo>,
+
+ /// A human-readable title, generally source filename.
+ pub title: Option<String>,
+
+ /// The format of the source file as a MIME type.
+ #[serde(default = "default_format")]
+ pub format: String,
+
+ /// Instance ID from `xmpMM:InstanceID` in XMP metadata.
+ #[serde(default = "default_instance_id")]
+ pub instance_id: String,
+
+ pub thumbnail: Option<ResourceRef>,
+
+ /// A List of ingredients
+ #[serde(default = "default_vec::<Ingredient>")]
+ pub ingredients: Vec<Ingredient>,
+
+ /// A list of assertions
+ #[serde(default = "default_vec::<AssertionDefinition>")]
+ pub assertions: Vec<AssertionDefinition>,
+
+ /// A list of redactions - URIs to a redacted assertions
+ pub redactions: Option<Vec<String>>,
+
+ pub label: Option<String>,
+}
+
+fn default_instance_id() -> String {
+ format!("xmp:iid:{}", Uuid::new_v4())
+}
+
+fn default_claim_generator_info() -> Vec<ClaimGeneratorInfo> {
+ [ClaimGeneratorInfo::default()].to_vec()
+}
+
+fn default_format() -> String {
+ "application/octet-stream".to_owned()
+}
+
+fn default_vec<T>() -> Vec<T> {
+ Vec::new()
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+#[serde(untagged)]
+pub enum AssertionData {
+ Cbor(serde_cbor::Value),
+ Json(serde_json::Value),
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+#[non_exhaustive]
+pub struct AssertionDefinition {
+ pub label: String,
+ pub data: AssertionData,
+}
+
+use serde::de::DeserializeOwned;
+
+use crate::assertion::AssertionDecodeError;
+impl AssertionDefinition {
+ pub(crate) fn to_assertion<T: DeserializeOwned>(&self) -> Result<T> {
+ match &self.data {
+ AssertionData::Json(value) => serde_json::from_value(value.clone()).map_err(|e| {
+ Error::AssertionDecoding(AssertionDecodeError::from_err(
+ self.label.to_owned(),
+ None,
+ "application/json".to_owned(),
+ e,
+ ))
+ }),
+ AssertionData::Cbor(value) => {
+ serde_cbor::value::from_value(value.clone()).map_err(|e| {
+ Error::AssertionDecoding(AssertionDecodeError::from_err(
+ self.label.to_owned(),
+ None,
+ "application/cbor".to_owned(),
+ e,
+ ))
+ })
+ }
+ }
+ }
+}
+
+/// A Builder is used to add a signed manifest to an asset.
+///
+/// # Example: Building and signing a manifest
+///
+/// ```
+/// # use c2pa::Result;
+/// use std::path::PathBuf;
+///
+/// use c2pa::{create_signer, Builder, SigningAlg};
+/// use serde::Serialize;
+/// use serde_json::json;
+/// use tempfile::tempdir;
+///
+/// #[derive(Serialize)]
+/// struct Test {
+/// my_tag: usize,
+/// }
+///
+/// # fn main() -> Result<()> {
+/// let manifest_json = json!({
+/// "claim_generator_info": [
+/// {
+/// "name": "c2pa_test",
+/// "version": "1.0.0"
+/// }
+/// ],
+/// "title": "Test_Manifest"
+/// }).to_string();
+///
+/// let mut builder = Builder::from_json(&manifest_json)?;
+/// builder.add_assertion("org.contentauth.test", &Test { my_tag: 42 })?;
+///
+/// let source = PathBuf::from("tests/fixtures/C.jpg");
+/// let dir = tempdir()?;
+/// let dest = dir.path().join("test_file.jpg");
+///
+/// // Create a ps256 signer using certs and key files
+/// let signcert_path = "tests/fixtures/certs/ps256.pub";
+/// let pkey_path = "tests/fixtures/certs/ps256.pem";
+/// let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?;
+///
+/// // embed a manifest using the signer
+/// builder.sign(
+/// "image/jpeg",
+/// &mut std::fs::File::open(&source)?,
+/// &mut std::fs::File::create(&dest)?,
+/// signer.as_ref(),
+/// )?;
+/// # Ok(())
+/// # }
+/// ```
+#[skip_serializing_none]
+#[derive(Debug, Default, Deserialize, Serialize)]
+pub struct Builder {
+ #[serde(flatten)]
+ pub definition: ManifestDefinition,
+
+ /// container for binary assets (like thumbnails)
+ #[serde(skip)]
+ resources: ResourceStore,
+}
+
+impl AsRef<Builder> for Builder {
+ fn as_ref(&self) -> &Self {
+ self
+ }
+}
+
+impl Builder {
+ /// Creates a new builder from a JSON [`ManifestDefinition`] string.
+ ///
+ /// # Arguments
+ /// * `json` - A JSON string representing the [`ManifestDefinition`].
+ /// # Returns
+ /// * A new [`Builder`].
+ pub fn from_json(json: &str) -> Result<Self> {
+ Ok(Self {
+ definition: serde_json::from_str(json).map_err(Error::JsonError)?,
+ ..Default::default()
+ })
+ }
+
+ /// Sets the MIME format for this [`Builder`].
+ ///
+ /// # Arguments
+ /// * `format` - The format of the asset associated with this [`Builder`].
+ /// # Returns
+ /// * A mutable reference to the [`Builder`].
+ pub fn set_format(&mut self, format: &str) -> &mut Self {
+ self.definition.format = format.to_string();
+ self
+ }
+
+ /// Sets a thumbnail for the [`Builder`].
+ ///
+ /// The thumbnail should represent the associated asset for this [`Builder`].
+ ///
+ /// # Arguments
+ /// * `format` - The format of the thumbnail.
+ /// * `stream` - A stream to read the thumbnail from.
+ /// # Returns
+ /// * A mutable reference to the [`Builder`].
+ /// # Errors
+ /// * If the thumbnail is not valid.
+ pub fn set_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self>
+ where
+ R: Read + Seek + ?Sized,
+ {
+ // just read into a buffer until resource store handles reading streams
+ let mut resource = Vec::new();
+ stream.read_to_end(&mut resource)?;
+ // add the resource and set the resource reference
+ self.resources
+ .add(&self.definition.instance_id.clone(), resource)?;
+ self.definition.thumbnail = Some(ResourceRef::new(
+ format,
+ self.definition.instance_id.clone(),
+ ));
+ Ok(self)
+ }
+
+ /// Adds a CBOR assertion to the manifest.
+ /// # Arguments
+ /// * `label` - A label for the assertion.
+ /// * `data` - The data for the assertion. The data is any Serde Serializable type.
+ /// # Returns
+ /// * A mutable reference to the [`Builder`].
+ /// # Errors
+ /// * If the assertion is not valid.
+ pub fn add_assertion<S, T>(&mut self, label: S, data: &T) -> Result<&mut Self>
+ where
+ S: Into<String>,
+ T: Serialize,
+ {
+ self.definition.assertions.push(AssertionDefinition {
+ label: label.into(),
+ data: AssertionData::Cbor(serde_cbor::value::to_value(data)?),
+ });
+ Ok(self)
+ }
+
+ /// Adds a Json assertion to the manifest.
+ /// # Arguments
+ /// * `label` - A label for the assertion.
+ /// * `data` - The data for the assertion. The data is any Serde Serializable type.
+ /// # Returns
+ /// * A mutable reference to the [`Builder`].
+ /// # Errors
+ /// * If the assertion is not valid.
+ pub fn add_assertion_json<S, T>(&mut self, label: S, data: &T) -> Result<&mut Self>
+ where
+ S: Into<String>,
+ T: Serialize,
+ {
+ self.definition.assertions.push(AssertionDefinition {
+ label: label.into(),
+ data: AssertionData::Json(serde_json::to_value(data)?),
+ });
+ Ok(self)
+ }
+
+ /// Adds an [`Ingredient`] to the manifest
+ /// # Arguments
+ /// * `ingredient_json` - A JSON string representing the [`Ingredient`].
+ /// * `format` - The format of the [`Ingredient`].
+ /// * `stream` - A stream to read the [`Ingredient`] from.
+ /// # Returns
+ /// * A mutable reference to the [`Ingredient`].
+ /// # Errors
+ /// * If the [`Ingredient`] is not valid
+ pub fn add_ingredient<'a, T, R>(
+ &'a mut self,
+ ingredient_json: T,
+ format: &str,
+ stream: &mut R,
+ ) -> Result<&'a mut Ingredient>
+ where
+ T: Into<String>,
+ R: Read + Seek + Send,
+ {
+ let ingredient: Ingredient = Ingredient::from_json(&ingredient_json.into())?;
+ let ingredient = ingredient.with_stream(format, stream)?;
+ self.definition.ingredients.push(ingredient);
+ #[allow(clippy::unwrap_used)]
+ Ok(self.definition.ingredients.last_mut().unwrap()) // ok since we just added it
+ }
+
+ /// Adds a resource to the manifest.
+ /// The id should match up with an identifier in the manifest.
+ /// # Arguments
+ /// * `id` - The identifier for the resource.
+ /// * `stream` - A stream to read the resource from.
+ /// # Returns
+ /// * A mutable reference to the builder.
+ /// # Errors
+ /// * If the resource is not valid.
+ pub fn add_resource(
+ &mut self,
+ id: &str,
+ mut stream: impl Read + Seek + Send,
+ ) -> Result<&mut Self> {
+ if self.resources.exists(id) {
+ return Err(Error::BadParam(id.to_string())); // todo add specific error
+ }
+ let mut buf = Vec::new();
+ let _size = stream.read_to_end(&mut buf)?;
+ self.resources.add(id, buf)?;
+ Ok(self)
+ }
+
+ /// Convert the Builder into a archive formatted stream.
+ ///
+ /// The archive is a zip formatted stream containing the manifest.json, resources, and ingredients.
+ /// # Arguments
+ /// * `stream` - A stream to write the zip into.
+ /// # Errors
+ /// * If the archive cannot be written.
+ pub fn to_archive(&mut self, stream: impl Write + Seek) -> Result<()> {
+ drop(
+ // this drop seems to be required to force a flush before reading back.
+ {
+ let mut zip = ZipWriter::new(stream);
+ let options =
+ FileOptions::default().compression_method(zip::CompressionMethod::Stored);
+ zip.start_file("manifest.json", options)
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+ zip.write_all(&serde_json::to_vec(self)?)?;
+ // add a folder to the zip file
+ zip.start_file("resources/", options)
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+ for (id, data) in self.resources.resources() {
+ zip.start_file(format!("resources/{}", id), options)
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+ zip.write_all(data)?;
+ }
+ for (index, ingredient) in self.definition.ingredients.iter().enumerate() {
+ zip.start_file(format!("ingredients/{}/", index), options)
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+ for (id, data) in ingredient.resources().resources() {
+ //println!("adding ingredient {}/{}", index, id);
+ zip.start_file(format!("ingredients/{}/{}", index, id), options)
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+ zip.write_all(data)?;
+ }
+ }
+ zip.finish()
+ }
+ .map_err(|e| Error::OtherError(Box::new(e)))?,
+ );
+ Ok(())
+ }
+
+ /// Unpacks an archive stream into a Builder.
+ /// # Arguments
+ /// * `stream` - A stream to read the archive from.
+ /// # Returns
+ /// * A new Builder.
+ /// # Errors
+ /// * If the archive cannot be read.
+ pub fn from_archive(stream: impl Read + Seek) -> Result<Self> {
+ let mut zip = ZipArchive::new(stream).map_err(|e| Error::OtherError(Box::new(e)))?;
+ let mut manifest = zip
+ .by_name("manifest.json")
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+ let mut manifest_json = Vec::new();
+ manifest.read_to_end(&mut manifest_json)?;
+ let mut builder: Builder =
+ serde_json::from_slice(&manifest_json).map_err(|e| Error::OtherError(Box::new(e)))?;
+ drop(manifest);
+ for i in 0..zip.len() {
+ let mut file = zip
+ .by_index(i)
+ .map_err(|e| Error::OtherError(Box::new(e)))?;
+
+ if file.name().starts_with("resources/") && file.name() != "resources/" {
+ let mut data = Vec::new();
+ file.read_to_end(&mut data)?;
+ let id = file
+ .name()
+ .split('/')
+ .nth(1)
+ .ok_or(Error::BadParam("Invalid resource path".to_string()))?;
+ //println!("adding resource {}", id);
+ builder.resources.add(id, data)?;
+ }
+ if file.name().starts_with("ingredients/") && file.name() != "ingredients/" {
+ let mut data = Vec::new();
+ file.read_to_end(&mut data)?;
+ let index: usize = file
+ .name()
+ .split('/')
+ .nth(1)
+ .ok_or_else(|| Error::BadParam("Invalid ingredient path".to_string()))?
+ .parse::<usize>()
+ .map_err(|_| Error::BadParam("Invalid ingredient path".to_string()))?;
+ let id = file.name().split('/').nth(2).unwrap_or_default();
+ if index >= builder.definition.ingredients.len() {
+ return Err(Error::OtherError(Box::new(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ format!("Invalid ingredient index {}", index),
+ ))))?; // todo add specific error
+ }
+ builder.definition.ingredients[index]
+ .resources_mut()
+ .add(id, data)?;
+ }
+ }
+ Ok(builder)
+ }
+
+ // Convert a Manifest into a Claim
+ fn to_claim(&self) -> Result<Claim> {
+ let definition = &self.definition;
+ let mut claim_generator_info = definition.claim_generator_info.clone();
+ // add the default claim generator info for this library
+ claim_generator_info.push(ClaimGeneratorInfo::default());
+
+ // build the claim_generator string since this is required
+ let claim_generator: String = claim_generator_info
+ .iter()
+ .map(|s| {
+ let name = s.name.replace(' ', "_");
+ if let Some(version) = s.version.as_deref() {
+ format!("{}/{}", name.to_lowercase(), version)
+ } else {
+ name
+ }
+ })
+ .collect::<Vec<String>>()
+ .join(" ");
+
+ let mut claim = match definition.label.as_ref() {
+ Some(label) => Claim::new_with_user_guid(&claim_generator, &label.to_string()),
+ None => Claim::new(&claim_generator, definition.vendor.as_deref()),
+ };
+
+ // add claim generator info to claim resolving icons
+ for info in &claim_generator_info {
+ let mut claim_info = info.to_owned();
+ if let Some(icon) = claim_info.icon.as_ref() {
+ claim_info.icon = Some(icon.to_hashed_uri(&self.resources, &mut claim)?);
+ }
+ claim.add_claim_generator_info(claim_info);
+ }
+
+ // if let Some(remote_op) = &self.remote_manifest {
+ // match remote_op {
+ // RemoteManifest::NoRemote => (),
+ // RemoteManifest::SideCar => claim.set_external_manifest(),
+ // RemoteManifest::Remote(r) => claim.set_remote_manifest(r)?,
+ // RemoteManifest::EmbedWithRemote(r) => claim.set_embed_remote_manifest(r)?,
+ // };
+ // }
+
+ if let Some(title) = definition.title.as_ref() {
+ claim.set_title(Some(title.to_owned()));
+ }
+ claim.format = definition.format.to_owned();
+ claim.instance_id = definition.instance_id.to_owned();
+
+ if let Some(thumb_ref) = definition.thumbnail.as_ref() {
+ // Setting the format to "none" will ensure that no claim thumbnail is added
+ if thumb_ref.format != "none" {
+ //let data = self.resources.get(&thumb_ref.identifier)?;
+ let mut stream = self.resources.open(thumb_ref)?;
+ let mut data = Vec::new();
+ stream.read_to_end(&mut data)?;
+ claim.add_assertion(&Thumbnail::new(
+ &labels::add_thumbnail_format(labels::CLAIM_THUMBNAIL, &thumb_ref.format),
+ data,
+ ))?;
+ }
+ }
+
+ let mut ingredient_map = HashMap::new();
+ // add all ingredients to the claim
+ for ingredient in &definition.ingredients {
+ //let ingredient = ingredient_builder.build(self)?;
+ let uri = ingredient.add_to_claim(
+ &mut claim,
+ definition.redactions.clone(),
+ Some(&self.resources),
+ )?;
+ ingredient_map.insert(ingredient.instance_id().to_string(), uri);
+ }
+
+ let salt = DefaultSalt::default();
+
+ // add any additional assertions
+ for manifest_assertion in &definition.assertions {
+ match manifest_assertion.label.as_str() {
+ l if l.starts_with(Actions::LABEL) => {
+ let version = labels::version(l);
+
+ let mut actions: Actions = manifest_assertion.to_assertion()?;
+
+ let ingredients_key = match version {
+ None | Some(1) => "ingredient",
+ Some(2) => "ingredients",
+ _ => return Err(Error::AssertionUnsupportedVersion),
+ };
+
+ // fixup parameters field from instance_id to ingredient uri
+ let needs_ingredient: Vec<(usize, crate::assertions::Action)> = actions
+ .actions()
+ .iter()
+ .enumerate()
+ .filter_map(|(i, a)| {
+ if a.instance_id().is_some()
+ && a.get_parameter(ingredients_key).is_none()
+ {
+ Some((i, a.clone()))
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ for (index, action) in needs_ingredient {
+ if let Some(id) = action.instance_id() {
+ if let Some(hash_url) = ingredient_map.get(id) {
+ let update = match ingredients_key {
+ "ingredient" => {
+ action.set_parameter(ingredients_key, hash_url.clone())
+ }
+ _ => {
+ // we only support on instanceId for actions, so only one ingredient on writing
+ action.set_parameter(ingredients_key, [hash_url.clone()])
+ }
+ }?;
+ actions = actions.update_action(index, update);
+ }
+ }
+ }
+
+ if let Some(templates) = actions.templates.as_mut() {
+ for template in templates {
+ // replace icon with hashed_uri
+ template.icon = match template.icon.take() {
+ Some(icon) => {
+ Some(icon.to_hashed_uri(&self.resources, &mut claim)?)
+ }
+ None => None,
+ };
+
+ // replace software agent with hashed_uri
+ template.software_agent = match template.software_agent.take() {
+ Some(SoftwareAgent::ClaimGeneratorInfo(mut info)) => {
+ if let Some(icon) = info.icon.as_mut() {
+ let icon =
+ icon.to_hashed_uri(&self.resources, &mut claim)?;
+ info.set_icon(icon);
+ }
+ Some(SoftwareAgent::ClaimGeneratorInfo(info))
+ }
+ agent => agent,
+ };
+ }
+ }
+
+ // convert icons in software agents to hashed uris
+ let actions_mut = actions.actions_mut();
+ #[allow(clippy::needless_range_loop)]
+ // clippy is wrong here, we reference index twice
+ for index in 0..actions_mut.len() {
+ let action = &actions_mut[index];
+ if let Some(SoftwareAgent::ClaimGeneratorInfo(info)) =
+ action.software_agent()
+ {
+ if let Some(icon) = info.icon.as_ref() {
+ let mut info = info.to_owned();
+ let icon_uri = icon.to_hashed_uri(&self.resources, &mut claim)?;
+ let update = info.set_icon(icon_uri);
+ let mut action = action.to_owned();
+ action = action.set_software_agent(update.to_owned());
+ actions_mut[index] = action;
+ }
+ }
+ }
+
+ claim.add_assertion(&actions)
+ }
+ CreativeWork::LABEL => {
+ let cw: CreativeWork = manifest_assertion.to_assertion()?;
+
+ claim.add_assertion_with_salt(&cw, &salt)
+ }
+ Exif::LABEL => {
+ let exif: Exif = manifest_assertion.to_assertion()?;
+ claim.add_assertion_with_salt(&exif, &salt)
+ }
+ _ => match &manifest_assertion.data {
+ AssertionData::Json(value) => claim.add_assertion_with_salt(
+ &User::new(&manifest_assertion.label, &serde_json::to_string(&value)?),
+ &salt,
+ ),
+ AssertionData::Cbor(value) => claim.add_assertion_with_salt(
+ &UserCbor::new(&manifest_assertion.label, serde_cbor::to_vec(value)?),
+ &salt,
+ ),
+ },
+ }?;
+ }
+
+ Ok(claim)
+ }
+
+ // Convert a Manifest into a Store
+ fn to_store(&self) -> Result<Store> {
+ let claim = self.to_claim()?;
+ // commit the claim
+ let mut store = Store::new();
+ let _provenance = store.commit_claim(claim)?;
+ Ok(store)
+ }
+
+ #[cfg(feature = "add_thumbnails")]
+ fn maybe_add_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self>
+ where
+ R: Read + Seek + ?Sized,
+ {
+ if self.definition.thumbnail.is_none() {
+ stream.rewind()?;
+ if let Ok((format, image)) =
+ crate::utils::thumbnail::make_thumbnail_from_stream(format, stream)
+ {
+ stream.rewind()?;
+ self.resources
+ .add(&self.definition.instance_id.clone(), image)?;
+ self.definition.thumbnail = Some(ResourceRef::new(
+ format,
+ self.definition.instance_id.clone(),
+ ));
+ }
+ }
+ Ok(self)
+ }
+
+ /// Embed a signed manifest into a stream using a supplied signer.
+ /// # Arguments
+ /// * `format` - The format of the stream
+ /// * `source` - The stream to read from
+ /// * `dest` - The stream to write to
+ /// * `signer` - The signer to use
+ /// # Returns
+ /// * The bytes of c2pa_manifest that was embedded.
+ /// # Errors
+ /// * If the manifest cannot be signed.
+ #[async_generic(async_signature(
+ &mut self,
+ format: &str,
+ source: &mut R,
+ dest: &mut W,
+ signer: &dyn AsyncSigner
+ ))]
+ pub fn sign<R, W>(
+ &mut self,
+ format: &str,
+ source: &mut R,
+ dest: &mut W,
+ signer: &dyn Signer,
+ ) -> Result<Vec<u8>>
+ where
+ R: Read + Seek + Send,
+ W: Write + Read + Seek + Send,
+ {
+ let format = format_to_mime(format);
+ self.definition.format = format.clone();
+ // todo:: read instance_id from xmp from stream ?
+ self.definition.instance_id = format!("xmp:iid:{}", Uuid::new_v4());
+
+ // generate thumbnail if we don't already have one
+ #[cfg(feature = "add_thumbnails")]
+ self.maybe_add_thumbnail(&format, source)?;
+
+ // convert the manifest to a store
+ let mut store = self.to_store()?;
+
+ // sign and write our store to to the output image file
+ if _sync {
+ store.save_to_stream(&format, source, dest, signer)
+ } else {
+ store
+ .save_to_stream_async(&format, source, dest, signer)
+ .await
+ }
+ }
+
+ #[cfg(feature = "file_io")]
+ /// Sign a file using a supplied signer.
+ /// # Arguments
+ /// * `source` - The path to the file to read from.
+ /// * `dest` - The path to the file to write to (this must not already exist).
+ /// * `signer` - The signer to use.
+ /// # Returns
+ /// * The bytes of c2pa_manifest that was created.
+ /// # Errors
+ /// * If the manifest cannot be signed.
+ pub fn sign_file<S, D>(&mut self, source: S, dest: D, signer: &dyn Signer) -> Result<Vec<u8>>
+ where
+ S: AsRef<std::path::Path>,
+ D: AsRef<std::path::Path>,
+ {
+ let source = source.as_ref();
+ let dest = dest.as_ref();
+ // formats must match but allow extensions to be slightly different (i.e. .jpeg vs .jpg)s
+ let format = crate::format_from_path(source).ok_or(crate::Error::UnsupportedType)?;
+ let format_dest = crate::format_from_path(dest).ok_or(crate::Error::UnsupportedType)?;
+ if format != format_dest {
+ return Err(crate::Error::BadParam(
+ "Source and destination file formats must match".to_string(),
+ ));
+ }
+ let mut source = std::fs::File::open(source)?;
+ if !dest.exists() {
+ // ensure the path to the file exists
+ if let Some(output_dir) = dest.parent() {
+ std::fs::create_dir_all(output_dir)?;
+ }
+ } else {
+ // if the file exists, we need to remove it to avoid appending to it
+ return Err(crate::Error::BadParam(
+ "Destination file already exists".to_string(),
+ ));
+ };
+ let mut dest = std::fs::File::create(dest)?;
+
+ self.sign(&format, &mut source, &mut dest, signer)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::expect_used)]
+ #![allow(clippy::unwrap_used)]
+ use std::io::Cursor;
+
+ use serde_json::json;
+ #[cfg(target_arch = "wasm32")]
+ use wasm_bindgen_test::*;
+
+ use super::*;
+ use crate::{manifest_store::ManifestStore, utils::test::temp_signer};
+ #[cfg(target_arch = "wasm32")]
+ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+ fn parent_json() -> String {
+ json!({
+ "title": "Parent Test",
+ "format": "image/jpeg",
+ "instance_id": "12345",
+ "relationship": "parentOf"
+ })
+ .to_string()
+ }
+
+ fn manifest_json() -> String {
+ json!({
+ "vendor": "test",
+ "claim_generator_info": [
+ {
+ "name": "c2pa_test",
+ "version": "1.0.0"
+ }
+ ],
+ "title": "Test_Manifest",
+ "format": "image/tiff",
+ "instance_id": "1234",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "thumbnail1.jpg"
+ },
+ "ingredients": [
+ {
+ "title": "Test",
+ "format": "image/jpeg",
+ "instance_id": "12345",
+ "relationship": "componentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "org.test.assertion",
+ "data": "assertion"
+ }
+ ]
+ })
+ .to_string()
+ }
+
+ const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
+
+ #[test]
+ /// example of creating a builder directly with a [`ManifestDefinition`]
+ fn test_manifest_store_builder() {
+ let mut image = Cursor::new(TEST_IMAGE);
+
+ let thumbnail_ref = ResourceRef::new("ingredient/jpeg", "5678");
+
+ let definition = ManifestDefinition {
+ vendor: Some("test".to_string()),
+ claim_generator_info: [ClaimGeneratorInfo::default()].to_vec(),
+ format: "image/tiff".to_string(),
+ title: Some("Test_Manifest".to_string()),
+ instance_id: "1234".to_string(),
+ thumbnail: Some(thumbnail_ref.clone()),
+ label: Some("ABCDE".to_string()),
+ ..Default::default()
+ };
+
+ let mut builder = Builder {
+ definition,
+ ..Default::default()
+ };
+
+ builder
+ .add_ingredient(parent_json(), "image/jpeg", &mut image)
+ .unwrap();
+
+ builder
+ .add_assertion("org.test.assertion", &"assertion".to_string())
+ .unwrap();
+
+ builder
+ .add_resource(&thumbnail_ref.identifier, Cursor::new(b"12345"))
+ .unwrap();
+
+ let definition = &builder.definition;
+ assert_eq!(definition.vendor, Some("test".to_string()));
+ assert_eq!(definition.title, Some("Test_Manifest".to_string()));
+ assert_eq!(definition.format, "image/tiff".to_string());
+ assert_eq!(definition.instance_id, "1234".to_string());
+ assert_eq!(definition.thumbnail, Some(thumbnail_ref));
+ assert_eq!(definition.ingredients[0].title(), "Parent Test".to_string());
+ assert_eq!(
+ definition.assertions[0].label,
+ "org.test.assertion".to_string()
+ );
+ assert_eq!(definition.label, Some("ABCDE".to_string()));
+ assert_eq!(
+ builder
+ .resources
+ .get(&builder.definition.thumbnail.unwrap().identifier)
+ .unwrap()
+ .into_owned(),
+ b"12345"
+ );
+ }
+
+ #[test]
+ fn test_from_json() {
+ // strip whitespace so we can compare later
+ let mut stripped_json = manifest_json();
+ stripped_json.retain(|c| !c.is_whitespace());
+ let mut builder = Builder::from_json(&stripped_json).unwrap();
+ builder.resources.add("5678", "12345").unwrap();
+ let definition = &builder.definition;
+ assert_eq!(definition.vendor, Some("test".to_string()));
+ assert_eq!(definition.title, Some("Test_Manifest".to_string()));
+ assert_eq!(definition.format, "image/tiff".to_string());
+ assert_eq!(definition.instance_id, "1234".to_string());
+ assert_eq!(
+ definition.thumbnail.clone().unwrap().identifier.as_str(),
+ "thumbnail1.jpg"
+ );
+ assert_eq!(definition.ingredients[0].title(), "Test".to_string());
+ assert_eq!(
+ definition.assertions[0].label,
+ "org.test.assertion".to_string()
+ );
+
+ // convert back to json and compare to original
+ let builder_json = serde_json::to_string(&builder.definition).unwrap();
+ assert_eq!(builder_json, stripped_json);
+ }
+
+ #[test]
+ fn test_builder_sign() {
+ #[derive(Serialize, Deserialize)]
+ struct TestAssertion {
+ answer: usize,
+ }
+ let format = "image/jpeg";
+ let mut source = Cursor::new(TEST_IMAGE);
+ let mut dest = Cursor::new(Vec::new());
+
+ let mut builder = Builder::from_json(&manifest_json()).unwrap();
+ builder
+ .add_ingredient(parent_json().to_string(), format, &mut source)
+ .unwrap();
+
+ builder
+ .resources
+ .add("thumbnail1.jpg", TEST_IMAGE.to_vec())
+ .unwrap();
+
+ builder
+ .add_assertion("org.life.meaning", &TestAssertion { answer: 42 })
+ .unwrap();
+
+ builder
+ .add_assertion_json("org.life.meaning.json", &TestAssertion { answer: 42 })
+ .unwrap();
+
+ // write the manifest builder to a zipped stream
+ let mut zipped = Cursor::new(Vec::new());
+ builder.to_archive(&mut zipped).unwrap();
+
+ // write the zipped stream to a file for debugging
+ std::fs::write("../target/test.zip", zipped.get_ref()).unwrap();
+
+ // unzip the manifest builder from the zipped stream
+ zipped.rewind().unwrap();
+ let mut _builder = Builder::from_archive(&mut zipped).unwrap();
+
+ // sign the ManifestStoreBuilder and write it to the output stream
+ let signer = temp_signer();
+ builder
+ .sign(format, &mut source, &mut dest, signer.as_ref())
+ .unwrap();
+
+ // read and validate the signed manifest store
+ dest.rewind().unwrap();
+ let manifest_store =
+ ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes");
+
+ println!("{}", manifest_store);
+ assert!(manifest_store.validation_status().is_none());
+ assert!(manifest_store.get_active().is_some());
+ let manifest = manifest_store.get_active().unwrap();
+ assert_eq!(manifest.title().unwrap(), "Test_Manifest");
+ let test_assertion: TestAssertion = manifest.find_assertion("org.life.meaning").unwrap();
+ assert_eq!(test_assertion.answer, 42);
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_builder_sign_file() {
+ let source = "tests/fixtures/CA.jpg";
+ let dir = tempfile::tempdir().unwrap();
+ let dest = dir.path().join("test_file.jpg");
+
+ let mut builder = Builder::from_json(&manifest_json()).unwrap();
+
+ builder
+ .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
+ .unwrap();
+
+ // sign the ManifestStoreBuilder and write it to the output stream
+ let signer = temp_signer();
+ builder.sign_file(source, &dest, signer.as_ref()).unwrap();
+
+ // read and validate the signed manifest store
+ let manifest_store = ManifestStore::from_file(&dest).expect("from_bytes");
+
+ println!("{}", manifest_store);
+ assert!(manifest_store.validation_status().is_none());
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ "Test_Manifest"
+ );
+ }
+
+ #[test]
+ #[cfg(feature = "file_io")]
+ fn test_builder_sign_assets() {
+ const TESTFILES: &[&str] = &[
+ "IMG_0003.jpg",
+ "sample1.png",
+ "sample1.webp",
+ "TUSCANY.TIF",
+ "sample1.svg",
+ //"APC_0808.dng",
+ "sample1.wav",
+ "test.avi",
+ //"sample1.mp3",
+ //"sample1.avif",
+ //"sample1.heic",
+ //"sample1.heif",
+ //"video1.mp4",
+ //"cloud_manifest.c2pa",
+ ];
+ for file_name in TESTFILES {
+ let extension = file_name.split('.').last().unwrap();
+ let format = extension;
+
+ let path = format!("tests/fixtures/{}", file_name);
+ println!("path: {}", path);
+ let mut source = std::fs::File::open(path).unwrap();
+ let mut dest = Cursor::new(Vec::new());
+
+ let mut builder = Builder::from_json(&manifest_json()).unwrap();
+ builder
+ .add_ingredient(parent_json(), format, &mut source)
+ .unwrap();
+
+ builder
+ .add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
+ .unwrap();
+
+ // sign the ManifestStoreBuilder and write it to the output stream
+ let signer = temp_signer();
+ builder
+ .sign(format, &mut source, &mut dest, signer.as_ref())
+ .unwrap();
+
+ // read and validate the signed manifest store
+ dest.rewind().unwrap();
+ let manifest_store =
+ ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes");
+
+ println!("{}", manifest_store);
+ assert!(manifest_store.validation_status().is_none());
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ "Test_Manifest"
+ );
+
+ // enable to write the signed manifests to a file for debugging
+ // let dest_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
+ // .join("../target")
+ // .join("signed")
+ // .join(file_name);
+
+ // std::fs::create_dir_all(dest_path.parent().unwrap()).unwrap();
+ // std::fs::write(&dest_path, dest.get_ref()).unwrap();
+ }
+ }
+
+ #[cfg_attr(not(target_arch = "wasm32"), actix::test)]
+ #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ async fn test_builder_remote_sign() {
+ let format = "image/jpeg";
+ let mut source = Cursor::new(TEST_IMAGE);
+ let mut dest = Cursor::new(Vec::new());
+
+ let mut builder = Builder::from_json(&manifest_json()).unwrap();
+ builder
+ .add_ingredient(&parent_json(), format, &mut source)
+ .unwrap();
+
+ builder
+ .resources
+ .add("thumbnail1.jpg", TEST_IMAGE.to_vec())
+ .unwrap();
+
+ // sign the ManifestStoreBuilder and write it to the output stream
+ let signer = crate::utils::test::temp_async_remote_signer();
+ builder
+ .sign_async(format, &mut source, &mut dest, signer.as_ref())
+ .await
+ .unwrap();
+
+ // read and validate the signed manifest store
+ dest.rewind().unwrap();
+ let manifest_store =
+ ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes");
+
+ println!("{}", manifest_store);
+ #[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working
+ assert!(manifest_store.validation_status().is_none());
+ assert_eq!(
+ manifest_store.get_active().unwrap().title().unwrap(),
+ "Test_Manifest"
+ );
+ }
+}
diff --git a/sdk/src/callback_signer.rs b/sdk/src/callback_signer.rs
@@ -0,0 +1,161 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+#![deny(missing_docs)]
+
+//! The `callback_signer` module provides a way to obtain a [`Signer`] or [`AsyncSigner`]
+//! using a callback and public signing certificates.
+
+use crate::{
+ error::{Error, Result},
+ AsyncSigner, Signer, SigningAlg,
+};
+
+/// Defines a callback function interface for a [`CallbackSigner`].
+///
+/// The callback should return a signature for the given data.
+/// The callback should return an error if the data cannot be signed.
+pub type CallbackFunc = dyn Fn(*const (), &[u8]) -> std::result::Result<Vec<u8>, Error>;
+
+/// Defines a signer that uses a callback to sign data.
+///
+/// The private key should only be known by the callback.
+pub struct CallbackSigner {
+ /// An opaque context for the signer, used to store any necessary state.
+ pub context: *const (),
+
+ /// The callback to use to sign data.
+ pub callback: Box<CallbackFunc>,
+
+ /// The signing algorithm to use.
+ pub alg: SigningAlg,
+
+ /// The public certificates to use in PEM format.
+ pub certs: Vec<u8>,
+
+ /// A max size to reserve for the signature.
+ pub reserve_size: usize,
+
+ /// The optional URL of a Time Stamping Authority.
+ pub tsa_url: Option<String>,
+}
+
+unsafe impl Send for CallbackSigner {}
+
+unsafe impl Sync for CallbackSigner {}
+
+impl CallbackSigner {
+ /// Create a new callback signer.
+ pub fn new<F, T>(callback: F, alg: SigningAlg, certs: T) -> Self
+ where
+ F: Fn(*const (), &[u8]) -> std::result::Result<Vec<u8>, Error> + 'static,
+ T: Into<Vec<u8>>,
+ {
+ let certs = certs.into();
+ let reserve_size = 10000 + certs.len();
+ Self {
+ context: std::ptr::null(),
+ callback: Box::new(callback),
+ alg,
+ certs,
+ reserve_size,
+ ..Default::default()
+ }
+ }
+
+ /// Set a time stamping authority URL to call when signing.
+ pub fn set_tsa_url<S: Into<String>>(mut self, url: S) -> Self {
+ self.tsa_url = Some(url.into());
+ self
+ }
+
+ /// Set a context value for the signer.
+ ///
+ /// This can be used to store any necessary state for the callback.
+ /// Safety: The context must be valid for the lifetime of the signer.
+ /// There is no Rust memory management for the context since it may also come from FFI.
+ pub fn set_context(mut self, context: *const ()) -> Self {
+ self.context = context;
+ self
+ }
+}
+
+// This default is only intended for struct completion, do not use on its own.
+impl Default for CallbackSigner {
+ fn default() -> Self {
+ Self {
+ context: std::ptr::null(),
+ callback: Box::new(|_, _| Err(Error::UnsupportedType)),
+ alg: SigningAlg::Es256,
+ certs: Vec::new(),
+ reserve_size: 10000,
+ tsa_url: None,
+ }
+ }
+}
+
+impl Signer for CallbackSigner {
+ fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
+ (self.callback)(self.context, data)
+ }
+
+ fn alg(&self) -> SigningAlg {
+ self.alg
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ let pems = pem::parse_many(&self.certs).map_err(|e| Error::OtherError(Box::new(e)))?;
+ Ok(pems.into_iter().map(|p| p.into_contents()).collect())
+ }
+
+ fn reserve_size(&self) -> usize {
+ self.reserve_size
+ }
+
+ fn time_authority_url(&self) -> Option<String> {
+ self.tsa_url.clone()
+ }
+}
+
+use async_trait::async_trait;
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+// I'm not sure if this is useful since the callback is still synchronous.
+impl AsyncSigner for CallbackSigner {
+ async fn sign(&self, data: Vec<u8>) -> Result<Vec<u8>> {
+ (self.callback)(self.context, &data)
+ }
+
+ fn alg(&self) -> SigningAlg {
+ self.alg
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ let pems = pem::parse_many(&self.certs).map_err(|e| Error::OtherError(Box::new(e)))?;
+ Ok(pems.into_iter().map(|p| p.into_contents()).collect())
+ }
+
+ fn reserve_size(&self) -> usize {
+ self.reserve_size
+ }
+
+ fn time_authority_url(&self) -> Option<String> {
+ self.tsa_url.clone()
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ async fn send_timestamp_request(&self, _message: &[u8]) -> Option<Result<Vec<u8>>> {
+ None
+ }
+}
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -11,7 +11,9 @@
// specific language governing permissions and limitations under
// each license.
-use std::{collections::HashMap, fmt, path::Path};
+#[cfg(feature = "file_io")]
+use std::path::Path;
+use std::{collections::HashMap, fmt};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@@ -42,7 +44,7 @@ use crate::{
DATABOX, DATABOXES, SIGNATURE,
},
},
- jumbf_io::{get_assetio_handler, get_assetio_handler_from_path},
+ jumbf_io::get_assetio_handler,
salt::{DefaultSalt, SaltGenerator, NO_SALT},
status_tracker::{log_item, OneShotStatusTracker, StatusTracker},
trust_handler::TrustHandlerConfig,
@@ -67,6 +69,7 @@ const GH_UA: &str = "Sec-CH-UA";
// having different implementations for functions as a single entry point can be
// used to handle different data types.
pub enum ClaimAssetData<'a> {
+ #[cfg(feature = "file_io")]
Path(&'a Path),
Bytes(&'a [u8], &'a str),
Stream(&'a mut dyn CAIRead, &'a str),
@@ -1294,6 +1297,7 @@ impl Claim {
if !dh.is_remote_hash() {
// only verify local hashes here
let hash_result = match asset_data {
+ #[cfg(feature = "file_io")]
ClaimAssetData::Path(asset_path) => {
dh.verify_hash(asset_path, Some(claim.alg()))
}
@@ -1341,6 +1345,7 @@ impl Claim {
let name = dh.name().map_or("unnamed".to_string(), default_str);
let hash_result = match asset_data {
+ #[cfg(feature = "file_io")]
ClaimAssetData::Path(asset_path) => {
dh.verify_hash(asset_path, Some(claim.alg()))
}
@@ -1391,11 +1396,15 @@ impl Claim {
let bh = BoxHash::from_assertion(hash_binding_assertion)?;
let hash_result = match asset_data {
+ #[cfg(feature = "file_io")]
ClaimAssetData::Path(asset_path) => {
- let box_hash_processor = get_assetio_handler_from_path(asset_path)
- .ok_or(Error::UnsupportedType)?
- .asset_box_hash_ref()
- .ok_or(Error::HashMismatch("Box hash not supported".to_string()))?;
+ let box_hash_processor =
+ crate::jumbf_io::get_assetio_handler_from_path(asset_path)
+ .ok_or(Error::UnsupportedType)?
+ .asset_box_hash_ref()
+ .ok_or(Error::HashMismatch(
+ "Box hash not supported".to_string(),
+ ))?;
bh.verify_hash(asset_path, Some(claim.alg()), box_hash_processor)
}
diff --git a/sdk/src/claim_generator_info.rs b/sdk/src/claim_generator_info.rs
@@ -39,6 +39,17 @@ pub struct ClaimGeneratorInfo {
other: HashMap<String, Value>,
}
+impl Default for ClaimGeneratorInfo {
+ fn default() -> Self {
+ Self {
+ name: crate::NAME.to_string(),
+ version: Some(env!("CARGO_PKG_VERSION").to_string()),
+ icon: None,
+ other: HashMap::new(),
+ }
+ }
+}
+
impl ClaimGeneratorInfo {
pub fn new<S: Into<String>>(name: S) -> Self {
Self {
diff --git a/sdk/src/cose_sign.rs b/sdk/src/cose_sign.rs
@@ -357,6 +357,7 @@ mod tests {
}
#[cfg(not(target_arch = "wasm32"))]
+ #[cfg(feature = "openssl")]
#[actix::test]
async fn test_sign_claim_async() {
use crate::{
diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs
@@ -762,7 +762,7 @@ pub(crate) fn check_ocsp_status(
}
// internal util function to dump the cert chain in PEM format
-fn dump_cert_chain(certs: &[Vec<u8>], output_path: Option<&std::path::Path>) -> Result<Vec<u8>> {
+fn dump_cert_chain(certs: &[Vec<u8>]) -> Result<Vec<u8>> {
let mut out_buf: Vec<u8> = Vec::new();
let mut writer = Cursor::new(out_buf);
@@ -771,12 +771,7 @@ fn dump_cert_chain(certs: &[Vec<u8>], output_path: Option<&std::path::Path>) ->
.map_err(|_e| Error::UnsupportedType)?;
c.write_pem(&mut writer)?;
}
-
out_buf = writer.into_inner();
- if let Some(op) = output_path {
- std::fs::write(op, &out_buf).map_err(Error::IoError)?;
- }
-
Ok(out_buf)
}
@@ -1044,7 +1039,7 @@ pub(crate) async fn verify_cose_async(
result.date = get_signing_time(&sign1, &data);
// return cert chain
- result.cert_chain = dump_cert_chain(&get_sign_certs(&sign1)?, None)?;
+ result.cert_chain = dump_cert_chain(&get_sign_certs(&sign1)?)?;
}
Ok(result)
@@ -1081,7 +1076,7 @@ pub(crate) fn get_signing_info(
let certs = match sign1 {
Ok(s) => match get_sign_certs(&s) {
- Ok(c) => dump_cert_chain(&c, None).unwrap_or_default(),
+ Ok(c) => dump_cert_chain(&c).unwrap_or_default(),
Err(_) => Vec::new(),
},
Err(_e) => Vec::new(),
@@ -1205,7 +1200,7 @@ pub(crate) fn verify_cose(
result.date = get_signing_time(&sign1, data);
// return cert chain
- result.cert_chain = dump_cert_chain(&certs, None)?;
+ result.cert_chain = dump_cert_chain(&certs)?;
result.revocation_status = Some(true);
}
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
@@ -288,8 +288,3 @@ pub enum Error {
/// A specialized `Result` type for C2PA toolkit operations.
pub type Result<T> = std::result::Result<T, Error>;
-
-#[cfg(feature = "openssl_sign")]
-pub(crate) fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error {
- Error::OpenSslError(err)
-}
diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs
@@ -22,6 +22,10 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
+#[cfg(feature = "file_io")]
+use crate::utils::mime::extension_to_mime;
+#[cfg(doc)]
+use crate::Manifest;
use crate::{
assertion::{get_thumbnail_image_type, Assertion, AssertionBase},
assertions::{self, labels, Metadata, Relationship, Thumbnail},
@@ -274,8 +278,6 @@ impl Ingredient {
/// if one exists.
///
/// If `None`, the ingredient has no [`Manifest`]s.
- ///
- /// [`Manifest`]: crate::Manifest
pub fn active_manifest(&self) -> Option<&str> {
self.active_manifest.as_deref()
}
@@ -434,17 +436,13 @@ impl Ingredient {
/// Sets a reference to Manifest C2PA data
pub fn set_manifest_data_ref(&mut self, data_ref: ResourceRef) -> Result<&mut Self> {
- // verify the resource referenced exists
- if !self.resources.exists(&data_ref.identifier) {
- return Err(Error::NotFound);
- };
self.manifest_data = Some(data_ref);
Ok(self)
}
/// Sets the Manifest C2PA data for this ingredient with bytes
pub fn set_manifest_data(&mut self, data: Vec<u8>) -> Result<&mut Self> {
- let base_id = self.instance_id().to_string();
+ let base_id = "manifest_data".to_string();
self.manifest_data = Some(
self.resources
.add_with(&base_id, "application/c2pa", data)?,
@@ -498,8 +496,8 @@ impl Ingredient {
.unwrap_or_else(|| "".into())
.to_lowercase();
- let format = crate::utils::mime::extension_to_mime(&extension)
- .unwrap_or("application/binary")
+ let format = extension_to_mime(&extension)
+ .unwrap_or("application/octet-stream")
.to_owned();
(title, extension, format)
}
@@ -507,9 +505,7 @@ impl Ingredient {
/// Generates an `Ingredient` from a file path, including XMP info
/// from the file if available.
///
- /// This is used for making asset ingredients that should not load [`ManifestStore`]s.
- ///
- /// [`ManifestStore`]: crate::ManifestStore
+ /// This does not read c2pa_data in a file, it only reads XMP
#[cfg(feature = "file_io")]
pub fn from_file_info<P: AsRef<Path>>(path: P) -> Self {
// get required information from the file path
@@ -533,7 +529,8 @@ impl Ingredient {
S: Into<String>,
{
let format = format.into();
- // if we can open the file try tto get xmp info
+
+ // try to get xmp info, if this fails all XmpInfo fields will be None
let xmp_info = XmpInfo::from_source(stream, &format);
let id = if let Some(id) = xmp_info.instance_id {
@@ -772,9 +769,62 @@ impl Ingredient {
/// This does not set title or hash
/// Thumbnail will be set only if one can be retrieved from a previous valid manifest
pub fn from_stream(format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
- let mut ingredient = Self::from_stream_info(stream, format, "untitled");
+ let ingredient = Self::from_stream_info(stream, format, "untitled");
stream.rewind()?;
+ ingredient.add_stream_internal(format, stream)
+ }
+
+ /// Create an Ingredient from JSON
+ pub fn from_json(json: &str) -> Result<Self> {
+ serde_json::from_str(json).map_err(Error::JsonError)
+ }
+
+ /// Adds a stream to an ingredient
+ ///
+ /// This allows you to predefine fields before adding the stream.
+ /// Sets manifest_data if the stream contains a manifest_store.
+ /// Sets thumbnail if not defined and a valid claim thumbnail is found or add_thumbnails is enabled.
+ /// Instance_id, document_id, and provenance will be overridden if found in the stream.
+ /// Format will be overridden only if it is the default (application/octet-stream).
+ #[cfg(feature = "unstable_api")]
+ pub(crate) fn with_stream<S: Into<String>>(
+ mut self,
+ format: S,
+ stream: &mut dyn CAIRead,
+ ) -> Result<Self> {
+ let format = format.into();
+
+ // try to get xmp info, if this fails all XmpInfo fields will be None
+ let xmp_info = XmpInfo::from_source(stream, &format);
+ if let Some(id) = xmp_info.instance_id {
+ self.instance_id = Some(id);
+ };
+
+ if let Some(id) = xmp_info.document_id {
+ self.document_id = Some(id);
+ };
+
+ if let Some(provenance) = xmp_info.provenance {
+ self.provenance = Some(provenance);
+ };
+
+ // only override format if it is the default
+ if self.format == "application/octet-stream" {
+ self.format = format.to_string();
+ };
+
+ // ensure we have an instance Id for v1 ingredients
+ if self.instance_id.is_none() {
+ self.instance_id = Some(default_instance_id());
+ };
+
+ stream.rewind()?;
+ self.add_stream_internal(&format, stream)
+ }
+
+ // Internal implementation to avoid code bloat.
+ fn add_stream_internal(mut self, format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
let mut validation_log = DetailedStatusTracker::new();
// retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found
@@ -785,13 +835,8 @@ impl Ingredient {
Store::from_jumbf(&manifest_bytes, &mut validation_log)
.and_then(|mut store| {
// verify the store
- //todo, change this when we have a stream version of verify
- let mut buf: Vec<u8> = Vec::new();
- stream.rewind()?;
- stream.read_to_end(&mut buf).map_err(Error::IoError)?;
- store
- .verify_from_buffer(&buf, format, &mut validation_log)
- .map(|_| store)
+ store.verify_from_stream(stream, format, &mut validation_log)?;
+ Ok(store)
})
.map_err(|e| {
// add a log entry for the error so we act like verify
@@ -808,15 +853,15 @@ impl Ingredient {
};
// set validation status from result and log
- ingredient.update_validation_status(result, manifest_bytes, &validation_log)?;
+ self.update_validation_status(result, manifest_bytes, &validation_log)?;
// create a thumbnail if we don't already have a manifest with a thumb we can use
#[cfg(feature = "add_thumbnails")]
- if ingredient.thumbnail.is_none() {
+ if self.thumbnail.is_none() {
stream.rewind()?;
match crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) {
Ok((format, image)) => {
- ingredient.set_thumbnail(format, image)?;
+ self.set_thumbnail(format, image)?;
}
Err(err) => {
log::warn!("Could not create thumbnail. {err}");
@@ -824,7 +869,7 @@ impl Ingredient {
}
}
- Ok(ingredient)
+ Ok(self)
}
/// Creates an `Ingredient` from a memory buffer (async version).
@@ -1024,8 +1069,17 @@ impl Ingredient {
&self,
claim: &mut Claim,
redactions: Option<Vec<String>>,
+ resources: Option<&ResourceStore>, // use alternate resource store (for Builder model)
) -> Result<HashedUri> {
let mut thumbnail = None;
+ // for Builder model, ingredient resources may be in the manifest
+ let get_resource = |id: &str| {
+ self.resources.get(id).or_else(|_| {
+ resources
+ .ok_or_else(|| Error::NotFound)
+ .and_then(|r| r.get(id))
+ })
+ };
// add the ingredient manifest_data to the claim
// this is how any existing claims are added to the new store
@@ -1048,7 +1102,7 @@ impl Ingredient {
};
// get the c2pa manifest bytes
- let manifest_data = self.resources.get(&resource_ref.identifier)?;
+ let manifest_data = get_resource(&resource_ref.identifier)?;
// have Store check and load ingredients and add them to a claim
let ingredient_store = Store::load_ingredient_to_claim(
@@ -1120,7 +1174,10 @@ impl Ingredient {
HashedUri::new(thumb_ref.identifier.clone(), thumb_ref.alg.clone(), &hash)
}
None => {
- let data = self.thumbnail_bytes()?;
+ let data = match self.thumbnail.as_ref() {
+ Some(thumbnail) => get_resource(&thumbnail.identifier),
+ None => Err(Error::NotFound),
+ }?;
if self.is_v2() {
// v2 ingredients use databoxes for thumbnails
claim.add_databox(
@@ -1144,7 +1201,7 @@ impl Ingredient {
let mut data = None;
if let Some(data_ref) = self.data_ref() {
- let box_data = self.resources.get(&data_ref.identifier)?;
+ let box_data = get_resource(&data_ref.identifier)?;
let hash_url = claim.add_databox(
&data_ref.format,
box_data.into_owned(),
@@ -1783,9 +1840,9 @@ mod tests_file_io {
ingredient.resources.set_base_path(folder);
assert!(ingredient.thumbnail_ref().is_none());
- assert!(ingredient
- .set_manifest_data_ref(ResourceRef::new("image/jpg", "foo"))
- .is_err());
+ // assert!(ingredient
+ // .set_manifest_data_ref(ResourceRef::new("image/jpg", "foo"))
+ // .is_err());
assert!(ingredient.manifest_data_ref().is_none());
// verify we can set a reference
assert!(ingredient
diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs
@@ -11,10 +11,10 @@
// specific language governing permissions and limitations under
// each license.
+use std::{collections::HashMap, io::Cursor};
+#[cfg(feature = "file_io")]
use std::{
- collections::HashMap,
fs::{self, File},
- io::Cursor,
path::{Path, PathBuf},
};
@@ -97,6 +97,7 @@ pub(crate) fn is_bmff_format(asset_type: &str) -> bool {
}
/// Return jumbf block from in memory asset
+#[allow(dead_code)]
pub fn load_jumbf_from_memory(asset_type: &str, data: &[u8]) -> Result<Vec<u8>> {
let mut buf_reader = Cursor::new(data);
@@ -143,31 +144,33 @@ pub fn save_jumbf_to_memory(asset_type: &str, data: &[u8], store_bytes: &[u8]) -
Ok(output_stream.into_inner())
}
-pub fn get_assetio_handler_from_path(asset_path: &Path) -> Option<&dyn AssetIO> {
+#[cfg(feature = "file_io")]
+pub(crate) fn get_assetio_handler_from_path(asset_path: &Path) -> Option<&dyn AssetIO> {
let ext = get_file_extension(asset_path)?;
ASSET_HANDLERS.get(&ext).map(|h| h.as_ref())
}
-pub fn get_assetio_handler(ext: &str) -> Option<&dyn AssetIO> {
+pub(crate) fn get_assetio_handler(ext: &str) -> Option<&dyn AssetIO> {
let ext = ext.to_lowercase();
ASSET_HANDLERS.get(&ext).map(|h| h.as_ref())
}
-pub fn get_cailoader_handler(asset_type: &str) -> Option<&dyn CAIReader> {
+pub(crate) fn get_cailoader_handler(asset_type: &str) -> Option<&dyn CAIReader> {
let asset_type = asset_type.to_lowercase();
ASSET_HANDLERS.get(&asset_type).map(|h| h.get_reader())
}
-pub fn get_caiwriter_handler(asset_type: &str) -> Option<&dyn CAIWriter> {
+pub(crate) fn get_caiwriter_handler(asset_type: &str) -> Option<&dyn CAIWriter> {
let asset_type = asset_type.to_lowercase();
CAI_WRITERS.get(&asset_type).map(|h| h.as_ref())
}
-pub fn get_file_extension(path: &Path) -> Option<String> {
+#[cfg(feature = "file_io")]
+pub(crate) fn get_file_extension(path: &Path) -> Option<String> {
let ext_osstr = path.extension()?;
let ext = ext_osstr.to_str()?;
@@ -175,7 +178,8 @@ pub fn get_file_extension(path: &Path) -> Option<String> {
Some(ext.to_lowercase())
}
-pub fn get_supported_file_extension(path: &Path) -> Option<String> {
+#[cfg(feature = "file_io")]
+pub(crate) fn get_supported_file_extension(path: &Path) -> Option<String> {
let ext = get_file_extension(path)?;
if ASSET_HANDLERS.get(&ext).is_some() {
@@ -185,6 +189,7 @@ pub fn get_supported_file_extension(path: &Path) -> Option<String> {
}
}
+#[cfg(feature = "file_io")]
/// save_jumbf to a file
/// in_path - path is source file
/// out_path - path to the output file
@@ -237,7 +242,8 @@ pub fn save_jumbf_to_file(data: &[u8], in_path: &Path, out_path: Option<&Path>)
/// replace_bytes - replacement bytes
/// returns the location where splice occurred
#[cfg(test)] // this only used in unit tests
-pub fn update_file_jumbf(
+#[cfg(feature = "file_io")]
+pub(crate) fn update_file_jumbf(
out_path: &Path,
search_bytes: &[u8],
replace_bytes: &[u8],
@@ -253,6 +259,7 @@ pub fn update_file_jumbf(
Ok(splice_point)
}
+#[cfg(feature = "file_io")]
/// load the JUMBF block from an asset if available
pub fn load_jumbf_from_file(in_path: &Path) -> Result<Vec<u8>> {
let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
@@ -266,7 +273,8 @@ pub fn load_jumbf_from_file(in_path: &Path) -> Result<Vec<u8>> {
}
}
-pub fn object_locations(in_path: &Path) -> Result<Vec<HashObjectPositions>> {
+#[cfg(feature = "file_io")]
+pub(crate) fn object_locations(in_path: &Path) -> Result<Vec<HashObjectPositions>> {
let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
match get_assetio_handler(&ext) {
@@ -275,7 +283,7 @@ pub fn object_locations(in_path: &Path) -> Result<Vec<HashObjectPositions>> {
}
}
-pub fn object_locations_from_stream(
+pub(crate) fn object_locations_from_stream(
format: &str,
stream: &mut dyn CAIRead,
) -> Result<Vec<HashObjectPositions>> {
@@ -285,6 +293,7 @@ pub fn object_locations_from_stream(
}
}
+#[cfg(feature = "file_io")]
/// removes the C2PA JUMBF from an asset
/// Note: Use with caution since this deletes C2PA data
/// It is useful when creating remote manifests from embedded manifests
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -18,27 +18,33 @@
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg, doc_cfg_hide))]
//! This library supports reading, creating and embedding C2PA data
-//! with JPEG and PNG images.
+//! with a variety of asset types.
//!
-//! To read or write a manifest file, you must add the `file_io` dependency to your Cargo.toml.
-//! EXCEPTION: If you are building for WASM, do not add this dependency.
+//! We have a new experimental Builder/Reader API that will eventually replace
+//! the existing methods of reading and writing C2PA data.
+//! The new API focuses on stream support and can do more with fewer methods.
+//! It will be supported in all language bindings and build environments.
+//! To try these out, you need to enable the `unstable_api` feature.
+//!
+//! To read with file based methods, you must add the `file_io` dependency to your Cargo.toml.
//! For example:
//!
//! ```text
-//! c2pa = {version="0.11.0", features=["file_io"]}
+//! c2pa = {version="0.32.0", features=["file_io"]}
//! ```
//!
//! # Example: Reading a ManifestStore
//!
//! ```
//! # use c2pa::Result;
-//! use c2pa::{assertions::Actions, ManifestStore};
+//! use c2pa::{assertions::Actions, Reader};
//!
//! # fn main() -> Result<()> {
-//! let manifest_store = ManifestStore::from_file("tests/fixtures/C.jpg")?;
-//! println!("{}", manifest_store);
+//! let stream = std::fs::File::open("tests/fixtures/C.jpg")?;
+//! let reader = Reader::from_stream("image/jpeg", stream)?;
+//! println!("{}", reader.json());
//!
-//! if let Some(manifest) = manifest_store.get_active() {
+//! if let Some(manifest) = reader.active_manifest() {
//! let actions: Actions = manifest.find_assertion(Actions::LABEL)?;
//! for action in actions.actions {
//! println!("{}\n", action.action());
@@ -54,7 +60,7 @@
//! # use c2pa::Result;
//! use std::path::PathBuf;
//!
-//! use c2pa::{create_signer, Manifest, SigningAlg};
+//! use c2pa::{create_signer, Builder, SigningAlg};
//! use serde::Serialize;
//! use tempfile::tempdir;
//!
@@ -64,97 +70,107 @@
//! }
//!
//! # fn main() -> Result<()> {
-//! let mut manifest = Manifest::new("my_app".to_owned());
-//! manifest.add_labeled_assertion("org.contentauth.test", &Test { my_tag: 42 })?;
-//!
-//! let source = PathBuf::from("tests/fixtures/C.jpg");
-//! let dir = tempdir()?;
-//! let dest = dir.path().join("test_file.jpg");
+//! let mut builder = Builder::from_json(r#"{"title": "Test"}"#)?;
+//! builder.add_assertion("org.contentauth.test", &Test { my_tag: 42 })?;
//!
//! // Create a ps256 signer using certs and key files
-//! let signcert_path = "tests/fixtures/certs/ps256.pub";
-//! let pkey_path = "tests/fixtures/certs/ps256.pem";
-//! let signer = create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None)?;
+//! let signer = create_signer::from_files(
+//! "tests/fixtures/certs/ps256.pub",
+//! "tests/fixtures/certs/ps256.pem",
+//! SigningAlg::Ps256,
+//! None,
+//! )?;
//!
//! // embed a manifest using the signer
-//! manifest.embed(&source, &dest, &*signer)?;
+//! std::fs::remove_file("../target/tmp/lib_sign.jpg"); // ensure the file does not exist
+//! builder.sign_file(
+//! "tests/fixtures/C.jpg",
+//! "../target/tmp/lib_sign.jpg",
+//! &*signer,
+//! )?;
//! # Ok(())
//! # }
//! ```
-pub use assertion::{Assertion, AssertionBase, AssertionCbor, AssertionJson};
-pub mod assertions;
+/// The internal name of the C2PA SDK
+pub const NAME: &str = "c2pa-rs";
-mod cose_validator;
+/// The version of this C2PA SDK
+pub const VERSION: &str = env!("CARGO_PKG_VERSION");
+// Public modules
+pub mod assertions;
+pub mod cose_sign;
#[cfg(feature = "openssl_sign")]
pub mod create_signer;
+pub mod jumbf_io;
+pub mod settings;
+pub mod validation_status;
+#[cfg(target_arch = "wasm32")]
+pub mod wasm;
-mod error;
+// Public exports
+#[cfg(feature = "v1_api")]
+pub use asset_io::{CAIRead, CAIReadWrite};
+#[cfg(feature = "unstable_api")]
+pub use builder::{Builder, ManifestDefinition};
+pub use callback_signer::{CallbackFunc, CallbackSigner};
+pub use claim_generator_info::ClaimGeneratorInfo;
pub use error::{Error, Result};
-
-mod ingredient;
+pub use hash_utils::{hash_stream_by_alg, HashRange};
pub use ingredient::Ingredient;
-pub mod jumbf_io;
-mod manifest;
+#[cfg(feature = "file_io")]
+pub use ingredient::{DefaultOptions, IngredientOptions};
pub use manifest::Manifest;
-mod manifest_assertion;
pub use manifest_assertion::{ManifestAssertion, ManifestAssertionKind};
-
-mod manifest_store;
+#[cfg(feature = "v1_api")]
pub use manifest_store::ManifestStore;
-
-mod manifest_store_report;
+#[cfg(feature = "v1_api")]
pub use manifest_store_report::ManifestStoreReport;
-
-mod resource_store;
-pub use resource_store::{ResourceRef, ResourceStore};
-
-mod signing_alg;
-#[cfg(feature = "file_io")]
-pub use ingredient::{DefaultOptions, IngredientOptions};
-pub use signing_alg::{SigningAlg, UnknownAlgorithmError};
-pub(crate) mod ocsp_utils;
-#[cfg(feature = "openssl_sign")]
-mod openssl;
-
-mod signer;
+#[cfg(feature = "unstable_api")]
+pub use reader::Reader;
+pub use resource_store::ResourceRef;
pub use signer::{AsyncSigner, RemoteSigner, Signer};
+pub use signing_alg::SigningAlg;
+pub use utils::mime::format_from_path;
+
+// Internal modules
#[allow(dead_code, clippy::enum_variant_names)]
pub(crate) mod asn1;
pub(crate) mod assertion;
pub(crate) mod asset_handlers;
pub(crate) mod asset_io;
-pub use asset_io::{CAIRead, CAIReadWrite};
-/// crate private declarations
+#[cfg(feature = "unstable_api")]
+pub(crate) mod builder;
+pub(crate) mod callback_signer;
pub(crate) mod claim;
-
-mod claim_generator_info;
-pub use claim_generator_info::ClaimGeneratorInfo;
-
-pub mod cose_sign;
-
+pub(crate) mod claim_generator_info;
+pub(crate) mod cose_validator;
#[cfg(all(feature = "xmp_write", feature = "file_io"))]
pub(crate) mod embedded_xmp;
+pub(crate) mod error;
pub(crate) mod hashed_uri;
+pub(crate) mod ingredient;
#[allow(dead_code)]
pub(crate) mod jumbf;
+pub(crate) mod manifest;
+pub(crate) mod manifest_assertion;
+pub(crate) mod manifest_store;
+pub(crate) mod manifest_store_report;
+pub(crate) mod ocsp_utils;
+#[cfg(feature = "openssl")]
+pub(crate) mod openssl;
+#[allow(dead_code)]
+// TODO: Remove this when the feature is released (used in tests only for some builds now)
+pub(crate) mod reader;
+pub(crate) mod resource_store;
pub(crate) mod salt;
-pub mod settings;
+pub(crate) mod signer;
+pub(crate) mod signing_alg;
pub(crate) mod status_tracker;
pub(crate) mod store;
pub(crate) mod time_stamp;
pub(crate) mod trust_handler;
pub(crate) mod utils;
-pub mod validation_status;
-pub use hash_utils::HashRange;
pub(crate) use utils::{cbor_types, hash_utils};
-pub use utils::{cbor_types::DateT, hash_utils::hash_stream_by_alg};
pub(crate) mod validator;
-#[cfg(target_arch = "wasm32")]
-pub mod wasm;
-
-/// The internal name of the C2PA SDK
-pub const NAME: &str = "c2pa-rs";
-/// The version of this C2PA SDK
-pub const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -32,12 +32,13 @@ use crate::{
asset_io::{CAIRead, CAIReadWrite},
claim::{Claim, RemoteManifest},
error::{Error, Result},
+ ingredient::Ingredient,
jumbf,
+ manifest_assertion::ManifestAssertion,
resource_store::{skip_serializing_resources, ResourceRef, ResourceStore},
salt::DefaultSalt,
store::Store,
- ClaimGeneratorInfo, HashRange, Ingredient, ManifestAssertion, ManifestAssertionKind,
- RemoteSigner, Signer,
+ ClaimGeneratorInfo, HashRange, ManifestAssertionKind, RemoteSigner, Signer, SigningAlg,
};
/// A Manifest represents all the information in a c2pa manifest
@@ -440,21 +441,6 @@ impl Manifest {
Ok(self)
}
- /// Sets the signature information for the report
- fn set_signature(
- &mut self,
- issuer: Option<&String>,
- time: Option<&String>,
- cert_serial: Option<&String>,
- ) -> &mut Self {
- self.signature_info = Some(SignatureInfo {
- issuer: issuer.cloned(),
- time: time.cloned(),
- cert_serial_number: cert_serial.cloned(),
- });
- self
- }
-
/// Returns the name of the signature issuer
pub fn issuer(&self) -> Option<String> {
self.signature_info.to_owned().and_then(|sig| sig.issuer)
@@ -662,22 +648,18 @@ impl Manifest {
}
}
- let issuer = claim.signing_issuer();
- let signing_time = claim
- .signing_time()
- .map(|signing_time| signing_time.to_rfc3339());
-
- if issuer.is_some() || signing_time.is_some() {
- debug!(
- "added signature issuer={:?} time={:?}",
- issuer, signing_time
- );
- manifest.set_signature(
- issuer.as_ref(),
- signing_time.as_ref(),
- claim.signing_cert_serial().as_ref(),
- );
- }
+ manifest.signature_info = match claim.signature_info() {
+ Some(signature_info) => Some(SignatureInfo {
+ alg: signature_info.alg,
+ issuer: signature_info.issuer_org,
+ time: signature_info.date.map(|d| d.to_rfc3339()),
+ cert_serial_number: signature_info.cert_serial_number.map(|s| s.to_string()),
+ cert_chain: String::from_utf8(signature_info.cert_chain)
+ .map_err(|_e| Error::CoseInvalidCert)?,
+ revocation_status: signature_info.revocation_status,
+ }),
+ None => None,
+ };
Ok(manifest)
}
@@ -778,7 +760,7 @@ impl Manifest {
let mut ingredient_map = HashMap::new();
// add all ingredients to the claim
for ingredient in &self.ingredients {
- let uri = ingredient.add_to_claim(&mut claim, self.redactions.clone())?;
+ let uri = ingredient.add_to_claim(&mut claim, self.redactions.clone(), None)?;
ingredient_map.insert(ingredient.instance_id(), uri);
}
@@ -1273,6 +1255,9 @@ impl std::fmt::Display for Manifest {
pub struct SignatureInfo {
/// human readable issuing authority for this signature
#[serde(skip_serializing_if = "Option::is_none")]
+ alg: Option<SigningAlg>,
+ /// human readable issuing authority for this signature
+ #[serde(skip_serializing_if = "Option::is_none")]
issuer: Option<String>,
/// The serial number of the certificate
@@ -1282,6 +1267,21 @@ pub struct SignatureInfo {
/// the time the signature was created
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<String>,
+
+ /// the cert chain for this claim
+ #[serde(skip)] // don't serialize this, let someone ask for it
+ cert_chain: String,
+
+ /// revocation status of the certificate
+ #[serde(skip_serializing_if = "Option::is_none")]
+ revocation_status: Option<bool>,
+}
+
+impl SignatureInfo {
+ // returns the cert chain for this signature
+ pub fn cert_chain(&self) -> &str {
+ &self.cert_chain
+ }
}
#[cfg(test)]
@@ -1301,8 +1301,10 @@ pub(crate) mod tests {
use crate::{
assertions::{c2pa_action, Action, Actions},
+ ingredient::Ingredient,
+ reader::Reader,
utils::test::{temp_remote_signer, temp_signer, TEST_VC},
- Ingredient, Manifest, Result,
+ Manifest, Result,
};
#[cfg(feature = "file_io")]
use crate::{
@@ -1688,10 +1690,9 @@ pub(crate) mod tests {
.embed_async_signed(&output, &output, &async_signer)
.await
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
- assert!(manifest_store.active_label().is_some());
+ let reader = Reader::from_file(&output).expect("from_file");
assert_eq!(
- manifest_store.get_active().unwrap().title().unwrap(),
+ reader.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
@@ -1709,10 +1710,9 @@ pub(crate) mod tests {
.embed_remote_signed(&output, &output, remote_signer.as_ref())
.await
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
- assert!(manifest_store.active_label().is_some());
+ let manifest_store = Reader::from_file(&output).expect("from_file");
assert_eq!(
- manifest_store.get_active().unwrap().title().unwrap(),
+ manifest_store.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
@@ -1730,10 +1730,10 @@ pub(crate) mod tests {
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
- assert_eq!(manifest_store.active_label(), Some("MyLabel"));
+
+ let reader = Reader::from_file(&output).expect("from_file");
assert_eq!(
- manifest_store.get_active().unwrap().title().unwrap(),
+ reader.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
@@ -1756,12 +1756,10 @@ pub(crate) mod tests {
.embed(&output, &output, signer.as_ref())
.expect("embed");
- //let manifest_store = crate::ManifestStore::from_file(&sidecar).expect("from_file");
- let manifest_store = crate::ManifestStore::from_bytes("application/c2pa", &c2pa_data, true)
- .expect("from_bytes");
- assert_eq!(manifest_store.active_label(), Some("MyLabel"));
+ let manifest_store =
+ Reader::from_stream("application/c2pa", Cursor::new(c2pa_data)).expect("from_bytes");
assert_eq!(
- manifest_store.get_active().unwrap().title().unwrap(),
+ manifest_store.active_manifest().unwrap().title().unwrap(),
TEST_SMALL_JPEG
);
}
@@ -1798,8 +1796,7 @@ pub(crate) mod tests {
.expect("embed_stream");
// try to load the image
- let manifest_store =
- crate::ManifestStore::from_bytes("image/jpeg", &out_vec, true).unwrap();
+ let manifest_store = Reader::from_stream("image/jpeg", Cursor::new(out_vec)).unwrap();
/* to be enabled later
// try to load the manifest
@@ -1834,7 +1831,7 @@ pub(crate) mod tests {
.expect("embed_stream");
// try to load the image
- let manifest_store = crate::ManifestStore::from_bytes("image/png", &out_vec, true).unwrap();
+ let manifest_store = Reader::from_stream("image/png", Cursor::new(out_vec)).unwrap();
/* to be enabled later
// try to load the manifest
@@ -1870,8 +1867,7 @@ pub(crate) mod tests {
.expect("embed_stream");
// try to load the image
- let manifest_store =
- crate::ManifestStore::from_bytes("image/webp", &out_vec, true).unwrap();
+ let manifest_store = Reader::from_stream("image/webp", Cursor::new(out_vec)).unwrap();
/* to be enabled later
// try to load the manifest
@@ -1907,14 +1903,14 @@ pub(crate) mod tests {
.embed_to_stream("jpeg", &mut stream, &mut output, signer.as_ref())
.expect("embed_stream");
- let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true)
- .expect("from_bytes");
+ stream.set_position(0);
+ let reader = Reader::from_stream("jpeg", &mut output).expect("from_bytes");
assert_eq!(
- manifest_store.get_active().unwrap().title().unwrap(),
+ reader.active_manifest().unwrap().title().unwrap(),
"EmbedStream"
);
#[cfg(feature = "add_thumbnails")]
- assert!(manifest_store.get_active().unwrap().thumbnail().is_some());
+ assert!(reader.active_manifest().unwrap().thumbnail().is_some());
//println!("{manifest_store}");main
}
@@ -1939,9 +1935,9 @@ pub(crate) mod tests {
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
- let manifest = manifest_store.get_active().unwrap();
+ let manifest = manifest_store.active_manifest().unwrap();
let ingredient_status = manifest.ingredients()[0].validation_status();
assert_eq!(
ingredient_status.unwrap()[0].code(),
@@ -1974,9 +1970,9 @@ pub(crate) mod tests {
assert_eq!(manifest.remote_manifest_url().unwrap(), url.to_string());
//let manifest_store = crate::ManifestStore::from_file(&sidecar).expect("from_file");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ let manifest_store = Reader::from_file(&output).expect("from_file");
assert_eq!(
- manifest_store.get_active().unwrap().title().unwrap(),
+ manifest_store.active_manifest().unwrap().title().unwrap(),
"XCAplus.jpg"
);
}
@@ -1997,8 +1993,8 @@ pub(crate) mod tests {
manifest
.embed(&output, &output, signer.as_ref())
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
- let active_manifest = manifest_store.get_active().unwrap();
+ let manifest_store = Reader::from_file(&output).expect("from_file");
+ let active_manifest = manifest_store.active_manifest().unwrap();
let (format, image) = active_manifest.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
assert_eq!(image.into_owned(), thumb_data);
@@ -2147,10 +2143,10 @@ pub(crate) mod tests {
.embed_to_stream("jpeg", &mut input, &mut output, signer.as_ref())
.expect("embed_stream");
- let manifest_store = crate::ManifestStore::from_bytes("jpeg", &output.into_inner(), true)
- .expect("from_bytes");
- println!("manifest_store = {manifest_store}");
- let m = manifest_store.get_active().unwrap();
+ output.set_position(0);
+ let reader = Reader::from_stream("jpeg", &mut output).expect("from_bytes");
+ println!("manifest_store = {reader}");
+ let m = reader.active_manifest().unwrap();
//println!("after = {m}");
@@ -2203,10 +2199,9 @@ pub(crate) mod tests {
.embed_from_memory("jpeg", image, signer.as_ref())
.expect("embed_stream");
- let manifest_store =
- crate::ManifestStore::from_bytes("jpeg", &output_image, true).expect("from_bytes");
- println!("manifest_store = {manifest_store}");
- let m = manifest_store.get_active().unwrap();
+ let reader = Reader::from_stream("jpeg", Cursor::new(output_image)).expect("from_bytes");
+ println!("manifest_store = {reader}");
+ let m = reader.active_manifest().unwrap();
assert!(m.thumbnail().is_some());
let (format, image) = m.thumbnail().unwrap();
@@ -2263,9 +2258,9 @@ pub(crate) mod tests {
.embed(&output, &output, signer.as_ref())
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
- println!("{manifest_store}");
- let active_manifest = manifest_store.get_active().unwrap();
+ let reader = Reader::from_file(&output).expect("from_file");
+ println!("{reader}");
+ let active_manifest = reader.active_manifest().unwrap();
let (format, _) = active_manifest.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
}
@@ -2289,9 +2284,9 @@ pub(crate) mod tests {
.embed(&output, &output, signer.as_ref())
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
- let active_manifest = manifest_store.get_active().unwrap();
+ let active_manifest = manifest_store.active_manifest().unwrap();
let (format, _) = active_manifest.thumbnail().unwrap();
assert_eq!(format, "image/jpeg");
}
@@ -2349,9 +2344,9 @@ pub(crate) mod tests {
.embed(&output, &output, signer.as_ref())
.expect("embed");
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
- let active_manifest = manifest_store.get_active().unwrap();
+ let active_manifest = manifest_store.active_manifest().unwrap();
assert!(active_manifest.thumbnail_ref().is_none());
assert!(active_manifest.thumbnail().is_none());
}
@@ -2435,7 +2430,7 @@ pub(crate) mod tests {
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&signed_manifest).unwrap();
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
assert!(manifest_store.validation_status().is_none());
}
@@ -2496,7 +2491,7 @@ pub(crate) mod tests {
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&signed_manifest).unwrap();
- let manifest_store = crate::ManifestStore::from_file(&output).expect("from_file");
+ let manifest_store = Reader::from_file(&output).expect("from_file");
println!("{manifest_store}");
assert!(manifest_store.validation_status().is_none());
}
@@ -2522,14 +2517,14 @@ pub(crate) mod tests {
.expect("embeddable_manifest");
// Validate the embeddable manifest against the asset bytes
- let manifest_store = crate::ManifestStore::from_manifest_and_asset_bytes(
+ let reader = Reader::from_manifest_data_and_stream(
&embeddable,
"image/jpeg",
- asset_bytes,
+ Cursor::new(asset_bytes),
)
.unwrap();
- println!("{manifest_store}");
- assert!(!manifest_store.manifests().is_empty());
- assert!(manifest_store.validation_status().is_none());
+ println!("{reader}");
+ assert!(reader.active_manifest().is_some());
+ assert!(reader.validation_status().is_none());
}
}
diff --git a/sdk/src/manifest_assertion.rs b/sdk/src/manifest_assertion.rs
@@ -153,7 +153,7 @@ impl ManifestAssertion {
/// };
/// # fn main() -> Result<()> {
/// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
- /// let _ma = ManifestAssertion::from_assertion(&actions)?;
+ /// let _ma = ManifestAssertion::from_labeled_assertion(Actions::LABEL, &actions)?;
/// # Ok(())
/// # }
/// ```
@@ -175,7 +175,7 @@ impl ManifestAssertion {
/// };
/// # fn main() -> Result<()> {
/// let actions = Actions::new().add_action(Action::new(c2pa_action::EDITED));
- /// let manifest_assertion = ManifestAssertion::from_assertion(&actions)?;
+ /// let manifest_assertion = ManifestAssertion::from_labeled_assertion(Actions::LABEL, &actions)?;
///
/// let actions: Actions = manifest_assertion.to_assertion()?;
/// for action in actions.actions {
diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs
@@ -11,24 +11,29 @@
// specific language governing permissions and limitations under
// each license.
-use std::collections::HashMap;
#[cfg(feature = "file_io")]
use std::path::Path;
+use std::{
+ collections::HashMap,
+ io::{Read, Seek, Write},
+};
+use async_generic::async_generic;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
-use serde::Serialize;
+use serde::{Deserialize, Serialize};
use crate::{
claim::ClaimAssetData,
+ jumbf::labels::manifest_label_from_uri,
status_tracker::{DetailedStatusTracker, StatusTracker},
store::Store,
utils::base64,
validation_status::{status_for_store, ValidationStatus},
- CAIRead, Manifest, Result,
+ Error, Manifest, Result,
};
-#[derive(Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
/// A Container for a set of Manifests and a ValidationStatus list
pub struct ManifestStore {
@@ -67,6 +72,7 @@ impl ManifestStore {
}
/// Returns a reference to manifest HashMap
+ #[cfg(feature = "v1_api")]
pub fn manifests(&self) -> &HashMap<String, Manifest> {
&self.manifests
}
@@ -76,6 +82,30 @@ impl ManifestStore {
self.manifests.get(label)
}
+ // writes a resource identified uri to the given stream
+ pub fn get_resource(&self, uri: &str, stream: impl Write + Read + Seek + Send) -> Result<u64> {
+ // get the manifest referenced by the uri, or the active one if None
+ let manifest = match manifest_label_from_uri(uri) {
+ Some(label) => self.get(&label),
+ None => self.get_active(),
+ };
+ if let Some(manifest) = manifest {
+ let mut resources = manifest.resources();
+ if !resources.exists(uri) {
+ // also search ingredients to support Reader model
+ for ingredient in manifest.ingredients() {
+ if ingredient.resources().exists(uri) {
+ resources = ingredient.resources();
+ break;
+ }
+ }
+ }
+ resources.write_stream(uri, stream)
+ } else {
+ Err(Error::ResourceNotFound(uri.to_owned()))
+ }
+ }
+
/// Returns a reference the [ValidationStatus] Vec or None
pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
self.validation_status.as_deref()
@@ -93,7 +123,7 @@ impl ManifestStore {
/// creates a ManifestStore from a Store writing resources to resource_path
#[cfg(feature = "file_io")]
- pub fn from_store_with_resources(
+ pub(crate) fn from_store_with_resources(
store: &Store,
validation_log: &impl StatusTracker,
resource_path: &Path,
@@ -138,6 +168,7 @@ impl ManifestStore {
}
/// Creates a new Manifest Store from a Manifest
+ #[allow(dead_code)]
pub fn from_manifest(manifest: &Manifest) -> Result<Self> {
use crate::status_tracker::OneShotStatusTracker;
let store = manifest.to_store()?;
@@ -150,6 +181,7 @@ impl ManifestStore {
}
/// Generate a Store from a format string and bytes.
+ #[cfg(feature = "v1_api")]
pub fn from_bytes(format: &str, image_bytes: &[u8], verify: bool) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
@@ -158,22 +190,36 @@ impl ManifestStore {
}
/// Generate a Store from a format string and stream.
+ #[async_generic(async_signature(
+ format: &str,
+ mut stream: impl Read + Seek + Send,
+ verify: bool,
+ ))]
pub fn from_stream(
format: &str,
- stream: &mut dyn CAIRead,
+ mut stream: impl Read + Seek + Send,
verify: bool,
) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
- let manifest_bytes = Store::load_jumbf_from_stream(format, stream)?;
+ let manifest_bytes = Store::load_jumbf_from_stream(format, &mut stream)?;
let store = Store::from_jumbf(&manifest_bytes, &mut validation_log)?;
if verify {
// verify store and claims
- Store::verify_store(
- &store,
- &mut ClaimAssetData::Stream(stream, format),
- &mut validation_log,
- )?;
+ if _sync {
+ Store::verify_store(
+ &store,
+ &mut ClaimAssetData::Stream(&mut stream, format),
+ &mut validation_log,
+ )?;
+ } else {
+ Store::verify_store_async(
+ &store,
+ &mut ClaimAssetData::Stream(&mut stream, format),
+ &mut validation_log,
+ )
+ .await?;
+ }
}
Ok(Self::from_store(&store, &validation_log))
}
@@ -191,6 +237,7 @@ impl ManifestStore {
/// # Ok(())
/// # }
/// ```
+ #[cfg(feature = "v1_api")]
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ManifestStore> {
let mut validation_log = DetailedStatusTracker::new();
@@ -214,6 +261,7 @@ impl ManifestStore {
/// # Ok(())
/// # }
/// ```
+ #[allow(dead_code)]
pub fn from_file_with_resources<P: AsRef<Path>>(
path: P,
resource_path: P,
@@ -229,6 +277,7 @@ impl ManifestStore {
}
/// Loads a ManifestStore from a file
+ #[allow(dead_code)]
pub async fn from_bytes_async(
format: &str,
image_bytes: &[u8],
@@ -321,7 +370,6 @@ impl ManifestStore {
/// #
/// # Ok(())
/// }
- /// ```
pub fn from_manifest_and_asset_bytes(
manifest_bytes: &[u8],
format: &str,
@@ -430,6 +478,7 @@ mod tests {
}
#[test]
+ #[cfg(feature = "v1_api")]
fn manifest_report_image() {
let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
@@ -448,6 +497,7 @@ mod tests {
#[cfg_attr(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ #[cfg(feature = "v1_api")]
async fn manifest_report_image_async() {
let image_bytes = include_bytes!("../tests/fixtures/CA.jpg");
@@ -468,6 +518,7 @@ mod tests {
#[test]
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
fn manifest_report_from_file() {
let manifest_store = ManifestStore::from_file("tests/fixtures/CA.jpg").unwrap();
println!("{manifest_store}");
@@ -484,6 +535,7 @@ mod tests {
#[cfg_attr(not(target_arch = "wasm32"), actix::test)]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
+ #[cfg(feature = "v1_api")]
async fn manifest_report_from_manifest_and_asset_bytes_async() {
let asset_bytes = include_bytes!("../tests/fixtures/cloud.jpg");
let manifest_bytes = include_bytes!("../tests/fixtures/cloud_manifest.c2pa");
@@ -502,6 +554,7 @@ mod tests {
#[test]
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
fn manifest_report_from_file_with_resources() {
let manifest_store = ManifestStore::from_file_with_resources(
"tests/fixtures/CIE-sig-CA.jpg",
@@ -521,10 +574,11 @@ mod tests {
}
#[test]
+ #[cfg(feature = "v1_api")]
fn manifest_report_from_stream() {
let image_bytes: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
- let mut stream = std::io::Cursor::new(image_bytes);
- let manifest_store = ManifestStore::from_stream("image/jpeg", &mut stream, true).unwrap();
+ let stream = std::io::Cursor::new(image_bytes);
+ let manifest_store = ManifestStore::from_stream("image/jpeg", stream, true).unwrap();
println!("{manifest_store}");
assert!(manifest_store.active_label().is_some());
diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs
@@ -13,6 +13,7 @@
use std::collections::HashMap;
#[cfg(feature = "file_io")]
+#[cfg(feature = "v1_api")]
use std::path::Path;
use atree::{Arena, Token};
@@ -20,14 +21,11 @@ use extfmt::Hexlify;
use serde::{Deserialize, Serialize};
use serde_json::Value;
+#[cfg(feature = "v1_api")]
+use crate::status_tracker::{DetailedStatusTracker, StatusTracker};
use crate::{
- assertion::AssertionData,
- claim::Claim,
- status_tracker::{DetailedStatusTracker, StatusTracker},
- store::Store,
- utils::base64,
- validation_status::ValidationStatus,
- Result,
+ assertion::AssertionData, claim::Claim, store::Store, utils::base64,
+ validation_status::ValidationStatus, Result,
};
/// Low level JSON based representation of Manifest Store - used for debugging
@@ -58,6 +56,7 @@ impl ManifestStoreReport {
/// Prints tree view of manifest store
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
pub fn dump_tree<P: AsRef<Path>>(path: P) -> Result<()> {
let mut validation_log = crate::status_tracker::DetailedStatusTracker::new();
let store = crate::store::Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
@@ -97,6 +96,7 @@ impl ManifestStoreReport {
/// Prints the certificate chain used to sign the active manifest.
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
pub fn dump_cert_chain<P: AsRef<Path>>(path: P) -> Result<()> {
let mut validation_log = DetailedStatusTracker::new();
let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
@@ -113,6 +113,7 @@ impl ManifestStoreReport {
/// Returns the certificate chain used to sign the active manifest.
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
pub fn cert_chain<P: AsRef<Path>>(path: P) -> Result<String> {
let mut validation_log = DetailedStatusTracker::new();
let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?;
@@ -120,12 +121,14 @@ impl ManifestStoreReport {
}
/// Returns the certificate used to sign the active manifest.
+ #[cfg(feature = "v1_api")]
pub fn cert_chain_from_bytes(format: &str, bytes: &[u8]) -> Result<String> {
let mut validation_log = DetailedStatusTracker::new();
let store = Store::load_from_memory(format, bytes, true, &mut validation_log)?;
store.get_provenance_cert_chain()
}
+ #[cfg(feature = "v1_api")]
/// Creates a ManifestStoreReport from an existing Store and a validation log
pub(crate) fn from_store_with_log(
store: &Store,
@@ -150,6 +153,7 @@ impl ManifestStoreReport {
Ok(report)
}
+ #[cfg(feature = "v1_api")]
/// Creates a ManifestStoreReport from image bytes and a format
pub fn from_bytes(format: &str, image_bytes: &[u8]) -> Result<Self> {
let mut validation_log = DetailedStatusTracker::new();
@@ -157,6 +161,7 @@ impl ManifestStoreReport {
Self::from_store_with_log(&store, &validation_log)
}
+ #[cfg(feature = "v1_api")]
/// Creates a ManifestStoreReport from a file
#[cfg(feature = "file_io")]
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
@@ -195,7 +200,9 @@ impl ManifestStoreReport {
// recurse down ingredients
for i in claim.ingredient_assertions() {
let ingredient_assertion =
- <crate::assertions::Ingredient as crate::AssertionBase>::from_assertion(i)?;
+ <crate::assertions::Ingredient as crate::assertion::AssertionBase>::from_assertion(
+ i,
+ )?;
// is this an ingredient
if let Some(ref c2pa_manifest) = &ingredient_assertion.c2pa_manifest {
@@ -389,10 +396,10 @@ fn b64_tag(mut json: String, tag: &str) -> String {
mod tests {
#![allow(clippy::expect_used)]
+ #[cfg(feature = "v1_api")]
use std::fs;
- use super::ManifestStoreReport;
- use crate::utils::test::fixture_path;
+ use crate::{manifest_store_report::ManifestStoreReport, utils::test::fixture_path};
#[test]
fn manifest_store_report() {
@@ -402,12 +409,14 @@ mod tests {
}
#[test]
+ #[cfg(feature = "v1_api")]
fn manifest_get_certchain_from_bytes() {
let bytes = fs::read(fixture_path("CA.jpg")).expect("missing test asset");
assert!(ManifestStoreReport::cert_chain_from_bytes("jpg", &bytes).is_ok())
}
#[test]
+ #[cfg(feature = "v1_api")]
fn manifest_get_certchain_from_bytes_no_manifest_err() {
let bytes = fs::read(fixture_path("no_manifest.jpg")).expect("missing test asset");
assert!(matches!(
@@ -418,6 +427,7 @@ mod tests {
#[test]
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
fn manifest_dump_tree() {
let asset_name = "CA.jpg";
let path = fixture_path(asset_name);
@@ -427,6 +437,7 @@ mod tests {
#[test]
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
fn manifest_dump_certchain() {
let asset_name = "CA.jpg";
let path = fixture_path(asset_name);
@@ -436,6 +447,7 @@ mod tests {
#[test]
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
fn manifest_get_certchain() {
let asset_name = "CA.jpg";
let path = fixture_path(asset_name);
@@ -444,6 +456,7 @@ mod tests {
#[test]
#[cfg(feature = "file_io")]
+ #[cfg(feature = "v1_api")]
fn manifest_get_certchain_no_manifest_err() {
let asset_name = "no_manifest.jpg";
let path = fixture_path(asset_name);
diff --git a/sdk/src/openssl/ec_signer.rs b/sdk/src/openssl/ec_signer.rs
@@ -24,7 +24,7 @@ use x509_parser::der_parser::{
use super::check_chain_order;
use crate::{
- error::{wrap_openssl_err, Error, Result},
+ error::{Error, Result},
signer::ConfigurableSigner,
Signer, SigningAlg,
};
@@ -50,8 +50,8 @@ impl ConfigurableSigner for EcSigner {
tsa_url: Option<String>,
) -> Result<Self> {
let certs_size = signcert.len();
- let pkey = EcKey::private_key_from_pem(pkey).map_err(wrap_openssl_err)?;
- let signcerts = X509::stack_from_pem(signcert).map_err(wrap_openssl_err)?;
+ let pkey = EcKey::private_key_from_pem(pkey).map_err(Error::OpenSslError)?;
+ let signcerts = X509::stack_from_pem(signcert).map_err(Error::OpenSslError)?;
// make sure cert chains are in order
if !check_chain_order(&signcerts) {
@@ -73,7 +73,7 @@ impl ConfigurableSigner for EcSigner {
impl Signer for EcSigner {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
- let key = PKey::from_ec_key(self.pkey.clone()).map_err(wrap_openssl_err)?;
+ let key = PKey::from_ec_key(self.pkey.clone()).map_err(Error::OpenSslError)?;
let mut signer = match self.alg {
SigningAlg::Es256 => openssl::sign::Signer::new(MessageDigest::sha256(), &key)?,
@@ -82,8 +82,8 @@ impl Signer for EcSigner {
_ => return Err(Error::UnsupportedType),
};
- signer.update(data).map_err(wrap_openssl_err)?;
- let der_sig = signer.sign_to_vec().map_err(wrap_openssl_err)?;
+ signer.update(data).map_err(Error::OpenSslError)?;
+ let der_sig = signer.sign_to_vec().map_err(Error::OpenSslError)?;
der_to_p1363(&der_sig, self.alg)
}
@@ -96,7 +96,7 @@ impl Signer for EcSigner {
let mut certs: Vec<Vec<u8>> = Vec::new();
for c in &self.signcerts {
- let cert = c.to_der().map_err(wrap_openssl_err)?;
+ let cert = c.to_der().map_err(Error::OpenSslError)?;
certs.push(cert);
}
diff --git a/sdk/src/openssl/ed_signer.rs b/sdk/src/openssl/ed_signer.rs
@@ -40,8 +40,8 @@ impl ConfigurableSigner for EdSigner {
tsa_url: Option<String>,
) -> Result<Self> {
let certs_size = signcert.len();
- let signcerts = X509::stack_from_pem(signcert).map_err(wrap_openssl_err)?;
- let pkey = PKey::private_key_from_pem(pkey).map_err(wrap_openssl_err)?;
+ let signcerts = X509::stack_from_pem(signcert).map_err(Error::OpenSslError)?;
+ let pkey = PKey::private_key_from_pem(pkey).map_err(Error::OpenSslError)?;
if alg != SigningAlg::Ed25519 {
return Err(Error::UnsupportedType); // only ed25519 is supported by C2PA
@@ -68,7 +68,7 @@ impl ConfigurableSigner for EdSigner {
impl Signer for EdSigner {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
let mut signer =
- openssl::sign::Signer::new_without_digest(&self.pkey).map_err(wrap_openssl_err)?;
+ openssl::sign::Signer::new_without_digest(&self.pkey).map_err(Error::OpenSslError)?;
let signed_data = signer.sign_oneshot_to_vec(data)?;
@@ -83,7 +83,7 @@ impl Signer for EdSigner {
let mut certs: Vec<Vec<u8>> = Vec::new();
for c in &self.signcerts {
- let cert = c.to_der().map_err(wrap_openssl_err)?;
+ let cert = c.to_der().map_err(Error::OpenSslError)?;
certs.push(cert);
}
@@ -99,10 +99,6 @@ impl Signer for EdSigner {
}
}
-fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error {
- Error::OpenSslError(err)
-}
-
#[cfg(test)]
#[cfg(feature = "file_io")]
mod tests {
diff --git a/sdk/src/openssl/mod.rs b/sdk/src/openssl/mod.rs
@@ -13,26 +13,32 @@
#[cfg(feature = "openssl_sign")]
mod rsa_signer;
+#[cfg(feature = "openssl_sign")]
pub(crate) use rsa_signer::RsaSigner;
#[cfg(feature = "openssl")]
mod rsa_validator;
+#[cfg(feature = "openssl")]
pub(crate) use rsa_validator::RsaValidator;
#[cfg(feature = "openssl_sign")]
mod ec_signer;
+#[cfg(feature = "openssl_sign")]
pub(crate) use ec_signer::EcSigner;
#[cfg(feature = "openssl")]
mod ec_validator;
+#[cfg(feature = "openssl")]
pub(crate) use ec_validator::EcValidator;
#[cfg(feature = "openssl_sign")]
mod ed_signer;
+#[cfg(feature = "openssl_sign")]
pub(crate) use ed_signer::EdSigner;
#[cfg(feature = "openssl")]
mod ed_validator;
+#[cfg(feature = "openssl")]
pub(crate) use ed_validator::EdValidator;
#[cfg(feature = "openssl")]
@@ -48,7 +54,7 @@ pub(crate) use openssl_trust_handler::OpenSSLTrustHandlerConfig;
#[cfg(test)]
pub(crate) mod temp_signer_async;
-#[cfg(feature = "openssl_sign")]
+#[cfg(feature = "openssl")]
use openssl::x509::X509;
#[cfg(test)]
#[allow(unused_imports)]
diff --git a/sdk/src/openssl/openssl_trust_handler.rs b/sdk/src/openssl/openssl_trust_handler.rs
@@ -20,18 +20,17 @@ use std::{
use asn1_rs::Oid;
use crate::{
- error::{wrap_openssl_err, Result},
hash_utils::hash_sha256,
trust_handler::{load_eku_configuration, TrustHandlerConfig},
utils::base64,
- Error,
+ Error, Result,
};
fn certs_der_to_x509(ders: &[Vec<u8>]) -> Result<Vec<openssl::x509::X509>> {
let mut certs: Vec<openssl::x509::X509> = Vec::new();
for d in ders {
- let cert = openssl::x509::X509::from_der(d).map_err(wrap_openssl_err)?;
+ let cert = openssl::x509::X509::from_der(d).map_err(Error::OpenSslError)?;
certs.push(cert);
}
@@ -39,7 +38,7 @@ fn certs_der_to_x509(ders: &[Vec<u8>]) -> Result<Vec<openssl::x509::X509>> {
}
fn load_trust_from_pem_data(trust_data: &[u8]) -> Result<Vec<openssl::x509::X509>> {
- openssl::x509::X509::stack_from_pem(trust_data).map_err(wrap_openssl_err)
+ openssl::x509::X509::stack_from_pem(trust_data).map_err(Error::OpenSslError)
}
// Struct to handle verification of trust chains
@@ -71,7 +70,7 @@ impl OpenSSLTrustHandlerConfig {
fn update_store(&mut self) -> Result<()> {
let mut builder =
- openssl::x509::store::X509StoreBuilder::new().map_err(wrap_openssl_err)?;
+ openssl::x509::store::X509StoreBuilder::new().map_err(Error::OpenSslError)?;
// add trust anchors
for t in &self.trust_anchors {
@@ -137,7 +136,7 @@ impl TrustHandlerConfig for OpenSSLTrustHandlerConfig {
if let Ok(cert_list) = openssl::x509::X509::stack_from_pem(&buffer) {
for cert in &cert_list {
- let cert_der = cert.to_der().map_err(wrap_openssl_err)?;
+ let cert_der = cert.to_der().map_err(Error::OpenSslError)?;
let cert_sha256 = hash_sha256(&cert_der);
let cert_hash_base64 = base64::encode(&cert_sha256);
@@ -238,16 +237,16 @@ pub(crate) fn verify_trust(
return Ok(true);
}
- let mut cert_chain = openssl::stack::Stack::new().map_err(wrap_openssl_err)?;
- let mut store_ctx = openssl::x509::X509StoreContext::new().map_err(wrap_openssl_err)?;
+ let mut cert_chain = openssl::stack::Stack::new().map_err(Error::OpenSslError)?;
+ let mut store_ctx = openssl::x509::X509StoreContext::new().map_err(Error::OpenSslError)?;
let chain = certs_der_to_x509(chain_der)?;
for c in chain {
- cert_chain.push(c).map_err(wrap_openssl_err)?;
+ cert_chain.push(c).map_err(Error::OpenSslError)?;
}
- let cert = openssl::x509::X509::from_der(cert_der).map_err(wrap_openssl_err)?;
+ let cert = openssl::x509::X509::from_der(cert_der).map_err(Error::OpenSslError)?;
- let mut builder = openssl::x509::store::X509StoreBuilder::new().map_err(wrap_openssl_err)?;
+ let mut builder = openssl::x509::store::X509StoreBuilder::new().map_err(Error::OpenSslError)?;
// todo: figure out the passthrough case
if th.get_anchors().is_empty() {
@@ -256,7 +255,7 @@ pub(crate) fn verify_trust(
// add trust anchors
for d in th.get_anchors() {
- let c = openssl::x509::X509::from_der(&d).map_err(wrap_openssl_err)?;
+ let c = openssl::x509::X509::from_der(&d).map_err(Error::OpenSslError)?;
builder.add_cert(c)?;
}
// finalize store
diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs
@@ -0,0 +1,237 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+//! The Reader provides a way to read a manifest store from an asset.
+//! It also performs validation on the manifest store.
+
+#[cfg(feature = "file_io")]
+use std::fs::{read, File};
+use std::io::{Read, Seek, Write};
+
+use async_generic::async_generic;
+
+#[cfg(feature = "file_io")]
+use crate::error::Error;
+use crate::{
+ claim::ClaimAssetData, error::Result, manifest_store::ManifestStore,
+ settings::get_settings_value, status_tracker::DetailedStatusTracker, store::Store,
+ validation_status::ValidationStatus, Manifest,
+};
+
+/// A reader for the manifest store.
+#[derive(Debug)]
+pub struct Reader {
+ pub(crate) manifest_store: ManifestStore,
+}
+
+impl Reader {
+ /// Create a manifest store Reader from a stream.
+ /// # Arguments
+ /// * `format` - The format of the stream.
+ /// * `stream` - The stream to read from.
+ /// # Returns
+ /// A reader for the manifest store.
+ /// # Errors
+ /// If the stream is not a valid manifest store.
+ /// validation status should be checked for non severe errors
+ /// # Example
+ /// ```no_run
+ /// use std::io::Cursor;
+ ///
+ /// use c2pa::Reader;
+ /// let mut stream = Cursor::new(include_bytes!("../tests/fixtures/CA.jpg"));
+ /// let reader = Reader::from_stream("image/jpeg", stream).unwrap();
+ /// println!("{}", reader.json());
+ /// ```
+ #[async_generic()]
+ pub fn from_stream(format: &str, mut stream: impl Read + Seek + Send) -> Result<Reader> {
+ let verify = get_settings_value::<bool>("verify.verify_after_reading")?; // defaults to true
+ let reader = if _sync {
+ ManifestStore::from_stream(format, &mut stream, verify)
+ } else {
+ ManifestStore::from_stream_async(format, &mut stream, verify).await
+ }?;
+ Ok(Reader {
+ manifest_store: reader,
+ })
+ }
+
+ #[cfg(feature = "file_io")]
+ /// Create a manifest store Reader from a file.
+ /// # Arguments
+ /// * `path` - The path to the file.
+ /// # Returns
+ /// A reader for the manifest store.
+ /// # Errors
+ /// If the file is not a valid manifest store.
+ /// validation status should be checked for non severe errors.
+ /// # Example
+ /// ```no_run
+ /// use c2pa::Reader;
+ /// let reader = Reader::from_file("path/to/file.jpg").unwrap();
+ /// ```
+ /// # Note
+ /// If the file does not have a manifest store, the function will check for a sidecar manifest
+ /// with the same name and a .c2pa extension.
+ pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Reader> {
+ let path = path.as_ref();
+ let format = crate::format_from_path(path).ok_or(crate::Error::UnsupportedType)?;
+ let mut file = File::open(path)?;
+ let result = Self::from_stream(&format, &mut file);
+ if let Err(Error::JumbfNotFound) = result {
+ // if not embedded or cloud, check for sidecar first and load if it exists
+ let potential_sidecar_path = path.with_extension("c2pa");
+ if potential_sidecar_path.exists() {
+ let manifest_data = read(potential_sidecar_path)?;
+ return Self::from_manifest_data_and_stream(&manifest_data, &format, &mut file);
+ }
+ }
+ result
+ }
+
+ /// Create a manifest store [`Reader`]` from a JSON string.
+ /// # Arguments
+ /// * `json` - A Json String containing a manifest store definition.
+ /// # Returns
+ /// A [`Reader`]` for the manifest store.
+ /// # Note
+ /// This should only be used for testing
+ /// Any referenced resources will not be available
+ pub fn from_json(json: &str) -> Result<Reader> {
+ let manifest_store = serde_json::from_str(json)?;
+ Ok(Reader { manifest_store })
+ }
+
+ /// Create a manifest store [`Reader`] from existing c2pa_data and a stream
+ /// You can use this to validate a remote manifest or a sidecar manifest
+ /// # Arguments
+ /// * `c2pa_data` - The c2pa data (a manifest store in JUMBF format)
+ /// * `format` - The format of the stream
+ /// * `stream` - The stream to verify the store against
+ /// # Returns
+ /// A [`Reader`] for the manifest store
+ /// # Errors
+ /// If the c2pa_data is not valid, or severe errors occur in validation
+ /// validation status should be checked for non severe errors
+ #[async_generic()]
+ pub fn from_manifest_data_and_stream(
+ c2pa_data: &[u8],
+ format: &str,
+ mut stream: impl Read + Seek + Send,
+ ) -> Result<Reader> {
+ let mut validation_log = DetailedStatusTracker::new();
+
+ // first we convert the JUMBF into a usable store
+ let store = Store::from_jumbf(c2pa_data, &mut validation_log)?;
+
+ if _sync {
+ Store::verify_store(
+ &store,
+ &mut ClaimAssetData::Stream(&mut stream, format),
+ &mut validation_log,
+ )?;
+ } else {
+ Store::verify_store_async(
+ &store,
+ &mut ClaimAssetData::Stream(&mut stream, format),
+ &mut validation_log,
+ )
+ .await?;
+ }
+
+ Ok(Reader {
+ manifest_store: ManifestStore::from_store(&store, &validation_log),
+ })
+ }
+
+ /// Get the manifest store as a JSON string
+ pub fn json(&self) -> String {
+ self.manifest_store.to_string()
+ }
+
+ /// Get the [`ValidationStatus`] array of the manifest store if it exists.
+ ///
+ /// This validation report only includes error statuses on applied to the active manifest.
+ /// And error statuses for ingredients that are not already reported by the ingredient status.
+ /// The uri field can be used to identify the associated manifest.
+ /// # Example
+ /// ```no_run
+ /// use c2pa::Reader;
+ /// let stream = std::io::Cursor::new(include_bytes!("../tests/fixtures/CA.jpg"));
+ /// let reader = Reader::from_stream("image/jpeg", stream).unwrap();
+ /// let status = reader.validation_status();
+ /// ```
+ /// # Note
+ /// The validation status should be checked for validation errors.
+ pub fn validation_status(&self) -> Option<&[ValidationStatus]> {
+ self.manifest_store.validation_status()
+ }
+
+ /// Return the active [`Manifest`] if it exists.
+ pub fn active_manifest(&self) -> Option<&Manifest> {
+ self.manifest_store.get_active()
+ }
+
+ /// Return the active [`Manifest`] label if one exists.
+ pub fn active_label(&self) -> Option<&str> {
+ self.manifest_store.active_label()
+ }
+
+ /// Return a [`Manifest`] for a given label if it exists.
+ /// # Arguments
+ /// * `label` - The label of the requested [`Manifest`]
+ pub fn get_manifest(&self, label: &str) -> Option<&Manifest> {
+ self.manifest_store.get(label)
+ }
+
+ /// Write a resource identified by URI to the given stream.
+ /// # Arguments
+ /// * `uri` - The URI of the resource to write (from an identifier field).
+ /// * `stream` - The stream to write to.
+ /// # Returns
+ /// The number of bytes written.
+ /// # Errors
+ /// If the resource does not exist.
+ /// # Example
+ /// ```no_run
+ /// use c2pa::Reader;
+ /// let stream = std::io::Cursor::new(Vec::new());
+ /// let reader = Reader::from_file("path/to/file.jpg").unwrap();
+ /// let manifest = reader.active_manifest().unwrap();
+ /// let uri = &manifest.thumbnail_ref().unwrap().identifier;
+ /// let bytes_written = reader.resource_to_stream(uri, stream).unwrap();
+ /// ```
+ pub fn resource_to_stream(
+ &self,
+ uri: &str,
+ mut stream: impl Write + Read + Seek + Send,
+ ) -> Result<usize> {
+ self.manifest_store
+ .get_resource(uri, &mut stream)
+ .map(|size| size as usize)
+ }
+}
+
+impl Default for Reader {
+ fn default() -> Self {
+ Self {
+ manifest_store: ManifestStore::new(),
+ }
+ }
+}
+
+impl std::fmt::Display for Reader {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(self.json().as_str())
+ }
+}
diff --git a/sdk/src/resource_store.rs b/sdk/src/resource_store.rs
@@ -11,7 +11,11 @@
// specific language governing permissions and limitations under
// each license.
-use std::{borrow::Cow, collections::HashMap};
+use std::{
+ borrow::Cow,
+ collections::HashMap,
+ io::{Cursor, Read, Seek, Write},
+};
#[cfg(feature = "file_io")]
use std::{
fs::{create_dir_all, read, write},
@@ -22,13 +26,15 @@ use std::{
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use crate::{assertions::AssetType, claim::Claim, hashed_uri::HashedUri, Error, Result};
+use crate::{
+ assertions::AssetType, asset_io::CAIRead, claim::Claim, hashed_uri::HashedUri, Error, Result,
+};
/// Function that is used by serde to determine whether or not we should serialize
/// resources based on the `serialize_resources` flag.
/// (Serialization is disabled by default.)
pub(crate) fn skip_serializing_resources(_: &ResourceStore) -> bool {
- !cfg!(feature = "serialize_thumbnails") || cfg!(test)
+ !cfg!(feature = "serialize_thumbnails") || cfg!(test) || cfg!(not(target_arch = "wasm32"))
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
@@ -97,17 +103,17 @@ pub struct ResourceRef {
/// Relative JUMBF URIs will be resolved with the manifest label.
/// Relative file paths will be resolved with the base path if provided.
pub identifier: String,
- #[serde(skip_serializing_if = "Option::is_none")]
/// More detailed data types as defined in the C2PA spec.
- pub data_types: Option<Vec<AssetType>>,
#[serde(skip_serializing_if = "Option::is_none")]
+ pub data_types: Option<Vec<AssetType>>,
/// The algorithm used to hash the resource (if applicable).
- pub alg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
+ pub alg: Option<String>,
/// The hash of the resource (if applicable).
+ #[serde(skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
}
@@ -296,6 +302,32 @@ impl ResourceStore {
)
}
+ pub fn write_stream(
+ &self,
+ id: &str,
+ mut stream: impl Write + Read + Seek + Send,
+ ) -> Result<u64> {
+ #[cfg(feature = "file_io")]
+ if !self.resources.contains_key(id) {
+ match self.base_path.as_ref() {
+ Some(base) => {
+ // read from, the file to stream
+ let path = base.join(id);
+ let mut file = std::fs::File::open(path)?;
+ return std::io::copy(&mut file, &mut stream).map_err(Error::IoError);
+ }
+ None => return Err(Error::ResourceNotFound(id.to_string())),
+ }
+ }
+ match self.resources().get(id) {
+ Some(data) => {
+ stream.write_all(data).map_err(Error::IoError)?;
+ Ok(data.len() as u64)
+ }
+ None => Err(Error::ResourceNotFound(id.to_string())),
+ }
+ }
+
/// Returns `true` if the resource has been added or exists as file.
pub fn exists(&self, id: &str) -> bool {
if !self.resources.contains_key(id) {
@@ -327,13 +359,26 @@ impl Default for ResourceStore {
}
}
+pub trait ResourceResolver {
+ fn open(&self, reference: &ResourceRef) -> Result<Box<dyn CAIRead>>;
+}
+
+impl ResourceResolver for ResourceStore {
+ fn open(&self, reference: &ResourceRef) -> Result<Box<dyn CAIRead>> {
+ let data = self.get(&reference.identifier)?.into_owned();
+ let cursor = Cursor::new(data);
+ Ok(Box::new(cursor))
+ }
+}
+
#[cfg(test)]
#[cfg(feature = "openssl_sign")]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
+
use super::*;
- use crate::{utils::test::temp_signer, Manifest};
+ use crate::{utils::test::temp_signer, Builder, Reader};
#[test]
#[cfg(feature = "openssl_sign")]
@@ -369,28 +414,31 @@ mod tests {
}]
}"#;
- let mut manifest = Manifest::from_json(json).expect("from json");
- manifest
- .resources_mut()
- .add("abc123", *value)
+ let mut builder = Builder::from_json(json).expect("from json");
+ builder
+ .add_resource("abc123", &mut Cursor::new(value))
.expect("add_resource");
- let ingredient = &mut manifest.ingredients_mut()[0];
- ingredient
- .resources_mut()
- .add("cba321", *value)
+ builder
+ .add_resource("cba321", &mut Cursor::new(value))
.expect("add_resource");
- println!("{manifest}");
let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg");
let signer = temp_signer();
// Embed a manifest using the signer.
- let output_image = manifest
- .embed_from_memory("jpeg", image, signer.as_ref())
- .expect("embed_stream");
-
- let _manifest_store =
- crate::ManifestStore::from_bytes("jpeg", &output_image, true).expect("from_bytes");
- // println!("{manifest_store}");
+ let mut output_image = Cursor::new(Vec::new());
+ builder
+ .sign(
+ "image/jpeg",
+ &mut Cursor::new(image),
+ &mut output_image,
+ &*signer,
+ )
+ .expect("sign");
+
+ output_image.set_position(0);
+ let reader = Reader::from_stream("jpeg", &mut output_image).expect("from_bytes");
+ let _json = reader.json();
+ println!("{_json}");
}
}
diff --git a/sdk/src/settings.rs b/sdk/src/settings.rs
@@ -11,9 +11,10 @@
// specific language governing permissions and limitations under
// each license.
+#[cfg(feature = "file_io")]
+use std::path::Path;
use std::{
io::{BufRead, BufReader, Cursor},
- path::Path,
sync::RwLock,
};
@@ -153,6 +154,7 @@ impl SettingsValidate for Core {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[allow(unused)]
pub(crate) struct Verify {
+ verify_after_reading: bool,
verify_after_sign: bool,
verify_trust: bool,
ocsp_fetch: bool,
@@ -162,6 +164,7 @@ pub(crate) struct Verify {
impl Default for Verify {
fn default() -> Self {
Self {
+ verify_after_reading: true,
verify_after_sign: true,
verify_trust: false,
ocsp_fetch: false,
@@ -195,7 +198,7 @@ impl SettingsValidate for Manifest {}
// setting for the entire C2PA-RS instance.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[allow(unused)]
-pub struct Settings {
+pub(crate) struct Settings {
trust: Trust,
core: Core,
verify: Verify,
@@ -204,6 +207,7 @@ pub struct Settings {
impl Settings {
#[allow(unused)]
+ #[cfg(feature = "file_io")]
pub fn from_file<P: AsRef<Path>>(setting_path: P) -> Result<Self> {
let ext = setting_path
.as_ref()
@@ -220,7 +224,7 @@ impl Settings {
let f = match format.to_lowercase().as_str() {
"json" => FileFormat::Json,
"json5" => FileFormat::Json5,
- "ini" => FileFormat::Ini,
+ //"ini" => FileFormat::Ini,
"toml" => FileFormat::Toml,
//"yaml" => FileFormat::Yaml,
"ron" => FileFormat::Ron,
@@ -294,7 +298,8 @@ pub(crate) fn get_settings() -> Option<Settings> {
// Load settings from configuration file
#[allow(unused)]
-pub fn load_settings<P: AsRef<Path>>(settings_path: P) -> Result<()> {
+#[cfg(feature = "file_io")]
+pub(crate) fn load_settings<P: AsRef<Path>>(settings_path: P) -> Result<()> {
let ext = settings_path
.as_ref()
.extension()
@@ -314,7 +319,8 @@ pub fn load_settings_from_str(settings_str: &str, format: &str) -> Result<()> {
// Save the current configuration to a json file.
#[allow(unused)]
-pub fn save_settings_as_json<P: AsRef<Path>>(settings_path: P) -> Result<()> {
+#[cfg(feature = "file_io")]
+pub(crate) fn save_settings_as_json<P: AsRef<Path>>(settings_path: P) -> Result<()> {
let settings =
get_settings().ok_or(Error::OtherError("could not get current settings".into()))?;
diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs
@@ -57,7 +57,9 @@ pub trait Signer {
.map(|url| crate::time_stamp::default_rfc3161_request(&url, headers, message))
}
#[cfg(target_arch = "wasm32")]
- fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>>;
+ fn send_timestamp_request(&self, _message: &[u8]) -> Option<Result<Vec<u8>>> {
+ None
+ }
/// OCSP response for the signing cert if available
/// This is the only C2PA supported cert revocation method.
@@ -158,6 +160,14 @@ pub trait AsyncSigner: Sync {
fn ocsp_val(&self) -> Option<Vec<u8>> {
None
}
+
+ /// If this returns true the sign function is responsible for for direct handling of the COSE structure.
+ ///
+ /// This is useful for cases where the signer needs to handle the COSE structure directly.
+ /// Not recommended for general use.
+ fn direct_cose_handling(&self) -> bool {
+ false
+ }
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
diff --git a/sdk/src/signing_alg.rs b/sdk/src/signing_alg.rs
@@ -15,6 +15,10 @@
use std::{fmt, str::FromStr};
+#[cfg(feature = "json_schema")]
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
/// Describes the digital signature algorithms allowed by the C2PA spec.
///
/// Per <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_digital_signatures>:
@@ -22,7 +26,8 @@ use std::{fmt, str::FromStr};
/// > All digital signatures that are stored in a C2PA Manifest shall
/// > be generated using one of the digital signature algorithms and
/// > key types listed as described in this section.
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub enum SigningAlg {
/// ECDSA with SHA-256
Es256,
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -47,6 +47,7 @@ use crate::{
get_assetio_handler, load_jumbf_from_stream, object_locations_from_stream,
save_jumbf_to_memory, save_jumbf_to_stream,
},
+ manifest_store_report::ManifestStoreReport,
salt::DefaultSalt,
settings::get_settings_value,
status_tracker::{log_item, OneShotStatusTracker, StatusTracker},
@@ -55,7 +56,7 @@ use crate::{
hash_utils::{hash_sha256, HashRange},
patch::patch_bytes,
},
- validation_status, AsyncSigner, ManifestStoreReport, RemoteSigner, Signer,
+ validation_status, AsyncSigner, RemoteSigner, Signer,
};
#[cfg(feature = "file_io")]
use crate::{
@@ -452,6 +453,7 @@ impl Store {
}
/// Return certificate chain for the provenance claim
+ #[cfg(feature = "v1_api")]
pub(crate) fn get_provenance_cert_chain(&self) -> Result<String> {
let claim = self.provenance_claim().ok_or(Error::ProvenanceMissing)?;
@@ -2075,6 +2077,56 @@ impl Store {
}
}
+ /// Async version of save_to_stream
+ ///
+ /// This can also handle remote signing if direct_cose_handling() is true
+ pub async fn save_to_stream_async(
+ &mut self,
+ format: &str,
+ input_stream: &mut dyn CAIRead,
+ output_stream: &mut dyn CAIReadWrite,
+ signer: &dyn AsyncSigner,
+ ) -> Result<Vec<u8>> {
+ let intermediate_output: Vec<u8> = Vec::new();
+ let mut intermediate_stream = Cursor::new(intermediate_output);
+
+ let jumbf_bytes = self.start_save_stream(
+ format,
+ input_stream,
+ &mut intermediate_stream,
+ signer.reserve_size(),
+ )?;
+
+ let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?;
+ let claim_bytes = pc.data()?;
+ let sig = if signer.direct_cose_handling() {
+ // let the signer do all the COSE processing and return the structured COSE data
+ signer.sign(claim_bytes).await?
+ } else {
+ self.sign_claim_async(pc, signer, signer.reserve_size())
+ .await?
+ };
+ let sig_placeholder = Store::sign_claim_placeholder(pc, signer.reserve_size());
+
+ match self.finish_save_stream(
+ jumbf_bytes,
+ format,
+ &mut intermediate_stream,
+ output_stream,
+ sig,
+ &sig_placeholder,
+ ) {
+ Ok((s, m)) => {
+ // save sig so store is up to date
+ let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
+ pc_mut.set_signature_val(s);
+
+ Ok(m)
+ }
+ Err(e) => Err(e),
+ }
+ }
+
/// Async RemoteSigner used to embed the claims store and returns memory representation of the
/// asset and manifest. Updates XMP with provenance record.
/// When called, the stream should contain an asset matching format.
@@ -3130,6 +3182,7 @@ pub mod tests {
use super::*;
use crate::{
+ assertion::AssertionJson,
assertions::{labels::BOX_HASH, Action, Actions, BoxHash, Uuid},
claim::AssertionStoreJsonFormat,
jumbf_io::{get_assetio_handler_from_path, update_file_jumbf},
@@ -3142,7 +3195,7 @@ pub mod tests {
write_jpeg_placeholder_file,
},
},
- AssertionJson, SigningAlg,
+ SigningAlg,
};
fn create_editing_claim(claim: &mut Claim) -> Result<&mut Claim> {
@@ -3408,7 +3461,6 @@ pub mod tests {
store.commit_claim(claim).unwrap();
- // JUMBF generation should fail because the certificate won't validate.
let r = store.save_to_asset(&ap, &signer, &op);
assert!(r.is_err());
assert_eq!(r.err().unwrap().to_string(), "COSE certificate has expired");
diff --git a/sdk/src/utils/mime.rs b/sdk/src/utils/mime.rs
@@ -18,38 +18,54 @@ pub fn extension_to_mime(extension: &str) -> Option<&'static str> {
"png" => "image/png",
"gif" => "image/gif",
"psd" => "image/vnd.adobe.photoshop",
- "tiff" => "image/tiff",
+ "tiff" | "tif" => "image/tiff",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"bmp" => "image/bmp",
"webp" => "image/webp",
- "dng" => "image/dng",
+ "dng" => "image/x-adobe-dng",
"heic" => "image/heic",
"heif" => "image/heif",
"mp2" | "mpa" | "mpe" | "mpeg" | "mpg" | "mpv2" => "video/mpeg",
"mp4" => "video/mp4",
+ "avi" => "video/avi",
"avif" => "image/avif",
"mov" | "qt" => "video/quicktime",
"m4a" => "audio/mp4",
"mid" | "rmi" => "audio/mid",
"mp3" => "audio/mpeg",
- "wav" => "audio/vnd.wav",
+ "wav" => "audio/wav",
"aif" | "aifc" | "aiff" => "audio/aiff",
"ogg" => "audio/ogg",
"pdf" => "application/pdf",
"ai" => "application/postscript",
+ "arw" => "image/x-sony-arw",
+ "nef" => "image/x-nikon-nef",
+ "c2pa" | "application/x-c2pa-manifest-store" | "application/c2pa" => "application/c2pa",
_ => return None,
})
}
+/// Convert a format to a MIME type
+/// formats can be passed in as extensions, e.g. "jpg" or "jpeg"
+/// or as MIME types, e.g. "image/jpeg"
+pub fn format_to_mime(format: &str) -> String {
+ match extension_to_mime(format) {
+ Some(mime) => mime,
+ None => format,
+ }
+ .to_string()
+}
+
/// Converts a format to a file extension
+#[cfg(feature = "file_io")]
pub fn format_to_extension(format: &str) -> Option<&'static str> {
Some(match format {
"jpg" | "jpeg" | "image/jpeg" => "jpg",
"png" | "image/png" => "png",
"gif" | "image/gif" => "gif",
"psd" | "image/vnd.adobe.photoshop" => "psd",
- "tiff" | "image/tiff" => "tiff",
+ "tiff" | "tif" | "image/tiff" => "tiff",
"svg" | "image/svg+xml" => "svg",
"ico" | "image/x-icon" => "ico",
"bmp" | "image/bmp" => "bmp",
@@ -60,15 +76,28 @@ pub fn format_to_extension(format: &str) -> Option<&'static str> {
"mp2" | "mpa" | "mpe" | "mpeg" | "mpg" | "mpv2" | "video/mpeg" => "mp2",
"mp4" | "video/mp4" => "mp4",
"avif" | "image/avif" => "avif",
+ "avi" | "video/avi" => "avi",
"mov" | "qt" | "video/quicktime" => "mov",
"m4a" | "audio/mp4" => "m4a",
"mid" | "rmi" | "audio/mid" => "mid",
"mp3" | "audio/mpeg" => "mp3",
- "wav" | "audio/vnd.wav" => "wav",
+ "wav" | "audio/wav" | "audio/wave" | "audio.vnd.wave" => "wav",
"aif" | "aifc" | "aiff" | "audio/aiff" => "aif",
"ogg" | "audio/ogg" => "ogg",
"pdf" | "application/pdf" => "pdf",
"ai" | "application/postscript" => "ai",
+ "arw" | "image/x-sony-arw" => "arw",
+ "nef" | "image/x-nikon-nef" => "nef",
+ "c2pa" | "application/x-c2pa-manifest-store" | "application/c2pa" => "c2pa",
_ => return None,
})
}
+
+/// Return a MIME type given a file path.
+///
+/// This function will use the file extension to determine the MIME type.
+pub fn format_from_path<P: AsRef<std::path::Path>>(path: P) -> Option<String> {
+ path.as_ref()
+ .extension()
+ .map(|ext| crate::utils::mime::format_to_mime(ext.to_string_lossy().as_ref()))
+}
diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs
@@ -16,7 +16,6 @@ pub(crate) mod cbor_types;
#[allow(dead_code)]
pub(crate) mod hash_utils;
pub(crate) mod merkle;
-#[cfg(feature = "file_io")]
pub(crate) mod mime;
#[allow(dead_code)] // for wasm build
pub(crate) mod patch;
diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs
@@ -414,6 +414,74 @@ pub fn temp_remote_signer() -> Box<dyn RemoteSigner> {
Box::new(TempRemoteSigner {})
}
+/// Create an AsyncSigner that acts as a RemoteSigner
+struct TempAsyncRemoteSigner {
+ signer: TempRemoteSigner,
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
+impl crate::signer::AsyncSigner for TempAsyncRemoteSigner {
+ // this will not be called but requires an implementation
+ async fn sign(&self, claim_bytes: Vec<u8>) -> Result<Vec<u8>> {
+ #[cfg(feature = "openssl_sign")]
+ {
+ let signer =
+ crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256);
+
+ // this would happen on some remote server
+ crate::cose_sign::cose_sign_async(&signer, &claim_bytes, self.reserve_size()).await
+ }
+ #[cfg(not(feature = "openssl_sign"))]
+ {
+ use std::io::{Seek, Write};
+
+ let mut sign_bytes = std::io::Cursor::new(vec![0u8; self.reserve_size()]);
+
+ sign_bytes.rewind()?;
+ sign_bytes.write_all(&claim_bytes)?;
+
+ // fake sig
+ Ok(sign_bytes.into_inner())
+ }
+ }
+
+ // signer will return a COSE structure
+ fn direct_cose_handling(&self) -> bool {
+ true
+ }
+
+ fn alg(&self) -> SigningAlg {
+ SigningAlg::Ps256
+ }
+
+ fn certs(&self) -> Result<Vec<Vec<u8>>> {
+ Ok(Vec::new())
+ }
+
+ fn reserve_size(&self) -> usize {
+ 10000
+ }
+
+ async fn send_timestamp_request(
+ &self,
+ _message: &[u8],
+ ) -> Option<crate::error::Result<Vec<u8>>> {
+ Some(Ok(Vec::new()))
+ }
+}
+
+/// Create a [`AsyncSigner`] that does it's own COSE handling for testing.
+///
+/// # Returns
+///
+/// Returns a boxed [`RemoteSigner`] instance.
+pub fn temp_async_remote_signer() -> Box<dyn crate::signer::AsyncSigner> {
+ Box::new(TempAsyncRemoteSigner {
+ signer: TempRemoteSigner {},
+ })
+}
+
#[test]
fn test_create_test_store() {
#[allow(clippy::expect_used)]
diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs
@@ -14,7 +14,7 @@
use chrono::{DateTime, Utc};
use x509_parser::num_bigint::BigUint;
-#[cfg(feature = "openssl_sign")]
+#[cfg(feature = "openssl")]
use crate::openssl::{EcValidator, EdValidator, RsaValidator};
use crate::{Result, SigningAlg};
@@ -72,7 +72,7 @@ pub(crate) fn get_validator(alg: SigningAlg) -> Box<dyn CoseValidator> {
}
}
-#[cfg(not(feature = "openssl_sign"))]
+#[cfg(not(feature = "openssl"))]
#[allow(dead_code)]
pub(crate) fn get_validator(_alg: SigningAlg) -> Box<dyn CoseValidator> {
Box::new(DummyValidator)
diff --git a/sdk/tests/common/compare_readers.rs b/sdk/tests/common/compare_readers.rs
@@ -0,0 +1,185 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// Compares two manifest stores and prints out the differences.
+use std::collections::HashMap;
+use std::{fs, path::Path};
+
+use c2pa::{Error, Reader, Result};
+
+use super::reader_from_file;
+
+#[allow(unused)]
+/// Compares all the files in two directories and returns a list of issues
+pub fn compare_folders<P: AsRef<Path>, Q: AsRef<Path>>(folder1: P, folder2: Q) -> Result<()> {
+ let folder1 = folder1.as_ref();
+ let folder2 = folder2.as_ref();
+
+ // handle the case we have files instead of folders
+ if folder1.is_file() && folder2.is_file() {
+ let issues = compare_files(folder1, folder2)?;
+ if !issues.is_empty() {
+ eprintln!("Failed {:?}", folder1);
+ for issue in issues {
+ eprintln!(" {}", issue);
+ }
+ } else {
+ println!("Passed {:?}", folder1);
+ }
+ return Ok(());
+ } else if !(folder1.is_dir() && folder2.is_dir()) {
+ eprintln!("must be two folders or two files");
+ return Err(Error::BadParam(
+ "must be two folders or two files".to_string(),
+ ));
+ }
+
+ for entry in fs::read_dir(folder1)? {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_file() {
+ let relative_path = path.strip_prefix(folder1).unwrap();
+ let other_path = folder2.join(relative_path);
+ //println!("Comparing {:?} to {:?}", path, other_path);
+ let mut issues = Vec::new();
+ if other_path.exists() {
+ let result = compare_files(&path, &other_path)?;
+ issues.extend(result);
+ } else {
+ issues.push(format!(
+ "File {} does not exist in {}",
+ relative_path.display(),
+ folder2.display()
+ ));
+ }
+ if !issues.is_empty() {
+ eprintln!("Failed {:?}", relative_path);
+ for issue in issues {
+ eprintln!(" {}", issue);
+ }
+ } else {
+ println!("Passed {:?}", relative_path);
+ }
+ }
+ }
+ Ok(())
+}
+
+/// Compares files with manifest stores and returns a list of issues
+pub fn compare_files<P: AsRef<Path>, Q: AsRef<Path>>(m1: P, m2: Q) -> Result<Vec<String>> {
+ let result1 = match m1.as_ref().extension() {
+ Some(ext) if ext == "json" => Reader::from_json(&fs::read_to_string(m1)?),
+ _ => reader_from_file(m1.as_ref()),
+ };
+ let result2 = match m2.as_ref().extension() {
+ Some(ext) if ext == "json" => Reader::from_json(&fs::read_to_string(m2)?),
+ _ => reader_from_file(m2.as_ref()),
+ };
+ match (result1, result2) {
+ (Ok(reader1), Ok(reader2)) => compare_readers(&reader1, &reader2),
+ (Err(Error::JumbfNotFound), Err(Error::JumbfNotFound)) => Ok(Vec::new()),
+ (_, Err(err)) => Err(err),
+ (Err(err), _) => Err(err),
+ }
+}
+
+/// Compares two manifest stores and returns a list of issues.
+pub fn compare_readers(reader1: &Reader, reader2: &Reader) -> Result<Vec<String>> {
+ // first we need to gather all the manifests in the order they are first seen recursively
+ let mut labels1 = Vec::new();
+ if let Some(label) = reader1.active_label() {
+ gather_manifests(reader1, label, &mut labels1);
+ }
+ let mut labels2 = Vec::new();
+ if let Some(label) = reader2.active_label() {
+ gather_manifests(reader2, label, &mut labels2);
+ }
+ // now we have two lists of manifests, we need to match them up
+ let manifest_map: HashMap<_, _> = labels1.into_iter().zip(labels2).collect();
+
+ // now we can compare the manifests
+ let mut issues = Vec::new();
+ for (label1, label2) in manifest_map.iter() {
+ // convert manifests into json values and compare them
+ let value1 = serde_json::to_value(reader1.get_manifest(label1))?;
+ let value2 = serde_json::to_value(reader2.get_manifest(label2))?;
+ compare_json_values(
+ &format!("manifests.{}", label1),
+ &value1,
+ &value2,
+ &mut issues,
+ );
+ }
+ Ok(issues)
+}
+
+// creates list of manifests in the order they are first seen from the active manifest
+fn gather_manifests(manifest_store: &Reader, manifest_label: &str, labels: &mut Vec<String>) {
+ if !labels.contains(&manifest_label.to_string()) {
+ labels.push(manifest_label.to_string());
+ }
+ if let Some(manifest) = manifest_store.get_manifest(manifest_label) {
+ for ingredient in manifest.ingredients() {
+ if let Some(label) = ingredient.active_manifest() {
+ gather_manifests(manifest_store, label, labels);
+ }
+ }
+ }
+}
+
+/// Recursively compare two ManifestStore JSON values
+fn compare_json_values(
+ path: &str,
+ val1: &serde_json::Value,
+ val2: &serde_json::Value,
+ issues: &mut Vec<String>,
+) {
+ match (val1, val2) {
+ (serde_json::Value::Object(map1), serde_json::Value::Object(map2)) => {
+ for (key, val1) in map1 {
+ let val2 = map2.get(key).unwrap_or(&serde_json::Value::Null);
+ compare_json_values(&format!("{}.{}", path, key), val1, val2, issues);
+ }
+
+ for (key, value) in map2 {
+ if map1.get(key).is_none() {
+ issues.push(format!("Added {}.{}: {}", path, key, value));
+ }
+ }
+ }
+ (serde_json::Value::Array(arr1), serde_json::Value::Array(arr2)) => {
+ for (i, (val1, val2)) in arr1.iter().zip(arr2.iter()).enumerate() {
+ compare_json_values(&format!("{}[{}]", path, i), val1, val2, issues);
+ }
+ }
+ (val1, val2) if val1 != val2 => {
+ if !(path.ends_with(".instance_id")
+ || path.ends_with(".instanceId")
+ || path.ends_with(".time")
+ || path.contains(".hash")
+ || path.contains("claim_generator") // changes with every version (todo: get more specific)
+ || val1.is_string() && val2.is_string() && val1.to_string().contains("urn:uuid:"))
+ {
+ if val2.is_null() {
+ issues.push(format!("Missing {}: {}", path, val1));
+ } else if val2.is_null() {
+ dbg!(&path);
+ issues.push(format!("Added {}: {}", path, val2));
+ } else {
+ issues.push(format!("Changed {}: {} vs {}", path, val1, val2));
+ }
+ }
+ }
+ _ => (),
+ }
+}
diff --git a/sdk/tests/common/mod.rs b/sdk/tests/common/mod.rs
@@ -0,0 +1,112 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+mod compare_readers;
+mod test_signer;
+
+use std::{
+ fs,
+ io::{Read, Seek},
+ path::{Path, PathBuf},
+};
+
+use c2pa::{format_from_path, Reader, Result};
+pub use compare_readers::compare_readers;
+#[allow(unused)]
+pub use test_signer::test_signer;
+
+#[allow(unused_macros)]
+macro_rules! assert_err {
+ ($expression:expr, $($pattern:tt)+) => {
+ match $expression {
+ $($pattern)+ => (),
+ ref e => panic!("expected `{}` but got `{:?}`", stringify!($($pattern)+), e),
+ }
+ }
+}
+#[allow(unused_imports)]
+pub(super) use assert_err;
+
+pub fn fixtures_path<P: AsRef<Path>>(file_name: P) -> std::path::PathBuf {
+ PathBuf::from("tests/fixtures").join(file_name)
+}
+
+pub fn known_good_path<P: AsRef<Path>>(file_name: P) -> std::path::PathBuf {
+ PathBuf::from("tests/known_good").join(file_name)
+}
+
+/// get a file from path without requiring file_io feature enabled in the c2pa crate
+pub fn reader_from_file<P: AsRef<Path>>(path: P) -> Result<Reader> {
+ let format = format_from_path(&path).ok_or(c2pa::Error::UnsupportedType)?;
+ Reader::from_stream(&format, &mut fs::File::open(&path)?)
+}
+
+#[allow(unused)]
+pub fn write_known_good<P: AsRef<Path>>(file_name: P) -> Result<()> {
+ let reader = reader_from_file(fixtures_path(&file_name))?;
+ let mut path = known_good_path(file_name);
+ path.set_extension("json");
+ fs::write(path, reader.json()).map_err(Into::into)
+}
+
+pub fn read_known_good<P: AsRef<Path>>(file_name: P) -> std::io::Result<String> {
+ let mut path = known_good_path(file_name);
+ path.set_extension("json");
+ fs::read_to_string(path)
+}
+
+#[allow(unused)]
+pub fn compare_to_known_good<P: AsRef<Path>>(reader: &Reader, file_name: P) -> Result<()> {
+ let known = read_known_good(&file_name)?;
+ let reader1 = Reader::from_json(&known)?;
+ //et reader2 = reader_from_file(fixtures_path(file_name))?;
+ let result = compare_readers(&reader1, reader)?;
+ assert!(result.is_empty(), "{}", result.join("\n"));
+ Ok(())
+}
+
+#[allow(unused)]
+pub fn compare_stream_to_known_good<P: AsRef<Path>, S: Read + Seek + Send>(
+ stream: &mut S,
+ format: &str,
+ known_file: P,
+) -> Result<()> {
+ let known = read_known_good(&known_file)?;
+ let reader1 = Reader::from_json(&known)?;
+ let reader2 = Reader::from_stream(format, stream)?;
+ let result = compare_readers(&reader1, &reader2)?;
+ assert!(result.is_empty(), "{}", result.join("\n"));
+ Ok(())
+}
+
+#[allow(unused)]
+pub fn fixture_stream(name: &str) -> Result<(String, fs::File)> {
+ let path = fixtures_path(name);
+ let format = format_from_path(&path).ok_or(c2pa::Error::UnsupportedType)?;
+ let stream = fs::File::open(&path)?;
+ Ok((format, stream))
+}
+
+#[allow(unused)]
+pub fn check_validation_status(reader: &Reader, code: &str) {
+ if let Some(validation_statuses) = reader.validation_status() {
+ assert!(
+ validation_statuses
+ .iter()
+ .any(|status| status.code() == code),
+ "Expected to find {code} in validation status"
+ );
+ } else {
+ panic!("Expected to find validation status");
+ }
+}
diff --git a/sdk/tests/common/test_signer.rs b/sdk/tests/common/test_signer.rs
@@ -0,0 +1,39 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use c2pa::{CallbackSigner, SigningAlg};
+
+const CERTS: &[u8] = include_bytes!("../../tests/fixtures/certs/ed25519.pub");
+const PRIVATE_KEY: &[u8] = include_bytes!("../../tests/fixtures/certs/ed25519.pem");
+
+pub fn test_signer() -> CallbackSigner {
+ let ed_signer = |_context: *const _, data: &[u8]| ed_sign(data, PRIVATE_KEY);
+ CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS)
+ .set_context("test" as *const _ as *const ())
+}
+
+fn ed_sign(data: &[u8], private_key: &[u8]) -> c2pa::Result<Vec<u8>> {
+ use ed25519_dalek::{Signature, Signer, SigningKey};
+ use pem::parse;
+
+ // Parse the PEM data to get the private key
+ let pem = parse(private_key).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
+ // For Ed25519, the key is 32 bytes long, so we skip the first 16 bytes of the PEM data
+ let key_bytes = &pem.contents()[16..];
+ let signing_key =
+ SigningKey::try_from(key_bytes).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
+ // Sign the data
+ let signature: Signature = signing_key.sign(data);
+
+ Ok(signature.to_bytes().to_vec())
+}
diff --git a/sdk/tests/fixtures/sample1.png b/sdk/tests/fixtures/sample1.png
Binary files differ.
diff --git a/sdk/tests/fixtures/simple_manifest.json b/sdk/tests/fixtures/simple_manifest.json
@@ -0,0 +1,31 @@
+{
+ "claim_generator": "test",
+ "claim_generator_info": [
+ {
+ "name": "test",
+ "version": "1.0"
+ }
+ ],
+ "format" : "image/jpeg",
+ "assertions": [
+ {
+ "label": "c2pa.actions.v2",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "parameters": {
+ "description": "import"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia",
+ "softwareAgent": {
+ "name": "TestApp",
+ "version": "1.0",
+ "something": "else"
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/sdk/tests/integration.rs b/sdk/tests/integration.rs
@@ -11,8 +11,8 @@
// specific language governing permissions and limitations under
// each license.
-/// complete functional integration test with acquisitions and ingredients
-// isolate from wasm by wrapping in module
+/// Complete functional integration test with parent and ingredients.
+// Isolate from wasm by wrapping in module.
#[cfg(feature = "file_io")]
mod integration_1 {
@@ -20,7 +20,9 @@ mod integration_1 {
use c2pa::{
assertions::{c2pa_action, Action, Actions},
- create_signer, settings, Ingredient, Manifest, ManifestStore, Result, Signer, SigningAlg,
+ create_signer,
+ settings::load_settings_from_str,
+ Ingredient, Manifest, ManifestStore, Result, Signer, SigningAlg,
};
use tempfile::tempdir;
@@ -53,7 +55,7 @@ mod integration_1 {
let replacement_val = serde_json::Value::String(trust_list).to_string(); // escape string
let setting = ta.replace("replacement_val", &replacement_val);
- c2pa::settings::load_settings_from_str(&setting, "json")?;
+ load_settings_from_str(&setting, "json")?;
enable_trust_checks = true;
}
@@ -62,7 +64,7 @@ mod integration_1 {
let replacement_val = serde_json::Value::String(allowed_list).to_string(); // escape string
let setting = al.replace("replacement_val", &replacement_val);
- c2pa::settings::load_settings_from_str(&setting, "json")?;
+ load_settings_from_str(&setting, "json")?;
enable_trust_checks = true;
}
@@ -71,17 +73,14 @@ mod integration_1 {
let replacement_val = serde_json::Value::String(trust_config).to_string(); // escape string
let setting = tc.replace("replacement_val", &replacement_val);
- c2pa::settings::load_settings_from_str(&setting, "json")?;
+ load_settings_from_str(&setting, "json")?;
enable_trust_checks = true;
}
// enable trust checks
if enable_trust_checks {
- c2pa::settings::load_settings_from_str(
- r#"{"verify": { "verify_trust": true} }"#,
- "json",
- )?;
+ load_settings_from_str(r#"{"verify": { "verify_trust": true} }"#, "json")?;
}
Ok(())
@@ -173,7 +172,7 @@ mod integration_1 {
fn test_embed_json_manifest() -> Result<()> {
let _protect = PROTECT.lock().unwrap();
- settings::reset_default_settings().unwrap();
+ c2pa::settings::reset_default_settings().unwrap();
// set up parent and destination paths
let dir = tempdir()?;
diff --git a/sdk/tests/known_good/C.json b/sdk/tests/known_good/C.json
@@ -0,0 +1,54 @@
+{
+ "active_manifest": "contentauth:urn:uuid:d17a0d3c-fd12-4381-b14f-1760b5fe0469",
+ "manifests": {
+ "contentauth:urn:uuid:d17a0d3c-fd12-4381-b14f-1760b5fe0469": {
+ "claim_generator": "make_test_images/0.24.0 c2pa-rs/0.24.0",
+ "title": "C.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:51c597ec-6364-4411-ad6e-88bf76e60134",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "Adobe make_test"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.created",
+ "softwareAgent": "Make Test Images 0.24.0",
+ "parameters": {
+ "name": "gradient"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2023-07-13T18:58:32+00:00"
+ },
+ "label": "contentauth:urn:uuid:d17a0d3c-fd12-4381-b14f-1760b5fe0469"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/sdk/tests/known_good/CA.json b/sdk/tests/known_good/CA.json
@@ -0,0 +1,74 @@
+{
+ "active_manifest": "contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921",
+ "manifests": {
+ "contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921": {
+ "claim_generator": "make_test_images/0.24.0 c2pa-rs/0.24.0",
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:68c08d77-0713-4127-9fea-1f6a6a8635ba",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "Adobe make_test"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2023-07-13T18:58:32+00:00"
+ },
+ "label": "contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921"
+ }
+ }
+}
+\ No newline at end of file
diff --git a/sdk/tests/known_good/CA_test.json b/sdk/tests/known_good/CA_test.json
@@ -0,0 +1,59 @@
+{
+ "active_manifest": "urn:uuid:d0d0c9a8-9ebb-45b1-a184-eb458e92cd47",
+ "manifests": {
+ "urn:uuid:d0d0c9a8-9ebb-45b1-a184-eb458e92cd47": {
+ "claim_generator": "test/1.0 c2pa-rs/0.31.1-dev",
+ "claim_generator_info": [
+ {
+ "name": "test",
+ "version": "1.0"
+ },
+ {
+ "name": "c2pa-rs",
+ "version": "0.31.1-dev"
+ }
+ ],
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:d6b460e3-fce8-4892-a123-bbb8c28cd9af",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [],
+ "assertions": [
+ {
+ "label": "c2pa.actions.v2",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "softwareAgent": {
+ "name": "TestApp",
+ "version": "1.0",
+ "something": "else"
+ },
+ "parameters": {
+ "description": "import"
+ },
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia"
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ed25519",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "638838410810235485828984295321338730070538954823"
+ },
+ "label": "urn:uuid:d0d0c9a8-9ebb-45b1-a184-eb458e92cd47"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "assertion.dataHash.mismatch",
+ "url": "self#jumbf=/c2pa/urn:uuid:d0d0c9a8-9ebb-45b1-a184-eb458e92cd47/c2pa.assertions/c2pa.hash.data",
+ "explanation": "asset hash error, name: jumbf manifest, error: hash verification( Hashes do not match )"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/sdk/tests/known_good/XCA.json b/sdk/tests/known_good/XCA.json
@@ -0,0 +1,81 @@
+{
+ "active_manifest": "contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921",
+ "manifests": {
+ "contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921": {
+ "claim_generator": "make_test_images/0.24.0 c2pa-rs/0.24.0",
+ "title": "CA.jpg",
+ "format": "image/jpeg",
+ "instance_id": "xmp:iid:68c08d77-0713-4127-9fea-1f6a6a8635ba",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.jpeg"
+ },
+ "ingredients": [
+ {
+ "title": "A.jpg",
+ "format": "image/jpeg",
+ "document_id": "xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "instance_id": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg"
+ },
+ "relationship": "parentOf"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "stds.schema-org.CreativeWork",
+ "data": {
+ "@context": "http://schema.org/",
+ "@type": "CreativeWork",
+ "author": [
+ {
+ "@type": "Person",
+ "name": "Adobe make_test"
+ }
+ ]
+ },
+ "kind": "Json"
+ },
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.opened",
+ "instanceId": "xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb",
+ "parameters": {
+ "ingredient": {
+ "url": "self#jumbf=c2pa.assertions/c2pa.ingredient",
+ "hash": "tTBD4/E0R0AjLUdJFpsVz3lE/KJUq22Vz0UGqzhEpVs="
+ }
+ }
+ },
+ {
+ "action": "c2pa.color_adjustments",
+ "parameters": {
+ "name": "brightnesscontrast"
+ }
+ }
+ ]
+ }
+ }
+ ],
+ "signature_info": {
+ "alg": "Ps256",
+ "issuer": "C2PA Test Signing Cert",
+ "cert_serial_number": "720724073027128164015125666832722375746636448153",
+ "time": "2023-07-13T18:58:32+00:00"
+ },
+ "label": "contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921"
+ }
+ },
+ "validation_status": [
+ {
+ "code": "assertion.dataHash.mismatch",
+ "url": "self#jumbf=/c2pa/contentauth:urn:uuid:5b639b8e-edc1-4e41-9c8e-c87fb1e36921/c2pa.assertions/c2pa.hash.data",
+ "explanation": "asset hash error, name: jumbf manifest, error: hash verification( Hashes do not match )"
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs
@@ -0,0 +1,39 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+use std::io::Cursor;
+
+use c2pa::{Builder, Result};
+
+mod common;
+use common::{compare_stream_to_known_good, fixtures_path, test_signer};
+
+#[test]
+fn test_builder_ca_jpg() -> Result<()> {
+ let manifest_def = std::fs::read_to_string(fixtures_path("simple_manifest.json"))?;
+ let mut builder = Builder::from_json(&manifest_def)?;
+
+ const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
+ let format = "image/jpeg";
+ let mut source = Cursor::new(TEST_IMAGE);
+
+ let mut dest = Cursor::new(Vec::new());
+ builder.sign(format, &mut source, &mut dest, &test_signer())?;
+
+ // dest.set_position(0);
+ // let path = common::known_good_path("CA_test.json");
+ // let reader = c2pa::Reader::from_stream(format, &mut dest)?;
+ // std::fs::write(path, reader.json())?;
+
+ compare_stream_to_known_good(&mut dest, format, "CA_test.json")
+}
diff --git a/sdk/tests/test_reader.rs b/sdk/tests/test_reader.rs
@@ -0,0 +1,66 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+mod common;
+use c2pa::{Error, Reader, Result};
+use common::{assert_err, compare_to_known_good, fixture_stream};
+
+#[test]
+#[cfg(feature = "file_io")]
+fn test_reader_not_found() -> Result<()> {
+ let result = Reader::from_file("not_found.png");
+ assert_err!(result, Err(Error::IoError(_)));
+ Ok(())
+}
+
+#[test]
+fn test_reader_no_jumbf() -> Result<()> {
+ let (format, mut stream) = fixture_stream("sample1.png")?;
+ let result = Reader::from_stream(&format, &mut stream);
+ assert_err!(result, Err(Error::JumbfNotFound));
+ Ok(())
+}
+
+#[test]
+fn test_reader_ca_jpg() -> Result<()> {
+ let (format, mut stream) = fixture_stream("CA.jpg")?;
+ let reader = Reader::from_stream(&format, &mut stream)?;
+ compare_to_known_good(&reader, "CA.json")
+}
+
+#[test]
+fn test_reader_c_jpg() -> Result<()> {
+ let (format, mut stream) = fixture_stream("C.jpg")?;
+ let reader = Reader::from_stream(&format, &mut stream)?;
+ compare_to_known_good(&reader, "C.json")
+}
+
+#[test]
+fn test_reader_xca_jpg() -> Result<()> {
+ let (format, mut stream) = fixture_stream("XCA.jpg")?;
+ let reader = Reader::from_stream(&format, &mut stream)?;
+ compare_to_known_good(&reader, "XCA.json")
+}
+
+#[test]
+#[ignore]
+/// Generates the known good for the above tests
+/// This is ignored by default
+/// to call use test -- --ignored
+fn write_known_goods() -> Result<()> {
+ let filenames = ["CA.jpg", "C.jpg", "XCA.jpg"];
+ for filename in &filenames {
+ common::write_known_good(filename)?;
+ }
+ Ok(())
+}
diff --git a/sdk/tests/v2_api_integration.rs b/sdk/tests/v2_api_integration.rs
@@ -0,0 +1,154 @@
+// Copyright 2024 Adobe. All rights reserved.
+// This file is licensed to you under the Apache License,
+// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+// or the MIT license (http://opensource.org/licenses/MIT),
+// at your option.
+
+// Unless required by applicable law or agreed to in writing,
+// this software is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
+// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
+// specific language governing permissions and limitations under
+// each license.
+
+/// Complete functional integration test with acquisitions and ingredients.
+// Isolate from wasm by wrapping in module.
+#[cfg(not(target_arch = "wasm32"))] // wasm doesn't support ed25519 yet
+mod integration_v2 {
+
+ use std::io::{Cursor, Seek};
+
+ use anyhow::Result;
+ use c2pa::{Builder, CallbackSigner, Reader, SigningAlg};
+ use serde_json::json;
+
+ const PARENT_JSON: &str = r#"
+ {
+ "title": "Parent Test",
+ "format": "image/jpeg",
+ "relationship": "parentOf"
+ }
+ "#;
+
+ const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
+ const CERTS: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pub");
+ const PRIVATE_KEY: &[u8] = include_bytes!("../tests/fixtures/certs/ed25519.pem");
+
+ fn get_manifest_def(title: &str, format: &str) -> String {
+ json!({
+ "title": title,
+ "format": format,
+ "claim_generator_info": [
+ {
+ "name": "c2pa test",
+ "version": env!("CARGO_PKG_VERSION")
+ }
+ ],
+ "thumbnail": {
+ "format": "image/jpeg",
+ "identifier": "manifest_thumbnail.jpg"
+ },
+ "ingredients": [
+ {
+ "title": "Test",
+ "format": "image/jpeg",
+ "instance_id": "12345",
+ "relationship": "inputTo"
+ }
+ ],
+ "assertions": [
+ {
+ "label": "c2pa.actions",
+ "data": {
+ "actions": [
+ {
+ "action": "c2pa.edited",
+ "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
+ "softwareAgent": "Adobe Firefly 0.1.0"
+ }
+ ]
+ }
+ }
+ ]
+ }).to_string()
+ }
+
+ #[test]
+ fn test_v2_integration() -> Result<()> {
+ let title = "CA.jpg";
+ let format = "image/jpeg";
+ let mut source = Cursor::new(TEST_IMAGE);
+
+ let json = get_manifest_def(title, format);
+
+ // don't try to verify on wasm since it doesn't support ed25519 yet
+
+ let mut builder = Builder::from_json(&json)?;
+ builder.add_ingredient(PARENT_JSON, format, &mut source)?;
+
+ // add a manifest thumbnail ( just reuse the image for now )
+ source.rewind()?;
+ builder.add_resource("manifest_thumbnail.jpg", &mut source)?;
+
+ // write the manifest builder to a zipped stream
+ let mut zipped = Cursor::new(Vec::new());
+ builder.to_archive(&mut zipped)?;
+
+ // write the zipped stream to a file for debugging
+ //let debug_path = format!("{}/../target/test.zip", env!("CARGO_MANIFEST_DIR"));
+ // std::fs::write(debug_path, zipped.get_ref())?;
+
+ // unzip the manifest builder from the zipped stream
+ zipped.rewind()?;
+
+ let mut dest = {
+ let ed_signer = |_context: *const _, data: &[u8]| ed_sign(data, PRIVATE_KEY);
+ let signer = CallbackSigner::new(ed_signer, SigningAlg::Ed25519, CERTS);
+ let mut builder = Builder::from_archive(&mut zipped)?;
+ // sign the ManifestStoreBuilder and write it to the output stream
+ let mut dest = Cursor::new(Vec::new());
+ builder.sign(format, &mut source, &mut dest, &signer)?;
+
+ // read and validate the signed manifest store
+ dest.rewind()?;
+ dest
+ };
+
+ let reader = Reader::from_stream(format, &mut dest)?;
+
+ // extract a thumbnail image from the ManifestStore
+ let mut thumbnail = Cursor::new(Vec::new());
+ if let Some(manifest) = reader.active_manifest() {
+ if let Some(thumbnail_ref) = manifest.thumbnail_ref() {
+ reader.resource_to_stream(&thumbnail_ref.identifier, &mut thumbnail)?;
+ println!(
+ "wrote thumbnail {} of size {}",
+ thumbnail_ref.format,
+ thumbnail.get_ref().len()
+ );
+ }
+ }
+
+ println!("{}", reader.json());
+ assert!(reader.validation_status().is_none());
+ assert_eq!(reader.active_manifest().unwrap().title().unwrap(), title);
+
+ Ok(())
+ }
+
+ fn ed_sign(data: &[u8], private_key: &[u8]) -> c2pa::Result<Vec<u8>> {
+ use ed25519_dalek::{Signature, Signer, SigningKey};
+ use pem::parse;
+
+ // Parse the PEM data to get the private key
+ let pem = parse(private_key).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
+ // For Ed25519, the key is 32 bytes long, so we skip the first 16 bytes of the PEM data
+ let key_bytes = &pem.contents()[16..];
+ let signing_key =
+ SigningKey::try_from(key_bytes).map_err(|e| c2pa::Error::OtherError(Box::new(e)))?;
+ // Sign the data
+ let signature: Signature = signing_key.sign(data);
+
+ Ok(signature.to_bytes().to_vec())
+ }
+}