From e12369a68e3e68e652d47c50ebe1e3f8915b0341 Mon Sep 17 00:00:00 2001 From: Picnoir Date: Tue, 8 Apr 2025 17:41:38 +0200 Subject: [PATCH 1/2] store URI: introduce multiple signatures support Add a `secretKeyFiles` URI parameter in the store URIs receiving a coma-separated list of Nix signing keyfiles. For instance: nix copy --to "file:///tmp/store?secret-keys=/tmp/key1,/tmp/key2" \ "$(nix build --print-out-paths nixpkgs#hello)" The keys passed through this new store URI parameter are merged with the key specified in the `secretKeyFile` parameter, if any. We'd like to rotate the signing key for cache.nixos.org. To simplify the transition, we'd like to sign the new paths with two keys: the new one and the current one. With this, the cache can support nix configurations only trusting the new key and legacy configurations only trusting the current key. See https://github.com/NixOS/rfcs/pull/149 for more informations behind the motivation. --- src/libstore/binary-cache-store.cc | 19 +++++++++++++++---- .../include/nix/store/binary-cache-store.hh | 5 ++++- tests/functional/signing.sh | 10 ++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index 60bd68026..744bccef0 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -29,8 +29,17 @@ BinaryCacheStore::BinaryCacheStore(const Params & params) , Store(params) { if (secretKeyFile != "") - signer = std::make_unique( - SecretKey { readFile(secretKeyFile) }); + signers.push_back(std::make_unique( + SecretKey { readFile(secretKeyFile) })); + + if (secretKeyFiles != "") { + std::stringstream ss(secretKeyFiles); + Path keyPath; + while (std::getline(ss, keyPath, ',')) { + signers.push_back(std::make_unique( + SecretKey { readFile(keyPath) })); + } + } StringSink sink; sink << narVersionMagic1; @@ -270,9 +279,11 @@ ref BinaryCacheStore::addToStoreCommon( stats.narWriteCompressedBytes += fileSize; stats.narWriteCompressionTimeMs += duration; - /* Atomically write the NAR info file.*/ - if (signer) narInfo->sign(*this, *signer); + for (auto &signer: signers) { + narInfo->sign(*this, *signer); + } + /* Atomically write the NAR info file.*/ writeNarInfo(narInfo); stats.narInfoWrite++; diff --git a/src/libstore/include/nix/store/binary-cache-store.hh b/src/libstore/include/nix/store/binary-cache-store.hh index da4906d3f..445a10328 100644 --- a/src/libstore/include/nix/store/binary-cache-store.hh +++ b/src/libstore/include/nix/store/binary-cache-store.hh @@ -32,6 +32,9 @@ struct BinaryCacheStoreConfig : virtual StoreConfig const Setting secretKeyFile{this, "", "secret-key", "Path to the secret key used to sign the binary cache."}; + const Setting secretKeyFiles{this, "", "secret-keys", + "List of comma-separated paths to the secret keys used to sign the binary cache."}; + const Setting localNarCache{this, "", "local-nar-cache", "Path to a local cache of NARs fetched from this binary cache, used by commands such as `nix store cat`."}; @@ -57,7 +60,7 @@ class BinaryCacheStore : public virtual BinaryCacheStoreConfig, { private: - std::unique_ptr signer; + std::vector> signers; protected: diff --git a/tests/functional/signing.sh b/tests/functional/signing.sh index 8ec093a48..2893efec7 100755 --- a/tests/functional/signing.sh +++ b/tests/functional/signing.sh @@ -110,3 +110,13 @@ nix store verify --store "$TEST_ROOT"/store0 -r "$outPath2" --trusted-public-key # Content-addressed stuff can be copied without signatures. nix copy --to "$TEST_ROOT"/store0 "$outPathCA" + +# Test multiple signing keys +nix copy --to "file://$TEST_ROOT/storemultisig?secret-keys=$TEST_ROOT/sk1,$TEST_ROOT/sk2" "$outPath" +for file in "$TEST_ROOT/storemultisig/"*.narinfo; do + if [[ "$(grep -cE '^Sig: cache[1,2]\.example.org' "$file")" -ne 2 ]]; then + echo "ERROR: Cannot find cache1.example.org and cache2.example.org signatures in ${file}" + cat "${file}" + exit 1 + fi +done From 7ea536fe84d2fad2ce2f291910f51c159531273e Mon Sep 17 00:00:00 2001 From: Picnoir Date: Mon, 14 Apr 2025 10:30:47 +0200 Subject: [PATCH 2/2] Narinfo sign: multiple signatures variant This is a small optimization used when we're signing a narinfo for multiple keys in one go. Using this sign variant, we only compute the NAR fingerprint once, then sign it with all the keys. --- src/libstore/binary-cache-store.cc | 4 +--- src/libstore/include/nix/store/path-info.hh | 1 + src/libstore/path-info.cc | 8 ++++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libstore/binary-cache-store.cc b/src/libstore/binary-cache-store.cc index 744bccef0..bdc281044 100644 --- a/src/libstore/binary-cache-store.cc +++ b/src/libstore/binary-cache-store.cc @@ -279,9 +279,7 @@ ref BinaryCacheStore::addToStoreCommon( stats.narWriteCompressedBytes += fileSize; stats.narWriteCompressionTimeMs += duration; - for (auto &signer: signers) { - narInfo->sign(*this, *signer); - } + narInfo->sign(*this, signers); /* Atomically write the NAR info file.*/ writeNarInfo(narInfo); diff --git a/src/libstore/include/nix/store/path-info.hh b/src/libstore/include/nix/store/path-info.hh index 9bd493422..4691bfa95 100644 --- a/src/libstore/include/nix/store/path-info.hh +++ b/src/libstore/include/nix/store/path-info.hh @@ -144,6 +144,7 @@ struct ValidPathInfo : UnkeyedValidPathInfo { std::string fingerprint(const Store & store) const; void sign(const Store & store, const Signer & signer); + void sign(const Store & store, const std::vector> & signers); /** * @return The `ContentAddressWithReferences` that determines the diff --git a/src/libstore/path-info.cc b/src/libstore/path-info.cc index df20edb3b..5400a9da1 100644 --- a/src/libstore/path-info.cc +++ b/src/libstore/path-info.cc @@ -40,6 +40,14 @@ void ValidPathInfo::sign(const Store & store, const Signer & signer) sigs.insert(signer.signDetached(fingerprint(store))); } +void ValidPathInfo::sign(const Store & store, const std::vector> & signers) +{ + auto fingerprint = this->fingerprint(store); + for (auto & signer: signers) { + sigs.insert(signer->signDetached(fingerprint)); + } +} + std::optional ValidPathInfo::contentAddressWithReferences() const { if (! ca)