Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion doc/api_ref/pubkey.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,17 @@ Botan implements the following signature algorithms:
- ``<user ID>,<HashFunction>``

#. ML-DSA (Dilithium).
Takes the optional parameter ``Deterministic`` (default) or ``Randomized``.
Takes and optional parameter string formed by comma-separated list of the following format ``[(Deterministic|Randomized),][Pure,][ctx_hex=<hex-value>]``, where the ordering of the comma-separated elements is arbitrary. The specification of ``Pure`` is redundant since currently only the pure variant of ML-DSA is implemented. The defaults of the other values are ``Randomized`` and an empty context parameter (`ctx_hex`). Non-empty context parameters are only supported by ML-DSA, but not by Dilithium. ``Deterministic`` or ``Randomized`` may also be specified in a verification operation thought it has no effect there.

#. ML-DSA-composite (draft-ietf-lamps-pq-composite-sigs-15).
Takes no parameters. The following algorithms are defined and are accessed by the respective
string value: MLDSA44-RSA2048-PKCS15-SHA256, MLDSA65-RSA3072-PKCS15-SHA512,
MLDSA65-RSA4096-PKCS15-SHA512, MLDSA44-RSA2048-PSS-SHA256, MLDSA65-RSA3072-PSS-SHA512,
MLDSA65-RSA4096-PSS-SHA512, MLDSA87-RSA3072-PSS-SHA512, MLDSA87-RSA4096-PSS-SHA512,
MLDSA44-ECDSA-P256-SHA256, MLDSA65-ECDSA-P256-SHA512, MLDSA65-ECDSA-P384-SHA512,
MLDSA65-ECDSA-brainpoolP256r1-SHA512, MLDSA87-ECDSA-P384-SHA512,
MLDSA87-ECDSA-brainpoolP384r1-SHA512, MLDSA87-ECDSA-P521-SHA512, MLDSA44-Ed25519-SHA512,
MLDSA65-Ed25519-SHA512, MLDSA87-Ed448-SHAKE256
#. SLH-DSA.
Takes the optional parameter ``Deterministic`` (default) or ``Randomized``.
#. XMSS. Takes no parameter.
Expand Down
107 changes: 95 additions & 12 deletions src/lib/pubkey/dilithium/dilithium_common/dilithium.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,28 @@
* (C) 2021-2022 Manuel Glaser - Rohde & Schwarz Cybersecurity
* (C) 2021-2023 Michael Boric, René Meusel - Rohde & Schwarz Cybersecurity
* (C) 2024 René Meusel - Rohde & Schwarz Cybersecurity
* (C) 2026 Falko Strenzke – MTG AG
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include <botan/dilithium.h>

#include <botan/exceptn.h>
#include <botan/hex.h>
#include <botan/rng.h>

#include <botan/internal/dilithium_algos.h>
#include <botan/internal/dilithium_keys.h>
#include <botan/internal/dilithium_symmetric_primitives.h>
#include <botan/internal/dilithium_types.h>
#include <botan/internal/fmt.h>
#include <botan/internal/keypair.h>
#include <botan/internal/parsing.h>
#include <botan/internal/pk_ops_impl.h>
#include <botan/internal/stl_util.h>

#include <string_view>

namespace Botan {
namespace {

Expand Down Expand Up @@ -61,6 +65,84 @@ DilithiumMode::Mode dilithium_mode_from_string(std::string_view str) {
throw Invalid_Argument(fmt("'{}' is not a valid Dilithium mode name", str));
}

class MLDSA_Signing_Parameters {
public:
template <typename T>
class Once_Settable {
public:
explicit Once_Settable(const T& default_value) : m_is_set(false), m_value(default_value) {}

void set_value(const T& value) {
if(m_is_set) {
throw Invalid_Argument(
"signature/verification parameter contains value for same key (at least) twice");
}
m_is_set = true;
m_value = value;
}

const T& value() const { return m_value; }

private:
bool m_is_set;
T m_value;
};

explicit MLDSA_Signing_Parameters(std::string_view param_str) : m_rndmzd(true), m_ctx({}) {
const char* error_tmpl = "Parameter string '{}' is not a valid ML-DSA or Dilithium parameter specification";
if(param_str.empty()) {
// return the defaults
return;
}
const auto v = split_on(param_str, ',');
for(const auto& y : v) {
if(y == "Pure") {
continue;
}
if(y == "Deterministic") {
m_rndmzd.set_value(false);
continue;
}
if(y == "Randomized") {
m_rndmzd.set_value(true);
continue;
}
const char* ctx_key = "ctx_hex";
const size_t prefix_len = std::char_traits<char>::length(ctx_key);
if(y.starts_with(ctx_key)) {
if((y.size() > prefix_len && y[prefix_len] != '=')) {
throw Invalid_Argument(fmt(error_tmpl), param_str);
}
std::string z;
// check whether a non-empty context parameter string was specified
if(y.size() > prefix_len + 1) {
// no whitespace allowed
if(y.find(' ') != std::string::npos) {
throw Invalid_Argument(fmt(error_tmpl), param_str);
}
// get the value of the hex-encoded context string
z = y.substr(prefix_len + 1);
}
m_ctx.set_value(hex_decode(z));
continue;
}
throw Invalid_Argument(Botan::fmt("invalid token parameter string for ML-DSA/Dilithium: ", y));
}
}

bool is_pure_signing() const { return m_pure_sign; }

bool is_randomized_signing() const { return m_rndmzd.value(); }

const std::vector<uint8_t>& user_context() const { return m_ctx.value(); }

private:
Once_Settable<bool> m_rndmzd;
Once_Settable<std::vector<uint8_t>> m_ctx;

const bool m_pure_sign = true;
};

} // namespace

DilithiumMode::DilithiumMode(const OID& oid) : m_mode(dilithium_mode_from_string(oid.to_formatted_string())) {}
Expand Down Expand Up @@ -129,10 +211,12 @@ bool DilithiumMode::is_available() const {

class Dilithium_Signature_Operation final : public PK_Ops::Signature {
public:
Dilithium_Signature_Operation(DilithiumInternalKeypair keypair, bool randomized) :
Dilithium_Signature_Operation(DilithiumInternalKeypair keypair,
bool randomized,
std::span<const uint8_t> user_context = {}) :
m_keypair(std::move(keypair)),
m_randomized(randomized),
m_h(m_keypair.second->mode().symmetric_primitives().get_message_hash(m_keypair.first->tr())),
m_h(m_keypair.second->mode().symmetric_primitives().get_message_hash(m_keypair.first->tr(), user_context)),
m_s1(ntt(m_keypair.second->s1().clone())),
m_s2(ntt(m_keypair.second->s2().clone())),
m_t0(ntt(m_keypair.second->t0().clone())),
Expand Down Expand Up @@ -249,11 +333,12 @@ class Dilithium_Signature_Operation final : public PK_Ops::Signature {

class Dilithium_Verification_Operation final : public PK_Ops::Verification {
public:
explicit Dilithium_Verification_Operation(std::shared_ptr<Dilithium_PublicKeyInternal> pubkey) :
explicit Dilithium_Verification_Operation(std::shared_ptr<Dilithium_PublicKeyInternal> pubkey,
std::span<const uint8_t> user_context = {}) :
m_pub_key(std::move(pubkey)),
m_A(Dilithium_Algos::expand_A(m_pub_key->rho(), m_pub_key->mode())),
m_t1_ntt_shifted(ntt(m_pub_key->t1() << DilithiumConstants::D)),
m_h(m_pub_key->mode().symmetric_primitives().get_message_hash(m_pub_key->tr())) {}
m_h(m_pub_key->mode().symmetric_primitives().get_message_hash(m_pub_key->tr(), user_context)) {}

void update(std::span<const uint8_t> input) override { m_h->update(input); }

Expand Down Expand Up @@ -382,9 +467,9 @@ std::unique_ptr<Private_Key> Dilithium_PublicKey::generate_another(RandomNumberG

std::unique_ptr<PK_Ops::Verification> Dilithium_PublicKey::create_verification_op(std::string_view params,
std::string_view provider) const {
BOTAN_ARG_CHECK(params.empty() || params == "Pure", "Unexpected parameters for verifying with Dilithium");
const MLDSA_Signing_Parameters sig_par(params);
if(provider.empty() || provider == "base") {
return std::make_unique<Dilithium_Verification_Operation>(m_public);
return std::make_unique<Dilithium_Verification_Operation>(m_public, sig_par.user_context());
}
throw Provider_Not_Found(algo_name(), provider);
}
Expand Down Expand Up @@ -438,17 +523,15 @@ std::unique_ptr<PK_Ops::Signature> Dilithium_PrivateKey::create_signature_op(Ran
std::string_view params,
std::string_view provider) const {
BOTAN_UNUSED(rng);

BOTAN_ARG_CHECK(params.empty() || params == "Deterministic" || params == "Randomized",
"Unexpected parameters for signing with ML-DSA/Dilithium");
const MLDSA_Signing_Parameters sig_par(params);

// FIPS 204, Section 3.4
// By default, this standard specifies the signing algorithm to use both
// types of randomness [fresh from the RNG and a value in the private key].
// This is referred to as the “hedged” variant of the signing procedure.
const bool randomized = (params.empty() || params == "Randomized");
if(provider.empty() || provider == "base") {
return std::make_unique<Dilithium_Signature_Operation>(DilithiumInternalKeypair{m_public, m_private}, randomized);
return std::make_unique<Dilithium_Signature_Operation>(
DilithiumInternalKeypair{m_public, m_private}, sig_par.is_randomized_signing(), sig_par.user_context());
}
throw Provider_Not_Found(algo_name(), provider);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@

namespace Botan {

DilithiumMessageHash::DilithiumMessageHash(DilithiumHashedPublicKey tr) :
m_tr(std::move(tr)), m_shake(XOF::create_or_throw("SHAKE-256")) {}
DilithiumMessageHash::DilithiumMessageHash(DilithiumHashedPublicKey tr, std::span<const uint8_t> user_context) :
m_tr(std::move(tr)),
m_user_context(user_context.begin(), user_context.end()),
m_shake(XOF::create_or_throw("SHAKE-256")) {}

DilithiumMessageHash::~DilithiumMessageHash() = default;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class RandomNumberGenerator;
*/
class DilithiumMessageHash /* NOLINT(*-special-member-functions) */ {
public:
explicit DilithiumMessageHash(DilithiumHashedPublicKey tr);
explicit DilithiumMessageHash(DilithiumHashedPublicKey tr, std::span<const uint8_t> user_context = {});

virtual ~DilithiumMessageHash();

Expand Down Expand Up @@ -65,12 +65,13 @@ class DilithiumMessageHash /* NOLINT(*-special-member-functions) */ {
void ensure_started() {
if(!m_was_started) {
// FIPS 204, page 17, footnote 4: By default, the context is the empty string [...]
start({});
start(m_user_context);
}
}

private:
DilithiumHashedPublicKey m_tr;
std::vector<uint8_t> m_user_context;
bool m_was_started = false;
std::unique_ptr<XOF> m_shake;
};
Expand Down Expand Up @@ -106,8 +107,9 @@ class Dilithium_Symmetric_Primitives_Base {
Dilithium_Symmetric_Primitives_Base(Dilithium_Symmetric_Primitives_Base&&) = delete;
Dilithium_Symmetric_Primitives_Base& operator=(Dilithium_Symmetric_Primitives_Base&&) = delete;

virtual std::unique_ptr<DilithiumMessageHash> get_message_hash(DilithiumHashedPublicKey tr) const {
return std::make_unique<DilithiumMessageHash>(std::move(tr));
virtual std::unique_ptr<DilithiumMessageHash> get_message_hash(DilithiumHashedPublicKey tr,
std::span<const uint8_t> user_context) const {
return std::make_unique<DilithiumMessageHash>(std::move(tr), user_context);
}

/// Computes the private random seed rho prime used for signing
Expand Down
5 changes: 3 additions & 2 deletions src/lib/pubkey/dilithium/ml_dsa/ml_dsa_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ class ML_DSA_Symmetric_Primitives final : public Dilithium_Symmetric_Primitives_
return seed;
}

std::unique_ptr<DilithiumMessageHash> get_message_hash(DilithiumHashedPublicKey tr) const override {
return std::make_unique<ML_DSA_MessageHash>(std::move(tr));
std::unique_ptr<DilithiumMessageHash> get_message_hash(DilithiumHashedPublicKey tr,
std::span<const uint8_t> user_context) const override {
return std::make_unique<ML_DSA_MessageHash>(std::move(tr), user_context);
}

std::optional<std::array<uint8_t, 2>> seed_expansion_domain_separator() const override {
Expand Down
62 changes: 62 additions & 0 deletions src/tests/test_dilithium.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <botan/hash.h>
#include <botan/pk_algs.h>
#include <botan/pubkey.h>
#include <botan/internal/fmt.h>

#include "test_pubkey.h"
#include "test_rng.h"
Expand Down Expand Up @@ -288,4 +289,65 @@ BOTAN_REGISTER_TEST("pubkey", "dilithium_keygen", Dilithium_Keygen_Tests);

} // namespace

#if defined(BOTAN_HAS_DILITHIUM_COMMON)

class MLDSA_Param_Tests final : public Test {
public:
struct Test_Case {
bool expect_success;
std::string param;
bool randomized_signature;
};

std::vector<Test::Result> run() override {
const std::string test_name = "MLDSA_Param_Tests";
std::vector<Test::Result> results;
auto rng = Test::new_rng(test_name);

const std::string msg = "The quick brown fox jumps over the lazy dog.";
const std::vector<uint8_t> msgvec(msg.data(), msg.data() + msg.size());

const Botan::Dilithium_PrivateKey priv_key(*rng, Botan::DilithiumMode::ML_DSA_4x4);

const std::vector<Test_Case> sign_params = {
Test_Case{true, std::string(""), true},
Test_Case{true, std::string("Randomized,Pure"), true},
Test_Case{true, std::string("Deterministic,ctx_hex=00AABB"), false},
Test_Case{false, std::string("ctx_hex=,ctx_hex="), true},
Test_Case{false, std::string("Randomized,Deterministic"), false},
Test_Case{false, std::string("ctx=AA"), true},
Test_Case{false, std::string("X"), true},
Test_Case{false, std::string("ctx=A"), true}};

for(const auto& [expect_success, param_str, random_sig] : sign_params) {
Test::Result result(Botan::fmt("{}: param string = '{}'", test_name, param_str));
std::unique_ptr<Botan::PK_Signer> signer;
bool exc = false;
try {
signer = std::make_unique<Botan::PK_Signer>(Botan::PK_Signer(priv_key, *rng, param_str));
} catch(Botan::Exception&) {
exc = true;
}
result.test_bool_eq(
Botan::fmt("parameters string '{}' for ML-DSA op was valid", param_str), !exc && signer, expect_success);
if(exc) {
results.push_back(result);
continue;
}
auto verifier = Botan::PK_Verifier(*priv_key.public_key(), param_str);
auto signature = signer->sign_message(msgvec, *rng);
auto signature2 = signer->sign_message(msgvec, *rng);
result.test_bool_eq(
Botan::fmt("signature randomization (param = '{}')", param_str), signature != signature2, random_sig);
result.test_is_true("signature verification", verifier.verify_message(msgvec, signature));
results.push_back(result);
}

return results;
}
};

BOTAN_REGISTER_TEST("pubkey", "mldsa_op_params", MLDSA_Param_Tests);
#endif

} // namespace Botan_Tests
Loading