blob: 12969d98a4e7fd23dc5ad9cd286458de2d8e6e56 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "claim/manifest_box_encoder.h"
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/functional/any_invocable.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/cord.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "assertion/assertion_cbor_encoder.h"
#include "assertion/assertion_referencer.h"
#include "async/async_context.h"
#include "claim/signer.h"
#include "constants/labels.h"
#include "jumbf/box_builder.h"
#include "jumbf/constants.h"
#include "jumbf/encode_params.h"
#include "jumbf/utils.h"
#include "proto/actions_assertion.pb.h"
#include "proto/assertion.pb.h"
#include "proto/generator_info.pb.h"
#include "proto/hashed_uri.pb.h"
#include "proto/manifest.pb.h"
#include "proto/multi_asset_hash_assertion.pb.h"
#include "salt/salt_generator.h"
#include "uuid/uuid.h"
namespace credentio {
namespace {
absl::StatusOr<jumbf::BuiltSuperBox> SerializeAssertionStore(
std::vector<jumbf::BuiltSuperBox> assertions) {
jumbf::SuperBoxBuilder assertion_store_builder(
kAssertionStoreUuid, {.label = absl::Cord(kAssertionStoreLabel)});
for (jumbf::BuiltSuperBox& assertion_box : assertions) {
ABSL_RETURN_IF_ERROR(
assertion_store_builder.AddChild(std::move(assertion_box)));
}
return std::move(assertion_store_builder).Finalize();
}
absl::StatusOr<jumbf::BuiltSuperBox> SerializeCborSuperBox(
const Uuid& uuid, absl::string_view label, absl::Cord cbor_content) {
jumbf::SuperBoxBuilder builder(uuid, {.label = absl::Cord(label)});
ABSL_RETURN_IF_ERROR(
builder.AddContent(jumbf::kCborBoxType, std::move(cbor_content)));
return std::move(builder).Finalize();
}
// Returns description box options for an assertion superbox labeled `label`.
// If `salt_generator` produces a non-empty salt, the salt is embedded as a C2PA
// salt box ('c2sh') in the description box private content, per C2PA
// specification v2.2 sections 8.4.2.3 and 11.1.4.1.2.
absl::StatusOr<jumbf::DescriptionBoxOptions> MakeSaltedDescriptionBoxOptions(
absl::string_view label, const SaltGenerator& salt_generator) {
jumbf::DescriptionBoxOptions options = {.label = absl::Cord(label)};
ABSL_ASSIGN_OR_RETURN(std::string salt, salt_generator.Generate());
if (salt.empty()) {
return options;
}
ABSL_ASSIGN_OR_RETURN(options.private_content,
jumbf::CreateC2paSaltBox(salt));
return options;
}
// Encodes `assertion` to CBOR with `encoder` and wraps it in a CBOR content
// type JUMBF superbox labeled with the assertion label and salted with
// `salt_generator`.
absl::StatusOr<jumbf::BuiltSuperBox> SerializeAssertionSuperBox(
const AssertionCborEncoder& encoder, const Assertion& assertion,
const SaltGenerator& salt_generator) {
ABSL_ASSIGN_OR_RETURN(absl::Cord cbor, encoder.ToCbor(assertion));
ABSL_ASSIGN_OR_RETURN(
jumbf::DescriptionBoxOptions description_options,
MakeSaltedDescriptionBoxOptions(assertion.label(), salt_generator));
jumbf::SuperBoxBuilder builder(jumbf::kCborBoxTypeUuid,
std::move(description_options));
ABSL_RETURN_IF_ERROR(
builder.AddContent(jumbf::kCborBoxType, std::move(cbor)));
return std::move(builder).Finalize();
}
struct SignatureBoxResult {
jumbf::BuiltSuperBox box;
};
void MakeSignatureBox(
absl::string_view claim_cbor, const ClaimSigner& claim_signer,
const ClaimSigner::Options& options,
absl::AnyInvocable<void(absl::StatusOr<SignatureBoxResult>) &&> cb) {
AsyncResolver<SignatureBoxResult> resolver(options.async_context,
std::move(cb));
auto done_cb =
[resolver = std::move(resolver)](
absl::StatusOr<ClaimSigner::SignResult> signature_status) mutable {
if (!signature_status.ok()) {
resolver.Return(signature_status.status());
return;
}
absl::StatusOr<jumbf::BuiltSuperBox> signature_box =
SerializeCborSuperBox(
kClaimSignatureUuid, kClaimSignatureLabel,
absl::Cord(std::move(signature_status->signature)));
if (!signature_box.ok()) {
resolver.Return(signature_box.status());
return;
}
resolver.Return(SignatureBoxResult{
.box = std::move(*signature_box),
});
};
claim_signer.Sign(claim_cbor, options, std::move(done_cb));
}
} // namespace
absl::Status ManifestBoxEncoder::PopulateMultiAssetPartHashes(
MultiAssetHashAssertion* assertion) const {
for (auto& part : *assertion->mutable_parts()) {
absl::string_view url = part.hash_assertion().url();
auto it = assertion_url_to_index_.find(url);
if (it == assertion_url_to_index_.end()) {
return absl::InternalError(
absl::StrCat("Could not find hash for url: ", url));
}
*part.mutable_hash_assertion() = assertion_hashed_uris_[it->second];
}
return absl::OkStatus();
}
absl::Status ManifestBoxEncoder::AddAssertion(Assertion& assertion) {
if (assertion.has_multi_asset_hash()) {
ABSL_RETURN_IF_ERROR(
PopulateMultiAssetPartHashes(assertion.mutable_multi_asset_hash()));
}
ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_box,
SerializeAssertionSuperBox(assertion_cbor_encoder_,
assertion, salt_generator_));
ABSL_ASSIGN_OR_RETURN(HashedUri assertion_hashed_uri,
assertion_reference_.Generate(assertion_box));
assertion_url_to_index_[assertion_hashed_uri.url()] =
assertion_hashed_uris_.size();
assertion_hashed_uris_.Add(std::move(assertion_hashed_uri));
assertion_boxes_.push_back(std::move(assertion_box));
return absl::OkStatus();
}
absl::Status ManifestBoxEncoder::AddActionsAssertion(Assertion assertion) {
for (ActionItem& action : *assertion.mutable_actions()->mutable_actions()) {
if (action.action() == "c2pa.opened" &&
action.parameters().ingredients_size() != 1) {
return absl::InvalidArgumentError(
"Opened action must have exactly one ingredient.");
}
if (action.parameters().ingredients_size() != 0) {
for (auto& ingredient :
*action.mutable_parameters()->mutable_ingredients()) {
auto it = assertion_url_to_index_.find(ingredient.url());
if (it == assertion_url_to_index_.end()) {
return absl::InternalError(absl::StrCat(
"Could not find hash for ingredient url: ", ingredient.url()));
}
ingredient = assertion_hashed_uris_[it->second];
}
}
if (action.parameters().has_redacted_uri()) {
redacted_assertions_.emplace_back(action.parameters().redacted_uri());
}
}
return AddAssertion(assertion);
}
absl::Status ManifestBoxEncoder::AddGatheredAssertion(
const GatheredAssertion& gathered_assertion) {
if (gathered_assertion.content_type() !=
GatheredAssertion::CONTENT_TYPE_JSON) {
return absl::InvalidArgumentError(
"Gathered assertion content type must be JSON.");
}
ABSL_ASSIGN_OR_RETURN(jumbf::DescriptionBoxOptions description_options,
MakeSaltedDescriptionBoxOptions(
gathered_assertion.label(), salt_generator_));
jumbf::SuperBoxBuilder builder(jumbf::kJsonBoxTypeUuid,
std::move(description_options));
ABSL_RETURN_IF_ERROR(builder.AddContent(
jumbf::kJsonBoxType, absl::Cord(gathered_assertion.data())));
ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_box,
std::move(builder).Finalize());
ABSL_ASSIGN_OR_RETURN(HashedUri assertion_hashed_uri,
assertion_reference_.Generate(assertion_box));
gathered_assertion_hashed_uris_.Add(std::move(assertion_hashed_uri));
assertion_boxes_.push_back(std::move(assertion_box));
return absl::OkStatus();
}
absl::Status ManifestBoxEncoder::AddAssertions() {
// Note that we must add an assertion before adding any other assertion that
// references it, so the referenced assertion's hash is available. So, for
// example, ingredient assertions must be added before actions assertions
// since some actions assertions reference ingredient assertions.
for (Assertion& assertion : manifest_.ingredient_assertions) {
ABSL_RETURN_IF_ERROR(AddAssertion(assertion));
}
ABSL_RETURN_IF_ERROR(AddActionsAssertion(manifest_.actions_assertion));
ABSL_RETURN_IF_ERROR(AddAssertion(manifest_.hard_binding_assertion));
for (Assertion& assertion : manifest_.part_hash_assertions) {
ABSL_RETURN_IF_ERROR(AddAssertion(assertion));
}
if (manifest_.multi_part_assertion.has_value()) {
ABSL_RETURN_IF_ERROR(AddAssertion(*manifest_.multi_part_assertion));
}
for (const auto& gathered_assertion : manifest_.gathered_assertions) {
ABSL_RETURN_IF_ERROR(AddGatheredAssertion(gathered_assertion));
}
return absl::OkStatus();
}
void ManifestBoxEncoder::EncodeClaimAndSignature(
const ClaimSigner::Options& options,
absl::AnyInvocable<void(absl::StatusOr<ManifestEncoderResult>) &&> cb) {
AsyncResolver<ManifestEncoderResult> resolver(options.async_context,
std::move(cb));
Claim claim;
claim.set_instance_id(manifest_.instance_id);
*claim.mutable_claim_generator_info() = generator_info_;
claim.mutable_created_assertions()->Swap(&assertion_hashed_uris_);
claim.mutable_gathered_assertions()->Swap(&gathered_assertion_hashed_uris_);
for (const std::string& redacted_assertion : redacted_assertions_) {
*claim.add_redacted_assertions() = redacted_assertion;
}
claim.set_signature(kClaimSignatureUrl);
claim.set_default_algorithm(absl::StrCat(hash_algorithm_));
auto prepare_claim = [&]() -> absl::StatusOr<std::string> {
ABSL_ASSIGN_OR_RETURN(std::string claim_cbor,
claim_cbor_encoder_.ToCbor(claim));
ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox claim_box,
SerializeCborSuperBox(kClaimUuid, kClaimV2Label,
absl::Cord(claim_cbor)));
ABSL_RETURN_IF_ERROR(manifest_builder_.AddChild(std::move(claim_box)));
return claim_cbor;
};
absl::StatusOr<std::string> claim_cbor = prepare_claim();
if (!claim_cbor.ok()) {
resolver.Return(claim_cbor.status());
return;
}
auto done_cb =
[this, resolver = std::move(resolver)](
absl::StatusOr<SignatureBoxResult> claim_signature) mutable {
auto finalize_manifest =
[&]() -> absl::StatusOr<ManifestEncoderResult> {
ABSL_RETURN_IF_ERROR(claim_signature.status());
ABSL_RETURN_IF_ERROR(
manifest_builder_.AddChild(std::move(claim_signature->box)));
ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox active_manifest,
std::move(manifest_builder_).Finalize());
ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox finalized_manifest_store,
std::move(manifest_.manifest_store_builder)
.Finalize(std::move(active_manifest)));
return ManifestEncoderResult{
.box = std::move(finalized_manifest_store),
};
};
resolver.Return(finalize_manifest());
};
MakeSignatureBox(*claim_cbor, claim_signer_, options, std::move(done_cb));
}
absl::Status ManifestBoxEncoder::EncodeAssertionStore() {
ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_store,
SerializeAssertionStore(std::move(assertion_boxes_)));
return manifest_builder_.AddChild(std::move(assertion_store));
}
void ManifestBoxEncoder::Encode(
const ClaimSigner::Options& options,
absl::AnyInvocable<void(absl::StatusOr<ManifestEncoderResult>) &&> cb) {
auto add_assertions_status = AddAssertions();
if (!add_assertions_status.ok()) {
std::move(cb)(add_assertions_status);
return;
}
auto encode_assertion_store_status = EncodeAssertionStore();
if (!encode_assertion_store_status.ok()) {
std::move(cb)(encode_assertion_store_status);
return;
}
EncodeClaimAndSignature(options, std::move(cb));
}
} // namespace credentio