blob: cc948c102538bc3211ec1b2daf1ebe576eee4020 [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/validator.h"
#include <optional>
#include <string>
#include <utility>
#include "absl/log/check.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/cord.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "constants/labels.h"
#include "constants/status_codes.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "jumbf/box_builder.h"
#include "jumbf/constants.h"
#include "jumbf/encode_params.h"
#include "proto/cose_verification_result.pb.h"
#include "proto/manifest.pb.h"
#include "proto/signature_info.pb.h"
#include "testing/cbor_utils.h"
#include "testing/jumbf_utils.h"
#include "testing/mock_cose_verifier.h"
#include "testing/proto_test_utils.h"
#include "testing/test_validation_tracker.h"
#include "uuid/uuid.h"
#include "validator/tracker.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::cbor::FromJson;
using ::credentio_testing::EqualsProto;
using ::credentio_testing::ParseTextProtoOrDie;
using ::jumbf::SuperBoxBuilder;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Optional;
using ::testing::Return;
using ::testing::SizeIs;
struct ClaimBoxOptions {
std::string signature;
};
jumbf::BuiltSuperBox CreateClaimBox(const ClaimBoxOptions& options) {
auto cbor = FromJson(absl::Substitute(
R"json({"signature": "$0", "created_assertions": []})json",
options.signature));
SuperBoxBuilder builder(kClaimUuid, jumbf::DescriptionBoxOptions{
.label = absl::Cord(kClaimV2Label)});
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord(cbor)));
auto built_box = std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
jumbf::BuiltSuperBox CreateClaimBoxFromJson(absl::string_view json) {
auto cbor = FromJson(json);
SuperBoxBuilder builder(kClaimUuid, jumbf::DescriptionBoxOptions{
.label = absl::Cord(kClaimV2Label)});
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord(cbor)));
absl::StatusOr<jumbf::BuiltSuperBox> built_box =
std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
jumbf::BuiltSuperBox CreateOldClaimBoxFromJson(absl::string_view json) {
auto cbor = FromJson(json);
SuperBoxBuilder builder(kClaimUuid, jumbf::DescriptionBoxOptions{
.label = absl::Cord(kClaimV1Label)});
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord(cbor)));
absl::StatusOr<jumbf::BuiltSuperBox> box = std::move(builder).Finalize();
CHECK_OK(box);
return *std::move(box);
}
jumbf::BuiltSuperBox CreateClaimBoxFromCbor(absl::string_view cbor) {
SuperBoxBuilder builder(kClaimUuid, jumbf::DescriptionBoxOptions{
.label = absl::Cord(kClaimV2Label)});
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord(cbor)));
auto built_box = std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
jumbf::BuiltSuperBox CreateClaimBoxWithMultipleContents(
absl::string_view cbor, absl::string_view other_cbor) {
SuperBoxBuilder builder(kClaimUuid, jumbf::DescriptionBoxOptions{
.label = absl::Cord(kClaimV2Label)});
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord(cbor)));
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord(other_cbor)));
auto built_box = std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
jumbf::BuiltSuperBox CreateClaimSignatureBox(
absl::string_view cose_certificate) {
SuperBoxBuilder builder(
kClaimSignatureUuid,
jumbf::DescriptionBoxOptions{.label = absl::Cord(kClaimSignatureLabel)});
CHECK_OK(
builder.AddContent(jumbf::kCborBoxType, absl::Cord(cose_certificate)));
absl::StatusOr<jumbf::BuiltSuperBox> built_box =
std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
jumbf::BuiltSuperBox CreateClaimSignatureBoxWithMultipleContents(
absl::string_view cose_certificate,
absl::string_view other_cose_certificate) {
SuperBoxBuilder builder(
kClaimSignatureUuid,
jumbf::DescriptionBoxOptions{.label = absl::Cord(kClaimSignatureLabel)});
CHECK_OK(
builder.AddContent(jumbf::kCborBoxType, absl::Cord(cose_certificate)));
CHECK_OK(builder.AddContent(jumbf::kCborBoxType,
absl::Cord(other_cose_certificate)));
auto built_box = std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
jumbf::BuiltSuperBox CreateAssertionStoreBox() {
SuperBoxBuilder builder(
kAssertionStoreUuid,
jumbf::DescriptionBoxOptions{.label = absl::Cord(kAssertionStoreLabel)});
CHECK_OK(builder.AddContent(jumbf::kCborBoxType, absl::Cord("foo")));
auto built_box = std::move(builder).Finalize();
CHECK_OK(built_box);
return *std::move(built_box);
}
SuperBoxBuilder CreateManifestBuilder() {
return SuperBoxBuilder(
kStandardManifestUuid,
jumbf::DescriptionBoxOptions{.label = absl::Cord("c2pa.manifest")});
}
std::optional<Claim> ValidateWithBuilder(const ClaimValidatorImpl& validator,
SuperBoxBuilder builder,
ValidationTracker& tracker) {
auto built_super_box = std::move(builder).Finalize();
CHECK_OK(built_super_box);
std::string manifest_bytes =
std::string(std::move(*built_super_box).AsCord());
return validator.Validate(ParseSuperBoxOrDie(&manifest_bytes), tracker);
}
TEST(ValidateClaimTest, ClaimMissing) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateClaimSignatureBox("some cbor")), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimSignatureMissing) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromCbor("some cbor")), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimSignatureMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, BothClaimAndSignatureMissing) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
auto failures = t.GetFailures();
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMissing));
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimSignatureMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, MultipleClaims) {
auto manifest = CreateManifestBuilder();
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromCbor("some cbor")), IsOk());
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromCbor("some other cbor")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateClaimSignatureBox("some cbor")), IsOk());
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMultiple));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimHasMultipleContents) {
auto manifest = CreateManifestBuilder();
auto claim_box =
CreateClaimBoxWithMultipleContents("some cbor", "some other cbor");
ASSERT_THAT(manifest.AddChild(std::move(claim_box)), IsOk());
ASSERT_THAT(manifest.AddChild(CreateClaimSignatureBox("some cbor")), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimCborInvalid));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimIsNotCborBox) {
auto manifest = CreateManifestBuilder();
SuperBoxBuilder claim_box(
kClaimUuid,
jumbf::DescriptionBoxOptions{.label = absl::Cord(kClaimV2Label)});
SuperBoxBuilder child_box(kClaimSignatureUuid, {});
ASSERT_THAT(child_box.AddContent(jumbf::kCborBoxType, absl::Cord("foo")),
IsOk());
ASSERT_THAT(claim_box.AddChild(std::move(child_box)), IsOk());
ASSERT_THAT(manifest.AddChild(std::move(claim_box)), IsOk());
ASSERT_THAT(manifest.AddChild(CreateClaimSignatureBox("some cbor")), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimCborInvalid));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimSignatureHasMultipleContents) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromCbor("some cbor")), IsOk());
auto claim_signature_box = CreateClaimSignatureBoxWithMultipleContents(
"some cbor", "some other cbor");
ASSERT_THAT(manifest.AddChild(std::move(claim_signature_box)), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimSignatureMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimSignatureIsNotCborBox) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromCbor("some_cbor")), IsOk());
SuperBoxBuilder claim_signature_box(
kClaimSignatureUuid,
jumbf::DescriptionBoxOptions{.label = absl::Cord(kClaimSignatureLabel)});
SuperBoxBuilder child_box(kClaimUuid, {});
ASSERT_THAT(child_box.AddContent(jumbf::kCborBoxType, absl::Cord("foo")),
IsOk());
ASSERT_THAT(claim_signature_box.AddChild(std::move(child_box)), IsOk());
ASSERT_THAT(manifest.AddChild(std::move(claim_signature_box)), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimSignatureMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimCborIsNotAMap) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(R"json([1, 2, 3])json")),
IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimCborInvalid));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimHasIncorrectSignature) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(CreateClaimBox({.signature = "not_valid"})),
IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimSignatureMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, CoseVerificationFails) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(
CreateClaimBox({.signature = "self#jumbf=c2pa.signature"})),
IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce([](absl::string_view cose_certificate,
absl::string_view claim_cbor) {
CoseVerificationResult result;
result.mutable_validation_status()->mutable_failures()->Add()->set_code(
absl::StrCat(FailureStatusCode::kGeneralError));
return result;
});
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(result, Eq(std::nullopt));
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kGeneralError));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
}
TEST(ValidateClaimTest, CoseVerifierFailsWithoutRecordingStatus) {
auto manifest = CreateManifestBuilder();
ASSERT_THAT(manifest.AddChild(
CreateClaimBox({.signature = "self#jumbf=c2pa.signature"})),
IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(result, Eq(std::nullopt));
// Internal error recorded because the mock verifier didn't record one.
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kGoogleInternalError));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
}
TEST(ValidateClaimTest, CreatedAssertionIsNotMap) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"signature": "self#jumbf=c2pa.signature",
"created_assertions": [{"url":"self#jumbf=c2pa.some_assertion"}],
"redacted_assertions": ["self#jumbf=c2pa.some_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, AssertionUriIsNotString) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"signature": "self#jumbf=c2pa.signature",
"created_assertions": [{
"url": 12,
"hash": "b64'abcd'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, HashedUriMissingRequiredFieldInCreated) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"signature": "self#jumbf=c2pa.signature",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_assertion"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, HashedUriMissingRequiredFieldInGathered) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"signature": "self#jumbf=c2pa.signature",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_assertion",
"hash": "b64'abcd'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_assertion"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, HashedUriMissingRequiredFieldInRedacted) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"signature": "self#jumbf=c2pa.signature",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_assertion",
"hash": "b64'abcd'"
}],
"redacted_assertions": [12]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMissing));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimV1ProvidedWithClaimV2Label) {
auto manifest = CreateManifestBuilder();
// Note use of v1 `assertions`.
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"dc:format": "image/jpeg",
"assertions": [
{"url": "self#jumbf=c2pa.some_created_assertion", "hash": "b64'aGVsbG8'"},
{"url": "self#jumbf=c2pa.some_gathered_assertion", "hash": "b64'aGVsbG8'"}
]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)),
IsOk()); // Uses c2pa.claim.v2 label.
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailureStatuses(),
Contains(C2paStatusIs(FailureStatusCode::kClaimMalformed,
"Claim is missing created assertions")));
}
TEST(ValidateClaimTest, ClaimV1Unsupported) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"dc:format": "image/jpeg",
"assertions": [
{"url": "self#jumbf=c2pa.some_created_assertion", "hash": "b64'aGVsbG8'"},
{"url": "self#jumbf=c2pa.some_gathered_assertion", "hash": "b64'aGVsbG8'"}
],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateOldClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kGoogleUnsupportedSpecVersion));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimV2HasNoSignature) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"dc:format": "image/jpeg",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
EXPECT_CALL(mock_cose_verifier, Verify)
.WillOnce(Return(CoseVerificationResult()));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMalformed));
EXPECT_THAT(t.GetSuccesses(), IsEmpty());
EXPECT_THAT(result, Eq(std::nullopt));
}
TEST(ValidateClaimTest, ClaimV2HasNoInstanceId) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"dc:format": "image/jpeg",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMalformed));
EXPECT_THAT(
result, Optional(EqualsProto(ParseTextProtoOrDie<Claim>(R"pb(
claim_generator_info {
name: "test_generator"
version: "1.0"
icon { url: "some_icon_link" hash: "hello" }
operating_system: "linux"
}
signature_info { cert_serial_number: "1234" issuer: "fake_issuer" }
created_assertions {
url: "self#jumbf=c2pa.some_created_assertion"
algorithm: ""
hash: "hello"
}
gathered_assertions {
url: "self#jumbf=c2pa.some_gathered_assertion"
algorithm: ""
hash: "hello"
}
label: "c2pa.claim.v2"
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb"))));
}
TEST(ValidateClaimTest, ClaimV2HasNoGeneratorInfo) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"dc:format": "image/jpeg",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMalformed));
EXPECT_THAT(
result, Optional(EqualsProto(ParseTextProtoOrDie<Claim>(R"pb(
instance_id: "1234"
label: "c2pa.claim.v2"
signature_info { cert_serial_number: "1234" issuer: "fake_issuer" }
claim_generator_info {}
created_assertions {
url: "self#jumbf=c2pa.some_created_assertion"
algorithm: ""
hash: "hello"
}
gathered_assertions {
url: "self#jumbf=c2pa.some_gathered_assertion"
algorithm: ""
hash: "hello"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb"))));
}
TEST(ValidateClaimTest, ClaimV2ProvidedInLegacyBox) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateOldClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kGoogleUnsupportedSpecVersion));
}
TEST(ValidateClaimTest, ClaimV2WithDcFormat) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"dc:format": "image/jpeg",
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
// We ignore `dc:format` fields in non-legacy claim boxes.
EXPECT_THAT(t.GetFailures(), IsEmpty());
}
TEST(ValidateClaimTest, ClaimV2LegacyAssertionsIgnored) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"assertions": [
{"url": "self#jumbf=c2pa.some_created_assertion", "hash": "b64'aGVsbG8'"},
{"url": "self#jumbf=c2pa.some_gathered_assertion", "hash": "b64'aGVsbG8'"}
],
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(), IsEmpty());
ASSERT_TRUE(result.has_value());
EXPECT_THAT(result->created_assertions(), SizeIs(1));
EXPECT_THAT(result->gathered_assertions(), SizeIs(1));
}
TEST(ValidateClaimTest, ClaimV2MissingSignature) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMalformed));
}
TEST(ValidateClaimTest, ClaimV2MissingCreatedAssertions) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimMalformed));
}
TEST(ValidateClaimTest, ClaimV2Ok) {
auto manifest = CreateManifestBuilder();
absl::string_view claim_json = R"json({
"instanceID": "1234",
"signature": "self#jumbf=c2pa.signature",
"signature_info": {
"cert_serial_number": "1234",
"issuer": "fake_issuer"
},
"claim_generator_info": {
"name": "test_generator",
"version": "1.0",
"icon": {
"url": "some_icon_link",
"hash": "b64'aGVsbG8'"
},
"operating_system": "linux"
},
"created_assertions": [{
"url": "self#jumbf=c2pa.some_created_assertion",
"hash": "b64'aGVsbG8'"
}],
"gathered_assertions": [{
"url": "self#jumbf=c2pa.some_gathered_assertion",
"hash": "b64'aGVsbG8'"
}],
"redacted_assertions": ["self#jumbf=c2pa.some_redacted_assertion"]
})json";
ASSERT_THAT(manifest.AddChild(CreateClaimBoxFromJson(claim_json)), IsOk());
ASSERT_THAT(
manifest.AddChild(CreateClaimSignatureBox("fake_cose_certificate")),
IsOk());
ASSERT_THAT(manifest.AddChild(CreateAssertionStoreBox()), IsOk());
MockCoseVerifier mock_cose_verifier;
SignatureInfo signature_info;
signature_info.set_issuer("fake_issuer");
signature_info.set_cert_serial_number("1234");
CoseVerificationResult verification_result;
*verification_result.mutable_signature_info() = signature_info;
EXPECT_CALL(mock_cose_verifier, Verify).WillOnce(Return(verification_result));
TestValidationTracker t;
ClaimValidatorImpl validator(&mock_cose_verifier);
auto result =
ValidateWithBuilder(validator, std::move(manifest), t.tracker());
EXPECT_THAT(t.GetFailures(), IsEmpty());
EXPECT_THAT(
result, Optional(EqualsProto(ParseTextProtoOrDie<Claim>(R"pb(
instance_id: "1234"
claim_generator_info {
name: "test_generator"
version: "1.0"
icon { url: "some_icon_link" hash: "hello" }
operating_system: "linux"
}
signature_info { cert_serial_number: "1234" issuer: "fake_issuer" }
created_assertions {
url: "self#jumbf=c2pa.some_created_assertion"
algorithm: ""
hash: "hello"
}
gathered_assertions {
url: "self#jumbf=c2pa.some_gathered_assertion"
algorithm: ""
hash: "hello"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
label: "c2pa.claim.v2"
)pb"))));
}
} // namespace
} // namespace credentio