From 823e912d18daff628cb41927c527317f9b3be61c Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 17 Apr 2026 16:04:02 +0200 Subject: [PATCH 1/3] Move Deleter into a separate header --- src/libfetchers/git-lfs-fetch.cc | 1 + src/libfetchers/git-utils.cc | 1 + .../include/nix/fetchers/git-utils.hh | 11 ----------- src/libutil/include/nix/util/deleter.hh | 19 +++++++++++++++++++ src/libutil/include/nix/util/file-system.hh | 11 ++--------- src/libutil/include/nix/util/meson.build | 1 + 6 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 src/libutil/include/nix/util/deleter.hh diff --git a/src/libfetchers/git-lfs-fetch.cc b/src/libfetchers/git-lfs-fetch.cc index 4585e68e58ee..9d2fb928d603 100644 --- a/src/libfetchers/git-lfs-fetch.cc +++ b/src/libfetchers/git-lfs-fetch.cc @@ -8,6 +8,7 @@ #include "nix/util/util.hh" #include "nix/util/hash.hh" #include "nix/store/ssh.hh" +#include "nix/util/deleter.hh" #include #include diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc index 1911ebdd9dc5..4751130acca2 100644 --- a/src/libfetchers/git-utils.cc +++ b/src/libfetchers/git-utils.cc @@ -14,6 +14,7 @@ #include "nix/util/thread-pool.hh" #include "nix/util/pool.hh" #include "nix/util/executable-path.hh" +#include "nix/util/deleter.hh" #include #include diff --git a/src/libfetchers/include/nix/fetchers/git-utils.hh b/src/libfetchers/include/nix/fetchers/git-utils.hh index eada8745c3eb..e462bc9e7b54 100644 --- a/src/libfetchers/include/nix/fetchers/git-utils.hh +++ b/src/libfetchers/include/nix/fetchers/git-utils.hh @@ -136,17 +136,6 @@ struct GitRepo virtual Hash dereferenceSingletonDirectory(const Hash & oid) = 0; }; -// A helper to ensure that the `git_*_free` functions get called. -template -struct Deleter -{ - template - void operator()(T * p) const - { - del(p); - }; -}; - // A helper to ensure that we don't leak objects returned by libgit2. template struct Setter diff --git a/src/libutil/include/nix/util/deleter.hh b/src/libutil/include/nix/util/deleter.hh new file mode 100644 index 000000000000..7f349b10ee4b --- /dev/null +++ b/src/libutil/include/nix/util/deleter.hh @@ -0,0 +1,19 @@ +#pragma once + +namespace nix { + +/** + * A helper for `std::unique_ptr` that ensures that C APIs that require manual memory management get properly freed. The + * template parameter `del` is a function that takes a pointer and frees it. + */ +template +struct Deleter +{ + template + void operator()(T * p) const + { + del(p); + }; +}; + +} // namespace nix \ No newline at end of file diff --git a/src/libutil/include/nix/util/file-system.hh b/src/libutil/include/nix/util/file-system.hh index 067240812f9a..1a5cf10bf29e 100644 --- a/src/libutil/include/nix/util/file-system.hh +++ b/src/libutil/include/nix/util/file-system.hh @@ -12,6 +12,7 @@ #include "nix/util/types.hh" #include "nix/util/file-descriptor.hh" #include "nix/util/file-path.hh" +#include "nix/util/deleter.hh" #include #include @@ -374,15 +375,7 @@ public: } }; -struct DIRDeleter -{ - void operator()(DIR * dir) const - { - closedir(dir); - } -}; - -typedef std::unique_ptr AutoCloseDir; +typedef std::unique_ptr> AutoCloseDir; /** * Create a temporary directory. diff --git a/src/libutil/include/nix/util/meson.build b/src/libutil/include/nix/util/meson.build index 8682f9c4dc16..6061c57c2851 100644 --- a/src/libutil/include/nix/util/meson.build +++ b/src/libutil/include/nix/util/meson.build @@ -31,6 +31,7 @@ headers = [ config_pub_h ] + files( 'config-impl.hh', 'configuration.hh', 'current-process.hh', + 'deleter.hh', 'demangle.hh', 'english.hh', 'environment-variables.hh', From a8acb833826caa5c83612bf7ed26f4e6e3a6e386 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 17 Apr 2026 16:55:59 +0200 Subject: [PATCH 2/3] 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 ecebf028442cdd1d84682e802d594f091707abe8 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 6 May 2026 16:39:38 +0200 Subject: [PATCH 3/3] 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