| // 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 "utils/crjson.h" | |
| #include <cstdint> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/status/status_matchers.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/escaping.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "constants/labels.h" | |
| #include "cppbor/cppbor.h" | |
| #include "gmock/gmock.h" | |
| #include "gtest/gtest.h" | |
| #include "jumbf/constants.h" | |
| #include "jumbf/test_utils.h" | |
| #include "nlohmann/json.hpp" | |
| #include "proto/ingredient_assertion.pb.h" | |
| #include "proto/ingredient_validation_result.pb.h" | |
| #include "proto/manifest.pb.h" | |
| #include "proto/validation_result.pb.h" | |
| #include "proto/validation_status.pb.h" | |
| namespace credentio { | |
| namespace { | |
| TEST(ConvertManifestStoreToCrJsonTest, ByteStringsAreBase64Encoded) { | |
| // 1. Construct a CBOR map containing a byte string. | |
| cppbor::Map map; | |
| map.add("hash_value", cppbor::Bstr("raw_bytes_here")); | |
| std::vector<uint8_t> encoded_cbor = map.encode(); | |
| absl::string_view cbor_view( | |
| reinterpret_cast<const char*>(encoded_cbor.data()), encoded_cbor.size()); | |
| // 2. Wrap it in a CborBox. | |
| std::string cbor_box = jumbf::EncodeCborBox(cbor_view); | |
| // 3. Wrap that in a SuperBox (representing an assertion). | |
| // Use a standard allowlisted assertion label, e.g., kDataHashAssertionLabel. | |
| std::string assertion_box = jumbf::EncodeSuperBox( | |
| jumbf::kCborBoxTypeUuid, kDataHashAssertionLabel, {cbor_box}, | |
| /*requestable=*/true); | |
| // 4. Wrap that in an assertion store SuperBox. | |
| std::string assertion_store_box = jumbf::EncodeSuperBox( | |
| kAssertionStoreUuid, kAssertionStoreLabel, {assertion_box}); | |
| // 5. Wrap that in a manifest SuperBox. | |
| std::string manifest_box = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "test_manifest", {assertion_store_box}); | |
| // 6. Wrap that in a manifest store SuperBox. | |
| std::string manifest_store_box = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); | |
| // 7. Call ConvertManifestStoreToCrJsonWithoutValidationResults. | |
| absl::StatusOr<nlohmann::json> j = | |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); | |
| ABSL_ASSERT_OK(j); | |
| // 8. Verify that the output JSON contains the base64 encoded byte string. | |
| // The expected format is "b64'<base64_data>'". | |
| std::string expected_b64 = | |
| absl::StrCat("b64'", absl::Base64Escape("raw_bytes_here"), "'"); | |
| ASSERT_TRUE(j->contains("manifests")); | |
| ASSERT_GT((*j)["manifests"].size(), 0); | |
| const auto& manifest = (*j)["manifests"][0]; | |
| ASSERT_TRUE(manifest.contains("assertions")); | |
| const auto& assertions = manifest["assertions"]; | |
| ASSERT_TRUE(assertions.contains(kDataHashAssertionLabel)); | |
| const auto& assertion_json = assertions[kDataHashAssertionLabel]; | |
| EXPECT_EQ(assertion_json["hash_value"], expected_b64); | |
| } | |
| TEST(ConvertManifestStoreToCrJsonTest, UnrecognizedAssertionIsIncluded) { | |
| // 1. Construct a CBOR map for a dummy assertion. | |
| cppbor::Map map; | |
| map.add("foo", "bar"); | |
| std::vector<uint8_t> encoded_cbor = map.encode(); | |
| absl::string_view cbor_view( | |
| reinterpret_cast<const char*>(encoded_cbor.data()), encoded_cbor.size()); | |
| // 2. Wrap it in a CborBox. | |
| std::string cbor_box = jumbf::EncodeCborBox(cbor_view); | |
| // 3. Wrap that in a SuperBox with an unrecognized label. | |
| std::string assertion_box = jumbf::EncodeSuperBox( | |
| jumbf::kCborBoxTypeUuid, "custom.unrecognized.assertion", {cbor_box}, | |
| /*requestable=*/true); | |
| // 4. Wrap that in an assertion store SuperBox. | |
| std::string assertion_store_box = jumbf::EncodeSuperBox( | |
| kAssertionStoreUuid, kAssertionStoreLabel, {assertion_box}); | |
| // 5. Wrap that in a manifest SuperBox. | |
| std::string manifest_box = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "test_manifest", {assertion_store_box}); | |
| // 6. Wrap that in a manifest store SuperBox. | |
| std::string manifest_store_box = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); | |
| // 7. Call ConvertManifestStoreToCrJsonWithoutValidationResults. | |
| absl::StatusOr<nlohmann::json> j = | |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); | |
| ABSL_ASSERT_OK(j); | |
| // 8. Verify that the output JSON contains the unrecognized assertion with | |
| // empty object. | |
| ASSERT_TRUE(j->contains("manifests")); | |
| ASSERT_GT((*j)["manifests"].size(), 0); | |
| const auto& manifest = (*j)["manifests"][0]; | |
| ASSERT_TRUE(manifest.contains("assertions")); | |
| const auto& assertions = manifest["assertions"]; | |
| ASSERT_TRUE(assertions.contains("custom.unrecognized.assertion")); | |
| EXPECT_TRUE(assertions["custom.unrecognized.assertion"].is_object()); | |
| EXPECT_TRUE(assertions["custom.unrecognized.assertion"].empty()); | |
| } | |
| TEST(ConvertManifestStoreToCrJsonTest, HandlesNonSuperBoxInAssertionStore) { | |
| // 1. Construct a CBOR box directly. This is not a SuperBox. | |
| std::string cbor_box = jumbf::EncodeCborBox("test"); | |
| // 2. Wrap that in an assertion store SuperBox. This is the error condition. | |
| std::string assertion_store_box = jumbf::EncodeSuperBox( | |
| kAssertionStoreUuid, kAssertionStoreLabel, {cbor_box}); | |
| // 3. Wrap that in a manifest SuperBox. | |
| std::string manifest_box = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "test_manifest", {assertion_store_box}); | |
| // 4. Wrap that in a manifest store SuperBox. | |
| std::string manifest_store_box = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); | |
| // 5. Call ConvertManifestStoreToCrJsonWithoutValidationResults. | |
| absl::StatusOr<nlohmann::json> j = | |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); | |
| ABSL_ASSERT_OK(j); | |
| // 6. Verify that the output JSON contains the error message. | |
| ASSERT_TRUE(j->contains("manifests")); | |
| ASSERT_GT((*j)["manifests"].size(), 0); | |
| const auto& manifest = (*j)["manifests"][0]; | |
| ASSERT_TRUE(manifest.contains("assertions")); | |
| const auto& assertions = manifest["assertions"]; | |
| // The error should be at the top level of the "assertions" object because | |
| // the structure is invalid before even getting to a specific assertion label. | |
| ASSERT_TRUE(assertions.contains("_error")); | |
| EXPECT_EQ(assertions["_error"], | |
| "Assertion store contains non-SuperBox content"); | |
| } | |
| TEST(ConvertManifestStoreToCrJsonTest, ClaimV2IsConverted) { | |
| // 1. Construct claim_generator_info map. | |
| cppbor::Map generator_info; | |
| generator_info.add("name", "Test Generator"); | |
| generator_info.add("version", "1.0"); | |
| // 2. Construct created_assertions array. | |
| cppbor::Array created_assertions; | |
| cppbor::Map assertion_link; | |
| assertion_link.add("url", "self#jumbf=/c2pa/assertions/custom.assertion"); | |
| created_assertions.add(std::move(assertion_link)); | |
| // 3. Construct the claim map. | |
| cppbor::Map claim_map; | |
| claim_map.add("instanceID", "test_instance_id"); | |
| claim_map.add("claim_generator_info", std::move(generator_info)); | |
| claim_map.add("signature", "self#jumbf=/c2pa/signature"); | |
| claim_map.add("created_assertions", std::move(created_assertions)); | |
| claim_map.add("dc:title", "Test Title"); | |
| std::vector<uint8_t> encoded_claim = claim_map.encode(); | |
| absl::string_view claim_view( | |
| reinterpret_cast<const char*>(encoded_claim.data()), | |
| encoded_claim.size()); | |
| // 4. Wrap in CborBox. | |
| std::string cbor_box = jumbf::EncodeCborBox(claim_view); | |
| // 5. Wrap in Claim V2 SuperBox. | |
| std::string claim_box = jumbf::EncodeSuperBox(jumbf::kCborBoxTypeUuid, | |
| "c2pa.claim.v2", {cbor_box}); | |
| // 6. Wrap in manifest SuperBox. | |
| std::string manifest_box = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "test_manifest", {claim_box}); | |
| // 7. Wrap in manifest store SuperBox. | |
| std::string manifest_store_box = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); | |
| // 8. Call ConvertManifestStoreToCrJsonWithoutValidationResults. | |
| absl::StatusOr<nlohmann::json> j = | |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); | |
| ABSL_ASSERT_OK(j); | |
| // 9. Verify claim.v2 fields in output JSON. | |
| ASSERT_TRUE(j->contains("manifests")); | |
| ASSERT_GT((*j)["manifests"].size(), 0); | |
| const auto& manifest = (*j)["manifests"][0]; | |
| ASSERT_TRUE(manifest.contains("claim.v2")); | |
| const auto& claim_json = manifest["claim.v2"]; | |
| EXPECT_EQ(claim_json["instanceID"], "test_instance_id"); | |
| EXPECT_EQ(claim_json["dc:title"], "Test Title"); | |
| EXPECT_EQ(claim_json["signature"], "self#jumbf=/c2pa/signature"); | |
| ASSERT_TRUE(claim_json.contains("claim_generator_info")); | |
| EXPECT_EQ(claim_json["claim_generator_info"]["name"], "Test Generator"); | |
| EXPECT_EQ(claim_json["claim_generator_info"]["version"], "1.0"); | |
| ASSERT_TRUE(claim_json.contains("created_assertions")); | |
| ASSERT_GT(claim_json["created_assertions"].size(), 0); | |
| EXPECT_EQ(claim_json["created_assertions"][0]["url"], | |
| "self#jumbf=/c2pa/assertions/custom.assertion"); | |
| } | |
| TEST(ConvertManifestStoreToCrJsonTest, ClaimV2WithMetadataIsConverted) { | |
| // 1. Construct claim_generator_info map. | |
| cppbor::Map generator_info; | |
| generator_info.add("name", "Test Generator"); | |
| generator_info.add("version", "1.0"); | |
| // 2. Construct created_assertions array. | |
| cppbor::Array created_assertions; | |
| cppbor::Map assertion_link; | |
| assertion_link.add("url", "self#jumbf=/c2pa/assertions/custom.assertion"); | |
| created_assertions.add(std::move(assertion_link)); | |
| // 3. Construct metadata map. | |
| cppbor::Map metadata_map; | |
| metadata_map.add("dateTime", "2023-05-21T00:00:00Z"); | |
| // rating | |
| cppbor::Array rating_array; | |
| cppbor::Map rating_map; | |
| rating_map.add("value", "suitable-for-all"); | |
| rating_map.add("code", "G"); | |
| rating_map.add("explanation", "General Audiences"); | |
| rating_array.add(std::move(rating_map)); | |
| metadata_map.add("rating", std::move(rating_array)); | |
| // dataSource | |
| cppbor::Map datasource_map; | |
| datasource_map.add("type", "camera"); | |
| datasource_map.add("details", "Captured by hardware"); | |
| metadata_map.add("dataSource", std::move(datasource_map)); | |
| // reference | |
| cppbor::Map reference_map; | |
| reference_map.add("url", "self#jumbf=/c2pa/assertions/custom.assertion"); | |
| metadata_map.add("reference", std::move(reference_map)); | |
| // localizations | |
| cppbor::Array localizations_array; | |
| cppbor::Map localization_map; | |
| localization_map.add("en", "English"); | |
| localizations_array.add(std::move(localization_map)); | |
| metadata_map.add("localizations", std::move(localizations_array)); | |
| // regionOfInterest | |
| cppbor::Map region_map; | |
| region_map.add("foo", "bar"); | |
| metadata_map.add("regionOfInterest", std::move(region_map)); | |
| // 4. Construct the claim map. | |
| cppbor::Map claim_map; | |
| claim_map.add("instanceID", "test_instance_id"); | |
| claim_map.add("claim_generator_info", std::move(generator_info)); | |
| claim_map.add("signature", "self#jumbf=/c2pa/signature"); | |
| claim_map.add("created_assertions", std::move(created_assertions)); | |
| claim_map.add("dc:title", "Test Title"); | |
| claim_map.add("metadata", std::move(metadata_map)); | |
| std::vector<uint8_t> encoded_claim = claim_map.encode(); | |
| absl::string_view claim_view( | |
| reinterpret_cast<const char*>(encoded_claim.data()), | |
| encoded_claim.size()); | |
| // 5. Wrap in CborBox. | |
| std::string cbor_box = jumbf::EncodeCborBox(claim_view); | |
| // 6. Wrap in Claim V2 SuperBox. | |
| std::string claim_box = jumbf::EncodeSuperBox(jumbf::kCborBoxTypeUuid, | |
| "c2pa.claim.v2", {cbor_box}); | |
| // 7. Wrap in manifest SuperBox. | |
| std::string manifest_box = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "test_manifest", {claim_box}); | |
| // 8. Wrap in manifest store SuperBox. | |
| std::string manifest_store_box = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); | |
| // 9. Call ConvertManifestStoreToCrJsonWithoutValidationResults. | |
| absl::StatusOr<nlohmann::json> j = | |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); | |
| ABSL_ASSERT_OK(j); | |
| // 10. Verify metadata fields in output JSON. | |
| ASSERT_TRUE(j->contains("manifests")); | |
| ASSERT_GT((*j)["manifests"].size(), 0); | |
| const auto& manifest = (*j)["manifests"][0]; | |
| ASSERT_TRUE(manifest.contains("claim.v2")); | |
| const auto& claim_json = manifest["claim.v2"]; | |
| ASSERT_TRUE(claim_json.contains("metadata")); | |
| const auto& metadata_json = claim_json["metadata"]; | |
| EXPECT_EQ(metadata_json["dateTime"], "2023-05-21T00:00:00Z"); | |
| ASSERT_TRUE(metadata_json.contains("rating")); | |
| ASSERT_GT(metadata_json["rating"].size(), 0); | |
| EXPECT_EQ(metadata_json["rating"][0]["value"], "suitable-for-all"); | |
| EXPECT_EQ(metadata_json["rating"][0]["code"], "G"); | |
| EXPECT_EQ(metadata_json["rating"][0]["explanation"], "General Audiences"); | |
| ASSERT_TRUE(metadata_json.contains("dataSource")); | |
| EXPECT_EQ(metadata_json["dataSource"]["type"], "camera"); | |
| EXPECT_EQ(metadata_json["dataSource"]["details"], "Captured by hardware"); | |
| ASSERT_TRUE(metadata_json.contains("reference")); | |
| EXPECT_EQ(metadata_json["reference"]["url"], | |
| "self#jumbf=/c2pa/assertions/custom.assertion"); | |
| // Localizations and RegionOfInterest are currently converted to empty objects | |
| // but we still verify they exist. | |
| ASSERT_TRUE(metadata_json.contains("localizations")); | |
| EXPECT_TRUE(metadata_json["localizations"].is_array()); | |
| ASSERT_TRUE(metadata_json.contains("regionOfInterest")); | |
| EXPECT_TRUE(metadata_json["regionOfInterest"].is_object()); | |
| } | |
| TEST(ConvertManifestStoreToCrJsonTest, ClaimSignatureAlgorithmsAreConverted) { | |
| struct TestCase { | |
| int64_t cose_alg; | |
| absl::string_view expected_alg_str; | |
| }; | |
| const TestCase kTestCases[] = { | |
| {-7, "ES256"}, {-35, "ES384"}, {-36, "ES512"}, {-37, "PS256"}, | |
| {-38, "PS384"}, {-39, "PS512"}, {-8, "EdDSA"}, | |
| }; | |
| for (const auto& tc : kTestCases) { | |
| cppbor::Map protected_header_map; | |
| protected_header_map.add(cppbor::Uint(1), cppbor::Nint(tc.cose_alg)); | |
| std::string protected_header = protected_header_map.toString(); | |
| cppbor::Array cose_array; | |
| cose_array.add(cppbor::Bstr(protected_header)); | |
| cose_array.add(cppbor::Map()); | |
| cose_array.add(cppbor::Null()); | |
| cose_array.add(cppbor::Bstr("dummy_sig")); | |
| cppbor::SemanticTag cose_sign1(18, std::move(cose_array)); | |
| std::string encoded_cose = cose_sign1.toString(); | |
| std::string cbor_box = jumbf::EncodeCborBox(encoded_cose); | |
| std::string sig_box = jumbf::EncodeSuperBox( | |
| jumbf::kCborBoxTypeUuid, kClaimSignatureLabel, {cbor_box}); | |
| std::string manifest_box = jumbf::EncodeSuperBox( | |
| kStandardManifestUuid, "test_manifest", {sig_box}); | |
| std::string manifest_store_box = jumbf::EncodeSuperBox( | |
| kManifestStoreUuid, kManifestStoreLabel, {manifest_box}); | |
| absl::StatusOr<nlohmann::json> j = | |
| ConvertToCrJsonWithoutValidationResults(manifest_store_box); | |
| ABSL_ASSERT_OK(j); | |
| ASSERT_TRUE(j->contains("manifests")); | |
| ASSERT_EQ((*j)["manifests"].size(), 1); | |
| const auto& manifest = (*j)["manifests"][0]; | |
| ASSERT_TRUE(manifest.contains("signature")); | |
| EXPECT_EQ(manifest["signature"]["algorithm"], tc.expected_alg_str); | |
| } | |
| } | |
| } // namespace | |
| } // namespace credentio |