blob: 9c95a156f1f6a5696b1530eb9c02e95010e4fe71 [file]
// 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_box_encoder.h"
#include <cstddef>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/log/absl_check.h"
#include "absl/status/status.h"
#include "absl/status/status_matchers.h"
#include "absl/status/statusor.h"
#include "absl/strings/cord.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "absl/synchronization/notification.h"
#include "assertion/assertion_cbor_encoder.h"
#include "assertion/assertion_referencer.h"
#include "async/async_context.h"
#include "claim/claim_cbor_encoder.h"
#include "claim/local_signer.h"
#include "claim/manifest_box.h"
#include "claim/manifest_store_builder.h"
#include "claim/signer.h"
#include "constants/labels.h"
#include "crypto/algorithms.h"
#include "crypto/crypto_read_handler.h"
#include "crypto/crypto_write_handler.h"
#include "crypto/default/default_crypto_read_handler.h"
#include "crypto/default/hasher.h"
#include "crypto/hash.h"
#include "gmock/gmock.h"
#include "google/protobuf/util/message_differencer.h"
#include "gtest/gtest.h"
#include "jumbf/box_builder.h"
#include "proto/actions_assertion.pb.h"
#include "proto/assertion.pb.h"
#include "proto/generator.pb.h"
#include "proto/generator_info.pb.h"
#include "proto/hashed_uri.pb.h"
#include "proto/ingredient_assertion.pb.h"
#include "proto/manifest.pb.h"
#include "proto/signature_info.pb.h"
#include "proto/validation_result.pb.h"
#include "proto/validation_status.pb.h"
#include "salt/salt_generator.h"
#include "testing/create_test_crypto_write_handler.h"
#include "testing/local_key_crypto_write_handler.h"
#include "testing/manifest_store_test_utils.h"
#include "testing/proto_test_utils.h"
#include "uuid/uuid.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::IsOkAndHolds;
using ::absl_testing::StatusIs;
using ::credentio_testing::ParseTextProtoOrDie;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Pointee;
constexpr auto kUuid =
Uuid::FromStringOrDie("12345678-1234-1234-1234-123456789012");
MATCHER_P(EqualsProtoIgnoringFieldsAndOrdering, expected, "") {
google::protobuf::util::MessageDifferencer differencer;
differencer.set_repeated_field_comparison(
google::protobuf::util::MessageDifferencer::AS_SET);
differencer.IgnoreField(HashedUri::descriptor()->FindFieldByName("hash"));
differencer.IgnoreField(
SignatureInfo::descriptor()->FindFieldByName("cert_serial_number"));
differencer.IgnoreField(
SignatureInfo::descriptor()->FindFieldByName("certificate_issuer"));
differencer.IgnoreField(
SignatureInfo::descriptor()->FindFieldByName("timestamping_time"));
std::string diff;
differencer.ReportDifferencesToString(&diff);
bool match = differencer.Compare(expected, arg);
if (!match) {
*result_listener << "\nDifference:\n" << diff;
}
return match;
}
ClaimSigner::Options ToClaimSignerOptions(
const GenerateManifestStoreOptions& options, AsyncContext context) {
ClaimSigner::Options result;
result.async_context = std::move(context);
switch (options.time_stamping_mode()) {
case GenerateManifestStoreOptions::TIME_STAMPING_REQUIRED:
result.time_stamping_mode = TimeStampingMode::kRequired;
break;
case GenerateManifestStoreOptions::TIME_STAMPING_OMITTED_WITH_PADDING:
result.time_stamping_mode = TimeStampingMode::kOmittedWithPadding;
break;
case GenerateManifestStoreOptions::TIME_STAMPING_OPTIONAL:
case GenerateManifestStoreOptions::TIME_STAMPING_MODE_UNSPECIFIED:
default:
result.time_stamping_mode = TimeStampingMode::kOptional;
break;
}
return result;
}
absl::StatusOr<jumbf::BuiltSuperBox> Encode(
ManifestBoxEncoder& manifest_box_encoder,
const GenerateManifestStoreOptions& options = {}) {
absl::Notification done;
absl::StatusOr<ManifestEncoderResult> result;
manifest_box_encoder.Encode(
ToClaimSignerOptions(options, AsyncContext()),
[&result, &done](absl::StatusOr<ManifestEncoderResult> res) {
result = std::move(res);
done.Notify();
});
done.WaitForNotification();
if (!result.ok()) {
return result.status();
}
return std::move(result->box);
}
class FakeUuidGenerator : public UuidGenerator {
public:
FakeUuidGenerator() = default;
Uuid Generate() const override { return kUuid; }
};
class FakeSaltGenerator : public SaltGenerator {
public:
explicit FakeSaltGenerator(absl::StatusOr<std::string> salt)
: salt_(std::move(salt)) {}
absl::StatusOr<std::string> Generate() const override { return salt_; }
private:
absl::StatusOr<std::string> salt_;
};
class FakeHasherFactory : public HasherFactory {
public:
absl::StatusOr<std::unique_ptr<Hasher>> Create() const override {
return CreateHasher("sha256");
}
HashAlgorithm algorithm() const override { return HashAlgorithm::kSha256; }
};
class ManifestBoxEncoderTest : public testing::Test {
protected:
ManifestBoxEncoderTest()
: assertion_cbor_encoder_(), claim_cbor_encoder_(GetClaimCborEncoder()) {
generator_info_.set_name("Test generator");
generator_info_.set_spec_version("2.2.0");
auto crypto_writer = CreateTestCryptoWriteHandler({});
ABSL_CHECK_OK(crypto_writer);
crypto_writer_ = std::move(*crypto_writer);
}
std::unique_ptr<CryptoWriteHandler> crypto_writer_;
GeneratorInfo generator_info_;
FakeHasherFactory hasher_factory_;
StandardAssertionCborEncoder assertion_cbor_encoder_;
const ClaimCborEncoder& claim_cbor_encoder_;
ManifestBox CreateMinimalManifestBox() {
return {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent { name: "test generator" }
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
}
};
TEST_F(ManifestBoxEncoderTest, GetEncodedManifest) {
ManifestBox manifest_box = CreateMinimalManifestBox();
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
hash: "D\242\244\274\235o\350\352\312\307\225|C\033\204\347\311\243\202\'\203c\362\331T1a\377P\271\255\366"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\331\271\373\3228\310\335>y\263\334\342\335\367\211Q\360\325\227\r\345$]\214\005\3047[\r#1\312"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
software_agent { name: "test generator" }
}
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
)pb");
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, TimeStampingModeUnspecified) {
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
auto crypto_writer =
CreateTestCryptoWriteHandler({.disable_timestamping = true});
ABSL_ASSERT_OK(crypto_writer);
LocalClaimSigner claim_signer(crypto_writer->get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
GenerateManifestStoreOptions options;
options.set_time_stamping_mode(
GenerateManifestStoreOptions::TIME_STAMPING_MODE_UNSPECIFIED);
// Should default to OPTIONAL, so even if timestamping fails, encode succeeds.
EXPECT_THAT(Encode(manifest_box_encoder, options), IsOk());
}
TEST_F(ManifestBoxEncoderTest, TimeStampingModeOptional) {
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
auto crypto_writer =
CreateTestCryptoWriteHandler({.disable_timestamping = true});
ABSL_ASSERT_OK(crypto_writer);
LocalClaimSigner claim_signer(crypto_writer->get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
GenerateManifestStoreOptions options;
options.set_time_stamping_mode(
GenerateManifestStoreOptions::TIME_STAMPING_OPTIONAL);
// Timestamping fails, but mode is OPTIONAL, so encode succeeds.
EXPECT_THAT(Encode(manifest_box_encoder, options), IsOk());
}
TEST_F(ManifestBoxEncoderTest, TimeStampingModeRequiredSuccess) {
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
auto crypto_writer =
CreateTestCryptoWriteHandler({.disable_timestamping = false});
ABSL_ASSERT_OK(crypto_writer);
LocalClaimSigner claim_signer(crypto_writer->get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
GenerateManifestStoreOptions options;
options.set_time_stamping_mode(
GenerateManifestStoreOptions::TIME_STAMPING_REQUIRED);
EXPECT_THAT(Encode(manifest_box_encoder, options), IsOk());
}
TEST_F(ManifestBoxEncoderTest, TimeStampingModeRequiredFailure) {
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
auto crypto_writer =
CreateTestCryptoWriteHandler({.disable_timestamping = true});
ABSL_ASSERT_OK(crypto_writer);
LocalClaimSigner claim_signer(crypto_writer->get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
GenerateManifestStoreOptions options;
options.set_time_stamping_mode(
GenerateManifestStoreOptions::TIME_STAMPING_REQUIRED);
// Timestamping fails, and mode is REQUIRED, so encode fails.
EXPECT_THAT(
Encode(manifest_box_encoder, options),
StatusIs(absl::StatusCode::kUnavailable, HasSubstr("TSA is disabled")));
}
TEST_F(ManifestBoxEncoderTest, TimeStampingModeOmittedWithPadding) {
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
// This should not cause a failure because timestamping should be skipped.
auto crypto_writer =
CreateTestCryptoWriteHandler({.disable_timestamping = true});
ABSL_ASSERT_OK(crypto_writer);
LocalClaimSigner claim_signer(crypto_writer->get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
GenerateManifestStoreOptions options;
options.set_time_stamping_mode(
GenerateManifestStoreOptions::TIME_STAMPING_OMITTED_WITH_PADDING);
EXPECT_THAT(Encode(manifest_box_encoder, options), IsOk());
}
TEST_F(ManifestBoxEncoderTest,
EncodeManifestWithInvalidOpenedActionNoIngredients) {
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent { name: "test generator" }
description: "created for testing"
}
}
)pb"),
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
EXPECT_THAT(Encode(manifest_box_encoder),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Opened action must have exactly one "
"ingredient.")));
}
TEST_F(ManifestBoxEncoderTest,
EncodeManifestWithInvalidOpenedActionMultipleIngredients) {
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent { name: "test generator" }
description: "created for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 2"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
EXPECT_THAT(Encode(manifest_box_encoder),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Opened action must have exactly one "
"ingredient.")));
}
TEST_F(ManifestBoxEncoderTest, EncodedManifestWithCreatedActionIngredient) {
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent { name: "test generator" }
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb"),
.ingredient_assertions =
{ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient 1"
media_type: "video/mp4"
relationship: "inputTo"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 2"
media_type: "video/mp4"
relationship: "inputTo"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb")},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
hash: "D\242\244\274\235o\350\352\312\307\225|C\033\204\347\311\243\202\'\203c\362\331T1a\377P\271\255\366"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\331\271\373\3228\310\335>y\263\334\342\335\367\211Q\360\325\227\r\345$]\214\005\3047[\r#1\312"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
software_agent { name: "test generator" }
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
}
assertions {
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient 1"
media_type: "video/mp4"
relationship: "inputTo"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 2"
media_type: "video/mp4"
relationship: "inputTo"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3__1"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
)pb");
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, EncodeManifestWithPlacedActionIngredients) {
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent { name: "test generator" }
description: "created for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.placed"
software_agent { name: "test generator" }
description: "placed for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 2"
media_type: "video/mp4"
relationship: "componentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3__2"
ingredient_v3 {
title: "test ingredient 3"
media_type: "video/mp4"
relationship: "componentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
description: "created for testing"
software_agent { name: "test generator" }
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.placed"
description: "placed for testing"
software_agent { name: "test generator" }
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__2"
}
}
}
}
}
assertions {
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "test ingredient 2"
media_type: "video/mp4"
relationship: "componentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.ingredient.v3__2"
ingredient_v3 {
title: "test ingredient 3"
media_type: "video/mp4"
relationship: "componentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3__1"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3__2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
informationals {
code: "ingredient.unknownProvenance"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
informationals {
code: "ingredient.unknownProvenance"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3__1"
}
informationals {
code: "ingredient.unknownProvenance"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3__2"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
)pb");
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, EncodeManifestWithOpenedAction) {
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent { name: "test generator" }
description: "created for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\202\377\224T\003\374\022X)\214\244z\337\002\020Wd\351\261\236\324\205\345T\321\267u\216\227]\332\014"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\300\273\\<\213\017\004\226\266\032\210!\274~\013A\274+\200\215\333[\270#\035\014\247\371\356i\006\214"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
hash: "D\242\244\274\235o\350\352\312\307\225|C\033\204\347\311\243\202\'\203c\362\331T1a\377P\271\255\366"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
description: "created for testing"
software_agent { name: "test generator" }
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
hash: "\300\273\\<\213\017\004\226\266\032\210!\274~\013A\274+\200\215\333[\270#\035\014\247\371\356i\006\214"
}
}
}
}
}
assertions {
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
informationals {
code: "ingredient.unknownProvenance"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
)pb");
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, EncodeManifestWithC2paOpenedAction) {
FakeUuidGenerator uuid_generator;
SimpleManifestStoreOptions options = {
.uuid_generator = &uuid_generator,
};
absl::StatusOr<std::string> ingredient_manifest_store_bytes =
CreateSimpleManifestStoreBytes(options);
ABSL_ASSERT_OK(ingredient_manifest_store_bytes);
ManifestStoreBuilder manifest_store_builder(&hasher_factory_);
absl::StatusOr<IngredientManifestUris> ingredient_manifest_uris =
manifest_store_builder.AddIngredientManifestStore(
*ingredient_manifest_store_bytes);
ABSL_ASSERT_OK(ingredient_manifest_uris);
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent { name: "test generator" }
description: "created for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
},
.manifest_store_builder = manifest_store_builder,
};
*manifest_box.ingredient_assertions[0]
.mutable_ingredient_v3()
->mutable_active_manifest() = ingredient_manifest_uris->active_manifest;
manifest_box.ingredient_assertions[0]
.mutable_ingredient_v3()
->mutable_validation_results()
->mutable_active_manifest()
->add_successes()
->set_code("assertion.dataHash.match");
*manifest_box.ingredient_assertions[0]
.mutable_ingredient_v3()
->mutable_claim_signature() = ingredient_manifest_uris->claim_signature;
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "03020100-0504-0706-0908-0f0e0d0c0b0a"
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
active_manifest {
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012"
}
validation_results {
active_manifest {
successes { code: "assertion.dataHash.match" }
}
}
claim_signature {
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
description: "created for testing"
software_agent { name: "test generator" }
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "ingredient.claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
}
}
ingredient_manifests {
label: "urn:c2pa:12345678-1234-1234-1234-123456789012"
claim {
claim_generator_info { name: "Test C2PA Generator" }
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.boxes"
algorithm: "sha256"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
software_agent { name: "test generator" }
}
}
}
assertions {
label: "c2pa.hash.boxes"
boxes_hash {
boxes { names: "some name" names: "other name" hash: "some hash" }
alg: "some alg"
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.assertions/c2pa.hash.boxes"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
)pb");
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, EncodeManifestWithRedactions) {
FakeUuidGenerator uuid_generator;
SimpleManifestStoreOptions options = {
.uuid_generator = &uuid_generator,
};
absl::StatusOr<std::string> ingredient_manifest_store_bytes =
CreateSimpleManifestStoreBytes(options);
ABSL_ASSERT_OK(ingredient_manifest_store_bytes);
ManifestStoreBuilder manifest_store_builder(&hasher_factory_);
absl::StatusOr<IngredientManifestUris> ingredient_manifest_uris =
manifest_store_builder.AddIngredientManifestStore(
*ingredient_manifest_store_bytes);
ABSL_ASSERT_OK(ingredient_manifest_uris);
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "03020100-0504-0706-0908-0f0e0d0c0b0a",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
software_agent { name: "test generator" }
description: "created for testing"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.redacted"
description: "redacted for testing"
}
}
)pb"),
.ingredient_assertions =
{
ParseTextProtoOrDie(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
)pb"),
},
.manifest_store_builder = manifest_store_builder,
};
std::string redacted_uri =
absl::StrJoin({ingredient_manifest_uris->active_manifest.url(),
kAssertionStoreLabel, kIngredientAssertionV3Label},
kManifestLabelDelimiter);
manifest_box.actions_assertion.mutable_actions()
->mutable_actions(1)
->mutable_parameters()
->set_redacted_uri(redacted_uri);
*manifest_box.ingredient_assertions[0]
.mutable_ingredient_v3()
->mutable_active_manifest() = ingredient_manifest_uris->active_manifest;
manifest_box.ingredient_assertions[0]
.mutable_ingredient_v3()
->mutable_validation_results()
->mutable_active_manifest()
->add_successes()
->set_code("assertion.dataHash.match");
*manifest_box.ingredient_assertions[0]
.mutable_ingredient_v3()
->mutable_claim_signature() = ingredient_manifest_uris->claim_signature;
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "03020100-0504-0706-0908-0f0e0d0c0b0a"
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "test ingredient"
media_type: "video/mp4"
relationship: "parentOf"
instance_id: "test_ingredient"
active_manifest {
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012"
}
validation_results {
active_manifest {
successes { code: "assertion.dataHash.match" }
}
}
claim_signature {
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
description: "test ingredient"
informational_uri: "https://test.com/ingredient"
}
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.opened"
description: "created for testing"
software_agent { name: "test generator" }
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
}
}
actions {
action: "c2pa.redacted"
description: "redacted for testing"
}
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "ingredient.claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.ingredient.v3"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
}
}
ingredient_manifests {
label: "urn:c2pa:12345678-1234-1234-1234-123456789012"
claim {
claim_generator_info { name: "Test C2PA Generator" }
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.boxes"
algorithm: "sha256"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
software_agent { name: "test generator" }
}
}
}
assertions {
label: "c2pa.hash.boxes"
boxes_hash {
boxes { names: "some name" names: "other name" hash: "some hash" }
alg: "some alg"
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.assertions/c2pa.hash.boxes"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:12345678-1234-1234-1234-123456789012/c2pa.signature"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
)pb");
expected_validated_result.mutable_active_manifest()
->mutable_claim()
->add_redacted_assertions(redacted_uri);
expected_validated_result.mutable_active_manifest()
->mutable_assertions(2)
->mutable_actions()
->mutable_actions(1)
->mutable_parameters()
->set_redacted_uri(redacted_uri);
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, EncodesPartHashAssertionsWithRelativePath) {
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "12345678-1234-1234-1234-123456789012",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent { name: "test generator" }
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb"),
.part_hash_assertions = {ParseTextProtoOrDie(
R"pb(
label: "c2pa.hash.bmff.v3.part"
bmff_based_hash {
exclusions { xpath: "" }
}
)pb"),
ParseTextProtoOrDie(
R"pb(
label: "c2pa.hash.bmff.v3.part__1"
bmff_based_hash {
exclusions { xpath: "" }
}
)pb")},
.multi_part_assertion = {ParseTextProtoOrDie(
R"pb(
label: "c2pa.hash.multi-asset"
multi_asset_hash {
parts {
location { byte_offset: 0 length: 10 }
hash_assertion {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part"
algorithm: "sha256"
hash: "some hash"
}
}
parts {
location { byte_offset: 10 length: 10 }
hash_assertion {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part__1"
algorithm: "sha256"
hash: "some hash"
}
}
pad: "\000\000"
pad2: ""
}
)pb")},
.ingredient_assertions = {},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
PartialValidationResultProto expected_validated_result = ParseTextProtoOrDie(
R"pb(
active_manifest {
label: "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a"
claim {
claim_generator_info {
name: "Test generator"
spec_version: "2.2.0"
}
signature_info {
issuer: "OU=Test Unit,O=Test Org,CN=Test Claim Signer"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part__1"
algorithm: "sha256"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.multi-asset"
algorithm: "sha256"
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
instance_id: "12345678-1234-1234-1234-123456789012"
}
assertions {
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
software_agent { name: "test generator" }
}
}
}
assertions {
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.hash.bmff.v3.part"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.hash.bmff.v3.part__1"
bmff_based_hash { exclusions { xpath: "" } }
}
assertions {
label: "c2pa.hash.multi-asset"
multi_asset_hash {
parts {
location { byte_offset: 0 length: 10 }
hash_assertion {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part"
}
}
parts {
location { byte_offset: 10 length: 10 }
hash_assertion {
url: "self#jumbf=c2pa.assertions/c2pa.hash.bmff.v3.part__1"
}
}
pad: "\000\000"
pad2: ""
}
}
validation {
successes {
code: "timeStamp.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.insideValidity"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "claimSignature.validated"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.multi-asset"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.actions.v2"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3.part"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3.part__1"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.signature"
}
}
}
hard_binding_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.bmff.v3"
multi_asset_hash_uri: "self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/c2pa.assertions/c2pa.hash.multi-asset"
)pb");
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
EXPECT_THAT(ValidateClaim(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result.active_manifest().claim())));
EXPECT_THAT(ValidateManifestStore(result_manifest_store_bytes.Flatten()),
IsOkAndHolds(Pointee(EqualsProtoIgnoringFieldsAndOrdering(
expected_validated_result))));
}
TEST_F(ManifestBoxEncoderTest, EncodeManifestWithGatheredAssertions) {
std::string label = "com.example.gathered";
std::string json_data = "{\"test\": \"data\"}";
GatheredAssertion gathered;
gathered.set_label(label);
gathered.set_data(json_data);
gathered.set_content_type(GatheredAssertion::CONTENT_TYPE_JSON);
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "test_instance_id",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent { name: "test generator" }
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb"),
.ingredient_assertions = {},
.gathered_assertions = {gathered},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
absl::Cord result_manifest_store_bytes =
std::move(*built_manifest_store_box).AsCord();
absl::StatusOr<Claim> claim =
ValidateClaim(result_manifest_store_bytes.Flatten());
ABSL_ASSERT_OK(claim);
ASSERT_EQ(claim->gathered_assertions_size(), 1);
EXPECT_EQ(claim->gathered_assertions(0).url(),
"self#jumbf=c2pa.assertions/com.example.gathered");
absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> validated =
ValidateManifestStore(result_manifest_store_bytes.Flatten());
ABSL_ASSERT_OK(validated);
ASSERT_TRUE((*validated)->has_active_manifest());
bool found_match = false;
for (const auto& success :
(*validated)->active_manifest().validation().successes()) {
if (success.code() == "assertion.hashedURI.match" &&
success.url() ==
"self#jumbf=/c2pa/urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a/"
"c2pa.assertions/com.example.gathered") {
found_match = true;
break;
}
}
EXPECT_TRUE(found_match);
}
TEST_F(ManifestBoxEncoderTest,
EncodeManifestWithInvalidGatheredAssertionContentTypeInvalid) {
GatheredAssertion gathered;
gathered.set_label("com.example.gathered");
gathered.set_data("{}");
gathered.set_content_type(GatheredAssertion::CONTENT_TYPE_UNSPECIFIED);
ManifestBox manifest_box = {
.label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a",
.instance_id = "test_instance_id",
.hard_binding_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash { exclusions { xpath: "" } }
)pb"),
.actions_assertion = ParseTextProtoOrDie(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent { name: "test generator" }
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb"),
.ingredient_assertions = {},
.gathered_assertions = {gathered},
.manifest_store_builder = ManifestStoreBuilder(&hasher_factory_),
};
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
EXPECT_THAT(Encode(manifest_box_encoder),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("content type must be JSON")));
}
// Returns the number of non-overlapping occurrences of `needle` in `haystack`.
int CountOccurrences(absl::string_view haystack, absl::string_view needle) {
int count = 0;
for (size_t pos = haystack.find(needle); pos != absl::string_view::npos;
pos = haystack.find(needle, pos + needle.size())) {
++count;
}
return count;
}
TEST_F(ManifestBoxEncoderTest, SaltsAllAssertionsWithSaltGenerator) {
constexpr absl::string_view kSalt = "0123456789abcdef";
FakeSaltGenerator salt_generator{std::string(kSalt)};
GatheredAssertion gathered;
gathered.set_label("com.example.gathered");
gathered.set_data("{}");
gathered.set_content_type(GatheredAssertion::CONTENT_TYPE_JSON);
ManifestBox manifest_box = CreateMinimalManifestBox();
manifest_box.gathered_assertions = {gathered};
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &salt_generator);
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
std::string manifest_store_bytes(
std::move(*built_manifest_store_box).AsCord());
// Hard binding, actions and gathered assertions each get one C2PA salt box
// ('c2sh') containing the salt.
EXPECT_EQ(CountOccurrences(manifest_store_bytes, "c2sh"), 3);
EXPECT_EQ(CountOccurrences(manifest_store_bytes, kSalt), 3);
// Salted assertions must still validate.
absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> validated =
ValidateManifestStore(manifest_store_bytes);
ABSL_ASSERT_OK(validated);
EXPECT_THAT((*validated)->active_manifest().validation().failures(),
IsEmpty());
}
TEST_F(ManifestBoxEncoderTest, DoesNotSaltAssertionsWithNoOpSaltGenerator) {
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &SaltGenerator::NoOp());
absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box =
Encode(manifest_box_encoder);
ABSL_ASSERT_OK(built_manifest_store_box);
std::string manifest_store_bytes(
std::move(*built_manifest_store_box).AsCord());
// No C2PA salt box ('c2sh') is written.
EXPECT_EQ(CountOccurrences(manifest_store_bytes, "c2sh"), 0);
}
TEST_F(ManifestBoxEncoderTest, ReturnsErrorWhenSaltGenerationFails) {
FakeSaltGenerator salt_generator{absl::InternalError("salt failure")};
ManifestBox manifest_box = CreateMinimalManifestBox();
AssertionReferencer assertion_reference(&hasher_factory_);
LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_);
ManifestBoxEncoder manifest_box_encoder(
&assertion_reference, &generator_info_, &claim_signer,
&assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box,
hasher_factory_.algorithm(), &salt_generator);
EXPECT_THAT(Encode(manifest_box_encoder),
StatusIs(absl::StatusCode::kInternal, HasSubstr("salt failure")));
}
} // namespace
} // namespace credentio