blob: 777cd27734cb03c2836da084f0ae9a2cf933401a [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/validator.h"
#include <optional>
#include <string>
#include <vector>
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "cbor/cbor.h"
#include "cbor/parse.h"
#include "constants/labels.h"
#include "constants/status_codes.h"
#include "jumbf/box.h"
#include "proto/assertion.pb.h"
#include "proto/cose_verification_result.pb.h"
#include "proto/generator_info.cbor.h"
#include "proto/generator_info.pb.h"
#include "proto/hashed_uri.pb.h"
#include "proto/manifest.pb.h"
#include "proto/signature_info.pb.h"
#include "uuid/uuid.h"
#include "validator/tracker.h"
namespace credentio {
namespace {
using ::credentio::Uuid;
using ::jumbf::CborBox;
using ::jumbf::SuperBox;
constexpr absl::string_view kSignatureKey = "signature";
constexpr absl::string_view kInstanceIdKey = "instanceID";
constexpr absl::string_view kClaimSignatureUri = "self#jumbf=c2pa.signature";
constexpr absl::string_view kClaimSignatureUriPrefix = "self#jumbf=";
constexpr absl::string_view kClaimSignatureUriSuffix = "c2pa.signature";
constexpr absl::string_view kClaimGeneratorInfoKey = "claim_generator_info";
constexpr absl::string_view kClaimGeneratorV1Key = "claim_generator";
constexpr absl::string_view kCreatedAssertionsKey = "created_assertions";
constexpr absl::string_view kGatheredAssertionsKey = "gathered_assertions";
constexpr absl::string_view kRedactedAssertionsKey = "redacted_assertions";
constexpr absl::string_view kUrlKey = "url";
constexpr absl::string_view kHashKey = "hash";
constexpr absl::string_view kAlgKey = "alg";
absl::StatusOr<absl::string_view> ExtractCbor(const SuperBox* box) {
// Check if the box only contains a single CBOR content type.
if (box->contents.size() != 1) {
return absl::InvalidArgumentError(
"box contains zero or more than one contents");
}
const auto& content = box->contents[0];
if (!content.Holds<CborBox>()) {
return absl::InvalidArgumentError("box does not contain a CBOR content");
}
return content.Get<CborBox>().payload;
}
// See hashed-uri-map in
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_uri_references.
struct HashedUriInternal {
absl::string_view url;
absl::string_view hash;
std::optional<absl::string_view> algorithm;
HashedUri ToProto(std::optional<absl::string_view> default_algorithm) const {
HashedUri hashed_uri;
hashed_uri.set_url(url);
hashed_uri.set_hash(hash);
hashed_uri.set_algorithm(
algorithm.value_or(default_algorithm.value_or("")));
return hashed_uri;
}
};
std::vector<HashedUriInternal> ParseHashedUriArray(
const cbor::ArrayView& uri_array, ValidationTracker& validation_tracker) {
std::vector<HashedUriInternal> assertions;
for (int i = 0; i < uri_array.size(); ++i) {
auto uri_map = uri_array.GetMap(i);
if (!uri_map.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kHashedUriMissing,
{});
continue;
}
auto url = uri_map->GetString(kUrlKey);
if (!url.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kHashedUriMissing,
{});
continue;
}
auto hash = uri_map->GetByteString(kHashKey);
if (!hash.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kHashedUriMissing,
{.url = *url});
continue;
}
auto algorithm = uri_map->GetOptionalString(kAlgKey);
assertions.push_back(
HashedUriInternal{.url = *url, .hash = *hash, .algorithm = algorithm});
}
return assertions;
}
GeneratorInfo CreateGeneratorInfo(const cbor::MapView& cbor_map,
absl::string_view claim_box_label,
ValidationTracker& validation_tracker) {
GeneratorInfo generator_info;
auto holder = cbor_map.GetOptionalMap(kClaimGeneratorInfoKey);
if (holder.has_value()) {
auto info_map = *holder;
auto status = cbor::ToProto(info_map, &generator_info);
if (!status.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimCborInvalid,
{.url = claim_box_label});
}
} else if (auto v1_generator =
cbor_map.GetOptionalString(kClaimGeneratorV1Key);
v1_generator.has_value()) {
// The v1 `claim_generator` is formatted as a User-Agent string, but
// that's at least arguably a "human readable string naming the claim
// generator" as the `name` field is described.
generator_info.set_name(*v1_generator);
} else {
validation_tracker.RecordFailure(
FailureStatusCode::kClaimMalformed,
{.url = claim_box_label,
.explanation = "Claim is missing generator info"});
}
return generator_info;
}
struct CreateClaimOptions {
const std::string label;
const cbor::MapView& claim_map;
const absl::Span<HashedUri> created_assertions;
const absl::Span<HashedUri> gathered_assertions;
const absl::Span<std::string> redacted_assertions;
const std::optional<SignatureInfo> signature_info;
std::optional<std::string> default_algorithm;
};
Claim CreateClaim(const CreateClaimOptions& options,
ValidationTracker& validation_tracker) {
// Construct the claim proto.
Claim claim;
claim.set_label(options.label);
// Add instance ID.
std::optional<absl::string_view> instance_id =
options.claim_map.GetOptionalString(kInstanceIdKey);
if (instance_id.has_value()) {
claim.set_instance_id(*instance_id);
} else {
validation_tracker.RecordFailure(
FailureStatusCode::kClaimMalformed,
{.url = options.label, .explanation = "Claim is missing instance ID"});
}
// Add signature info.
if (options.signature_info.has_value()) {
*claim.mutable_signature_info() = *options.signature_info;
}
// Add generator info.
*claim.mutable_claim_generator_info() =
CreateGeneratorInfo(options.claim_map, options.label, validation_tracker);
// Add assertion URIs
claim.mutable_created_assertions()->Add(options.created_assertions.begin(),
options.created_assertions.end());
claim.mutable_gathered_assertions()->Add(options.gathered_assertions.begin(),
options.gathered_assertions.end());
claim.mutable_redacted_assertions()->Add(options.redacted_assertions.begin(),
options.redacted_assertions.end());
if (options.default_algorithm.has_value()) {
claim.set_default_algorithm(*options.default_algorithm);
}
return claim;
}
bool IsClaimBox(absl::string_view label, const Uuid& uuid) {
if (label == kClaimV2Label && uuid == kClaimUuid) {
return true;
}
if (label == kClaimV1Label && uuid == kClaimUuid) {
return true;
}
return false;
}
bool IsLegacyClaimBox(absl::string_view label, const Uuid& uuid) {
return label == kClaimV1Label && uuid == kClaimUuid;
}
// Checks if the signature URI is a self#jumbf location in the current manifest
// that is being processed (provided in the `manifest` parameter).
bool IsValidSignatureUri(absl::string_view uri,
const jumbf::SuperBox& manifest) {
// Check if the signature URI is a relative URI first for efficiency.
if (uri == kClaimSignatureUri) {
return true;
}
return manifest.description.label.has_value() &&
uri == absl::StrCat(kClaimSignatureUriPrefix, "/c2pa/",
*manifest.description.label, "/",
kClaimSignatureUriSuffix);
}
} // namespace
std::optional<Claim> ClaimValidatorImpl::Validate(
const jumbf::SuperBox& manifest,
ValidationTracker& validation_tracker) const {
std::optional<const jumbf::SuperBox*> claim_box;
std::string claim_box_label = "";
std::optional<const jumbf::SuperBox*> claim_signature_box;
// Parse manifest boxes to find the claim and claim signature boxes.
for (const auto& box : manifest.contents) {
if (box.Holds<SuperBox>()) {
const auto& description = box.Get<SuperBox>().description;
absl::string_view label = description.label.value_or("");
Uuid uid = description.type_uuid;
if (IsLegacyClaimBox(label, uid)) {
validation_tracker.RecordFailure(
FailureStatusCode::kGoogleUnsupportedSpecVersion,
{.url = label, .explanation = "Unsupported claim version"});
return std::nullopt;
}
if (IsClaimBox(label, uid)) {
if (claim_box.has_value()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimMultiple,
{.url = label});
}
if (box.Holds<SuperBox>()) {
claim_box = &box.Get<SuperBox>();
claim_box_label = label;
}
} else if (label == kClaimSignatureLabel && uid == kClaimSignatureUuid) {
if (box.Holds<SuperBox>()) {
claim_signature_box = &box.Get<SuperBox>();
}
}
}
}
// Claim box should be present and it shall consist of a single CBOR content
// type. Extract the CBOR content type from the claim box if these
// conditions are met, otherwise update the failure status.
absl::string_view claim_cbor = "";
if (!claim_box.has_value()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimMissing,
{.url = claim_box_label});
} else {
auto result = ExtractCbor(*claim_box);
if (!result.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimCborInvalid,
{.url = claim_box_label});
} else {
claim_cbor = *result;
}
}
// Parse the claim CBOR as a map and validate the signature field.
// The signature URI must refer to a location within the same C2PA Manifest
// box (a self#jumbf location). Moreover, since the claim signature label
// must be `c2pa.signature`, this implies that the signature URI should be
// equal to `self#jumbf:c2pa.signature`.
AssertionUris assertion_uris;
std::optional<cbor::MapView> claim_map;
auto claim_result = cbor::Parse(claim_cbor);
if (!claim_result.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimCborInvalid,
{.url = claim_box_label});
} else {
auto claim_map_result = (*claim_result)->AsMap();
if (!claim_map_result.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimCborInvalid,
{.url = claim_box_label});
} else {
// Validate the signature field.
claim_map = *claim_map_result;
auto signature_uri = claim_map->GetString(kSignatureKey);
if (!signature_uri.ok()) {
validation_tracker.RecordFailure(
FailureStatusCode::kClaimMalformed,
{.url = claim_box_label,
.explanation = "Claim is missing signature URI"});
} else if (!IsValidSignatureUri(*signature_uri, manifest)) {
validation_tracker.RecordFailure(
FailureStatusCode::kClaimSignatureMissing,
{.url = kClaimSignatureLabel});
}
// Extract the optional `alg` field.
assertion_uris.default_algorithm = claim_map->GetOptionalString(kAlgKey);
// Extract the required `created_assertions` field.
assertion_uris.created_assertions =
claim_map->GetOptionalArray(kCreatedAssertionsKey);
// Extract the optional `gathered_assertions` field.
assertion_uris.gathered_assertions =
claim_map->GetOptionalArray(kGatheredAssertionsKey);
// Extract the optional `redacted_assertions` field
assertion_uris.redacted_assertions =
claim_map->GetOptionalArray(kRedactedAssertionsKey);
// Confirm that the `created_assertions` field is set. This field is
// required in c2pa.claim.v2.
if (!assertion_uris.created_assertions.has_value()) {
validation_tracker.RecordFailure(
FailureStatusCode::kClaimMalformed,
{.url = claim_box_label,
.explanation = "Claim is missing created assertions"});
}
}
}
// Claim signature box should be present and it shall consist of a single
// CBOR content type. Extract the CBOR content type from the claim signature
// box if these conditions are met, otherwise update the failure status.
std::optional<absl::string_view> claim_signature_cbor;
if (!claim_signature_box.has_value()) {
validation_tracker.RecordFailure(FailureStatusCode::kClaimSignatureMissing,
{.url = kClaimSignatureLabel});
} else {
auto result = ExtractCbor(*claim_signature_box);
if (!result.ok()) {
validation_tracker.RecordFailure(
FailureStatusCode::kClaimSignatureMissing,
{.url = kClaimSignatureLabel});
} else {
claim_signature_cbor = *result;
}
}
std::optional<SignatureInfo> signature_info;
if (claim_signature_cbor.has_value() && !claim_cbor.empty()) {
CoseVerificationResult verification_result =
cose_verifier_.Verify(*claim_signature_cbor, claim_cbor);
if (verification_result.has_signature_info()) {
signature_info = verification_result.signature_info();
}
validation_tracker.MergeStatuses(verification_result.validation_status());
if (!verification_result.has_signature_info() &&
!validation_tracker.HasFailures()) {
LOG_EVERY_N_SEC(ERROR, 60) << "Signature verification "
"failed but no failure was recorded";
validation_tracker.RecordFailure(FailureStatusCode::kGoogleInternalError,
{.url = kClaimSignatureLabel});
}
} else {
validation_tracker.RecordFailure(FailureStatusCode::kClaimSignatureMissing,
{.url = kClaimSignatureLabel});
}
ClaimValidatorImpl::ValidateAssertionResult validate_assertion_result;
if (claim_box.has_value()) {
validate_assertion_result =
ValidateAssertions(assertion_uris, validation_tracker);
}
if (validation_tracker.HasFailures()) {
return std::nullopt;
}
auto options = CreateClaimOptions{
.label =
claim_box.has_value()
? std::string(claim_box.value()->description.label.value_or(""))
: "",
.claim_map = *claim_map,
.created_assertions =
absl::MakeSpan(validate_assertion_result.created_assertions),
.gathered_assertions =
absl::MakeSpan(validate_assertion_result.gathered_assertions),
.redacted_assertions =
absl::MakeSpan(validate_assertion_result.redacted_assertions),
.signature_info = signature_info,
.default_algorithm = assertion_uris.default_algorithm,
};
return CreateClaim(options, validation_tracker);
}
ClaimValidatorImpl::ValidateAssertionResult
ClaimValidatorImpl::ValidateAssertions(
const AssertionUris& assertion_uris,
ValidationTracker& validation_tracker) const {
// Parse CBOR maps in hashed URI arrays
std::vector<HashedUriInternal> created_assertions, gathered_assertions;
ClaimValidatorImpl::ValidateAssertionResult result;
if (assertion_uris.created_assertions.has_value()) {
created_assertions = ParseHashedUriArray(*assertion_uris.created_assertions,
validation_tracker);
}
if (assertion_uris.gathered_assertions.has_value()) {
gathered_assertions = ParseHashedUriArray(
*assertion_uris.gathered_assertions, validation_tracker);
}
for (const auto& a : created_assertions) {
result.created_assertions.push_back(
a.ToProto(assertion_uris.default_algorithm));
}
for (const auto& a : gathered_assertions) {
result.gathered_assertions.push_back(
a.ToProto(assertion_uris.default_algorithm));
}
std::vector<std::string> redacted_assertions;
if (assertion_uris.redacted_assertions.has_value()) {
for (int i = 0; i < assertion_uris.redacted_assertions->size(); ++i) {
auto uri = assertion_uris.redacted_assertions->GetString(i);
if (!uri.ok()) {
validation_tracker.RecordFailure(FailureStatusCode::kHashedUriMissing,
{});
} else {
redacted_assertions.push_back(std::string(*uri));
}
}
}
result.redacted_assertions = redacted_assertions;
return result;
}
} // namespace credentio