blob: 97d627d57d7393aaa64e76da8abf40e0065cb41d [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 "assertion/actions_assertion_validator.h"
#include <string>
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "constants/ingredient_relationships.h"
#include "constants/labels.h"
#include "constants/status_codes.h"
#include "proto/actions_assertion.pb.h"
#include "proto/assertion.pb.h"
#include "proto/hashed_uri.pb.h"
#include "proto/ingredient_assertion.pb.h"
#include "proto/manifest.pb.h"
#include "validator/tracker.h"
namespace credentio {
namespace {
absl::string_view Basename(absl::string_view path) {
return path.substr(path.find_last_of('/') + 1);
}
bool RequiresParentOfRelationship(absl::string_view action) {
return action == "c2pa.opened" || action == "c2pa.repackaged" ||
action == "c2pa.transcoded";
}
bool RequiresComponentOfRelationship(absl::string_view action) {
return action == "c2pa.placed" || action == "c2pa.removed";
}
bool RequiresIngredientValidation(absl::string_view action) {
return RequiresParentOfRelationship(action) ||
RequiresComponentOfRelationship(action);
}
template <class IngredientAssertionType>
bool ValidateIngredientRelationship(absl::string_view action,
const IngredientAssertionType& ingredient,
absl::string_view assertion_url,
ValidationTracker& validation_tracker) {
// 3.c.ii.A. Check that the ingredient relationship is parentOf.
if (RequiresParentOfRelationship(action)) {
if (ingredient.relationship() != kIngredientRelationshipParentOf) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.url = assertion_url,
.explanation = absl::StrFormat(
"action %s has wrong ingredient relationship: %s; want %s",
action, ingredient.relationship(),
kIngredientRelationshipParentOf)});
return false;
}
}
// 3.c.ii.B. Check that the ingredient relationship is componentOf.
if (RequiresComponentOfRelationship(action)) {
if (ingredient.relationship() != kIngredientRelationshipComponentOf) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.url = assertion_url,
.explanation = absl::StrFormat(
"action %s has wrong ingredient relationship: %s; "
"want %s",
action, ingredient.relationship(),
kIngredientRelationshipComponentOf)});
return false;
}
}
return true;
}
// See
// https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_c2pa_actions_validation.
template <class ActionsAssertionType>
bool ValidateActions(
const ActionsAssertionType& assertion,
absl::flat_hash_map<std::string, const Assertion*> assertion_store,
ValidationTracker& validation_tracker) {
// 1. Ensure that it has an actions field.
if (assertion.actions().empty()) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionMalformed, {});
return false;
}
for (auto i = 0; i < assertion.actions().size(); ++i) {
const auto& action = assertion.actions(i);
// 3.a. Ensure that it has an action field.
if (action.action().empty()) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionMalformed,
{.explanation = "empty action"});
return false;
}
// 3.b. If the action field is c2pa.created or c2pa.opened, it must be the
// first action.
if (action.action() == "c2pa.created" || action.action() == "c2pa.opened") {
if (i != 0) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionMalformed,
{.explanation = "created/opened not first action"});
return false;
}
}
// 3.c. Check ingredient for certain actions.
if (RequiresIngredientValidation(action.action())) {
// 3.c.i. Check ingredients are present.
if (!action.has_parameters()) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.explanation = absl::StrFormat("action %s missing parameters",
action.action())});
return false;
}
if (action.parameters().ingredients().size() == 0) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.explanation = absl::StrFormat("action %s missing ingredients",
action.action())});
return false;
}
// 3.c.ii. For each ingredient, check that it has a url and it is valid.
for (const auto& ingredient : action.parameters().ingredients()) {
if (!ingredient.has_url() || ingredient.url().empty()) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.explanation = absl::StrFormat(
"action %s missing ingredient url", action.action())});
return false;
}
auto label = Basename(ingredient.url());
if (!assertion_store.contains(label)) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.explanation = absl::StrFormat(
"action %s missing ingredient assertion", action.action())});
return false;
}
auto* assertion = assertion_store[label];
std::string assertion_url = absl::StrCat(
kAssertionStoreLabel, kManifestLabelDelimiter, assertion->label());
if (assertion->has_ingredient_v3()) {
if (!ValidateIngredientRelationship(
action.action(), assertion->ingredient_v3(), assertion_url,
validation_tracker)) {
return false;
}
} else {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionIngredientMismatch,
{.url = assertion_url,
.explanation = absl::StrFormat(
"action %s missing ingredient assertion", action.action())});
return false;
}
}
}
// 3.d. If the action is c2pa.redacted, check that the redacted field that
// is a member of the parameters object for the presence of a JUMBF URI and
// that the URI resolves to an assertion.
if (action.action() == "c2pa.redacted") {
if (action.parameters().redacted_uri().empty()) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionRedactionMismatch,
{.explanation = absl::StrFormat(
"action %s missing redacted in parameters", action.action())});
return false;
}
auto redacted_label = Basename(action.parameters().redacted_uri());
if (!assertion_store.contains(redacted_label)) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionRedactionMismatch,
{.explanation = absl::StrFormat(
"action %s missing redacted assertion", action.action())});
return false;
}
}
}
return true;
}
// 2. Check the first action in each actions assertion.
template <class ActionsAssertionType>
bool ValidateFirstAction(absl::string_view label,
const ActionsAssertionType& assertion,
bool is_first_action_assertion,
ValidationTracker& validation_tracker) {
bool is_created_or_opened =
assertion.actions_size() > 0 &&
(assertion.actions(0).action() == "c2pa.created" ||
assertion.actions(0).action() == "c2pa.opened");
if (is_first_action_assertion && !is_created_or_opened) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionMalformed,
{.explanation = "the first action is not c2pa.created or c2pa.opened"});
return false;
}
if (is_created_or_opened && !is_first_action_assertion) {
validation_tracker.RecordFailure(
FailureStatusCode::kAssertionActionMalformed,
{.explanation = "c2pa.created or c2pa.opened is not the first action"});
return false;
}
return true;
}
} // namespace
bool ActionsAssertionValidator::Validate(
const Manifest& manifest, ValidationTracker& validation_tracker) const {
absl::flat_hash_map<std::string, const Assertion*> assertion_store;
for (const auto& assertion : manifest.assertions()) {
assertion_store[assertion.label()] = &assertion;
}
absl::flat_hash_set<std::string> created_assertions;
created_assertions.reserve(manifest.claim().created_assertions_size());
for (const auto& assertion : manifest.claim().created_assertions()) {
created_assertions.insert(std::string(Basename(assertion.url())));
}
bool is_first_action_assertion = true;
auto validate_actions_assertion = [&](absl::string_view label,
const auto& actions_assertion) -> bool {
if (!ValidateActions(actions_assertion, assertion_store,
validation_tracker)) {
return false;
}
if (created_assertions.contains(label) &&
!ValidateFirstAction(label, actions_assertion,
is_first_action_assertion, validation_tracker)) {
return false;
}
is_first_action_assertion = false;
return true;
};
for (const auto& assertion : manifest.assertions()) {
if (assertion.has_actions_v1() &&
!validate_actions_assertion(assertion.label(),
assertion.actions_v1())) {
return false;
}
if (assertion.has_actions() &&
!validate_actions_assertion(assertion.label(), assertion.actions())) {
return false;
}
}
return true;
}
} // namespace credentio