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); } 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..604b6e36ff40 100644 --- a/src/libstore/keys.cc +++ b/src/libstore/keys.cc @@ -11,14 +11,16 @@ 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 { - 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/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/include/nix/util/signature/local-keys.hh b/src/libutil/include/nix/util/signature/local-keys.hh index 789fb831f0f3..beebfeed375d 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -41,24 +41,27 @@ struct Signature auto operator<=>(const Signature &) const = default; }; +enum KeyType { + Ed25519, + MLDSA44, + MLDSA65, + MLDSA87, +}; + +KeyType parseKeyType(std::string_view s); + +const StringSet & getKeyTypes(); + +// FIXME: remove this class. struct Key { - 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(std::string_view name, std::string && key) : name(name) , key(std::move(key)) @@ -70,27 +73,35 @@ 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 std::unique_ptr toPublicKey() const; - static SecretKey generate(std::string_view name); + /** + * Return a PEM PKCS#8 encoding of this secret key. The Nix-specific + * key name is not included. Only ML-DSA keys are supported. + */ + virtual std::string toPEM() const; -private: - SecretKey(std::string_view name, std::string && key) - : Key(name, std::move(key)) - { - } + 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 @@ -104,20 +115,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; -private: - PublicKey(std::string_view name, std::string && key) - : Key(name, std::move(key)) - { - } - friend struct SecretKey; + /** + * Return a PEM SubjectPublicKeyInfo encoding of this public key. + * The Nix-specific key name is not included. Only ML-DSA keys are + * supported. + */ + 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 d03dbe975466..92765360a829 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,8 +54,8 @@ struct LocalSigner : Signer private: - SecretKey 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 51f94cee006a..07b74e93559a 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -1,16 +1,25 @@ #include #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>; +using AutoBIO = std::unique_ptr>; + /** * Parse a colon-separated string where the second part is Base64-encoded. * @@ -45,6 +54,53 @@ 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()})); } +const char * toOpenSSLKeyType(KeyType type) +{ + 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: + default: + throw Error("key type is not supported by OpenSSL"); + } +} + +/** + * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` and verify that the key is ML-DSA-*. + */ +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; +} + +/** + * Parse a DER-encoded `SubjectPublicKeyInfo` and verify that the key is ML-DSA-*. + */ +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; +} + +AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type) +{ + auto pkey = parsePublicKey(der); + + 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; +} + } // anonymous namespace Signature Signature::parse(std::string_view s) @@ -81,19 +137,31 @@ Strings Signature::toStrings(const std::set & sigs) return res; } -Key::Key(std::string_view s, bool sensitiveValue) +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() { - 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; - } + static StringSet validKeyTypes = [] { + StringSet s; + for (const auto & [k, _] : keyTypeMap) { + s.insert(std::string(k)); + } + return s; + }(); + 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 @@ -101,46 +169,307 @@ 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) - 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 +std::unique_ptr SecretKey::toPublicKey() 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), - }; + throw Error("conversion to public key is not implemented for this key type"); } -PublicKey SecretKey::toPublicKey() const +std::string SecretKey::toPEM() 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)); + throw Error("conversion to PEM is not implemented for this key type"); } -SecretKey SecretKey::generate(std::string_view name) +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 { - unsigned char pk[crypto_sign_PUBLICKEYBYTES]; - unsigned char sk[crypto_sign_SECRETKEYBYTES]; - if (crypto_sign_keypair(pk, sk) != 0) - throw Error("key generation failed"); + Ed25519SecretKey(std::string_view name, std::string && _key) + : SecretKey(name, std::move(_key)) + { + 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]; + 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), + }; + } + + std::unique_ptr toPublicKey() const override + { + unsigned char pk[crypto_sign_PUBLICKEYBYTES]; + crypto_sign_ed25519_sk_to_pk(pk, (unsigned char *) key.data()); + return std::make_unique(name, std::string((char *) pk, crypto_sign_PUBLICKEYBYTES)); + } +}; + +struct OpenSSLPublicKey : PublicKey +{ + KeyType type; + AutoEVP_PKEY pkey; + + 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); + } - return SecretKey(name, std::string((char *) sk, crypto_sign_SECRETKEYBYTES)); + 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"); + + 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 + { + 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; + AutoEVP_PKEY pkey; + + 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); + } + + 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) + 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 + { + experimentalFeatureSettings.require(Xp::MLDSA); + + AutoEVP_MD_CTX ctx(EVP_MD_CTX_new()); + if (!ctx) + throw Error("EVP_MD_CTX_new 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) + 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), + }; + } + + std::unique_ptr toPublicKey() const override + { + experimentalFeatureSettings.require(Xp::MLDSA); + + 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); + + auto pubKey = parsePublicKey(der, type); + + return std::make_unique(type, name, std::move(der), std::move(pubKey)); + } + + std::string toPEM() const override + { + 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); + } +}; + +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); experimentalFeatureSettings.isEnabled(Xp::MLDSA) && pkey) { + 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), std::move(pkey)); + } 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; + } } -PublicKey::PublicKey(std::string_view s) - : Key{s, false} +std::unique_ptr SecretKey::generate(std::string_view name, KeyType type) { - if (key.size() != crypto_sign_PUBLICKEYBYTES) - throw Error("public key is not valid"); + switch (type) { + + case KeyType::Ed25519: + return Ed25519SecretKey::generate(name); + + case KeyType::MLDSA44: + case KeyType::MLDSA65: + case KeyType::MLDSA87: + return OpenSSLSecretKey::generate(name, type); + + default: + unreachable(); + } +} + +std::unique_ptr PublicKey::parse(std::string_view s) +{ + 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); experimentalFeatureSettings.isEnabled(Xp::MLDSA) && pkey) { + 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), std::move(pkey)); + } 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 @@ -153,15 +482,13 @@ 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"); + // Unsupported key type, can't verify. + return false; +} - return crypto_sign_verify_detached( - (unsigned char *) sig.sig.data(), - (unsigned char *) data.data(), - data.size(), - (unsigned char *) key.data()) - == 0; +std::string PublicKey::toPEM() const +{ + throw Error("conversion to PEM is not implemented for this key type"); } bool verifyDetached(std::string_view data, const Signature & sig, const PublicKeys & publicKeys) @@ -170,7 +497,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 fff03fc30db1..493754c4e371 100644 --- a/src/libutil/signature/signer.cc +++ b/src/libutil/signature/signer.cc @@ -5,20 +5,20 @@ 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() { - return publicKey; + return *publicKey; } } // namespace nix 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/nix-store/nix-store.cc b/src/nix/nix-store/nix-store.cc index d6649d3e96dd..1cc4d7aa9c07 100644 --- a/src/nix/nix-store/nix-store.cc +++ b/src/nix/nix-store/nix-store.cc @@ -1101,10 +1101,10 @@ 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); + 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 c72204cea3d4..600c7b42903e 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}; @@ -149,6 +148,7 @@ static auto rCmdSign = registerCommand2({"store", "sign"}); struct CmdKeyGenerateSecret : Command { std::string keyName; + std::string keyType = "ed25519"; CmdKeyGenerateSecret() { @@ -159,6 +159,13 @@ struct CmdKeyGenerateSecret : Command .handler = {&keyName}, .required = true, }); + + addFlag({ + .longName = "key-type", + .description = fmt("Type of key: one of %s.", concatStringsSep(", ", getKeyTypes())), + .labels = {"type"}, + .handler = {&keyType}, + }); } std::string description() override @@ -176,7 +183,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()); } }; @@ -196,9 +203,50 @@ 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()); + } +}; + +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 + { + logger->stop(); + writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO))->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 + { + logger->stop(); + writeFull(getStandardOutput(), PublicKey::parse(drainFD(STDIN_FILENO))->toPEM()); } }; @@ -210,6 +258,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/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 bfa21fcff76b..103d0c70c5b0 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -1,15 +1,49 @@ #!/usr/bin/env bash +experimental_features="ml-dsa" + source common.sh +runTests() { + clearStoreIfPossible clearCache -nix-store --generate-binary-cache-key cache1.example.org "$TEST_ROOT"/sk1 "$TEST_ROOT"/pk1 +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) +# 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 "conversion to PEM is not implemented for this key type" + 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 + 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") @@ -120,3 +154,10 @@ for file in "$TEST_ROOT/storemultisig/"*.narinfo; do exit 1 fi done + +} + +runTests ed25519 +runTests ml-dsa-44 +runTests ml-dsa-65 +runTests ml-dsa-87