Skip to content

Commit ed5d521

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 ed5d521

11 files changed

Lines changed: 176 additions & 44 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-tests/local-keys.cc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ TEST(local_keys, signAndVerify)
2424
ASSERT_EQ(sig.keyName, "test-key-1");
2525
ASSERT_TRUE(pk->verifyDetached("hello world", sig));
2626

27-
auto sk2 = SecretKey::parse(sk->to_string());
27+
auto sk2 = SecretKey::parse(sk->to_string(), false);
2828
ASSERT_EQ(sk2->name, sk->name);
2929
ASSERT_EQ(sk2->key, sk->key);
3030

@@ -58,7 +58,7 @@ TEST(local_keys, rfc8032TestVector)
5858
auto skBytes = seed + pubKeyBytes;
5959
auto skString = "test:" + base64::encode(std::as_bytes(std::span<const char>{skBytes.data(), skBytes.size()}));
6060

61-
auto sk = SecretKey::parse(skString);
61+
auto sk = SecretKey::parse(skString, false);
6262
auto sig = sk->signDetached(message);
6363

6464
ASSERT_EQ(sig.keyName, "test");
@@ -157,7 +157,7 @@ TEST(local_keys, rfc6979EcdsaP384TestVector)
157157
"99ef4aeb15f178cea1fe40db2603138f130e740a19624526203b6351d0a3a94fa329c145786e679e7b82c71a38628ac8");
158158

159159
auto skString = "rfc6979-test:" + base64::encode(std::as_bytes(std::span<const char>{skDer.data(), skDer.size()}));
160-
auto sk = SecretKey::parse(skString);
160+
auto sk = SecretKey::parse(skString, false);
161161

162162
auto sig = sk->signDetached("sample");
163163
ASSERT_EQ(sig.keyName, "rfc6979-test");
@@ -198,7 +198,7 @@ runMlDsaAcvpTest(std::string_view variant, std::string_view derPrefixHex, size_t
198198
auto der = base16::decode(derPrefixHex) + sk;
199199
auto skString =
200200
std::string(variant) + ":" + base64::encode(std::as_bytes(std::span<const char>{der.data(), der.size()}));
201-
auto parsed = SecretKey::parse(skString);
201+
auto parsed = SecretKey::parse(skString, false);
202202

203203
auto sig = parsed->signDetached(message);
204204
ASSERT_EQ(sig.keyName, std::string(variant));

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: 110 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,69 @@ 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 (found == nullptr && !err.has_value() && !OSSL_STORE_eof(ctx)) {
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+
// Clean it up to avoid a leak.
184+
EVP_PKEY_free(found);
185+
found = nullptr;
186+
err = Error("multiple matches for OpenSSL store URI '%s'", uri);
187+
}
188+
}
189+
OSSL_STORE_INFO_free(info);
190+
}
191+
}
192+
OSSL_STORE_close(ctx);
193+
194+
if (err.has_value()) {
195+
throw err.value();
196+
}
197+
198+
return AutoEVP_PKEY(found);
199+
}
200+
201+
/**
202+
* Parse a DER-encoded PKCS#8 `PrivateKeyInfo` or a URI.
203+
* @param key the DER to parse, or a URI
204+
* @param isUri true if it is a URI
205+
*/
206+
AutoEVP_PKEY parsePrivateKey(std::string_view key, bool isUri)
126207
{
127-
auto p = (const unsigned char *) der.data();
128-
AutoEVP_PKEY pkey(d2i_AutoPrivateKey(nullptr, &p, der.size()));
129-
return pkey;
208+
if (isUri)
209+
return parseOsslStoreUri(key);
210+
else {
211+
auto p = (const unsigned char *) key.data();
212+
return AutoEVP_PKEY(d2i_AutoPrivateKey(nullptr, &p, key.size()));
213+
}
130214
}
131215

132216
/**
@@ -157,10 +241,10 @@ AutoEVP_PKEY parsePublicKey(std::string_view der, KeyType type)
157241

158242
Signature Signature::parse(std::string_view s)
159243
{
160-
auto [keyName, sig] = parseColonBase64(s, "signature");
244+
auto sig = parseColonBase64(s, "signature", false);
161245
return Signature{
162-
.keyName = std::move(keyName),
163-
.sig = std::move(sig),
246+
.keyName = std::move(sig.label),
247+
.sig = std::move(sig.data),
164248
};
165249
}
166250

@@ -477,18 +561,20 @@ struct OpenSSLSecretKey : SecretKey
477561
}
478562
};
479563

480-
std::unique_ptr<SecretKey> SecretKey::parse(std::string_view s)
564+
std::unique_ptr<SecretKey> SecretKey::parse(std::string_view s, bool forceUri = false)
481565
{
482566
try {
483-
auto [name, key] = parseColonBase64(s, "key");
567+
auto key = parseColonBase64(s, "key", experimentalFeatureSettings.isEnabled(Xp::Keystore));
568+
if (forceUri && !key.isUri)
569+
throw Error("secret key was not a URI");
484570

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) {
571+
if (!key.isUri && key.data.size() == crypto_sign_SECRETKEYBYTES)
572+
return std::make_unique<Ed25519SecretKey>(key.label, std::move(key.data));
573+
else if (auto pkey = parsePrivateKey(key.data, key.isUri); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) {
488574
auto type = detectOpenSSLKeyType(pkey.get());
489575
if (!type)
490576
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));
577+
return std::make_unique<OpenSSLSecretKey>(*type, key.label, std::move(key.data), std::move(pkey));
492578
} else
493579
throw Error("secret key is not valid");
494580

@@ -519,15 +605,15 @@ std::unique_ptr<SecretKey> SecretKey::generate(std::string_view name, KeyType ty
519605
std::unique_ptr<PublicKey> PublicKey::parse(std::string_view s)
520606
{
521607
try {
522-
auto [name, key] = parseColonBase64(s, "key");
608+
auto key = parseColonBase64(s, "key", false);
523609

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) {
610+
if (key.data.size() == crypto_sign_PUBLICKEYBYTES)
611+
return std::make_unique<Ed25519PublicKey>(key.label, std::move(key.data));
612+
else if (auto pkey = parsePublicKey(key.data); experimentalFeatureSettings.isEnabled(Xp::CNSA) && pkey) {
527613
auto type = detectOpenSSLKeyType(pkey.get());
528614
if (!type)
529615
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));
616+
return std::make_unique<OpenSSLPublicKey>(*type, key.label, std::move(key.data), std::move(pkey));
531617
} else
532618
throw Error("public key is not valid");
533619
} catch (Error & e) {

0 commit comments

Comments
 (0)