From 0dee7956c13b5db15e71eb8f734935e656e2c122 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 17 Apr 2026 16:55:59 +0200 Subject: [PATCH 01/14] Add support for signing store paths using ML-DSA-65 ML-DSA-65 is a post-quantum cryptography signaturew scheme/ To use, just call `nix key generate-secret` with `--key-type ml-dsa-65`, otherwise it works the same as ed25519 (libsodium) signatures except that it produces much bigger keys/signatures --- .../include/nix/util/signature/local-keys.hh | 23 +- src/libutil/signature/local-keys.cc | 229 ++++++++++++++++-- src/nix/nix-store/nix-store.cc | 2 +- src/nix/sigs.cc | 10 +- tests/functional/signing.sh | 15 +- 5 files changed, 242 insertions(+), 37 deletions(-) diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 789fb831f0f3..30c5e894ac13 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -41,8 +41,16 @@ struct Signature auto operator<=>(const Signature &) const = default; }; +enum KeyType { + Ed25519, + MLDSA65, +}; + +KeyType parseKeyType(std::string_view s); + struct Key { + KeyType type; std::string name; std::string key; @@ -59,8 +67,9 @@ protected: */ Key(std::string_view s, bool sensitiveValue); - Key(std::string_view name, std::string && key) - : name(name) + Key(KeyType type, std::string_view name, std::string && key) + : type(type) + , name(name) , key(std::move(key)) { } @@ -79,11 +88,11 @@ struct SecretKey : Key PublicKey toPublicKey() const; - static SecretKey generate(std::string_view name); + static SecretKey generate(std::string_view name, KeyType type); private: - SecretKey(std::string_view name, std::string && key) - : Key(name, std::move(key)) + SecretKey(KeyType type, std::string_view name, std::string && key) + : Key(type, name, std::move(key)) { } }; @@ -107,8 +116,8 @@ struct PublicKey : Key bool verifyDetachedAnon(std::string_view data, const Signature & sig) const; private: - PublicKey(std::string_view name, std::string && key) - : Key(name, std::move(key)) + PublicKey(KeyType type, std::string_view name, std::string && key) + : Key(type, name, std::move(key)) { } friend struct SecretKey; diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 51f94cee006a..c38d3c5b4d3a 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -1,16 +1,23 @@ #include #include #include +#include +#include #include "nix/util/base-n.hh" #include "nix/util/signature/local-keys.hh" #include "nix/util/json-utils.hh" #include "nix/util/util.hh" +#include "nix/util/deleter.hh" namespace nix { namespace { +using AutoEVP_PKEY = std::unique_ptr>; +using AutoEVP_PKEY_CTX = std::unique_ptr>; +using AutoEVP_MD_CTX = std::unique_ptr>; + /** * Parse a colon-separated string where the second part is Base64-encoded. * @@ -45,6 +52,49 @@ std::string serializeColonBase64(std::string_view name, std::string_view data) return std::string(name) + ":" + base64::encode(std::as_bytes(std::span{data.data(), data.size()})); } +/** + * DER encoding of the ML-DSA-65 algorithm OID `2.16.840.1.101.3.4.3.18` + * as it appears inside a PKCS#8 `PrivateKeyInfo` or `SubjectPublicKeyInfo`. + */ +constexpr std::string_view mlDsa65OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x12"; + +bool isMLDSA65Der(std::string_view data) +{ + return data.substr(0, 64).find(mlDsa65OidDer) != std::string_view::npos; +} + +/** + * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` and verify that the key is ML-DSA-65. + */ +AutoEVP_PKEY parseMLDSA65PrivateKey(std::string_view der) +{ + auto p = (const unsigned char *) der.data(); + AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size())); + if (!pkey) + throw Error("d2i_AutoPrivateKey failed for ML-DSA-65 key"); + + if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") != 1) + throw Error("private key is not ML-DSA-65 (got '%s')", EVP_PKEY_get0_type_name(pkey.get())); + + return pkey; +} + +/** + * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-65. + */ +AutoEVP_PKEY parseMLDSA65PublicKey(std::string_view der) +{ + auto p = (const unsigned char *) der.data(); + AutoEVP_PKEY pkey(d2i_PUBKEY(nullptr, &p, der.size())); + if (!pkey) + throw Error("d2i_PUBKEY failed for ML-DSA-65 key"); + + if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") != 1) + throw Error("public key is not ML-DSA-65 (got '%s')", EVP_PKEY_get0_type_name(pkey.get())); + + return pkey; +} + } // anonymous namespace Signature Signature::parse(std::string_view s) @@ -81,6 +131,15 @@ Strings Signature::toStrings(const std::set & sigs) return res; } +KeyType parseKeyType(std::string_view s) +{ + if (s == "ed25519") + return KeyType::Ed25519; + if (s == "ml-dsa-65") + return KeyType::MLDSA65; + throw UsageError("unknown key type '%s'", s); +} + Key::Key(std::string_view s, bool sensitiveValue) { try { @@ -104,42 +163,134 @@ std::string Key::to_string() const SecretKey::SecretKey(std::string_view s) : Key{s, true} { - if (key.size() != crypto_sign_SECRETKEYBYTES) + if (key.size() == crypto_sign_SECRETKEYBYTES) + type = KeyType::Ed25519; + else if (isMLDSA65Der(key)) + type = KeyType::MLDSA65; + else throw Error("secret key is not valid"); } Signature SecretKey::signDetached(std::string_view data) const { - unsigned char sig[crypto_sign_BYTES]; - unsigned long long sigLen; - crypto_sign_detached(sig, &sigLen, (unsigned char *) data.data(), data.size(), (unsigned char *) key.data()); - return Signature{ - .keyName = name, - .sig = std::string((char *) sig, sigLen), - }; + switch (type) { + + case KeyType::Ed25519: + unsigned char sig[crypto_sign_BYTES]; + unsigned long long sigLen; + crypto_sign_detached(sig, &sigLen, (unsigned char *) data.data(), data.size(), (unsigned char *) key.data()); + return Signature{ + .keyName = name, + .sig = std::string((char *) sig, sigLen), + }; + + case KeyType::MLDSA65: { + auto pkey = parseMLDSA65PrivateKey(key); + + AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); + if (!ctx) + throw Error("EVP_MD_CTX_new failed"); + + if (EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get()) <= 0) + throw Error("EVP_DigestSignInit failed"); + + size_t sigLen = 0; + if (EVP_DigestSign(ctx.get(), nullptr, &sigLen, (const unsigned char *) data.data(), data.size()) <= 0) + throw Error("EVP_DigestSign (get length) failed"); + + std::string sig(sigLen, '\0'); + if (EVP_DigestSign( + ctx.get(), (unsigned char *) sig.data(), &sigLen, (const unsigned char *) data.data(), data.size()) + <= 0) + throw Error("EVP_DigestSign failed"); + sig.resize(sigLen); + + return Signature{ + .keyName = name, + .sig = std::move(sig), + }; + } + + default: + unreachable(); + } } PublicKey SecretKey::toPublicKey() const { - unsigned char pk[crypto_sign_PUBLICKEYBYTES]; - crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); - return PublicKey(name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); + switch (type) { + + case KeyType::Ed25519: + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); + return PublicKey(type, name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); + + case KeyType::MLDSA65: { + auto pkey = parseMLDSA65PrivateKey(key); + + unsigned char * derBuf = nullptr; + int derLen = i2d_PUBKEY(pkey.get(), &derBuf); + if (derLen < 0) + throw Error("i2d_PUBKEY failed"); + std::string der((const char *) derBuf, derLen); + OPENSSL_free(derBuf); + + return PublicKey(type, name, std::move(der)); + } + + default: + unreachable(); + } } -SecretKey SecretKey::generate(std::string_view name) +SecretKey SecretKey::generate(std::string_view name, KeyType type) { - unsigned char pk[crypto_sign_PUBLICKEYBYTES]; - unsigned char sk[crypto_sign_SECRETKEYBYTES]; - if (crypto_sign_keypair(pk, sk) != 0) - throw Error("key generation failed"); + switch (type) { - return SecretKey(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + case KeyType::Ed25519: + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + if (crypto_sign_keypair(pk, sk) != 0) + throw Error("key generation failed"); + + return SecretKey(KeyType::Ed25519, name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + + case KeyType::MLDSA65: { + AutoEVP_PKEY_CTX ctx(EVP_PKEY_CTX_new_from_name(nullptr, "ML-DSA-65", nullptr)); + if (!ctx) + throw Error("EVP_PKEY_CTX_new_from_name failed for ML-DSA-65"); + + if (EVP_PKEY_keygen_init(ctx.get()) <= 0) + throw Error("EVP_PKEY_keygen_init failed"); + + EVP_PKEY * rawPkey = nullptr; + if (EVP_PKEY_generate(ctx.get(), &rawPkey) <= 0) + throw Error("EVP_PKEY_generate failed"); + AutoEVP_PKEY pkey(rawPkey); + + unsigned char * derBuf = nullptr; + int derLen = i2d_PrivateKey(pkey.get(), &derBuf); + if (derLen < 0) + throw Error("i2d_PrivateKey failed"); + std::string der((const char *) derBuf, derLen); + OPENSSL_free(derBuf); + + return SecretKey(KeyType::MLDSA65, name, std::move(der)); + } + + default: + unreachable(); + } } PublicKey::PublicKey(std::string_view s) : Key{s, false} { - if (key.size() != crypto_sign_PUBLICKEYBYTES) + if (key.size() == crypto_sign_PUBLICKEYBYTES) + type = KeyType::Ed25519; + else if (isMLDSA65Der(key)) + type = KeyType::MLDSA65; + else throw Error("public key is not valid"); } @@ -153,15 +304,41 @@ bool PublicKey::verifyDetached(std::string_view data, const Signature & sig) con bool PublicKey::verifyDetachedAnon(std::string_view data, const Signature & sig) const { - if (sig.sig.size() != crypto_sign_BYTES) - throw Error("signature is not valid"); + switch (type) { + + case KeyType::Ed25519: + if (sig.sig.size() != crypto_sign_BYTES) + return false; + + return crypto_sign_verify_detached( + (unsigned char *) sig.sig.data(), + (unsigned char *) data.data(), + data.size(), + (unsigned char *) key.data()) + == 0; + + case KeyType::MLDSA65: { + auto pkey = parseMLDSA65PublicKey(key); + + AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); + if (!ctx) + throw Error("EVP_MD_CTX_new failed"); + + if (EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get()) <= 0) + throw Error("EVP_DigestVerifyInit failed"); + + return EVP_DigestVerify( + ctx.get(), + (const unsigned char *) sig.sig.data(), + sig.sig.size(), + (const unsigned char *) data.data(), + data.size()) + == 1; + } - return crypto_sign_verify_detached( - (unsigned char *) sig.sig.data(), - (unsigned char *) data.data(), - data.size(), - (unsigned char *) key.data()) - == 0; + default: + unreachable(); + } } bool verifyDetached(std::string_view data, const Signature & sig, const PublicKeys & publicKeys) diff --git a/src/nix/nix-store/nix-store.cc b/src/nix/nix-store/nix-store.cc index d6649d3e96dd..ffb6249e931e 100644 --- a/src/nix/nix-store/nix-store.cc +++ b/src/nix/nix-store/nix-store.cc @@ -1101,7 +1101,7 @@ static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs) std::string secretKeyFile = *i++; std::string publicKeyFile = *i++; - auto secretKey = SecretKey::generate(keyName); + auto secretKey = SecretKey::generate(keyName, KeyType::Ed25519); writeFile(publicKeyFile, secretKey.toPublicKey().to_string(), 0666, FsSync::Yes); writeFile(secretKeyFile, secretKey.to_string(), 0600, FsSync::Yes); diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index c72204cea3d4..a1970b022f64 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -149,6 +149,7 @@ static auto rCmdSign = registerCommand2({"store", "sign"}); struct CmdKeyGenerateSecret : Command { std::string keyName; + std::string keyType = "ed25519"; CmdKeyGenerateSecret() { @@ -159,6 +160,13 @@ struct CmdKeyGenerateSecret : Command .handler = {&keyName}, .required = true, }); + + addFlag({ + .longName = "key-type", + .description = "Type of key: `ed25519` or `ml-dsa-65`.", + .labels = {"type"}, + .handler = {&keyType}, + }); } std::string description() override @@ -176,7 +184,7 @@ struct CmdKeyGenerateSecret : Command void run() override { logger->stop(); - writeFull(getStandardOutput(), SecretKey::generate(keyName).to_string()); + writeFull(getStandardOutput(), SecretKey::generate(keyName, parseKeyType(keyType)).to_string()); } }; diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index bfa21fcff76b..ce87973acf24 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -5,9 +5,15 @@ source common.sh clearStoreIfPossible clearCache -nix-store --generate-binary-cache-key cache1.example.org "$TEST_ROOT"/sk1 "$TEST_ROOT"/pk1 +runTests() { + +keyType="$1" + +nix key generate-secret --key-name cache1.example.org --key-type "$keyType" > "$TEST_ROOT"/sk1 +nix key convert-secret-to-public < "$TEST_ROOT"/sk1 > "$TEST_ROOT"/pk1 pk1=$(cat "$TEST_ROOT"/pk1) -nix-store --generate-binary-cache-key cache2.example.org "$TEST_ROOT"/sk2 "$TEST_ROOT"/pk2 +nix key generate-secret --key-name cache2.example.org --key-type "$keyType" > "$TEST_ROOT"/sk2 +nix key convert-secret-to-public < "$TEST_ROOT"/sk2 > "$TEST_ROOT"/pk2 pk2=$(cat "$TEST_ROOT"/pk2) # Build a path. @@ -120,3 +126,8 @@ for file in "$TEST_ROOT/storemultisig/"*.narinfo; do exit 1 fi done + +} + +runTests ed25519 +#runTests ml-dsa-65 From 492d8e2872ee30f41ad71ceae7d230e3c5d4fd08 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 6 May 2026 16:39:38 +0200 Subject: [PATCH 02/14] Run ML-DSA-65 in deterministic mode --- src/libutil/signature/local-keys.cc | 14 ++++++++++++-- tests/functional/signing.sh | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index c38d3c5b4d3a..a77a1d104822 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -1,6 +1,7 @@ #include #include #include +#include #include #include @@ -191,8 +192,17 @@ Signature SecretKey::signDetached(std::string_view data) const if (!ctx) throw Error("EVP_MD_CTX_new failed"); - if (EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get()) <= 0) - throw Error("EVP_DigestSignInit failed"); + /* Generate a deterministic signature (i.e. only depending on the key and the data) since Ed25519 is also + deterministic. Note from RFC-9882: "The signer SHOULD NOT use the deterministic variant of ML-DSA on + platforms where side-channel attacks or fault attacks are a concern." */ + int deterministic = 1; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_int(OSSL_SIGNATURE_PARAM_DETERMINISTIC, &deterministic), + OSSL_PARAM_construct_end(), + }; + + if (EVP_DigestSignInit_ex(ctx.get(), nullptr, nullptr, nullptr, nullptr, pkey.get(), params) <= 0) + throw Error("EVP_DigestSignInit_ex failed"); size_t sigLen = 0; if (EVP_DigestSign(ctx.get(), nullptr, &sigLen, (const unsigned char *) data.data(), data.size()) <= 0) diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index ce87973acf24..0cd25d6cb6c2 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -2,11 +2,11 @@ source common.sh +runTests() { + clearStoreIfPossible clearCache -runTests() { - keyType="$1" nix key generate-secret --key-name cache1.example.org --key-type "$keyType" > "$TEST_ROOT"/sk1 @@ -130,4 +130,4 @@ done } runTests ed25519 -#runTests ml-dsa-65 +runTests ml-dsa-65 From 66f639fc74fee07ee4738b6915b48b13672204c6 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 11:12:18 +0200 Subject: [PATCH 03/14] Fix warning --- src/libexpr/primops/wasm.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/primops/wasm.cc b/src/libexpr/primops/wasm.cc index 05b8a3ca38d9..b59cbec93e90 100644 --- a/src/libexpr/primops/wasm.cc +++ b/src/libexpr/primops/wasm.cc @@ -460,7 +460,7 @@ struct NixWasmInstance while (!args.empty()) { auto arg = &getValue(args[0]); auto tmp = state.allocValue(); - tmp->mkApp(res, {arg}); + tmp->mkApp(res, arg); res = tmp; args = args.subspan(1); } From a6c429b3fb9cee54b9835a9fedbd2a6f33411b9a Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 12:18:43 +0200 Subject: [PATCH 04/14] Support ML-DSA-44 and ML-DSA-87 --- .../include/nix/util/signature/local-keys.hh | 2 + src/libutil/signature/local-keys.cc | 118 +++++++++++++----- tests/functional/signing.sh | 2 + 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 30c5e894ac13..7b09508d730b 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -43,7 +43,9 @@ struct Signature enum KeyType { Ed25519, + MLDSA44, MLDSA65, + MLDSA87, }; KeyType parseKeyType(std::string_view s); diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index a77a1d104822..361c393afc97 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -54,44 +54,75 @@ std::string serializeColonBase64(std::string_view name, std::string_view data) } /** - * DER encoding of the ML-DSA-65 algorithm OID `2.16.840.1.101.3.4.3.18` - * as it appears inside a PKCS#8 `PrivateKeyInfo` or `SubjectPublicKeyInfo`. + * DER encodings of the ML-DSA algorithm OIDs as they appear inside a + * PKCS#8 `PrivateKeyInfo` or `SubjectPublicKeyInfo`: + * + * - ML-DSA-44: `2.16.840.1.101.3.4.3.17` + * - ML-DSA-65: `2.16.840.1.101.3.4.3.18` + * - ML-DSA-87: `2.16.840.1.101.3.4.3.19` */ +constexpr std::string_view mlDsa44OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x11"; constexpr std::string_view mlDsa65OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x12"; +constexpr std::string_view mlDsa87OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x13"; + +std::optional isMLDSA(std::string_view data) +{ + auto prefix = data.substr(0, 64); + if (prefix.find(mlDsa44OidDer) != std::string_view::npos) + return KeyType::MLDSA44; + if (prefix.find(mlDsa65OidDer) != std::string_view::npos) + return KeyType::MLDSA65; + if (prefix.find(mlDsa87OidDer) != std::string_view::npos) + return KeyType::MLDSA87; + return std::nullopt; +} -bool isMLDSA65Der(std::string_view data) +static const char * toOpenSSLKeyType(KeyType type) { - return data.substr(0, 64).find(mlDsa65OidDer) != std::string_view::npos; + switch (type) { + case KeyType::MLDSA44: + return "ML-DSA-44"; + case KeyType::MLDSA65: + return "ML-DSA-65"; + case KeyType::MLDSA87: + return "ML-DSA-87"; + case KeyType::Ed25519: + throw Error("key type is not supported by OpenSSL"); + } } /** - * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` and verify that the key is ML-DSA-65. + * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` and verify that the key is ML-DSA-*. */ -AutoEVP_PKEY parseMLDSA65PrivateKey(std::string_view der) +AutoEVP_PKEY parseMLDSAPrivateKey(std::string_view der, KeyType type) { auto p = (const unsigned char *) der.data(); AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size())); if (!pkey) - throw Error("d2i_AutoPrivateKey failed for ML-DSA-65 key"); + throw Error("d2i_AutoPrivateKey failed for ML-DSA key"); + + auto typeS = toOpenSSLKeyType(type); - if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") != 1) - throw Error("private key is not ML-DSA-65 (got '%s')", EVP_PKEY_get0_type_name(pkey.get())); + if (EVP_PKEY_is_a(pkey.get(), typeS) != 1) + throw Error("private key is not '%s' (got '%s')", typeS, EVP_PKEY_get0_type_name(pkey.get())); return pkey; } /** - * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-65. + * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-*. */ -AutoEVP_PKEY parseMLDSA65PublicKey(std::string_view der) +AutoEVP_PKEY parseMLDSAPublicKey(std::string_view der, KeyType type) { auto p = (const unsigned char *) der.data(); AutoEVP_PKEY pkey(d2i_PUBKEY(nullptr, &p, der.size())); if (!pkey) - throw Error("d2i_PUBKEY failed for ML-DSA-65 key"); + throw Error("d2i_PUBKEY failed for ML-DSA key"); - if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") != 1) - throw Error("public key is not ML-DSA-65 (got '%s')", EVP_PKEY_get0_type_name(pkey.get())); + auto typeS = toOpenSSLKeyType(type); + + if (EVP_PKEY_is_a(pkey.get(), typeS) != 1) + throw Error("public key is not '%s'' (got '%s')", typeS, EVP_PKEY_get0_type_name(pkey.get())); return pkey; } @@ -134,11 +165,23 @@ Strings Signature::toStrings(const std::set & sigs) KeyType parseKeyType(std::string_view s) { - if (s == "ed25519") - return KeyType::Ed25519; - if (s == "ml-dsa-65") - return KeyType::MLDSA65; - throw UsageError("unknown key type '%s'", s); + static std::unordered_map keyTypeMap{ + {"ed25519", KeyType::Ed25519}, + {"ml-dsa-44", KeyType::MLDSA44}, + {"ml-dsa-65", KeyType::MLDSA65}, + {"ml-dsa-87", KeyType::MLDSA87}, + }; + auto i = keyTypeMap.find(s); + if (i != keyTypeMap.end()) + return i->second; + static StringSet validKeyTypes = [] { + StringSet s; + for (const auto & [k, _] : keyTypeMap) { + s.insert(std::string(k)); + } + return s; + }(); + throw UsageError("unknown key type '%s'; valid key types are %s", s, concatStringsSep(", ", validKeyTypes)); } Key::Key(std::string_view s, bool sensitiveValue) @@ -166,8 +209,8 @@ SecretKey::SecretKey(std::string_view s) { if (key.size() == crypto_sign_SECRETKEYBYTES) type = KeyType::Ed25519; - else if (isMLDSA65Der(key)) - type = KeyType::MLDSA65; + else if (auto mldsa = isMLDSA(key)) + type = *mldsa; else throw Error("secret key is not valid"); } @@ -185,8 +228,10 @@ Signature SecretKey::signDetached(std::string_view data) const .sig = std::string((char *) sig, sigLen), }; - case KeyType::MLDSA65: { - auto pkey = parseMLDSA65PrivateKey(key); + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: { + auto pkey = parseMLDSAPrivateKey(key, type); AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) @@ -235,8 +280,10 @@ PublicKey SecretKey::toPublicKey() const crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); return PublicKey(type, name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); - case KeyType::MLDSA65: { - auto pkey = parseMLDSA65PrivateKey(key); + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: { + auto pkey = parseMLDSAPrivateKey(key, type); unsigned char * derBuf = nullptr; int derLen = i2d_PUBKEY(pkey.get(), &derBuf); @@ -265,10 +312,13 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) return SecretKey(KeyType::Ed25519, name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); - case KeyType::MLDSA65: { - AutoEVP_PKEY_CTX ctx(EVP_PKEY_CTX_new_from_name(nullptr, "ML-DSA-65", nullptr)); + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: { + auto typeS = toOpenSSLKeyType(type); + AutoEVP_PKEY_CTX ctx(EVP_PKEY_CTX_new_from_name(nullptr, typeS, nullptr)); if (!ctx) - throw Error("EVP_PKEY_CTX_new_from_name failed for ML-DSA-65"); + throw Error("EVP_PKEY_CTX_new_from_name failed for '%s'", typeS); if (EVP_PKEY_keygen_init(ctx.get()) <= 0) throw Error("EVP_PKEY_keygen_init failed"); @@ -285,7 +335,7 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) std::string der((const char *) derBuf, derLen); OPENSSL_free(derBuf); - return SecretKey(KeyType::MLDSA65, name, std::move(der)); + return SecretKey(type, name, std::move(der)); } default: @@ -298,8 +348,8 @@ PublicKey::PublicKey(std::string_view s) { if (key.size() == crypto_sign_PUBLICKEYBYTES) type = KeyType::Ed25519; - else if (isMLDSA65Der(key)) - type = KeyType::MLDSA65; + else if (auto mldsa = isMLDSA(key)) + type = *mldsa; else throw Error("public key is not valid"); } @@ -327,8 +377,10 @@ bool PublicKey::verifyDetachedAnon(std::string_view data, const Signature & sig) (unsigned char *) key.data()) == 0; - case KeyType::MLDSA65: { - auto pkey = parseMLDSA65PublicKey(key); + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: { + auto pkey = parseMLDSAPublicKey(key, type); AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index 0cd25d6cb6c2..5357e1e2f296 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -130,4 +130,6 @@ done } runTests ed25519 +runTests ml-dsa-44 runTests ml-dsa-65 +runTests ml-dsa-87 From a4ee9f531a1617e49f99f4a12ac147e9740bba06 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 12:38:02 +0200 Subject: [PATCH 05/14] Get rid of hacky key type detection --- src/libutil/signature/local-keys.cc | 76 ++++++++++++++--------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 361c393afc97..a39905c4e9ac 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -53,30 +53,6 @@ std::string serializeColonBase64(std::string_view name, std::string_view data) return std::string(name) + ":" + base64::encode(std::as_bytes(std::span{data.data(), data.size()})); } -/** - * DER encodings of the ML-DSA algorithm OIDs as they appear inside a - * PKCS#8 `PrivateKeyInfo` or `SubjectPublicKeyInfo`: - * - * - ML-DSA-44: `2.16.840.1.101.3.4.3.17` - * - ML-DSA-65: `2.16.840.1.101.3.4.3.18` - * - ML-DSA-87: `2.16.840.1.101.3.4.3.19` - */ -constexpr std::string_view mlDsa44OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x11"; -constexpr std::string_view mlDsa65OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x12"; -constexpr std::string_view mlDsa87OidDer = "\x06\x09\x60\x86\x48\x01\x65\x03\x04\x03\x13"; - -std::optional isMLDSA(std::string_view data) -{ - auto prefix = data.substr(0, 64); - if (prefix.find(mlDsa44OidDer) != std::string_view::npos) - return KeyType::MLDSA44; - if (prefix.find(mlDsa65OidDer) != std::string_view::npos) - return KeyType::MLDSA65; - if (prefix.find(mlDsa87OidDer) != std::string_view::npos) - return KeyType::MLDSA87; - return std::nullopt; -} - static const char * toOpenSSLKeyType(KeyType type) { switch (type) { @@ -94,12 +70,18 @@ static const char * toOpenSSLKeyType(KeyType type) /** * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` and verify that the key is ML-DSA-*. */ -AutoEVP_PKEY parseMLDSAPrivateKey(std::string_view der, KeyType type) +static AutoEVP_PKEY parsePrivateKey(std::string_view der) { auto p = (const unsigned char *) der.data(); AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size())); + return pkey; +} + +static AutoEVP_PKEY parsePrivateKey(std::string_view der, KeyType type) +{ + auto pkey = parsePrivateKey(der); if (!pkey) - throw Error("d2i_AutoPrivateKey failed for ML-DSA key"); + throw Error("private key is not PKCS#8-encoded"); auto typeS = toOpenSSLKeyType(type); @@ -112,12 +94,16 @@ AutoEVP_PKEY parseMLDSAPrivateKey(std::string_view der, KeyType type) /** * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-*. */ -AutoEVP_PKEY parseMLDSAPublicKey(std::string_view der, KeyType type) +static AutoEVP_PKEY parsePublicKey(std::string_view der) { auto p = (const unsigned char *) der.data(); AutoEVP_PKEY pkey(d2i_PUBKEY(nullptr, &p, der.size())); - if (!pkey) - throw Error("d2i_PUBKEY failed for ML-DSA key"); + return pkey; +} + +static AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type) +{ + auto pkey = parsePublicKey(der); auto typeS = toOpenSSLKeyType(type); @@ -209,9 +195,16 @@ SecretKey::SecretKey(std::string_view s) { if (key.size() == crypto_sign_SECRETKEYBYTES) type = KeyType::Ed25519; - else if (auto mldsa = isMLDSA(key)) - type = *mldsa; - else + else if (auto pkey = parsePrivateKey(key)) { + if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) + type = KeyType::MLDSA44; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") == 1) + type = KeyType::MLDSA65; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-87") == 1) + type = KeyType::MLDSA87; + else + throw Error("secret key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); + } else throw Error("secret key is not valid"); } @@ -231,7 +224,7 @@ Signature SecretKey::signDetached(std::string_view data) const case KeyType::MLDSA44: case KeyType::MLDSA65: case KeyType::MLDSA87: { - auto pkey = parseMLDSAPrivateKey(key, type); + auto pkey = parsePrivateKey(key, type); AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) @@ -283,7 +276,7 @@ PublicKey SecretKey::toPublicKey() const case KeyType::MLDSA44: case KeyType::MLDSA65: case KeyType::MLDSA87: { - auto pkey = parseMLDSAPrivateKey(key, type); + auto pkey = parsePrivateKey(key, type); unsigned char * derBuf = nullptr; int derLen = i2d_PUBKEY(pkey.get(), &derBuf); @@ -348,9 +341,16 @@ PublicKey::PublicKey(std::string_view s) { if (key.size() == crypto_sign_PUBLICKEYBYTES) type = KeyType::Ed25519; - else if (auto mldsa = isMLDSA(key)) - type = *mldsa; - else + else if (auto pkey = parsePublicKey(key)) { + if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) + type = KeyType::MLDSA44; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") == 1) + type = KeyType::MLDSA65; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-87") == 1) + type = KeyType::MLDSA87; + else + throw Error("public key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); + } else throw Error("public key is not valid"); } @@ -380,7 +380,7 @@ bool PublicKey::verifyDetachedAnon(std::string_view data, const Signature & sig) case KeyType::MLDSA44: case KeyType::MLDSA65: case KeyType::MLDSA87: { - auto pkey = parseMLDSAPublicKey(key, type); + auto pkey = parsePublicKey(key, type); AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) From 1508a580de7dc1823bbaaa5de2b90f13bf698bf2 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 14:02:55 +0200 Subject: [PATCH 06/14] Add commands for converting keys to PEM --- .../include/nix/util/signature/local-keys.hh | 13 ++++ src/libutil/signature/local-keys.cc | 59 +++++++++++++++++++ src/nix/key-convert-public-to-pem.md | 29 +++++++++ src/nix/key-convert-secret-to-pem.md | 29 +++++++++ src/nix/key-generate-secret.md | 9 ++- src/nix/sigs.cc | 46 +++++++++++++++ tests/functional/signing.sh | 26 ++++++++ 7 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/nix/key-convert-public-to-pem.md create mode 100644 src/nix/key-convert-secret-to-pem.md diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 7b09508d730b..46d417db8203 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -90,6 +90,12 @@ struct SecretKey : Key PublicKey toPublicKey() const; + /** + * Return a PEM PKCS#8 encoding of this secret key. The Nix-specific + * key name is not included. Only ML-DSA keys are supported. + */ + std::string toPEM() const; + static SecretKey generate(std::string_view name, KeyType type); private: @@ -117,6 +123,13 @@ struct PublicKey : Key */ bool verifyDetachedAnon(std::string_view data, const Signature & sig) const; + /** + * Return a PEM SubjectPublicKeyInfo encoding of this public key. + * The Nix-specific key name is not included. Only ML-DSA keys are + * supported. + */ + std::string toPEM() const; + private: PublicKey(KeyType type, std::string_view name, std::string && key) : Key(type, name, std::move(key)) diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index a39905c4e9ac..3cf28e21c29c 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -18,6 +18,7 @@ namespace { using AutoEVP_PKEY = std::unique_ptr>; using AutoEVP_PKEY_CTX = std::unique_ptr>; using AutoEVP_MD_CTX = std::unique_ptr>; +using AutoBIO = std::unique_ptr>; /** * Parse a colon-separated string where the second part is Base64-encoded. @@ -293,6 +294,35 @@ PublicKey SecretKey::toPublicKey() const } } +std::string SecretKey::toPEM() const +{ + switch (type) { + + case KeyType::Ed25519: + throw Error("Ed25519 secret keys cannot be converted to PEM"); + + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: { + auto pkey = parsePrivateKey(key, type); + + AutoBIO bio(BIO_new(BIO_s_mem())); + if (!bio) + throw Error("BIO_new failed"); + + if (PEM_write_bio_PrivateKey(bio.get(), pkey.get(), nullptr, nullptr, 0, nullptr, nullptr) <= 0) + throw Error("PEM_write_bio_PrivateKey failed"); + + char * data = nullptr; + long len = BIO_get_mem_data(bio.get(), &data); + return std::string(data, len); + } + + default: + unreachable(); + } +} + SecretKey SecretKey::generate(std::string_view name, KeyType type) { switch (type) { @@ -403,6 +433,35 @@ bool PublicKey::verifyDetachedAnon(std::string_view data, const Signature & sig) } } +std::string PublicKey::toPEM() const +{ + switch (type) { + + case KeyType::Ed25519: + throw Error("Ed25519 public keys cannot be converted to PEM"); + + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: { + auto pkey = parsePublicKey(key, type); + + AutoBIO bio(BIO_new(BIO_s_mem())); + if (!bio) + throw Error("BIO_new failed"); + + if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) + throw Error("PEM_write_bio_PUBKEY failed"); + + char * data = nullptr; + long len = BIO_get_mem_data(bio.get(), &data); + return std::string(data, len); + } + + default: + unreachable(); + } +} + bool verifyDetached(std::string_view data, const Signature & sig, const PublicKeys & publicKeys) { auto key = publicKeys.find(sig.keyName); diff --git a/src/nix/key-convert-public-to-pem.md b/src/nix/key-convert-public-to-pem.md new file mode 100644 index 000000000000..010886341aba --- /dev/null +++ b/src/nix/key-convert-public-to-pem.md @@ -0,0 +1,29 @@ +R""( + +# Examples + +* Convert a Nix public key to PEM: + + ```console + # nix key convert-secret-to-public < secret-key \ + | nix key convert-public-to-pem + -----BEGIN PUBLIC KEY----- + … + -----END PUBLIC KEY----- + ``` + +* Convert a public key to PEM and decode it using OpenSSL: + + ```console + # nix key convert-public-to-pem < public-key \ + | openssl pkey -pubin -text -noout + ML-DSA-87 Public-Key: + pub: + … + ``` + +# Description + +This command reads a Nix public verification key (as produced by `nix key convert-secret-to-public`) from standard input and writes the corresponding PEM `SubjectPublicKeyInfo` to standard output. The key name is not included in the PEM output. + +)"" diff --git a/src/nix/key-convert-secret-to-pem.md b/src/nix/key-convert-secret-to-pem.md new file mode 100644 index 000000000000..f1ec2ca32491 --- /dev/null +++ b/src/nix/key-convert-secret-to-pem.md @@ -0,0 +1,29 @@ +R""( + +# Examples + +* Convert an ML-DSA-65 secret key to PEM: + + ```console + # nix key generate-secret --key-name cache.example.org-1 --key-type ml-dsa-65 \ + | nix key convert-secret-to-pem + -----BEGIN PRIVATE KEY----- + … + -----END PRIVATE KEY----- + ``` + +* Convert a secret key to PEM and decode it using OpenSSL: + + ```console + # nix key convert-secret-to-pem < secret-key \ + | openssl pkey -text -noout + ML-DSA-87 Private-Key: + seed: + … + ``` + +# Description + +This command reads a Nix signing key generated by `nix key generate-secret` from standard input and writes the corresponding PEM PKCS#8 private key to standard output. The key name is not included in the PEM output. + +)"" diff --git a/src/nix/key-generate-secret.md b/src/nix/key-generate-secret.md index 609b1abccb90..839cd8892b26 100644 --- a/src/nix/key-generate-secret.md +++ b/src/nix/key-generate-secret.md @@ -24,7 +24,7 @@ R""( # Description -This command generates a new Ed25519 secret key for signing store +This command generates a new secret key for signing store paths and prints it on standard output. Use `nix key convert-secret-to-public` to get the corresponding public key for verifying signed store paths. @@ -36,10 +36,15 @@ the host name of your cache (e.g. `cache.example.org`) with a suffix denoting the number of the key (to be incremented every time you need to revoke a key). +Nix supports keys in the following formats (specified using the `--key-type` option): + +* `ed25519` (libsodium). This is the default key type. It produces compact keys and signatures, but may not be resistant to attacks using quantum computers. +* `ml-dsa-44`, `ml-dsa-65`, `ml-dsa-87` (OpenSSL). These generate much larger keys and signatures, but are believed to be resistant to quantum attacks. + # Format Both secret and public keys are represented as the key name followed -by a base-64 encoding of the Ed25519 key data, e.g. +by a base-64 encoding of the key data, e.g. ``` cache.example.org-0:E7lAO+MsPwTFfPXsdPtW8GKui/5ho4KQHVcAGnX+Tti1V4dUxoVoqLyWJ4YESuZJwQ67GVIksDt47og+tPVUZw== diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index a1970b022f64..77acbb2befa9 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -210,6 +210,50 @@ struct CmdKeyConvertSecretToPublic : Command } }; +struct CmdKeyConvertSecretToPem : Command +{ + std::string description() override + { + return "convert a secret key read from standard input to PEM PKCS#8 format"; + } + + std::string doc() override + { + return +#include "key-convert-secret-to-pem.md" + ; + } + + void run() override + { + SecretKey secretKey(drainFD(STDIN_FILENO)); + logger->stop(); + writeFull(getStandardOutput(), secretKey.toPEM()); + } +}; + +struct CmdKeyConvertPublicToPem : Command +{ + std::string description() override + { + return "convert a public key read from standard input to PEM SubjectPublicKeyInfo format"; + } + + std::string doc() override + { + return +#include "key-convert-public-to-pem.md" + ; + } + + void run() override + { + PublicKey publicKey(drainFD(STDIN_FILENO)); + logger->stop(); + writeFull(getStandardOutput(), publicKey.toPEM()); + } +}; + struct CmdKey : NixMultiCommand { CmdKey() @@ -218,6 +262,8 @@ struct CmdKey : NixMultiCommand { {"generate-secret", []() { return make_ref(); }}, {"convert-secret-to-public", []() { return make_ref(); }}, + {"convert-secret-to-pem", []() { return make_ref(); }}, + {"convert-public-to-pem", []() { return make_ref(); }}, }) { } diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index 5357e1e2f296..c1b7a96c137e 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -16,6 +16,32 @@ nix key generate-secret --key-name cache2.example.org --key-type "$keyType" > "$ nix key convert-secret-to-public < "$TEST_ROOT"/sk2 > "$TEST_ROOT"/pk2 pk2=$(cat "$TEST_ROOT"/pk2) +# Test PEM conversion. +if [[ "$keyType" == "ed25519" ]]; then + # Ed25519 keys cannot be converted to PEM. + expectStderr 1 nix key convert-secret-to-pem < "$TEST_ROOT"/sk1 | grepQuiet "Ed25519 secret keys cannot be converted to PEM" + expectStderr 1 nix key convert-public-to-pem < "$TEST_ROOT"/pk1 | grepQuiet "Ed25519 public keys cannot be converted to PEM" +else + # ML-DSA-* keys can be converted to PEM. + nix key convert-secret-to-pem < "$TEST_ROOT"/sk1 > "$TEST_ROOT"/sk1.pem + grepQuiet "^-----BEGIN PRIVATE KEY-----$" "$TEST_ROOT"/sk1.pem + grepQuiet "^-----END PRIVATE KEY-----$" "$TEST_ROOT"/sk1.pem + + nix key convert-public-to-pem < "$TEST_ROOT"/pk1 > "$TEST_ROOT"/pk1.pem + grepQuiet "^-----BEGIN PUBLIC KEY-----$" "$TEST_ROOT"/pk1.pem + grepQuiet "^-----END PUBLIC KEY-----$" "$TEST_ROOT"/pk1.pem + + # If openssl is available, verify that it can parse the PEM keys. + if type -p openssl > /dev/null; then + openssl pkey -text -noout < "$TEST_ROOT"/sk1.pem + openssl pkey -pubin -text -noout < "$TEST_ROOT"/pk1.pem + fi + + # Feeding a secret key to convert-public-to-pem (or vice versa) should fail. + expect 1 nix key convert-public-to-pem < "$TEST_ROOT"/sk1 + expect 1 nix key convert-secret-to-pem < "$TEST_ROOT"/pk1 +fi + # Build a path. outPath=$(nix-build dependencies.nix --no-out-link --secret-key-files "$TEST_ROOT/sk1 $TEST_ROOT/sk2") From 6b7e59fd3080ba1d5ffff40a8ecdac7a1e4d8afb Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 14:38:07 +0200 Subject: [PATCH 07/14] Fix build --- src/libutil/signature/local-keys.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 3cf28e21c29c..b044ec9d3a33 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -64,6 +64,7 @@ static const char * toOpenSSLKeyType(KeyType type) case KeyType::MLDSA87: return "ML-DSA-87"; case KeyType::Ed25519: + default: throw Error("key type is not supported by OpenSSL"); } } From d7d0310bb51efb93933a0465cc87d46d368d31a4 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 21:53:23 +0200 Subject: [PATCH 08/14] Make SecretKey virtual --- src/libstore/binary-cache-store.cc | 4 +- src/libstore/keys.cc | 4 +- src/libstore/store-api.cc | 6 +- .../include/nix/util/signature/local-keys.hh | 20 ++- .../include/nix/util/signature/signer.hh | 4 +- src/libutil/signature/local-keys.cc | 128 ++++++++++-------- src/libutil/signature/signer.cc | 8 +- src/nix/sigs.cc | 9 +- src/perl/lib/Nix/Store.xs | 2 +- tests/functional/signing.sh | 2 +- 10 files changed, 96 insertions(+), 91 deletions(-) diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index f95d8a866194..c00d56c62c3d 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -28,13 +28,13 @@ BinaryCacheStore::BinaryCacheStore(Config & config) : config{config} { if (!config.secretKeyFile.get().empty()) - signers.push_back(std::make_unique(SecretKey{readFile(config.secretKeyFile.get())})); + signers.push_back(std::make_unique(SecretKey::parse(readFile(config.secretKeyFile.get())))); if (config.secretKeyFiles != "") { std::stringstream ss(config.secretKeyFiles); std::string keyPath; while (std::getline(ss, keyPath, ',')) { - signers.push_back(std::make_unique(SecretKey{readFile(keyPath)})); + signers.push_back(std::make_unique(SecretKey::parse(readFile(keyPath)))); } } diff --git a/src/libstore/keys.cc b/src/libstore/keys.cc index 8b02e7a66819..657c35d5cfc5 100644 --- a/src/libstore/keys.cc +++ b/src/libstore/keys.cc @@ -17,8 +17,8 @@ PublicKeys getDefaultPublicKeys() for (const auto & secretKeyFile : settings.secretKeyFiles.get()) { try { - SecretKey secretKey(readFile(secretKeyFile)); - publicKeys.emplace(secretKey.name, secretKey.toPublicKey()); + auto secretKey = SecretKey::parse(readFile(secretKeyFile)); + publicKeys.emplace(secretKey->name, secretKey->toPublicKey()); } catch (SystemError & e) { /* Ignore unreadable key files. That's normal in a multi-user installation. */ diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 57952b1dfd4a..1145c2574e56 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -1269,8 +1269,7 @@ void Store::signPathInfo(ValidPathInfo & info) auto secretKeyFiles = settings.secretKeyFiles; for (auto & secretKeyFile : secretKeyFiles.get()) { - SecretKey secretKey(readFile(secretKeyFile)); - LocalSigner signer(std::move(secretKey)); + LocalSigner signer(SecretKey::parse(readFile(secretKeyFile))); info.sign(*this, signer); } } @@ -1282,8 +1281,7 @@ void Store::signRealisation(Realisation & realisation) auto secretKeyFiles = settings.secretKeyFiles; for (auto & secretKeyFile : secretKeyFiles.get()) { - SecretKey secretKey(readFile(secretKeyFile)); - LocalSigner signer(std::move(secretKey)); + LocalSigner signer(SecretKey::parse(readFile(secretKeyFile))); realisation.sign(realisation.id, signer); } } diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 46d417db8203..99ea2e09131d 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -81,28 +81,26 @@ struct PublicKey; struct SecretKey : Key { - SecretKey(std::string_view s); + using Key::Key; + + virtual ~SecretKey() {}; + + static std::unique_ptr parse(std::string_view s); /** * Return a detached signature of the given string. */ - Signature signDetached(std::string_view s) const; + virtual Signature signDetached(std::string_view s) const; - PublicKey toPublicKey() const; + virtual PublicKey toPublicKey() const; /** * Return a PEM PKCS#8 encoding of this secret key. The Nix-specific * key name is not included. Only ML-DSA keys are supported. */ - std::string toPEM() const; + virtual std::string toPEM() const; static SecretKey generate(std::string_view name, KeyType type); - -private: - SecretKey(KeyType type, std::string_view name, std::string && key) - : Key(type, name, std::move(key)) - { - } }; struct PublicKey : Key @@ -130,12 +128,10 @@ struct PublicKey : Key */ std::string toPEM() const; -private: PublicKey(KeyType type, std::string_view name, std::string && key) : Key(type, name, std::move(key)) { } - friend struct SecretKey; }; /** diff --git a/src/libutil/include/nix/util/signature/signer.hh b/src/libutil/include/nix/util/signature/signer.hh index d03dbe975466..22f7ce3080c2 100644 --- a/src/libutil/include/nix/util/signature/signer.hh +++ b/src/libutil/include/nix/util/signature/signer.hh @@ -46,7 +46,7 @@ using Signers = std::map; */ struct LocalSigner : Signer { - LocalSigner(SecretKey && privateKey); + LocalSigner(std::unique_ptr && privateKey); Signature signDetached(std::string_view s) const override; @@ -54,7 +54,7 @@ struct LocalSigner : Signer private: - SecretKey privateKey; + std::unique_ptr privateKey; PublicKey publicKey; }; diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index b044ec9d3a33..30acae0267cb 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -192,29 +192,31 @@ std::string Key::to_string() const return serializeColonBase64(name, key); } -SecretKey::SecretKey(std::string_view s) - : Key{s, true} +Signature SecretKey::signDetached(std::string_view s) const { - if (key.size() == crypto_sign_SECRETKEYBYTES) - type = KeyType::Ed25519; - else if (auto pkey = parsePrivateKey(key)) { - if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) - type = KeyType::MLDSA44; - else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") == 1) - type = KeyType::MLDSA65; - else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-87") == 1) - type = KeyType::MLDSA87; - else - throw Error("secret key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); - } else - throw Error("secret key is not valid"); + throw Error("signing is not implemented for this key type"); } -Signature SecretKey::signDetached(std::string_view data) const +PublicKey SecretKey::toPublicKey() const { - switch (type) { + throw Error("conversion to public key is not implemented for this key type"); +} - case KeyType::Ed25519: +std::string SecretKey::toPEM() const +{ + throw Error("conversion to PEM is not implemented for this key type"); +} + +struct Ed25519SecretKey : SecretKey +{ + Ed25519SecretKey(std::string_view name, std::string && _key) + : SecretKey(KeyType::Ed25519, name, std::move(_key)) + { + assert(key.size() == crypto_sign_SECRETKEYBYTES); + } + + Signature signDetached(std::string_view data) const override + { unsigned char sig[crypto_sign_BYTES]; unsigned long long sigLen; crypto_sign_detached(sig, &sigLen, (unsigned char *) data.data(), data.size(), (unsigned char *) key.data()); @@ -222,10 +224,26 @@ Signature SecretKey::signDetached(std::string_view data) const .keyName = name, .sig = std::string((char *) sig, sigLen), }; + } - case KeyType::MLDSA44: - case KeyType::MLDSA65: - case KeyType::MLDSA87: { + PublicKey toPublicKey() const override + { + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); + return PublicKey(type, name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); + } +}; + +struct OpenSSLSecretKey : SecretKey +{ + OpenSSLSecretKey(KeyType type, std::string_view name, std::string && key) + : SecretKey(type, name, std::move(key)) + { + assert(type == KeyType::MLDSA44 || type == KeyType::MLDSA65 || type == KeyType::MLDSA87); + } + + Signature signDetached(std::string_view data) const override + { auto pkey = parsePrivateKey(key, type); AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); @@ -261,23 +279,8 @@ Signature SecretKey::signDetached(std::string_view data) const }; } - default: - unreachable(); - } -} - -PublicKey SecretKey::toPublicKey() const -{ - switch (type) { - - case KeyType::Ed25519: - unsigned char pk[crypto_sign_PUBLICKEYBYTES]; - crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); - return PublicKey(type, name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); - - case KeyType::MLDSA44: - case KeyType::MLDSA65: - case KeyType::MLDSA87: { + PublicKey toPublicKey() const override + { auto pkey = parsePrivateKey(key, type); unsigned char * derBuf = nullptr; @@ -290,21 +293,8 @@ PublicKey SecretKey::toPublicKey() const return PublicKey(type, name, std::move(der)); } - default: - unreachable(); - } -} - -std::string SecretKey::toPEM() const -{ - switch (type) { - - case KeyType::Ed25519: - throw Error("Ed25519 secret keys cannot be converted to PEM"); - - case KeyType::MLDSA44: - case KeyType::MLDSA65: - case KeyType::MLDSA87: { + std::string toPEM() const override + { auto pkey = parsePrivateKey(key, type); AutoBIO bio(BIO_new(BIO_s_mem())); @@ -318,9 +308,33 @@ std::string SecretKey::toPEM() const long len = BIO_get_mem_data(bio.get(), &data); return std::string(data, len); } +}; - default: - unreachable(); +std::unique_ptr SecretKey::parse(std::string_view s) +{ + try { + auto [name, key] = parseColonBase64(s, "key"); + + if (key.size() == crypto_sign_SECRETKEYBYTES) + return std::make_unique(name, std::move(key)); + else if (auto pkey = parsePrivateKey(key)) { + KeyType type; + if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) + type = KeyType::MLDSA44; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") == 1) + type = KeyType::MLDSA65; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-87") == 1) + type = KeyType::MLDSA87; + else + throw Error("secret key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); + return std::make_unique(type, name, std::move(key)); + } else + throw Error("secret key is not valid"); + + } catch (Error & e) { + // Don't show the entire key for security. + e.addTrace({}, "while decoding key '%s…'", s.substr(0, 32)); + throw; } } @@ -334,7 +348,7 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) if (crypto_sign_keypair(pk, sk) != 0) throw Error("key generation failed"); - return SecretKey(KeyType::Ed25519, name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + return Ed25519SecretKey(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); case KeyType::MLDSA44: case KeyType::MLDSA65: @@ -359,7 +373,7 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) std::string der((const char *) derBuf, derLen); OPENSSL_free(derBuf); - return SecretKey(type, name, std::move(der)); + return OpenSSLSecretKey(type, name, std::move(der)); } default: diff --git a/src/libutil/signature/signer.cc b/src/libutil/signature/signer.cc index fff03fc30db1..b8be4f3b3e18 100644 --- a/src/libutil/signature/signer.cc +++ b/src/libutil/signature/signer.cc @@ -5,15 +5,15 @@ namespace nix { -LocalSigner::LocalSigner(SecretKey && privateKey) - : privateKey(privateKey) - , publicKey(privateKey.toPublicKey()) +LocalSigner::LocalSigner(std::unique_ptr && _privateKey) + : privateKey(std::move(_privateKey)) + , publicKey(privateKey->toPublicKey()) { } Signature LocalSigner::signDetached(std::string_view s) const { - return privateKey.signDetached(s); + return privateKey->signDetached(s); } const PublicKey & LocalSigner::getPublicKey() diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index 77acbb2befa9..4a262fa37b10 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -121,8 +121,7 @@ struct CmdSign : StorePathsCommand void run(ref store, StorePaths && storePaths) override { - SecretKey secretKey(readFile(secretKeyFile)); - LocalSigner signer(std::move(secretKey)); + LocalSigner signer(SecretKey::parse(readFile(secretKeyFile))); size_t added{0}; @@ -204,9 +203,8 @@ struct CmdKeyConvertSecretToPublic : Command void run() override { - SecretKey secretKey(drainFD(STDIN_FILENO)); logger->stop(); - writeFull(getStandardOutput(), secretKey.toPublicKey().to_string()); + writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO))->toPublicKey().to_string()); } }; @@ -226,9 +224,8 @@ struct CmdKeyConvertSecretToPem : Command void run() override { - SecretKey secretKey(drainFD(STDIN_FILENO)); logger->stop(); - writeFull(getStandardOutput(), secretKey.toPEM()); + writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO))->toPEM()); } }; diff --git a/src/perl/lib/Nix/Store.xs b/src/perl/lib/Nix/Store.xs index 505faf0279cb..d7493ceeff70 100644 --- a/src/perl/lib/Nix/Store.xs +++ b/src/perl/lib/Nix/Store.xs @@ -301,7 +301,7 @@ SV * convertHash(char * algo, char * s, int toBase32) SV * signString(char * secretKey_, char * msg) PPCODE: try { - auto sig = SecretKey(secretKey_).signDetached(msg).to_string(); + auto sig = SecretKey::parse(secretKey_)->signDetached(msg).to_string(); XPUSHs(sv_2mortal(newSVpv(sig.c_str(), sig.size()))); } catch (Error & e) { croak("%s", e.what()); diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index c1b7a96c137e..e145a641cb3c 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -19,7 +19,7 @@ pk2=$(cat "$TEST_ROOT"/pk2) # Test PEM conversion. if [[ "$keyType" == "ed25519" ]]; then # Ed25519 keys cannot be converted to PEM. - expectStderr 1 nix key convert-secret-to-pem < "$TEST_ROOT"/sk1 | grepQuiet "Ed25519 secret keys cannot be converted to PEM" + expectStderr 1 nix key convert-secret-to-pem < "$TEST_ROOT"/sk1 | grepQuiet "conversion to PEM is not implemented for this key type" expectStderr 1 nix key convert-public-to-pem < "$TEST_ROOT"/pk1 | grepQuiet "Ed25519 public keys cannot be converted to PEM" else # ML-DSA-* keys can be converted to PEM. From a26dd7de218e528b7e5a8af3ebf14fee04ee6da9 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 22:23:26 +0200 Subject: [PATCH 09/14] Make PublicKey virtual --- src/libstore/keys.cc | 6 +- .../include/nix/util/signature/local-keys.hh | 40 ++-- .../include/nix/util/signature/signer.hh | 4 +- src/libutil/signature/local-keys.cc | 224 +++++++++--------- src/libutil/signature/signer.cc | 2 +- src/nix/nix-store/nix-store.cc | 4 +- src/nix/sigs.cc | 7 +- tests/functional/signing.sh | 2 +- 8 files changed, 143 insertions(+), 146 deletions(-) diff --git a/src/libstore/keys.cc b/src/libstore/keys.cc index 657c35d5cfc5..604b6e36ff40 100644 --- a/src/libstore/keys.cc +++ b/src/libstore/keys.cc @@ -11,10 +11,12 @@ PublicKeys getDefaultPublicKeys() // FIXME: filter duplicates for (const auto & s : settings.trustedPublicKeys.get()) { - PublicKey key(s); - publicKeys.emplace(key.name, key); + auto key = PublicKey::parse(s); + auto name = key->name; + publicKeys.emplace(name, std::move(key)); } + // FIXME: keep secret keys in memory (see Store::signRealisation()). for (const auto & secretKeyFile : settings.secretKeyFiles.get()) { try { auto secretKey = SecretKey::parse(readFile(secretKeyFile)); diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 99ea2e09131d..986f0547e1b7 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -52,26 +52,15 @@ KeyType parseKeyType(std::string_view s); struct Key { - KeyType type; - std::string name; - std::string key; + const std::string name; + const std::string key; std::string to_string() const; protected: - /** - * Construct Key from a string in the format - * ‘:’. - * - * @param sensitiveValue Avoid displaying the raw Base64 in error - * messages to avoid leaking private keys. - */ - Key(std::string_view s, bool sensitiveValue); - - Key(KeyType type, std::string_view name, std::string && key) - : type(type) - , name(name) + Key(std::string_view name, std::string && key) + : name(name) , key(std::move(key)) { } @@ -92,7 +81,7 @@ struct SecretKey : Key */ virtual Signature signDetached(std::string_view s) const; - virtual PublicKey toPublicKey() const; + virtual std::unique_ptr toPublicKey() const; /** * Return a PEM PKCS#8 encoding of this secret key. The Nix-specific @@ -100,12 +89,16 @@ struct SecretKey : Key */ virtual std::string toPEM() const; - static SecretKey generate(std::string_view name, KeyType type); + static std::unique_ptr generate(std::string_view name, KeyType type); }; struct PublicKey : Key { - PublicKey(std::string_view data); + using Key::Key; + + virtual ~PublicKey() {}; + + static std::unique_ptr parse(std::string_view s); /** * @return true iff `sig` and this key's names match, and `sig` is a @@ -119,25 +112,20 @@ struct PublicKey : Key * * @param sig the raw signature bytes (not Base64 encoded). */ - bool verifyDetachedAnon(std::string_view data, const Signature & sig) const; + virtual bool verifyDetachedAnon(std::string_view data, const Signature & sig) const; /** * Return a PEM SubjectPublicKeyInfo encoding of this public key. * The Nix-specific key name is not included. Only ML-DSA keys are * supported. */ - std::string toPEM() const; - - PublicKey(KeyType type, std::string_view name, std::string && key) - : Key(type, name, std::move(key)) - { - } + virtual std::string toPEM() const; }; /** * Map from key names to public keys */ -typedef std::map PublicKeys; +typedef std::map> PublicKeys; /** * @return true iff ‘sig’ is a correct signature over ‘data’ using one diff --git a/src/libutil/include/nix/util/signature/signer.hh b/src/libutil/include/nix/util/signature/signer.hh index 22f7ce3080c2..92765360a829 100644 --- a/src/libutil/include/nix/util/signature/signer.hh +++ b/src/libutil/include/nix/util/signature/signer.hh @@ -54,8 +54,8 @@ struct LocalSigner : Signer private: - std::unique_ptr privateKey; - PublicKey publicKey; + const std::unique_ptr privateKey; + const std::unique_ptr publicKey; }; } // namespace nix diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 30acae0267cb..1fe4437679f2 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -54,7 +54,7 @@ std::string serializeColonBase64(std::string_view name, std::string_view data) return std::string(name) + ":" + base64::encode(std::as_bytes(std::span{data.data(), data.size()})); } -static const char * toOpenSSLKeyType(KeyType type) +const char * toOpenSSLKeyType(KeyType type) { switch (type) { case KeyType::MLDSA44: @@ -72,14 +72,14 @@ static const char * toOpenSSLKeyType(KeyType type) /** * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` and verify that the key is ML-DSA-*. */ -static AutoEVP_PKEY parsePrivateKey(std::string_view der) +AutoEVP_PKEY parsePrivateKey(std::string_view der) { auto p = (const unsigned char *) der.data(); AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size())); return pkey; } -static AutoEVP_PKEY parsePrivateKey(std::string_view der, KeyType type) +AutoEVP_PKEY parsePrivateKey(std::string_view der, KeyType type) { auto pkey = parsePrivateKey(der); if (!pkey) @@ -96,14 +96,14 @@ static AutoEVP_PKEY parsePrivateKey(std::string_view der, KeyType type) /** * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-*. */ -static AutoEVP_PKEY parsePublicKey(std::string_view der) +AutoEVP_PKEY parsePublicKey(std::string_view der) { auto p = (const unsigned char *) der.data(); AutoEVP_PKEY pkey(d2i_PUBKEY(nullptr, &p, der.size())); return pkey; } -static AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type) +AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type) { auto pkey = parsePublicKey(der); @@ -172,21 +172,6 @@ KeyType parseKeyType(std::string_view s) throw UsageError("unknown key type '%s'; valid key types are %s", s, concatStringsSep(", ", validKeyTypes)); } -Key::Key(std::string_view s, bool sensitiveValue) -{ - try { - auto [parsedName, parsedKey] = parseColonBase64(s, "key"); - name = std::move(parsedName); - key = std::move(parsedKey); - } catch (Error & e) { - std::string extra; - if (!sensitiveValue) - extra = fmt(" with raw value '%s'", s); - e.addTrace({}, "while decoding key named '%s'%s", name, extra); - throw; - } -} - std::string Key::to_string() const { return serializeColonBase64(name, key); @@ -197,7 +182,7 @@ Signature SecretKey::signDetached(std::string_view s) const throw Error("signing is not implemented for this key type"); } -PublicKey SecretKey::toPublicKey() const +std::unique_ptr SecretKey::toPublicKey() const { throw Error("conversion to public key is not implemented for this key type"); } @@ -207,10 +192,32 @@ std::string SecretKey::toPEM() const throw Error("conversion to PEM is not implemented for this key type"); } +struct Ed25519PublicKey : PublicKey +{ + Ed25519PublicKey(std::string_view name, std::string && _key) + : PublicKey(name, std::move(_key)) + { + assert(key.size() == crypto_sign_PUBLICKEYBYTES); + } + + bool verifyDetachedAnon(std::string_view data, const Signature & sig) const override + { + if (sig.sig.size() != crypto_sign_BYTES) + return false; + + return crypto_sign_verify_detached( + (unsigned char *) sig.sig.data(), + (unsigned char *) data.data(), + data.size(), + (unsigned char *) key.data()) + == 0; + } +}; + struct Ed25519SecretKey : SecretKey { Ed25519SecretKey(std::string_view name, std::string && _key) - : SecretKey(KeyType::Ed25519, name, std::move(_key)) + : SecretKey(name, std::move(_key)) { assert(key.size() == crypto_sign_SECRETKEYBYTES); } @@ -226,18 +233,69 @@ struct Ed25519SecretKey : SecretKey }; } - PublicKey toPublicKey() const override + std::unique_ptr toPublicKey() const override { unsigned char pk[crypto_sign_PUBLICKEYBYTES]; crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); - return PublicKey(type, name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); + return std::make_unique(name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); + } +}; + +struct OpenSSLPublicKey : PublicKey +{ + KeyType type; + + OpenSSLPublicKey(KeyType type, std::string_view name, std::string && key) + : PublicKey(name, std::move(key)) + , type(type) + { + assert(type == KeyType::MLDSA44 || type == KeyType::MLDSA65 || type == KeyType::MLDSA87); + } + + bool verifyDetachedAnon(std::string_view data, const Signature & sig) const override + { + auto pkey = parsePublicKey(key, type); + + AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); + if (!ctx) + throw Error("EVP_MD_CTX_new failed"); + + if (EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get()) <= 0) + throw Error("EVP_DigestVerifyInit failed"); + + return EVP_DigestVerify( + ctx.get(), + (const unsigned char *) sig.sig.data(), + sig.sig.size(), + (const unsigned char *) data.data(), + data.size()) + == 1; + } + + std::string toPEM() const override + { + auto pkey = parsePublicKey(key, type); + + AutoBIO bio(BIO_new(BIO_s_mem())); + if (!bio) + throw Error("BIO_new failed"); + + if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) + throw Error("PEM_write_bio_PUBKEY failed"); + + char * data = nullptr; + long len = BIO_get_mem_data(bio.get(), &data); + return std::string(data, len); } }; struct OpenSSLSecretKey : SecretKey { + KeyType type; + OpenSSLSecretKey(KeyType type, std::string_view name, std::string && key) - : SecretKey(type, name, std::move(key)) + : SecretKey(name, std::move(key)) + , type(type) { assert(type == KeyType::MLDSA44 || type == KeyType::MLDSA65 || type == KeyType::MLDSA87); } @@ -279,7 +337,7 @@ struct OpenSSLSecretKey : SecretKey }; } - PublicKey toPublicKey() const override + std::unique_ptr toPublicKey() const override { auto pkey = parsePrivateKey(key, type); @@ -290,7 +348,7 @@ struct OpenSSLSecretKey : SecretKey std::string der((const char *) derBuf, derLen); OPENSSL_free(derBuf); - return PublicKey(type, name, std::move(der)); + return std::make_unique(type, name, std::move(der)); } std::string toPEM() const override @@ -338,7 +396,7 @@ std::unique_ptr SecretKey::parse(std::string_view s) } } -SecretKey SecretKey::generate(std::string_view name, KeyType type) +std::unique_ptr SecretKey::generate(std::string_view name, KeyType type) { switch (type) { @@ -348,7 +406,7 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) if (crypto_sign_keypair(pk, sk) != 0) throw Error("key generation failed"); - return Ed25519SecretKey(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + return std::make_unique(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); case KeyType::MLDSA44: case KeyType::MLDSA65: @@ -373,7 +431,7 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) std::string der((const char *) derBuf, derLen); OPENSSL_free(derBuf); - return OpenSSLSecretKey(type, name, std::move(der)); + return std::make_unique(type, name, std::move(der)); } default: @@ -381,22 +439,31 @@ SecretKey SecretKey::generate(std::string_view name, KeyType type) } } -PublicKey::PublicKey(std::string_view s) - : Key{s, false} +std::unique_ptr PublicKey::parse(std::string_view s) { - if (key.size() == crypto_sign_PUBLICKEYBYTES) - type = KeyType::Ed25519; - else if (auto pkey = parsePublicKey(key)) { - if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) - type = KeyType::MLDSA44; - else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") == 1) - type = KeyType::MLDSA65; - else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-87") == 1) - type = KeyType::MLDSA87; - else - throw Error("public key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); - } else - throw Error("public key is not valid"); + try { + auto [name, key] = parseColonBase64(s, "key"); + + if (key.size() == crypto_sign_PUBLICKEYBYTES) + return std::make_unique(name, std::move(key)); + else if (auto pkey = parsePublicKey(key)) { + KeyType type; + if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) + type = KeyType::MLDSA44; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-65") == 1) + type = KeyType::MLDSA65; + else if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-87") == 1) + type = KeyType::MLDSA87; + else + throw Error("public key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); + return std::make_unique(type, name, std::move(key)); + } else + throw Error("public key is not valid"); + } catch (Error & e) { + // Don't show the entire key for security. + e.addTrace({}, "while decoding key '%s'", s); + throw; + } } bool PublicKey::verifyDetached(std::string_view data, const Signature & sig) const @@ -409,72 +476,13 @@ bool PublicKey::verifyDetached(std::string_view data, const Signature & sig) con bool PublicKey::verifyDetachedAnon(std::string_view data, const Signature & sig) const { - switch (type) { - - case KeyType::Ed25519: - if (sig.sig.size() != crypto_sign_BYTES) - return false; - - return crypto_sign_verify_detached( - (unsigned char *) sig.sig.data(), - (unsigned char *) data.data(), - data.size(), - (unsigned char *) key.data()) - == 0; - - case KeyType::MLDSA44: - case KeyType::MLDSA65: - case KeyType::MLDSA87: { - auto pkey = parsePublicKey(key, type); - - AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); - if (!ctx) - throw Error("EVP_MD_CTX_new failed"); - - if (EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get()) <= 0) - throw Error("EVP_DigestVerifyInit failed"); - - return EVP_DigestVerify( - ctx.get(), - (const unsigned char *) sig.sig.data(), - sig.sig.size(), - (const unsigned char *) data.data(), - data.size()) - == 1; - } - - default: - unreachable(); - } + // Unsupported key type, can't verify. + return false; } std::string PublicKey::toPEM() const { - switch (type) { - - case KeyType::Ed25519: - throw Error("Ed25519 public keys cannot be converted to PEM"); - - case KeyType::MLDSA44: - case KeyType::MLDSA65: - case KeyType::MLDSA87: { - auto pkey = parsePublicKey(key, type); - - AutoBIO bio(BIO_new(BIO_s_mem())); - if (!bio) - throw Error("BIO_new failed"); - - if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) - throw Error("PEM_write_bio_PUBKEY failed"); - - char * data = nullptr; - long len = BIO_get_mem_data(bio.get(), &data); - return std::string(data, len); - } - - default: - unreachable(); - } + throw Error("conversion to PEM is not implemented for this key type"); } bool verifyDetached(std::string_view data, const Signature & sig, const PublicKeys & publicKeys) @@ -483,7 +491,7 @@ bool verifyDetached(std::string_view data, const Signature & sig, const PublicKe if (key == publicKeys.end()) return false; - return key->second.verifyDetachedAnon(data, sig); + return key->second->verifyDetachedAnon(data, sig); } } // namespace nix diff --git a/src/libutil/signature/signer.cc b/src/libutil/signature/signer.cc index b8be4f3b3e18..493754c4e371 100644 --- a/src/libutil/signature/signer.cc +++ b/src/libutil/signature/signer.cc @@ -18,7 +18,7 @@ Signature LocalSigner::signDetached(std::string_view s) const const PublicKey & LocalSigner::getPublicKey() { - return publicKey; + return *publicKey; } } // namespace nix diff --git a/src/nix/nix-store/nix-store.cc b/src/nix/nix-store/nix-store.cc index ffb6249e931e..1cc4d7aa9c07 100644 --- a/src/nix/nix-store/nix-store.cc +++ b/src/nix/nix-store/nix-store.cc @@ -1103,8 +1103,8 @@ static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs) auto secretKey = SecretKey::generate(keyName, KeyType::Ed25519); - writeFile(publicKeyFile, secretKey.toPublicKey().to_string(), 0666, FsSync::Yes); - writeFile(secretKeyFile, secretKey.to_string(), 0600, FsSync::Yes); + writeFile(publicKeyFile, secretKey->toPublicKey()->to_string(), 0666, FsSync::Yes); + writeFile(secretKeyFile, secretKey->to_string(), 0600, FsSync::Yes); } static void opVersion(Strings opFlags, Strings opArgs) diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index 4a262fa37b10..2e34b2517a09 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -183,7 +183,7 @@ struct CmdKeyGenerateSecret : Command void run() override { logger->stop(); - writeFull(getStandardOutput(), SecretKey::generate(keyName, parseKeyType(keyType)).to_string()); + writeFull(getStandardOutput(), SecretKey::generate(keyName, parseKeyType(keyType))->to_string()); } }; @@ -204,7 +204,7 @@ struct CmdKeyConvertSecretToPublic : Command void run() override { logger->stop(); - writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO))->toPublicKey().to_string()); + writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO))->toPublicKey()->to_string()); } }; @@ -245,9 +245,8 @@ struct CmdKeyConvertPublicToPem : Command void run() override { - PublicKey publicKey(drainFD(STDIN_FILENO)); logger->stop(); - writeFull(getStandardOutput(), publicKey.toPEM()); + writeFull(getStandardOutput(), PublicKey::parse(drainFD(STDIN_FILENO))->toPEM()); } }; diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index e145a641cb3c..e2321d6f12e2 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -20,7 +20,7 @@ pk2=$(cat "$TEST_ROOT"/pk2) if [[ "$keyType" == "ed25519" ]]; then # Ed25519 keys cannot be converted to PEM. expectStderr 1 nix key convert-secret-to-pem < "$TEST_ROOT"/sk1 | grepQuiet "conversion to PEM is not implemented for this key type" - expectStderr 1 nix key convert-public-to-pem < "$TEST_ROOT"/pk1 | grepQuiet "Ed25519 public keys cannot be converted to PEM" + expectStderr 1 nix key convert-public-to-pem < "$TEST_ROOT"/pk1 | grepQuiet "conversion to PEM is not implemented for this key type" else # ML-DSA-* keys can be converted to PEM. nix key convert-secret-to-pem < "$TEST_ROOT"/sk1 > "$TEST_ROOT"/sk1.pem From 797d7370bd7b629cfca1349c96d237d1b9fd88e0 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 22:31:10 +0200 Subject: [PATCH 10/14] Keep parsed OpenSSL keys in memory --- src/libutil/signature/local-keys.cc | 42 +++++++++-------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 1fe4437679f2..4c9438e97636 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -79,20 +79,6 @@ AutoEVP_PKEY parsePrivateKey(std::string_view der) return pkey; } -AutoEVP_PKEY parsePrivateKey(std::string_view der, KeyType type) -{ - auto pkey = parsePrivateKey(der); - if (!pkey) - throw Error("private key is not PKCS#8-encoded"); - - auto typeS = toOpenSSLKeyType(type); - - if (EVP_PKEY_is_a(pkey.get(), typeS) != 1) - throw Error("private key is not '%s' (got '%s')", typeS, EVP_PKEY_get0_type_name(pkey.get())); - - return pkey; -} - /** * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-*. */ @@ -244,18 +230,18 @@ struct Ed25519SecretKey : SecretKey struct OpenSSLPublicKey : PublicKey { KeyType type; + AutoEVP_PKEY pkey; - OpenSSLPublicKey(KeyType type, std::string_view name, std::string && key) + OpenSSLPublicKey(KeyType type, std::string_view name, std::string && key, AutoEVP_PKEY && pkey) : PublicKey(name, std::move(key)) , type(type) + , pkey(std::move(pkey)) { assert(type == KeyType::MLDSA44 || type == KeyType::MLDSA65 || type == KeyType::MLDSA87); } bool verifyDetachedAnon(std::string_view data, const Signature & sig) const override { - auto pkey = parsePublicKey(key, type); - AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) throw Error("EVP_MD_CTX_new failed"); @@ -274,8 +260,6 @@ struct OpenSSLPublicKey : PublicKey std::string toPEM() const override { - auto pkey = parsePublicKey(key, type); - AutoBIO bio(BIO_new(BIO_s_mem())); if (!bio) throw Error("BIO_new failed"); @@ -292,18 +276,18 @@ struct OpenSSLPublicKey : PublicKey struct OpenSSLSecretKey : SecretKey { KeyType type; + AutoEVP_PKEY pkey; - OpenSSLSecretKey(KeyType type, std::string_view name, std::string && key) + OpenSSLSecretKey(KeyType type, std::string_view name, std::string && key, AutoEVP_PKEY && pkey) : SecretKey(name, std::move(key)) , type(type) + , pkey(std::move(pkey)) { assert(type == KeyType::MLDSA44 || type == KeyType::MLDSA65 || type == KeyType::MLDSA87); } Signature signDetached(std::string_view data) const override { - auto pkey = parsePrivateKey(key, type); - AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) throw Error("EVP_MD_CTX_new failed"); @@ -339,8 +323,6 @@ struct OpenSSLSecretKey : SecretKey std::unique_ptr toPublicKey() const override { - auto pkey = parsePrivateKey(key, type); - unsigned char * derBuf = nullptr; int derLen = i2d_PUBKEY(pkey.get(), &derBuf); if (derLen < 0) @@ -348,13 +330,13 @@ struct OpenSSLSecretKey : SecretKey std::string der((const char *) derBuf, derLen); OPENSSL_free(derBuf); - return std::make_unique(type, name, std::move(der)); + auto pubKey = parsePublicKey(der, type); + + return std::make_unique(type, name, std::move(der), std::move(pubKey)); } std::string toPEM() const override { - auto pkey = parsePrivateKey(key, type); - AutoBIO bio(BIO_new(BIO_s_mem())); if (!bio) throw Error("BIO_new failed"); @@ -385,7 +367,7 @@ std::unique_ptr SecretKey::parse(std::string_view s) type = KeyType::MLDSA87; else throw Error("secret key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); - return std::make_unique(type, name, std::move(key)); + return std::make_unique(type, name, std::move(key), std::move(pkey)); } else throw Error("secret key is not valid"); @@ -431,7 +413,7 @@ std::unique_ptr SecretKey::generate(std::string_view name, KeyType ty std::string der((const char *) derBuf, derLen); OPENSSL_free(derBuf); - return std::make_unique(type, name, std::move(der)); + return std::make_unique(type, name, std::move(der), std::move(pkey)); } default: @@ -456,7 +438,7 @@ std::unique_ptr PublicKey::parse(std::string_view s) type = KeyType::MLDSA87; else throw Error("public key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get())); - return std::make_unique(type, name, std::move(key)); + return std::make_unique(type, name, std::move(key), std::move(pkey)); } else throw Error("public key is not valid"); } catch (Error & e) { From d9eaf05b4634d1c05f63d5d206a5be0fe2447aba Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 22:36:43 +0200 Subject: [PATCH 11/14] Clean up generate() --- src/libutil/signature/local-keys.cc | 67 ++++++++++++++++------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 4c9438e97636..7ac9b6329e16 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -208,6 +208,16 @@ struct Ed25519SecretKey : SecretKey assert(key.size() == crypto_sign_SECRETKEYBYTES); } + static std::unique_ptr generate(std::string_view name) + { + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + unsigned char sk[crypto_sign_SECRETKEYBYTES]; + if (crypto_sign_keypair(pk, sk) != 0) + throw Error("key generation failed"); + + return std::make_unique(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + } + Signature signDetached(std::string_view data) const override { unsigned char sig[crypto_sign_BYTES]; @@ -286,6 +296,31 @@ struct OpenSSLSecretKey : SecretKey assert(type == KeyType::MLDSA44 || type == KeyType::MLDSA65 || type == KeyType::MLDSA87); } + static std::unique_ptr generate(std::string_view name, KeyType type) + { + auto typeS = toOpenSSLKeyType(type); + AutoEVP_PKEY_CTX ctx(EVP_PKEY_CTX_new_from_name(nullptr, typeS, nullptr)); + if (!ctx) + throw Error("EVP_PKEY_CTX_new_from_name failed for '%s'", typeS); + + if (EVP_PKEY_keygen_init(ctx.get()) <= 0) + throw Error("EVP_PKEY_keygen_init failed"); + + EVP_PKEY * rawPkey = nullptr; + if (EVP_PKEY_generate(ctx.get(), &rawPkey) <= 0) + throw Error("EVP_PKEY_generate failed"); + AutoEVP_PKEY pkey(rawPkey); + + unsigned char * derBuf = nullptr; + int derLen = i2d_PrivateKey(pkey.get(), &derBuf); + if (derLen < 0) + throw Error("i2d_PrivateKey failed"); + std::string der((const char *) derBuf, derLen); + OPENSSL_free(derBuf); + + return std::make_unique(type, name, std::move(der), std::move(pkey)); + } + Signature signDetached(std::string_view data) const override { AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); @@ -383,38 +418,12 @@ std::unique_ptr SecretKey::generate(std::string_view name, KeyType ty switch (type) { case KeyType::Ed25519: - unsigned char pk[crypto_sign_PUBLICKEYBYTES]; - unsigned char sk[crypto_sign_SECRETKEYBYTES]; - if (crypto_sign_keypair(pk, sk) != 0) - throw Error("key generation failed"); - - return std::make_unique(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + return Ed25519SecretKey::generate(name); case KeyType::MLDSA44: case KeyType::MLDSA65: - case KeyType::MLDSA87: { - auto typeS = toOpenSSLKeyType(type); - AutoEVP_PKEY_CTX ctx(EVP_PKEY_CTX_new_from_name(nullptr, typeS, nullptr)); - if (!ctx) - throw Error("EVP_PKEY_CTX_new_from_name failed for '%s'", typeS); - - if (EVP_PKEY_keygen_init(ctx.get()) <= 0) - throw Error("EVP_PKEY_keygen_init failed"); - - EVP_PKEY * rawPkey = nullptr; - if (EVP_PKEY_generate(ctx.get(), &rawPkey) <= 0) - throw Error("EVP_PKEY_generate failed"); - AutoEVP_PKEY pkey(rawPkey); - - unsigned char * derBuf = nullptr; - int derLen = i2d_PrivateKey(pkey.get(), &derBuf); - if (derLen < 0) - throw Error("i2d_PrivateKey failed"); - std::string der((const char *) derBuf, derLen); - OPENSSL_free(derBuf); - - return std::make_unique(type, name, std::move(der), std::move(pkey)); - } + case KeyType::MLDSA87: + return OpenSSLSecretKey::generate(name, type); default: unreachable(); From d4186f4b20a09590da0399015086eb7a7071267e Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 13 May 2026 22:38:04 +0200 Subject: [PATCH 12/14] Add FIXME --- src/libutil/include/nix/util/signature/local-keys.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 986f0547e1b7..4e88bce67076 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -50,6 +50,7 @@ enum KeyType { KeyType parseKeyType(std::string_view s); +// FIXME: remove this class. struct Key { const std::string name; From 9e4242f1c7db8498c2babfde4b6cf6126a82e478 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 14 May 2026 15:21:29 +0200 Subject: [PATCH 13/14] Improve help text --- .../include/nix/util/signature/local-keys.hh | 2 ++ src/libutil/signature/local-keys.cc | 28 +++++++++++-------- src/nix/sigs.cc | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/libutil/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 4e88bce67076..beebfeed375d 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -50,6 +50,8 @@ enum KeyType { KeyType parseKeyType(std::string_view s); +const StringSet & getKeyTypes(); + // FIXME: remove this class. struct Key { diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 7ac9b6329e16..0a835368a73e 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -137,17 +137,15 @@ Strings Signature::toStrings(const std::set & sigs) return res; } -KeyType parseKeyType(std::string_view s) +static std::unordered_map keyTypeMap{ + {"ed25519", KeyType::Ed25519}, + {"ml-dsa-44", KeyType::MLDSA44}, + {"ml-dsa-65", KeyType::MLDSA65}, + {"ml-dsa-87", KeyType::MLDSA87}, +}; + +const StringSet & getKeyTypes() { - static std::unordered_map keyTypeMap{ - {"ed25519", KeyType::Ed25519}, - {"ml-dsa-44", KeyType::MLDSA44}, - {"ml-dsa-65", KeyType::MLDSA65}, - {"ml-dsa-87", KeyType::MLDSA87}, - }; - auto i = keyTypeMap.find(s); - if (i != keyTypeMap.end()) - return i->second; static StringSet validKeyTypes = [] { StringSet s; for (const auto & [k, _] : keyTypeMap) { @@ -155,7 +153,15 @@ KeyType parseKeyType(std::string_view s) } return s; }(); - throw UsageError("unknown key type '%s'; valid key types are %s", s, concatStringsSep(", ", validKeyTypes)); + return validKeyTypes; +} + +KeyType parseKeyType(std::string_view s) +{ + auto i = keyTypeMap.find(s); + if (i != keyTypeMap.end()) + return i->second; + throw UsageError("unknown key type '%s'; valid key types are %s", s, concatStringsSep(", ", getKeyTypes())); } std::string Key::to_string() const diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index 2e34b2517a09..600c7b42903e 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -162,7 +162,7 @@ struct CmdKeyGenerateSecret : Command addFlag({ .longName = "key-type", - .description = "Type of key: `ed25519` or `ml-dsa-65`.", + .description = fmt("Type of key: one of %s.", concatStringsSep(", ", getKeyTypes())), .labels = {"type"}, .handler = {&keyType}, }); From bf1a60c29bd7d6309d78215ca780fba16978e3fa Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 14 May 2026 15:56:10 +0200 Subject: [PATCH 14/14] Add ml-dsa experimental feature --- src/libutil/experimental-features.cc | 10 +++++++++- .../include/nix/util/experimental-features.hh | 1 + src/libutil/signature/local-keys.cc | 13 +++++++++++-- tests/functional/signing.sh | 2 ++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index 334a410bb863..2f62a41c4716 100644 --- a/src/libutil/experimental-features.cc +++ b/src/libutil/experimental-features.cc @@ -25,7 +25,7 @@ struct ExperimentalFeatureDetails * feature, we either have no issue at all if few features are not added * at the end of the list, or a proper merge conflict if they are. */ -constexpr size_t numXpFeatures = 1 + static_cast(Xp::Provenance); +constexpr size_t numXpFeatures = 1 + static_cast(Xp::MLDSA); constexpr std::array xpFeatureDetails = {{ { @@ -305,6 +305,14 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "", }, + { + .tag = Xp::MLDSA, + .name = "ml-dsa", + .description = R"( + Enable support for ML-DSA keys and signatures. + )", + .trackingUrl = "", + }, }}; static_assert( diff --git a/src/libutil/include/nix/util/experimental-features.hh b/src/libutil/include/nix/util/experimental-features.hh index 85ca58e23da1..963c0270887c 100644 --- a/src/libutil/include/nix/util/experimental-features.hh +++ b/src/libutil/include/nix/util/experimental-features.hh @@ -41,6 +41,7 @@ enum struct ExperimentalFeature { WasmBuiltin, WasmDerivations, Provenance, + MLDSA, }; extern std::set stabilizedFeatures; diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 0a835368a73e..07b74e93559a 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -258,6 +258,9 @@ struct OpenSSLPublicKey : PublicKey bool verifyDetachedAnon(std::string_view data, const Signature & sig) const override { + if (!experimentalFeatureSettings.isEnabled(Xp::MLDSA)) + return false; + AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) throw Error("EVP_MD_CTX_new failed"); @@ -304,6 +307,8 @@ struct OpenSSLSecretKey : SecretKey static std::unique_ptr generate(std::string_view name, KeyType type) { + experimentalFeatureSettings.require(Xp::MLDSA); + auto typeS = toOpenSSLKeyType(type); AutoEVP_PKEY_CTX ctx(EVP_PKEY_CTX_new_from_name(nullptr, typeS, nullptr)); if (!ctx) @@ -329,6 +334,8 @@ struct OpenSSLSecretKey : SecretKey Signature signDetached(std::string_view data) const override { + experimentalFeatureSettings.require(Xp::MLDSA); + AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); if (!ctx) throw Error("EVP_MD_CTX_new failed"); @@ -364,6 +371,8 @@ struct OpenSSLSecretKey : SecretKey std::unique_ptr toPublicKey() const override { + experimentalFeatureSettings.require(Xp::MLDSA); + unsigned char * derBuf = nullptr; int derLen = i2d_PUBKEY(pkey.get(), &derBuf); if (derLen < 0) @@ -398,7 +407,7 @@ std::unique_ptr SecretKey::parse(std::string_view s) if (key.size() == crypto_sign_SECRETKEYBYTES) return std::make_unique(name, std::move(key)); - else if (auto pkey = parsePrivateKey(key)) { + else if (auto pkey = parsePrivateKey(key); experimentalFeatureSettings.isEnabled(Xp::MLDSA) && pkey) { KeyType type; if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) type = KeyType::MLDSA44; @@ -443,7 +452,7 @@ std::unique_ptr PublicKey::parse(std::string_view s) if (key.size() == crypto_sign_PUBLICKEYBYTES) return std::make_unique(name, std::move(key)); - else if (auto pkey = parsePublicKey(key)) { + else if (auto pkey = parsePublicKey(key); experimentalFeatureSettings.isEnabled(Xp::MLDSA) && pkey) { KeyType type; if (EVP_PKEY_is_a(pkey.get(), "ML-DSA-44") == 1) type = KeyType::MLDSA44; diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index e2321d6f12e2..103d0c70c5b0 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +experimental_features="ml-dsa" + source common.sh runTests() {