blob: 9ded34e27de812fda6322c7979820473beb9fbaf [file]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "assertion/assertion_parser.h"
#include <optional>
#include <string>
#include "absl/strings/match.h"
#include "absl/strings/string_view.h"
#include "constants/status_codes.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "proto/assertion.pb.h"
#include "testing/cbor_utils.h"
#include "testing/jumbf_utils.h"
#include "testing/proto_test_utils.h"
#include "testing/test_validation_tracker.h"
#include "validator/tracker.h"
namespace credentio {
namespace {
using ::cbor::FromJson;
using ::credentio::CreateActionsAssertionBoxFromCbor;
using ::credentio::CreateAssertionBoxFromCbor;
using ::credentio::CreateBmffBasedHashAssertionBoxFromCbor;
using ::credentio::CreateDataHashAssertionBoxFromCbor;
using ::credentio::StripMultipleInstanceSuffix;
using ::credentio_testing::EqualsProto;
using ::credentio_testing::ParseTextProtoOrDie;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Optional;
MATCHER_P2(FailureStatusIs, code, explanation, "") {
return arg.code() == absl::StrCat(code) &&
absl::StrContains(arg.explanation(), explanation);
}
class AssertionParserTest : public ::testing::Test {
protected:
AssertionParserTest() = default;
credentio::AssertionParser parser_;
};
TEST_F(AssertionParserTest, ParseActionsAssertion) {
auto cbor = FromJson(R"json(
{
"actions": [
{
"action": "c2pa.created",
"softwareAgent": {
"name": "Joe's Photo Editor",
"version": "2.0",
"operating_system": "Windows 10"
},
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia",
"parameters": {
"instanceID": "ed610ae51f604002be3dbf0c589a2f1f",
"ingredients" : [
{
"url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient__1",
"alg": "sha256",
"hash" : "b64'aGFzaA=='"
},
{
"url": "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient__2",
"alg": "sha256",
"hash" : "b64'aGFzaA=='"
}
]
}
},
{
"action": "c2pa.filtered",
"softwareAgent" : {
"name": "Joe's Photo Editor",
"version": "2.0",
"operating_system": "Windows 10"
}
},
{
"action": "c2pa.cropped",
"softwareAgentIndex": 1
}
],
"templates": [{
"action": "com.joesphoto.filter",
"description": "Magic Filter",
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic",
"softwareAgent" : {
"name": "Joe's Photo Editor",
"version": "2.0",
"schema.org.SoftwareApplication.operatingSystem": "Windows 10"
},
"icon": {
"url": "http://cdn.examplephotoagency.com/logo.svg",
"hash": "b64'aGFzaA=='"
}
}],
"metadata": {
"reviewRatings": [
{
"value": 1,
"explanation": "Content bindings did not validate"
}
],
"reference": {
"url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.actions__1",
"alg": "sha256",
"hash": "b64'aGFzaA=='"
}
},
"softwareAgents": [
{
"name": "Joe's Photo Editor",
"version": "2.0",
"operating_system": "Windows 10"
},
{
"name": "Bob's Photo Editor",
"version": "1.0",
"operating_system": "Windows 10"
}
]
})json");
auto expected = ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.actions.v2"
actions {
actions {
action: "c2pa.created"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia"
software_agent {
name: "Joe\'s Photo Editor"
version: "2.0"
operating_system: "Windows 10"
}
parameters {
instance_id: "ed610ae51f604002be3dbf0c589a2f1f"
ingredients {
url: "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.ingredient__1"
algorithm: "sha256"
hash: "hash"
}
ingredients {
url: "self#jumbf=c2pa/joe-ed:urn:uuid:EFGH/c2pa.assertions/c2pa.ingredient__2"
algorithm: "sha256"
hash: "hash"
}
}
}
actions {
action: "c2pa.filtered"
software_agent {
name: "Joe\'s Photo Editor"
version: "2.0"
operating_system: "Windows 10"
}
}
actions { action: "c2pa.cropped", software_agent_index: 1 }
templates {
action: "com.joesphoto.filter"
description: "Magic Filter"
digital_source_type: "http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic"
software_agent { name: "Joe\'s Photo Editor" version: "2.0" }
icon { url: "http://cdn.examplephotoagency.com/logo.svg" hash: "hash" }
}
software_agents {
name: "Joe\'s Photo Editor"
version: "2.0"
operating_system: "Windows 10"
}
software_agents {
name: "Bob\'s Photo Editor"
version: "1.0"
operating_system: "Windows 10"
}
}
)pb");
credentio::TestValidationTracker t;
EXPECT_THAT(parser_.ParseCbor("c2pa.actions.v2", cbor, t.tracker()),
Optional(EqualsProto(expected)));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseBmffBasedHashAssertion) {
credentio::TestValidationTracker t;
std::string bmff_based_hash_assertion_cbor = FromJson(R"json(
{
"hash": "b64'Zm9v'",
"name": "Example `c2pa.hash.bmff.v2` assertion",
"exclusions": [
{
"data": [{ "value": "b64'Zm9v'", "offset": 8 }],
"xpath": "/uuid"
},
{ "xpath": "/ftyp" },
{ "xpath": "/mfra" },
{ "xpath": "/moov[1]/pssh" },
{
"data": [{ "value": "b64'Zm9v'", "offset": 20 }],
"xpath": "/emsg"
}
]
})json");
EXPECT_THAT(
parser_.ParseCbor("c2pa.hash.bmff.v2", bmff_based_hash_assertion_cbor,
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.hash.bmff.v2"
bmff_based_hash {
hash: "foo"
name: "Example `c2pa.hash.bmff.v2` assertion"
exclusions {
data { value: "foo" offset: 8 }
xpath: "/uuid"
}
exclusions { xpath: "/ftyp" }
exclusions { xpath: "/mfra" }
exclusions { xpath: "/moov[1]/pssh" }
exclusions {
data { value: "foo" offset: 20 }
xpath: "/emsg"
}
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseDataHashAssertion) {
credentio::TestValidationTracker t;
std::string data_hash_assertion_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");
EXPECT_THAT(
parser_.ParseCbor("c2pa.hash.data", data_hash_assertion_cbor,
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::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"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseAiDisclosureAssertion) {
credentio::TestValidationTracker t;
std::string ai_disclosure_cbor = FromJson(R"json(
{
"modelType": "c2pa.types.model.onnx",
"modelName": "My Awesome AI Model",
"modelIdentifier": "https://example.com/models/awesome-ai-v1",
"contentProfile": {
"humanOversightLevel": "prompt_guided",
"scientificDomain": ["cs.AI"]
}
})json");
EXPECT_THAT(
parser_.ParseCbor("c2pa.ai-disclosure", ai_disclosure_cbor, t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.ai-disclosure"
ai_disclosure {
model_type: "c2pa.types.model.onnx"
model_name: "My Awesome AI Model"
model_identifier: "https://example.com/models/awesome-ai-v1"
content_profile {
human_oversight_level: "prompt_guided"
scientific_domain: "cs.AI"
}
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseEnvironmentalSustainabilityAssertion) {
credentio::TestValidationTracker t;
std::string environmental_sustainability_cbor = FromJson(R"json(
{
"energy_kwh": {
"value": 1.23,
"measurementMethod": "com.example.direct-measurement"
},
"carbon_kgco2e": {
"value": 0.456
},
"water_litres": {
"value": 7.89,
"measurementMethod": "com.example.estimation"
}
})json");
EXPECT_THAT(
parser_.ParseCbor("c2pa.environmental-sustainability",
environmental_sustainability_cbor, t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.environmental-sustainability"
environmental_sustainability {
energy_kwh {
value: 1.23
measurement_method: "com.example.direct-measurement"
}
carbon_kgco2e { value: 0.456 }
water_litres {
value: 7.89
measurement_method: "com.example.estimation"
}
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseSoftBindingAssertion) {
credentio::TestValidationTracker t;
std::string soft_binding_assertion_cbor = FromJson(R"json(
{
"alg": "soft-algorithm",
"name": "soft-binding-name",
"alg-params": "b64'cGFyYW1z'",
"blocks": [
{
"scope": {
"extent": "b64'ZXh0ZW50'",
"timespan": {
"start": 100,
"end": 200
}
},
"value": "b64'dmFsdWU='"
}
],
"bindingMetadata": {
"description": "metadata-desc",
"contact": "metadata-contact",
"informationalUrl": "metadata-url"
}
})json");
EXPECT_THAT(
parser_.ParseCbor("c2pa.soft-binding", soft_binding_assertion_cbor,
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.soft-binding"
soft_binding {
algorithm: "soft-algorithm"
name: "soft-binding-name"
alg_params: "params"
blocks {
scope {
extent: "extent"
timespan { start: 100 end: 200 }
}
value: "value"
}
binding_metadata {
description: "metadata-desc"
contact: "metadata-contact"
informational_url: "metadata-url"
}
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseInvalidSoftBindingAssertion) {
// Soft binding assertion is missing the `alg` field.
std::string soft_binding_assertion_cbor = FromJson(R"json(
{
"name": "soft-binding-name"
})json");
credentio::TestValidationTracker t;
EXPECT_THAT(parser_.ParseCbor("c2pa.soft-binding",
soft_binding_assertion_cbor, t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kAssertionCborInvalid,
"c2pa.soft-binding")));
}
TEST_F(AssertionParserTest, ParseIngredientAssertionV2Unsupported) {
credentio::TestValidationTracker t;
std::string ingredient_0_assertion_cbor = FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"relationship": "ingredient-relationship",
"documentID": "ingredient-document-id",
"instanceID": "ingredient-instance-id",
"data": {
"url": "ingredient-data-url",
"alg": "ingredient-data-alg",
"hash": "b64'aGFzaA=='",
"size": 1234567890
},
"c2pa_manifest": {
"url": "ingredient-manifest-url",
"alg": "ingredient-manifest-alg",
"hash": "b64'aGFzaA=='"
},
"thumbnail": {
"url": "ingredient-thumbnail-url",
"alg": "ingredient-thumbnail-alg",
"hash": "b64'aGFzaA=='"
},
"validationStatus": [
{
"code": "status.1",
"url": "url.1",
"explanation": "explanation.1"
},
{
"code": "status.2",
"url": "url.2",
"explanation": "explanation.2"
}
],
"description": "ingredient-description",
"informational_URI": "ingredient-informational-uri",
"metadata": {
"reviewRatings": [
{
"value": 1,
"explanation": "can't dance to it"
}
]
}
})json");
EXPECT_THAT(parser_.ParseCbor("c2pa.ingredient.v2",
ingredient_0_assertion_cbor, t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.tracker().GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kGoogleUnsupportedSpecVersion,
"c2pa.ingredient.v2")));
}
TEST_F(AssertionParserTest, ParseIngredientAssertionV3) {
credentio::TestValidationTracker t;
std::string ingredient_0_assertion_cbor = FromJson(R"json(
{
"dc:title": "image 1.jpg",
"metadata": {
"reviewRatings": [
{
"value": 5,
"explanation": "Content bindings validated"
}
]
},
"dc:format": "image/jpeg",
"thumbnail": {
"url": "self#jumbf=c2pa/urn:c2pa:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.thumbnail.ingredient_1.jpg",
"hash": "b64'UjRAYWiAq4lfCRDmksWAlDJN/XtHHFFwMWymsZsm3j8='"
},
"relationship": "parentOf",
"activeManifest": {
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322",
"hash": "b64'1kjJTO108b71cL95UxgfHD3eDgk9VrCedW8n3fYTRMk='"
},
"claimSignature": {
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature",
"hash": "b64'85KAvU3+3YgtIjj6IV0fzKwj8si/85+gevVSK2Iw+S0='"
},
"validationResults": {
"activeManifest": {
"success": [
{
"code": "claimSignature.validated",
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
},
{
"code": "signingCredential.trusted",
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
},
{
"code": "timeStamp.validated",
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
},
{
"code": "timeStamp.trusted",
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
},
{
"code": "assertion.hashedURI.match",
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3"
}
],
"informational": [{
"code": "signingCredential.ocsp.skipped",
"url": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}],
"failure": []
},
"ingredientDeltas": [
{
"ingredientAssertionURI": "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3",
"validationDeltas": {
"success": [],
"informational": [],
"failure": [{
"code": "assertion.hashedURI.mismatch",
"url": "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.metadata"
}]
}
},
{
"ingredientAssertionURI": "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.ingredient.v3",
"validationDeltas": {
"success": [],
"informational": [],
"failure": [{
"code": "signingCredential.untrusted",
"url": "self#jumbf=c2pa/urn:c2pa:72C28A7C-7F5B-4301-B373-3183C10AF7C5/c2pa.signature"
}]
}
}
]
}
})json");
auto result = parser_.ParseCbor("c2pa.ingredient.v3",
ingredient_0_assertion_cbor, t.tracker());
ASSERT_THAT(t.GetFailureStatuses(), IsEmpty());
EXPECT_THAT(
result,
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.ingredient.v3"
ingredient_v3 {
title: "image 1.jpg"
media_type: "image/jpeg"
relationship: "parentOf"
active_manifest {
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322"
hash: "\326H\311L\355t\361\276\365p\277yS\030\037\034=\336\016\t=V\260\236uo\'\335\366\023D\311"
}
claim_signature {
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
hash: "\363\222\200\275M\376\335\210-\"8\372!]\037\314\254#\362\310\277\363\237\240z\365R+b0\371-"
}
thumbnail {
url: "self#jumbf=c2pa/urn:c2pa:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.thumbnail.ingredient_1.jpg"
hash: "R4@ah\200\253\211_\t\020\346\222\305\200\2242M\375{G\034Qp1l\246\261\233&\336?"
}
validation_results {
active_manifest {
successes {
code: "claimSignature.validated"
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
successes {
code: "signingCredential.trusted"
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
successes {
code: "timeStamp.validated"
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
successes {
code: "timeStamp.trusted"
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
successes {
code: "assertion.hashedURI.match"
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3"
}
informationals {
code: "signingCredential.ocsp.skipped"
url: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.signature"
}
}
ingredient_deltas {
ingredient_assertion_uri: "self#jumbf=c2pa/urn:c2pa:5E7B01FC-4932-4BAB-AB32-D4F12A8AA322/c2pa.assertions/c2pa.ingredient.v3"
validation_deltas {
failures {
code: "assertion.hashedURI.mismatch"
url: "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.metadata"
}
}
}
ingredient_deltas {
ingredient_assertion_uri: "self#jumbf=c2pa/urn:c2pa:F095F30E-6CD5-4BF7-8C44-CE8420CA9FB7/c2pa.assertions/c2pa.ingredient.v3"
validation_deltas {
failures {
code: "signingCredential.untrusted"
url: "self#jumbf=c2pa/urn:c2pa:72C28A7C-7F5B-4301-B373-3183C10AF7C5/c2pa.signature"
}
}
}
}
}
)pb"))));
}
TEST_F(AssertionParserTest, ParseBoxesHashAssertion) {
credentio::TestValidationTracker t;
std::string boxes_hash_assertion_cbor = FromJson(R"json(
{
"alg" : "sha256",
"boxes": [
{
"names" : ["SOI", "APP0", "APP2"],
"hash" : "b64'Zm9v'"
},
{
"names" : ["C2PA"],
"hash" : "b64'YmFy'"
}
]
})json");
EXPECT_THAT(
parser_.ParseCbor("c2pa.hash.boxes", boxes_hash_assertion_cbor,
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.hash.boxes"
boxes_hash {
alg: "sha256"
boxes { names: "SOI" names: "APP0" names: "APP2" hash: "foo" }
boxes { names: "C2PA" hash: "bar" }
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseThumbnailClaimAssertion) {
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseThumbnail("c2pa.thumbnail.claim.png", "image/png", "foo",
"foo.png", t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.thumbnail.claim.png"
claim_thumbnail {
media_type: "image/png"
data: "foo"
file_name: "foo.png"
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseThumbnailIngredientAssertion) {
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseThumbnail("c2pa.thumbnail.ingredient.png", "image/png",
"foo", "foo.png", t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.thumbnail.ingredient.png"
ingredient_thumbnail {
media_type: "image/png"
data: "foo"
file_name: "foo.png"
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseThumbnailAssertionUnrecognizedLabelIgnored) {
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseThumbnail("c2pa.thumbnail.something.png", "image/png", "foo",
std::nullopt, t.tracker()),
Eq(std::nullopt));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseMetadataAssertion) {
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseMetadata("c2pa.metadata",
R"json({
"@context": "bar",
"foo": "baz"
})json",
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.metadata"
metadata {
value {
fields {
key: "@context"
value { string_value: "bar" }
}
fields {
key: "foo"
value { string_value: "baz" }
}
}
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseMetadataAssertionCustomLabel) {
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseMetadata("com.litware.metadata",
R"json({
"@context": "bar",
"foo": "baz"
})json",
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "com.litware.metadata"
metadata {
value {
fields {
key: "@context"
value { string_value: "bar" }
}
fields {
key: "foo"
value { string_value: "baz" }
}
}
}
)pb"))));
EXPECT_THAT(t.tracker().GetFailureStatuses(), IsEmpty());
}
TEST_F(AssertionParserTest, ParseMetadataAssertionMissingContext) {
credentio::TestValidationTracker t;
EXPECT_THAT(parser_.ParseMetadata("c2pa.metadata",
R"json({
"@something": "bar"
})json",
t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.tracker().GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kGeneralError, "@context")));
}
TEST_F(AssertionParserTest, ParseMetadataAssertionInvalidJson) {
credentio::TestValidationTracker t;
EXPECT_THAT(parser_.ParseMetadata("c2pa.metadata",
R"json({
"foo":
})json",
t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.tracker().GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kAssertionJsonInvalid,
"c2pa.metadata")));
}
TEST_F(AssertionParserTest, ParseRepositoryReceiptAssertion) {
credentio::TestValidationTracker t;
std::string repository_receipt_json = R"json(
{
"repository": {
"uri": "https://repository.example.com/manifests/xyz123",
"manifestId": "urn:uuid:12345678-1234-5678-1234-567812345678"
},
"anchor": {
"uri": "https://anchor.example.com/receipts/9876",
"parameters": {
"version": "1.0"
},
"proof": {
"signature": "c2lnbmF0dXJl"
}
}
})json";
EXPECT_THAT(
parser_.ParseMetadata("c2pa.repository-receipt", repository_receipt_json,
t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.repository-receipt"
repository_receipt {
repository {
uri: "https://repository.example.com/manifests/xyz123"
manifest_id: "urn:uuid:12345678-1234-5678-1234-567812345678"
}
anchor {
uri: "https://anchor.example.com/receipts/9876"
parameters {
fields {
key: "version"
value { string_value: "1.0" }
}
}
proof {
fields {
key: "signature"
value { string_value: "c2lnbmF0dXJl" }
}
}
}
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseInvalidActionsAssertion) {
// Actions assertion is missing the `action` field.
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [
{
}
]
})json");
auto actions_assertion_box =
CreateActionsAssertionBoxFromCbor(actions_assertion_cbor);
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kAssertionCborInvalid,
"c2pa.actions.v2")));
}
TEST_F(AssertionParserTest, ParseInvalidBmffBasedHashAssertion) {
// `xpath` field is missing in the exclusion.
std::string bmff_based_hash_assertion_cbor = FromJson(R"json(
{ "exclusions": [{}] }
)json");
auto bmff_based_hash_assertion_box =
CreateBmffBasedHashAssertionBoxFromCbor(bmff_based_hash_assertion_cbor);
credentio::TestValidationTracker t;
EXPECT_THAT(parser_.ParseCbor("c2pa.hash.bmff.v2",
bmff_based_hash_assertion_cbor, t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kAssertionCborInvalid,
"c2pa.hash.bmff.v2")));
}
TEST_F(AssertionParserTest, ParseInvalidDataHashAssertion) {
// Data hash assertion is missing the `hash` field.
std::string data_hash_assertion_cbor = FromJson(R"json({})json");
auto data_hash_assertion_box =
CreateDataHashAssertionBoxFromCbor(data_hash_assertion_cbor);
credentio::TestValidationTracker t;
EXPECT_THAT(parser_.ParseCbor("c2pa.hash.data", data_hash_assertion_cbor,
t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kAssertionCborInvalid,
"c2pa.hash.data")));
}
TEST_F(AssertionParserTest, ParseAssertionWithUnsupportedLabel) {
std::string actions_assertion_cbor = FromJson(R"json({})json");
auto actions_assertion_box =
CreateAssertionBoxFromCbor("random_label", actions_assertion_cbor);
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseCbor("random_label", actions_assertion_cbor, t.tracker()),
Eq(std::nullopt));
// Skip recording failures for now.
EXPECT_THAT(t.GetFailures(), IsEmpty());
}
TEST_F(AssertionParserTest, ParseIngredientAssertionV1Unsupported) {
credentio::TestValidationTracker t;
std::string ingredient_0_assertion_cbor = FromJson(R"json(
{
"dc:title": "ingredient-title",
"dc:format": "ingredient-format",
"documentID": "ingredient-document-id",
"instanceID": "ingredient-instance-id",
"relationship": "ingredient-relationship",
"c2pa_manifest": {
"url": "ingredient-manifest-url",
"alg": "ingredient-manifest-alg",
"hash": "b64'aGFzaA=='"
},
"thumbnail": {
"url": "ingredient-thumbnail-url",
"alg": "ingredient-thumbnail-alg",
"hash": "b64'aGFzaA=='"
},
"validationStatus": [
{
"code": "status.1",
"url": "url.1",
"explanation": "explanation.1"
},
{
"code": "status.2",
"url": "url.2",
"explanation": "explanation.2"
}
],
"metadata": {
"reviewRatings": [
{
"value": 1,
"explanation": "can't dance to it"
}
]
}
})json");
EXPECT_THAT(parser_.ParseCbor("c2pa.ingredient", ingredient_0_assertion_cbor,
t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.tracker().GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kGoogleUnsupportedSpecVersion,
"c2pa.ingredient")));
}
TEST_F(AssertionParserTest, ParseMultiAssetHashAssertion) {
credentio::TestValidationTracker t;
std::string multi_asset_hash_assertion_cbor = FromJson(R"json(
{
"parts": [
{
"location": {
"byteOffset": 0,
"length": 100
},
"hashAssertion": {
"url": "hash-url-1",
"alg": "sha256",
"hash": "b64'aGFzaA=='"
}
},
{
"location": {
"bmffBox": "test-bmff-box"
},
"hashAssertion": {
"url": "hash-url-2",
"alg": "sha256",
"hash": "b64'aGFzaA=='"
},
"optional": true
}
],
"pad": "b64'Zm9v'"
})json");
EXPECT_THAT(
parser_.ParseCbor("c2pa.hash.multi-asset",
multi_asset_hash_assertion_cbor, t.tracker()),
Optional(EqualsProto(ParseTextProtoOrDie<credentio::Assertion>(R"pb(
label: "c2pa.hash.multi-asset"
multi_asset_hash {
parts {
location { byte_offset: 0 length: 100 }
hash_assertion {
url: "hash-url-1"
algorithm: "sha256"
hash: "hash"
}
}
parts {
location { bmff_box: "test-bmff-box" }
hash_assertion {
url: "hash-url-2"
algorithm: "sha256"
hash: "hash"
}
is_optional: true
}
pad: "foo"
}
)pb"))));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST(StripMultipleInstanceSuffixTest, Works) {
EXPECT_EQ(StripMultipleInstanceSuffix("c2pa.hash.data"), "c2pa.hash.data");
EXPECT_EQ(StripMultipleInstanceSuffix("c2pa.hash.data__1"), "c2pa.hash.data");
EXPECT_EQ(StripMultipleInstanceSuffix("c2pa.hash.data__13"),
"c2pa.hash.data");
EXPECT_EQ(StripMultipleInstanceSuffix(""), "");
}
TEST_F(AssertionParserTest, ParseActionsAssertionEmptyRelatedAssertions) {
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [
{
"action": "c2pa.opened",
"parameters": {
"relatedAssertions": []
}
}
]
})json");
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()),
Eq(std::nullopt));
EXPECT_THAT(t.GetFailureStatuses(),
testing::Contains(FailureStatusIs(
credentio::FailureStatusCode::kAssertionCborInvalid,
"c2pa.actions.v2")));
}
TEST_F(AssertionParserTest, ParseActionsAssertionMissingRelatedAssertions) {
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [
{
"action": "c2pa.opened",
"parameters": {
"instanceID": "ed610ae51f604002be3dbf0c589a2f1f"
}
}
]
})json");
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()),
testing::Ne(std::nullopt));
EXPECT_FALSE(t.tracker().HasFailures());
}
TEST_F(AssertionParserTest, ParseActionsAssertionNonEmptyRelatedAssertions) {
std::string actions_assertion_cbor = FromJson(R"json(
{
"actions": [
{
"action": "c2pa.opened",
"parameters": {
"relatedAssertions": [
{
"url": "self#jumbf=c2pa/joe-ed:urn:uuid:ABCD/c2pa.assertions/c2pa.other",
"alg": "sha256",
"hash" : "b64'aGFzaA=='"
}
]
}
}
]
})json");
credentio::TestValidationTracker t;
EXPECT_THAT(
parser_.ParseCbor("c2pa.actions.v2", actions_assertion_cbor, t.tracker()),
testing::Ne(std::nullopt));
EXPECT_FALSE(t.tracker().HasFailures());
}
} // namespace
} // namespace credentio