| // 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 <cstddef> | |
| #include <cstdint> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/nullability.h" | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/numbers.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include "cbor/cbor.h" | |
| #include "cbor/parse.h" | |
| #include "constants/labels.h" | |
| #include "cose/sig_structure.h" | |
| #include "cose/simple_cms_parser.h" | |
| #include "crypto/algorithms.h" | |
| #include "google/protobuf/repeated_ptr_field.h" | |
| #include "jumbf/box.h" | |
| #include "jumbf/convert_json.h" // IWYU pragma: keep | |
| #include "jumbf/parse.h" | |
| #include "nlohmann/json.hpp" | |
| #include "nlohmann/json_fwd.hpp" | |
| #include "openssl/asn1.h" | |
| #include "openssl/bn.h" | |
| #include "openssl/mem.h" | |
| #include "openssl/obj.h" | |
| #include "openssl/x509.h" | |
| #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" | |
| #include "tsp/timestamp_verifier.h" | |
| #include "utils/crjson_utils.h" | |
| #include "validator/result.h" | |
| namespace credentio { | |
| namespace { | |
| using Json = ::nlohmann::json; | |
| // ============================================================================= | |
| // JUMBF/CBOR Helpers | |
| // ============================================================================= | |
| template <typename T> | |
| absl::StatusOr<const T&> EnsureAndGetSingleBox(const jumbf::SuperBox& box) { | |
| if (box.contents.size() != 1) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat(box.description.label.value_or("unspecified"), | |
| " box must contain exactly one box")); | |
| } | |
| if (!box.contents[0].Holds<T>()) { | |
| return absl::InvalidArgumentError(absl::StrCat( | |
| box.description.label.value_or("unspecified"), | |
| " must contained box of index: ", box.contents[0].payload.index())); | |
| } | |
| return box.contents[0].Get<T>(); | |
| } | |
| template <typename T> | |
| struct ParsedCbor { | |
| std::unique_ptr<cbor::ParseResult> parse_result; | |
| T cbor_view; | |
| }; | |
| absl::StatusOr<ParsedCbor<cbor::MapView>> ParseCborMap(absl::string_view cbor) { | |
| ABSL_ASSIGN_OR_RETURN(std::unique_ptr<cbor::ParseResult> parse_result, | |
| cbor::Parse(cbor)); | |
| ABSL_ASSIGN_OR_RETURN(cbor::MapView cbor_map, parse_result->AsMap()); | |
| return ParsedCbor<cbor::MapView>{std::move(parse_result), cbor_map}; | |
| } | |
| absl::StatusOr<ParsedCbor<cbor::MapView>> GetCborMap( | |
| const jumbf::SuperBox& box) { | |
| ABSL_ASSIGN_OR_RETURN(const jumbf::CborBox& cbor_box, | |
| EnsureAndGetSingleBox<jumbf::CborBox>(box)); | |
| return ParseCborMap(cbor_box.payload); | |
| } | |
| enum class ManifestType { | |
| kUnknown, | |
| kStandard, | |
| kUpdate, | |
| kCompressed, | |
| kTimestamp, | |
| }; | |
| ManifestType GetManifestType(const jumbf::SuperBox& superbox) { | |
| const auto& uuid = superbox.description.type_uuid; | |
| if (uuid == kStandardManifestUuid) { | |
| return ManifestType::kStandard; | |
| } | |
| if (uuid == kUpdateManifestUuid) { | |
| return ManifestType::kUpdate; | |
| } | |
| if (uuid == kCompressedManifestUuid) { | |
| return ManifestType::kCompressed; | |
| } | |
| if (uuid == kTimestampManifestUuid) { | |
| return ManifestType::kTimestamp; | |
| } | |
| return ManifestType::kUnknown; | |
| } | |
| // ============================================================================= | |
| // Generic Array Converters | |
| // ============================================================================= | |
| Json ConvertArrayOfStrings(uint32_t _, const cbor::ItemView& item) { | |
| auto val = item.GetString(); | |
| if (!val.ok()) { | |
| return Json({{"_error", val.status().message()}}); | |
| } | |
| return Json(*val); | |
| } | |
| // ============================================================================= | |
| // Assertion Converters | |
| // ============================================================================= | |
| Json ConvertThumbnailAssertion(const jumbf::SuperBox& box) { | |
| Json crjson = Json::object(); | |
| absl::string_view label = box.description.label.value_or(""); | |
| if (absl::StartsWith(label, "c2pa.thumbnail.claim")) { | |
| crjson["thumbnailType"] = 0; | |
| } else if (absl::StartsWith(label, "c2pa.thumbnail.ingredient")) { | |
| crjson["thumbnailType"] = 1; | |
| } | |
| size_t prefix_len = absl::StartsWith(label, "c2pa.thumbnail.claim") ? 20 : 25; | |
| size_t last_dot = label.rfind('.'); | |
| if (last_dot != absl::string_view::npos && last_dot >= prefix_len) { | |
| absl::string_view mime_type = label.substr(last_dot + 1); | |
| size_t suffix_start = mime_type.find("__"); | |
| if (suffix_start != absl::string_view::npos) { | |
| mime_type = mime_type.substr(0, suffix_start); | |
| } | |
| crjson["mimeType"] = std::string(mime_type); | |
| } | |
| return crjson; | |
| } | |
| Json ConvertMetadataAssertion(const jumbf::SuperBox& box) { | |
| Json crjson = Json::object(); | |
| auto json_box = EnsureAndGetSingleBox<jumbf::JsonBox>(box); | |
| if (json_box.ok()) { | |
| Json j; | |
| jumbf::to_json(j, *json_box); | |
| if (j.contains("json")) { | |
| return j["json"]; | |
| } else { | |
| crjson["_error"] = "Failed to convert JSON box to JSON"; | |
| return crjson; | |
| } | |
| } else { | |
| crjson["_error"] = "Metadata assertion is not a JSON box"; | |
| return crjson; | |
| } | |
| } | |
| bool LabelMatches(absl::string_view label, absl::string_view base_label) { | |
| if (!absl::StartsWith(label, base_label)) { | |
| return false; | |
| } | |
| if (label.size() == base_label.size()) { | |
| return true; | |
| } | |
| if (label.size() > base_label.size() + 2 && | |
| label.substr(base_label.size(), 2) == "__") { | |
| absl::string_view number = label.substr(base_label.size() + 2); | |
| uint64_t val; | |
| return absl::SimpleAtoi(number, &val); | |
| } | |
| return false; | |
| } | |
| // This is an allowlist of standard assertions that are supported by C2PA v2.4. | |
| bool IsAllowlistedStandardAssertion(absl::string_view label) { | |
| return LabelMatches(label, kDataHashAssertionLabel) || | |
| LabelMatches(label, kDataHashAssertionPartLabel) || | |
| LabelMatches(label, kBmffBasedHashAssertionV2Label) || | |
| LabelMatches(label, kBmffBasedHashAssertionV3Label) || | |
| LabelMatches(label, kBmffBasedHashAssertionV2PartLabel) || | |
| LabelMatches(label, kBmffBasedHashAssertionV3PartLabel) || | |
| LabelMatches(label, kBoxesHashAssertionLabel) || | |
| LabelMatches(label, kBoxesHashAssertionPartLabel) || | |
| LabelMatches(label, kMultiAssetHashAssertionLabel) || | |
| LabelMatches(label, kCollectionDataHashAssertionLabel) || | |
| LabelMatches(label, kSoftBindingAssertionLabel) || | |
| LabelMatches(label, kActionsAssertionV1Label) || | |
| LabelMatches(label, kActionsAssertionV2Label) || | |
| LabelMatches(label, kIngredientAssertionV1Label) || | |
| LabelMatches(label, kIngredientAssertionV2Label) || | |
| LabelMatches(label, kIngredientAssertionV3Label) || | |
| LabelMatches(label, "font.info") || | |
| LabelMatches(label, "c2pa.font.info") || | |
| LabelMatches(label, "c2pa.cloud-data") || | |
| LabelMatches(label, "c2pa.session-keys") || | |
| LabelMatches(label, "c2pa.ai-disclosure") || | |
| LabelMatches(label, "c2pa.certificate-status") || | |
| LabelMatches(label, "c2pa.asset-ref") || | |
| LabelMatches(label, "c2pa.depthmap") || | |
| LabelMatches(label, "c2pa.repository-receipt") || | |
| LabelMatches(label, "c2pa.embedded_data") || | |
| LabelMatches(label, "c2pa.environmental-sustainability") || | |
| LabelMatches(label, "c2pa.time-stamp"); | |
| } | |
| Json ConvertAssertionStore(const jumbf::SuperBox& box) { | |
| Json crjson = Json::object(); | |
| for (const jumbf::ContentBox& content_box : box.contents) { | |
| if (!content_box.Holds<jumbf::SuperBox>()) { | |
| crjson["_error"] = "Assertion store contains non-SuperBox content"; | |
| continue; | |
| } | |
| const jumbf::SuperBox& child_box = content_box.Get<jumbf::SuperBox>(); | |
| absl::string_view label = child_box.description.label.value_or(""); | |
| if (absl::StartsWith(label, "c2pa.thumbnail.claim") || | |
| absl::StartsWith(label, "c2pa.thumbnail.ingredient")) { | |
| crjson[label] = ConvertThumbnailAssertion(child_box); | |
| } else if (LabelMatches(label, "c2pa.metadata")) { | |
| crjson[label] = ConvertMetadataAssertion(child_box); | |
| } else if (IsAllowlistedStandardAssertion(label)) { | |
| if (auto cbor_box = EnsureAndGetSingleBox<jumbf::CborBox>(child_box); | |
| cbor_box.ok()) { | |
| Json j; | |
| jumbf::to_json(j, *cbor_box); | |
| if (j.contains("cbor")) { | |
| crjson[label] = j["cbor"]; | |
| } else { | |
| crjson[label] = Json::object(); | |
| crjson[label]["_error"] = "Failed to convert CBOR to JSON"; | |
| } | |
| } else if (auto json_box = | |
| EnsureAndGetSingleBox<jumbf::JsonBox>(child_box); | |
| json_box.ok()) { | |
| Json j; | |
| jumbf::to_json(j, *json_box); | |
| if (j.contains("json")) { | |
| crjson[label] = j["json"]; | |
| } else { | |
| crjson[label] = Json::object(); | |
| crjson[label]["_error"] = "Failed to convert JSON box to JSON"; | |
| } | |
| } | |
| } else { | |
| crjson[label] = Json::object(); | |
| } | |
| } | |
| return crjson; | |
| } | |
| // ============================================================================= | |
| // Claim Helpers | |
| // ============================================================================= | |
| // Converts a hashed URI map to a crJSON object. | |
| // - CBOR CDDL: | |
| // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_embedded | |
| Json ConvertHashedUri(const cbor::MapView& uri_map) { | |
| Json crjson = Json::object(); | |
| RecordString(uri_map, "url", true, crjson); | |
| RecordString(uri_map, "alg", false, crjson); | |
| RecordByteString(uri_map, "hash", true, crjson); | |
| return crjson; | |
| } | |
| Json ConvertClaimGeneratorInfo(const cbor::MapView& map) { | |
| Json crjson = Json::object(); | |
| RecordString(map, "name", true, crjson); | |
| RecordString(map, "version", false, crjson); | |
| RecordMap(map, "icon", false, ConvertHashedUri, crjson); | |
| RecordString(map, "operating_system", false, crjson); | |
| return crjson; | |
| } | |
| Json ConvertRating(const cbor::MapView& map) { | |
| Json rating = Json::object(); | |
| RecordString(map, "value", true, rating); | |
| RecordString(map, "code", false, rating); | |
| RecordString(map, "explanation", false, rating); | |
| return rating; | |
| } | |
| Json ConvertSourceMap(const cbor::MapView& map) { | |
| Json crjson = Json::object(); | |
| RecordString(map, "type", true, crjson); | |
| RecordString(map, "details", false, crjson); | |
| return crjson; | |
| } | |
| Json ConvertLocalizations(const cbor::MapView& map) { | |
| Json crjson = Json::object(); | |
| // Dynamic fields... | |
| return crjson; | |
| } | |
| Json ConvertRegionOfInterest(const cbor::MapView& map) { | |
| Json crjson = Json::object(); | |
| // No fields in the spec. | |
| return crjson; | |
| } | |
| Json ConvertMetadata(const cbor::MapView& map) { | |
| Json crjson = Json::object(); | |
| RecordString(map, "dateTime", false, crjson); | |
| RecordArrayOfMaps(map, "rating", false, ConvertRating, crjson); | |
| RecordMap(map, "reference", false, ConvertHashedUri, crjson); | |
| RecordMap(map, "dataSource", false, ConvertSourceMap, crjson); | |
| RecordArrayOfMaps(map, "localizations", false, ConvertLocalizations, crjson); | |
| RecordMap(map, "regionOfInterest", false, ConvertRegionOfInterest, crjson); | |
| return crjson; | |
| } | |
| // ============================================================================= | |
| // Claim Converters | |
| // ============================================================================= | |
| Json ConvertClaimV2(const jumbf::SuperBox& box) { | |
| Json crjson = Json::object(); | |
| auto parsed_cbor = GetCborMap(box); | |
| if (!parsed_cbor.ok()) { | |
| crjson["_error"] = parsed_cbor.status().message(); | |
| return crjson; | |
| } | |
| RecordString(parsed_cbor->cbor_view, "instanceID", true, crjson); | |
| RecordMap(parsed_cbor->cbor_view, "claim_generator_info", true, | |
| ConvertClaimGeneratorInfo, crjson); | |
| RecordString(parsed_cbor->cbor_view, "signature", true, crjson); | |
| RecordArrayOfMaps(parsed_cbor->cbor_view, "created_assertions", true, | |
| ConvertHashedUri, crjson); | |
| RecordArrayOfMaps(parsed_cbor->cbor_view, "gathered_assertions", true, | |
| ConvertHashedUri, crjson); | |
| RecordString(parsed_cbor->cbor_view, "dc:title", false, crjson); | |
| RecordArray(parsed_cbor->cbor_view, "redacted_assertions", true, | |
| ConvertArrayOfStrings, crjson); | |
| RecordString(parsed_cbor->cbor_view, "alg", false, crjson); | |
| RecordString(parsed_cbor->cbor_view, "alg_soft", false, crjson); | |
| RecordString(parsed_cbor->cbor_view, "specVersion", false, crjson); | |
| RecordMap(parsed_cbor->cbor_view, "metadata", false, ConvertMetadata, crjson); | |
| return crjson; | |
| } | |
| // ============================================================================= | |
| // Signature Helpers | |
| // ============================================================================= | |
| Json ConvertDNParsed(X509_NAME* name) { | |
| Json dn_map = Json::object(); | |
| if (!name) return dn_map; | |
| int count = X509_NAME_entry_count(name); | |
| for (int i = 0; i < count; ++i) { | |
| X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i); | |
| ASN1_OBJECT* obj = X509_NAME_ENTRY_get_object(entry); | |
| ASN1_STRING* str = X509_NAME_ENTRY_get_data(entry); | |
| int nid = OBJ_obj2nid(obj); | |
| const char* short_name = OBJ_nid2sn(nid); | |
| unsigned char* utf8_str = nullptr; | |
| int len = ASN1_STRING_to_UTF8(&utf8_str, str); | |
| if (len >= 0) { | |
| if (short_name) { | |
| dn_map[short_name] = | |
| std::string(reinterpret_cast<char*>(utf8_str), len); | |
| } else { | |
| char oid_buf[128]; | |
| OBJ_obj2txt(oid_buf, sizeof(oid_buf), obj, 1); | |
| dn_map[oid_buf] = std::string(reinterpret_cast<char*>(utf8_str), len); | |
| } | |
| OPENSSL_free(utf8_str); | |
| } | |
| } | |
| return dn_map; | |
| } | |
| std::string ConvertASN1TimeToISO8601(const ASN1_TIME* t) { | |
| if (!t) return ""; | |
| int64_t posix_time; | |
| if (ASN1_TIME_to_posix(t, &posix_time) == 0) { | |
| return ""; | |
| } | |
| return absl::FormatTime(absl::RFC3339_full, absl::FromUnixSeconds(posix_time), | |
| absl::UTCTimeZone()); | |
| } | |
| Json ConvertCertificateInfo(absl::string_view certificate_der) { | |
| Json cert_info = Json::object(); | |
| const uint8_t* p = reinterpret_cast<const uint8_t*>(certificate_der.data()); | |
| X509* cert = d2i_X509(nullptr, &p, certificate_der.size()); | |
| if (!cert) { | |
| cert_info["_error"] = "Failed to parse certificate"; | |
| return cert_info; | |
| } | |
| ASN1_INTEGER* serial = X509_get_serialNumber(cert); | |
| BIGNUM* bn = ASN1_INTEGER_to_BN(serial, nullptr); | |
| if (bn) { | |
| char* hex = BN_bn2hex(bn); | |
| if (hex) { | |
| cert_info["serialNumber"] = std::string(hex); | |
| OPENSSL_free(hex); | |
| } | |
| BN_free(bn); | |
| } | |
| cert_info["subject"] = ConvertDNParsed(X509_get_subject_name(cert)); | |
| cert_info["issuer"] = ConvertDNParsed(X509_get_issuer_name(cert)); | |
| Json validity = Json::object(); | |
| validity["notBefore"] = ConvertASN1TimeToISO8601(X509_get0_notBefore(cert)); | |
| validity["notAfter"] = ConvertASN1TimeToISO8601(X509_get0_notAfter(cert)); | |
| cert_info["validity"] = validity; | |
| X509_free(cert); | |
| return cert_info; | |
| } | |
| // ============================================================================= | |
| // Signature Converter | |
| // ============================================================================= | |
| Json ConvertClaimSignature(const jumbf::SuperBox& box) { | |
| Json crjson = Json::object(); | |
| auto cbor_box = EnsureAndGetSingleBox<jumbf::CborBox>(box); | |
| if (!cbor_box.ok()) { | |
| return crjson; | |
| } | |
| auto cose_sign1 = DecodeCoseSign1TaggedStructure(cbor_box->payload); | |
| if (!cose_sign1.ok()) { | |
| return crjson; | |
| } | |
| auto protected_header = DecodeProtectedHeader(cose_sign1->protected_header); | |
| if (protected_header.ok()) { | |
| crjson["algorithm"] = absl::StrCat(protected_header->alg); | |
| } | |
| std::vector<std::string> cert_chain; | |
| if (!cose_sign1->unprotected_header.certificate_chain.empty()) { | |
| cert_chain = cose_sign1->unprotected_header.certificate_chain; | |
| } else if (protected_header.ok()) { | |
| cert_chain = protected_header->certificate_chain; | |
| } | |
| if (!cert_chain.empty()) { | |
| crjson["certificateInfo"] = ConvertCertificateInfo(cert_chain[0]); | |
| } | |
| // Handle Timestamp | |
| auto timestamp_container = cose_sign1->unprotected_header.sig_tst2; | |
| if (!timestamp_container.has_value()) { | |
| timestamp_container = cose_sign1->unprotected_header.sig_tst; | |
| } | |
| if (timestamp_container.has_value() && | |
| !timestamp_container->tst_tokens.empty()) { | |
| if (timestamp_container->tst_tokens.size() == 1) { | |
| absl::string_view timestamp_token = | |
| timestamp_container->tst_tokens[0].val; | |
| TimestampVerifier verifier(nullptr); // Skip trust checks | |
| auto parsed_token = SimpleParsedTimestampToken::Create(timestamp_token); | |
| if (parsed_token.ok()) { | |
| auto verified_timestamp = verifier.VerifyTimestampToken(**parsed_token); | |
| if (verified_timestamp.ok()) { | |
| Json ts_info = Json::object(); | |
| ts_info["timestamp"] = absl::FormatTime( | |
| absl::RFC3339_full, verified_timestamp->asserted_time(), | |
| absl::UTCTimeZone()); | |
| ts_info["certificateInfo"] = | |
| ConvertCertificateInfo(verified_timestamp->tsa_certificate()); | |
| crjson["timeStampInfo"] = ts_info; | |
| } | |
| } | |
| } | |
| } | |
| return crjson; | |
| } | |
| Json ConvertValidationStatusSetToJson( | |
| const ValidationStatusSet& validation, | |
| std::optional<absl::string_view> spec_version = std::nullopt, | |
| std::optional<absl::string_view> trust_list_uri = std::nullopt) { | |
| Json v = Json::object(); | |
| v["success"] = Json::array(); | |
| v["informational"] = Json::array(); | |
| v["failure"] = Json::array(); | |
| if (spec_version.has_value()) { | |
| v["specVersion"] = *spec_version; | |
| } | |
| if (trust_list_uri.has_value()) { | |
| v["trustListURI"] = *trust_list_uri; | |
| } | |
| v["validationTime"] = | |
| absl::FormatTime(absl::RFC3339_full, absl::Now(), absl::UTCTimeZone()); | |
| for (const auto& success : validation.successes()) { | |
| Json s = Json::object(); | |
| s["code"] = success.code(); | |
| if (!success.url().empty()) { | |
| s["url"] = success.url(); | |
| } | |
| if (!success.explanation().empty()) { | |
| s["explanation"] = success.explanation(); | |
| } | |
| v["success"].push_back(s); | |
| } | |
| for (const auto& informational : validation.informationals()) { | |
| Json i = Json::object(); | |
| i["code"] = informational.code(); | |
| if (!informational.url().empty()) { | |
| i["url"] = informational.url(); | |
| } | |
| if (!informational.explanation().empty()) { | |
| i["explanation"] = informational.explanation(); | |
| } | |
| v["informational"].push_back(i); | |
| } | |
| for (const auto& failure : validation.failures()) { | |
| Json f = Json::object(); | |
| f["code"] = failure.code(); | |
| if (!failure.url().empty()) { | |
| f["url"] = failure.url(); | |
| } | |
| if (!failure.explanation().empty()) { | |
| f["explanation"] = failure.explanation(); | |
| } | |
| v["failure"].push_back(f); | |
| } | |
| return v; | |
| } | |
| // ============================================================================= | |
| // Top-Level Converters | |
| // ============================================================================= | |
| Json ConvertManifestToCrJson( | |
| const jumbf::SuperBox& manifest_box, | |
| const ValidationStatusSet* validation_status = nullptr, | |
| const google::protobuf::RepeatedPtrField<IngredientDeltaValidationResult>* | |
| ingredient_deltas = nullptr, | |
| std::optional<absl::string_view> spec_version = std::nullopt, | |
| std::optional<absl::string_view> trust_list_uri = std::nullopt) { | |
| Json crjson = Json::object(); | |
| crjson["label"] = manifest_box.description.label.value_or(""); | |
| switch (GetManifestType(manifest_box)) { | |
| case ManifestType::kStandard: | |
| crjson["isUpdateManifest"] = false; | |
| crjson["isCompressedManifest"] = false; | |
| break; | |
| case ManifestType::kUpdate: | |
| crjson["isUpdateManifest"] = true; | |
| crjson["isCompressedManifest"] = false; | |
| break; | |
| case ManifestType::kCompressed: | |
| crjson["isCompressedManifest"] = true; | |
| crjson["_error"] = "Compressed manifests are not supported"; | |
| // Compressed manifest are not supported, so mark an error and return. | |
| return crjson; | |
| default: | |
| crjson["_error"] = "Unknown manifest type"; | |
| // Unknown manifest type, so mark an error and return. | |
| return crjson; | |
| } | |
| crjson["assertions"] = Json::object(); | |
| crjson["signature"] = Json::object(); | |
| if (validation_status != nullptr) { | |
| crjson["validationResults"] = ConvertValidationStatusSetToJson( | |
| *validation_status, spec_version, trust_list_uri); | |
| } else { | |
| crjson["validationResults"] = Json::object(); | |
| crjson["validationResults"]["success"] = Json::array(); | |
| crjson["validationResults"]["informational"] = Json::array(); | |
| crjson["validationResults"]["failure"] = Json::array(); | |
| if (spec_version.has_value()) { | |
| crjson["validationResults"]["specVersion"] = *spec_version; | |
| } | |
| if (trust_list_uri.has_value()) { | |
| crjson["validationResults"]["trustListURI"] = *trust_list_uri; | |
| } | |
| crjson["validationResults"]["validationTime"] = | |
| absl::FormatTime(absl::RFC3339_full, absl::Now(), absl::UTCTimeZone()); | |
| } | |
| if (ingredient_deltas != nullptr && !ingredient_deltas->empty()) { | |
| crjson["ingredientDeltas"] = Json::array(); | |
| for (const auto& delta : *ingredient_deltas) { | |
| Json d = Json::object(); | |
| d["ingredientAssertionURI"] = delta.ingredient_assertion_uri(); | |
| // Ingredient deltas should not have spec version or trust list URI. | |
| d["validationDeltas"] = | |
| ConvertValidationStatusSetToJson(delta.validation_deltas()); | |
| crjson["ingredientDeltas"].push_back(d); | |
| } | |
| } | |
| for (const jumbf::ContentBox& content_box : manifest_box.contents) { | |
| if (!content_box.Holds<jumbf::SuperBox>()) { | |
| crjson["_error"] = "Manifest contains non-SuperBox content"; | |
| continue; | |
| } | |
| const jumbf::SuperBox& child_box = content_box.Get<jumbf::SuperBox>(); | |
| if (child_box.description.label.value_or("") == kAssertionStoreLabel) { | |
| crjson["assertions"] = ConvertAssertionStore(child_box); | |
| } else if (child_box.description.label.value_or("") == kClaimV2Label) { | |
| crjson["claim.v2"] = ConvertClaimV2(child_box); | |
| } else if (child_box.description.label.value_or("") == | |
| kClaimSignatureLabel) { | |
| crjson["signature"] = ConvertClaimSignature(child_box); | |
| } else if (child_box.description.label.value_or("") == "c2pa.databoxes") { | |
| // Deprecated, ignore it. | |
| } else { | |
| // Ignore unhandled top-level boxes in manifest. | |
| } | |
| } | |
| return crjson; | |
| } | |
| absl::flat_hash_map<std::string, ValidationStatusSet> IndexResults( | |
| const ValidationResultProto* absl_nullable validation_result) { | |
| absl::flat_hash_map<std::string, ValidationStatusSet> manifests; | |
| if (validation_result != nullptr) { | |
| if (validation_result->has_active_manifest()) { | |
| manifests[validation_result->active_manifest().label()] = | |
| validation_result->active_manifest().validation(); | |
| } | |
| for (const auto& ingredient : validation_result->ingredient_manifests()) { | |
| manifests[ingredient.label()] = ingredient.validation(); | |
| } | |
| } | |
| return manifests; | |
| } | |
| absl::StatusOr<Json> ConvertManifestStoreToCrJsonInternal( | |
| absl::string_view raw_manifest_store, | |
| const ValidationResultProto* absl_nullable validation_result, | |
| const IngredientValidationResultProto* absl_nullable | |
| ingredient_validation_result) { | |
| auto indexed_validation_results = IndexResults(validation_result); | |
| // Get the spec version and trust list URI from the validation results. | |
| std::optional<absl::string_view> spec_version; | |
| std::optional<absl::string_view> trust_list_uri; | |
| if (validation_result != nullptr) { | |
| if (validation_result->has_spec_version()) { | |
| spec_version = validation_result->spec_version(); | |
| } | |
| if (validation_result->has_trust_list_uri()) { | |
| trust_list_uri = validation_result->trust_list_uri(); | |
| } | |
| } else if (ingredient_validation_result != nullptr) { | |
| if (ingredient_validation_result->has_ingredient_validation_results()) { | |
| if (ingredient_validation_result->ingredient_validation_results() | |
| .has_spec_version()) { | |
| spec_version = | |
| ingredient_validation_result->ingredient_validation_results() | |
| .spec_version(); | |
| } | |
| if (ingredient_validation_result->ingredient_validation_results() | |
| .has_trust_list_uri()) { | |
| trust_list_uri = | |
| ingredient_validation_result->ingredient_validation_results() | |
| .trust_list_uri(); | |
| } | |
| } | |
| } | |
| ABSL_ASSIGN_OR_RETURN( | |
| jumbf::SuperBox manifest_store_box, | |
| jumbf::ConsumeSuperBox(&raw_manifest_store, /*recursion_limit=*/-1)); | |
| // Build the crJSON. | |
| Json crjson = Json::object(); | |
| // Add the context. | |
| crjson["@context"] = | |
| Json::array({"https://c2pa.org/crjson/crJSON.schema.json"}); | |
| // Add the manifests. | |
| crjson["manifests"] = Json::array(); | |
| bool is_active_manifest = true; | |
| for (auto it = manifest_store_box.contents.rbegin(); | |
| it != manifest_store_box.contents.rend(); ++it) { | |
| const jumbf::ContentBox& content_box = *it; | |
| if (!content_box.Holds<jumbf::SuperBox>()) { | |
| crjson["_error"] = "Manifest store contains non-SuperBox content"; | |
| continue; | |
| } | |
| auto manifest_box = content_box.Get<jumbf::SuperBox>(); | |
| auto results = indexed_validation_results.find( | |
| manifest_box.description.label.value_or("")); | |
| const ValidationStatusSet* validation_status = nullptr; | |
| if (results != indexed_validation_results.end()) { | |
| validation_status = &results->second; | |
| } else if (is_active_manifest && ingredient_validation_result != nullptr && | |
| ingredient_validation_result | |
| ->has_ingredient_validation_results()) { | |
| validation_status = | |
| &ingredient_validation_result->ingredient_validation_results() | |
| .active_manifest(); | |
| } | |
| crjson["manifests"].push_back(ConvertManifestToCrJson( | |
| manifest_box, validation_status, | |
| /*ingredient_deltas=*/nullptr, spec_version, trust_list_uri)); | |
| is_active_manifest = false; // Only first one is active | |
| } | |
| // JSON generator info. | |
| crjson["jsonGenerator"]["name"] = "Google C2PA Toolkit"; | |
| crjson["jsonGenerator"]["version"] = "0.0.1"; | |
| return crjson; | |
| } | |
| } // namespace | |
| absl::StatusOr<Json> ConvertToCrJsonWithoutValidationResults( | |
| absl::string_view raw_manifest_store) { | |
| return ConvertManifestStoreToCrJsonInternal(raw_manifest_store, nullptr, | |
| nullptr); | |
| } | |
| absl::StatusOr<Json> ConvertToCrJson( | |
| const ValidationResult& validation_result) { | |
| return ConvertManifestStoreToCrJsonInternal( | |
| validation_result.manifest_store_bytes(), &validation_result.proto(), | |
| nullptr); | |
| } | |
| absl::StatusOr<Json> ConvertToCrJson( | |
| const IngredientValidationResultProto& ingredient_validation_result) { | |
| return ConvertManifestStoreToCrJsonInternal( | |
| ingredient_validation_result.manifest_store_bytes(), nullptr, | |
| &ingredient_validation_result); | |
| } | |
| } // namespace credentio |