| // 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/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" |
| |
| 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) { |
| auto parse_result = cbor::Parse(cbor); |
| if (!parse_result.ok()) { |
| return parse_result.status(); |
| } |
| auto cbor_map = (*parse_result)->AsMap(); |
| if (!cbor_map.ok()) { |
| return cbor_map.status(); |
| } |
| return ParsedCbor<cbor::MapView>{std::move(*parse_result), *cbor_map}; |
| } |
| |
| absl::StatusOr<ParsedCbor<cbor::MapView>> GetCborMap( |
| const jumbf::SuperBox& box) { |
| auto cbor_box = EnsureAndGetSingleBox<jumbf::CborBox>(box); |
| if (!cbor_box.ok()) { |
| return cbor_box.status(); |
| } |
| 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 |
| // ============================================================================= |
| |
| std::string SigningAlgorithmToString(SigningAlgorithm alg) { |
| switch (alg) { |
| case SigningAlgorithm::kEs256: |
| return "ES256"; |
| case SigningAlgorithm::kEs384: |
| return "ES384"; |
| case SigningAlgorithm::kEs512: |
| return "ES512"; |
| case SigningAlgorithm::kPs256: |
| return "PS256"; |
| case SigningAlgorithm::kPs384: |
| return "PS384"; |
| case SigningAlgorithm::kPs512: |
| return "PS512"; |
| case SigningAlgorithm::kEdDsa: |
| return "Ed25519"; |
| } |
| return "Unknown"; |
| } |
| |
| 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"] = SigningAlgorithmToString(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(); |
| } |
| } |
| } |
| |
| auto manifest_store_box = |
| jumbf::ConsumeSuperBox(&raw_manifest_store, /*recursion_limit=*/-1); |
| if (!manifest_store_box.ok()) { |
| return manifest_store_box.status(); |
| } |
| |
| // 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( |
| absl::string_view raw_manifest_store, |
| const ValidationResultProto& validation_result) { |
| return ConvertManifestStoreToCrJsonInternal(raw_manifest_store, |
| &validation_result, nullptr); |
| } |
| |
| absl::StatusOr<Json> ConvertToCrJson( |
| absl::string_view raw_manifest_store, |
| const IngredientValidationResultProto& ingredient_validation_result) { |
| return ConvertManifestStoreToCrJsonInternal(raw_manifest_store, nullptr, |
| &ingredient_validation_result); |
| } |
| |
| } // namespace credentio |