blob: aa421d324978b6de0651b265affdf397702ea219 [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_assembler.h"
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/log/die_if_null.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "claim/manifest_box.h"
#include "claim/manifest_store_builder.h"
#include "constants/ingredient_relationships.h"
#include "constants/labels.h"
#include "constants/spec_version.h"
#include "proto/actions_assertion.pb.h"
#include "proto/assertion.pb.h"
#include "proto/bmff_based_hash_assertion.pb.h"
#include "proto/boxes_hash_assertion.pb.h"
#include "proto/collection_data_hash_assertion.pb.h"
#include "proto/data_hash_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/ingredient_validation_result.pb.h"
#include "proto/multi_asset_hash_assertion.pb.h"
#include "utils/cbor_padding.h"
namespace credentio {
namespace {
absl::Status ValidateRequest(const GenerateManifestStoreRequest& request) {
if (request.hard_binding().binding_case() == HardBinding::BINDING_NOT_SET) {
return absl::InvalidArgumentError(
"The request does not have a hard binding.");
}
for (const auto& gathered : request.gathered_assertions()) {
if (gathered.label().empty()) {
return absl::InvalidArgumentError(
"Gathered assertion must have a non-empty label.");
}
if (gathered.content_type() ==
GatheredAssertion::CONTENT_TYPE_UNSPECIFIED) {
return absl::InvalidArgumentError(
"Gathered assertion content type must be specified.");
}
}
return absl::OkStatus();
}
void ConvertActionMetadata(const ActionMetadata& action_metadata,
ActionItem* action_item) {
if (action_metadata.has_software_agent()) {
*action_item->mutable_software_agent() = action_metadata.software_agent();
}
if (action_metadata.has_description()) {
action_item->set_description(action_metadata.description());
}
if (action_metadata.has_multiple_instances()) {
action_item->mutable_parameters()->set_multiple_instances(
action_metadata.multiple_instances());
}
}
absl::StatusOr<Assertion> ConvertIngredientAssertion(
const IngredientInput& ingredient, absl::string_view label,
absl::string_view relationship,
const std::optional<IngredientManifestUris>& ingredient_uris,
const std::optional<IngredientValidationResults>&
ingredient_validation_results,
const std::optional<std::string>& digital_source_type) {
Assertion assertion;
// Convert Assertion box fields.
assertion.set_label(label);
auto* ingredient_v3 = assertion.mutable_ingredient_v3();
// Convert Ingredient Assertion V3 fields.
if (ingredient.metadata().has_title()) {
ingredient_v3->set_title(ingredient.metadata().title());
}
if (ingredient.metadata().has_media_type()) {
ingredient_v3->set_media_type(ingredient.metadata().media_type());
} else if (ingredient.ingredient_validation_results().has_media_type()) {
ingredient_v3->set_media_type(
ingredient.ingredient_validation_results().media_type());
}
ingredient_v3->set_relationship(relationship);
if (ingredient.metadata().has_instance_id()) {
ingredient_v3->set_instance_id(ingredient.metadata().instance_id());
}
if (ingredient.metadata().has_description()) {
ingredient_v3->set_description(ingredient.metadata().description());
}
if (ingredient.metadata().has_informational_uri()) {
ingredient_v3->set_informational_uri(
ingredient.metadata().informational_uri());
}
if (ingredient_uris.has_value()) {
// The ingredient URIs provided by the ManifestStoreBuilder should always
// have the active manifest and claim signature URIs set.
if (ingredient_uris->active_manifest.url().empty()) {
return absl::InternalError(
"Should not happen. Ingredient URI missing active manifest.");
}
*ingredient_v3->mutable_active_manifest() =
ingredient_uris->active_manifest;
if (ingredient_uris->claim_signature.url().empty()) {
return absl::InternalError(
"Should not happen. Ingredient URI missing claim signature.");
}
*ingredient_v3->mutable_claim_signature() =
ingredient_uris->claim_signature;
}
if (ingredient_validation_results.has_value()) {
*ingredient_v3->mutable_validation_results() =
ingredient_validation_results.value();
}
if (digital_source_type.has_value()) {
ingredient_v3->set_digital_source_type(*digital_source_type);
}
return assertion;
}
// An ingredient listed in a GenerateManifestStoreRequest.
class Ingredient {
public:
explicit Ingredient(std::string label, const IngredientInput* input)
: label_(std::move(label)), input_(*ABSL_DIE_IF_NULL(input)) {}
absl::Status SetRelationship(absl::string_view relationship) {
if (relationship_.empty()) {
relationship_ = relationship;
} else if (relationship != relationship_) {
return absl::InvalidArgumentError(
absl::Substitute("Inconsistent use of ingredient ($0 and $1)",
relationship_, relationship));
}
return absl::OkStatus();
}
absl::string_view label() const { return label_; }
absl::string_view relationship() const { return relationship_; }
const IngredientInput& input() const { return input_; }
private:
const std::string label_;
const IngredientInput& input_;
absl::string_view relationship_;
};
// Creates an assertion label per
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_multiple_instances.
std::string CreateLabel(absl::string_view base_label, int index) {
if (index == 0) {
return std::string(base_label);
}
return absl::StrCat(base_label, "__", index);
}
class PartLabelProvider {
public:
absl::StatusOr<std::string> GetLabel(PartBinding::BindingCase binding_case) {
absl::string_view base_label;
switch (binding_case) {
case PartBinding::kBmffBasedHash:
base_label = kBmffBasedHashAssertionV3PartLabel;
break;
case PartBinding::kBoxHash:
base_label = kBoxesHashAssertionPartLabel;
break;
case PartBinding::kDataHash:
base_label = kDataHashAssertionPartLabel;
break;
default:
return absl::InvalidArgumentError(
absl::StrCat("Unsupported assertion type: ", binding_case));
}
return CreateLabel(base_label, assertion_type_to_count_[base_label]++);
}
private:
absl::flat_hash_map<absl::string_view, int32_t> assertion_type_to_count_;
};
absl::Status AddMultiAssetHashToManifestBox(
const GenerateManifestStoreRequest& request, ManifestBox& manifest_box) {
if (request.hard_binding().part_bindings_size() == 0) {
// Doesn't have any multi-asset hash assertion information.
return absl::OkStatus();
}
Assertion& multi_asset_hash_assertion =
manifest_box.multi_part_assertion.emplace();
multi_asset_hash_assertion.set_label(kMultiAssetHashAssertionLabel);
PartLabelProvider part_label_provider;
for (const auto& part : request.hard_binding().part_bindings()) {
Assertion& part_hash_assertion =
manifest_box.part_hash_assertions.emplace_back();
ABSL_ASSIGN_OR_RETURN(std::string label,
part_label_provider.GetLabel(part.binding_case()));
part_hash_assertion.set_label(std::move(label));
switch (part.binding_case()) {
case PartBinding::kBmffBasedHash:
*part_hash_assertion.mutable_bmff_based_hash() = part.bmff_based_hash();
break;
case PartBinding::kBoxHash:
*part_hash_assertion.mutable_boxes_hash() = part.box_hash();
PadBoxesHashAssertion(*part_hash_assertion.mutable_boxes_hash());
break;
case PartBinding::kDataHash:
*part_hash_assertion.mutable_data_hash() = part.data_hash();
PadDataHashAssertion(*part_hash_assertion.mutable_data_hash());
break;
default:
return absl::InvalidArgumentError(
absl::StrCat("Unsupported assertion type: ", part.binding_case()));
}
MultiAssetPart& multi_asset_part =
*multi_asset_hash_assertion.mutable_multi_asset_hash()->add_parts();
multi_asset_part.mutable_location()->set_byte_offset(
part.location().byte_offset());
multi_asset_part.mutable_location()->set_length(part.location().length());
multi_asset_part.mutable_hash_assertion()->set_url(absl::StrCat(
"self#jumbf=c2pa.assertions/", part_hash_assertion.label()));
multi_asset_part.set_is_optional(part.is_optional());
}
PadMultiAssetHashAssertion(
*multi_asset_hash_assertion.mutable_multi_asset_hash());
return absl::OkStatus();
}
} // namespace
// A single manifest box assembly operation.
class StandardManifestBoxAssembler::AssembleOp {
public:
explicit AssembleOp(const StandardManifestBoxAssembler& assembler,
const GenerateManifestStoreRequest& request)
: assembler_(assembler), request_(request) {
ingredients_.reserve(request.ingredients_size());
for (int i = 0; i < request.ingredients_size(); ++i) {
ingredients_.push_back(
Ingredient(CreateLabel(kIngredientAssertionV3Label, i),
&request.ingredients(i)));
}
}
absl::StatusOr<ManifestBox> Run() {
ABSL_RETURN_IF_ERROR(ValidateRequest(request_));
ManifestBox manifest_box = {.manifest_store_builder = ManifestStoreBuilder(
&assembler_.hash_factory_)};
manifest_box.label = absl::StrCat(
"urn:c2pa:", assembler_.uuid_generator_.Generate().ToString());
if (request_.has_instance_id()) {
manifest_box.instance_id = request_.instance_id();
} else {
manifest_box.instance_id =
assembler_.uuid_generator_.Generate().ToString();
}
// Convert hard binding assertion.
{
Assertion& hard_binding = manifest_box.hard_binding_assertion;
if (request_.hard_binding().has_bmff_based_hash()) {
hard_binding.set_label(kBmffBasedHashAssertionV3Label);
*hard_binding.mutable_bmff_based_hash() =
request_.hard_binding().bmff_based_hash();
} else if (request_.hard_binding().has_box_hash()) {
hard_binding.set_label(kBoxesHashAssertionLabel);
*hard_binding.mutable_boxes_hash() = request_.hard_binding().box_hash();
PadBoxesHashAssertion(*hard_binding.mutable_boxes_hash());
} else if (request_.hard_binding().has_data_hash()) {
hard_binding.set_label(kDataHashAssertionLabel);
*hard_binding.mutable_data_hash() = request_.hard_binding().data_hash();
PadDataHashAssertion(*hard_binding.mutable_data_hash());
} else if (request_.hard_binding().has_collection_data_hash()) {
hard_binding.set_label(kCollectionDataHashAssertionLabel);
*hard_binding.mutable_collection_data_hash() =
request_.hard_binding().collection_data_hash();
}
}
ABSL_RETURN_IF_ERROR(
AddMultiAssetHashToManifestBox(request_, manifest_box));
// Process inception action.
Assertion& actions_assertion = manifest_box.actions_assertion;
ActionsAssertion& actions = *actions_assertion.mutable_actions();
std::optional<int> opened_ingredient_index = std::nullopt;
{
actions_assertion.set_label(kActionsAssertionV2Label);
if (request_.actions().has_all_actions_included()) {
actions.set_all_actions_included(
request_.actions().all_actions_included());
}
if (request_.actions().has_created()) {
ABSL_RETURN_IF_ERROR(ConvertActionWithInputIngredients(
"c2pa.created", request_.actions().created(),
actions.add_actions()));
} else if (request_.actions().has_opened()) {
OpenedAction opened_action = request_.actions().opened();
ABSL_RETURN_IF_ERROR(
ConvertOpenedAction(opened_action, actions.add_actions()));
opened_ingredient_index = opened_action.ingredient_index();
} else {
return absl::InvalidArgumentError(
"missing inception action (created or opened)");
}
}
// Process remaining actions.
for (const auto& action : request_.actions().actions()) {
switch (action.action_case()) {
case Action::kAddedText:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.addedText", action.added_text(), actions.add_actions()));
break;
case Action::kAdjustedColor:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.adjustedColor", action.adjusted_color(),
actions.add_actions()));
break;
case Action::kChangedSpeed:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.changedSpeed", action.changed_speed(),
actions.add_actions()));
break;
case Action::kConverted:
ABSL_RETURN_IF_ERROR(ConvertSimpleAction(
"c2pa.converted", action.converted(), actions.add_actions()));
break;
case Action::kCropped:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.cropped", action.cropped(), actions.add_actions()));
break;
case Action::kDeleted:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.deleted", action.deleted(), actions.add_actions()));
break;
case Action::kDrawing:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.drawing", action.drawing(), actions.add_actions()));
break;
case Action::kDubbed:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.dubbed", action.dubbed(), actions.add_actions()));
break;
case Action::kEdited:
ABSL_RETURN_IF_ERROR(ConvertActionWithInputIngredients(
"c2pa.edited", action.edited(), actions.add_actions()));
break;
case Action::kEditedMetadata:
ABSL_RETURN_IF_ERROR(ConvertSimpleAction("c2pa.edited.metadata",
action.edited_metadata(),
actions.add_actions()));
break;
case Action::kEnhanced:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.enhanced", action.enhanced(), actions.add_actions()));
break;
case Action::kFiltered:
ABSL_RETURN_IF_ERROR(ConvertActionWithInputIngredients(
"c2pa.filtered", action.filtered(), actions.add_actions()));
break;
case Action::kOrientation:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.orientation", action.orientation(), actions.add_actions()));
break;
case Action::kPlaced:
ABSL_RETURN_IF_ERROR(
ConvertPlacedAction(action.placed(), actions.add_actions()));
break;
case Action::kRepackaged:
ABSL_RETURN_IF_ERROR(ConvertRepackagedAction(
"c2pa.repackaged", action.repackaged(), actions.add_actions(),
opened_ingredient_index));
break;
case Action::kResized:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.resized", action.resized(), actions.add_actions()));
break;
case Action::kTranscoded:
ABSL_RETURN_IF_ERROR(ConvertTranscodedAction(
"c2pa.transcoded", action.transcoded(), actions.add_actions(),
opened_ingredient_index));
break;
case Action::kTrimmed:
ABSL_RETURN_IF_ERROR(ConvertDigitalSourceTypeAction(
"c2pa.trimmed", action.trimmed(), actions.add_actions()));
break;
case Action::kUnknown:
ABSL_RETURN_IF_ERROR(ConvertSimpleAction(
"c2pa.unknown", action.unknown(), actions.add_actions()));
break;
case Action::ACTION_NOT_SET:
return absl::InvalidArgumentError("empty action");
}
}
// Add redaction actions from ingredients.
absl::flat_hash_set<absl::string_view> seen_redaction_uris;
for (const auto& ingredient : ingredients_) {
if (ingredient.input().has_ingredient_validation_results()) {
const IngredientValidationResultProto& result =
ingredient.input().ingredient_validation_results();
for (const auto& redaction : result.redactions()) {
// Currently, duplicate manifests are skipped in order of iteration
// over `ingredients_`.
if (!seen_redaction_uris.insert(redaction.uri()).second) {
continue;
}
ActionItem* action = actions.add_actions();
action->set_action("c2pa.redacted");
action->set_reason(redaction.reason());
action->mutable_parameters()->set_redacted_uri(redaction.uri());
}
}
}
// Import ingredients.
for (const auto& ingredient : ingredients_) {
ABSL_RETURN_IF_ERROR(
ImportIngredient(ingredient, manifest_box.ingredient_assertions,
manifest_box.manifest_store_builder));
}
// Copy gathered assertions.
manifest_box.gathered_assertions.assign(
request_.gathered_assertions().begin(),
request_.gathered_assertions().end());
return manifest_box;
}
private:
absl::StatusOr<Ingredient*> GetIngredient(int ingredient_index) {
if (ingredient_index < 0 || ingredient_index >= ingredients_.size()) {
return absl::InvalidArgumentError(
absl::Substitute("ingredient index out of range [0, $0]: $1",
ingredients_.size(), ingredient_index));
}
return &ingredients_[ingredient_index];
}
absl::Status AddIngredientToAction(int ingredient_index,
absl::string_view relationship,
ActionItem* action_item) {
ABSL_ASSIGN_OR_RETURN(Ingredient * ingredient,
GetIngredient(ingredient_index));
ABSL_RETURN_IF_ERROR(ingredient->SetRelationship(relationship));
action_item->mutable_parameters()->add_ingredients()->set_url(
absl::StrCat(kAssertionStoreUrlPrefix, ingredient->label()));
return absl::OkStatus();
}
absl::Status ConvertOpenedAction(const OpenedAction& action,
ActionItem* action_item) {
ABSL_RETURN_IF_ERROR(
ConvertSimpleAction("c2pa.opened", action, action_item));
if (!action.has_ingredient_index()) {
return absl::InvalidArgumentError(
"Opened action requires an ingredient index.");
}
return AddIngredientToAction(action.ingredient_index(),
kIngredientRelationshipParentOf, action_item);
}
absl::Status ConvertPlacedAction(const PlacedAction& action,
ActionItem* action_item) {
ABSL_RETURN_IF_ERROR(
ConvertSimpleAction("c2pa.placed", action, action_item));
if (action.ingredient_indexes_size() == 0) {
return absl::InvalidArgumentError("missing ingredients in placed action");
}
for (int ingredient_index : action.ingredient_indexes()) {
ABSL_RETURN_IF_ERROR(AddIngredientToAction(
ingredient_index, kIngredientRelationshipComponentOf, action_item));
}
return absl::OkStatus();
}
absl::Status ConvertTranscodedAction(
absl::string_view name, const TranscodedAction& action,
ActionItem* action_item, std::optional<int> opened_ingredient_index) {
ABSL_RETURN_IF_ERROR(
ConvertDigitalSourceTypeAction(name, action, action_item));
if (opened_ingredient_index.has_value()) {
// We add this redundant ingredient reference to satisfy 2.1 validators,
// which require an ingredient reference in c2pa.transcoded and
// c2pa.repackaged actions.
return AddIngredientToAction(*opened_ingredient_index,
kIngredientRelationshipParentOf,
action_item);
}
return absl::OkStatus();
}
absl::Status ConvertRepackagedAction(
absl::string_view name, const RepackagedAction& action,
ActionItem* action_item, std::optional<int> opened_ingredient_index) {
if (!opened_ingredient_index.has_value()) {
return absl::InvalidArgumentError(
absl::Substitute("$0 action requires an `opened` action.", name));
}
ABSL_RETURN_IF_ERROR(ConvertSimpleAction(name, action, action_item));
// We add this redundant ingredient reference to satisfy 2.1 validators,
// which require an ingredient reference in c2pa.transcoded and
// c2pa.repackaged actions.
return AddIngredientToAction(*opened_ingredient_index,
kIngredientRelationshipParentOf, action_item);
}
// CreatedAction, EditedAction, FilteredAction.
template <typename T>
absl::Status ConvertActionWithInputIngredients(absl::string_view name,
const T& action,
ActionItem* action_item) {
ABSL_RETURN_IF_ERROR(
ConvertDigitalSourceTypeAction(name, action, action_item));
for (int ingredient_index : action.input_ingredient_indexes()) {
ABSL_RETURN_IF_ERROR(AddIngredientToAction(
ingredient_index, kIngredientRelationshipInputTo, action_item));
}
return absl::OkStatus();
}
template <typename T>
absl::Status ConvertDigitalSourceTypeAction(absl::string_view name,
const T& action,
ActionItem* action_item) {
ABSL_RETURN_IF_ERROR(ConvertSimpleAction(name, action, action_item));
if (action.has_digital_source_type()) {
action_item->set_digital_source_type(action.digital_source_type());
}
return absl::OkStatus();
}
template <typename T>
absl::Status ConvertSimpleAction(absl::string_view name, const T& action,
ActionItem* action_item) {
action_item->set_action(name);
if (action.has_action_metadata()) {
ConvertActionMetadata(action.action_metadata(), action_item);
}
return absl::OkStatus();
}
// Imports an ingredient's manifest store and creates an ingredient
// assertion.
absl::Status ImportIngredient(const Ingredient& ingredient,
std::vector<Assertion>& ingredient_assertions,
ManifestStoreBuilder& manifest_store_builder) {
if (ingredient.relationship().empty()) {
return absl::InvalidArgumentError(
"ingredient is not referenced by any action.");
}
std::optional<IngredientManifestUris> ingredient_manifest_uris;
std::optional<IngredientValidationResults> ingredient_validation_results;
std::optional<std::string> digital_source_type;
if (ingredient.input().has_ingredient_validation_results()) {
const IngredientValidationResultProto& result =
ingredient.input().ingredient_validation_results();
if (result.has_legacy_ingredient_metadata()) {
// Treat ingredients with unsupported manifests like ingredients without
// C2PA at all.
} else if (result.has_manifest_store_bytes()) {
if (!result.has_ingredient_validation_results()) {
return absl::InvalidArgumentError(
"Ingredient manifest store validation results "
"cannot be empty.");
}
ABSL_ASSIGN_OR_RETURN(ingredient_manifest_uris,
manifest_store_builder.AddIngredientManifestStore(
result.manifest_store_bytes()),
_ << "adding ingredient manifest store");
ingredient_validation_results = result.ingredient_validation_results();
} else if (result.has_digital_source_type() &&
assembler_.spec_version_ >= SpecVersion::kC2pa_2_4) {
digital_source_type = result.digital_source_type();
}
}
ABSL_ASSIGN_OR_RETURN(
Assertion ingredient_assertion,
ConvertIngredientAssertion(
ingredient.input(), ingredient.label(), ingredient.relationship(),
ingredient_manifest_uris, ingredient_validation_results,
digital_source_type));
ingredient_assertions.push_back(ingredient_assertion);
return absl::OkStatus();
}
const StandardManifestBoxAssembler& assembler_;
const GenerateManifestStoreRequest& request_;
std::vector<Ingredient> ingredients_;
};
absl::StatusOr<ManifestBox> StandardManifestBoxAssembler::Assemble(
const GenerateManifestStoreRequest& request) const {
AssembleOp op(*this, request);
return op.Run();
}
} // namespace credentio