| // 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 "claim/manifest_store_builder.h" | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/log/check.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/cord.h" | |
| #include "absl/strings/string_view.h" | |
| #include "assertion/assertion_encoder.h" | |
| #include "constants/labels.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/box_builder.h" | |
| #include "jumbf/encode_params.h" | |
| #include "jumbf/parse.h" | |
| #include "jumbf/test_utils.h" | |
| #include "proto/assertion.pb.h" | |
| #include "proto/hashed_uri.pb.h" | |
| #include "testing/proto_test_utils.h" | |
| #include "uuid/uuid.h" | |
| namespace credentio { | |
| namespace { | |
| using ::absl_testing::IsOkAndHolds; | |
| using ::absl_testing::StatusIs; | |
| using ::credentio_testing::EqualsProto; | |
| using ::credentio_testing::ParseTextProtoOrDie; | |
| using ::testing::ElementsAre; | |
| using ::testing::HasSubstr; | |
| inline constexpr Uuid kUnknownUuid( | |
| Uuid::FromStringOrDie("00000000-0000-0000-0000-000000000000")); | |
| class FakeHasherFactory : public HasherFactory { | |
| public: | |
| absl::StatusOr<std::unique_ptr<Hasher>> Create() const override { | |
| return CreateHasher("sha256"); | |
| } | |
| HashAlgorithm algorithm() const override { return HashAlgorithm::kSha256; } | |
| }; | |
| bool IsManifestStore(const jumbf::SuperBox& superbox) { | |
| return superbox.description.label == kManifestStoreLabel && | |
| superbox.description.type_uuid == kManifestStoreUuid; | |
| } | |
| bool IsManifest(const jumbf::SuperBox& superbox) { | |
| const auto& uuid = superbox.description.type_uuid; | |
| return uuid == kStandardManifestUuid || uuid == kUpdateManifestUuid || | |
| uuid == kCompressedManifestUuid || uuid == kTimestampManifestUuid; | |
| } | |
| absl::StatusOr<std::vector<std::string>> ExtractManifestLabels( | |
| absl::string_view manifest_store) { | |
| std::vector<std::string> labels; | |
| ABSL_ASSIGN_OR_RETURN( | |
| auto ms_superbox, | |
| jumbf::ConsumeSuperBox(&manifest_store, /*recursion_limit=*/9)); | |
| if (!IsManifestStore(ms_superbox)) { | |
| return absl::NotFoundError( | |
| "JUMBF SuperBox does not contain a manifest store"); | |
| } | |
| for (const auto& content : ms_superbox.contents) { | |
| if (content.Holds<jumbf::SuperBox>()) { | |
| const auto& superbox = content.Get<jumbf::SuperBox>(); | |
| if (IsManifest(superbox)) { | |
| labels.push_back(std::string(superbox.description.label.value_or(""))); | |
| } | |
| } | |
| } | |
| return labels; | |
| } | |
| class ManifestStoreBuilderTest : public ::testing::Test { | |
| protected: | |
| ManifestStoreBuilderTest() : hasher_factory_(), builder_(&hasher_factory_) {}; | |
| FakeHasherFactory hasher_factory_; | |
| ManifestStoreBuilder builder_; | |
| StandardAssertionEncoder cbor_encoder_; | |
| }; | |
| TEST_F(ManifestStoreBuilderTest, AddActiveManifest) { | |
| jumbf::SuperBoxBuilder active_manifest_builder( | |
| kStandardManifestUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord("manifest_1")}); | |
| std::string cbor_box_1 = jumbf::EncodeCborBox("cbor_box_1"); | |
| ABSL_EXPECT_OK(active_manifest_builder.AddChild(absl::Cord(cbor_box_1))); | |
| auto active_manifest_or = std::move(active_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(active_manifest_or); | |
| jumbf::BuiltSuperBox active_manifest = *std::move(active_manifest_or); | |
| jumbf::SuperBoxBuilder expected_manifest_builder( | |
| kStandardManifestUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord("manifest_1")}); | |
| std::string expected_cbor_box_1 = jumbf::EncodeCborBox("cbor_box_1"); | |
| ABSL_EXPECT_OK(expected_manifest_builder.AddChild(absl::Cord(cbor_box_1))); | |
| auto expected_manifest_or = std::move(expected_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(expected_manifest_or); | |
| jumbf::BuiltSuperBox expected_manifest = *std::move(expected_manifest_or); | |
| absl::Cord expected_manifest_cord = std::move(expected_manifest).AsCord(); | |
| auto manifest_store_or = | |
| std::move(builder_).Finalize(std::move(active_manifest)); | |
| ABSL_ASSERT_OK(manifest_store_or); | |
| jumbf::BuiltSuperBox manifest_store = *std::move(manifest_store_or); | |
| absl::Cord manifest_store_cord = std::move(manifest_store).AsCord(); | |
| EXPECT_THAT(manifest_store_cord, HasSubstr(expected_manifest_cord)); | |
| } | |
| TEST_F(ManifestStoreBuilderTest, AddIngredientManifestStoreNoManifests) { | |
| std::string ingredient_cbor_box_1 = | |
| jumbf::EncodeCborBox("ingredient_cbor_box_1"); | |
| std::string ingredient_manifest_store_1 = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {ingredient_cbor_box_1}); | |
| EXPECT_THAT(builder_.AddIngredientManifestStore(ingredient_manifest_store_1), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("No active manifest found"))); | |
| } | |
| TEST_F(ManifestStoreBuilderTest, AddIngredientManifestStoreNoClaimSignature) { | |
| std::string ingredient_cbor_box_1 = | |
| jumbf::EncodeCborBox("ingredient_cbor_box_1"); | |
| std::string ingredient_manifest_1 = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "ingredient_manifest_1", {ingredient_cbor_box_1}); | |
| std::string ingredient_manifest_store_1 = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {ingredient_manifest_1}); | |
| EXPECT_THAT(builder_.AddIngredientManifestStore(ingredient_manifest_store_1), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("No claim signature found in the manifest"))); | |
| } | |
| TEST_F(ManifestStoreBuilderTest, AddIngredientManifestStore) { | |
| jumbf::SuperBoxBuilder active_manifest_builder( | |
| kStandardManifestUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord("active_manifest")}); | |
| std::string active_cbor_box = jumbf::EncodeCborBox("active_cbor_box"); | |
| ABSL_EXPECT_OK(active_manifest_builder.AddChild(absl::Cord(active_cbor_box))); | |
| auto active_manifest_or = std::move(active_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(active_manifest_or); | |
| jumbf::BuiltSuperBox active_manifest = *std::move(active_manifest_or); | |
| std::string ingredient_cbor_box_1 = | |
| jumbf::EncodeCborBox("ingredient_cbor_box_1"); | |
| std::string ingredient_signature_box_1 = jumbf::EncodeSuperBox( | |
| kClaimSignatureUuid, kClaimSignatureLabel, {ingredient_cbor_box_1}); | |
| std::string ingredient_manifest_1 = | |
| jumbf::EncodeSuperBox(kStandardManifestUuid, "ingredient_manifest_1", | |
| {ingredient_signature_box_1}); | |
| std::string ingredient_active_cbor_box_1 = | |
| jumbf::EncodeCborBox("ingredient_cbor_box_1"); | |
| std::string ingredient_active_signature_box_1 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {ingredient_active_cbor_box_1}); | |
| std::string ingredient_active_manifest_1 = | |
| jumbf::EncodeSuperBox(kUpdateManifestUuid, "ingredient_active_manifest_1", | |
| {ingredient_active_signature_box_1}); | |
| std::string ingredient_unknown_box_1 = jumbf::EncodeSuperBox( | |
| kUnknownUuid, "ingredient_unknown_box_1", {ingredient_cbor_box_1}); | |
| std::string ingredient_manifest_store_1 = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, | |
| {ingredient_manifest_1, ingredient_active_manifest_1, | |
| ingredient_cbor_box_1, ingredient_unknown_box_1}); | |
| std::string ingredient_cbor_box_2 = | |
| jumbf::EncodeCborBox("ingredient_cbor_box_2"); | |
| std::string ingredient_signature_box_2 = jumbf::EncodeSuperBox( | |
| kClaimSignatureUuid, kClaimSignatureLabel, {ingredient_cbor_box_2}); | |
| std::string ingredient_manifest_2 = | |
| jumbf::EncodeSuperBox(kCompressedManifestUuid, "ingredient_manifest_2", | |
| {ingredient_signature_box_2}); | |
| std::string ingredient_active_cbor_box_2 = | |
| jumbf::EncodeCborBox("ingredient_cbor_box_2"); | |
| std::string ingredient_active_signature_box_2 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {ingredient_active_cbor_box_2}); | |
| std::string ingredient_active_manifest_2 = jumbf::EncodeSuperBox( | |
| kTimestampManifestUuid, "ingredient_active_manifest_2", | |
| {ingredient_active_signature_box_2}); | |
| std::string ingredient_manifest_store_2 = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, | |
| {ingredient_manifest_2, ingredient_active_manifest_2}); | |
| jumbf::SuperBoxBuilder expected_manifest_builder( | |
| kStandardManifestUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord("active_manifest")}); | |
| std::string expected_cbor_box = jumbf::EncodeCborBox("active_cbor_box"); | |
| ABSL_EXPECT_OK( | |
| expected_manifest_builder.AddChild(absl::Cord(expected_cbor_box))); | |
| auto expected_manifest_or = std::move(expected_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(expected_manifest_or); | |
| jumbf::BuiltSuperBox expected_manifest = *std::move(expected_manifest_or); | |
| absl::Cord expected_manifest_cord = std::move(expected_manifest).AsCord(); | |
| std::string expected_manifest_store = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, | |
| {ingredient_manifest_1, ingredient_active_manifest_1, | |
| ingredient_manifest_2, ingredient_active_manifest_2, | |
| std::string(expected_manifest_cord)}, | |
| /*requestable=*/true); | |
| auto uris_1_or = | |
| builder_.AddIngredientManifestStore(ingredient_manifest_store_1); | |
| ABSL_ASSERT_OK(uris_1_or); | |
| IngredientManifestUris ingredient_manifest_uris_1 = *std::move(uris_1_or); | |
| auto expected_active_manifest_uri_1 = ParseTextProtoOrDie<HashedUri>( | |
| R"pb( | |
| url: "self#jumbf=/c2pa/ingredient_active_manifest_1" | |
| hash: "=s\030\315\353\317\241\271c\342&\233\315gV72\270\344\200\371\"\326\237p\302\033n\223a\357\023" | |
| )pb"); | |
| auto expected_claim_signature_uri_1 = ParseTextProtoOrDie<HashedUri>( | |
| R"pb( | |
| url: "self#jumbf=/c2pa/ingredient_active_manifest_1/c2pa.signature" | |
| hash: "\370\347\000R\200\003\005\024\211ivc_(\243\013C\342\367\215\375\022v\206\257\023\363\251\221\270k\311" | |
| )pb"); | |
| EXPECT_THAT(ingredient_manifest_uris_1.active_manifest, | |
| EqualsProto(expected_active_manifest_uri_1)); | |
| EXPECT_THAT(ingredient_manifest_uris_1.claim_signature, | |
| EqualsProto(expected_claim_signature_uri_1)); | |
| auto uris_2_or = | |
| builder_.AddIngredientManifestStore(ingredient_manifest_store_2); | |
| ABSL_ASSERT_OK(uris_2_or); | |
| IngredientManifestUris ingredient_manifest_uris_2 = *std::move(uris_2_or); | |
| auto expected_active_manifest_uri_2 = ParseTextProtoOrDie<HashedUri>( | |
| R"pb( | |
| url: "self#jumbf=/c2pa/ingredient_active_manifest_2" | |
| hash: "\231\t\034\211\312\271D\262\317\006\314\206EAv\243\276\2335\254\357]\302\217\20208\214\360A@\"" | |
| )pb"); | |
| auto expected_claim_signature_uri_2 = ParseTextProtoOrDie<HashedUri>( | |
| R"pb( | |
| url: "self#jumbf=/c2pa/ingredient_active_manifest_2/c2pa.signature" | |
| hash: "Q\234\255\026\232\274\373\314X\317\271N=\260`\030\355\343\024\245h\326^\306\364\205\202\367\200\233f\371" | |
| )pb"); | |
| EXPECT_THAT(ingredient_manifest_uris_2.active_manifest, | |
| EqualsProto(expected_active_manifest_uri_2)); | |
| EXPECT_THAT(ingredient_manifest_uris_2.claim_signature, | |
| EqualsProto(expected_claim_signature_uri_2)); | |
| auto manifest_store_or = | |
| std::move(builder_).Finalize(std::move(active_manifest)); | |
| ABSL_ASSERT_OK(manifest_store_or); | |
| jumbf::BuiltSuperBox manifest_store = *std::move(manifest_store_or); | |
| absl::Cord manifest_store_cord = std::move(manifest_store).AsCord(); | |
| EXPECT_THAT(std::string(manifest_store_cord), expected_manifest_store); | |
| } | |
| TEST_F(ManifestStoreBuilderTest, AddIngredientManifestStore_Deduplicates) { | |
| jumbf::SuperBoxBuilder active_manifest_builder( | |
| kStandardManifestUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord("active_manifest")}); | |
| ABSL_EXPECT_OK(active_manifest_builder.AddChild( | |
| absl::Cord(jumbf::EncodeCborBox("active_cbor_box")))); | |
| auto active_manifest_or = std::move(active_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(active_manifest_or); | |
| jumbf::BuiltSuperBox active_manifest = *std::move(active_manifest_or); | |
| std::string signature_1 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {jumbf::EncodeCborBox("signature_1")}); | |
| std::string signature_2 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {jumbf::EncodeCborBox("signature_2")}); | |
| std::string signature_3 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {jumbf::EncodeCborBox("signature_3")}); | |
| std::string ingredient_manifest_1 = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "ingredient_manifest_1", {signature_1}); | |
| std::string ingredient_manifest_2 = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "ingredient_manifest_2", {signature_2}); | |
| std::string ingredient_manifest_3 = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "ingredient_manifest_3", {signature_3}); | |
| // These two ingredient manifest stores have ingredient_manifest_1 in common. | |
| std::string ingredient_manifest_store_3 = | |
| jumbf::EncodeSuperBox(kManifestStoreUuid, kManifestStoreLabel, | |
| {ingredient_manifest_1, ingredient_manifest_3}); | |
| std::string ingredient_manifest_store_2 = | |
| jumbf::EncodeSuperBox(kManifestStoreUuid, kManifestStoreLabel, | |
| {ingredient_manifest_1, ingredient_manifest_2}); | |
| auto uris_2_or = | |
| builder_.AddIngredientManifestStore(ingredient_manifest_store_2); | |
| ABSL_ASSERT_OK(uris_2_or); | |
| auto uris_3a_or = | |
| builder_.AddIngredientManifestStore(ingredient_manifest_store_3); | |
| ABSL_ASSERT_OK(uris_3a_or); | |
| // Add the same ingredient manifest store again. | |
| auto uris_3b_or = | |
| builder_.AddIngredientManifestStore(ingredient_manifest_store_3); | |
| ABSL_ASSERT_OK(uris_3b_or); | |
| auto manifest_store_or = | |
| std::move(builder_).Finalize(std::move(active_manifest)); | |
| ABSL_ASSERT_OK(manifest_store_or); | |
| jumbf::BuiltSuperBox manifest_store = *std::move(manifest_store_or); | |
| absl::Cord manifest_store_cord = std::move(manifest_store).AsCord(); | |
| EXPECT_THAT( | |
| ExtractManifestLabels(manifest_store_cord.Flatten()), | |
| IsOkAndHolds(ElementsAre("ingredient_manifest_1", "ingredient_manifest_2", | |
| "ingredient_manifest_3", "active_manifest"))); | |
| } | |
| TEST_F(ManifestStoreBuilderTest, FinalizeFailsWhenActiveManifestIsIngredient) { | |
| jumbf::SuperBoxBuilder active_manifest_builder( | |
| kStandardManifestUuid, | |
| jumbf::DescriptionBoxOptions{.label = absl::Cord("active_manifest")}); | |
| ABSL_EXPECT_OK(active_manifest_builder.AddChild( | |
| absl::Cord(jumbf::EncodeCborBox("active_cbor_box")))); | |
| auto active_manifest_or = std::move(active_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(active_manifest_or); | |
| jumbf::BuiltSuperBox active_manifest = *std::move(active_manifest_or); | |
| std::string signature_1 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {jumbf::EncodeCborBox("signature_1")}); | |
| std::string signature_2 = | |
| jumbf::EncodeSuperBox(kClaimSignatureUuid, kClaimSignatureLabel, | |
| {jumbf::EncodeCborBox("signature_2")}); | |
| std::string ingredient_manifest_1 = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "ingredient_manifest", {signature_1}); | |
| std::string ingredient_manifest_with_active_manifest_label = | |
| jumbf::EncodeSuperBox(kStandardManifestUuid, "active_manifest", | |
| {signature_2}); | |
| std::string ingredient_manifest_store = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, | |
| {ingredient_manifest_1, ingredient_manifest_with_active_manifest_label}); | |
| ABSL_ASSERT_OK( | |
| builder_.AddIngredientManifestStore(ingredient_manifest_store)); | |
| EXPECT_THAT( | |
| std::move(builder_).Finalize(std::move(active_manifest)), | |
| StatusIs( | |
| absl::StatusCode::kInternal, | |
| HasSubstr( | |
| "active manifest label is also an ingredient manifest label"))); | |
| } | |
| TEST_F(ManifestStoreBuilderTest, FinalizeFailsWhenActiveManifestHasNoLabel) { | |
| jumbf::SuperBoxBuilder active_manifest_builder( | |
| kStandardManifestUuid, jumbf::DescriptionBoxOptions{}); | |
| ABSL_EXPECT_OK(active_manifest_builder.AddChild( | |
| absl::Cord(jumbf::EncodeCborBox("active_cbor_box")))); | |
| auto active_manifest_or = std::move(active_manifest_builder).Finalize(); | |
| ABSL_ASSERT_OK(active_manifest_or); | |
| jumbf::BuiltSuperBox active_manifest = *std::move(active_manifest_or); | |
| EXPECT_THAT(std::move(builder_).Finalize(std::move(active_manifest)), | |
| StatusIs(absl::StatusCode::kInvalidArgument, | |
| HasSubstr("active manifest label is missing"))); | |
| } | |
| } // namespace | |
| } // namespace credentio |