| // Copyright 2026 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // | |
| #include "assertion/parse_assertion.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/strings/string_view.h" | |
| #include "gmock/gmock.h" | |
| #include "google/protobuf/struct.pb.h" | |
| #include "gtest/gtest.h" | |
| #include "nlohmann/json.hpp" | |
| #include "proto/actions_assertion.pb.h" | |
| #include "proto/ai_disclosure_assertion.pb.h" | |
| #include "proto/assertion.pb.h" | |
| #include "proto/asset_reference_assertion.pb.h" | |
| #include "proto/asset_types_assertion.pb.h" | |
| #include "proto/bmff_based_hash_assertion.pb.h" | |
| #include "proto/boxes_hash_assertion.pb.h" | |
| #include "proto/collection_data_hash_assertion.pb.h" | |
| #include "proto/data_hash_assertion.pb.h" | |
| #include "proto/environmental_sustainability_assertion.pb.h" | |
| #include "proto/ingredient_assertion.pb.h" | |
| #include "proto/repository_receipt_assertion.pb.h" | |
| #include "testing/cbor_utils.h" | |
| #include "testing/proto_test_utils.h" | |
| namespace credentio { | |
| namespace { | |
| using Json = ::nlohmann::json; | |
| using ::absl_testing::IsOk; | |
| using ::absl_testing::StatusIs; | |
| using ::cbor::FromJson; | |
| using ::credentio::ParseStruct; | |
| using ::credentio_testing::EqualsProto; | |
| using ::credentio_testing::ParseTextProtoOrDie; | |
| using ::testing::HasSubstr; | |
| TEST(ParseAssertionTest, ActionsAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "allActionsIncluded": true, | |
| "actions": [ | |
| { | |
| "action": "c2pa.created", | |
| "softwareAgent": { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "operating_system": "Windows 10" | |
| }, | |
| "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", | |
| "parameters": { | |
| "instanceID": "ed610ae51f604002be3dbf0c589a2f1f", | |
| "ingredients" : [ | |
| { | |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient.v3__1", | |
| "alg": "sha256", | |
| "hash" : "b64'aGFzaA=='" | |
| }, | |
| { | |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient.v3__2", | |
| "alg": "sha256", | |
| "hash" : "b64'aGFzaA=='" | |
| } | |
| ], | |
| "relatedAssertions": [ | |
| { | |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.other", | |
| "alg": "sha256", | |
| "hash" : "b64'aGFzaA=='" | |
| } | |
| ] | |
| } | |
| }, | |
| { | |
| "action": "c2pa.filtered", | |
| "softwareAgent" : { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "operating_system": "Windows 10" | |
| } | |
| }, | |
| { | |
| "action": "c2pa.cropped" | |
| } | |
| ], | |
| "templates": [{ | |
| "action": "com.joesphoto.filter", | |
| "description": "Magic Filter", | |
| "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic", | |
| "softwareAgent" : { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "schema.org.SoftwareApplication.operatingSystem": "Windows 10" | |
| }, | |
| "icon": { | |
| "url": "http://cdn.examplephotoagency.com/logo.svg", | |
| "hash": "b64'aGFzaA=='" | |
| } | |
| }] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::ActionsAssertion>(R"pb( | |
| all_actions_included: true | |
| actions { | |
| action: "c2pa.created" | |
| digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" | |
| software_agent { | |
| name: "Joe\'s Photo Editor" | |
| version: "2.0" | |
| operating_system: "Windows 10" | |
| } | |
| parameters { | |
| instance_id: "ed610ae51f604002be3dbf0c589a2f1f" | |
| ingredients { | |
| url: "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient.v3__1" | |
| algorithm: "sha256" | |
| hash: "hash" | |
| } | |
| ingredients { | |
| url: "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient.v3__2" | |
| algorithm: "sha256" | |
| hash: "hash" | |
| } | |
| related_assertions { | |
| url: "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.other" | |
| algorithm: "sha256" | |
| hash: "hash" | |
| } | |
| } | |
| } | |
| actions { | |
| action: "c2pa.filtered" | |
| software_agent { | |
| name: "Joe\'s Photo Editor" | |
| version: "2.0" | |
| operating_system: "Windows 10" | |
| } | |
| } | |
| actions { action: "c2pa.cropped" } | |
| templates { | |
| action: "com.joesphoto.filter" | |
| description: "Magic Filter" | |
| digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic" | |
| software_agent { name: "Joe\'s Photo Editor" version: "2.0" } | |
| icon { url: "http://cdn.examplephotoagency.com/logo.svg" hash: "hash" } | |
| } | |
| )pb"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, EmptyRelatedAssertionsFails) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "action": "c2pa.created", | |
| "softwareAgent": { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "operating_system": "Windows 10" | |
| }, | |
| "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia", | |
| "parameters": { | |
| "instanceID": "ed610ae51f604002be3dbf0c589a2f1f", | |
| "ingredients" : [ | |
| { | |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient.v3__1", | |
| "alg": "sha256", | |
| "hash" : "b64'aGFzaA=='" | |
| }, | |
| { | |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient.v3__2", | |
| "alg": "sha256", | |
| "hash" : "b64'aGFzaA=='" | |
| } | |
| ], | |
| "relatedAssertions": [] | |
| } | |
| }, | |
| { | |
| "action": "c2pa.filtered", | |
| "softwareAgent" : { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "operating_system": "Windows 10" | |
| } | |
| }, | |
| { | |
| "action": "c2pa.cropped" | |
| } | |
| ], | |
| "templates": [{ | |
| "action": "com.joesphoto.filter", | |
| "description": "Magic Filter", | |
| "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic", | |
| "softwareAgent" : { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "schema.org.SoftwareApplication.operatingSystem": "Windows 10" | |
| }, | |
| "icon": { | |
| "url": "http://cdn.examplephotoagency.com/logo.svg", | |
| "hash": "b64'aGFzaA=='" | |
| } | |
| }] | |
| })json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Field relatedAssertions cannot be empty"))); | |
| } | |
| TEST(ParseAssertionTest, ActionV1IngredientField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "action": "c2pa.opened", | |
| "parameters": { | |
| "ingredient" : { | |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient.v3", | |
| "alg": "sha256", | |
| "hash" : "b64'aGFzaA=='" | |
| } | |
| } | |
| } | |
| ] | |
| })json"); | |
| // The `ingredient` field in v1 `parameters-map` is exposed in `ingredients`. | |
| auto expected = ParseTextProtoOrDie<credentio::ActionsAssertionV1>(R"pb( | |
| actions { | |
| action: "c2pa.opened" | |
| parameters { | |
| ingredients { | |
| url: "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient.v3" | |
| algorithm: "sha256" | |
| hash: "hash" | |
| } | |
| } | |
| } | |
| )pb"); | |
| credentio::ActionsAssertionV1 proto; | |
| EXPECT_THAT(ParseAssertion(cbor, &proto), IsOk()); | |
| EXPECT_THAT(proto, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionInvalidNotAMap) { | |
| auto cbor = FromJson(R"json([ | |
| { | |
| "action": "c2pa.cropped" | |
| } | |
| ])json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("CBOR parsed result is not a map"))); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionInvalidNoAction) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "softwareAgent" : { | |
| "name": "Joe's Photo Editor", | |
| "version": "2.0", | |
| "operating_system": "Windows 10" | |
| } | |
| } | |
| ] | |
| })json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: action"))); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionInvalidActionNotAString) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "action": 123 | |
| } | |
| ] | |
| })json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: action"))); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionSkipsInvalidDigitalSourceType) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "action": "c2pa.created", | |
| "digitalSourceType": 123 | |
| } | |
| ] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::ActionsAssertion>(R"pb( | |
| actions { action: "c2pa.created" } | |
| )pb"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionInvalidActionArrayContainsNonMap) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "action": "c2pa.created", | |
| "digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia" | |
| }, | |
| 123 | |
| ] | |
| })json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("CBOR item is not a map"))); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionInvalidMalformedCbor) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": [ | |
| { | |
| "action": "c2pa.created" | |
| } | |
| ] | |
| })json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor.substr(0, 10), &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("CBOR parsing failed"))); | |
| } | |
| TEST(ParseAssertionTest, ActionsAssertionSkipsInvalidNotArrayActions) { | |
| // Invalid `actions`, as it should be an array, not a map. | |
| auto cbor = FromJson(R"json( | |
| { | |
| "actions": | |
| { | |
| "action": "c2pa.created" | |
| } | |
| })json"); | |
| credentio::ActionsAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: actions"))); | |
| } | |
| TEST(ParseAssertionTest, IngredientAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "dc:title": "ingredient-title", | |
| "dc:format": "ingredient-format", | |
| "relationship": "ingredient-relationship", | |
| "instanceID": "ingredient-instance-id", | |
| "data": { | |
| "url": "ingredient-data-url", | |
| "alg": "ingredient-data-alg", | |
| "hash": "b64'aGFzaA=='", | |
| "size": 1234567890 | |
| }, | |
| "activeManifest": { | |
| "url": "ingredient-manifest-url", | |
| "alg": "ingredient-manifest-alg", | |
| "hash": "b64'aGFzaA=='" | |
| }, | |
| "thumbnail": { | |
| "url": "ingredient-thumbnail-url", | |
| "alg": "ingredient-thumbnail-alg", | |
| "hash": "b64'aGFzaA=='" | |
| }, | |
| "validationResults": { | |
| "activeManifest": { | |
| "success": [ | |
| { | |
| "code": "status.1", | |
| "url": "url.1", | |
| "explanation": "explanation.1" | |
| }, | |
| { | |
| "code": "status.2", | |
| "url": "url.2", | |
| "explanation": "explanation.2" | |
| } | |
| ], | |
| "failure": [], | |
| "informational": [] | |
| } | |
| }, | |
| "description": "ingredient-description", | |
| "informationalURI": "ingredient-informational-uri" | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::IngredientAssertionV3>(R"pb( | |
| title: "ingredient-title" | |
| media_type: "ingredient-format" | |
| relationship: "ingredient-relationship" | |
| instance_id: "ingredient-instance-id" | |
| data { | |
| url: "ingredient-data-url" | |
| algorithm: "ingredient-data-alg" | |
| hash: "hash" | |
| size: 1234567890 | |
| } | |
| active_manifest { | |
| url: "ingredient-manifest-url" | |
| algorithm: "ingredient-manifest-alg" | |
| hash: "hash" | |
| } | |
| thumbnail { | |
| url: "ingredient-thumbnail-url" | |
| algorithm: "ingredient-thumbnail-alg" | |
| hash: "hash" | |
| } | |
| validation_results { | |
| active_manifest { | |
| successes: { | |
| code: "status.1" | |
| url: "url.1" | |
| explanation: "explanation.1" | |
| } | |
| successes: { | |
| code: "status.2" | |
| url: "url.2" | |
| explanation: "explanation.2" | |
| } | |
| } | |
| } | |
| description: "ingredient-description" | |
| informational_uri: "ingredient-informational-uri" | |
| )pb"); | |
| credentio::IngredientAssertionV3 result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, AssetReferenceAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "references": [ | |
| { | |
| "description": "A copy of the asset on the web", | |
| "reference": { | |
| "uri": "https://some.storage.us/foo" | |
| } | |
| }, | |
| { | |
| "description": "A copy of the asset on IPFS", | |
| "reference": { | |
| "uri": "ipfs://cid" | |
| } | |
| } | |
| ] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::AssetReferenceAssertion>(R"pb( | |
| references { | |
| description: "A copy of the asset on the web" | |
| reference { uri: "https://some.storage.us/foo" } | |
| } | |
| references { | |
| description: "A copy of the asset on IPFS" | |
| reference { uri: "ipfs://cid" } | |
| } | |
| )pb"); | |
| credentio::AssetReferenceAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, AssetReferenceAssertionInvalidMissingRequiredField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "references": [ | |
| { | |
| "reference": { | |
| } | |
| } | |
| ] | |
| })json"); | |
| credentio::AssetReferenceAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: uri"))); | |
| } | |
| TEST(ParseAssertionTest, AssetTypesAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "dc:format": "image/jpeg", | |
| "types": | |
| [ | |
| { | |
| "type": "c2pa.types.model.tensorflow", | |
| "version": "2.11.0" | |
| }, | |
| { | |
| "type": "c2pa.types.savedmodel", | |
| "version": "2.11.0" | |
| } | |
| ] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::AssetTypesAssertion>(R"pb( | |
| media_type: "image/jpeg" | |
| types { type: "c2pa.types.model.tensorflow" version: "2.11.0" } | |
| types { type: "c2pa.types.savedmodel" version: "2.11.0" } | |
| )pb"); | |
| credentio::AssetTypesAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, AssetTypesAssertionInvalidMissingRequiredField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "types": | |
| [ | |
| { | |
| "version": "2.11.0" | |
| } | |
| ] | |
| })json"); | |
| credentio::AssetTypesAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: type"))); | |
| } | |
| TEST(ParseAssertionTest, DataHashAssertionOkay) { | |
| // The `pad` and `pad2` fields are omitted by the parser. | |
| auto cbor = FromJson(R"json( | |
| { | |
| "alg": "sha256", | |
| "pad": "b64'Zm9v'", | |
| "pad2": "b64'Zm9v'", | |
| "hash": "b64'Zm9v'", | |
| "name": "JUMBF manifest", | |
| "exclusions": [ | |
| { | |
| "start": 9960, | |
| "length": 4213 | |
| }, | |
| { | |
| "start": 14173, | |
| "length": 1000 | |
| } | |
| ] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::DataHashAssertion>(R"pb( | |
| alg: "sha256" | |
| hash: "foo" | |
| name: "JUMBF manifest" | |
| exclusions { start: 9960 length: 4213 } | |
| exclusions { start: 14173 length: 1000 } | |
| )pb"); | |
| credentio::DataHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, DataHashAssertionInvalidMissingRequiredField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "alg": "sha256" | |
| })json"); | |
| credentio::DataHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: hash"))); | |
| } | |
| TEST(ParseAssertionTest, BmffBasedHashAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "hash": "b64'Zm9v'", | |
| "name": "Example `c2pa.hash.bmff.v2` assertion", | |
| "exclusions": [ | |
| { | |
| "data": [{ "value": "b64'Zm9v'", "offset": 8 }], | |
| "xpath": "/uuid" | |
| }, | |
| { "xpath": "/ftyp" }, | |
| { "xpath": "/mfra" }, | |
| { "xpath": "/moov[1]/pssh" }, | |
| { | |
| "data": [{ "value": "b64'Zm9v'", "offset": 20 }], | |
| "xpath": "/emsg" | |
| } | |
| ] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::BmffBasedHashAssertion>(R"pb( | |
| hash: "foo" | |
| name: "Example `c2pa.hash.bmff.v2` assertion" | |
| exclusions { | |
| data { value: "foo" offset: 8 } | |
| xpath: "/uuid" | |
| } | |
| exclusions { xpath: "/ftyp" } | |
| exclusions { xpath: "/mfra" } | |
| exclusions { xpath: "/moov[1]/pssh" } | |
| exclusions { | |
| data { value: "foo" offset: 20 } | |
| xpath: "/emsg" | |
| } | |
| )pb"); | |
| credentio::BmffBasedHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, BmffBasedHashAssertionInvalidMissingRequiredField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "name": "Example `c2pa.hash.bmff.v2` assertion", | |
| "exclusions": [ | |
| { | |
| "data": [ | |
| { | |
| "value": "2P7D1hsOSDySl1goh37EgQ==", | |
| "offset": 8 | |
| } | |
| ] | |
| } | |
| ] | |
| })json"); | |
| credentio::BmffBasedHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: xpath"))); | |
| } | |
| TEST(ParseAssertionTest, BoxesHashAssertionOkay) { | |
| auto cbor = FromJson(R"json({ | |
| "alg" : "sha256", | |
| "boxes": [ | |
| { | |
| "names" : ["SOI", "APP0", "APP2"], | |
| "hash" : "b64'Zm9v'" | |
| }, | |
| { | |
| "names" : ["C2PA"], | |
| "hash" : "b64'YmFy'" | |
| } | |
| ] | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::BoxesHashAssertion>(R"pb( | |
| alg: "sha256" | |
| boxes { names: "SOI" names: "APP0" names: "APP2" hash: "foo" } | |
| boxes { names: "C2PA" hash: "bar" } | |
| )pb"); | |
| credentio::BoxesHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, BoxesHashAssertionInvalidMissingRequiredField) { | |
| auto cbor = FromJson(R"json({ | |
| "boxes": [ | |
| { | |
| "names" : ["SOI", "APP0", "APP2"] | |
| } | |
| ] | |
| })json"); | |
| credentio::BoxesHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: hash"))); | |
| } | |
| TEST(ParseAssertionTest, CollectionDataHashAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "alg": "sha256", | |
| "uris": [ | |
| { | |
| "uri": "foo1.txt", | |
| "hash": "b64'Zm9v'", | |
| "size": 1234, | |
| "dc:format": "text/plain", | |
| "data_types": [ | |
| { | |
| "type": "c2pa.types.model.tensorflow", | |
| "version": "2.11.0" | |
| } | |
| ] | |
| }, | |
| { | |
| "uri": "foo2.jpg", | |
| "hash": "b64'YmFy'", | |
| "dc:format": "image/jpeg" | |
| } | |
| ], | |
| "zip_central_directory_hash": "b64'emlwSGFzaA=='" | |
| })json"); | |
| auto expected = | |
| ParseTextProtoOrDie<credentio::CollectionDataHashAssertion>(R"pb( | |
| alg: "sha256" | |
| uris { | |
| uri: "foo1.txt" | |
| hash: "foo" | |
| size: 1234 | |
| type: "text/plain" | |
| data_types { type: "c2pa.types.model.tensorflow" version: "2.11.0" } | |
| } | |
| uris { uri: "foo2.jpg" hash: "bar" type: "image/jpeg" } | |
| zip_central_directory_hash: "zipHash" | |
| )pb"); | |
| credentio::CollectionDataHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, | |
| CollectionDataHashAssertionInvalidMissingRequiredField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "uris": [ | |
| { | |
| "uri": "foo1.txt", | |
| "hash": "b64'Zm9v'" | |
| } | |
| ] | |
| })json"); | |
| credentio::CollectionDataHashAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: alg"))); | |
| } | |
| TEST(ParseAssertionTest, SoftBindingAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "alg": "soft-algorithm", | |
| "name": "soft-binding-name", | |
| "pad": "b64'Zm9v'", | |
| "pad2": "b64'Zm9v'", | |
| "alg-params": "b64'cGFyYW1z'", | |
| "blocks": [ | |
| { | |
| "scope": { | |
| "extent": "b64'ZXh0ZW50'", | |
| "timespan": { | |
| "start": 100, | |
| "end": 200 | |
| } | |
| }, | |
| "value": "b64'dmFsdWU='" | |
| } | |
| ], | |
| "bindingMetadata": { | |
| "description": "metadata-desc", | |
| "contact": "metadata-contact", | |
| "informationalUrl": "metadata-url" | |
| } | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::SoftBindingAssertion>(R"pb( | |
| algorithm: "soft-algorithm" | |
| name: "soft-binding-name" | |
| alg_params: "params" | |
| blocks { | |
| scope { | |
| extent: "extent" | |
| timespan { start: 100 end: 200 } | |
| } | |
| value: "value" | |
| } | |
| binding_metadata { | |
| description: "metadata-desc" | |
| contact: "metadata-contact" | |
| informational_url: "metadata-url" | |
| } | |
| )pb"); | |
| credentio::SoftBindingAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, SoftBindingAssertionMissingRequiredField) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "name": "soft-binding-name" | |
| })json"); | |
| credentio::SoftBindingAssertion result; | |
| EXPECT_THAT(ParseAssertion(cbor, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Missing required field: alg"))); | |
| } | |
| TEST(ParseAssertionTest, ParseStructOkay) { | |
| auto json = R"json({ | |
| "string": "bar", | |
| "int": 123, | |
| "float": 123.456, | |
| "bool": true, | |
| "null": null, | |
| "array": [1, 2, 3], | |
| "object": { | |
| "nested": "string" | |
| } | |
| })json"; | |
| auto expected = ParseTextProtoOrDie<google::protobuf::Struct>(R"pb( | |
| fields { | |
| key: "string" | |
| value { string_value: "bar" } | |
| } | |
| fields { | |
| key: "int" | |
| value { number_value: 123 } | |
| } | |
| fields { | |
| key: "float" | |
| value { number_value: 123.456 } | |
| } | |
| fields { | |
| key: "bool" | |
| value { bool_value: true } | |
| } | |
| fields { | |
| key: "null" | |
| value { null_value: NULL_VALUE } | |
| } | |
| fields { | |
| key: "array" | |
| value { | |
| list_value { | |
| values { number_value: 1 } | |
| values { number_value: 2 } | |
| values { number_value: 3 } | |
| } | |
| } | |
| } | |
| fields { | |
| key: "object" | |
| value { | |
| struct_value { | |
| fields { | |
| key: "nested" | |
| value { string_value: "string" } | |
| } | |
| } | |
| } | |
| } | |
| )pb"); | |
| google::protobuf::Struct result; | |
| EXPECT_THAT(ParseStruct(json, &result), IsOk()); | |
| EXPECT_THAT(result, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, ParseStructInvalidJson) { | |
| auto json = R"json({ | |
| "failed | |
| })json"; | |
| google::protobuf::Struct result; | |
| EXPECT_THAT(ParseStruct(json, &result), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("Invalid JSON: could not parse"))); | |
| } | |
| TEST(ParseAssertionTest, AiDisclosureAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "modelType": "c2pa.types.model.onnx", | |
| "modelName": "My Awesome AI Model", | |
| "modelIdentifier": "https://example.com/models/awesome-ai-v1", | |
| "contentProfile": { | |
| "humanOversightLevel": "prompt_guided", | |
| "scientificDomain": ["cs.AI", "cs.CV"] | |
| } | |
| })json"); | |
| auto expected = ParseTextProtoOrDie<credentio::AiDisclosureAssertion>(R"pb( | |
| model_type: "c2pa.types.model.onnx" | |
| model_name: "My Awesome AI Model" | |
| model_identifier: "https://example.com/models/awesome-ai-v1" | |
| content_profile { | |
| human_oversight_level: "prompt_guided" | |
| scientific_domain: "cs.AI" | |
| scientific_domain: "cs.CV" | |
| } | |
| )pb"); | |
| credentio::AiDisclosureAssertion actual; | |
| ABSL_ASSERT_OK(credentio::ParseAssertion(cbor, &actual)); | |
| EXPECT_THAT(actual, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, EnvironmentalSustainabilityAssertionOkay) { | |
| auto cbor = FromJson(R"json( | |
| { | |
| "energy_kwh": { | |
| "value": 1.23, | |
| "measurementMethod": "com.example.direct-measurement" | |
| }, | |
| "carbon_kgco2e": { | |
| "value": 0.456 | |
| }, | |
| "water_litres": { | |
| "value": 7.89, | |
| "measurementMethod": "com.example.estimation" | |
| } | |
| })json"); | |
| auto expected = | |
| ParseTextProtoOrDie<credentio::EnvironmentalSustainabilityAssertion>(R"pb( | |
| energy_kwh { | |
| value: 1.23 | |
| measurement_method: "com.example.direct-measurement" | |
| } | |
| carbon_kgco2e { value: 0.456 } | |
| water_litres { | |
| value: 7.89 | |
| measurement_method: "com.example.estimation" | |
| } | |
| )pb"); | |
| credentio::EnvironmentalSustainabilityAssertion actual; | |
| ABSL_ASSERT_OK(credentio::ParseAssertion(cbor, &actual)); | |
| EXPECT_THAT(actual, EqualsProto(expected)); | |
| } | |
| TEST(ParseAssertionTest, RepositoryReceiptAssertionOkay) { | |
| auto json = R"json( | |
| { | |
| "repository": { | |
| "uri": "https://repository.example.com/manifests/xyz123", | |
| "manifestId": "urn:uuid:12345678-1234-5678-1234-567812345678" | |
| }, | |
| "anchor": { | |
| "uri": "https://anchor.example.com/receipts/9876", | |
| "parameters": { | |
| "version": "1.0" | |
| }, | |
| "proof": { | |
| "signature": "c2lnbmF0dXJl" | |
| } | |
| } | |
| })json"; | |
| auto expected = | |
| ParseTextProtoOrDie<credentio::RepositoryReceiptAssertion>(R"pb( | |
| repository { | |
| uri: "https://repository.example.com/manifests/xyz123" | |
| manifest_id: "urn:uuid:12345678-1234-5678-1234-567812345678" | |
| } | |
| anchor { | |
| uri: "https://anchor.example.com/receipts/9876" | |
| parameters { | |
| fields { | |
| key: "version" | |
| value { string_value: "1.0" } | |
| } | |
| } | |
| proof { | |
| fields { | |
| key: "signature" | |
| value { string_value: "c2lnbmF0dXJl" } | |
| } | |
| } | |
| } | |
| )pb"); | |
| credentio::RepositoryReceiptAssertion actual; | |
| ABSL_ASSERT_OK(credentio::ParseAssertion(json, &actual)); | |
| EXPECT_THAT(actual, EqualsProto(expected)); | |
| } | |
| } // namespace | |
| } // namespace credentio |