| // 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 "claim/manifest_box_encoder.h" | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/container/flat_hash_map.h" | |
| #include "absl/functional/any_invocable.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/cord.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "assertion/assertion_cbor_encoder.h" | |
| #include "assertion/assertion_referencer.h" | |
| #include "async/async_context.h" | |
| #include "claim/signer.h" | |
| #include "constants/labels.h" | |
| #include "jumbf/box_builder.h" | |
| #include "jumbf/constants.h" | |
| #include "jumbf/encode_params.h" | |
| #include "jumbf/utils.h" | |
| #include "proto/actions_assertion.pb.h" | |
| #include "proto/assertion.pb.h" | |
| #include "proto/generator_info.pb.h" | |
| #include "proto/hashed_uri.pb.h" | |
| #include "proto/manifest.pb.h" | |
| #include "proto/multi_asset_hash_assertion.pb.h" | |
| #include "salt/salt_generator.h" | |
| #include "uuid/uuid.h" | |
| namespace credentio { | |
| namespace { | |
| absl::StatusOr<jumbf::BuiltSuperBox> SerializeAssertionStore( | |
| std::vector<jumbf::BuiltSuperBox> assertions) { | |
| jumbf::SuperBoxBuilder assertion_store_builder( | |
| kAssertionStoreUuid, {.label = absl::Cord(kAssertionStoreLabel)}); | |
| for (jumbf::BuiltSuperBox& assertion_box : assertions) { | |
| ABSL_RETURN_IF_ERROR( | |
| assertion_store_builder.AddChild(std::move(assertion_box))); | |
| } | |
| return std::move(assertion_store_builder).Finalize(); | |
| } | |
| absl::StatusOr<jumbf::BuiltSuperBox> SerializeCborSuperBox( | |
| const Uuid& uuid, absl::string_view label, absl::Cord cbor_content) { | |
| jumbf::SuperBoxBuilder builder(uuid, {.label = absl::Cord(label)}); | |
| ABSL_RETURN_IF_ERROR( | |
| builder.AddContent(jumbf::kCborBoxType, std::move(cbor_content))); | |
| return std::move(builder).Finalize(); | |
| } | |
| // Returns description box options for an assertion superbox labeled `label`. | |
| // If `salt_generator` produces a non-empty salt, the salt is embedded as a C2PA | |
| // salt box ('c2sh') in the description box private content, per C2PA | |
| // specification v2.2 sections 8.4.2.3 and 11.1.4.1.2. | |
| absl::StatusOr<jumbf::DescriptionBoxOptions> MakeSaltedDescriptionBoxOptions( | |
| absl::string_view label, const SaltGenerator& salt_generator) { | |
| jumbf::DescriptionBoxOptions options = {.label = absl::Cord(label)}; | |
| ABSL_ASSIGN_OR_RETURN(std::string salt, salt_generator.Generate()); | |
| if (salt.empty()) { | |
| return options; | |
| } | |
| ABSL_ASSIGN_OR_RETURN(options.private_content, | |
| jumbf::CreateC2paSaltBox(salt)); | |
| return options; | |
| } | |
| // Encodes `assertion` to CBOR with `encoder` and wraps it in a CBOR content | |
| // type JUMBF superbox labeled with the assertion label and salted with | |
| // `salt_generator`. | |
| absl::StatusOr<jumbf::BuiltSuperBox> SerializeAssertionSuperBox( | |
| const AssertionCborEncoder& encoder, const Assertion& assertion, | |
| const SaltGenerator& salt_generator) { | |
| ABSL_ASSIGN_OR_RETURN(absl::Cord cbor, encoder.ToCbor(assertion)); | |
| ABSL_ASSIGN_OR_RETURN( | |
| jumbf::DescriptionBoxOptions description_options, | |
| MakeSaltedDescriptionBoxOptions(assertion.label(), salt_generator)); | |
| jumbf::SuperBoxBuilder builder(jumbf::kCborBoxTypeUuid, | |
| std::move(description_options)); | |
| ABSL_RETURN_IF_ERROR( | |
| builder.AddContent(jumbf::kCborBoxType, std::move(cbor))); | |
| return std::move(builder).Finalize(); | |
| } | |
| struct SignatureBoxResult { | |
| jumbf::BuiltSuperBox box; | |
| }; | |
| void MakeSignatureBox( | |
| absl::string_view claim_cbor, const ClaimSigner& claim_signer, | |
| const ClaimSigner::Options& options, | |
| absl::AnyInvocable<void(absl::StatusOr<SignatureBoxResult>) &&> cb) { | |
| AsyncResolver<SignatureBoxResult> resolver(options.async_context, | |
| std::move(cb)); | |
| auto done_cb = | |
| [resolver = std::move(resolver)]( | |
| absl::StatusOr<ClaimSigner::SignResult> signature_status) mutable { | |
| if (!signature_status.ok()) { | |
| resolver.Return(signature_status.status()); | |
| return; | |
| } | |
| absl::StatusOr<jumbf::BuiltSuperBox> signature_box = | |
| SerializeCborSuperBox( | |
| kClaimSignatureUuid, kClaimSignatureLabel, | |
| absl::Cord(std::move(signature_status->signature))); | |
| if (!signature_box.ok()) { | |
| resolver.Return(signature_box.status()); | |
| return; | |
| } | |
| resolver.Return(SignatureBoxResult{ | |
| .box = std::move(*signature_box), | |
| }); | |
| }; | |
| claim_signer.Sign(claim_cbor, options, std::move(done_cb)); | |
| } | |
| } // namespace | |
| absl::Status ManifestBoxEncoder::PopulateMultiAssetPartHashes( | |
| MultiAssetHashAssertion* assertion) const { | |
| for (auto& part : *assertion->mutable_parts()) { | |
| absl::string_view url = part.hash_assertion().url(); | |
| auto it = assertion_url_to_index_.find(url); | |
| if (it == assertion_url_to_index_.end()) { | |
| return absl::InternalError( | |
| absl::StrCat("Could not find hash for url: ", url)); | |
| } | |
| *part.mutable_hash_assertion() = assertion_hashed_uris_[it->second]; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ManifestBoxEncoder::AddAssertion(Assertion& assertion) { | |
| if (assertion.has_multi_asset_hash()) { | |
| ABSL_RETURN_IF_ERROR( | |
| PopulateMultiAssetPartHashes(assertion.mutable_multi_asset_hash())); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_box, | |
| SerializeAssertionSuperBox(assertion_cbor_encoder_, | |
| assertion, salt_generator_)); | |
| ABSL_ASSIGN_OR_RETURN(HashedUri assertion_hashed_uri, | |
| assertion_reference_.Generate(assertion_box)); | |
| assertion_url_to_index_[assertion_hashed_uri.url()] = | |
| assertion_hashed_uris_.size(); | |
| assertion_hashed_uris_.Add(std::move(assertion_hashed_uri)); | |
| assertion_boxes_.push_back(std::move(assertion_box)); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ManifestBoxEncoder::AddActionsAssertion(Assertion assertion) { | |
| for (ActionItem& action : *assertion.mutable_actions()->mutable_actions()) { | |
| if (action.action() == "c2pa.opened" && | |
| action.parameters().ingredients_size() != 1) { | |
| return absl::InvalidArgumentError( | |
| "Opened action must have exactly one ingredient."); | |
| } | |
| if (action.parameters().ingredients_size() != 0) { | |
| for (auto& ingredient : | |
| *action.mutable_parameters()->mutable_ingredients()) { | |
| auto it = assertion_url_to_index_.find(ingredient.url()); | |
| if (it == assertion_url_to_index_.end()) { | |
| return absl::InternalError(absl::StrCat( | |
| "Could not find hash for ingredient url: ", ingredient.url())); | |
| } | |
| ingredient = assertion_hashed_uris_[it->second]; | |
| } | |
| } | |
| if (action.parameters().has_redacted_uri()) { | |
| redacted_assertions_.emplace_back(action.parameters().redacted_uri()); | |
| } | |
| } | |
| return AddAssertion(assertion); | |
| } | |
| absl::Status ManifestBoxEncoder::AddGatheredAssertion( | |
| const GatheredAssertion& gathered_assertion) { | |
| if (gathered_assertion.content_type() != | |
| GatheredAssertion::CONTENT_TYPE_JSON) { | |
| return absl::InvalidArgumentError( | |
| "Gathered assertion content type must be JSON."); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(jumbf::DescriptionBoxOptions description_options, | |
| MakeSaltedDescriptionBoxOptions( | |
| gathered_assertion.label(), salt_generator_)); | |
| jumbf::SuperBoxBuilder builder(jumbf::kJsonBoxTypeUuid, | |
| std::move(description_options)); | |
| ABSL_RETURN_IF_ERROR(builder.AddContent( | |
| jumbf::kJsonBoxType, absl::Cord(gathered_assertion.data()))); | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_box, | |
| std::move(builder).Finalize()); | |
| ABSL_ASSIGN_OR_RETURN(HashedUri assertion_hashed_uri, | |
| assertion_reference_.Generate(assertion_box)); | |
| gathered_assertion_hashed_uris_.Add(std::move(assertion_hashed_uri)); | |
| assertion_boxes_.push_back(std::move(assertion_box)); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status ManifestBoxEncoder::AddAssertions() { | |
| // Note that we must add an assertion before adding any other assertion that | |
| // references it, so the referenced assertion's hash is available. So, for | |
| // example, ingredient assertions must be added before actions assertions | |
| // since some actions assertions reference ingredient assertions. | |
| for (Assertion& assertion : manifest_.ingredient_assertions) { | |
| ABSL_RETURN_IF_ERROR(AddAssertion(assertion)); | |
| } | |
| ABSL_RETURN_IF_ERROR(AddActionsAssertion(manifest_.actions_assertion)); | |
| ABSL_RETURN_IF_ERROR(AddAssertion(manifest_.hard_binding_assertion)); | |
| for (Assertion& assertion : manifest_.part_hash_assertions) { | |
| ABSL_RETURN_IF_ERROR(AddAssertion(assertion)); | |
| } | |
| if (manifest_.multi_part_assertion.has_value()) { | |
| ABSL_RETURN_IF_ERROR(AddAssertion(*manifest_.multi_part_assertion)); | |
| } | |
| for (const auto& gathered_assertion : manifest_.gathered_assertions) { | |
| ABSL_RETURN_IF_ERROR(AddGatheredAssertion(gathered_assertion)); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| void ManifestBoxEncoder::EncodeClaimAndSignature( | |
| const ClaimSigner::Options& options, | |
| absl::AnyInvocable<void(absl::StatusOr<ManifestEncoderResult>) &&> cb) { | |
| AsyncResolver<ManifestEncoderResult> resolver(options.async_context, | |
| std::move(cb)); | |
| Claim claim; | |
| claim.set_instance_id(manifest_.instance_id); | |
| *claim.mutable_claim_generator_info() = generator_info_; | |
| claim.mutable_created_assertions()->Swap(&assertion_hashed_uris_); | |
| claim.mutable_gathered_assertions()->Swap(&gathered_assertion_hashed_uris_); | |
| for (const std::string& redacted_assertion : redacted_assertions_) { | |
| *claim.add_redacted_assertions() = redacted_assertion; | |
| } | |
| claim.set_signature(kClaimSignatureUrl); | |
| claim.set_default_algorithm(absl::StrCat(hash_algorithm_)); | |
| auto prepare_claim = [&]() -> absl::StatusOr<std::string> { | |
| ABSL_ASSIGN_OR_RETURN(std::string claim_cbor, | |
| claim_cbor_encoder_.ToCbor(claim)); | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox claim_box, | |
| SerializeCborSuperBox(kClaimUuid, kClaimV2Label, | |
| absl::Cord(claim_cbor))); | |
| ABSL_RETURN_IF_ERROR(manifest_builder_.AddChild(std::move(claim_box))); | |
| return claim_cbor; | |
| }; | |
| absl::StatusOr<std::string> claim_cbor = prepare_claim(); | |
| if (!claim_cbor.ok()) { | |
| resolver.Return(claim_cbor.status()); | |
| return; | |
| } | |
| auto done_cb = | |
| [this, resolver = std::move(resolver)]( | |
| absl::StatusOr<SignatureBoxResult> claim_signature) mutable { | |
| auto finalize_manifest = | |
| [&]() -> absl::StatusOr<ManifestEncoderResult> { | |
| ABSL_RETURN_IF_ERROR(claim_signature.status()); | |
| ABSL_RETURN_IF_ERROR( | |
| manifest_builder_.AddChild(std::move(claim_signature->box))); | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox active_manifest, | |
| std::move(manifest_builder_).Finalize()); | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox finalized_manifest_store, | |
| std::move(manifest_.manifest_store_builder) | |
| .Finalize(std::move(active_manifest))); | |
| return ManifestEncoderResult{ | |
| .box = std::move(finalized_manifest_store), | |
| }; | |
| }; | |
| resolver.Return(finalize_manifest()); | |
| }; | |
| MakeSignatureBox(*claim_cbor, claim_signer_, options, std::move(done_cb)); | |
| } | |
| absl::Status ManifestBoxEncoder::EncodeAssertionStore() { | |
| ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_store, | |
| SerializeAssertionStore(std::move(assertion_boxes_))); | |
| return manifest_builder_.AddChild(std::move(assertion_store)); | |
| } | |
| void ManifestBoxEncoder::Encode( | |
| const ClaimSigner::Options& options, | |
| absl::AnyInvocable<void(absl::StatusOr<ManifestEncoderResult>) &&> cb) { | |
| auto add_assertions_status = AddAssertions(); | |
| if (!add_assertions_status.ok()) { | |
| std::move(cb)(add_assertions_status); | |
| return; | |
| } | |
| auto encode_assertion_store_status = EncodeAssertionStore(); | |
| if (!encode_assertion_store_status.ok()) { | |
| std::move(cb)(encode_assertion_store_status); | |
| return; | |
| } | |
| EncodeClaimAndSignature(options, std::move(cb)); | |
| } | |
| } // namespace credentio |