| // 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 "validator/graph.h" | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include "absl/container/flat_hash_set.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" // IWYU pragma: keep | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/escaping.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/substitute.h" | |
| #include "absl/types/span.h" | |
| #include "assertion/validator.h" | |
| #include "constants/status_codes.h" | |
| #include "crypto/algorithms.h" | |
| #include "crypto/default/hasher.h" | |
| #include "crypto/hash.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "jumbf/box.h" | |
| #include "jumbf/uri.h" | |
| #include "proto/assertion.pb.h" | |
| #include "proto/data_hash_assertion.pb.h" | |
| #include "proto/hashed_uri.pb.h" | |
| #include "proto/ingredient_assertion.pb.h" | |
| #include "proto/manifest.pb.h" | |
| #include "proto/validation_result.pb.h" | |
| #include "testing/fake_assertion_validator.h" | |
| #include "testing/fake_claim_validator.h" | |
| #include "testing/jumbf_utils.h" | |
| #include "testing/mock_assertion_validator.h" | |
| #include "testing/proto_test_utils.h" | |
| #include "validator/tracker.h" | |
| #include "validator/validator_options.h" | |
| namespace credentio { | |
| namespace { | |
| using ::credentio_testing::EqualsProto; | |
| using ::credentio_testing::ParseTextProtoOrDie; | |
| using ::testing::_; | |
| using ::testing::Contains; | |
| using ::testing::IsEmpty; | |
| using ::testing::Property; | |
| using ::testing::SizeIs; | |
| using ::testing::UnorderedElementsAre; | |
| class NoopHashChecker : public HashChecker { | |
| public: | |
| void Update(absl::string_view data) override {} | |
| bool Check(absl::string_view hash) override { return true; } | |
| private: | |
| absl::string_view hash_value_; | |
| }; | |
| class NoopHashCheckerFactory : public HashCheckerFactory { | |
| public: | |
| absl::StatusOr<std::unique_ptr<HashChecker>> Create( | |
| HashAlgorithm algorithm) const override { | |
| return std::make_unique<NoopHashChecker>(); | |
| } | |
| }; | |
| Claim DefaultClaim() { | |
| Claim claim; | |
| claim.set_default_algorithm("sha256"); | |
| return claim; | |
| } | |
| class ManifestGraphTest : public ::testing::Test { | |
| protected: | |
| ManifestGraphTest() | |
| : claim_signature_box_bytes_( | |
| EncodeClaimSignatureBox("claim-signature-cbor")), | |
| claim_signature_box_(ParseSuperBoxOrDie(&claim_signature_box_bytes_)), | |
| empty_data_hash_assertion_(ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.data' data_hash {}")) {} | |
| std::string claim_signature_box_bytes_; | |
| jumbf::SuperBox claim_signature_box_; | |
| NoopHashCheckerFactory noop_hash_checker_factory_; | |
| ValidatorOptions options_; | |
| Assertion empty_data_hash_assertion_; | |
| }; | |
| TEST_F(ManifestGraphTest, ClaimValidationFails) { | |
| FakeClaimValidator claim_validator({FailureStatusCode::kGeneralError}, | |
| /*informationals=*/{}, /*successes=*/{}); | |
| auto assertion_validator = std::make_unique<AssertionValidator>(); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = CreateStandardManifest("urn:c2pa:1", {}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, | |
| assertion_validator.get(), &claim_validator, | |
| &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, | |
| EqualsProto(ParseTextProtoOrDie<PartialValidationResultProto>( | |
| R"pb(active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| failures { | |
| code: "general.error" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, StandardManifestAccepted) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {})}; | |
| assertion_validator.SetOutputAssertions("/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, PopulatesMultiAssetHashUri) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {})}; | |
| Assertion multi_asset_hash_assertion = ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.multi-asset' multi_asset_hash {}"); | |
| Assertion part0_hash_assertion = ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.data.part' data_hash {}"); | |
| Assertion part1_hash_assertion = ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.data.part__1' data_hash {}"); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, multi_asset_hash_assertion, | |
| part0_hash_assertion, part1_hash_assertion}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.hash.multi-asset" | |
| multi_asset_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.hash.data.part" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.hash.data.part__1" | |
| data_hash {} | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| multi_asset_hash_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.multi-asset" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, PopulatesHardBindingAndMultiAssetHashUris) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {})}; | |
| Assertion part1_data_hash_assertion = ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.data.part' data_hash {}"); | |
| Assertion part2_boxes_hash_assertion = ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.boxes.part' boxes_hash {}"); | |
| Assertion multi_asset_hash_assertion = ParseTextProtoOrDie<Assertion>( | |
| "label: 'c2pa.hash.multi-asset' multi_asset_hash {}"); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {part1_data_hash_assertion, part2_boxes_hash_assertion, | |
| multi_asset_hash_assertion, empty_data_hash_assertion_}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data.part" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.hash.boxes.part" | |
| boxes_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.hash.multi-asset" | |
| multi_asset_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| multi_asset_hash_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.multi-asset" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientStandardManifestAccepted) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:2", {}), | |
| }; | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:1" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[1], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| // claimSignature.validated contains the "wrong" URL as it's added by the | |
| // FakeClaimValidator. | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:2" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2" | |
| } | |
| successes { | |
| code: "ingredient.claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/c2pa.ingredient.v3" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:1" } | |
| claim_signature { url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" } | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, UpdateManifestAccepted) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| auto standard_manifest = | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}); | |
| auto update_manifest = CreateUpdateManifest("urn:c2pa:2", {}); | |
| assertion_validator.SetOutputAssertions("/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| relationship: "parentOf" | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:1" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| } | |
| })pb")}); | |
| std::vector<jumbf::SuperBox> manifests = {standard_manifest, update_manifest}; | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[1], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:2" | |
| is_update_manifest: true | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| relationship: "parentOf" | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:1" } | |
| claim_signature { url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" } | |
| } | |
| } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2" | |
| } | |
| successes { | |
| code: "ingredient.claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/c2pa.ingredient.v3" | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:1" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, | |
| StandardManifestWithIngredientUpdateManifestAccepted) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:IngredientStandard", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:IngredientUpdate", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:ActiveStandard", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| auto standard_manifest = CreateStandardManifest("urn:c2pa:IngredientStandard", | |
| {claim_signature_box_}); | |
| auto update_manifest = | |
| CreateUpdateManifest("urn:c2pa:IngredientUpdate", {claim_signature_box_}); | |
| auto active_manifest = CreateStandardManifest("urn:c2pa:ActiveStandard", {}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:IngredientUpdate", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| relationship: "parentOf" | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientStandard" | |
| } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientStandard/c2pa.signature" | |
| } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:ActiveStandard", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| relationship: "parentOf" | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientUpdate" | |
| } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientUpdate/c2pa.signature" | |
| } | |
| })pb")}); | |
| std::vector<jumbf::SuperBox> manifests = {standard_manifest, update_manifest, | |
| active_manifest}; | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[2], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:ActiveStandard" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:ActiveStandard" | |
| } | |
| successes { | |
| code: "ingredient.claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:ActiveStandard/c2pa.assertions/c2pa.ingredient.v3" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| relationship: "parentOf" | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:IngredientUpdate" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientUpdate/c2pa.signature" | |
| } | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:IngredientUpdate" | |
| is_update_manifest: true | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| relationship: "parentOf" | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientStandard" | |
| } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientStandard/c2pa.signature" | |
| } | |
| } | |
| } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientUpdate" | |
| } | |
| successes { | |
| code: "ingredient.claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientUpdate/c2pa.assertions/c2pa.ingredient.v3" | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:IngredientStandard" | |
| claim { default_algorithm: "sha256" } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:IngredientStandard" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:ActiveStandard/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestV3UrlIsInvalid) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = CreateStandardManifest("urn:c2pa:1", {}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(label: "ingredient_v3" | |
| ingredient_v3 { | |
| active_manifest { url: "invalid_url" } | |
| })pb")}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "ingredient_v3" | |
| ingredient_v3 { active_manifest { url: "invalid_url" } } | |
| } | |
| validation { | |
| failures { | |
| code: "ingredient.manifest.missing" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/ingredient_v3" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3ClaimSignatureHashMatch) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:2", {}), | |
| }; | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>(absl::Substitute( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:1" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| hash: "$0" | |
| algorithm: "sha256" | |
| } | |
| })pb", | |
| absl::CEscape(GetHash("sha256", claim_signature_box_))))}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[1], &uri_resolver, &assertion_validator, | |
| &claim_validator, &DefaultHashCheckerFactory(), | |
| &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT((*result)->active_manifest().validation().failures(), IsEmpty()); | |
| EXPECT_THAT( | |
| (*result)->active_manifest().validation().successes(), | |
| Contains(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb( | |
| code: "ingredient.claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/c2pa.ingredient.v3" | |
| )pb")))); | |
| ASSERT_THAT((*result)->ingredient_manifests(), SizeIs(1)); | |
| EXPECT_THAT((*result)->ingredient_manifests(0).validation().failures(), | |
| IsEmpty()); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3ClaimSignatureHashMismatch) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:2", {}), | |
| }; | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(label: "ingredient_v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:1" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| hash: "wrong hash" | |
| algorithm: "sha256" | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[1], &uri_resolver, &assertion_validator, | |
| &claim_validator, &DefaultHashCheckerFactory(), | |
| &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT((*result)->active_manifest().validation().failures(), | |
| Contains(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb( | |
| code: "ingredient.claimSignature.mismatch" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/ingredient_v3" | |
| )pb")))); | |
| EXPECT_THAT((*result)->ingredient_manifests(), IsEmpty()); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestHashMatch) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:2", {}), | |
| }; | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>(absl::Substitute( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| hash: "$0" | |
| algorithm: "sha256" | |
| } | |
| })pb", | |
| absl::CEscape(GetHash("sha256", manifests[0]))))}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[1], &uri_resolver, &assertion_validator, | |
| &claim_validator, &DefaultHashCheckerFactory(), | |
| &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT((*result)->active_manifest().validation().failures(), IsEmpty()); | |
| EXPECT_THAT( | |
| (*result)->active_manifest().validation().successes(), | |
| Contains(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb( | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/c2pa.ingredient.v3" | |
| )pb")))); | |
| ASSERT_THAT((*result)->ingredient_manifests(), SizeIs(1)); | |
| EXPECT_THAT((*result)->ingredient_manifests(0).validation().failures(), | |
| IsEmpty()); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestHashMismatch) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:2", {}), | |
| }; | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| hash: "wrong hash" | |
| algorithm: "sha256" | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[1], &uri_resolver, &assertion_validator, | |
| &claim_validator, &DefaultHashCheckerFactory(), | |
| &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT( | |
| (*result)->active_manifest().validation().failures(), | |
| Contains(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb( | |
| code: "ingredient.manifest.mismatch" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/c2pa.ingredient.v3" | |
| )pb")))); | |
| EXPECT_THAT((*result)->ingredient_manifests(), IsEmpty()); | |
| } | |
| TEST_F(ManifestGraphTest, ManifestLegacyLabel) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| claim_validator.SetClaimResult("urn:uuid:some_label", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box, active_manifest; | |
| active_manifest.description.label = "urn:uuid:some_label"; | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:uuid:some_label", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(ingredient_v3 { | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:uuid:some_label" | |
| } | |
| })pb")}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT( | |
| **result, | |
| EqualsProto(ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:uuid:some_label" | |
| validation { | |
| failures { | |
| code: "com.google.unsupportedSpecVersion" | |
| url: "self#jumbf=/c2pa/urn:uuid:some_label" | |
| explanation: "deprecated manifest label format" | |
| } | |
| } | |
| } | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestUrlValidButManifestMissing) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| claim_validator.SetClaimResult("urn:c2pa:some_label", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = | |
| CreateStandardManifest("urn:c2pa:some_label", {}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:some_label", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(label: "ingredient_v3" | |
| ingredient_v3 { | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:some_label" | |
| } | |
| })pb")}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:some_label" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "ingredient_v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:some_label" } | |
| } | |
| } | |
| validation { | |
| failures { | |
| code: "ingredient.manifest.missing" | |
| url: "self#jumbf=/c2pa/urn:c2pa:some_label/c2pa.assertions/ingredient_v3" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:some_label/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestV3SelfRedacted) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| Claim claim = DefaultClaim(); | |
| claim.add_redacted_assertions("self#jumbf=self_redacted"); | |
| claim_validator.SetClaimResult("urn:c2pa:1", claim); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = CreateStandardManifest("urn:c2pa:1", {}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT( | |
| **result, | |
| EqualsProto(ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { | |
| redacted_assertions: "self#jumbf=self_redacted" | |
| default_algorithm: "sha256" | |
| } | |
| validation { | |
| failures { | |
| code: "assertion.selfRedacted" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestV3InvalidRedactedAssertionUri) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| Claim claim = DefaultClaim(); | |
| claim.add_redacted_assertions("invalid_uri"); | |
| claim_validator.SetClaimResult("urn:c2pa:1", claim); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = CreateStandardManifest("urn:c2pa:1", {}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT( | |
| **result, | |
| EqualsProto(ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { | |
| redacted_assertions: "invalid_uri" | |
| default_algorithm: "sha256" | |
| } | |
| validation { | |
| failures { | |
| code: "assertion.missing" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientManifestV3InvalidRedactedAssertionPath) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| Claim claim = DefaultClaim(); | |
| claim.add_redacted_assertions("self#jumbf=/invalid_path"); | |
| claim_validator.SetClaimResult("urn:c2pa:1", claim); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = CreateStandardManifest("urn:c2pa:1", {}); | |
| auto uri_resolver = jumbf::UriResolver::WithSingleRootChild(&root_box); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT( | |
| **result, | |
| EqualsProto(ParseTextProtoOrDie<PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { | |
| redacted_assertions: "self#jumbf=/invalid_path" | |
| default_algorithm: "sha256" | |
| } | |
| validation { | |
| failures { | |
| code: "assertion.missing" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, RedactedAssertionsProvidedToAssertionValidator) { | |
| // Manifest graph: | |
| // urn:c2pa:4 (active) has ingredients urn:c2pa:2, urn:c2pa:3 | |
| // unn:c2pa:2 has ingredient urn:c2pa:1 | |
| // urn:c2pa:3 has ingredient urn:c2pa:1 | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| Claim claim1 = DefaultClaim(); | |
| Claim claim2 = ParseTextProtoOrDie<Claim>(R"pb( | |
| default_algorithm: "sha256" | |
| redacted_assertions: "self#jumbf=/c2pa/urn:c2pa:1/claim1_assertion_redacted_by_claim2" | |
| )pb"); | |
| Claim claim3 = ParseTextProtoOrDie<Claim>(R"pb( | |
| default_algorithm: "sha256" | |
| redacted_assertions: "self#jumbf=/c2pa/urn:c2pa:1/claim1_assertion_redacted_by_claim3" | |
| )pb"); | |
| Claim claim4 = ParseTextProtoOrDie<Claim>(R"pb( | |
| default_algorithm: "sha256" | |
| redacted_assertions: "self#jumbf=/c2pa/urn:c2pa:1/claim1_assertion_redacted_by_claim4" | |
| redacted_assertions: "self#jumbf=/c2pa/urn:c2pa:2/claim2_assertion_redacted_by_claim4" | |
| redacted_assertions: "self#jumbf=/c2pa/urn:c2pa:3/claim3_assertion_redacted_by_claim4" | |
| )pb"); | |
| claim_validator.SetClaimResult("urn:c2pa:1", claim1); | |
| claim_validator.SetClaimResult("urn:c2pa:2", claim2); | |
| claim_validator.SetClaimResult("urn:c2pa:3", claim3); | |
| claim_validator.SetClaimResult("urn:c2pa:4", claim4); | |
| MockAssertionValidator assertion_validator; | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:3", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:4", {})); | |
| EXPECT_CALL(assertion_validator, | |
| ValidateClaimAssertions( | |
| _, _, Property(&Manifest::label, "urn:c2pa:1"), _)) | |
| .WillOnce( | |
| [&](const jumbf::UriResolver&, | |
| const absl::flat_hash_set<std::string>& redacted_assertion_paths, | |
| Manifest& manifest, ValidationTracker&) { | |
| EXPECT_THAT( | |
| redacted_assertion_paths, | |
| // Note that the assertion redacted by claim3 does not appear | |
| // here, because we haven't reached claim3 yet in the DFS | |
| // when we process claim1. See discussion in | |
| // https://github.com/c2pa-org/specs-core/issues/1540. | |
| UnorderedElementsAre( | |
| "/c2pa/urn:c2pa:1/claim1_assertion_redacted_by_claim4", | |
| "/c2pa/urn:c2pa:1/claim1_assertion_redacted_by_claim2")); | |
| }); | |
| EXPECT_CALL(assertion_validator, | |
| ValidateClaimAssertions( | |
| _, _, Property(&Manifest::label, "urn:c2pa:2"), _)) | |
| .WillOnce( | |
| [&](const jumbf::UriResolver&, | |
| const absl::flat_hash_set<std::string>& redacted_assertion_paths, | |
| Manifest& manifest, ValidationTracker&) { | |
| EXPECT_THAT( | |
| redacted_assertion_paths, | |
| UnorderedElementsAre( | |
| "/c2pa/urn:c2pa:2/claim2_assertion_redacted_by_claim4")); | |
| auto* active_manifest = manifest.add_assertions() | |
| ->mutable_ingredient_v3() | |
| ->mutable_active_manifest(); | |
| active_manifest->set_url("self#jumbf=/c2pa/urn:c2pa:1"); | |
| }); | |
| EXPECT_CALL(assertion_validator, | |
| ValidateClaimAssertions( | |
| _, _, Property(&Manifest::label, "urn:c2pa:3"), _)) | |
| .WillOnce( | |
| [&](const jumbf::UriResolver&, | |
| const absl::flat_hash_set<std::string>& redacted_assertion_paths, | |
| Manifest& manifest, ValidationTracker&) { | |
| EXPECT_THAT( | |
| redacted_assertion_paths, | |
| UnorderedElementsAre( | |
| "/c2pa/urn:c2pa:3/claim3_assertion_redacted_by_claim4")); | |
| auto* active_manifest = manifest.add_assertions() | |
| ->mutable_ingredient_v3() | |
| ->mutable_active_manifest(); | |
| active_manifest->set_url("self#jumbf=/c2pa/urn:c2pa:1"); | |
| }); | |
| EXPECT_CALL(assertion_validator, | |
| ValidateClaimAssertions( | |
| _, _, Property(&Manifest::label, "urn:c2pa:4"), _)) | |
| .WillOnce( | |
| [&](const jumbf::UriResolver&, | |
| const absl::flat_hash_set<std::string>& redacted_assertion_paths, | |
| Manifest& manifest, ValidationTracker&) { | |
| EXPECT_THAT(redacted_assertion_paths, IsEmpty()); | |
| auto* active_manifest1 = manifest.add_assertions() | |
| ->mutable_ingredient_v3() | |
| ->mutable_active_manifest(); | |
| active_manifest1->set_url("self#jumbf=/c2pa/urn:c2pa:2"); | |
| auto* active_manifest2 = manifest.add_assertions() | |
| ->mutable_ingredient_v3() | |
| ->mutable_active_manifest(); | |
| active_manifest2->set_url("self#jumbf=/c2pa/urn:c2pa:3"); | |
| }); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests.back(), &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientNesting) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:3", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:4", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:5", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:6", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests = { | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:2", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:3", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:4", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:5", {claim_signature_box_}), | |
| CreateStandardManifest("urn:c2pa:6", {claim_signature_box_}), | |
| }; | |
| for (int i = 2; i <= 6; ++i) { | |
| assertion_validator.SetOutputAssertions( | |
| absl::StrCat("/c2pa/urn:c2pa:", i), | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>(absl::Substitute( | |
| R"pb( | |
| label: "c2pa.ingredient.v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:$0" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:$0/c2pa.signature" | |
| } | |
| })pb", | |
| i - 1))}); | |
| } | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[5], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT((*result)->active_manifest().validation().failures(), IsEmpty()); | |
| ASSERT_THAT((*result)->ingredient_manifests(), SizeIs(5)); | |
| EXPECT_THAT((*result)->ingredient_manifests(0).validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT((*result)->ingredient_manifests(1).validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT((*result)->ingredient_manifests(2).validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT((*result)->ingredient_manifests(3).validation().failures(), | |
| IsEmpty()); | |
| EXPECT_THAT((*result)->ingredient_manifests(4).validation().failures(), | |
| IsEmpty()); | |
| } | |
| TEST_F(ManifestGraphTest, GraphContainsLoop) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:active_manifest", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:ingredient_manifest", | |
| DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| jumbf::SuperBox active_manifest = CreateStandardManifest( | |
| "urn:c2pa:active_manifest", {claim_signature_box_}); | |
| jumbf::SuperBox ingredient_manifest = CreateStandardManifest( | |
| "urn:c2pa:ingredient_manifest", {claim_signature_box_}); | |
| jumbf::SuperBox manifest_store = | |
| CreateManifestStore({ingredient_manifest, active_manifest}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:active_manifest", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(ingredient_v3 { | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:ingredient_manifest" | |
| } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:ingredient_manifest/c2pa.signature" | |
| } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:ingredient_manifest", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(ingredient_v3 { | |
| active_manifest { | |
| url: "self#jumbf=/c2pa/urn:c2pa:active_manifest" | |
| } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:active_manifest/c2pa.signature" | |
| } | |
| })pb")}); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&active_manifest, &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT((*result)->active_manifest().validation().failures(), IsEmpty()); | |
| ASSERT_THAT((*result)->ingredient_manifests(), SizeIs(1)); | |
| EXPECT_THAT((*result)->ingredient_manifests(0).validation().failures(), | |
| IsEmpty()); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3WithInvalidClaimSignatureUri) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(label: "ingredient_v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| claim_signature { url: "invalid_uri" } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| failures { | |
| code: "ingredient.claimSignature.missing" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/ingredient_v3" | |
| explanation: "self#jumbf= prefix required" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "ingredient_v3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| claim_signature { url: "invalid_uri" } | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3WithoutManifest) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(label: "ingredient_v3" | |
| ingredient_v3 { | |
| relationship: "componentOf" | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "ingredient_v3" | |
| ingredient_v3 { | |
| relationship: "componentOf" | |
| claim_signature { url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" } | |
| } | |
| } | |
| validation { | |
| informationals { | |
| code: "ingredient.unknownProvenance" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/ingredient_v3" | |
| } | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3InputToWithoutManifest) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb(ingredient_v3 { | |
| relationship: "inputTo" | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| ingredient_v3 { | |
| relationship: "inputTo" | |
| claim_signature { url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" } | |
| } | |
| } | |
| validation { | |
| # No ingredient.unknownProvenance informational for inputTo ingredient. | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3WithValidClaimSignatureUri) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back( | |
| CreateStandardManifest("urn:c2pa:1", {claim_signature_box_})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| claim_signature { | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "assert" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| claim_signature { url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.signature" } | |
| } | |
| } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| successes { | |
| code: "ingredient.claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert" | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:2" | |
| claim { default_algorithm: "sha256" } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3WithMultipleParents) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:3", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:4", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:3", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:4", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_2" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| })pb"), | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:3", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_2" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_3" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "assert_for_2" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } } | |
| } | |
| assertions { | |
| label: "assert_for_3" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:2" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/assert_for_4" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "assert_for_4" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:4" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:4" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:3" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:3" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:3/c2pa.assertions/assert_for_4" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "assert_for_4" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3AllButOneIngredientManifestIsValid) { | |
| FakeClaimValidator claim_validator(/*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimSuccesses( | |
| "urn:c2pa:1", {SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| claim_validator.SetClaimSuccesses( | |
| "urn:c2pa:2", {SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:3", DefaultClaim()); | |
| claim_validator.SetClaimSuccesses( | |
| "urn:c2pa:3", {SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimInformationals( | |
| "urn:c2pa:3", {InformationalStatusCode::kIngredientUnknownProvenance}); | |
| claim_validator.SetClaimResult("urn:c2pa:4", DefaultClaim()); | |
| claim_validator.SetClaimFailures( | |
| "urn:c2pa:4", {FailureStatusCode::kClaimSignatureMismatch}); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:3", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:4", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_2" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| })pb"), | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } | |
| })pb"), | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "assert_for_2" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } } | |
| } | |
| assertions { | |
| label: "assert_for_3" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } } | |
| } | |
| assertions { | |
| label: "assert_for_4" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } } | |
| } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_2" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_3" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_4" | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:2" | |
| claim { default_algorithm: "sha256" } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2" | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:3" | |
| claim { default_algorithm: "sha256" } | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:3" | |
| } | |
| informationals { | |
| code: "ingredient.unknownProvenance" | |
| url: "self#jumbf=/c2pa/urn:c2pa:3" | |
| } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:4" | |
| claim { default_algorithm: "sha256" } | |
| validation { | |
| failures { | |
| code: "claimSignature.mismatch" | |
| url: "self#jumbf=/c2pa/urn:c2pa:4" | |
| } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientV3WithRedactions) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| Claim claim1 = DefaultClaim(); | |
| Claim claim2 = DefaultClaim(); | |
| claim1.add_redacted_assertions("self#jumbf=/c2pa/urn:c2pa:2/some_assertion"); | |
| claim2.add_created_assertions()->set_url("self#jumbf=some_assertion"); | |
| claim_validator.SetClaimResult("urn:c2pa:1", claim1); | |
| claim_validator.SetClaimResult("urn:c2pa:2", claim2); | |
| claim_validator.SetClaimResult("urn:c2pa:3", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:4", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:3", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:4", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_2" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| })pb"), | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:3", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| EXPECT_THAT(**result, EqualsProto(ParseTextProtoOrDie< | |
| PartialValidationResultProto>(R"pb( | |
| active_manifest { | |
| label: "urn:c2pa:1" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_2" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/assert_for_3" | |
| } | |
| } | |
| claim { | |
| redacted_assertions: "self#jumbf=/c2pa/urn:c2pa:2/some_assertion" | |
| default_algorithm: "sha256" | |
| } | |
| assertions { | |
| label: "c2pa.hash.data" | |
| data_hash {} | |
| } | |
| assertions { | |
| label: "assert_for_2" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } } | |
| } | |
| assertions { | |
| label: "assert_for_3" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:2" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:2/c2pa.assertions/assert_for_4" | |
| } | |
| } | |
| claim { | |
| created_assertions { url: "self#jumbf=some_assertion" } | |
| default_algorithm: "sha256" | |
| } | |
| assertions { | |
| label: "assert_for_4" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } } | |
| } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:4" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:4" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| } | |
| ingredient_manifests { | |
| label: "urn:c2pa:3" | |
| validation { | |
| successes { | |
| code: "claimSignature.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:3" | |
| } | |
| successes { | |
| code: "ingredient.manifest.validated" | |
| url: "self#jumbf=/c2pa/urn:c2pa:3/c2pa.assertions/assert_for_4" | |
| } | |
| } | |
| claim { default_algorithm: "sha256" } | |
| assertions { | |
| label: "assert_for_4" | |
| ingredient_v3 { active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } } | |
| } | |
| } | |
| hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:1/c2pa.assertions/c2pa.hash.data" | |
| )pb"))); | |
| } | |
| TEST_F(ManifestGraphTest, IngredientAssertionsWithFailures) { | |
| FakeClaimValidator claim_validator( | |
| /*failures=*/{}, /*informationals=*/{}, | |
| /*successes=*/{SuccessStatusCode::kClaimSignatureValidated}); | |
| claim_validator.SetClaimResult("urn:c2pa:1", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:2", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:3", DefaultClaim()); | |
| claim_validator.SetClaimResult("urn:c2pa:4", DefaultClaim()); | |
| FakeAssertionValidator assertion_validator(/*failures=*/{}, /*successes=*/{}); | |
| jumbf::SuperBox root_box; | |
| std::vector<jumbf::SuperBox> manifests; | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:1", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:2", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:3", {})); | |
| manifests.push_back(CreateStandardManifest("urn:c2pa:4", {})); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:1", | |
| {empty_data_hash_assertion_, | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_2" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:2" } | |
| validation_results { | |
| active_manifest { | |
| failures { code: "assertion.dataHash.mismatch" url: "blah2" } | |
| } | |
| } | |
| })pb"), | |
| ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_3" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:3" } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:2", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| validation_results { | |
| active_manifest { | |
| failures { code: "assertion.dataHash.mismatch" url: "blah4" } | |
| } | |
| } | |
| })pb")}); | |
| assertion_validator.SetOutputAssertions( | |
| "/c2pa/urn:c2pa:3", | |
| {ParseTextProtoOrDie<Assertion>( | |
| R"pb( | |
| label: "assert_for_4" | |
| ingredient_v3 { | |
| active_manifest { url: "self#jumbf=/c2pa/urn:c2pa:4" } | |
| validation_results { | |
| active_manifest { | |
| failures { code: "assertion.dataHash.malformed" url: "blah4" } | |
| } | |
| } | |
| })pb")}); | |
| jumbf::SuperBox manifest_store = CreateManifestStore(manifests); | |
| jumbf::UriResolver uri_resolver = | |
| jumbf::UriResolver::WithSingleRootChild(&manifest_store); | |
| ManifestGraph graph(&manifests[0], &uri_resolver, &assertion_validator, | |
| &claim_validator, &noop_hash_checker_factory_, &options_); | |
| auto result = graph.Validate(); | |
| ABSL_ASSERT_OK(result); | |
| ASSERT_THAT((*result)->ingredient_manifests(), SizeIs(3)); | |
| EXPECT_THAT((*result)->ingredient_manifests(0).label(), "urn:c2pa:2"); | |
| EXPECT_THAT((*result)->ingredient_manifests(0).validation().failures(), | |
| Contains(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb( | |
| code: "assertion.dataHash.mismatch" | |
| url: "blah2" | |
| explanation: "from original validator" | |
| )pb")))); | |
| EXPECT_THAT((*result)->ingredient_manifests(1).label(), "urn:c2pa:4"); | |
| EXPECT_THAT((*result)->ingredient_manifests(1).validation().failures(), | |
| Contains(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb( | |
| code: "assertion.dataHash.mismatch" | |
| url: "blah4" | |
| explanation: "from original validator" | |
| )pb")))); | |
| } | |
| } // namespace | |
| } // namespace credentio |