From 6cef38ca3659be9d65b05645e651418090f02a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Meusel?= Date: Fri, 16 Aug 2024 10:23:38 +0200 Subject: [PATCH 1/3] Refactor: move convenience method to header --- src/lib/pubkey/pubkey.cpp | 5 ----- src/lib/pubkey/pubkey.h | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib/pubkey/pubkey.cpp b/src/lib/pubkey/pubkey.cpp index 15fb3632157..c7add4f8cb4 100644 --- a/src/lib/pubkey/pubkey.cpp +++ b/src/lib/pubkey/pubkey.cpp @@ -368,11 +368,6 @@ void PK_Verifier::set_input_format(Signature_Format format) { m_sig_format = format; } -bool PK_Verifier::verify_message(const uint8_t msg[], size_t msg_length, const uint8_t sig[], size_t sig_length) { - update(msg, msg_length); - return check_signature(sig, sig_length); -} - void PK_Verifier::update(std::string_view in) { this->update(cast_char_ptr_to_uint8(in.data()), in.size()); } diff --git a/src/lib/pubkey/pubkey.h b/src/lib/pubkey/pubkey.h index d7b551a9784..624840bec8b 100644 --- a/src/lib/pubkey/pubkey.h +++ b/src/lib/pubkey/pubkey.h @@ -314,7 +314,10 @@ class BOTAN_PUBLIC_API(2, 0) PK_Verifier final { * @param sig_length the length of the above byte array sig * @return true if the signature is valid */ - bool verify_message(const uint8_t msg[], size_t msg_length, const uint8_t sig[], size_t sig_length); + bool verify_message(const uint8_t msg[], size_t msg_length, const uint8_t sig[], size_t sig_length) { + update(msg, msg_length); + return check_signature(sig, sig_length); + } /** * Verify a signature. From a3d56a4c26f34808b7e324cc13f2a70593045f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Meusel?= Date: Fri, 16 Aug 2024 11:24:38 +0200 Subject: [PATCH 2/3] Introduce PK_Signer/PK_Verifier ::set_associated_data() This is a new concept introduced by FIPS 204 and 205 (ML-DSA, SLH-DSA) where applications get the chance to provide some context that is incorporated into their signatures. It is conceptually similar to the associated data in an AEAD, therefore it behaves similarly in the Signer/Verifier. Note that algorithms that don't support AD, are supposed to always throw when an application calls set_associated_data() on them. There is also a predicate function is_valid_associated_data_length() for applications to generically check for the support of it. Co-Authored-By: Fabian Albert --- src/lib/pubkey/pk_ops.cpp | 10 +++++++++ src/lib/pubkey/pk_ops.h | 42 +++++++++++++++++++++++++++++++++++++ src/lib/pubkey/pubkey.cpp | 22 ++++++++++++++++++++ src/lib/pubkey/pubkey.h | 44 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+) diff --git a/src/lib/pubkey/pk_ops.cpp b/src/lib/pubkey/pk_ops.cpp index 5a01d61c7e6..f1806b23f57 100644 --- a/src/lib/pubkey/pk_ops.cpp +++ b/src/lib/pubkey/pk_ops.cpp @@ -82,6 +82,16 @@ secure_vector PK_Ops::Key_Agreement_with_KDF::agree(size_t key_len, return z; } +void PK_Ops::Signature::set_associated_data(std::span associated_data) { + BOTAN_UNUSED(associated_data); + throw Not_Implemented("This signature scheme does not support labels for signing"); +} + +void PK_Ops::Verification::set_associated_data(std::span associated_data) { + BOTAN_UNUSED(associated_data); + throw Not_Implemented("This signature scheme does not support labels for verification"); +} + namespace { std::unique_ptr create_signature_hash(std::string_view padding) { diff --git a/src/lib/pubkey/pk_ops.h b/src/lib/pubkey/pk_ops.h index 330268cd465..94608d45786 100644 --- a/src/lib/pubkey/pk_ops.h +++ b/src/lib/pubkey/pk_ops.h @@ -79,6 +79,18 @@ class BOTAN_UNSTABLE_API Decryption { */ class BOTAN_UNSTABLE_API Verification { public: + /** + * Incorporate an application-defined associated data into the signature + * verification. This is useful for domain separation, but is not supported + * by all signature schemes. The valid associated data's length is checked + * beforehand, using is_valid_associated_data_length(). + * + * Note: The associated data is meant to be kept between individual message + * verifications. It is the implementer's responsibility to handle + * this state correctly. + */ + virtual void set_associated_data(std::span label); + /** * Add more data to the message currently being signed * @param input the input to be hashed/verified @@ -96,6 +108,15 @@ class BOTAN_UNSTABLE_API Verification { */ virtual std::string hash_function() const = 0; + /** + * @returns true if the associated data length is valid for this signature scheme. + * Schemes that don't support associated data always return false. + */ + virtual bool is_valid_associated_data_length(size_t length) const { + BOTAN_UNUSED(length); + return false; + } + virtual ~Verification() = default; }; @@ -104,6 +125,18 @@ class BOTAN_UNSTABLE_API Verification { */ class BOTAN_UNSTABLE_API Signature { public: + /** + * Incorporate an application-defined label into the signature. This is + * useful for domain separation, but is not supported by all signature + * schemes. The valid length of the label is checked beforehand, using + * is_valid_associated_data_length(). + * + * Note: The associated data is meant to be kept between individual message + * signings. It is the implementer's responsibility to handle this + * state correctly. + */ + virtual void set_associated_data(std::span associated_data); + /** * Add more data to the message currently being signed * @param input the input to be hashed/signed @@ -133,6 +166,15 @@ class BOTAN_UNSTABLE_API Signature { */ virtual std::string hash_function() const = 0; + /** + * @returns true if the label length is valid for this signature scheme + * Schemes that don't support labels always return false. + */ + virtual bool is_valid_associated_data_length(size_t length) const { + BOTAN_UNUSED(length); + return false; + } + virtual ~Signature() = default; }; diff --git a/src/lib/pubkey/pubkey.cpp b/src/lib/pubkey/pubkey.cpp index c7add4f8cb4..0762bb8766c 100644 --- a/src/lib/pubkey/pubkey.cpp +++ b/src/lib/pubkey/pubkey.cpp @@ -272,6 +272,13 @@ PK_Signer::~PK_Signer() = default; PK_Signer::PK_Signer(PK_Signer&&) noexcept = default; PK_Signer& PK_Signer::operator=(PK_Signer&&) noexcept = default; +void PK_Signer::set_associated_data(std::span associated_data) { + if(!is_valid_associated_data_length(associated_data.size())) { + throw Invalid_Argument("PK_Signer: Associated data does not have a supported length"); + } + m_op->set_associated_data(associated_data); +} + void PK_Signer::update(std::string_view in) { this->update(cast_char_ptr_to_uint8(in.data()), in.size()); } @@ -313,6 +320,10 @@ size_t PK_Signer::signature_length() const { } } +bool PK_Signer::is_valid_associated_data_length(size_t length) const { + return m_op->is_valid_associated_data_length(length); +} + std::vector PK_Signer::signature(RandomNumberGenerator& rng) { std::vector sig = m_op->sign(rng); @@ -368,6 +379,17 @@ void PK_Verifier::set_input_format(Signature_Format format) { m_sig_format = format; } +bool PK_Verifier::is_valid_associated_data_length(size_t length) const { + return m_op->is_valid_associated_data_length(length); +} + +void PK_Verifier::set_associated_data(std::span associated_data) { + if(!is_valid_associated_data_length(associated_data.size())) { + throw Invalid_Argument("PK_Verifier: Associated data does not have a supported length"); + } + m_op->set_associated_data(associated_data); +} + void PK_Verifier::update(std::string_view in) { this->update(cast_char_ptr_to_uint8(in.data()), in.size()); } diff --git a/src/lib/pubkey/pubkey.h b/src/lib/pubkey/pubkey.h index 624840bec8b..7f02af94bb8 100644 --- a/src/lib/pubkey/pubkey.h +++ b/src/lib/pubkey/pubkey.h @@ -198,6 +198,23 @@ class BOTAN_PUBLIC_API(2, 0) PK_Signer final { return sign_message(in.data(), in.size(), rng); } + /** + * Incorporate application-defined associated data into the signature. This + * is useful for (e.g.) domain separation, but is not supported by all + * signature schemes. This must be called at most once and before any calls + * to update(). + * + * Unless reset by another call to set_associated_data(), it is kept between + * signature creations. + * + * @sa is_valid_associated_data_length + * @throws Invalid_Argument if associated data is not supported, or the data's + * length is invalid + * + * @param associated_data an application-defined associated data + */ + void set_associated_data(std::span associated_data); + /** * Add a message part (single byte). * @param in the byte to add @@ -258,6 +275,11 @@ class BOTAN_PUBLIC_API(2, 0) PK_Signer final { */ std::string hash_function() const; + /** + * @returns true if the associated data's length is valid for this signature scheme + */ + bool is_valid_associated_data_length(size_t length) const; + private: std::unique_ptr m_op; Signature_Format m_sig_format; @@ -329,6 +351,23 @@ class BOTAN_PUBLIC_API(2, 0) PK_Verifier final { return verify_message(msg.data(), msg.size(), sig.data(), sig.size()); } + /** + * Incorporate application-defined associated data into the signature. This + * is useful for (e.g.) domain separation, but is not supported by all + * signature schemes. This must be called at most once and before any calls + * to update(). + * + * Unless reset by another call to set_associated_data(), it is kept between + * signature verifications. + * + * @sa is_valid_associated_data_length + * @throws Invalid_Argument if associated data is not supported, or the data's + * length is invalid + * + * @param associated_data an application-defined associated data + */ + void set_associated_data(std::span associated_data); + /** * Add a message part (single byte) of the message corresponding to the * signature to be verified. @@ -388,6 +427,11 @@ class BOTAN_PUBLIC_API(2, 0) PK_Verifier final { */ std::string hash_function() const; + /** + * @returns true if the associated data's length is valid for this signature scheme + */ + bool is_valid_associated_data_length(size_t length) const; + private: std::unique_ptr m_op; Signature_Format m_sig_format; From 15d44b739e0ba3e069ed776485d499bf9e0e030f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Meusel?= Date: Fri, 16 Aug 2024 15:39:20 +0200 Subject: [PATCH 3/3] Add basic asymmetric roundtrip tests This piggy-backs on the PK_Key_Generation_Test to perform basic roundtrip tests for Sign/Verify, Encrypt/Decrypt, Encaps/Decaps, and Key Agreement, depending on the capabilities of the keys. This is explicitly not meant to be exhaustive tests but rather be a centralized sanity-check for the PK_*** operators and their meta-data methods. Co-Authored-By: Fabian Albert --- src/tests/test_pubkey.cpp | 184 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/src/tests/test_pubkey.cpp b/src/tests/test_pubkey.cpp index cbe93447793..7dd0d1f9bed 100644 --- a/src/tests/test_pubkey.cpp +++ b/src/tests/test_pubkey.cpp @@ -570,11 +570,167 @@ void test_pbe_roundtrip(Test::Result& result, } #endif +std::vector> get_suitable_signing_parameters(std::string_view algo) { + if(algo.starts_with("Dilithium") || algo.starts_with("ML-DSA") || algo == "SPHINCS+") { + return {{"", ""}, {"Deterministic", ""}, {"Randomized", ""}}; + } else if(algo == "RSA") { + return {{"PSS(SHA-256)", "PSS(SHA-256)"}, {"PKCS1v15(SHA-256)", "PKCS1v15(SHA-256)"}}; + } else if(algo == "ECDSA" || algo == "ECGDSA" || algo == "ECKCDSA") { + return {{"SHA-256", "SHA-256"}}; + } else if(algo == "DSA") { + return {{"SHA-256", "SHA-256"}}; + } else if(algo == "Ed25519") { + return {{"Pure", "Pure"}, {"Ed25519ph", "Ed25519ph"}}; + } else if(algo == "Ed448") { + return {{"", ""}, {"Ed448ph", "Ed448ph"}}; + } else if(algo == "SM2") { + return {{"ALICE123@YAHOO.COM,SM3", "ALICE123@YAHOO.COM,SM3"}}; + } else if(algo == "XMSS" || algo == "HSS-LMS") { + return {{"", ""}}; + } else if(algo.starts_with("GOST-34.10")) { + return {{"SHA-256", "SHA-256"}}; + } + + throw Test_Error(Botan::fmt("No default signing parameters for {}", algo)); +} + +std::vector get_suitable_encryption_parameters(std::string_view algo) { + if(algo == "RSA" || algo == "ElGamal") { + return {"EME-PKCS1-v1_5", "OAEP(SHA-256,MGF1(SHA-256),securelabel)"}; + } else if(algo == "SM2") { + return {"", "SHA-256"}; + } + + throw Test_Error(Botan::fmt("No default encryption parameters for {}", algo)); +} + +std::vector get_suitable_encapsulation_parameters(std::string_view algo) { + if(algo == "Kyber" || algo == "RSA" || algo == "McEliece" || algo == "FrodoKEM") { + return {"Raw"}; + } + + throw Test_Error(Botan::fmt("No default encapsulation parameters for {}", algo)); +} + +void test_signature_roundtrip(Test::Result& result, const Botan::Private_Key& key, Botan::RandomNumberGenerator& rng) { + for(const auto& [sig_param, verify_param] : get_suitable_signing_parameters(key.algo_name())) { + Botan::PK_Signer signer(key, rng, sig_param); + Botan::PK_Verifier verifier(key, verify_param); + + auto test_sig_roundtrip = [&](std::string_view test_name) { + const auto message_1 = Botan::hex_decode("deadbeef"); + const auto message_2 = Botan::hex_decode("badeaffe"); + + const auto sig_1 = signer.sign_message(message_1, rng); + const auto sig_2 = signer.sign_message(message_2, rng); + result.confirm(Botan::fmt("expected signature length ({})", test_name), + sig_1.size() <= signer.signature_length()); + + // The messages are verified in reverse order to ensure the persistence + // of the associated data. If the associated data were to reset after + // each operation, this would provoke a failure. + result.test_eq( + Botan::fmt("signature roundtrip 2 ({})", test_name), verifier.verify_message(message_2, sig_2), true); + result.test_eq( + Botan::fmt("signature roundtrip 1 ({})", test_name), verifier.verify_message(message_1, sig_1), true); + }; + + test_sig_roundtrip("without associated data"); + + const auto ad = Botan::hex_decode("baadcafefeedface"); + const auto signer_can_ad = signer.is_valid_associated_data_length(ad.size()); + const auto verifier_can_ad = verifier.is_valid_associated_data_length(ad.size()); + result.confirm("associated data support is consistent", signer_can_ad == verifier_can_ad); + if(signer_can_ad && verifier_can_ad) { + signer.set_associated_data(ad); + verifier.set_associated_data(ad); + test_sig_roundtrip("with associated data"); + } else { + result.test_throws( + "if associated data is not supported, set_associated_data throws in signer", + [&] { signer.set_associated_data(ad); }); + + result.test_throws( + "if associated data is not supported, set_associated_data throws in verifier", + [&] { verifier.set_associated_data(ad); }); + } + } +} + +void test_encryption_roundtrip(Test::Result& result, const Botan::Private_Key& key, Botan::RandomNumberGenerator& rng) { + for(const auto& param : get_suitable_encryption_parameters(key.algo_name())) { + Botan::PK_Encryptor_EME enc(key, rng, param); + const auto message = Botan::hex_decode("deadbeef"); + result.test_gte("ciphertext has reasonable length", enc.ciphertext_length(message.size()), 116); + result.test_lte("maximum input size is reasonable", enc.maximum_input_size(), 512); + const auto ct = enc.encrypt(message, rng); + result.test_lte("ciphertext stays within bounds", ct.size(), enc.ciphertext_length(message.size())); + + Botan::PK_Decryptor_EME dec(key, rng, param); + result.test_gte("plaintext has a reasonable length", dec.plaintext_length(ct.size()), 10); + const auto peer_message = dec.decrypt(ct); + result.test_eq("encryption roundtrip", peer_message, message); + result.test_lte("plaintext stays within bounds", peer_message.size(), dec.plaintext_length(ct.size())); + } +} + +void test_key_agreement_roundtrip(Test::Result& result, + const Botan::Private_Key& key, + Botan::RandomNumberGenerator& rng) { + auto my_pubkey = key.public_key(); + + // Note that KEX keys are _requiredd_ to support generate_another() and + // that raw_public_key_bits() must return the canonical public value. + // This is tested/ensured before this function is called. + + auto peer_key = key.generate_another(rng); + auto peer_pubkey = peer_key->public_key(); + + // This is "us" + Botan::PK_Key_Agreement ka(key, rng, "Raw"); + const size_t shared_key_length = ka.agreed_value_size(); + result.test_gte("agreed value size", shared_key_length, 32); + const auto shared_key = ka.derive_key(0 /* no KDF */, peer_pubkey->raw_public_key_bits()); + result.test_is_eq("shared key length", shared_key.size(), shared_key_length); + + // This is "peer" + Botan::PK_Key_Agreement ka_peer(*peer_key, rng, "Raw"); + result.test_eq("peer agreed value size", ka_peer.agreed_value_size(), shared_key_length); + const auto shared_key_peer = ka_peer.derive_key(0 /* no KDF */, my_pubkey->raw_public_key_bits()); + result.test_eq("peer shared key length", shared_key_peer.size(), shared_key_length); + + result.test_eq("shared key matches", shared_key, shared_key_peer); +} + +void test_key_encapsulation_roundtrip(Test::Result& result, + const Botan::Private_Key& key, + Botan::RandomNumberGenerator& rng) { + for(const auto& param : get_suitable_encapsulation_parameters(key.algo_name())) { + auto my_pubkey = key.public_key(); + + Botan::PK_KEM_Encryptor enc(*my_pubkey, param); + const size_t enc_len = enc.encapsulated_key_length(); + const size_t shared_len = enc.shared_key_length(0 /* no KDF */); + result.test_gte("encapsed key has a reasonable length", enc_len, 32); + result.test_gte("shared key has a reasonable length", shared_len, 16); + const auto [ct, shared_secret] = Botan::KEM_Encapsulation::destructure(enc.encrypt(rng)); + result.test_eq("shared secret length matches", ct.size(), enc_len); + result.test_eq("shared secret length matches", shared_secret.size(), shared_len); + + Botan::PK_KEM_Decryptor dec(key, rng, param); + result.test_gte("peer encapsed key has a reasonable length", dec.encapsulated_key_length(), enc_len); + result.test_gte("peer shared key has a reasonable length", dec.shared_key_length(0 /* no KDF */), shared_len); + const auto shared_secret_peer = dec.decrypt(ct); + result.test_eq("shared secret matches", shared_secret, shared_secret_peer); + } +} + } // namespace std::vector PK_Key_Generation_Test::run() { std::vector results; + bool roundtrips_ran = false; for(const auto& param : keygen_params()) { const std::string report_name = algo_name() + (param.empty() ? param : " " + param); @@ -735,6 +891,34 @@ std::vector PK_Key_Generation_Test::run() { test_pbe_roundtrip(result, key, "PBES2(AES-128/CBC,Scrypt)", this->rng()); #endif + + // Below are a few smoke tests trying out trivial roundtrips and sanity + // checking for the various public key operations. Those are not meant + // to be exhaustive for all algorithms, but rather to catch some common + // mistakes in the implementation of the public key interface. + // + // Given the amount of algorithm parameter sets, we only run those tests + // for a single instance of each algorithm, if --run-long-tests is not set. + + if(Test::run_long_tests() || !roundtrips_ran) { + if(key.supports_operation(Botan::PublicKeyOperation::Signature)) { + test_signature_roundtrip(result, key, this->rng()); + } + + if(key.supports_operation(Botan::PublicKeyOperation::Encryption)) { + test_encryption_roundtrip(result, key, this->rng()); + } + + if(key.supports_operation(Botan::PublicKeyOperation::KeyAgreement)) { + test_key_agreement_roundtrip(result, key, this->rng()); + } + + if(key.supports_operation(Botan::PublicKeyOperation::KeyEncapsulation)) { + test_key_encapsulation_roundtrip(result, key, this->rng()); + } + + roundtrips_ran = true; + } } result.end_timer();