Skip to content

Commit 9c1b587

Browse files
committed
experimental-features: add 'keystore'
Support OpenSSL keystores. Formatting is identical to our normal private key format (keyname:private-key-here) but OpenSSL will parse it as a URI (e.g. keyname:scheme:private-key-here). Add --key-uri in addition to --key-file that automatically enables the 'keystore' feature and performs the signature, without the need to put the private key in a file. This allows using PEM-formatted private keys if desired (e.g. mykey:file:/etc/nix/mykey.pem), in addition to PKCS#11 (e.g. mykey:pkcs11:id=%01;object=mykey;token=nixpkcs;type=private?foo). Tested using [nixpkcs](https://github.com/numinit/nixpkcs) by injecting an OpenSSL config into Nix that adds support for the PKCS#11 scheme with pkcs11-provider. Signing: ``` $ nix-shell -p openssl pkcs11-provider yubico-piv-tool $ openssl ecparam -genkey -name secp384r1 -noout -out p384.pem $ echo "p384:file:$(realpath p384.pem)" > p384.uri $ ./src/nix/nix store sign \ /nix/store/icq1cx1x7fjxim84sfanrv1j3vgb1qwp-pkcs11-provider-1.1 \ --key-file ./p384.key \ --extra-experimental-features 'cnsa keystore' $ nixpkcs-uri ca pkcs11:id=%02;token=YubiKey%20PIV%20%236108039;type=private?\ module-path=%2Fnix%2Fstore%2Fxcmf5v8y8vn5g5krsr2cyxp7hjmjgijc-yubico-piv-tool-2.7.2%2Flib%2Flibykcs11.so&\ pin-source=file%3A%2Fetc%2Fnixpkcs%2Fyubikeys%2F6108039%2Fuser.pin $ # generated with nixpkcs: $ export OPENSSL_CONF='/nix/store/gq3izqn2wflfr5cxan2nqz0nrww415h3-openssl-with-pkcs11.openssl.cnf' $ ./src/nix/nix store sign \ /nix/store/icq1cx1x7fjxim84sfanrv1j3vgb1qwp-pkcs11-provider-1.1 \ --key-uri yubikey-6108039:$(nixpkcs-uri ca) \ --extra-experimental-features cnsa ``` Verifying: ``` $ nix path-info --json --json-format 2 \ /nix/store/icq1cx1x7fjxim84sfanrv1j3vgb1qwp-pkcs11-provider-1.1 { "info": { "icq1cx1x7fjxim84sfanrv1j3vgb1qwp-pkcs11-provider-1.1": { "ca": null, "deriver": "1lparccpa6kjh2sc7n4hkd3vkr4n1c1h-pkcs11-provider-1.1.drv", "narHash": "sha256-iS7ETDBufxea39YxmAWeJ67NHcSuPAvONWe462pQpAk=", "narSize": 613744, "references": [ "1xj3zlgsv40gbhc0fxm0fphxsd4b7l7k-p11-kit-0.25.9", "daamdpmaz2vjvna55ccrc30qw3qb8h6d-glibc-2.40-66", "llswcygvgv9x2sa3z6j7i0g5iqqmn5gn-openssl-3.6.0" ], "registrationTime": 1779338946, "signatures": [ "cache.nixos.org-1:mULTk4OTkR3WVcGF1ClS3kJdQcRMlgbjy7GhH0inFKe9qi4Fw9kVDb/3SaYpbXTgQzfpQJypI91Jx9lq5JhwBg==", "p384:MGUCMQDXldyCdoiVKOp/Mqf1cDjZ1lmmNgmnedh6eJFeHFtMgck0EjsfFXnWe/TMH+Rc1boCMDhvOj9n8yUkkketqM1thIE6fqiFp5lUYZ3KEZ2l8B2q4Sm1V/3ASeVYzBJ7y5hLeQ==", "yubikey-6108039:MGUCMQCzcVYwFttNbQxcxflbIsmEcAEPCI2fiNZEissy0razpmZDMT0MdjuIsN8HYyFe7f8CMFVxVfVn0kqXE3C01RWIVLy5BslkFX3xYTI6w56ooSWo4jRZCbdVXoKWNO5YVJcvYg==" ], "storeDir": "/nix/store", "ultimate": false, "version": 2 } }, "storeDir": "/nix/store", "version": 2 } ```
1 parent 6b78b5d commit 9c1b587

9 files changed

Lines changed: 166 additions & 38 deletions

File tree

src/libstore/binary-cache-store.cc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,18 @@ namespace nix {
2727
BinaryCacheStore::BinaryCacheStore(Config & config)
2828
: config{config}
2929
{
30-
if (!config.secretKeyFile.get().empty())
31-
signers.push_back(std::make_unique<LocalSigner>(SecretKey::parse(readFile(config.secretKeyFile.get()))));
30+
auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore);
31+
if (!config.secretKeyFile.get().empty()) {
32+
auto isUri = keystoreEnabled && config.secretKeyFile.get().string().find(':') != std::string::npos;
33+
signers.push_back(std::make_unique<LocalSigner>(SecretKey::parse(isUri ? config.secretKeyFile.get().string() : readFile(config.secretKeyFile.get()), isUri && keystoreEnabled)));
34+
}
3235

3336
if (config.secretKeyFiles != "") {
3437
std::stringstream ss(config.secretKeyFiles);
3538
std::string keyPath;
3639
while (std::getline(ss, keyPath, ',')) {
37-
signers.push_back(std::make_unique<LocalSigner>(SecretKey::parse(readFile(keyPath))));
40+
auto isUri = keystoreEnabled && keyPath.find(':') != std::string::npos;
41+
signers.push_back(std::make_unique<LocalSigner>(SecretKey::parse(isUri ? keyPath : readFile(keyPath), isUri)));
3842
}
3943
}
4044

src/libstore/include/nix/store/binary-cache-store.hh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ struct BinaryCacheStoreConfig : virtual StoreConfig
4040
)"};
4141

4242
Setting<AbsolutePath> secretKeyFile{
43-
this, "", "secret-key", "Path to the secret key used to sign the binary cache."};
43+
this, "", "secret-key", "Path or URI to the secret key used to sign the binary cache."};
4444

4545
Setting<std::string> secretKeyFiles{
46-
this, "", "secret-keys", "List of comma-separated paths to the secret keys used to sign the binary cache."};
46+
this, "", "secret-keys", "List of comma-separated paths or URIs to the secret keys used to sign the binary cache."};
4747

4848
Setting<std::optional<AbsolutePath>> localNarCache{
4949
this,

src/libstore/keys.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ PublicKeys getDefaultPublicKeys()
1717
}
1818

1919
// FIXME: keep secret keys in memory (see Store::signRealisation()).
20+
auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore);
2021
for (const auto & secretKeyFile : settings.secretKeyFiles.get()) {
2122
try {
22-
auto secretKey = SecretKey::parse(readFile(secretKeyFile));
23+
auto isUri = keystoreEnabled && secretKeyFile.find(':') != std::string::npos;
24+
auto secretKey = SecretKey::parse(isUri ? secretKeyFile : readFile(secretKeyFile), isUri);
2325
publicKeys.emplace(secretKey->name, secretKey->toPublicKey());
2426
} catch (SystemError & e) {
2527
/* Ignore unreadable key files. That's normal in a

src/libstore/store-api.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,10 +1266,12 @@ void Store::signPathInfo(ValidPathInfo & info)
12661266
{
12671267
// FIXME: keep secret keys in memory.
12681268

1269+
auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore);
12691270
auto secretKeyFiles = settings.secretKeyFiles;
12701271

12711272
for (auto & secretKeyFile : secretKeyFiles.get()) {
1272-
LocalSigner signer(SecretKey::parse(readFile(secretKeyFile)));
1273+
auto isUri = keystoreEnabled && secretKeyFile.find(':') != std::string::npos;
1274+
LocalSigner signer(SecretKey::parse(isUri ? secretKeyFile : readFile(secretKeyFile), isUri));
12731275
info.sign(*this, signer);
12741276
}
12751277
}
@@ -1278,10 +1280,12 @@ void Store::signRealisation(Realisation & realisation)
12781280
{
12791281
// FIXME: keep secret keys in memory.
12801282

1283+
auto keystoreEnabled = experimentalFeatureSettings.isEnabled(Xp::Keystore);
12811284
auto secretKeyFiles = settings.secretKeyFiles;
12821285

12831286
for (auto & secretKeyFile : secretKeyFiles.get()) {
1284-
LocalSigner signer(SecretKey::parse(readFile(secretKeyFile)));
1287+
auto isUri = keystoreEnabled && secretKeyFile.find(':') != std::string::npos;
1288+
LocalSigner signer(SecretKey::parse(isUri ? secretKeyFile : readFile(secretKeyFile), isUri));
12851289
realisation.sign(realisation.id, signer);
12861290
}
12871291
}

src/libutil/experimental-features.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct ExperimentalFeatureDetails
2525
* feature, we either have no issue at all if few features are not added
2626
* at the end of the list, or a proper merge conflict if they are.
2727
*/
28-
constexpr size_t numXpFeatures = 1 + static_cast<size_t>(Xp::CNSA);
28+
constexpr size_t numXpFeatures = 1 + static_cast<size_t>(Xp::Keystore);
2929

3030
constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails = {{
3131
{
@@ -315,6 +315,14 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails
315315
)",
316316
.trackingUrl = "",
317317
},
318+
{
319+
.tag = Xp::Keystore,
320+
.name = "keystore",
321+
.description = R"(
322+
Enable support for loading signing keys from OpenSSL store URIs.
323+
)",
324+
.trackingUrl = "",
325+
},
318326
}};
319327

320328
static_assert(

src/libutil/include/nix/util/experimental-features.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ enum struct ExperimentalFeature {
4242
WasmDerivations,
4343
Provenance,
4444
CNSA,
45+
Keystore,
4546
};
4647

4748
extern std::set<std::string> stabilizedFeatures;

src/libutil/include/nix/util/signature/local-keys.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ struct SecretKey : Key
7878

7979
virtual ~SecretKey() {};
8080

81-
static std::unique_ptr<SecretKey> parse(std::string_view s);
81+
static std::unique_ptr<SecretKey> parse(std::string_view s, bool forceUri);
8282

8383
/**
8484
* Return a detached signature of the given string.

src/libutil/signature/local-keys.cc

Lines changed: 106 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
#include <boost/algorithm/string/trim.hpp>
12
#include <nlohmann/json.hpp>
23
#include <ranges>
34
#include <sodium.h>
45
#include <openssl/core_names.h>
6+
#include <openssl/err.h>
57
#include <openssl/evp.h>
68
#include <openssl/pem.h>
9+
#include <openssl/store.h>
10+
#include <openssl/ui.h>
711

812
#include "nix/util/base-n.hh"
913
#include "nix/util/signature/local-keys.hh"
14+
#include "nix/util/configuration.hh"
1015
#include "nix/util/json-utils.hh"
1116
#include "nix/util/util.hh"
1217
#include "nix/util/deleter.hh"
@@ -27,25 +32,48 @@ std::string_view keyNamePart(std::string_view s)
2732
}
2833

2934
/**
30-
* Parse a colon-separated string where the second part is Base64-encoded.
35+
* Some data with a label that can also be a URI to something.
36+
* Used for keys and signatures.
37+
*/
38+
struct LabeledData {
39+
std::string label;
40+
std::string data;
41+
bool isUri;
42+
};
43+
44+
/**
45+
* Parse a colon-separated string where the second part is Base64-encoded or a URI.
3146
*
3247
* @param s The string to parse in the format `<name>:<base64-data>`.
3348
* @param typeName Name of the type being parsed (for error messages).
49+
* @param allowUri true if we should allow URIs
3450
* @return A pair of (name, decoded-data).
3551
*/
36-
std::pair<std::string, std::string> parseColonBase64(std::string_view s, std::string_view typeName)
52+
LabeledData parseColonBase64(std::string_view s, std::string_view typeName, bool allowUri)
3753
{
3854
size_t colon = s.find(':');
3955
if (colon == std::string::npos || colon == 0)
4056
throw FormatError("%s is corrupt", typeName);
4157

4258
auto name = std::string(s.substr(0, colon));
43-
auto data = base64::decode(s.substr(colon + 1));
59+
auto encodedData = s.substr(colon + 1);
60+
if (name.empty() || encodedData.empty())
61+
throw FormatError("%s is corrupt", typeName);
4462

45-
if (name.empty() || data.empty())
63+
bool isUri = false;
64+
std::string data;
65+
if (!allowUri || encodedData.find(':') == std::string::npos) {
66+
data = base64::decode(encodedData);
67+
} else {
68+
// Maybe a URI.
69+
isUri = true;
70+
data = boost::trim_right_copy(encodedData);
71+
}
72+
73+
if (data.empty())
4674
throw FormatError("%s is corrupt", typeName);
4775

48-
return {std::move(name), std::move(data)};
76+
return {.label = std::move(name), .data = std::move(data), .isUri = isUri};
4977
}
5078

5179
/**
@@ -120,13 +148,65 @@ std::optional<KeyType> detectOpenSSLKeyType(EVP_PKEY * pkey)
120148
}
121149

122150
/**
123-
* Parse a DER-encoded PKCS#8 `PrivateKeyInfo`.
151+
* Parses an OpenSSL store URI, trying to find a single matching PKEY object.
152+
* @param uri the URI
153+
* @returns the PKEY
124154
*/
125-
AutoEVP_PKEY parsePrivateKey(std::string_view der)
155+
static AutoEVP_PKEY parseOsslStoreUri(std::string_view uri) {
156+
std::optional<Error> err;
157+
char errBuf[512];
158+
159+
OSSL_STORE_CTX *ctx = OSSL_STORE_open(
160+
uri.data(),
161+
UI_get_default_method(),
162+
nullptr,
163+
nullptr,
164+
nullptr
165+
);
166+
if (ctx == nullptr) {
167+
auto errCode = ERR_get_error();
168+
ERR_error_string_n(errCode, errBuf, sizeof(errBuf));
169+
err = Error("error opening OpenSSL store URI '%s': %s (%x)", uri, errBuf, errCode);
170+
}
171+
172+
// Find the first matching private key object. If there are more than one, error.
173+
EVP_PKEY *found = nullptr;
174+
while (!OSSL_STORE_eof(ctx) && found == nullptr && !err.has_value()) {
175+
OSSL_STORE_INFO *info = OSSL_STORE_load(ctx);
176+
if (info != nullptr) {
177+
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
178+
// Use get1 over get0 because we want to take a reference to it.
179+
// get0 will only last as long as 'info'.
180+
if (found == nullptr)
181+
found = OSSL_STORE_INFO_get1_PKEY(info);
182+
else
183+
err = Error("multiple matches for OpenSSL store URI '%s'", uri);
184+
}
185+
OSSL_STORE_INFO_free(info);
186+
}
187+
}
188+
OSSL_STORE_close(ctx);
189+
190+
if (err.has_value()) {
191+
throw err.value();
192+
}
193+
194+
return AutoEVP_PKEY(found);
195+
}
196+
197+
/**
198+
* Parse a DER-encoded PKCS#8 `PrivateKeyInfo` or a URI.
199+
* @param key the DER to parse, or a URI
200+
* @param isUri true if it is a URI
201+
*/
202+
AutoEVP_PKEY parsePrivateKey(std::string_view key, bool isUri)
126203
{
127-
auto p = (const unsigned char *) der.data();
128-
AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size()));
129-
return pkey;
204+
if (isUri)
205+
return parseOsslStoreUri(key);
206+
else {
207+
auto p = (const unsigned char *) key.data();
208+
return AutoEVP_PKEY(d2i_AutoPrivateKey(nullptr, &p, key.size()));
209+
}
130210
}
131211

132212
/**
@@ -157,10 +237,10 @@ AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type)
157237

158238
Signature Signature::parse(std::string_view s)
159239
{
160-
auto [keyName, sig] = parseColonBase64(s, "signature");
240+
auto sig = parseColonBase64(s, "signature", false);
161241
return Signature{
162-
.keyName = std::move(keyName),
163-
.sig = std::move(sig),
242+
.keyName = std::move(sig.label),
243+
.sig = std::move(sig.data),
164244
};
165245
}
166246

@@ -477,18 +557,20 @@ struct OpenSSLSecretKey : SecretKey
477557
}
478558
};
479559

480-
std::unique_ptr<SecretKey> SecretKey::parse(std::string_view s)
560+
std::unique_ptr<SecretKey> SecretKey::parse(std::string_view s, bool forceUri = false)
481561
{
482562
try {
483-
auto [name, key] = parseColonBase64(s, "key");
563+
auto key = parseColonBase64(s, "key", experimentalFeatureSettings.isEnabled(Xp::Keystore));
564+
if (forceUri && !key.isUri)
565+
throw Error("secret key was not a URI");
484566

485-
if (key.size() == crypto_sign_SECRETKEYBYTES)
486-
return std::make_unique<Ed25519SecretKey>(name, std::move(key));
487-
else if (auto pkey = parsePrivateKey(key); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) {
567+
if (!key.isUri && key.data.size() == crypto_sign_SECRETKEYBYTES)
568+
return std::make_unique<Ed25519SecretKey>(key.label, std::move(key.data));
569+
else if (auto pkey = parsePrivateKey(key.data, key.isUri); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) {
488570
auto type = detectOpenSSLKeyType(pkey.get());
489571
if (!type)
490572
throw Error("secret key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get()));
491-
return std::make_unique<OpenSSLSecretKey>(*type, name, std::move(key), std::move(pkey));
573+
return std::make_unique<OpenSSLSecretKey>(*type, key.label, std::move(key.data), std::move(pkey));
492574
} else
493575
throw Error("secret key is not valid");
494576

@@ -519,15 +601,15 @@ std::unique_ptr<SecretKey> SecretKey::generate(std::string_view name, KeyType ty
519601
std::unique_ptr<PublicKey> PublicKey::parse(std::string_view s)
520602
{
521603
try {
522-
auto [name, key] = parseColonBase64(s, "key");
604+
auto key = parseColonBase64(s, "key", false);
523605

524-
if (key.size() == crypto_sign_PUBLICKEYBYTES)
525-
return std::make_unique<Ed25519PublicKey>(name, std::move(key));
526-
else if (auto pkey = parsePublicKey(key); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) {
606+
if (key.data.size() == crypto_sign_PUBLICKEYBYTES)
607+
return std::make_unique<Ed25519PublicKey>(key.label, std::move(key.data));
608+
else if (auto pkey = parsePublicKey(key.data); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) {
527609
auto type = detectOpenSSLKeyType(pkey.get());
528610
if (!type)
529611
throw Error("public key has unsupported type '%s'", EVP_PKEY_get0_type_name(pkey.get()));
530-
return std::make_unique<OpenSSLPublicKey>(*type, name, std::move(key), std::move(pkey));
612+
return std::make_unique<OpenSSLPublicKey>(*type, key.label, std::move(key.data), std::move(pkey));
531613
} else
532614
throw Error("public key is not valid");
533615
} catch (Error & e) {

src/nix/sigs.cc

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#include "nix/store/globals.hh"
2+
#include "nix/util/config-global.hh"
3+
#include "nix/util/configuration.hh"
14
#include "nix/util/signals.hh"
25
#include "nix/cmd/command.hh"
36
#include "nix/main/shared.hh"
@@ -100,17 +103,28 @@ static auto rCmdCopySigs = registerCommand2<CmdCopySigs>({"store", "copy-sigs"})
100103
struct CmdSign : StorePathsCommand
101104
{
102105
std::filesystem::path secretKeyFile;
106+
std::string secretKeyUri;
103107

104108
CmdSign()
105109
{
106110
addFlag({
107111
.longName = "key-file",
108112
.shortName = 'k',
109-
.description = "File containing the secret signing key.",
113+
.description = "File containing the secret signing key, or a URI to one.",
110114
.labels = {"file"},
111115
.handler = {&secretKeyFile},
112116
.completer = completePath,
113-
.required = true,
117+
.required = false,
118+
});
119+
addFlag({
120+
.longName = "key-uri",
121+
.shortName = 'u',
122+
.description = "Name-prefixed URI pointing to an OpenSSL keystore-compatible "
123+
"secret signing key (e.g. keyname:file:/etc/nix/my.key). "
124+
"Enables feature 'keystore' automatically and overrides 'key-file'.",
125+
.labels = {"uri"},
126+
.handler = {&secretKeyUri},
127+
.required = false,
114128
});
115129
}
116130

@@ -121,7 +135,20 @@ struct CmdSign : StorePathsCommand
121135

122136
void run(ref<Store> store, StorePaths && storePaths) override
123137
{
124-
LocalSigner signer(SecretKey::parse(readFile(secretKeyFile)));
138+
std::string secretKey;
139+
if (secretKeyUri.empty()) {
140+
if (secretKeyFile.empty()) {
141+
throw UsageError("you must specify either a key file or URI");
142+
} else {
143+
secretKey = readFile(secretKeyFile);
144+
}
145+
} else {
146+
// Passing key-uri implies 'keystore'.
147+
experimentalFeatureSettings.set("extra-experimental-features", "keystore");
148+
secretKey = secretKeyUri;
149+
}
150+
151+
LocalSigner signer(SecretKey::parse(secretKey, !secretKeyUri.empty()));
125152

126153
size_t added{0};
127154

@@ -189,7 +216,7 @@ struct CmdKeyGenerateSecret : Command
189216

190217
struct CmdKeyConvertSecretToPublic : Command
191218
{
192-
std::string description() override
219+
std::string description() overridE
193220
{
194221
return "generate a public key for verifying store paths from a secret key read from standard input";
195222
}

0 commit comments

Comments
 (0)