| // 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; |
| ASSERT_THAT(credentio::ParseAssertion(cbor, &actual), IsOk()); |
| 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; |
| ASSERT_THAT(credentio::ParseAssertion(cbor, &actual), IsOk()); |
| 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; |
| ASSERT_THAT(credentio::ParseAssertion(json, &actual), IsOk()); |
| EXPECT_THAT(actual, EqualsProto(expected)); |
| } |
| |
| } // namespace |
| } // namespace credentio |