| // 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 |