Public release: - Salt assertions by default in GeneratorOptions. - Add RandomSaltGenerator::Default(). - Add RandomSaltGenerator and return absl::StatusOr from Sa... - Plumb salt_generator through GeneratorOptions. - Plumb SaltGenerator through CreateManifestEncoder. - Support salting assertions in ManifestBoxEncoder. - Rename AssertionEncoder to AssertionCborEncoder and move ... GitOrigin-RevId: f992ff2f29d57da04aa41ee75e83311f3edcd521 Change-Id: Iee438a9b0b480a92455fdfb86bfadca1fe9acd69
diff --git a/assertion/BUILD b/assertion/BUILD index a7250bb..4ad9372 100644 --- a/assertion/BUILD +++ b/assertion/BUILD
@@ -90,13 +90,11 @@ ) cc_library( - name = "assertion_encoder", - srcs = ["assertion_encoder.cc"], - hdrs = ["assertion_encoder.h"], + name = "assertion_cbor_encoder", + srcs = ["assertion_cbor_encoder.cc"], + hdrs = ["assertion_cbor_encoder.h"], deps = [ "//cbor", - "//jumbf:box_builder", - "//jumbf:constants", "//proto:actions_assertion_cbor_proto", "//proto:actions_assertion_cc_proto", "//proto:assertion_cc_proto", @@ -116,20 +114,16 @@ "@abseil-cpp//absl/status:status_macros", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings:cord", - "@abseil-cpp//absl/strings:string_view", ], ) cc_test( - name = "assertion_encoder_test", - srcs = ["assertion_encoder_test.cc"], + name = "assertion_cbor_encoder_test", + srcs = ["assertion_cbor_encoder_test.cc"], deps = [ - ":assertion_encoder", - ":assertion_parser", + ":assertion_cbor_encoder", "//cbor", "//cbor:parse", - "//jumbf:box", - "//jumbf:parse", "//proto:actions_assertion_cbor_proto", "//proto:actions_assertion_cc_proto", "//proto:assertion_cc_proto", @@ -142,13 +136,8 @@ "//proto:ingredient_assertion_cbor_proto", "//proto:ingredient_assertion_cc_proto", "//testing:proto_test_utils", - "//testing:test_validation_tracker", - "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:status_macros", "@abseil-cpp//absl/status:status_matchers", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:string_view", "@googletest//:gtest_main", ], )
diff --git a/assertion/assertion_encoder.cc b/assertion/assertion_cbor_encoder.cc similarity index 76% rename from assertion/assertion_encoder.cc rename to assertion/assertion_cbor_encoder.cc index 2599280..d36b710 100644 --- a/assertion/assertion_encoder.cc +++ b/assertion/assertion_cbor_encoder.cc
@@ -13,19 +13,14 @@ // limitations under the License. // -#include "assertion/assertion_encoder.h" - -#include <utility> +#include "assertion/assertion_cbor_encoder.h" #include "absl/base/no_destructor.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/string_view.h" #include "cbor/options.h" -#include "jumbf/box_builder.h" -#include "jumbf/constants.h" #include "proto/actions_assertion.cbor.h" #include "proto/actions_assertion.pb.h" #include "proto/assertion.pb.h" @@ -54,7 +49,7 @@ } // namespace -absl::StatusOr<absl::Cord> StandardAssertionEncoder::ToCbor( +absl::StatusOr<absl::Cord> StandardAssertionCborEncoder::ToCbor( const Assertion& assertion) const { if (assertion.has_actions()) { return EncodeProtoToCbor(assertion.actions(), options_); @@ -80,18 +75,8 @@ return absl::InvalidArgumentError("Assertion not supported."); } -absl::StatusOr<jumbf::BuiltSuperBox> AssertionEncoder::ToJumbf( - const Assertion& assertion) const { - ABSL_ASSIGN_OR_RETURN(absl::Cord cbor_data, ToCbor(assertion)); - jumbf::SuperBoxBuilder assertion_builder( - jumbf::kCborBoxTypeUuid, {.label = absl::Cord(assertion.label())}); - ABSL_RETURN_IF_ERROR( - assertion_builder.AddContent(jumbf::kCborBoxType, std::move(cbor_data))); - return std::move(assertion_builder).Finalize(); // NOLINT: rvalue method call -} - -const AssertionEncoder& GetAssertionEncoder() { - static const absl::NoDestructor<StandardAssertionEncoder> encoder; +const AssertionCborEncoder& GetAssertionCborEncoder() { + static const absl::NoDestructor<StandardAssertionCborEncoder> encoder; return *encoder; }
diff --git a/assertion/assertion_cbor_encoder.h b/assertion/assertion_cbor_encoder.h new file mode 100644 index 0000000..82d44fb --- /dev/null +++ b/assertion/assertion_cbor_encoder.h
@@ -0,0 +1,53 @@ +// 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. +// + +#ifndef THIRD_PARTY_CREDENTIO_ASSERTION_ASSERTION_CBOR_ENCODER_H_ +#define THIRD_PARTY_CREDENTIO_ASSERTION_ASSERTION_CBOR_ENCODER_H_ + +#include "absl/status/statusor.h" +#include "absl/strings/cord.h" +#include "cbor/options.h" +#include "proto/assertion.pb.h" + +namespace credentio { + +// An encoder to serialize an Assertion's metadata to the CBOR format. +class AssertionCborEncoder { + public: + virtual ~AssertionCborEncoder() = default; + + virtual absl::StatusOr<absl::Cord> ToCbor( + const Assertion& assertion) const = 0; +}; + +class StandardAssertionCborEncoder : public AssertionCborEncoder { + public: + explicit StandardAssertionCborEncoder( + bool skip_validity_checks_for_test = false) + : options_({.skip_validation = skip_validity_checks_for_test}) {} + ~StandardAssertionCborEncoder() override = default; + + absl::StatusOr<absl::Cord> ToCbor(const Assertion& assertion) const override; + + private: + cbor::FromProtoOptions options_; +}; + +// Returns a singleton StandardAssertionCborEncoder. +const AssertionCborEncoder& GetAssertionCborEncoder(); + +} // namespace credentio + +#endif // THIRD_PARTY_CREDENTIO_ASSERTION_ASSERTION_CBOR_ENCODER_H_
diff --git a/assertion/assertion_encoder_test.cc b/assertion/assertion_cbor_encoder_test.cc similarity index 79% rename from assertion/assertion_encoder_test.cc rename to assertion/assertion_cbor_encoder_test.cc index 1faa065..e16d47e 100644 --- a/assertion/assertion_encoder_test.cc +++ b/assertion/assertion_cbor_encoder_test.cc
@@ -13,27 +13,16 @@ // limitations under the License. // -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_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" @@ -46,7 +35,6 @@ #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 { @@ -59,18 +47,18 @@ using ::credentio_testing::ParseTextProtoOrDie; using ::testing::HasSubstr; -TEST(AssertionEncoderTest, UnsupportedAssertionToEncode) { +TEST(AssertionCborEncoderTest, UnsupportedAssertionToEncode) { auto assertion = ParseTextProtoOrDie<Assertion>(R"pb( label: "unsupported_assertion" )pb"); - StandardAssertionEncoder encoder; + StandardAssertionCborEncoder encoder; EXPECT_THAT(encoder.ToCbor(assertion), StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Assertion not supported."))); } -TEST(AssertionEncoderTest, EncodeAction) { +TEST(AssertionCborEncoderTest, EncodeAction) { auto assertion = ParseTextProtoOrDie<Assertion>(R"pb( label: "c2pa.actions.v2" actions { @@ -126,7 +114,7 @@ } )pb"); - StandardAssertionEncoder encoder; + StandardAssertionCborEncoder encoder; auto cbor = encoder.ToCbor(assertion); ABSL_ASSERT_OK(cbor); std::string cbor_string(*cbor); @@ -140,7 +128,7 @@ EXPECT_THAT(result, EqualsProto(assertion.actions())); } -TEST(AssertionEncoderTest, EncodeBmffBasedHash) { +TEST(AssertionCborEncoderTest, EncodeBmffBasedHash) { auto assertion = ParseTextProtoOrDie<Assertion>(R"pb( label: "c2pa.hash.bmff.v3" bmff_based_hash { @@ -163,7 +151,7 @@ } )pb"); - StandardAssertionEncoder encoder; + StandardAssertionCborEncoder encoder; auto cbor = encoder.ToCbor(assertion); ABSL_ASSERT_OK(cbor); std::string cbor_string(*cbor); @@ -177,7 +165,7 @@ EXPECT_THAT(result, EqualsProto(assertion.bmff_based_hash())); } -TEST(AssertionEncoderTest, EncodeDataHash) { +TEST(AssertionCborEncoderTest, EncodeDataHash) { auto assertion = ParseTextProtoOrDie<Assertion>(R"pb( label: "c2pa.hash.data" data_hash { @@ -191,7 +179,7 @@ } )pb"); - StandardAssertionEncoder encoder; + StandardAssertionCborEncoder encoder; auto cbor = encoder.ToCbor(assertion); ABSL_ASSERT_OK(cbor); std::string cbor_string(*cbor); @@ -211,7 +199,7 @@ EXPECT_THAT(actual, EqualsProto(expected)); } -TEST(AssertionEncoderTest, EncodeIngredientAssertion) { +TEST(AssertionCborEncoderTest, EncodeIngredientAssertion) { auto assertion = ParseTextProtoOrDie<Assertion>(R"pb( label: "c2pa.ingredient.v3" ingredient_v3 { @@ -289,7 +277,7 @@ } )pb"); - StandardAssertionEncoder encoder; + StandardAssertionCborEncoder encoder; auto cbor = encoder.ToCbor(assertion); ABSL_ASSERT_OK(cbor); std::string cbor_string(*cbor); @@ -303,7 +291,7 @@ EXPECT_THAT(result, EqualsProto(assertion.ingredient_v3())); } -TEST(AssertionEncoderTest, EncodeBoxesHash) { +TEST(AssertionCborEncoderTest, EncodeBoxesHash) { auto assertion = ParseTextProtoOrDie<Assertion>(R"pb( label: "c2pa.hash.boxes" boxes_hash { @@ -314,7 +302,7 @@ } )pb"); - StandardAssertionEncoder encoder; + StandardAssertionCborEncoder encoder; auto cbor = encoder.ToCbor(assertion); ABSL_ASSERT_OK(cbor); std::string cbor_string(*cbor); @@ -328,50 +316,8 @@ EXPECT_THAT(result, EqualsProto(assertion.boxes_hash())); } -TEST(GetAssertionEncoderTest, ReturnsSameInstance) { - EXPECT_EQ(&GetAssertionEncoder(), &GetAssertionEncoder()); -} - -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; - 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)); +TEST(GetAssertionCborEncoderTest, ReturnsSameInstance) { + EXPECT_EQ(&GetAssertionCborEncoder(), &GetAssertionCborEncoder()); } } // namespace
diff --git a/assertion/assertion_encoder.h b/assertion/assertion_encoder.h deleted file mode 100644 index 822bb04..0000000 --- a/assertion/assertion_encoder.h +++ /dev/null
@@ -1,56 +0,0 @@ -// 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. -// - -#ifndef THIRD_PARTY_CREDENTIO_ASSERTION_ASSERTION_ENCODER_H_ -#define THIRD_PARTY_CREDENTIO_ASSERTION_ASSERTION_ENCODER_H_ - -#include "absl/status/statusor.h" -#include "absl/strings/cord.h" -#include "cbor/options.h" -#include "jumbf/box_builder.h" -#include "proto/assertion.pb.h" - -namespace credentio { - -// An encoder to serialize an Assertion's metadata to the CBOR or JUMBF format. -class AssertionEncoder { - public: - virtual ~AssertionEncoder() = default; - - virtual absl::StatusOr<absl::Cord> ToCbor( - const Assertion& assertion) const = 0; - - virtual absl::StatusOr<jumbf::BuiltSuperBox> ToJumbf( - const Assertion& assertion) const; -}; - -class StandardAssertionEncoder : public AssertionEncoder { - public: - explicit StandardAssertionEncoder(bool skip_validity_checks_for_test = false) - : options_({.skip_validation = skip_validity_checks_for_test}) {} - ~StandardAssertionEncoder() override = default; - - absl::StatusOr<absl::Cord> ToCbor(const Assertion& assertion) const override; - - private: - cbor::FromProtoOptions options_; -}; - -// Returns a singleton StandardAssertionEncoder. -const AssertionEncoder& GetAssertionEncoder(); - -} // namespace credentio - -#endif // THIRD_PARTY_CREDENTIO_ASSERTION_ASSERTION_ENCODER_H_
diff --git a/claim/BUILD b/claim/BUILD index a945a02..33a14d9 100644 --- a/claim/BUILD +++ b/claim/BUILD
@@ -219,7 +219,7 @@ srcs = ["manifest_store_builder_test.cc"], deps = [ ":manifest_store_builder", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//constants:labels", "//crypto:algorithms", "//crypto:hash", @@ -262,19 +262,22 @@ ":claim_cbor_encoder", ":manifest_box", ":signer", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//assertion:assertion_referencer", "//async:async_context", "//constants:labels", "//crypto:algorithms", "//jumbf:box_builder", "//jumbf:constants", + "//jumbf:encode_params", + "//jumbf:utils", "//proto:actions_assertion_cc_proto", "//proto:assertion_cc_proto", "//proto:generator_info_cc_proto", "//proto:hashed_uri_cc_proto", "//proto:manifest_cc_proto", "//proto:multi_asset_hash_assertion_cc_proto", + "//salt:salt_generator", "//uuid", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/container:flat_hash_map", @@ -300,7 +303,7 @@ ":manifest_box_encoder", ":manifest_store_builder", ":signer", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//assertion:assertion_referencer", "//async:async_context", "//constants:labels", @@ -321,6 +324,7 @@ "//proto:signature_info_cc_proto", "//proto:validation_result_cc_proto", "//proto:validation_status_cc_proto", + "//salt:salt_generator", "//testing:create_test_crypto_write_handler", "//testing:local_key_crypto_write_handler", "//testing:manifest_store_test_utils", @@ -464,7 +468,7 @@ ":manifest_box_assembler", ":manifest_box_encoder", ":signer", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//assertion:assertion_referencer", "//async:async_context", "//constants:spec_version", @@ -478,6 +482,7 @@ "//proto:generator_info_cc_proto", "//proto:hashed_uri_cc_proto", "//proto:manifest_cc_proto", + "//salt:salt_generator", "//uuid", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/functional:any_invocable", @@ -499,7 +504,7 @@ ":manifest_box_encoder", ":manifest_store_builder", ":signer", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//async:async_context", "//crypto:crypto_read_handler", "//crypto:crypto_write_handler", @@ -512,6 +517,8 @@ "//proto:manifest_cc_proto", "//proto:signature_info_cc_proto", "//proto:validation_result_cc_proto", + "//proto:validation_status_cc_proto", + "//salt:salt_generator", "//testing:create_test_crypto_write_handler", "//testing:manifest_store_test_utils", "//testing:proto_test_utils",
diff --git a/claim/generator.cc b/claim/generator.cc index 1417c1a..05faf35 100644 --- a/claim/generator.cc +++ b/claim/generator.cc
@@ -23,7 +23,7 @@ #include "absl/log/die_if_null.h" #include "absl/status/status.h" #include "absl/status/statusor.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "assertion/assertion_referencer.h" #include "async/async_context.h" #include "claim/claim_cbor_encoder.h" @@ -41,6 +41,7 @@ #include "proto/generator_info.pb.h" #include "proto/hashed_uri.pb.h" #include "proto/manifest.pb.h" +#include "salt/salt_generator.h" namespace credentio { namespace { @@ -71,13 +72,15 @@ GeneratorInfo generator_info, const HasherFactory* absl_nonnull hash_factory, ClaimSigner* absl_nonnull claim_signer, - const AssertionEncoder* absl_nonnull assertion_cbor_encoder, - const ClaimCborEncoder* absl_nonnull claim_cbor_encoder) + const AssertionCborEncoder* absl_nonnull assertion_cbor_encoder, + const ClaimCborEncoder* absl_nonnull claim_cbor_encoder, + const SaltGenerator* absl_nonnull salt_generator) : generator_info_(std::move(generator_info)), hash_factory_(*ABSL_DIE_IF_NULL(hash_factory)), claim_signer_(*ABSL_DIE_IF_NULL(claim_signer)), assertion_cbor_encoder_(*ABSL_DIE_IF_NULL(assertion_cbor_encoder)), - claim_cbor_encoder_(*ABSL_DIE_IF_NULL(claim_cbor_encoder)) {} + claim_cbor_encoder_(*ABSL_DIE_IF_NULL(claim_cbor_encoder)), + salt_generator_(*ABSL_DIE_IF_NULL(salt_generator)) {} void Encode(ManifestBox* absl_nonnull manifest, const GenerateManifestStoreOptions& options, AsyncContext context, @@ -88,7 +91,7 @@ auto manifest_box_encoder = std::make_shared<ManifestBoxEncoder>( assertion_reference.get(), &generator_info_, &claim_signer_, &assertion_cbor_encoder_, &claim_cbor_encoder_, manifest, - hash_factory_.algorithm()); + hash_factory_.algorithm(), &salt_generator_); manifest_box_encoder->Encode( ToClaimSignerOptions(options, std::move(context)), [assertion_reference, manifest_box_encoder, cb = std::move(cb)]( @@ -101,8 +104,9 @@ GeneratorInfo generator_info_; const HasherFactory& hash_factory_; ClaimSigner& claim_signer_; - const AssertionEncoder& assertion_cbor_encoder_; + const AssertionCborEncoder& assertion_cbor_encoder_; const ClaimCborEncoder& claim_cbor_encoder_; + const SaltGenerator& salt_generator_; }; } // namespace @@ -116,6 +120,7 @@ const CreateManifestEncoderOptions& options) { return std::make_unique<ManifestEncoderImpl>( options.generator_info, options.hash_factory, options.claim_signer, - options.assertion_cbor_encoder, options.claim_cbor_encoder); + options.assertion_cbor_encoder, options.claim_cbor_encoder, + options.salt_generator); } } // namespace credentio
diff --git a/claim/generator.h b/claim/generator.h index a2f59d5..c179612 100644 --- a/claim/generator.h +++ b/claim/generator.h
@@ -21,7 +21,7 @@ #include "absl/base/nullability.h" #include "absl/functional/any_invocable.h" #include "absl/status/statusor.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "async/async_context.h" #include "claim/claim_cbor_encoder.h" #include "claim/manifest_box.h" @@ -33,6 +33,7 @@ #include "proto/generator.pb.h" #include "proto/generator_info.pb.h" #include "proto/manifest.pb.h" +#include "salt/salt_generator.h" #include "uuid/uuid.h" namespace credentio { @@ -74,11 +75,14 @@ // Required. ClaimSigner* absl_nonnull claim_signer; // Required. - const AssertionEncoder* absl_nonnull assertion_cbor_encoder; + const AssertionCborEncoder* absl_nonnull assertion_cbor_encoder; // Required. const ClaimCborEncoder* absl_nonnull claim_cbor_encoder; // Required. const HasherFactory* absl_nonnull hash_factory; + // Required. Salts every assertion; use `SaltGenerator::NoOp()` to disable + // salting. Must outlive the created encoder. + const SaltGenerator* absl_nonnull salt_generator; }; absl::StatusOr<std::unique_ptr<ManifestEncoder>> CreateManifestEncoder(
diff --git a/claim/generator_test.cc b/claim/generator_test.cc index 8484500..5118f64 100644 --- a/claim/generator_test.cc +++ b/claim/generator_test.cc
@@ -15,6 +15,7 @@ #include "claim/generator.h" +#include <cstddef> #include <memory> #include <string> #include <utility> @@ -26,7 +27,7 @@ #include "absl/strings/cord.h" #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "async/async_context.h" #include "claim/claim_cbor_encoder.h" #include "claim/local_signer.h" @@ -49,6 +50,8 @@ #include "proto/manifest.pb.h" #include "proto/signature_info.pb.h" #include "proto/validation_result.pb.h" +#include "proto/validation_status.pb.h" +#include "salt/salt_generator.h" #include "testing/create_test_crypto_write_handler.h" #include "testing/manifest_store_test_utils.h" #include "testing/proto_test_utils.h" @@ -63,6 +66,7 @@ using ::credentio_testing::ParseTextProtoOrDie; using ::credentio_testing::PartiallyEqualsProto; using ::testing::HasSubstr; +using ::testing::IsEmpty; using ::testing::Pointee; using ::testing::Pointwise; @@ -89,6 +93,16 @@ Uuid Generate() const override { return kUuid; } }; +class FakeSaltGenerator : public SaltGenerator { + public: + explicit FakeSaltGenerator(std::string salt) : salt_(std::move(salt)) {} + + absl::StatusOr<std::string> Generate() const override { return salt_; } + + private: + std::string salt_; +}; + class ManifestAssemblerTest : public testing::Test { protected: ManifestAssemblerTest() { @@ -573,9 +587,10 @@ CreateManifestEncoder({ .generator_info = generator_info_, .claim_signer = claim_signer_.get(), - .assertion_cbor_encoder = &GetAssertionEncoder(), + .assertion_cbor_encoder = &GetAssertionCborEncoder(), .claim_cbor_encoder = &GetClaimCborEncoder(), .hash_factory = &hash_factory_, + .salt_generator = &SaltGenerator::NoOp(), }); ABSL_CHECK_OK(manifest_encoder); manifest_encoder_ = std::move(*manifest_encoder); @@ -860,5 +875,58 @@ expected_validated_result)))); } +TEST_F(ManifestEncoderTest, EncodeManifestWithSaltGenerator) { + constexpr absl::string_view kSalt = "generator_salt16"; + FakeSaltGenerator salt_generator{std::string(kSalt)}; + absl::StatusOr<std::unique_ptr<ManifestEncoder>> manifest_encoder = + CreateManifestEncoder({ + .generator_info = generator_info_, + .claim_signer = claim_signer_.get(), + .assertion_cbor_encoder = &GetAssertionCborEncoder(), + .claim_cbor_encoder = &GetClaimCborEncoder(), + .hash_factory = &hash_factory_, + .salt_generator = &salt_generator, + }); + ABSL_ASSERT_OK(manifest_encoder); + ManifestBox manifest_box = { + .label = "urn:c2pa:03020100-0504-0706-0908-0f0e0d0c0b0a", + .instance_id = "12345678-1234-1234-1234-123456789012", + .hard_binding_assertion = ParseTextProtoOrDie(R"pb( + label: "c2pa.hash.bmff.v3" + bmff_based_hash { exclusions { xpath: "" } } + )pb"), + .actions_assertion = ParseTextProtoOrDie(R"pb( + label: "c2pa.actions.v2" + actions { + actions { + action: "c2pa.created" + digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + } + )pb"), + .ingredient_assertions = {}, + .manifest_store_builder = ManifestStoreBuilder(&hash_factory_), + }; + + absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = + Encode(**manifest_encoder, manifest_box); + ABSL_ASSERT_OK(built_manifest_store_box); + std::string manifest_store_bytes( + std::move(*built_manifest_store_box).AsCord()); + + // The hard binding and actions assertions are each salted once. + int salt_count = 0; + for (size_t pos = manifest_store_bytes.find(kSalt); pos != std::string::npos; + pos = manifest_store_bytes.find(kSalt, pos + kSalt.size())) { + ++salt_count; + } + EXPECT_EQ(salt_count, 2); + absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> validated = + ValidateManifestStore(manifest_store_bytes); + ABSL_ASSERT_OK(validated); + EXPECT_THAT((*validated)->active_manifest().validation().failures(), + IsEmpty()); +} + } // namespace } // namespace credentio
diff --git a/claim/manifest_box_encoder.cc b/claim/manifest_box_encoder.cc index d93c169..12969d9 100644 --- a/claim/manifest_box_encoder.cc +++ b/claim/manifest_box_encoder.cc
@@ -27,18 +27,22 @@ #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 { @@ -63,6 +67,39 @@ 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; }; @@ -118,7 +155,8 @@ } ABSL_ASSIGN_OR_RETURN(jumbf::BuiltSuperBox assertion_box, - assertion_encoder_.ToJumbf(assertion)); + 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()] = @@ -160,9 +198,11 @@ return absl::InvalidArgumentError( "Gathered assertion content type must be JSON."); } - jumbf::SuperBoxBuilder builder( - jumbf::kJsonBoxTypeUuid, - {.label = absl::Cord(gathered_assertion.label())}); + 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,
diff --git a/claim/manifest_box_encoder.h b/claim/manifest_box_encoder.h index b78550d..77fc5b5 100644 --- a/claim/manifest_box_encoder.h +++ b/claim/manifest_box_encoder.h
@@ -27,7 +27,7 @@ #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/cord.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "assertion/assertion_referencer.h" #include "claim/claim_cbor_encoder.h" #include "claim/manifest_box.h" @@ -40,6 +40,7 @@ #include "proto/generator_info.pb.h" #include "proto/hashed_uri.pb.h" #include "proto/multi_asset_hash_assertion.pb.h" +#include "salt/salt_generator.h" namespace credentio { @@ -54,9 +55,10 @@ const AssertionReferencer* absl_nonnull assertion_reference, const GeneratorInfo* absl_nonnull generator_info, const ClaimSigner* absl_nonnull claim_signer, - const AssertionEncoder* absl_nonnull assertion_cbor_encoder, + const AssertionCborEncoder* absl_nonnull assertion_cbor_encoder, const ClaimCborEncoder* absl_nonnull claim_cbor_encoder, - ManifestBox* absl_nonnull manifest, HashAlgorithm hash_algorithm) + ManifestBox* absl_nonnull manifest, HashAlgorithm hash_algorithm, + const SaltGenerator* absl_nonnull salt_generator) : assertion_reference_(*ABSL_DIE_IF_NULL(assertion_reference)), generator_info_(*ABSL_DIE_IF_NULL(generator_info)), claim_signer_(*ABSL_DIE_IF_NULL(claim_signer)), @@ -64,8 +66,9 @@ {.label = absl::Cord(manifest->label)}), manifest_(*ABSL_DIE_IF_NULL(manifest)), hash_algorithm_(hash_algorithm), - assertion_encoder_(*ABSL_DIE_IF_NULL(assertion_cbor_encoder)), - claim_cbor_encoder_(*ABSL_DIE_IF_NULL(claim_cbor_encoder)) {} + assertion_cbor_encoder_(*ABSL_DIE_IF_NULL(assertion_cbor_encoder)), + claim_cbor_encoder_(*ABSL_DIE_IF_NULL(claim_cbor_encoder)), + salt_generator_(*ABSL_DIE_IF_NULL(salt_generator)) {} // Encodes the manifest to a JUMBF-type box. // Consumes the provided ManifestBox. @@ -79,7 +82,8 @@ private: // Encodes the given assertion and adds it to assertion_boxes_, - // assertion_hashed_uris_, and assertion_url_to_index_. + // assertion_hashed_uris_, and assertion_url_to_index_. The assertion box is + // salted with salt_generator_. absl::Status AddAssertion(Assertion& assertion); // Populates hashes for hashed URIs in the given actions assertion, then adds @@ -126,8 +130,9 @@ // Needed for resolving references between assertions. absl::flat_hash_map<std::string, size_t> assertion_url_to_index_; HashAlgorithm hash_algorithm_; - const AssertionEncoder& assertion_encoder_; + const AssertionCborEncoder& assertion_cbor_encoder_; const ClaimCborEncoder& claim_cbor_encoder_; + const SaltGenerator& salt_generator_; }; } // namespace credentio
diff --git a/claim/manifest_box_encoder_test.cc b/claim/manifest_box_encoder_test.cc index d03ed35..9c95a15 100644 --- a/claim/manifest_box_encoder_test.cc +++ b/claim/manifest_box_encoder_test.cc
@@ -15,6 +15,7 @@ #include "claim/manifest_box_encoder.h" +#include <cstddef> #include <memory> #include <string> #include <utility> @@ -28,7 +29,7 @@ #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "assertion/assertion_referencer.h" #include "async/async_context.h" #include "claim/claim_cbor_encoder.h" @@ -57,6 +58,7 @@ #include "proto/signature_info.pb.h" #include "proto/validation_result.pb.h" #include "proto/validation_status.pb.h" +#include "salt/salt_generator.h" #include "testing/create_test_crypto_write_handler.h" #include "testing/local_key_crypto_write_handler.h" #include "testing/manifest_store_test_utils.h" @@ -70,6 +72,7 @@ using ::absl_testing::StatusIs; using ::credentio_testing::ParseTextProtoOrDie; using ::testing::HasSubstr; +using ::testing::IsEmpty; using ::testing::Pointee; constexpr auto kUuid = @@ -140,6 +143,17 @@ Uuid Generate() const override { return kUuid; } }; +class FakeSaltGenerator : public SaltGenerator { + public: + explicit FakeSaltGenerator(absl::StatusOr<std::string> salt) + : salt_(std::move(salt)) {} + + absl::StatusOr<std::string> Generate() const override { return salt_; } + + private: + absl::StatusOr<std::string> salt_; +}; + class FakeHasherFactory : public HasherFactory { public: absl::StatusOr<std::unique_ptr<Hasher>> Create() const override { @@ -161,7 +175,7 @@ std::unique_ptr<CryptoWriteHandler> crypto_writer_; GeneratorInfo generator_info_; FakeHasherFactory hasher_factory_; - StandardAssertionEncoder assertion_cbor_encoder_; + StandardAssertionCborEncoder assertion_cbor_encoder_; const ClaimCborEncoder& claim_cbor_encoder_; ManifestBox CreateMinimalManifestBox() { @@ -274,7 +288,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -298,7 +312,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); GenerateManifestStoreOptions options; options.set_time_stamping_mode( GenerateManifestStoreOptions::TIME_STAMPING_MODE_UNSPECIFIED); @@ -316,7 +330,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); GenerateManifestStoreOptions options; options.set_time_stamping_mode( GenerateManifestStoreOptions::TIME_STAMPING_OPTIONAL); @@ -334,7 +348,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); GenerateManifestStoreOptions options; options.set_time_stamping_mode( GenerateManifestStoreOptions::TIME_STAMPING_REQUIRED); @@ -351,7 +365,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); GenerateManifestStoreOptions options; options.set_time_stamping_mode( GenerateManifestStoreOptions::TIME_STAMPING_REQUIRED); @@ -372,7 +386,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); GenerateManifestStoreOptions options; options.set_time_stamping_mode( GenerateManifestStoreOptions::TIME_STAMPING_OMITTED_WITH_PADDING); @@ -406,7 +420,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); EXPECT_THAT(Encode(manifest_box_encoder), StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Opened action must have exactly one " @@ -472,7 +486,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); EXPECT_THAT(Encode(manifest_box_encoder), StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("Opened action must have exactly one " @@ -660,7 +674,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -915,7 +929,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -1080,7 +1094,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -1349,7 +1363,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -1643,7 +1657,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -1850,7 +1864,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); ABSL_ASSERT_OK(built_manifest_store_box); @@ -1900,7 +1914,7 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = Encode(manifest_box_encoder); @@ -1969,12 +1983,89 @@ ManifestBoxEncoder manifest_box_encoder( &assertion_reference, &generator_info_, &claim_signer, &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, - hasher_factory_.algorithm()); + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); EXPECT_THAT(Encode(manifest_box_encoder), StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("content type must be JSON"))); } +// Returns the number of non-overlapping occurrences of `needle` in `haystack`. +int CountOccurrences(absl::string_view haystack, absl::string_view needle) { + int count = 0; + for (size_t pos = haystack.find(needle); pos != absl::string_view::npos; + pos = haystack.find(needle, pos + needle.size())) { + ++count; + } + return count; +} + +TEST_F(ManifestBoxEncoderTest, SaltsAllAssertionsWithSaltGenerator) { + constexpr absl::string_view kSalt = "0123456789abcdef"; + FakeSaltGenerator salt_generator{std::string(kSalt)}; + GatheredAssertion gathered; + gathered.set_label("com.example.gathered"); + gathered.set_data("{}"); + gathered.set_content_type(GatheredAssertion::CONTENT_TYPE_JSON); + ManifestBox manifest_box = CreateMinimalManifestBox(); + manifest_box.gathered_assertions = {gathered}; + AssertionReferencer assertion_reference(&hasher_factory_); + LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_); + ManifestBoxEncoder manifest_box_encoder( + &assertion_reference, &generator_info_, &claim_signer, + &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, + hasher_factory_.algorithm(), &salt_generator); + + absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = + Encode(manifest_box_encoder); + ABSL_ASSERT_OK(built_manifest_store_box); + std::string manifest_store_bytes( + std::move(*built_manifest_store_box).AsCord()); + + // Hard binding, actions and gathered assertions each get one C2PA salt box + // ('c2sh') containing the salt. + EXPECT_EQ(CountOccurrences(manifest_store_bytes, "c2sh"), 3); + EXPECT_EQ(CountOccurrences(manifest_store_bytes, kSalt), 3); + // Salted assertions must still validate. + absl::StatusOr<std::unique_ptr<PartialValidationResultProto>> validated = + ValidateManifestStore(manifest_store_bytes); + ABSL_ASSERT_OK(validated); + EXPECT_THAT((*validated)->active_manifest().validation().failures(), + IsEmpty()); +} + +TEST_F(ManifestBoxEncoderTest, DoesNotSaltAssertionsWithNoOpSaltGenerator) { + ManifestBox manifest_box = CreateMinimalManifestBox(); + AssertionReferencer assertion_reference(&hasher_factory_); + LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_); + ManifestBoxEncoder manifest_box_encoder( + &assertion_reference, &generator_info_, &claim_signer, + &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, + hasher_factory_.algorithm(), &SaltGenerator::NoOp()); + + absl::StatusOr<jumbf::BuiltSuperBox> built_manifest_store_box = + Encode(manifest_box_encoder); + ABSL_ASSERT_OK(built_manifest_store_box); + std::string manifest_store_bytes( + std::move(*built_manifest_store_box).AsCord()); + + // No C2PA salt box ('c2sh') is written. + EXPECT_EQ(CountOccurrences(manifest_store_bytes, "c2sh"), 0); +} + +TEST_F(ManifestBoxEncoderTest, ReturnsErrorWhenSaltGenerationFails) { + FakeSaltGenerator salt_generator{absl::InternalError("salt failure")}; + ManifestBox manifest_box = CreateMinimalManifestBox(); + AssertionReferencer assertion_reference(&hasher_factory_); + LocalClaimSigner claim_signer(crypto_writer_.get(), &hasher_factory_); + ManifestBoxEncoder manifest_box_encoder( + &assertion_reference, &generator_info_, &claim_signer, + &assertion_cbor_encoder_, &claim_cbor_encoder_, &manifest_box, + hasher_factory_.algorithm(), &salt_generator); + + EXPECT_THAT(Encode(manifest_box_encoder), + StatusIs(absl::StatusCode::kInternal, HasSubstr("salt failure"))); +} + } // namespace } // namespace credentio
diff --git a/claim/manifest_store_builder_test.cc b/claim/manifest_store_builder_test.cc index bced268..f1642c7 100644 --- a/claim/manifest_store_builder_test.cc +++ b/claim/manifest_store_builder_test.cc
@@ -28,7 +28,7 @@ #include "absl/status/statusor.h" #include "absl/strings/cord.h" #include "absl/strings/string_view.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "constants/labels.h" #include "crypto/algorithms.h" #include "crypto/default/hasher.h" @@ -103,7 +103,7 @@ ManifestStoreBuilderTest() : hasher_factory_(), builder_(&hasher_factory_) {}; FakeHasherFactory hasher_factory_; ManifestStoreBuilder builder_; - StandardAssertionEncoder cbor_encoder_; + StandardAssertionCborEncoder cbor_encoder_; }; TEST_F(ManifestStoreBuilderTest, AddActiveManifest) {
diff --git a/generator/BUILD b/generator/BUILD index 79b6e83..44517e6 100644 --- a/generator/BUILD +++ b/generator/BUILD
@@ -83,7 +83,7 @@ ":generator", ":generator_impl", ":manifest_store_transformer", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//async:async_context", "//claim:claim_cbor_encoder", "//claim:generator", @@ -109,6 +109,7 @@ "//proto:manifest_cc_proto", "//proto:signature_info_cc_proto", "//proto:validation_result_cc_proto", + "//salt:salt_generator", "//testing:create_test_crypto_write_handler", "//testing:manifest_store_test_utils", "//testing:proto_test_utils", @@ -130,7 +131,7 @@ hdrs = ["generator_options.h"], deps = [ ":manifest_store_transformer", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//claim:claim_cbor_encoder", "//claim:generator", "//claim:signer", @@ -138,6 +139,8 @@ "//crypto:hash", "//crypto/default:hasher", "//proto:generator_info_cc_proto", + "//salt:random_salt_generator", + "//salt:salt_generator", "//uuid", "@abseil-cpp//absl/base:nullability", ], @@ -151,13 +154,14 @@ ":generator", ":generator_impl", ":generator_options", - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//async:async_context", "//claim:claim_cbor_encoder", "//claim:generator", "//claim:manifest_box_assembler", "//claim:signer", "//crypto:hash", + "//salt:salt_generator", "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/functional:any_invocable", "@abseil-cpp//absl/status", @@ -181,9 +185,12 @@ "//crypto:hash", "//proto:generator_cc_proto", "//proto:generator_info_cc_proto", + "//salt:salt_generator", + "//testing:proto_test_utils", "@abseil-cpp//absl/functional:any_invocable", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings:string_view", "@abseil-cpp//absl/synchronization", "@googletest//:gtest_main",
diff --git a/generator/generator_factory.cc b/generator/generator_factory.cc index b30e6d6..0d2b1ef 100644 --- a/generator/generator_factory.cc +++ b/generator/generator_factory.cc
@@ -23,7 +23,7 @@ #include "absl/status/status.h" #include "absl/status/status_macros.h" #include "absl/status/statusor.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "async/async_context.h" #include "claim/claim_cbor_encoder.h" #include "claim/generator.h" @@ -33,6 +33,7 @@ #include "generator/generator.h" #include "generator/generator_impl.h" #include "generator/generator_options.h" +#include "salt/salt_generator.h" namespace credentio { namespace { @@ -112,11 +113,12 @@ .claim_signer = claim_signer, .assertion_cbor_encoder = options.assertion_cbor_encoder ? options.assertion_cbor_encoder - : &GetAssertionEncoder(), + : &GetAssertionCborEncoder(), .claim_cbor_encoder = options.claim_cbor_encoder ? options.claim_cbor_encoder : &GetClaimCborEncoder(), .hash_factory = hash_factory, + .salt_generator = options.salt_generator, })); return std::make_unique<ManifestStoreGeneratorImpl>( std::move(manifest_assembler), std::move(manifest_encoder), @@ -134,6 +136,9 @@ if (options.uuid_generator == nullptr) { return absl::InvalidArgumentError("uuid_generator is required"); } + if (options.salt_generator == nullptr) { + return absl::InvalidArgumentError("salt_generator is required"); + } if (options.hash_factory == nullptr) { return absl::InvalidArgumentError("hash_factory is required"); }
diff --git a/generator/generator_factory_test.cc b/generator/generator_factory_test.cc index 4de7043..196b1ef 100644 --- a/generator/generator_factory_test.cc +++ b/generator/generator_factory_test.cc
@@ -22,6 +22,7 @@ #include "absl/functional/any_invocable.h" #include "absl/status/status.h" #include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" #include "async/async_context.h" @@ -36,11 +37,14 @@ #include "gtest/gtest.h" #include "proto/generator.pb.h" #include "proto/generator_info.pb.h" +#include "salt/salt_generator.h" +#include "testing/proto_test_utils.h" namespace credentio { namespace { using ::absl_testing::StatusIs; +using ::credentio_testing::ParseTextProtoOrDie; using ::testing::HasSubstr; using ::testing::NotNull; @@ -69,6 +73,19 @@ int* call_count_; }; +class CountingSaltGenerator : public SaltGenerator { + public: + absl::StatusOr<std::string> Generate() const override { + ++call_count_; + return "0123456789abcdef"; + } + + int call_count() const { return call_count_; } + + private: + mutable int call_count_ = 0; +}; + TEST(GeneratorFactoryTest, ValidateOptionsRejectsNullSigningClaimSigner) { FakeClaimSigner template_signer; GeneratorOptions options; @@ -119,6 +136,20 @@ HasSubstr("hash_factory is required"))); } +TEST(GeneratorFactoryTest, ValidateOptionsRejectsNullSaltGenerator) +__attribute__((no_sanitize("nullability"))) { + FakeClaimSigner signing_signer; + FakeClaimSigner template_signer; + GeneratorOptions options; + options.signing_claim_signer = &signing_signer; + options.template_claim_signer = &template_signer; + options.salt_generator = nullptr; + + EXPECT_THAT(CreateGenerator(std::move(options)), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("salt_generator is required"))); +} + TEST(GeneratorFactoryTest, ValidateOptionsRejectsNullManifestAssemblerFactory) { FakeClaimSigner signing_signer; FakeClaimSigner template_signer; @@ -219,5 +250,54 @@ HasSubstr("generator_context"))); } +TEST(GeneratorFactoryTest, SaltGeneratorUsedBySigningAndTemplateGenerators) { + FakeClaimSigner signing_signer; + FakeClaimSigner template_signer; + CountingSaltGenerator salt_generator; + GeneratorOptions options; + options.generator_info.set_name("TestGenerator"); + options.generator_info.set_spec_version("2.2.0"); + options.signing_claim_signer = &signing_signer; + options.template_claim_signer = &template_signer; + options.salt_generator = &salt_generator; + absl::StatusOr<std::unique_ptr<ManifestStoreGenerator>> generator = + CreateGenerator(std::move(options)); + ABSL_ASSERT_OK(generator); + GenerateManifestStoreRequest request = ParseTextProtoOrDie(R"pb( + hard_binding { + box_hash { + alg: "sha256" + boxes { names: "some name" hash: "some hash" } + } + } + actions { + created { + digital_source_type: "https://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture" + } + } + )pb"); + + absl::Notification gen_done; + (*generator) + ->GenerateManifestStore( + request, AsyncContext(), + [&](absl::StatusOr<GenerateManifestStoreResponse> res) { + gen_done.Notify(); + }); + gen_done.WaitForNotification(); + // One salt each for the hard binding and actions assertions. + EXPECT_EQ(salt_generator.call_count(), 2); + + absl::Notification template_done; + (*generator) + ->GenerateManifestStoreTemplate( + request, AsyncContext(), + [&](absl::StatusOr<ManifestStoreTemplate> res) { + template_done.Notify(); + }); + template_done.WaitForNotification(); + EXPECT_EQ(salt_generator.call_count(), 4); +} + } // namespace } // namespace credentio
diff --git a/generator/generator_impl_test.cc b/generator/generator_impl_test.cc index 689d69c..32b2ce4 100644 --- a/generator/generator_impl_test.cc +++ b/generator/generator_impl_test.cc
@@ -26,7 +26,7 @@ #include "absl/strings/cord.h" #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "async/async_context.h" #include "claim/claim_cbor_encoder.h" #include "claim/generator.h" @@ -57,6 +57,7 @@ #include "proto/manifest.pb.h" #include "proto/signature_info.pb.h" #include "proto/validation_result.pb.h" +#include "salt/salt_generator.h" #include "testing/create_test_crypto_write_handler.h" #include "testing/manifest_store_test_utils.h" #include "testing/proto_test_utils.h" @@ -210,9 +211,10 @@ CreateManifestEncoder({ .generator_info = generator_info_, .claim_signer = claim_signer_.get(), - .assertion_cbor_encoder = &GetAssertionEncoder(), + .assertion_cbor_encoder = &GetAssertionCborEncoder(), .claim_cbor_encoder = &GetClaimCborEncoder(), .hash_factory = &hash_factory_, + .salt_generator = &SaltGenerator::NoOp(), }); ABSL_CHECK_OK(manifest_encoder);
diff --git a/generator/generator_options.h b/generator/generator_options.h index db3c507..e765356 100644 --- a/generator/generator_options.h +++ b/generator/generator_options.h
@@ -17,7 +17,7 @@ #define THIRD_PARTY_CREDENTIO_GENERATOR_GENERATOR_OPTIONS_H_ #include "absl/base/nullability.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "claim/claim_cbor_encoder.h" #include "claim/generator.h" #include "claim/signer.h" @@ -26,6 +26,8 @@ #include "crypto/hash.h" #include "generator/manifest_store_transformer.h" #include "proto/generator_info.pb.h" +#include "salt/random_salt_generator.h" +#include "salt/salt_generator.h" #include "uuid/uuid.h" namespace credentio { @@ -43,7 +45,7 @@ // Optional. If provided, must outlive the created generator. const HasherFactory* absl_nonnull hash_factory = &DefaultHasherFactory(); // Optional. If provided, must outlive the created generator. - const AssertionEncoder* assertion_cbor_encoder = nullptr; + const AssertionCborEncoder* assertion_cbor_encoder = nullptr; // Optional. If provided, must outlive the created generator. const ClaimCborEncoder* claim_cbor_encoder = nullptr; // Optional. If provided, must outlive the created generator. @@ -52,6 +54,10 @@ ManifestAssemblerFactory manifest_assembler_factory = CreateManifestAssembler; // The C2PA specification version the generated manifests conform to. SpecVersion spec_version = SpecVersion::kC2pa_2_2; + // Optional. Salts every assertion; use `SaltGenerator::NoOp()` to disable + // salting. If provided, must outlive the created generator. + const SaltGenerator* absl_nonnull salt_generator = + &RandomSaltGenerator::Default(); }; } // namespace credentio
diff --git a/salt/BUILD b/salt/BUILD index 5531c49..d04a345 100644 --- a/salt/BUILD +++ b/salt/BUILD
@@ -13,11 +13,42 @@ # limitations under the License. # -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") package(default_visibility = ["//visibility:public"]) cc_library( name = "salt_generator", + srcs = ["salt_generator.cc"], hdrs = ["salt_generator.h"], + deps = [ + "@abseil-cpp//absl/base:no_destructor", + "@abseil-cpp//absl/status:statusor", + ], +) + +cc_library( + name = "random_salt_generator", + srcs = ["random_salt_generator.cc"], + hdrs = ["random_salt_generator.h"], + deps = [ + ":salt_generator", + "@abseil-cpp//absl/base:no_destructor", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@boringssl//:crypto", + ], +) + +cc_test( + name = "random_salt_generator_test", + srcs = ["random_salt_generator_test.cc"], + deps = [ + ":random_salt_generator", + ":salt_generator", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/status:status_matchers", + "@abseil-cpp//absl/status:statusor", + "@googletest//:gtest_main", + ], )
diff --git a/salt/random_salt_generator.cc b/salt/random_salt_generator.cc new file mode 100644 index 0000000..977eb15 --- /dev/null +++ b/salt/random_salt_generator.cc
@@ -0,0 +1,42 @@ +// 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 "salt/random_salt_generator.h" + +#include <cstdint> +#include <string> + +#include "absl/base/no_destructor.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "openssl/rand.h" +#include "salt/salt_generator.h" + +namespace credentio { + +const RandomSaltGenerator& RandomSaltGenerator::Default() { + static const absl::NoDestructor<RandomSaltGenerator> kDefaultSaltGenerator; + return *kDefaultSaltGenerator; +} + +absl::StatusOr<std::string> RandomSaltGenerator::Generate() const { + std::string salt(kDefaultSaltLength, '\0'); + if (RAND_bytes(reinterpret_cast<uint8_t*>(salt.data()), salt.size()) != 1) { + return absl::InternalError("Failed to generate random salt."); + } + return salt; +} + +} // namespace credentio
diff --git a/salt/random_salt_generator.h b/salt/random_salt_generator.h new file mode 100644 index 0000000..aaaa67c --- /dev/null +++ b/salt/random_salt_generator.h
@@ -0,0 +1,39 @@ +// 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. +// + +#ifndef THIRD_PARTY_CREDENTIO_SALT_RANDOM_SALT_GENERATOR_H_ +#define THIRD_PARTY_CREDENTIO_SALT_RANDOM_SALT_GENERATOR_H_ + +#include <string> + +#include "absl/status/statusor.h" +#include "salt/salt_generator.h" + +namespace credentio { + +// Generates salts of `kDefaultSaltLength` bytes from a cryptographically secure +// pseudorandom number generator, for use as C2PA salts (C2PA specification v2.2 +// Section 8.4.2.3). Thread-safe. +class RandomSaltGenerator final : public SaltGenerator { + public: + // Returns a process-wide instance. + static const RandomSaltGenerator& Default(); + + absl::StatusOr<std::string> Generate() const override; +}; + +} // namespace credentio + +#endif // THIRD_PARTY_CREDENTIO_SALT_RANDOM_SALT_GENERATOR_H_
diff --git a/salt/random_salt_generator_test.cc b/salt/random_salt_generator_test.cc new file mode 100644 index 0000000..a341015 --- /dev/null +++ b/salt/random_salt_generator_test.cc
@@ -0,0 +1,62 @@ +// 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 "salt/random_salt_generator.h" + +#include <string> +#include <utility> + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status_matchers.h" +#include "absl/status/statusor.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "salt/salt_generator.h" + +namespace credentio { +namespace { + +using ::absl_testing::IsOkAndHolds; +using ::testing::SizeIs; + +TEST(RandomSaltGeneratorTest, GeneratesSaltOfDefaultLength) { + RandomSaltGenerator salt_generator; + + EXPECT_THAT(salt_generator.Generate(), + IsOkAndHolds(SizeIs(kDefaultSaltLength))); +} + +TEST(RandomSaltGeneratorTest, DefaultReturnsSameInstance) { + EXPECT_EQ(&RandomSaltGenerator::Default(), &RandomSaltGenerator::Default()); + EXPECT_THAT(RandomSaltGenerator::Default().Generate(), + IsOkAndHolds(SizeIs(kDefaultSaltLength))); +} + +TEST(RandomSaltGeneratorTest, GeneratesUniqueSalts) { + constexpr int kNumSalts = 1000; + RandomSaltGenerator salt_generator; + + absl::flat_hash_set<std::string> salts; + for (int i = 0; i < kNumSalts; ++i) { + absl::StatusOr<std::string> salt = salt_generator.Generate(); + ABSL_ASSERT_OK(salt); + salts.insert(*std::move(salt)); + } + + EXPECT_THAT(salts, SizeIs(kNumSalts)); +} + +} // namespace +} // namespace credentio
diff --git a/salt/salt_generator.cc b/salt/salt_generator.cc new file mode 100644 index 0000000..9db77cb --- /dev/null +++ b/salt/salt_generator.cc
@@ -0,0 +1,40 @@ +// 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 "salt/salt_generator.h" + +#include <string> + +#include "absl/base/no_destructor.h" +#include "absl/status/statusor.h" + +namespace credentio { +namespace { + +class NoOpSaltGenerator final : public SaltGenerator { + public: + absl::StatusOr<std::string> Generate() const override { + return std::string(); + } +}; + +} // namespace + +const SaltGenerator& SaltGenerator::NoOp() { + static const absl::NoDestructor<NoOpSaltGenerator> kNoOpSaltGenerator; + return *kNoOpSaltGenerator; +} + +} // namespace credentio
diff --git a/salt/salt_generator.h b/salt/salt_generator.h index b274c75..c5827e9 100644 --- a/salt/salt_generator.h +++ b/salt/salt_generator.h
@@ -19,10 +19,12 @@ #include <cstddef> #include <string> +#include "absl/status/statusor.h" + namespace credentio { -// Default salt length in bytes (128 bits of entropy) per C2PA specification -// Section 8.3.1.3 and Section 8.4.2.3. +// Default salt length in bytes (128 bits of entropy). C2PA specification v2.2 +// Section 8.4.2.3 allows salts of 16 or 32 bytes. inline constexpr size_t kDefaultSaltLength = 16; // Abstract interface for generating assertion salt bytes. @@ -34,8 +36,13 @@ public: virtual ~SaltGenerator() = default; - // Generates random salt bytes of length `kDefaultSaltLength`. - virtual std::string Generate() const = 0; + // Returns a generator that produces empty salts, i.e. assertions are not + // salted. + static const SaltGenerator& NoOp(); + + // Generates random salt bytes of length `kDefaultSaltLength`. An empty result + // means no salt. Returns an error if a salt cannot be generated. + virtual absl::StatusOr<std::string> Generate() const = 0; }; } // namespace credentio
diff --git a/testing/BUILD b/testing/BUILD index 74e0186..f97d799 100644 --- a/testing/BUILD +++ b/testing/BUILD
@@ -242,7 +242,7 @@ srcs = ["jumbf_utils.cc"], hdrs = ["jumbf_utils.h"], deps = [ - "//assertion:assertion_encoder", + "//assertion:assertion_cbor_encoder", "//constants:labels", "//crypto:hash", "//crypto/default:hasher",
diff --git a/testing/jumbf_utils.cc b/testing/jumbf_utils.cc index d275786..cd1a2c6 100644 --- a/testing/jumbf_utils.cc +++ b/testing/jumbf_utils.cc
@@ -28,7 +28,7 @@ #include "absl/status/status_matchers.h" // IWYU pragma: keep #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "constants/labels.h" #include "crypto/default/hasher.h" #include "crypto/hash.h" @@ -126,7 +126,7 @@ } std::string EncodeAssertionBoxOrDie(const Assertion& assertion, - const AssertionEncoder& encoder) { + const AssertionCborEncoder& encoder) { auto assertion_cbor = encoder.ToCbor(assertion); ABSL_CHECK_OK(assertion_cbor); std::string assertion_cbor_box =
diff --git a/testing/jumbf_utils.h b/testing/jumbf_utils.h index e1912ff..c0ac50a 100644 --- a/testing/jumbf_utils.h +++ b/testing/jumbf_utils.h
@@ -22,7 +22,7 @@ #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "assertion/assertion_encoder.h" +#include "assertion/assertion_cbor_encoder.h" #include "jumbf/box.h" #include "proto/assertion.pb.h" @@ -61,7 +61,7 @@ // Returns a serialized assertion JUMBF box. std::string EncodeAssertionBoxOrDie(const Assertion& assertion, - const AssertionEncoder& encoder); + const AssertionCborEncoder& encoder); // Returns the hash of the given box per // https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html#_hashed_uris.