blob: 2e1b28e4a85f5b1c2ca9e405f97002d467a0e8d1 [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 "assertion/validator.h"
#include <string>
#include <vector>
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "absl/types/span.h"
#include "constants/status_codes.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "jumbf/box.h"
#include "jumbf/uri.h"
#include "proto/assertion.pb.h"
#include "proto/hashed_uri.pb.h"
#include "proto/manifest.pb.h"
#include "testing/cbor_utils.h"
#include "testing/jumbf_utils.h"
#include "testing/proto_test_utils.h"
#include "testing/test_validation_tracker.h"
namespace credentio {
namespace {
using ::cbor::FromJson;
using ::credentio_testing::EqualsProto;
using ::credentio_testing::ParseTextProtoOrDie;
using ::credentio_testing::PartiallyEqualsProto;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::SizeIs;
constexpr absl::string_view kManifestLabel1 =
"urn:uuid:F9168C5E-CEB2-4FAA-B6BF-329BF39FA1E4";
constexpr absl::string_view kManifestLabel2 =
"urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA";
constexpr absl::string_view kManifestLabel3 =
"urn:uuid:ABABABAB-0404-0505-0606-FFFFEEEEDDDD";
void AddCreatedAssertion(Claim* claim, jumbf::SuperBox assertion) {
absl::string_view algorithm = "sha256";
auto* created_assertion = claim->add_created_assertions();
created_assertion->set_url(
absl::StrCat("self#jumbf=c2pa.assertions/",
assertion.description.label.value_or("UNKNOWN_LABEL")));
created_assertion->set_algorithm(algorithm);
created_assertion->set_hash(GetHash(algorithm, assertion));
}
class AssertionValidatorTest : public ::testing::Test {
protected:
std::string ingredient_assertion_cbor_ = cbor::FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"relationship": "componentOf",
"instanceID": "ingredient-instance-id",
"data": {
"url": "ingredient-data-url",
"alg": "ingredient-data-alg",
"hash": "b64'aGFzaA=='",
"size": 1234567890
},
"description": "ingredient-description",
"informationalURI": "ingredient-informational-uri",
"metadata": {
"reviewRatings": [
{
"value": 1,
"explanation": "can't dance to it"
}
]
}
}
)json");
std::string asset_reference_assertion_cbor_ = cbor::FromJson(R"json(
{
"references": [
{
"description": "A copy of the asset on the web",
"reference": {
"uri": "https://some.storage.us/foo"
}
}
]
}
)json");
std::string data_hash_assertion_cbor_ = cbor::FromJson(R"json(
{
"alg": "sha256",
"pad": "b64'Zm9v'",
"hash": "b64'Zm9v'",
"name": "JUMBF manifest",
"exclusions": [
{
"start": 9960,
"length": 4213
},
{
"start": 14173,
"length": 1000
}
]
})json");
std::string embedded_file_ = "fake embedded file contents";
std::string metadata_json_ = R"json(
{
"@context" : {
"exif": "http://ns.adobe.com/exif/1.0/",
"exifEX": "http://cipa.jp/exif/2.32/",
"tiff": "http://ns.adobe.com/tiff/1.0/",
"Iptc4xmpCore": "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/",
"Iptc4xmpExt": "http://iptc.org/std/Iptc4xmpExt/2008-02-29/",
"dc" : "http://purl.org/dc/elements/1.1/",
"photoshop" : "http://ns.adobe.com/photoshop/1.0/"
},
"photoshop:DateCreated": "Aug 31, 2022",
"Iptc4xmpExt:DigitalSourceType": "https://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture",
"Iptc4xmpExt:LocationCreated": {
"Iptc4xmpExt:City": "San Francisco"
},
"Iptc4xmpExt:PersonInImage": [
"Erika Fictional"
],
"Iptc4xmpCore:AltTextAccessibility": "Photo of Erika Fictional standing in front of the Golden Gate Bridge at sunset.",
"exif:GPSVersionID": "2.2.0.0",
"exif:GPSLatitude": "39,21.102N",
"exif:GPSLongitude": "74,26.5737W",
"exif:GPSAltitudeRef": 0,
"exif:GPSAltitude": "100963/29890",
"exif:GPSTimeStamp": "2019-09-22T18:22:57Z",
"exif:GPSSpeedRef": "K",
"exif:GPSSpeed": "4009/161323",
"exif:GPSImgDirectionRef": "T",
"exif:GPSImgDirection": "296140/911",
"exif:GPSDestBearingRef": "T",
"exif:GPSDestBearing": "296140/911",
"exif:GPSHPositioningError": "13244/2207",
"exif:ExposureTime": "1/100",
"exif:FNumber": 4.0,
"exif:ColorSpace": 1,
"exif:DigitalZoomRatio": 2.0,
"tiff:Make": "CameraCompany",
"tiff:Model": "Shooter S1",
"exifEX:LensMake": "CameraCompany",
"exifEX:LensModel": "17.0-35.0 mm",
"exifEX:LensSpecification": { "@list": [ 1.55, 4.2, 1.6, 2.4 ] }
}
)json";
};
TEST_F(AssertionValidatorTest, ValidateOk) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.metadata"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3__1",
ingredient_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromJson("c2pa.metadata", metadata_json_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(
manifest.assertions(),
ElementsAre(
EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
exclusions { start: 9960 length: 4213 }
exclusions { start: 14173 length: 1000 }
alg: "sha256"
hash: "foo"
name: "JUMBF manifest"
}
)pb")),
EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "ingredient-title"
media_type: "ingredient-format"
relationship: "componentOf"
instance_id: "ingredient-instance-id"
data {
url: "ingredient-data-url"
algorithm: "ingredient-data-alg"
hash: "hash"
size: 1234567890
}
description: "ingredient-description"
informational_uri: "ingredient-informational-uri"
}
)pb")),
EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3__1"
ingredient_v3 {
title: "ingredient-title"
media_type: "ingredient-format"
relationship: "componentOf"
instance_id: "ingredient-instance-id"
data {
url: "ingredient-data-url"
algorithm: "ingredient-data-alg"
hash: "hash"
size: 1234567890
}
description: "ingredient-description"
informational_uri: "ingredient-informational-uri"
}
)pb")),
EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.metadata"
metadata {
value {
fields {
key: "@context"
value {
struct_value {
fields {
key: "Iptc4xmpCore"
value {
string_value: "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
}
}
fields {
key: "Iptc4xmpExt"
value {
string_value: "http://iptc.org/std/Iptc4xmpExt/2008-02-29/"
}
}
fields {
key: "dc"
value {
string_value: "http://purl.org/dc/elements/1.1/"
}
}
fields {
key: "exif"
value { string_value: "http://ns.adobe.com/exif/1.0/" }
}
fields {
key: "exifEX"
value { string_value: "http://cipa.jp/exif/2.32/" }
}
fields {
key: "photoshop"
value {
string_value: "http://ns.adobe.com/photoshop/1.0/"
}
}
fields {
key: "tiff"
value { string_value: "http://ns.adobe.com/tiff/1.0/" }
}
}
}
}
fields {
key: "Iptc4xmpCore:AltTextAccessibility"
value {
string_value: "Photo of Erika Fictional standing in front of the Golden Gate Bridge at sunset."
}
}
fields {
key: "Iptc4xmpExt:DigitalSourceType"
value {
string_value: "https://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"
}
}
fields {
key: "Iptc4xmpExt:LocationCreated"
value {
struct_value {
fields {
key: "Iptc4xmpExt:City"
value { string_value: "San Francisco" }
}
}
}
}
fields {
key: "Iptc4xmpExt:PersonInImage"
value {
list_value { values { string_value: "Erika Fictional" } }
}
}
fields {
key: "exif:ColorSpace"
value { number_value: 1 }
}
fields {
key: "exif:DigitalZoomRatio"
value { number_value: 2 }
}
fields {
key: "exif:ExposureTime"
value { string_value: "1/100" }
}
fields {
key: "exif:FNumber"
value { number_value: 4 }
}
fields {
key: "exif:GPSAltitude"
value { string_value: "100963/29890" }
}
fields {
key: "exif:GPSAltitudeRef"
value { number_value: 0 }
}
fields {
key: "exif:GPSDestBearing"
value { string_value: "296140/911" }
}
fields {
key: "exif:GPSDestBearingRef"
value { string_value: "T" }
}
fields {
key: "exif:GPSHPositioningError"
value { string_value: "13244/2207" }
}
fields {
key: "exif:GPSImgDirection"
value { string_value: "296140/911" }
}
fields {
key: "exif:GPSImgDirectionRef"
value { string_value: "T" }
}
fields {
key: "exif:GPSLatitude"
value { string_value: "39,21.102N" }
}
fields {
key: "exif:GPSLongitude"
value { string_value: "74,26.5737W" }
}
fields {
key: "exif:GPSSpeed"
value { string_value: "4009/161323" }
}
fields {
key: "exif:GPSSpeedRef"
value { string_value: "K" }
}
fields {
key: "exif:GPSTimeStamp"
value { string_value: "2019-09-22T18:22:57Z" }
}
fields {
key: "exif:GPSVersionID"
value { string_value: "2.2.0.0" }
}
fields {
key: "exifEX:LensMake"
value { string_value: "CameraCompany" }
}
fields {
key: "exifEX:LensModel"
value { string_value: "17.0-35.0 mm" }
}
fields {
key: "exifEX:LensSpecification"
value {
struct_value {
fields {
key: "@list"
value {
list_value {
values { number_value: 1.55 }
values { number_value: 4.2 }
values { number_value: 1.6 }
values { number_value: 2.4 }
}
}
}
}
}
}
fields {
key: "photoshop:DateCreated"
value { string_value: "Aug 31, 2022" }
}
fields {
key: "tiff:Make"
value { string_value: "CameraCompany" }
}
fields {
key: "tiff:Model"
value { string_value: "Shooter S1" }
}
}
}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateActionsRedacted) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
redacted_assertions: "self#jumbf=c2pa.assertions/c2pa.actions"
)pb");
jumbf::SuperBox manifest_store =
CreateManifestStore({CreateStandardManifest(kManifestLabel1, {})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionActionRedacted));
}
TEST_F(AssertionValidatorTest, ValidateActionsV2Redacted) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
redacted_assertions: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
)pb");
jumbf::SuperBox manifest_store =
CreateManifestStore({CreateStandardManifest(kManifestLabel1, {})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionActionRedacted));
}
TEST_F(AssertionValidatorTest, ValidateActionsMultiInstanceRedacted) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
redacted_assertions: "self#jumbf=c2pa.assertions/c2pa.actions.v2__1"
)pb");
jumbf::SuperBox manifest_store =
CreateManifestStore({CreateStandardManifest(kManifestLabel1, {})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionActionRedacted));
}
TEST_F(AssertionValidatorTest, ValidateUnresolvableUri) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "c2pa.assertions/unresolvable.assertion"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateAssertionMissing) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/no.such.assertion"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateAssertionMissingNotInAssertionStore) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.somewhere/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest,
ValidateAssertionMissingNotChildOfAssertionStore) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/somewhere/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
redacted_assertions: "self#jumbf=c2pa.some_redacted_assertion"
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateAssertionMissingInNestedBox) {
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
jumbf::SuperBox{
.description = {.label = "nested"},
.contents = {{.payload = CreateAssertionBoxFromCbor(
"c2pa.hash.data", data_hash_assertion_cbor_)}},
},
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
auto manifest = ParseTextProtoOrDie<Manifest>(R"pb(
label: "urn:uuid:F9168C5E-CEB2-4FAA-B6BF-329BF39FA1E4"
claim {
created_assertions {
url: "self#jumbf=c2pa.assertions/nested/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
}
)pb");
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateThumbnailAssertion) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.png"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.png"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromEmbeddedFile("c2pa.thumbnail.claim.png",
"image/png", embedded_file_,
"claim.thumbnail.png"),
CreateAssertionBoxFromEmbeddedFile("c2pa.thumbnail.ingredient.png",
"image/png", embedded_file_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(), IsEmpty());
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb")),
EqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.thumbnail.claim.png"
claim_thumbnail {
media_type: "image/png"
data: "fake embedded file contents"
file_name: "claim.thumbnail.png"
})pb")),
EqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.thumbnail.ingredient.png"
ingredient_thumbnail {
media_type: "image/png"
data: "fake embedded file contents"
})pb"))));
}
TEST_F(AssertionValidatorTest, ValidateUndeclaredAssertion) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionUndeclared));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateMalformedAssertion) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
// Add a CBOR box to the assertion store.
assertion_store.contents.push_back({.payload = jumbf::CborBox{}});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionUndeclared));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateSoftBindingMissingForWatermarkedBound) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created"
},
{
"action": "c2pa.watermarked.bound"
}
]
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionActionSoftBindingMissing));
}
TEST_F(AssertionValidatorTest, ValidateSoftBindingPresentForWatermarkedBound) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.soft-binding"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created"
},
{
"action": "c2pa.watermarked.bound"
}
]
}
)json");
std::string soft_binding_cbor = cbor::FromJson(R"json(
{
"alg": "soft-algorithm",
"blocks": [
{
"scope": {
"timespan": {
"start": 100,
"end": 200
}
},
"value": "b64'dmFsdWU='"
}
]
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
CreateAssertionBoxFromCbor("c2pa.soft-binding", soft_binding_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateSoftBindingMissingForWatermarked) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created"
},
{
"action": "c2pa.watermarked"
}
]
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionActionSoftBindingMissing));
}
TEST_F(AssertionValidatorTest, ValidateSoftBindingPresentForWatermarked) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.soft-binding"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created"
},
{
"action": "c2pa.watermarked"
}
]
}
)json");
std::string soft_binding_cbor = cbor::FromJson(R"json(
{
"alg": "soft-algorithm",
"blocks": [
{
"scope": {
"timespan": {
"start": 100,
"end": 200
}
},
"value": "b64'dmFsdWU='"
}
]
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
CreateAssertionBoxFromCbor("c2pa.soft-binding", soft_binding_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateInaccessibleAssertion) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "https://foo.bar/my_assertion"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateInvalidCbor) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", "not valid cbor"),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionCborInvalid));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateAssertionAbsolutePath) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=/c2pa/urn:uuid:F9168C5E-CEB2-4FAA-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(), IsEmpty());
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateAssertionOutsideManifest) {
// This URI points to the manifest with kManifestLabel2.
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {}),
CreateStandardManifest(kManifestLabel2, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionOutsideManifest));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateInvalidAlgorithm) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "not_an_algo"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAlgorithmUnsupported));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateInvalidHash) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "not_a_hash"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionHashedUriMismatch));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateInvalidHashSha384) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha384"
hash: "not_a_hash"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionHashedUriMismatch));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateInvalidHashSha512) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha512"
hash: "not_a_hash"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionHashedUriMismatch));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateActionsUnknown) {
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.unknown"}]
}
)json");
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions", actions_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kGoogleAssertionActionUnknownAction));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateActionsV2Unknown) {
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.unknown"}]
}
)json");
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kGoogleAssertionActionUnknownAction));
EXPECT_THAT(manifest.assertions(), IsEmpty());
}
TEST_F(AssertionValidatorTest, ValidateActionsV2Created) {
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.created"}]
}
)json");
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(), IsEmpty());
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.hash.data")pb"))));
}
TEST_F(AssertionValidatorTest, InvalidStandardManifestNoHardBinding) {
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.created"}]
}
)json");
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kClaimHardBindingsMissing));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb"))));
}
TEST_F(AssertionValidatorTest, InvalidStandardManifestMultipleHardBindings) {
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.created"}]
}
)json");
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMultipleHardBindings));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.hash.data")pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.hash.data")pb"))));
}
TEST_F(AssertionValidatorTest,
InvalidStandardManifestMultipleParentIngredients) {
std::string ingredient_cbor = cbor::FromJson(R"json(
{
"relationship": "parentOf",
"instanceID": "ingredient-instance-id"
}
)json");
std::string actions_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.created"}]
}
)json");
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3", ingredient_cbor),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3__1", ingredient_cbor),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kManifestMultipleParents));
EXPECT_THAT(
manifest.assertions(),
ElementsAre(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions { actions { action: "c2pa.created" } }
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.hash.data")pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.ingredient.v3")pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(
R"pb(label: "c2pa.ingredient.v3__1")pb"))));
}
TEST_F(AssertionValidatorTest, ValidUpdateManifest) {
jumbf::SuperBox std_assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
auto std_manifest =
CreateStandardManifest(kManifestLabel1, {std_assertion_store});
std::string ingredient_assertion_cbor = cbor::FromJson(absl::Substitute(
R"json(
{
"relationship": "parentOf",
"instanceID": "ingredient-instance-id",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/$0",
"alg": "sha256",
"hash": "b64'$1'"
}
}
)json",
kManifestLabel1, absl::Base64Escape(GetHash("sha256", std_manifest))));
jumbf::SuperBox ingredient_assertion_box = CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3", ingredient_assertion_cbor);
std::string actions_assertion_cbor = cbor::FromJson(absl::Substitute(
R"json(
{
"actions": [{
"action": "c2pa.opened",
"parameters": {
"ingredients": [{
"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3",
"alg": "sha256",
"hash": "b64'$0'"
}]
}
}]
}
)json",
absl::Base64Escape(GetHash("sha256", ingredient_assertion_box))));
jumbf::SuperBox actions_assertion_box =
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor);
jumbf::SuperBox update_assertion_store = CreateAssertionStoreBox(
{actions_assertion_box, ingredient_assertion_box});
jumbf::SuperBox update_manifest =
CreateUpdateManifest(kManifestLabel2, {update_assertion_store});
jumbf::SuperBox manifest_store =
CreateManifestStore({std_manifest, update_manifest});
Claim update_claim;
AddCreatedAssertion(&update_claim, actions_assertion_box);
AddCreatedAssertion(&update_claim, ingredient_assertion_box);
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_is_update_manifest(true);
manifest.set_label(kManifestLabel2);
*manifest.mutable_claim() = update_claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
}
TEST_F(AssertionValidatorTest, InvalidUpdateManifestMultipleParentIngredients) {
jumbf::SuperBox std_assertion_store_1 = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox std_assertion_store_2 = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
auto std_manifest_1 =
CreateStandardManifest(kManifestLabel1, {std_assertion_store_1});
auto std_manifest_2 =
CreateStandardManifest(kManifestLabel2, {std_assertion_store_2});
std::string actions_assertion_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.opened"}]
}
)json");
std::string ingredient_assertion_1_cbor = cbor::FromJson(absl::Substitute(
R"json(
{
"relationship": "parentOf",
"instanceID": "ingredient-instance-id-1",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/$0",
"alg": "sha256",
"hash": "b64'$1'"
}
}
)json",
kManifestLabel1, absl::Base64Escape(GetHash("sha256", std_manifest_1))));
std::string ingredient_assertion_2_cbor = cbor::FromJson(absl::Substitute(
R"json(
{
"relationship": "parentOf",
"instanceID": "ingredient-instance-id-2",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/$0",
"alg": "sha256",
"hash": "b64'$1'"
}
}
)json",
kManifestLabel2, absl::Base64Escape(GetHash("sha256", std_manifest_2))));
jumbf::SuperBox actions_assertion_box =
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor);
jumbf::SuperBox ingredient_assertion_1_box = CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3", ingredient_assertion_1_cbor);
jumbf::SuperBox ingredient_assertion_2_box = CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3__1", ingredient_assertion_2_cbor);
jumbf::SuperBox update_assertion_store = CreateAssertionStoreBox({
actions_assertion_box,
ingredient_assertion_1_box,
ingredient_assertion_2_box,
});
jumbf::SuperBox update_manifest =
CreateUpdateManifest(kManifestLabel3, {update_assertion_store});
jumbf::SuperBox manifest_store =
CreateManifestStore({std_manifest_1, std_manifest_2, update_manifest});
Claim update_claim;
AddCreatedAssertion(&update_claim, actions_assertion_box);
AddCreatedAssertion(&update_claim, ingredient_assertion_1_box);
AddCreatedAssertion(&update_claim, ingredient_assertion_2_box);
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_is_update_manifest(true);
manifest.set_label(kManifestLabel3);
*manifest.mutable_claim() = update_claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kManifestUpdateWrongParents));
}
TEST_F(AssertionValidatorTest, InvalidUpdateManifestHardBindingsPresent) {
jumbf::SuperBox std_assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
auto std_manifest =
CreateStandardManifest(kManifestLabel1, {std_assertion_store});
std::string actions_assertion_cbor = cbor::FromJson(R"json(
{
"actions": [{"action": "c2pa.opened"}]
}
)json");
std::string ingredient_assertion_cbor = cbor::FromJson(absl::Substitute(
R"json(
{
"relationship": "parentOf",
"instanceID": "ingredient-instance-id",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/$0",
"alg": "sha256",
"hash": "b64'$1'"
}
}
)json",
kManifestLabel1, absl::Base64Escape(GetHash("sha256", std_manifest))));
jumbf::SuperBox actions_assertion_box =
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor);
jumbf::SuperBox ingredient_assertion_box = CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3", ingredient_assertion_cbor);
jumbf::SuperBox binding_assertion_box =
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_);
jumbf::SuperBox update_assertion_store = CreateAssertionStoreBox({
actions_assertion_box,
ingredient_assertion_box,
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox update_manifest =
CreateUpdateManifest(kManifestLabel2, {update_assertion_store});
jumbf::SuperBox manifest_store =
CreateManifestStore({std_manifest, update_manifest});
Claim update_claim;
AddCreatedAssertion(&update_claim, actions_assertion_box);
AddCreatedAssertion(&update_claim, ingredient_assertion_box);
AddCreatedAssertion(&update_claim, binding_assertion_box);
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_is_update_manifest(true);
manifest.set_label(kManifestLabel2);
*manifest.mutable_claim() = update_claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kManifestUpdateInvalid));
}
TEST_F(AssertionValidatorTest, InvalidUpdateManifestDisallowedAction) {
jumbf::SuperBox std_assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
auto std_manifest =
CreateStandardManifest(kManifestLabel1, {std_assertion_store});
// c2pa.cropped is not allowed in an update manifest.
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [{"action": "c2pa.opened"}, {"action": "c2pa.cropped"}]
}
)json");
std::string ingredient_assertion_cbor = FromJson(absl::Substitute(
R"json(
{
"relationship": "parentOf",
"instanceID": "ingredient-instance-id",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/$0",
"alg": "sha256",
"hash": "b64'$1'"
}
}
)json",
kManifestLabel1, absl::Base64Escape(GetHash("sha256", std_manifest))));
jumbf::SuperBox actions_assertion_box =
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor);
jumbf::SuperBox ingredient_assertion_box = CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3", ingredient_assertion_cbor);
jumbf::SuperBox update_assertion_store = CreateAssertionStoreBox(
{actions_assertion_box, ingredient_assertion_box});
jumbf::SuperBox update_manifest =
CreateUpdateManifest(kManifestLabel2, {update_assertion_store});
jumbf::SuperBox manifest_store =
CreateManifestStore({std_manifest, update_manifest});
Claim update_claim;
AddCreatedAssertion(&update_claim, actions_assertion_box);
AddCreatedAssertion(&update_claim, ingredient_assertion_box);
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_is_update_manifest(true);
manifest.set_label(kManifestLabel2);
*manifest.mutable_claim() = update_claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kManifestUpdateInvalid));
}
TEST_F(AssertionValidatorTest, InvalidUpdateManifestNoIngredients) {
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [{"action": "c2pa.opened"}]
}
)json");
jumbf::SuperBox actions_assertion_box =
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor);
jumbf::SuperBox update_assertion_store =
CreateAssertionStoreBox({actions_assertion_box});
jumbf::SuperBox update_manifest =
CreateUpdateManifest(kManifestLabel2, {update_assertion_store});
jumbf::SuperBox manifest_store = CreateManifestStore({update_manifest});
Claim update_claim;
AddCreatedAssertion(&update_claim, actions_assertion_box);
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_is_update_manifest(true);
manifest.set_label(kManifestLabel2);
*manifest.mutable_claim() = update_claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kManifestUpdateWrongParents));
}
TEST_F(AssertionValidatorTest, InvalidUpdateManifestWrongIngredientRelation) {
jumbf::SuperBox std_assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
auto std_manifest =
CreateStandardManifest(kManifestLabel1, {std_assertion_store});
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [{"action": "c2pa.opened"}]
}
)json");
// Relationship here is "componentOf" instead of "parentOf".
std::string ingredient_assertion_cbor = FromJson(absl::Substitute(
R"json(
{
"relationship": "componentOf",
"instanceID": "ingredient-instance-id",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/$0",
"alg": "sha256",
"hash": "b64'$1'"
}
}
)json",
kManifestLabel1, absl::Base64Escape(GetHash("sha256", std_manifest))));
jumbf::SuperBox actions_assertion_box =
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor);
jumbf::SuperBox ingredient_assertion_box = CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3", ingredient_assertion_cbor);
jumbf::SuperBox update_assertion_store = CreateAssertionStoreBox(
{actions_assertion_box, ingredient_assertion_box});
jumbf::SuperBox update_manifest =
CreateUpdateManifest(kManifestLabel2, {update_assertion_store});
jumbf::SuperBox manifest_store =
CreateManifestStore({std_manifest, update_manifest});
Claim update_claim;
AddCreatedAssertion(&update_claim, actions_assertion_box);
AddCreatedAssertion(&update_claim, ingredient_assertion_box);
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_is_update_manifest(true);
manifest.set_label(kManifestLabel2);
*manifest.mutable_claim() = update_claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kManifestUpdateWrongParents));
}
TEST_F(AssertionValidatorTest, IngredientV1Unsupported) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"relationship": "componentOf",
"instanceID": "ingredient-instance-id",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA",
"alg": "sha256",
"hash": "b64'EliDwzUVTZC6NXyCMndU824N4FKwkHxkx68nnsVWYfg='"
},
"thumbnail": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.thumbnail.ingredient_1.jpg",
"alg": "sha256",
"hash": "b64'Jl/aF6NGEbFTPYooH/aA3FeRsM4KEcJbNeEcjnVoVQk='"
}
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient", ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox thumbnail_box = {
.description =
{
.label = "c2pa.thumbnail.ingredient_1.jpg",
},
.raw_bytes = "b64'Zm9v'",
};
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store}),
CreateStandardManifest(kManifestLabel2,
{assertion_store, thumbnail_box})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "com.google.unsupportedSpecVersion"
explanation: "c2pa.ingredient (v1) and c2pa.ingredient.v2 assertions are not supported"
)pb"))));
}
TEST_F(AssertionValidatorTest, IngredientV2Unsupported) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"relationship": "componentOf",
"c2pa_manifest": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA",
"alg": "sha256",
"hash": "b64'EliDwzUVTZC6NXyCMndU824N4FKwkHxkx68nnsVWYfg='"
},
"thumbnail": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.thumbnail.ingredient_1.jpg",
"alg": "sha256",
"hash": "b64'Jl/aF6NGEbFTPYooH/aA3FeRsM4KEcJbNeEcjnVoVQk='"
}
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v2",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox thumbnail_box = {
.description =
{
.label = "c2pa.thumbnail.ingredient_1.jpg",
},
.raw_bytes = "b64'Zm9v'",
};
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store}),
CreateStandardManifest(kManifestLabel2,
{assertion_store, thumbnail_box})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "com.google.unsupportedSpecVersion"
explanation: "c2pa.ingredient (v1) and c2pa.ingredient.v2 assertions are not supported"
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateIngredientV3) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"relationship": "componentOf",
"activeManifest": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA",
"alg": "sha256",
"hash": "b64'EliDwzUVTZC6NXyCMndU824N4FKwkHxkx68nnsVWYfg='"
},
"claimSignature": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.signature",
"alg": "sha256",
"hash": "b64'Jl/aF6NGEbFTPYooH/aA3FeRsM4KEcJbNeEcjnVoVQk='"
},
"thumbnail": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.thumbnail.ingredient_1.jpg",
"alg": "sha256",
"hash": "b64'Jl/aF6NGEbFTPYooH/aA3FeRsM4KEcJbNeEcjnVoVQk='"
},
"validationResults": {
"activeManifest": {
"success": [{"code": "assertion.dataHash.match"}],
"informational": [],
"failure": []
}
}
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox thumbnail_box = {
.description =
{
.label = "c2pa.thumbnail.ingredient_1.jpg",
},
.raw_bytes = "b64'Zm9v'",
};
jumbf::SuperBox claim_signature_box = {
.description =
{
.label = "c2pa.signature",
},
.raw_bytes = "b64'Zm9v'",
};
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store}),
CreateStandardManifest(kManifestLabel2, {assertion_store, thumbnail_box,
claim_signature_box})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
ASSERT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(
manifest.assertions(),
ElementsAre(
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
active_manifest {
url: "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA"
}
thumbnail {
url: "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.thumbnail.ingredient_1.jpg"
}
}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateIngredientV3Failed) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"relationship": "componentOf",
"activeManifest": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA",
"alg": "sha256",
"hash": "b64'Zm9v'"
},
"thumbnail": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA/c2pa.thumbnail.ingredient_1.jpg",
"alg": "sha256",
"hash": "b64'BAD/HASH'"
},
"validationResults": {}
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox thumbnail_box = {
.description =
{
.label = "c2pa.thumbnail.ingredient_1.jpg",
},
.raw_bytes = "b64'Zm9v'",
};
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store}),
CreateStandardManifest(kManifestLabel2,
{assertion_store, thumbnail_box})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMismatch));
}
TEST_F(AssertionValidatorTest, ValidateIngredientV3FailedBadRelationship) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json(
{
"relationship": "someOtherRelationship"
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.ingredient.malformed"
explanation: "Unrecognized ingredient relationship"
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateIngredientV3FailedMissingRelationship) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json({})json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.ingredient.malformed"
explanation: "Missing ingredient relationship"
)pb"))));
}
TEST_F(AssertionValidatorTest,
ValidateIngredientV3FailedBothActiveManifestAndDigitalSourceType) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json(
{
"relationship": "componentOf",
"activeManifest": {
"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3",
"alg": "sha256",
"hash": "b64'Zm9v'"
},
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.ingredient.malformed"
explanation: "Ingredient assertion must not contain both activeManifest and digitalSourceType"
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateIngredientV3FailedNoHardBindingSuccess) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json({
"relationship": "componentOf",
"activeManifest": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA",
"alg": "sha256",
"hash": "b64'Zm9v'"
},
"validationResults": {
"activeManifest": {
"success": [{"code": "assertion.hashedURI.match"}],
"informational": [],
"failure": []
}
}
})json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.ingredient.malformed"
explanation: "no hard binding check recorded"
)pb"))));
}
TEST_F(AssertionValidatorTest,
ValidateIngredientV3UpdateManifestFailedNoValidationResults) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string ingredient_assertion_cbor = FromJson(R"json({
"relationship": "parentOf",
"activeManifest": {
"url": "self#jumbf=/c2pa/urn:uuid:AFAFAFAF-0101-0202-0303-ABCDEFFEDCBA",
"alg": "sha256",
"hash": "b64'Zm9v'"
},
"validationResults": {}
})json");
jumbf::SuperBox assertion_store =
CreateAssertionStoreBox({CreateAssertionBoxFromCbor(
"c2pa.ingredient.v3", ingredient_assertion_cbor)});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateUpdateManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
manifest.set_is_update_manifest(true);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "assertion.ingredient.malformed"
explanation: "no validation successes or failures recorded"
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateActionsV2Ingredients) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created",
"parameters" : {
"ingredients" : [
{
"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3",
"alg": "sha256",
"hash" : "b64'EliDwzUVTZC6NXyCMndU824N4FKwkHxkx68nnsVWYfg='"
},
{
"url": "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1",
"alg": "sha256",
"hash" : "b64'EliDwzUVTZC6NXyCMndU824N4FKwkHxkx68nnsVWYfg='"
}
]
}
}
]
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3__1",
ingredient_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
ASSERT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(
manifest.assertions(),
ElementsAre(
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
parameters {
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
}
ingredients {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3__1"
}
}
}
}
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3"
)pb")),
PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3__1"
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidateActionsV2IngredientsFailed) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.actions.v2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient__1"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient__2"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created",
"parameters" : {
"ingredients" : [
{
"url": "self#jumbf=c2pa.assertions/c2pa.ingredient__1",
"alg": "sha256",
"hash" : "b64'EliDwzUVTZC6NXyCMndU824N4FKwkHxkx68nnsVWYfg='"
},
{
"url": "self#jumbf=c2pa.assertions/c2pa.ingredient__2",
"alg": "sha256",
"hash" : "b64'Zm9v'"
}
]
}
}
]
}
)json");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.actions.v2", actions_assertion_cbor),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient__1",
ingredient_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient__2",
ingredient_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kHashedUriMismatch));
}
TEST_F(AssertionValidatorTest, MissingAssertionFailure) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.metadata"
algorithm: "sha256"
hash: "hash-is-irrelevant"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMissing));
EXPECT_THAT(manifest.assertions(), SizeIs(1));
}
TEST_F(AssertionValidatorTest, MissingRedactedAssertionOk) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.metadata"
algorithm: "sha256"
hash: "hash-is-irrelevant"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.metadata", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(manifest.assertions(), SizeIs(2));
EXPECT_THAT(manifest.assertions(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {}
)pb"))));
EXPECT_THAT(manifest.assertions(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.metadata"
redacted_by_generator {}
)pb"))));
}
TEST_F(AssertionValidatorTest, RedactedAssertionWithZeroJsonDataOk) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.metadata"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromJson("c2pa.metadata", "\0\0\0"),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.metadata", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(manifest.assertions(), SizeIs(2));
EXPECT_THAT(manifest.assertions(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.metadata"
redacted_by_generator {}
)pb"))));
}
TEST_F(AssertionValidatorTest, RedactedAssertionWithNonzeroJsonDataFails) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.metadata"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromJson("c2pa.metadata", metadata_json_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.metadata", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionNotRedacted));
EXPECT_THAT(manifest.assertions(), SizeIs(1));
}
TEST_F(AssertionValidatorTest, RedactedAssertionWithZeroCborDataOk) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "hash-is-irrelevant"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3", "\0\0\0"),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.ingredient.v3", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(manifest.assertions(), SizeIs(2));
EXPECT_THAT(manifest.assertions(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.ingredient.v3"
redacted_by_generator {}
)pb"))));
}
TEST_F(AssertionValidatorTest, RedactedAssertionWithNonZeroCborDataFails) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.ingredient.v3"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.ingredient.v3",
ingredient_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.ingredient.v3", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionNotRedacted));
EXPECT_THAT(manifest.assertions(), SizeIs(1));
}
TEST_F(AssertionValidatorTest, RedactedAssertionWithZeroEmbeddedFileDataOk) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.png"
algorithm: "sha256"
hash: "hash-is-irrelevant"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromEmbeddedFile("c2pa.thumbnail.claim.png",
"image/png", "\0\0\0",
"claim.thumbnail.png"),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.thumbnail.claim.png", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(manifest.assertions(), SizeIs(2));
EXPECT_THAT(manifest.assertions(),
Contains(PartiallyEqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.thumbnail.claim.png"
redacted_by_generator {}
)pb"))));
}
TEST_F(AssertionValidatorTest,
RedactedAssertionWithNonZeroEmbeddedFileDataFails) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.thumbnail.claim.png"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromEmbeddedFile("c2pa.thumbnail.claim.png",
"image/png", embedded_file_,
"claim.thumbnail.png"),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
std::string redacted_assertion_path = absl::Substitute(
"/c2pa/$0/c2pa.assertions/c2pa.thumbnail.claim.png", kManifestLabel1);
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(
uri_resolver, /*redacted_assertion_paths=*/{redacted_assertion_path},
manifest, t.tracker());
EXPECT_THAT(t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionNotRedacted));
EXPECT_THAT(manifest.assertions(), SizeIs(1));
}
TEST_F(AssertionValidatorTest, MissingIconUriFails) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
claim_generator_info {
name: "generator_icon"
icon { algorithm: "sha256" hash: "hash-is-irrelevant" }
}
label: "c2pa.claim.v2"
)pb");
jumbf::SuperBox assertion_store =
CreateAssertionStoreBox({CreateAssertionBoxFromCbor(
"c2pa.hash.data", data_hash_assertion_cbor_)});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "hashedUri.missing"
url: "self#jumbf=/c2pa/urn:uuid:1234/c2pa.claim.v2"
explanation: "Icon URI is missing"
)pb"))));
EXPECT_THAT(manifest.assertions(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
exclusions { start: 9960 length: 4213 }
exclusions { start: 14173 length: 1000 }
alg: "sha256"
hash: "foo"
name: "JUMBF manifest"
}
)pb"))));
}
TEST_F(AssertionValidatorTest, ExternalIconUriFails) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
claim_generator_info {
name: "generator_icon"
icon {
url: "https://example.com/icon.png"
algorithm: "sha256"
hash: "hash-is-irrelevant"
}
}
label: "c2pa.claim.v2"
)pb");
jumbf::SuperBox assertion_store =
CreateAssertionStoreBox({CreateAssertionBoxFromCbor(
"c2pa.hash.data", data_hash_assertion_cbor_)});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "com.google.hashedUri.unsupported"
url: "self#jumbf=/c2pa/urn:uuid:1234/c2pa.claim.v2"
explanation: "Icon URI is pointing to an external resource"
)pb"))));
EXPECT_THAT(manifest.assertions(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
exclusions { start: 9960 length: 4213 }
exclusions { start: 14173 length: 1000 }
alg: "sha256"
hash: "foo"
name: "JUMBF manifest"
}
)pb"))));
}
TEST_F(AssertionValidatorTest, IconUriPointsToMissingLocationFails) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
claim_generator_info {
name: "generator_icon"
icon {
url: "self#jumbf=c2pa.missing"
algorithm: "sha256"
hash: "hash-is-irrelevant"
}
}
label: "c2pa.claim.v2"
)pb");
jumbf::SuperBox assertion_store =
CreateAssertionStoreBox({CreateAssertionBoxFromCbor(
"c2pa.hash.data", data_hash_assertion_cbor_)});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailureStatuses(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<ValidationStatus>(R"pb(
code: "hashedUri.missing"
url: "self#jumbf=/c2pa/urn:uuid:1234/c2pa.claim.v2"
)pb"))));
EXPECT_THAT(manifest.assertions(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
exclusions { start: 9960 length: 4213 }
exclusions { start: 14173 length: 1000 }
alg: "sha256"
hash: "foo"
name: "JUMBF manifest"
}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidIconUri) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
claim_generator_info {
name: "generator_icon"
icon {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
}
label: "c2pa.claim.v2"
)pb");
jumbf::SuperBox assertion_store =
CreateAssertionStoreBox({CreateAssertionBoxFromCbor(
"c2pa.hash.data", data_hash_assertion_cbor_)});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(manifest.assertions(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
exclusions { start: 9960 length: 4213 }
exclusions { start: 14173 length: 1000 }
alg: "sha256"
hash: "foo"
name: "JUMBF manifest"
}
)pb"))));
}
TEST_F(AssertionValidatorTest, ValidIconUriFallbacksToDefaultAlgorithm) {
Claim claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
claim_generator_info {
name: "generator_icon"
icon {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
}
default_algorithm: "sha256"
label: "c2pa.claim.v2"
)pb");
jumbf::SuperBox assertion_store =
CreateAssertionStoreBox({CreateAssertionBoxFromCbor(
"c2pa.hash.data", data_hash_assertion_cbor_)});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(manifest.assertions(),
ElementsAre(EqualsProto(ParseTextProtoOrDie<Assertion>(R"pb(
label: "c2pa.hash.data"
data_hash {
exclusions { start: 9960 length: 4213 }
exclusions { start: 14173 length: 1000 }
alg: "sha256"
hash: "foo"
name: "JUMBF manifest"
}
)pb"))));
}
TEST_F(AssertionValidatorTest, FailsWhenMultipleHardBindings) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data__1"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.hash.data__1",
data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(
t.GetFailures(),
ContainsFailure(FailureStatusCode::kAssertionMultipleHardBindings));
EXPECT_THAT(manifest.assertions(), SizeIs(2));
}
TEST_F(AssertionValidatorTest,
PassesWhenAdditionalHardAssertionsArePartsOfMultiAssetHash) {
auto claim = ParseTextProtoOrDie<Claim>(R"pb(
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data.part"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
created_assertions {
url: "self#jumbf=c2pa.assertions/c2pa.hash.data"
algorithm: "sha256"
hash: "\x12\x58\x83\xc3\x35\x15\x4d\x90\xba\x35\x7c\x82\x32\x77\x54\xf3\x6e\x0d\xe0\x52\xb0\x90\x7c\x64\xc7\xaf\x27\x9e\xc5\x56\x61\xf8"
}
)pb");
jumbf::SuperBox assertion_store = CreateAssertionStoreBox({
CreateAssertionBoxFromCbor("c2pa.hash.data.part",
data_hash_assertion_cbor_),
CreateAssertionBoxFromCbor("c2pa.hash.data", data_hash_assertion_cbor_),
});
jumbf::SuperBox manifest_store = CreateManifestStore(
{CreateStandardManifest(kManifestLabel1, {assertion_store})});
jumbf::UriResolver uri_resolver =
jumbf::UriResolver::WithSingleRootChild(&manifest_store);
AssertionValidator validator;
TestValidationTracker t;
Manifest manifest;
manifest.set_label(kManifestLabel1);
*manifest.mutable_claim() = claim;
validator.ValidateClaimAssertions(uri_resolver, {}, manifest, t.tracker());
EXPECT_THAT(t.GetFailures(), IsEmpty());
EXPECT_THAT(manifest.assertions(), SizeIs(2));
}
} // namespace
} // namespace credentio