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
11 changes: 11 additions & 0 deletions news.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ Version 3.14.0, Not Yet Released
passed to the ``Credentials_Manager`` and other callbacks is now the
canonical (lowercased) form.

* XMSS keys and certificates now use the object identifier and public key
encoding specified in RFC 9802: the algorithm identifier is
``1.3.6.1.5.5.7.6.34`` and the raw public key is placed in the
SubjectPublicKeyInfo without an OCTET STRING wrapper. Keys and certificates
using the previous OID (``0.4.0.127.0.15.1.1.13.0``) and encoding from
draft-vangeest-x509-hash-sigs are still accepted for loading and signature
verification. The encoding of the key bits is now selected by the OID; keys
that combine the RFC 9802 OID with the OCTET STRING wrapper, or the previous
OID with an unwrapped key, are rejected. The RFC 9802 OID for XMSS^MT is
registered as well.

* Fix a bug introduced in 3.13.0 where, in builds without the system RNG,
``RandomNumberGenerator::randomize_with_ts_input`` passed only the low 32 bits
of the timestamp, and never the process id, as additional input. (GH #5924)
Expand Down
11 changes: 8 additions & 3 deletions src/build-data/oids.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
1.3.6.1.4.1.25258.1.10.3 = Dilithium-8x7-AES-r3

# HSS-LMS
# draft-gazdag-x509-hash-sigs-01
# id-alg-hss-lms-hashsig from RFC 8708, also used for X.509 by RFC 9802
1.2.840.113549.1.9.16.3.17 = HSS-LMS

# HSS-LMS private key (since the format of an HSS-LMS private key is not specified,
Expand Down Expand Up @@ -125,8 +125,13 @@
# XMSS
1.3.6.1.4.1.25258.1.5 = XMSS-draft6
1.3.6.1.4.1.25258.1.8 = XMSS-draft12
# draft-vangeest-x509-hash-sigs-03
0.4.0.127.0.15.1.1.13.0 = XMSS
# id-alg-xmss-hashsig and id-alg-xmssmt-hashsig from RFC 9802
1.3.6.1.5.5.7.6.34 = XMSS
1.3.6.1.5.5.7.6.35 = XMSSMT
# Legacy OIDs from draft-vangeest-x509-hash-sigs-03; accepted when loading
# keys and verifying signatures, but no longer emitted
0.4.0.127.0.15.1.1.13.0 = XMSS-draft-vangeest
0.4.0.127.0.15.1.1.14.0 = XMSSMT-draft-vangeest

# X9.62 ecPublicKey, valid for ECDSA and ECDH (RFC 3279 sec 2.3.5)
1.2.840.10045.2.1 = ECDSA
Expand Down
18 changes: 15 additions & 3 deletions src/lib/asn1/static_oids.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* This file was automatically generated by ./src/scripts/dev_tools/gen_oids.py on 2026-07-17
* This file was automatically generated by src/scripts/dev_tools/gen_oids.py on 2026-09-14
* All manual changes will be lost. Edit the script instead.
*
* Botan is released under the Simplified BSD License (see license.txt)
Expand Down Expand Up @@ -208,7 +208,9 @@ std::optional<std::string_view> OID_Map::lookup_static_oid(const OID& oid) {
case 0x3F20F:
return if_match(oid, {1, 3, 36, 3, 2, 1}, "RIPEMD-160");
case 0x4266E:
return if_match(oid, {0, 4, 0, 127, 0, 15, 1, 1, 13, 0}, "XMSS");
return if_match(oid, {0, 4, 0, 127, 0, 15, 1, 1, 13, 0}, "XMSS-draft-vangeest");
case 0x4272F:
return if_match(oid, {0, 4, 0, 127, 0, 15, 1, 1, 14, 0}, "XMSSMT-draft-vangeest");
case 0x478C4:
return if_match(oid, {1, 2, 410, 200004, 1, 4}, "SEED/CBC");
case 0x47D98:
Expand Down Expand Up @@ -585,6 +587,10 @@ std::optional<std::string_view> OID_Map::lookup_static_oid(const OID& oid) {
return if_match(oid, {1, 3, 6, 1, 5, 5, 7, 3, 8}, "PKIX.TimeStamping");
case 0x94929:
return if_match(oid, {1, 3, 6, 1, 5, 5, 7, 3, 9}, "PKIX.OCSPSigning");
case 0x94B85:
return if_match(oid, {1, 3, 6, 1, 5, 5, 7, 6, 34}, "XMSS");
case 0x94B86:
return if_match(oid, {1, 3, 6, 1, 5, 5, 7, 6, 35}, "XMSSMT");
case 0x94CEA:
return if_match(oid, {1, 3, 6, 1, 5, 5, 7, 8, 5}, "PKIX.XMPPAddr");
case 0x94CEE:
Expand Down Expand Up @@ -979,12 +985,16 @@ std::optional<OID> OID_Map::lookup_static_oid_name(std::string_view req) {
return if_match(req, "frp256v1", {1, 2, 250, 1, 223, 101, 256, 1});
case 0x4A9EE:
return if_match(req, "ClassicMcEliece_6960119f", {1, 3, 6, 1, 4, 1, 22554, 5, 1, 8});
case 0x4AF62:
return if_match(req, "XMSS-draft-vangeest", {0, 4, 0, 127, 0, 15, 1, 1, 13, 0});
case 0x4BF87:
return if_match(req, "PKIX.TNAuthList", {1, 3, 6, 1, 5, 5, 7, 1, 26});
case 0x4C088:
return if_match(req, "eFrodoKEM-976-AES", {1, 3, 6, 1, 4, 1, 25258, 1, 17, 2});
case 0x4C513:
return if_match(req, "DSA/SHA-224", {2, 16, 840, 1, 101, 3, 4, 3, 1});
case 0x4C6C6:
return if_match(req, "XMSSMT", {1, 3, 6, 1, 5, 5, 7, 6, 35});
case 0x4C806:
return if_match(req, "DSA/SHA-256", {2, 16, 840, 1, 101, 3, 4, 3, 2});
case 0x4D740:
Expand Down Expand Up @@ -1286,7 +1296,7 @@ std::optional<OID> OID_Map::lookup_static_oid_name(std::string_view req) {
case 0x980F5:
return if_match(req, "GOST.SubjectSigningTool", {1, 2, 643, 100, 111});
case 0x98B03:
return if_match(req, "XMSS", {0, 4, 0, 127, 0, 15, 1, 1, 13, 0});
return if_match(req, "XMSS", {1, 3, 6, 1, 5, 5, 7, 6, 34});
case 0x9A6B2:
return if_match(req, "ECKCDSA/SHA-1", {1, 2, 410, 200004, 1, 100, 4, 3});
case 0x9B1CF:
Expand All @@ -1311,6 +1321,8 @@ std::optional<OID> OID_Map::lookup_static_oid_name(std::string_view req) {
return if_match(req, "RIPEMD-160", {1, 3, 36, 3, 2, 1});
case 0x9D503:
return if_match(req, "RSA/PKCS1v15(SHA-256)", {1, 2, 840, 113549, 1, 1, 11});
case 0x9E078:
return if_match(req, "XMSSMT-draft-vangeest", {0, 4, 0, 127, 0, 15, 1, 1, 14, 0});
case 0x9EC88:
return if_match(req, "DSA/SHA-3(512)", {2, 16, 840, 1, 101, 3, 4, 3, 8});
case 0x9EF36:
Expand Down
4 changes: 2 additions & 2 deletions src/lib/pubkey/pk_algs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ std::unique_ptr<Public_Key> load_public_key(const AlgorithmIdentifier& alg_id,
#endif

#if defined(BOTAN_HAS_XMSS_RFC8391)
if(alg_name == "XMSS") {
if(alg_name == "XMSS" || alg_name == "XMSS-draft-vangeest") {
return std::make_unique<XMSS_PublicKey>(alg_id, key_bits);
}
#endif
Expand Down Expand Up @@ -417,7 +417,7 @@ std::unique_ptr<Private_Key> load_private_key(const AlgorithmIdentifier& alg_id,
#endif

#if defined(BOTAN_HAS_XMSS_RFC8391)
if(alg_name == "XMSS") {
if(alg_name == "XMSS" || alg_name == "XMSS-draft-vangeest") {
return std::make_unique<XMSS_PrivateKey>(alg_id, key_bits);
}
#endif
Expand Down
24 changes: 18 additions & 6 deletions src/lib/pubkey/xmss/xmss.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,21 @@ class BOTAN_PUBLIC_API(2, 0) XMSS_PublicKey : public virtual Public_Key {
/**
* Loads a public key from an X.509 SubjectPublicKeyInfo.
*
* Public key must be encoded as in draft-vangeest-x509-hash-sigs-03.
* The OID of @p alg_id selects the encoding of @p key_bits: the raw
* public key for the RFC 9802 OID, or the raw public key wrapped in an
* OCTET STRING for the OID of draft-vangeest-x509-hash-sigs-03, which
* Botan versions before 3.14 emitted. An AlgorithmIdentifier without an
* OID denotes the raw public key.
*
* @param alg_id the X.509 AlgorithmIdentifier
* @param key_bits DER encoded public key bits
* @param key_bits the public key bits
*/
XMSS_PublicKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> key_bits);

/**
* Loads a public key.
*
* Public key must be encoded as in draft-vangeest-x509-hash-sigs-03.
*
* @param key_bits DER encoded public key bits
* @param key_bits the raw public key as returned by raw_public_key_bits()
*/
BOTAN_DEPRECATED("Use the constructor taking an AlgorithmIdentifier")
BOTAN_FUTURE_EXPLICIT XMSS_PublicKey(std::span<const uint8_t> key_bits);
Expand Down Expand Up @@ -188,8 +190,13 @@ class BOTAN_PUBLIC_API(2, 0) XMSS_PrivateKey final : public virtual XMSS_PublicK
/**
* Loads a private key from a PKCS #8 PrivateKeyInfo.
*
* With the RFC 9802 OID or the OID of draft-vangeest-x509-hash-sigs-03,
* @p key_bits is the private key serialized using raw_private_key() and
* wrapped in an OCTET STRING. An AlgorithmIdentifier without an OID
* denotes the raw private key.
*
* @param alg_id the PKCS #8 AlgorithmIdentifier
* @param key_bits An XMSS private key serialized using raw_private_key().
* @param key_bits the private key bits
**/
XMSS_PrivateKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> key_bits);

Expand Down Expand Up @@ -272,6 +279,11 @@ class BOTAN_PUBLIC_API(2, 0) XMSS_PrivateKey final : public virtual XMSS_PublicK
// derived private key body runs.
struct Keygen_Material;

struct RawKeyTag {};

/// Constructs the key from the already unwrapped raw private key
XMSS_PrivateKey(secure_vector<uint8_t> raw_key, RawKeyTag tag);

XMSS_PrivateKey(XMSS_Parameters::xmss_algorithm_t xmss_algo_id,
WOTS_Derivation_Method wots_derivation_method,
Keygen_Material material);
Expand Down
43 changes: 27 additions & 16 deletions src/lib/pubkey/xmss/xmss_privatekey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,30 @@ namespace Botan {

namespace {

// fall back to raw decoding for previous versions, which did not encode an OCTET STRING
secure_vector<uint8_t> extract_raw_private_key(std::span<const uint8_t> key_bits, const XMSS_Parameters& xmss_params) {
secure_vector<uint8_t> raw_key;

// The public part of the input key bits was already parsed, so we can
// decide depending on the buffer length whether this must be BER decoded.
if(key_bits.size() == xmss_params.raw_private_key_size() ||
key_bits.size() == xmss_params.raw_legacy_private_key_size()) {
raw_key.assign(key_bits.begin(), key_bits.end());
} else {
/*
* The PKCS #8 private key payload is the raw private key wrapped in an OCTET
* STRING, under the RFC 9802 OID as well as under the OID of
* draft-vangeest-x509-hash-sigs used by earlier Botan versions. An empty
* AlgorithmIdentifier denotes the raw key, as accepted by the constructor
* without an AlgorithmIdentifier.
*/
secure_vector<uint8_t> unwrap_private_key_bits(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> key_bits) {
// The XMSS parameter set is carried in the key bits; no AlgorithmIdentifier parameters are defined
if(!alg_id.parameters_are_empty()) {
throw Decoding_Error("Unexpected parameters for XMSS private key");
}

if(alg_id.oid().empty()) {
return secure_vector<uint8_t>(key_bits.begin(), key_bits.end());
}

if(alg_id.oid() == OID::from_string("XMSS") || alg_id.oid() == OID::from_string("XMSS-draft-vangeest")) {
secure_vector<uint8_t> raw_key;
BER_Decoder(key_bits, BER_Decoder::Limits::DER()).decode(raw_key, ASN1_Type::OctetString).verify_end();
return raw_key;
}

return raw_key;
throw Decoding_Error("Unexpected AlgorithmIdentifier for XMSS private key");
}

} // namespace
Expand Down Expand Up @@ -85,7 +95,7 @@ class XMSS_PrivateKey_Internal final {
m_private_seed,
m_prf)) {}

XMSS_PrivateKey_Internal(XMSS_Parameters::xmss_algorithm_t xmss_algo_id, std::span<const uint8_t> key_bits) :
XMSS_PrivateKey_Internal(XMSS_Parameters::xmss_algorithm_t xmss_algo_id, std::span<const uint8_t> raw_key) :
m_xmss_params(XMSS_Parameters::from_id(xmss_algo_id)), m_wots_params(m_xmss_params.wots_parameters()) {
/*
The code requires sizeof(size_t) >= ceil(tree_height / 8)
Expand All @@ -97,8 +107,6 @@ class XMSS_PrivateKey_Internal final {
*/
static_assert(sizeof(size_t) >= 4, "size_t is big enough to support leaf index");

const secure_vector<uint8_t> raw_key = extract_raw_private_key(key_bits, m_xmss_params);

if(raw_key.size() != m_xmss_params.raw_private_key_size() &&
raw_key.size() != m_xmss_params.raw_legacy_private_key_size()) {
throw Decoding_Error("Invalid XMSS private key size");
Expand Down Expand Up @@ -195,8 +203,11 @@ XMSS_PrivateKey::XMSS_PrivateKey(std::span<const uint8_t> key_bits) :
XMSS_PrivateKey(AlgorithmIdentifier(), key_bits) {}

XMSS_PrivateKey::XMSS_PrivateKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> key_bits) :
XMSS_PublicKey(alg_id, key_bits),
m_private(std::make_shared<XMSS_PrivateKey_Internal>(xmss_parameters().oid(), key_bits)) {}
XMSS_PrivateKey(unwrap_private_key_bits(alg_id, key_bits), RawKeyTag{}) {}

XMSS_PrivateKey::XMSS_PrivateKey(secure_vector<uint8_t> raw_key, RawKeyTag /*tag*/) :
XMSS_PublicKey(AlgorithmIdentifier(), raw_key),
m_private(std::make_shared<XMSS_PrivateKey_Internal>(xmss_parameters().oid(), raw_key)) {}

struct XMSS_PrivateKey::Keygen_Material {
secure_vector<uint8_t> private_seed;
Expand Down
79 changes: 46 additions & 33 deletions src/lib/pubkey/xmss/xmss_publickey.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/*
* XMSS Public Key
* An XMSS: Extended Hash-Based Signature public key.
* The XMSS public key does not support the X509 standard. Instead the
* raw format described in [1] is used.
* The raw key format described in [1] is used. When embedded in a
* SubjectPublicKeyInfo, the raw key is placed in the BIT STRING without
* any further ASN.1 wrapping, as specified in [2].
*
* [1] XMSS: Extended Hash-Based Signatures,
* Request for Comments: 8391
* Release: May 2018.
* https://datatracker.ietf.org/doc/rfc8391/
* [2] Use of the HSS and XMSS Hash-Based Signature Algorithms in
* Internet X.509 Public Key Infrastructure,
* Request for Comments: 9802
* https://datatracker.ietf.org/doc/rfc9802/
*
* (C) 2016,2017 Matthias Gierlings
*
Expand All @@ -17,7 +22,6 @@
#include <botan/xmss.h>

#include <botan/ber_dec.h>
#include <botan/der_enc.h>
#include <botan/pk_options.h>
#include <botan/rng.h>
#include <botan/internal/buffer_slicer.h>
Expand All @@ -44,27 +48,40 @@ XMSS_Parameters::xmss_algorithm_t deserialize_xmss_oid(std::span<const uint8_t>
return static_cast<XMSS_Parameters::xmss_algorithm_t>(raw_id);
}

// fall back to raw decoding for previous versions, which did not encode an OCTET STRING
std::vector<uint8_t> extract_raw_public_key(std::span<const uint8_t> key_bits) {
std::vector<uint8_t> raw_key;
try {
BER_Decoder(key_bits, BER_Decoder::Limits::DER()).decode(raw_key, ASN1_Type::OctetString).verify_end();
/*
* RFC 9802 places the raw XMSS public key directly into the SubjectPublicKeyInfo
* BIT STRING. draft-vangeest-x509-hash-sigs, which earlier Botan versions and
* some other implementations followed, used a different OID and wrapped the raw
* key in an OCTET STRING. The OID therefore determines the encoding. An empty
* AlgorithmIdentifier denotes the raw key, as accepted by the constructor
* without an AlgorithmIdentifier.
*/
std::vector<uint8_t> unwrap_public_key_bits(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> key_bits) {
// The XMSS parameter set is carried in the key bits; no AlgorithmIdentifier parameters are defined
if(!alg_id.parameters_are_empty()) {
throw Decoding_Error("Unexpected parameters for XMSS public key");
}

// Smoke check the decoded key. Valid raw keys might be decodable as BER
// and they might be either a sole public key or a concatenation of public
// and private key (with the optional WOTS+ derivation identifier).
const XMSS_Parameters params = XMSS_Parameters::from_id(deserialize_xmss_oid(raw_key));
if(raw_key.size() != params.raw_public_key_size() && raw_key.size() != params.raw_private_key_size() &&
raw_key.size() != params.raw_legacy_private_key_size()) {
throw Decoding_Error("unpacked XMSS key does not have the correct length");
}
} catch(Decoding_Error&) {
raw_key.assign(key_bits.begin(), key_bits.end());
} catch(Not_Implemented&) {
raw_key.assign(key_bits.begin(), key_bits.end());
if(alg_id.oid().empty() || alg_id.oid() == OID::from_string("XMSS")) {
return std::vector<uint8_t>(key_bits.begin(), key_bits.end());
}

if(alg_id.oid() == OID::from_string("XMSS-draft-vangeest")) {
std::vector<uint8_t> raw_key;
BER_Decoder(key_bits, BER_Decoder::Limits::DER()).decode(raw_key, ASN1_Type::OctetString).verify_end();
return raw_key;
}

return raw_key;
throw Decoding_Error("Unexpected AlgorithmIdentifier for XMSS public key");
}

XMSS_Parameters parameters_from_raw_key(std::span<const uint8_t> raw_key) {
const auto xmss_oid = deserialize_xmss_oid(raw_key);
try {
return XMSS_Parameters::from_id(xmss_oid);
} catch(const Not_Implemented&) {
throw Decoding_Error("Unknown XMSS algorithm id in encoded key");
}
}

} // namespace
Expand Down Expand Up @@ -108,14 +125,8 @@ XMSS_PublicKey::XMSS_PublicKey(XMSS_Parameters::xmss_algorithm_t xmss_oid, Rando
XMSS_PublicKey::XMSS_PublicKey(std::span<const uint8_t> key_bits) : XMSS_PublicKey(AlgorithmIdentifier(), key_bits) {}

XMSS_PublicKey::XMSS_PublicKey(const AlgorithmIdentifier& alg_id, std::span<const uint8_t> key_bits) {
// The XMSS parameter set is carried in the key bits; no AlgorithmIdentifier parameters are defined
if(!alg_id.parameters_are_empty()) {
throw Decoding_Error("Unexpected parameters for XMSS public key");
}

const auto raw_key = extract_raw_public_key(key_bits);
const auto xmss_oid = deserialize_xmss_oid(raw_key);
const auto params = XMSS_Parameters::from_id(xmss_oid);
const auto raw_key = unwrap_public_key_bits(alg_id, key_bits);
const auto params = parameters_from_raw_key(raw_key);
if(raw_key.size() < params.raw_public_key_size()) {
throw Decoding_Error("Invalid XMSS public key size detected");
}
Expand Down Expand Up @@ -178,7 +189,10 @@ std::unique_ptr<PK_Ops::Verification> XMSS_PublicKey::_create_verification_op(
std::unique_ptr<PK_Ops::Verification> XMSS_PublicKey::create_x509_verification_op(const AlgorithmIdentifier& alg_id,
std::string_view provider) const {
if(provider == "base" || provider.empty()) {
if(alg_id != this->algorithm_identifier()) {
// Signatures created with the legacy OID from draft-vangeest-x509-hash-sigs
// remain verifiable
const AlgorithmIdentifier legacy_alg_id("XMSS-draft-vangeest", AlgorithmIdentifier::USE_EMPTY_PARAM);
if(alg_id != this->algorithm_identifier() && alg_id != legacy_alg_id) {
throw Decoding_Error("Unexpected AlgorithmIdentifier for XMSS X509 signature");
}
return std::make_unique<XMSS_Verification_Operation>(*this);
Expand All @@ -191,9 +205,8 @@ std::vector<uint8_t> XMSS_PublicKey::raw_public_key_bits() const {
}

std::vector<uint8_t> XMSS_PublicKey::public_key_bits() const {
std::vector<uint8_t> output;
DER_Encoder(output).encode(raw_public_key_bits(), ASN1_Type::OctetString);
return output;
// RFC 9802 Section 5.2: the raw XMSS public key is used without ASN.1 wrapping
return raw_public_key_bits();
}

std::vector<uint8_t> XMSS_PublicKey::raw_public_key() const {
Expand Down
Loading
Loading