blob: f1642c792813eef02abcc53808d820b94974f956 [file] [edit]
// 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_cbor_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_;
StandardAssertionCborEncoder 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