| // 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/assertion_parser.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "absl/strings/match.h" |
| #include "absl/strings/string_view.h" |
| #include "constants/status_codes.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "proto/assertion.pb.h" |
| #include "testing/cbor_utils.h" |
| #include "testing/jumbf_utils.h" |
| #include "testing/proto_test_utils.h" |
| #include "testing/test_validation_tracker.h" |
| #include "validator/tracker.h" |
| |
| namespace credentio { |
| namespace { |
| |
| using ::cbor::FromJson; |
| using ::credentio::CreateActionsAssertionBoxFromCbor; |
| using ::credentio::CreateAssertionBoxFromCbor; |
| using ::credentio::CreateBmffBasedHashAssertionBoxFromCbor; |
| using ::credentio::CreateDataHashAssertionBoxFromCbor; |
| using ::credentio::StripMultipleInstanceSuffix; |
| using ::credentio_testing::EqualsProto; |
| using ::credentio_testing::ParseTextProtoOrDie; |
| using ::testing::Eq; |
| using ::testing::IsEmpty; |
| using ::testing::Optional; |
| |
| MATCHER_P2(FailureStatusIs, code, explanation, "") { |
| return arg.code() == absl::StrCat(code) && |
| absl::StrContains(arg.explanation(), explanation); |
| } |
| |
| class AssertionParserTest : public ::testing::Test { |
| protected: |
| AssertionParserTest() = default; |
| credentio::AssertionParser parser_; |
| }; |
| |
| TEST_F(AssertionParserTest, ParseActionsAssertion) { |
| 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__1", |
| "alg": "sha256", |
| "hash" : "b64'aGFzaA=='" |
| }, |
| { |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient__2", |
| "alg": "sha256", |
| "hash" : "b64'aGFzaA=='" |
| } |
| ] |
| } |
| }, |
| { |
| "action": "c2pa.filtered", |
| "softwareAgent" : { |
| "name": "Joe's Photo Editor", |
| "version": "2.0", |
| "operating_system": "Windows 10" |
| } |
| }, |
| { |
| "action": "c2pa.cropped", |
| "softwareAgentIndex": 1 |
| } |
| ], |
| "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=='" |
| } |
| }], |
| "metadata": { |
| "reviewRatings": [ |
| { |
| "value": 1, |
| "explanation": "Content bindings did not validate" |
| } |
| ], |
| "reference": { |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.actions__1", |
| "alg": "sha256", |
| "hash": "b64'aGFzaA=='" |
| } |
| }, |
| "softwareAgents": [ |
| { |
| "name": "Joe's Photo Editor", |
| "version": "2.0", |
| "operating_system": "Windows 10" |
| }, |
| { |
| "name": "Bob's Photo Editor", |
| "version": "1.0", |
| "operating_system": "Windows 10" |
| } |
| ] |
| })json"); |
| auto expected = ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.actions.v2" |
| actions { |
| 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__1" |
| algorithm: "sha256" |
| hash: "hash" |
| } |
| ingredients { |
| url: "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient__2" |
| 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", software_agent_index: 1 } |
| 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" } |
| } |
| software_agents { |
| name: "Joe\'s Photo Editor" |
| version: "2.0" |
| operating_system: "Windows 10" |
| } |
| software_agents { |
| name: "Bob\'s Photo Editor" |
| version: "1.0" |
| operating_system: "Windows 10" |
| } |
| } |
| )pb"); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT(parser_.ParseCbor("c2pa.actions.v2", cbor, t.tracker()), |
| Optional(EqualsProto(expected))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseBmffBasedHashAssertion) { |
| credentio::TestValidationTracker t; |
| std::string bmff_based_hash_assertion_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"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.hash.bmff.v2", bmff_based_hash_assertion_cbor, |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.hash.bmff.v2" |
| bmff_based_hash { |
| 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")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseDataHashAssertion) { |
| credentio::TestValidationTracker t; |
| std::string data_hash_assertion_cbor = FromJson(R"json( |
| { |
| "alg": "sha256", |
| "pad": "b64'Zm9v'", |
| "hash": "b64'Zm9v'", |
| "name": "JUMBF manifest", |
| "exclusions": [ |
| { |
| "start": 9960, |
| "length": 4213 |
| }, |
| { |
| "start": 14173, |
| "length": 1000 |
| } |
| ] |
| })json"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.hash.data", data_hash_assertion_cbor, |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.hash.data" |
| data_hash { |
| exclusions { start: 9960 length: 4213 } |
| exclusions { start: 14173 length: 1000 } |
| alg: "sha256" |
| hash: "foo" |
| name: "JUMBF manifest" |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseAiDisclosureAssertion) { |
| credentio::TestValidationTracker t; |
| std::string ai_disclosure_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"] |
| } |
| })json"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.ai-disclosure", ai_disclosure_cbor, t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.ai-disclosure" |
| ai_disclosure { |
| 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" |
| } |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseEnvironmentalSustainabilityAssertion) { |
| credentio::TestValidationTracker t; |
| std::string environmental_sustainability_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"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.environmental-sustainability", |
| environmental_sustainability_cbor, t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.environmental-sustainability" |
| environmental_sustainability { |
| 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")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseSoftBindingAssertion) { |
| credentio::TestValidationTracker t; |
| std::string soft_binding_assertion_cbor = FromJson(R"json( |
| { |
| "alg": "soft-algorithm", |
| "name": "soft-binding-name", |
| "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"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.soft-binding", soft_binding_assertion_cbor, |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.soft-binding" |
| soft_binding { |
| 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")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseInvalidSoftBindingAssertion) { |
| // Soft binding assertion is missing the `alg` field. |
| std::string soft_binding_assertion_cbor = FromJson(R"json( |
| { |
| "name": "soft-binding-name" |
| })json"); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT(parser_.ParseCbor("c2pa.soft-binding", |
| soft_binding_assertion_cbor, t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kAssertionCborInvalid, |
| "c2pa.soft-binding"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseIngredientAssertionV2Unsupported) { |
| credentio::TestValidationTracker t; |
| std::string ingredient_0_assertion_cbor = FromJson(R"json( |
| { |
| "dc:title": "ingredient-title", |
| "dc:format": "ingredient-format", |
| "relationship": "ingredient-relationship", |
| "documentID": "ingredient-document-id", |
| "instanceID": "ingredient-instance-id", |
| "data": { |
| "url": "ingredient-data-url", |
| "alg": "ingredient-data-alg", |
| "hash": "b64'aGFzaA=='", |
| "size": 1234567890 |
| }, |
| "c2pa_manifest": { |
| "url": "ingredient-manifest-url", |
| "alg": "ingredient-manifest-alg", |
| "hash": "b64'aGFzaA=='" |
| }, |
| "thumbnail": { |
| "url": "ingredient-thumbnail-url", |
| "alg": "ingredient-thumbnail-alg", |
| "hash": "b64'aGFzaA=='" |
| }, |
| "validationStatus": [ |
| { |
| "code": "status.1", |
| "url": "url.1", |
| "explanation": "explanation.1" |
| }, |
| { |
| "code": "status.2", |
| "url": "url.2", |
| "explanation": "explanation.2" |
| } |
| ], |
| "description": "ingredient-description", |
| "informational_URI": "ingredient-informational-uri", |
| "metadata": { |
| "reviewRatings": [ |
| { |
| "value": 1, |
| "explanation": "can't dance to it" |
| } |
| ] |
| } |
| })json"); |
| EXPECT_THAT(parser_.ParseCbor("c2pa.ingredient.v2", |
| ingredient_0_assertion_cbor, t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.tracker().GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kGoogleUnsupportedSpecVersion, |
| "c2pa.ingredient.v2"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseIngredientAssertionV3) { |
| credentio::TestValidationTracker t; |
| std::string ingredient_0_assertion_cbor = FromJson(R"json( |
| { |
| "dc:title": "image 1.jpg", |
| "metadata": { |
| "reviewRatings": [ |
| { |
| "value": 5, |
| "explanation": "Content bindings validated" |
| } |
| ] |
| }, |
| "dc:format": "image/jpeg", |
| "thumbnail": { |
| "url": "self#jumbf=c2pa/urn:c2pa:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.thumbnail.ingredient_1.jpg", |
| "hash": "b64'UjRAYWiAq4lfCRDmksWAlDJN/XtHHFFwMWymsZsm3j8='" |
| }, |
| "relationship": "parentOf", |
| "activeManifest": { |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322", |
| "hash": "b64'1kjJTO108b71cL95UxgfHD3eDgk9VrCedW8n3fYTRMk='" |
| }, |
| "claimSignature": { |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature", |
| "hash": "b64'85KAvU3+3YgtIjj6IV0fzKwj8si/85+gevVSK2Iw+S0='" |
| }, |
| "validationResults": { |
| "activeManifest": { |
| "success": [ |
| { |
| "code": "claimSignature.validated", |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| }, |
| { |
| "code": "signingCredential.trusted", |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| }, |
| { |
| "code": "timeStamp.validated", |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| }, |
| { |
| "code": "timeStamp.trusted", |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| }, |
| { |
| "code": "assertion.hashedURI.match", |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3" |
| } |
| ], |
| "informational": [{ |
| "code": "signingCredential.ocsp.skipped", |
| "url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| }], |
| "failure": [] |
| }, |
| "ingredientDeltas": [ |
| { |
| "ingredientAssertionURI": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3", |
| "validationDeltas": { |
| "success": [], |
| "informational": [], |
| "failure": [{ |
| "code": "assertion.hashedURI.mismatch", |
| "url": "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.metadata" |
| }] |
| } |
| }, |
| { |
| "ingredientAssertionURI": "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.ingredient.v3", |
| "validationDeltas": { |
| "success": [], |
| "informational": [], |
| "failure": [{ |
| "code": "signingCredential.untrusted", |
| "url": "self#jumbf=c2pa/urn:c2pa:72C28A7C-7F5B-4301-B373-3183C10AF7C5/c2pa.signature" |
| }] |
| } |
| } |
| ] |
| } |
| })json"); |
| auto result = parser_.ParseCbor("c2pa.ingredient.v3", |
| ingredient_0_assertion_cbor, t.tracker()); |
| ASSERT_THAT(t.GetFailureStatuses(), IsEmpty()); |
| EXPECT_THAT( |
| result, |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.ingredient.v3" |
| ingredient_v3 { |
| title: "image 1.jpg" |
| media_type: "image/jpeg" |
| relationship: "parentOf" |
| active_manifest { |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322" |
| hash: "\326H\311L\355t\361\276\365p\277yS\030\037\034=\336\016\t=V\260\236uo\'\335\366\023D\311" |
| } |
| claim_signature { |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| hash: "\363\222\200\275M\376\335\210-\"8\372!]\037\314\254#\362\310\277\363\237\240z\365R+b0\371-" |
| } |
| thumbnail { |
| url: "self#jumbf=c2pa/urn:c2pa:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.thumbnail.ingredient_1.jpg" |
| hash: "R4@ah\200\253\211_\t\020\346\222\305\200\2242M\375{G\034Qp1l\246\261\233&\336?" |
| } |
| validation_results { |
| active_manifest { |
| successes { |
| code: "claimSignature.validated" |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| } |
| successes { |
| code: "signingCredential.trusted" |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| } |
| successes { |
| code: "timeStamp.validated" |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| } |
| successes { |
| code: "timeStamp.trusted" |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| } |
| successes { |
| code: "assertion.hashedURI.match" |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3" |
| } |
| informationals { |
| code: "signingCredential.ocsp.skipped" |
| url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature" |
| } |
| } |
| ingredient_deltas { |
| ingredient_assertion_uri: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3" |
| validation_deltas { |
| failures { |
| code: "assertion.hashedURI.mismatch" |
| url: "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.metadata" |
| } |
| } |
| } |
| ingredient_deltas { |
| ingredient_assertion_uri: "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.ingredient.v3" |
| validation_deltas { |
| failures { |
| code: "signingCredential.untrusted" |
| url: "self#jumbf=c2pa/urn:c2pa:72C28A7C-7F5B-4301-B373-3183C10AF7C5/c2pa.signature" |
| } |
| } |
| } |
| } |
| } |
| )pb")))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseBoxesHashAssertion) { |
| credentio::TestValidationTracker t; |
| std::string boxes_hash_assertion_cbor = FromJson(R"json( |
| { |
| "alg" : "sha256", |
| "boxes": [ |
| { |
| "names" : ["SOI", "APP0", "APP2"], |
| "hash" : "b64'Zm9v'" |
| }, |
| { |
| "names" : ["C2PA"], |
| "hash" : "b64'YmFy'" |
| } |
| ] |
| })json"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.hash.boxes", boxes_hash_assertion_cbor, |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.hash.boxes" |
| boxes_hash { |
| alg: "sha256" |
| boxes { names: "SOI" names: "APP0" names: "APP2" hash: "foo" } |
| boxes { names: "C2PA" hash: "bar" } |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseThumbnailClaimAssertion) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseThumbnail("c2pa.thumbnail.claim.png", "image/png", "foo", |
| "foo.png", t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.thumbnail.claim.png" |
| claim_thumbnail { |
| media_type: "image/png" |
| data: "foo" |
| file_name: "foo.png" |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseThumbnailIngredientAssertion) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseThumbnail("c2pa.thumbnail.ingredient.png", "image/png", |
| "foo", "foo.png", t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.thumbnail.ingredient.png" |
| ingredient_thumbnail { |
| media_type: "image/png" |
| data: "foo" |
| file_name: "foo.png" |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseThumbnailAssertionUnrecognizedLabelIgnored) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseThumbnail("c2pa.thumbnail.something.png", "image/png", "foo", |
| std::nullopt, t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseMetadataAssertion) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseMetadata("c2pa.metadata", |
| R"json({ |
| "@context": "bar", |
| "foo": "baz" |
| })json", |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.metadata" |
| metadata { |
| value { |
| fields { |
| key: "@context" |
| value { string_value: "bar" } |
| } |
| fields { |
| key: "foo" |
| value { string_value: "baz" } |
| } |
| } |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseMetadataAssertionCustomLabel) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseMetadata("com.litware.metadata", |
| R"json({ |
| "@context": "bar", |
| "foo": "baz" |
| })json", |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "com.litware.metadata" |
| metadata { |
| value { |
| fields { |
| key: "@context" |
| value { string_value: "bar" } |
| } |
| fields { |
| key: "foo" |
| value { string_value: "baz" } |
| } |
| } |
| } |
| )pb")))); |
| EXPECT_THAT(t.tracker().GetFailureStatuses(), IsEmpty()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseMetadataAssertionMissingContext) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT(parser_.ParseMetadata("c2pa.metadata", |
| R"json({ |
| "@something": "bar" |
| })json", |
| t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.tracker().GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kGeneralError, "@context"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseMetadataAssertionInvalidJson) { |
| credentio::TestValidationTracker t; |
| EXPECT_THAT(parser_.ParseMetadata("c2pa.metadata", |
| R"json({ |
| "foo": |
| })json", |
| t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.tracker().GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kAssertionJsonInvalid, |
| "c2pa.metadata"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseRepositoryReceiptAssertion) { |
| credentio::TestValidationTracker t; |
| std::string repository_receipt_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"; |
| EXPECT_THAT( |
| parser_.ParseMetadata("c2pa.repository-receipt", repository_receipt_json, |
| t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.repository-receipt" |
| repository_receipt { |
| 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")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseInvalidActionsAssertion) { |
| // Actions assertion is missing the `action` field. |
| std::string actions_assertion_cbor = FromJson(R"json( |
| { |
| "actions": [ |
| { |
| } |
| ] |
| })json"); |
| auto actions_assertion_box = |
| CreateActionsAssertionBoxFromCbor(actions_assertion_cbor); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kAssertionCborInvalid, |
| "c2pa.actions.v2"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseInvalidBmffBasedHashAssertion) { |
| // `xpath` field is missing in the exclusion. |
| std::string bmff_based_hash_assertion_cbor = FromJson(R"json( |
| { "exclusions": [{}] } |
| )json"); |
| auto bmff_based_hash_assertion_box = |
| CreateBmffBasedHashAssertionBoxFromCbor(bmff_based_hash_assertion_cbor); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT(parser_.ParseCbor("c2pa.hash.bmff.v2", |
| bmff_based_hash_assertion_cbor, t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kAssertionCborInvalid, |
| "c2pa.hash.bmff.v2"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseInvalidDataHashAssertion) { |
| // Data hash assertion is missing the `hash` field. |
| std::string data_hash_assertion_cbor = FromJson(R"json({})json"); |
| auto data_hash_assertion_box = |
| CreateDataHashAssertionBoxFromCbor(data_hash_assertion_cbor); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT(parser_.ParseCbor("c2pa.hash.data", data_hash_assertion_cbor, |
| t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kAssertionCborInvalid, |
| "c2pa.hash.data"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseAssertionWithUnsupportedLabel) { |
| std::string actions_assertion_cbor = FromJson(R"json({})json"); |
| auto actions_assertion_box = |
| CreateAssertionBoxFromCbor("random_label", actions_assertion_cbor); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseCbor("random_label", actions_assertion_cbor, t.tracker()), |
| Eq(std::nullopt)); |
| // Skip recording failures for now. |
| EXPECT_THAT(t.GetFailures(), IsEmpty()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseIngredientAssertionV1Unsupported) { |
| credentio::TestValidationTracker t; |
| std::string ingredient_0_assertion_cbor = FromJson(R"json( |
| { |
| "dc:title": "ingredient-title", |
| "dc:format": "ingredient-format", |
| "documentID": "ingredient-document-id", |
| "instanceID": "ingredient-instance-id", |
| "relationship": "ingredient-relationship", |
| "c2pa_manifest": { |
| "url": "ingredient-manifest-url", |
| "alg": "ingredient-manifest-alg", |
| "hash": "b64'aGFzaA=='" |
| }, |
| "thumbnail": { |
| "url": "ingredient-thumbnail-url", |
| "alg": "ingredient-thumbnail-alg", |
| "hash": "b64'aGFzaA=='" |
| }, |
| "validationStatus": [ |
| { |
| "code": "status.1", |
| "url": "url.1", |
| "explanation": "explanation.1" |
| }, |
| { |
| "code": "status.2", |
| "url": "url.2", |
| "explanation": "explanation.2" |
| } |
| ], |
| "metadata": { |
| "reviewRatings": [ |
| { |
| "value": 1, |
| "explanation": "can't dance to it" |
| } |
| ] |
| } |
| })json"); |
| EXPECT_THAT(parser_.ParseCbor("c2pa.ingredient", ingredient_0_assertion_cbor, |
| t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.tracker().GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kGoogleUnsupportedSpecVersion, |
| "c2pa.ingredient"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseMultiAssetHashAssertion) { |
| credentio::TestValidationTracker t; |
| std::string multi_asset_hash_assertion_cbor = FromJson(R"json( |
| { |
| "parts": [ |
| { |
| "location": { |
| "byteOffset": 0, |
| "length": 100 |
| }, |
| "hashAssertion": { |
| "url": "hash-url-1", |
| "alg": "sha256", |
| "hash": "b64'aGFzaA=='" |
| } |
| }, |
| { |
| "location": { |
| "bmffBox": "test-bmff-box" |
| }, |
| "hashAssertion": { |
| "url": "hash-url-2", |
| "alg": "sha256", |
| "hash": "b64'aGFzaA=='" |
| }, |
| "optional": true |
| } |
| ], |
| "pad": "b64'Zm9v'" |
| })json"); |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.hash.multi-asset", |
| multi_asset_hash_assertion_cbor, t.tracker()), |
| Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb( |
| label: "c2pa.hash.multi-asset" |
| multi_asset_hash { |
| parts { |
| location { byte_offset: 0 length: 100 } |
| hash_assertion { |
| url: "hash-url-1" |
| algorithm: "sha256" |
| hash: "hash" |
| } |
| } |
| parts { |
| location { bmff_box: "test-bmff-box" } |
| hash_assertion { |
| url: "hash-url-2" |
| algorithm: "sha256" |
| hash: "hash" |
| } |
| is_optional: true |
| } |
| pad: "foo" |
| } |
| )pb")))); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST(StripMultipleInstanceSuffixTest, Works) { |
| EXPECT_EQ(StripMultipleInstanceSuffix("c2pa.hash.data"), "c2pa.hash.data"); |
| EXPECT_EQ(StripMultipleInstanceSuffix("c2pa.hash.data__1"), "c2pa.hash.data"); |
| EXPECT_EQ(StripMultipleInstanceSuffix("c2pa.hash.data__13"), |
| "c2pa.hash.data"); |
| EXPECT_EQ(StripMultipleInstanceSuffix(""), ""); |
| } |
| |
| TEST_F(AssertionParserTest, ParseActionsAssertionEmptyRelatedAssertions) { |
| std::string actions_assertion_cbor = FromJson(R"json( |
| { |
| "actions": [ |
| { |
| "action": "c2pa.opened", |
| "parameters": { |
| "relatedAssertions": [] |
| } |
| } |
| ] |
| })json"); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()), |
| Eq(std::nullopt)); |
| EXPECT_THAT(t.GetFailureStatuses(), |
| testing::Contains(FailureStatusIs( |
| credentio::FailureStatusCode::kAssertionCborInvalid, |
| "c2pa.actions.v2"))); |
| } |
| |
| TEST_F(AssertionParserTest, ParseActionsAssertionMissingRelatedAssertions) { |
| std::string actions_assertion_cbor = FromJson(R"json( |
| { |
| "actions": [ |
| { |
| "action": "c2pa.opened", |
| "parameters": { |
| "instanceID": "ed610ae51f604002be3dbf0c589a2f1f" |
| } |
| } |
| ] |
| })json"); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()), |
| testing::Ne(std::nullopt)); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| TEST_F(AssertionParserTest, ParseActionsAssertionNonEmptyRelatedAssertions) { |
| std::string actions_assertion_cbor = FromJson(R"json( |
| { |
| "actions": [ |
| { |
| "action": "c2pa.opened", |
| "parameters": { |
| "relatedAssertions": [ |
| { |
| "url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.other", |
| "alg": "sha256", |
| "hash" : "b64'aGFzaA=='" |
| } |
| ] |
| } |
| } |
| ] |
| })json"); |
| credentio::TestValidationTracker t; |
| EXPECT_THAT( |
| parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()), |
| testing::Ne(std::nullopt)); |
| EXPECT_FALSE(t.tracker().HasFailures()); |
| } |
| |
| } // namespace |
| } // namespace credentio |