blob: 46759a4c1ed535be4614091eb78dbf64849c27a9 [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/assertion_encoder.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/status_matchers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "assertion/assertion_parser.h"
#include "cbor/cbor.h"
#include "cbor/parse.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "jumbf/box.h"
#include "jumbf/parse.h"
#include "proto/actions_assertion.cbor.h"
#include "proto/actions_assertion.pb.h"
#include "proto/assertion.pb.h"
#include "proto/bmff_based_hash_assertion.cbor.h"
#include "proto/bmff_based_hash_assertion.pb.h"
#include "proto/boxes_hash_assertion.cbor.h"
#include "proto/boxes_hash_assertion.pb.h"
#include "proto/data_hash_assertion.cbor.h"
#include "proto/data_hash_assertion.pb.h"
#include "proto/ingredient_assertion.cbor.h"
#include "proto/ingredient_assertion.pb.h"
#include "testing/proto_test_utils.h"
#include "testing/test_validation_tracker.h"
namespace credentio {
namespace {
using ::absl_testing::IsOk;
using ::absl_testing::StatusIs;
using ::cbor::Parse;
using ::credentio_testing::EqualsProto;
using ::credentio_testing::ParseTextProtoOrDie;
using ::testing::HasSubstr;
TEST(AssertionEncoderTest, UnsupportedAssertionToEncode) {
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "unsupported_assertion"
)pb");
StandardAssertionEncoder encoder;
EXPECT_THAT(encoder.ToCbor(assertion),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("Assertion not supported.")));
}
TEST(AssertionEncoderTest, EncodeAction) {
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions {
all_actions_included: true
actions {
action: "c2pa.created"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
software_agent {
name: "Joe\'s Photo Editor"
version: "2.0"
operating_system: "Windows 10"
}
parameters {
instance_id: "ed610ae51f604002be3dbf0c589a2f1f"
ingredients {
url: "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient__1"
algorithm: "sha256"
hash: "hash"
}
ingredients {
url: "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient__2"
algorithm: "sha256"
hash: "hash"
}
}
}
actions {
action: "c2pa.filtered"
software_agent {
name: "Joe\'s Photo Editor"
version: "2.0"
operating_system: "Windows 10"
}
}
actions { action: "c2pa.cropped", software_agent_index: 1 }
templates {
action: "com.joesphoto.filter"
description: "Magic Filter"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic"
software_agent { name: "Joe\'s Photo Editor" version: "2.0" }
icon { url: "http://cdn.examplephotoagency.com/logo.svg" hash: "hash" }
}
software_agents {
name: "Joe\'s Photo Editor"
version: "2.0"
operating_system: "Windows 10"
}
software_agents {
name: "Bob\'s Photo Editor"
version: "1.0"
operating_system: "Windows 10"
}
}
)pb");
StandardAssertionEncoder encoder;
auto cbor = encoder.ToCbor(assertion);
ABSL_ASSERT_OK(cbor);
std::string cbor_string(*cbor);
auto parse_result = Parse(cbor_string);
ABSL_ASSERT_OK(parse_result);
auto map = (*parse_result)->AsMap();
ABSL_ASSERT_OK(map);
credentio::ActionsAssertion result;
auto status = cbor::ToProto(*map, &result);
ABSL_ASSERT_OK(status);
EXPECT_THAT(result, EqualsProto(assertion.actions()));
}
TEST(AssertionEncoderTest, EncodeBmffBasedHash) {
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.bmff.v3"
bmff_based_hash {
hash: "Zm9v",
name: "Example `c2pa.hash.bmff.v2` assertion",
exclusions:
[ {
data:
[ { value: "Zm9v", offset: 8 }],
xpath: "/uuid"
}
, { xpath: "/ftyp" }
, { xpath: "/mfra" }
, { xpath: "/moov[1]/pssh" }
, {
data:
[ { value: "Zm9v", offset: 20 }],
xpath: "/emsg"
}]
}
)pb");
StandardAssertionEncoder encoder;
auto cbor = encoder.ToCbor(assertion);
ABSL_ASSERT_OK(cbor);
std::string cbor_string(*cbor);
auto parse_result = Parse(cbor_string);
ABSL_ASSERT_OK(parse_result);
auto map = (*parse_result)->AsMap();
ABSL_ASSERT_OK(map);
credentio::BmffBasedHashAssertion result;
auto status = cbor::ToProto(*map, &result);
ABSL_ASSERT_OK(status);
EXPECT_THAT(result, EqualsProto(assertion.bmff_based_hash()));
}
TEST(AssertionEncoderTest, EncodeDataHash) {
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
alg: "sha256",
pad: "b64'Zm9v'",
hash: "b64'Zm9v'",
name: "JUMBF manifest",
exclusions:
[ { start: 9960, length: 4213 }
, { start: 14173, length: 1000 }]
}
)pb");
StandardAssertionEncoder encoder;
auto cbor = encoder.ToCbor(assertion);
ABSL_ASSERT_OK(cbor);
std::string cbor_string(*cbor);
auto parse_result = Parse(cbor_string);
ABSL_ASSERT_OK(parse_result);
auto map = (*parse_result)->AsMap();
ABSL_ASSERT_OK(map);
EXPECT_THAT(map->GetByteString("pad"), IsOk());
credentio::DataHashAssertion result;
auto status = cbor::ToProto(*map, &result);
ABSL_ASSERT_OK(status);
auto expected = assertion.data_hash();
expected.clear_pad();
auto actual = result;
actual.clear_pad();
EXPECT_THAT(actual, EqualsProto(expected));
}
TEST(AssertionEncoderTest, EncodeIngredientAssertion) {
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "image 1.jpg",
media_type: "image/jpeg",
thumbnail: {
url: "self#jumbf=c2pa/urn:c2pa:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.thumbnail.ingredient_1.jpg",
hash: "b64'UjRAYWiAq4lfCRDmksWAlDJN/XtHHFFwMWymsZsm3j8='"
},
relationship: "parentOf",
active_manifest: {
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322",
hash: "b64'1kjJTO108b71cL95UxgfHD3eDgk9VrCedW8n3fYTRMk='"
},
claim_signature: {
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature",
hash: "b64'85KAvU3+3YgtIjj6IV0fzKwj8si/85+gevVSK2Iw+S0='"
},
validation_results: {
active_manifest: {
successes:
[ {
code: "claimSignature.validated",
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
, {
code: "signingCredential.trusted",
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
, {
code: "timeStamp.validated",
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
, {
code: "timeStamp.trusted",
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
, {
code: "assertion.hashedURI.match",
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3"
}],
informationals:
[ {
code: "signingCredential.ocsp.skipped",
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}],
failures: []
},
ingredient_deltas:
[ {
ingredient_assertion_uri: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3",
validation_deltas: {
successes: [],
informationals: [],
failures:
[ {
code: "assertion.hashedURI.mismatch",
url: "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.metadata"
}]
}
}
, {
ingredient_assertion_uri: "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.ingredient.v3",
validation_deltas: {
successes: [],
informationals: [],
failures:
[ {
code: "signingCredential.untrusted",
url: "self#jumbf=c2pa/urn:c2pa:72C28A7C-7F5B-4301-B373-3183C10AF7C5/c2pa.signature"
}]
}
}]
}
}
)pb");
StandardAssertionEncoder encoder;
auto cbor = encoder.ToCbor(assertion);
ABSL_ASSERT_OK(cbor);
std::string cbor_string(*cbor);
auto parse_result = Parse(cbor_string);
ABSL_ASSERT_OK(parse_result);
auto map = (*parse_result)->AsMap();
ABSL_ASSERT_OK(map);
credentio::IngredientAssertionV3 result;
auto status = cbor::ToProto(*map, &result);
ABSL_ASSERT_OK(status);
EXPECT_THAT(result, EqualsProto(assertion.ingredient_v3()));
}
TEST(AssertionEncoderTest, EncodeBoxesHash) {
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.boxes"
boxes_hash {
alg: "sha256",
boxes:
[ { names: [ "SOI", "APP0", "APP2" ], hash: "b64'Zm9v'" }
, { names: [ "C2PA" ], hash: "b64'YmFy'" }]
}
)pb");
StandardAssertionEncoder encoder;
auto cbor = encoder.ToCbor(assertion);
ABSL_ASSERT_OK(cbor);
std::string cbor_string(*cbor);
auto parse_result = Parse(cbor_string);
ABSL_ASSERT_OK(parse_result);
auto map = (*parse_result)->AsMap();
ABSL_ASSERT_OK(map);
credentio::BoxesHashAssertion result;
auto status = cbor::ToProto(*map, &result);
ABSL_ASSERT_OK(status);
EXPECT_THAT(result, EqualsProto(assertion.boxes_hash()));
}
TEST(GetAssertionEncoderTest, ReturnsSameInstanceForSameParam) {
const auto& encoder1 = GetAssertionEncoder(true);
const auto& encoder2 = GetAssertionEncoder(true);
EXPECT_EQ(&encoder1, &encoder2);
const auto& encoder3 = GetAssertionEncoder(false);
const auto& encoder4 = GetAssertionEncoder(false);
EXPECT_EQ(&encoder3, &encoder4);
}
TEST(GetAssertionEncoderTest, ReturnsDifferentInstancesForDifferentParams) {
const auto& encoder1 = GetAssertionEncoder(true);
const auto& encoder2 = GetAssertionEncoder(false);
EXPECT_NE(&encoder1, &encoder2);
}
absl::StatusOr<Assertion> ParseAssertionBox(absl::string_view assertion_data) {
ABSL_ASSIGN_OR_RETURN(auto assertion_box,
jumbf::ConsumeSuperBox(&assertion_data, 9));
if (assertion_box.contents.size() != 1) {
return absl::InvalidArgumentError(absl::StrCat(
"the content size is not 1, but ", assertion_box.contents.size()));
}
AssertionParser parser;
TestValidationTracker validation_tracker;
std::optional<Assertion> assertion =
parser.ParseCbor(assertion_box.description.label.value_or(""),
assertion_box.contents[0].Get<jumbf::CborBox>().payload,
validation_tracker.tracker());
if (!assertion.has_value()) {
return absl::InvalidArgumentError(
absl::StrJoin(validation_tracker.GetFailures(), ";"));
}
return assertion.value();
}
TEST(AssertionEncoderTest, ToJumbf) {
StandardAssertionEncoder encoder(
/*skip_validity_checks_for_test=*/false);
auto assertion = ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
software_agent { name: "test generator" }
description: "created for testing"
digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture"
}
}
)pb");
auto assertion_box = encoder.ToJumbf(assertion);
ABSL_ASSERT_OK(assertion_box);
auto serialized = std::move(*assertion_box).AsCord();
auto parsed_assertion = ParseAssertionBox(serialized.Flatten());
ABSL_ASSERT_OK(parsed_assertion);
EXPECT_THAT(*parsed_assertion, EqualsProto(assertion));
}
} // namespace
} // namespace credentio