diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index c00d56c62c3d..8a47d1f50033 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -27,14 +27,29 @@ namespace nix { BinaryCacheStore::BinaryCacheStore(Config & config) : config{config} { - if (!config.secretKeyFile.get().empty()) - signers.push_back(std::make_unique(SecretKey::parse(readFile(config.secretKeyFile.get())))); + auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore); + if (!config.secretKeyFile.get().empty()) { + auto isUri = keystoreEnabled && !std::get<0>(splitColon(config.secretKeyFile.get().string())).empty(); + signers.push_back( + std::make_unique( + SecretKey::parse( + isUri ? config.secretKeyFile.get().string() : readFile(config.secretKeyFile.get()), + isUri + ) + ) + ); + } if (config.secretKeyFiles != "") { std::stringstream ss(config.secretKeyFiles); std::string keyPath; while (std::getline(ss, keyPath, ',')) { - signers.push_back(std::make_unique(SecretKey::parse(readFile(keyPath)))); + auto isUri = keystoreEnabled && !std::get<0>(splitColon(keyPath)).empty(); + signers.push_back( + std::make_unique( + SecretKey::parse(isUri ? keyPath : readFile(keyPath), isUri) + ) + ); } } diff --git a/src/libstore/include/nix/store/binary-cache-store.hh b/src/libstore/include/nix/store/binary-cache-store.hh index fbd347e7684f..188f13a2e407 100644 --- a/src/libstore/include/nix/store/binary-cache-store.hh +++ b/src/libstore/include/nix/store/binary-cache-store.hh @@ -40,10 +40,10 @@ struct BinaryCacheStoreConfig : virtual StoreConfig )"}; Setting secretKeyFile{ - this, "", "secret-key", "Path to the secret key used to sign the binary cache."}; + this, "", "secret-key", "Path or URI to the secret key used to sign the binary cache."}; Setting secretKeyFiles{ - this, "", "secret-keys", "List of comma-separated paths to the secret keys used to sign the binary cache."}; + this, "", "secret-keys", "List of comma-separated paths or URIs to the secret keys used to sign the binary cache."}; Setting> localNarCache{ this, diff --git a/src/libstore/keys.cc b/src/libstore/keys.cc index 604b6e36ff40..41a3f9523420 100644 --- a/src/libstore/keys.cc +++ b/src/libstore/keys.cc @@ -1,5 +1,6 @@ #include "nix/util/file-system.hh" #include "nix/store/globals.hh" +#include "nix/util/signature/local-keys.hh" #include "nix/store/keys.hh" namespace nix { @@ -17,9 +18,11 @@ PublicKeys getDefaultPublicKeys() } // FIXME: keep secret keys in memory (see Store::signRealisation()). + auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore); for (const auto & secretKeyFile : settings.secretKeyFiles.get()) { try { - auto secretKey = SecretKey::parse(readFile(secretKeyFile)); + auto isUri = keystoreEnabled && !std::get<0>(splitColon(secretKeyFile)).empty(); + auto secretKey = SecretKey::parse(isUri ? secretKeyFile : readFile(secretKeyFile), isUri); publicKeys.emplace(secretKey->name, secretKey->toPublicKey()); } catch (SystemError & e) { /* Ignore unreadable key files. That's normal in a diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc index 1145c2574e56..3897a680d95e 100644 --- a/src/libstore/store-api.cc +++ b/src/libstore/store-api.cc @@ -1266,10 +1266,12 @@ void Store::signPathInfo(ValidPathInfo & info) { // FIXME: keep secret keys in memory. + auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore); auto secretKeyFiles = settings.secretKeyFiles; for (auto & secretKeyFile : secretKeyFiles.get()) { - LocalSigner signer(SecretKey::parse(readFile(secretKeyFile))); + auto isUri = keystoreEnabled && !std::get<0>(splitColon(secretKeyFile)).empty(); + LocalSigner signer(SecretKey::parse(isUri ? secretKeyFile : readFile(secretKeyFile), isUri)); info.sign(*this, signer); } } @@ -1278,10 +1280,12 @@ void Store::signRealisation(Realisation & realisation) { // FIXME: keep secret keys in memory. + auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore); auto secretKeyFiles = settings.secretKeyFiles; for (auto & secretKeyFile : secretKeyFiles.get()) { - LocalSigner signer(SecretKey::parse(readFile(secretKeyFile))); + auto isUri = keystoreEnabled && !std::get<0>(splitColon(secretKeyFile)).empty(); + LocalSigner signer(SecretKey::parse(isUri ? secretKeyFile : readFile(secretKeyFile), isUri)); realisation.sign(realisation.id, signer); } } diff --git a/src/libutil-tests/local-keys.cc b/src/libutil-tests/local-keys.cc index fc83ca8da825..e41c89a4aae2 100644 --- a/src/libutil-tests/local-keys.cc +++ b/src/libutil-tests/local-keys.cc @@ -24,7 +24,7 @@ TEST(local_keys, signAndVerify) ASSERT_EQ(sig.keyName, "test-key-1"); ASSERT_TRUE(pk->verifyDetached("hello world", sig)); - auto sk2 = SecretKey::parse(sk->to_string()); + auto sk2 = SecretKey::parse(sk->to_string(), false); ASSERT_EQ(sk2->name, sk->name); ASSERT_EQ(sk2->key, sk->key); @@ -58,7 +58,7 @@ TEST(local_keys, rfc8032TestVector) auto skBytes = seed + pubKeyBytes; auto skString = "test:" + base64::encode(std::as_bytes(std::span{skBytes.data(), skBytes.size()})); - auto sk = SecretKey::parse(skString); + auto sk = SecretKey::parse(skString, false); auto sig = sk->signDetached(message); ASSERT_EQ(sig.keyName, "test"); @@ -157,7 +157,7 @@ TEST(local_keys, rfc6979EcdsaP384TestVector) "99ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38628ac8"); auto skString = "rfc6979-test:" + base64::encode(std::as_bytes(std::span{skDer.data(), skDer.size()})); - auto sk = SecretKey::parse(skString); + auto sk = SecretKey::parse(skString, false); auto sig = sk->signDetached("sample"); ASSERT_EQ(sig.keyName, "rfc6979-test"); @@ -198,7 +198,7 @@ runMlDsaAcvpTest(std::string_view variant, std::string_view derPrefixHex, size_t auto der = base16::decode(derPrefixHex) + sk; auto skString = std::string(variant) + ":" + base64::encode(std::as_bytes(std::span{der.data(), der.size()})); - auto parsed = SecretKey::parse(skString); + auto parsed = SecretKey::parse(skString, false); auto sig = parsed->signDetached(message); ASSERT_EQ(sig.keyName, std::string(variant)); diff --git a/src/libutil/experimental-features.cc b/src/libutil/experimental-features.cc index bcd4cf54624a..5fa62f699826 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::CNSA); +constexpr size_t numXpFeatures = 1 + static_cast(Xp::Keystore); constexpr std::array xpFeatureDetails = {{ { @@ -315,6 +315,14 @@ constexpr std::array xpFeatureDetails )", .trackingUrl = "", }, + { + .tag = Xp::Keystore, + .name = "keystore", + .description = R"( + Enable support for loading signing keys from OpenSSL store URIs. + )", + .trackingUrl = "", + }, }}; static_assert( diff --git a/src/libutil/include/nix/util/experimental-features.hh b/src/libutil/include/nix/util/experimental-features.hh index f2bdecee5e54..67ec27876e78 100644 --- a/src/libutil/include/nix/util/experimental-features.hh +++ b/src/libutil/include/nix/util/experimental-features.hh @@ -42,6 +42,7 @@ enum struct ExperimentalFeature { WasmDerivations, Provenance, CNSA, + Keystore, }; 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 6d7620335174..0fe5358cded7 100644 --- a/src/libutil/include/nix/util/signature/local-keys.hh +++ b/src/libutil/include/nix/util/signature/local-keys.hh @@ -49,6 +49,8 @@ enum KeyType { ECDSAP384, }; +std::tuple splitColon(std::string_view s); + KeyType parseKeyType(std::string_view s); const StringSet & getKeyTypes(); @@ -78,7 +80,7 @@ struct SecretKey : Key virtual ~SecretKey() {}; - static std::unique_ptr parse(std::string_view s); + static std::unique_ptr parse(std::string_view s, bool forceUri); /** * Return a detached signature of the given string. diff --git a/src/libutil/signature/local-keys.cc b/src/libutil/signature/local-keys.cc index 49c8407dda09..430ad95f6472 100644 --- a/src/libutil/signature/local-keys.cc +++ b/src/libutil/signature/local-keys.cc @@ -1,12 +1,17 @@ +#include #include #include #include #include +#include #include #include +#include +#include #include "nix/util/base-n.hh" #include "nix/util/signature/local-keys.hh" +#include "nix/util/configuration.hh" #include "nix/util/json-utils.hh" #include "nix/util/util.hh" #include "nix/util/deleter.hh" @@ -20,32 +25,44 @@ using AutoEVP_PKEY_CTX = std::unique_ptr>; using AutoBIO = std::unique_ptr>; -std::string_view keyNamePart(std::string_view s) -{ - auto colon = s.find(':'); - return colon == std::string_view::npos ? std::string_view{} : s.substr(0, colon); -} +/** + * Some data with a label that can also be a URI to something. + * Used for keys and signatures. + */ +struct LabeledData { + std::string label; + std::string data; + bool isUri; +}; /** - * Parse a colon-separated string where the second part is Base64-encoded. + * Parse a colon-separated string where the second part is Base64-encoded or a URI. * * @param s The string to parse in the format `:`. * @param typeName Name of the type being parsed (for error messages). + * @param allowUri true if we should allow URIs * @return A pair of (name, decoded-data). */ -std::pair parseColonBase64(std::string_view s, std::string_view typeName) +LabeledData parseColonBase64OrUri(std::string_view s, std::string_view typeName, bool allowUri) { - size_t colon = s.find(':'); - if (colon == std::string::npos || colon == 0) + auto [name, rest] = splitColon(s); + if (name.empty() || rest.empty()) throw FormatError("%s is corrupt", typeName); - auto name = std::string(s.substr(0, colon)); - auto data = base64::decode(s.substr(colon + 1)); + bool isUri = false; + std::string data; + if (!allowUri || std::get<0>(splitColon(rest)).empty()) + data = base64::decode(rest); + else { + // Maybe a URI. If it's from a file, there may be a newline at the end, so trim it. + isUri = true; + data = boost::trim_right_copy(rest); + } - if (name.empty() || data.empty()) + if (data.empty()) throw FormatError("%s is corrupt", typeName); - return {std::move(name), std::move(data)}; + return {.label = std::string(name), .data = std::move(data), .isUri = isUri}; } /** @@ -120,13 +137,73 @@ std::optional detectOpenSSLKeyType(EVP_PKEY * pkey) } /** - * Parse a DER-encoded PKCS#8 `PrivateKeyInfo`. + * Parses an OpenSSL store URI, trying to find a single matching PKEY object. + * @param uri the URI + * @returns the PKEY */ -AutoEVP_PKEY parsePrivateKey(std::string_view der) +static AutoEVP_PKEY parseOsslStoreUri(std::string_view uri) { + std::optional err; + + // Per https://docs.openssl.org/3.5/man3/ERR_error_string/#description: + // buf must be at least 256 bytes long, so just make it twice that. + char errBuf[512] = {0}; + + OSSL_STORE_CTX *ctx = OSSL_STORE_open( + uri.data(), + UI_get_default_method(), + nullptr, + nullptr, + nullptr + ); + if (ctx == nullptr) { + auto errCode = ERR_get_error(); + ERR_error_string_n(errCode, errBuf, sizeof(errBuf)); + err = Error("error opening OpenSSL store URI '%s': %s (%x)", uri, errBuf, errCode); + } + + // Find the first matching private key object. If there are more than one, error. + EVP_PKEY *found = nullptr; + while (!err.has_value() && !OSSL_STORE_eof(ctx)) { + OSSL_STORE_INFO *info = OSSL_STORE_load(ctx); + if (info != nullptr) { + if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) { + // Use get1 over get0 because we want to take a reference to it. + // get0 will only last as long as 'info'. + if (found == nullptr) + found = OSSL_STORE_INFO_get1_PKEY(info); + else { + // Clean it up to avoid a leak. + EVP_PKEY_free(found); + found = nullptr; + err = Error("multiple matches for OpenSSL store URI '%s'", uri); + } + } + OSSL_STORE_INFO_free(info); + } + } + + if (ctx != nullptr) + OSSL_STORE_close(ctx); + + if (err.has_value()) + throw err.value(); + + return AutoEVP_PKEY(found); +} + +/** + * Parse a DER-encoded PKCS#8 `PrivateKeyInfo` or a URI. + * @param key the DER to parse, or a URI + * @param isUri true if it is a URI + */ +AutoEVP_PKEY parsePrivateKey(std::string_view key, bool isUri) { - auto p = (const unsigned char *) der.data(); - AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size())); - return pkey; + if (isUri) + return parseOsslStoreUri(key); + else { + auto p = (const unsigned char *) key.data(); + return AutoEVP_PKEY(d2i_AutoPrivateKey(nullptr, &p, key.size())); + } } /** @@ -157,10 +234,10 @@ AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type) Signature Signature::parse(std::string_view s) { - auto [keyName, sig] = parseColonBase64(s, "signature"); + auto sig = parseColonBase64OrUri(s, "signature", false); return Signature{ - .keyName = std::move(keyName), - .sig = std::move(sig), + .keyName = std::move(sig.label), + .sig = std::move(sig.data), }; } @@ -209,6 +286,20 @@ const StringSet & getKeyTypes() return validKeyTypes; } +/** + * Splits a string_view at ':', and returns the parts before and after. + * If there is no ':', will return the prefix as empty and the suffix as the whole string. + * @param s The string to parse. + * @return A pair of (name, data). + */ +std::tuple splitColon(std::string_view s) +{ + auto colon = s.find(':'); + auto prefix = colon == std::string_view::npos ? std::string_view{} : s.substr(0, colon); + auto suffix = colon == std::string_view::npos ? s : s.substr(colon + 1); + return {prefix, suffix}; +} + KeyType parseKeyType(std::string_view s) { auto i = keyTypeMap.find(s); @@ -477,23 +568,25 @@ struct OpenSSLSecretKey : SecretKey } }; -std::unique_ptr SecretKey::parse(std::string_view s) +std::unique_ptr SecretKey::parse(std::string_view s, bool forceUri = false) { try { - auto [name, key] = parseColonBase64(s, "key"); + auto key = parseColonBase64OrUri(s, "key", experimentalFeatureSettings.isEnabled(Xp::Keystore)); + if (forceUri && !key.isUri) + throw Error("secret key was not a URI"); - if (key.size() == crypto_sign_SECRETKEYBYTES) - return std::make_unique(name, std::move(key)); - else if (auto pkey = parsePrivateKey(key); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) { + if (!key.isUri && key.data.size() == crypto_sign_SECRETKEYBYTES) + return std::make_unique(key.label, std::move(key.data)); + else if (auto pkey = parsePrivateKey(key.data, key.isUri); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) { auto type = detectOpenSSLKeyType(pkey.get()); if (!type) 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)); + return std::make_unique(*type, key.label, std::move(key.data), std::move(pkey)); } else throw Error("secret key is not valid"); } catch (Error & e) { - e.addTrace({}, "while decoding key '%s'", keyNamePart(s)); + e.addTrace({}, "while decoding key '%s'", std::get<0>(splitColon(s))); throw; } } @@ -519,19 +612,19 @@ std::unique_ptr SecretKey::generate(std::string_view name, KeyType ty std::unique_ptr PublicKey::parse(std::string_view s) { try { - auto [name, key] = parseColonBase64(s, "key"); + auto key = parseColonBase64OrUri(s, "key", false); - if (key.size() == crypto_sign_PUBLICKEYBYTES) - return std::make_unique(name, std::move(key)); - else if (auto pkey = parsePublicKey(key); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) { + if (key.data.size() == crypto_sign_PUBLICKEYBYTES) + return std::make_unique(key.label, std::move(key.data)); + else if (auto pkey = parsePublicKey(key.data); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) { auto type = detectOpenSSLKeyType(pkey.get()); if (!type) 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)); + return std::make_unique(*type, key.label, std::move(key.data), std::move(pkey)); } else throw Error("public key is not valid"); } catch (Error & e) { - e.addTrace({}, "while decoding key '%s'", keyNamePart(s)); + e.addTrace({}, "while decoding key '%s'", std::get<0>(splitColon(s))); throw; } } diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index 600c7b42903e..e6d5c9d9a09d 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -1,3 +1,6 @@ +#include "nix/store/globals.hh" +#include "nix/util/config-global.hh" +#include "nix/util/configuration.hh" #include "nix/util/signals.hh" #include "nix/cmd/command.hh" #include "nix/main/shared.hh" @@ -100,17 +103,28 @@ static auto rCmdCopySigs = registerCommand2({"store", "copy-sigs"}) struct CmdSign : StorePathsCommand { std::filesystem::path secretKeyFile; + std::string secretKeyUri; CmdSign() { addFlag({ .longName = "key-file", .shortName = 'k', - .description = "File containing the secret signing key.", + .description = "File containing the secret signing key, or a URI to one.", .labels = {"file"}, .handler = {&secretKeyFile}, .completer = completePath, - .required = true, + .required = false, + }); + addFlag({ + .longName = "key-uri", + .shortName = 'u', + .description = "Name-prefixed URI pointing to an OpenSSL keystore-compatible " + "secret signing key (e.g. keyname:file:/etc/nix/my.key). " + "Enables feature 'keystore' automatically and overrides 'key-file'.", + .labels = {"uri"}, + .handler = {&secretKeyUri}, + .required = false, }); } @@ -121,7 +135,20 @@ struct CmdSign : StorePathsCommand void run(ref store, StorePaths && storePaths) override { - LocalSigner signer(SecretKey::parse(readFile(secretKeyFile))); + std::string secretKey; + if (secretKeyUri.empty()) { + if (secretKeyFile.empty()) { + throw UsageError("you must specify either a key file or URI"); + } else { + secretKey = readFile(secretKeyFile); + } + } else { + // Passing key-uri implies 'keystore'. + experimentalFeatureSettings.set("extra-experimental-features", "keystore"); + secretKey = secretKeyUri; + } + + LocalSigner signer(SecretKey::parse(secretKey, !secretKeyUri.empty())); size_t added{0}; @@ -204,7 +231,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), false)->toPublicKey()->to_string()); } }; @@ -225,7 +252,7 @@ struct CmdKeyConvertSecretToPem : Command void run() override { logger->stop(); - writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO))->toPEM()); + writeFull(getStandardOutput(), SecretKey::parse(drainFD(STDIN_FILENO), false)->toPEM()); } }; diff --git a/src/perl/lib/Nix/Store.xs b/src/perl/lib/Nix/Store.xs index d7493ceeff70..2e974916af4e 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::parse(secretKey_)->signDetached(msg).to_string(); + auto sig = SecretKey::parse(secretKey_, false)->signDetached(msg).to_string(); XPUSHs(sv_2mortal(newSVpv(sig.c_str(), sig.size()))); } catch (Error & e) { croak("%s", e.what());