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