Signer infrastructure: Prep for #9076

This sets up infrastructure in libutil to allow for signing other than
by a secret key in memory. #9076 uses this to implement remote signing.

(Split from that PR to allow reviewing in smaller chunks.)

Co-Authored-By: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
John Ericson 2024-01-03 15:02:20 -05:00
parent 315aade89d
commit 12bb8cdd38
24 changed files with 233 additions and 70 deletions

View File

@ -12,7 +12,6 @@
#include "realisation.hh" #include "realisation.hh"
#include "globals.hh" #include "globals.hh"
#include "store-api.hh" #include "store-api.hh"
#include "crypto.hh"
#include "posix-source-accessor.hh" #include "posix-source-accessor.hh"
#include <sodium.h> #include <sodium.h>

View File

@ -28,7 +28,8 @@ BinaryCacheStore::BinaryCacheStore(const Params & params)
, Store(params) , Store(params)
{ {
if (secretKeyFile != "") if (secretKeyFile != "")
secretKey = std::unique_ptr<SecretKey>(new SecretKey(readFile(secretKeyFile))); signer = std::make_unique<LocalSigner>(
SecretKey { readFile(secretKeyFile) });
StringSink sink; StringSink sink;
sink << narVersionMagic1; sink << narVersionMagic1;
@ -274,7 +275,7 @@ ref<const ValidPathInfo> BinaryCacheStore::addToStoreCommon(
stats.narWriteCompressionTimeMs += duration; stats.narWriteCompressionTimeMs += duration;
/* Atomically write the NAR info file.*/ /* Atomically write the NAR info file.*/
if (secretKey) narInfo->sign(*this, *secretKey); if (signer) narInfo->sign(*this, *signer);
writeNarInfo(narInfo); writeNarInfo(narInfo);

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
///@file ///@file
#include "crypto.hh" #include "signature/local-keys.hh"
#include "store-api.hh" #include "store-api.hh"
#include "log-store.hh" #include "log-store.hh"
@ -57,8 +57,7 @@ class BinaryCacheStore : public virtual BinaryCacheStoreConfig,
{ {
private: private:
std::unique_ptr<Signer> signer;
std::unique_ptr<SecretKey> secretKey;
protected: protected:

View File

@ -15,8 +15,6 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <sodium/core.h>
#ifdef __GLIBC__ #ifdef __GLIBC__
# include <gnu/lib-names.h> # include <gnu/lib-names.h>
# include <nss.h> # include <nss.h>
@ -409,9 +407,6 @@ void initLibStore() {
initLibUtil(); initLibUtil();
if (sodium_init() == -1)
throw Error("could not initialise libsodium");
loadConfFile(); loadConfFile();
preloadNSS(); preloadNSS();

31
src/libstore/keys.cc Normal file
View File

@ -0,0 +1,31 @@
#include "file-system.hh"
#include "globals.hh"
#include "keys.hh"
namespace nix {
PublicKeys getDefaultPublicKeys()
{
PublicKeys publicKeys;
// FIXME: filter duplicates
for (auto s : settings.trustedPublicKeys.get()) {
PublicKey key(s);
publicKeys.emplace(key.name, key);
}
for (auto secretKeyFile : settings.secretKeyFiles.get()) {
try {
SecretKey secretKey(readFile(secretKeyFile));
publicKeys.emplace(secretKey.name, secretKey.toPublicKey());
} catch (SysError & e) {
/* Ignore unreadable key files. That's normal in a
multi-user installation. */
}
}
return publicKeys;
}
}

10
src/libstore/keys.hh Normal file
View File

@ -0,0 +1,10 @@
#pragma once
///@file
#include "signature/local-keys.hh"
namespace nix {
PublicKeys getDefaultPublicKeys();
}

View File

@ -14,6 +14,7 @@
#include "signals.hh" #include "signals.hh"
#include "posix-fs-canonicalise.hh" #include "posix-fs-canonicalise.hh"
#include "posix-source-accessor.hh" #include "posix-source-accessor.hh"
#include "keys.hh"
#include <iostream> #include <iostream>
#include <algorithm> #include <algorithm>
@ -1578,7 +1579,8 @@ void LocalStore::signRealisation(Realisation & realisation)
for (auto & secretKeyFile : secretKeyFiles.get()) { for (auto & secretKeyFile : secretKeyFiles.get()) {
SecretKey secretKey(readFile(secretKeyFile)); SecretKey secretKey(readFile(secretKeyFile));
realisation.sign(secretKey); LocalSigner signer(std::move(secretKey));
realisation.sign(signer);
} }
} }
@ -1590,7 +1592,8 @@ void LocalStore::signPathInfo(ValidPathInfo & info)
for (auto & secretKeyFile : secretKeyFiles.get()) { for (auto & secretKeyFile : secretKeyFiles.get()) {
SecretKey secretKey(readFile(secretKeyFile)); SecretKey secretKey(readFile(secretKeyFile));
info.sign(*this, secretKey); LocalSigner signer(std::move(secretKey));
info.sign(*this, signer);
} }
} }

View File

@ -8,7 +8,7 @@ libstore_SOURCES := $(wildcard $(d)/*.cc $(d)/builtins/*.cc $(d)/build/*.cc)
libstore_LIBS = libutil libstore_LIBS = libutil
libstore_LDFLAGS += $(SQLITE3_LIBS) $(LIBCURL_LIBS) $(SODIUM_LIBS) -pthread libstore_LDFLAGS += $(SQLITE3_LIBS) $(LIBCURL_LIBS) -pthread
ifdef HOST_LINUX ifdef HOST_LINUX
libstore_LDFLAGS += -ldl libstore_LDFLAGS += -ldl
endif endif

View File

@ -38,9 +38,9 @@ std::string ValidPathInfo::fingerprint(const Store & store) const
} }
void ValidPathInfo::sign(const Store & store, const SecretKey & secretKey) void ValidPathInfo::sign(const Store & store, const Signer & signer)
{ {
sigs.insert(secretKey.signDetached(fingerprint(store))); sigs.insert(signer.signDetached(fingerprint(store)));
} }
std::optional<ContentAddressWithReferences> ValidPathInfo::contentAddressWithReferences() const std::optional<ContentAddressWithReferences> ValidPathInfo::contentAddressWithReferences() const

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
///@file ///@file
#include "crypto.hh" #include "signature/signer.hh"
#include "path.hh" #include "path.hh"
#include "hash.hh" #include "hash.hh"
#include "content-address.hh" #include "content-address.hh"
@ -107,7 +107,7 @@ struct ValidPathInfo : UnkeyedValidPathInfo {
*/ */
std::string fingerprint(const Store & store) const; std::string fingerprint(const Store & store) const;
void sign(const Store & store, const SecretKey & secretKey); void sign(const Store & store, const Signer & signer);
/** /**
* @return The `ContentAddressWithReferences` that determines the * @return The `ContentAddressWithReferences` that determines the

View File

@ -1,7 +1,5 @@
#include "store-dir-config.hh" #include "store-dir-config.hh"
#include <sodium.h>
namespace nix { namespace nix {
static void checkName(std::string_view path, std::string_view name) static void checkName(std::string_view path, std::string_view name)
@ -49,9 +47,7 @@ StorePath StorePath::dummy("ffffffffffffffffffffffffffffffff-x");
StorePath StorePath::random(std::string_view name) StorePath StorePath::random(std::string_view name)
{ {
Hash hash(HashAlgorithm::SHA1); return StorePath(Hash::random(HashAlgorithm::SHA1), name);
randombytes_buf(hash.hash, hash.hashSize);
return StorePath(hash, name);
} }
StorePath StoreDirConfig::parseStorePath(std::string_view path) const StorePath StoreDirConfig::parseStorePath(std::string_view path) const

View File

@ -1,6 +1,7 @@
#include "realisation.hh" #include "realisation.hh"
#include "store-api.hh" #include "store-api.hh"
#include "closure.hh" #include "closure.hh"
#include "signature/local-keys.hh"
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
namespace nix { namespace nix {
@ -113,9 +114,9 @@ std::string Realisation::fingerprint() const
return serialized.dump(); return serialized.dump();
} }
void Realisation::sign(const SecretKey & secretKey) void Realisation::sign(const Signer &signer)
{ {
signatures.insert(secretKey.signDetached(fingerprint())); signatures.insert(signer.signDetached(fingerprint()));
} }
bool Realisation::checkSignature(const PublicKeys & publicKeys, const std::string & sig) const bool Realisation::checkSignature(const PublicKeys & publicKeys, const std::string & sig) const

View File

@ -8,7 +8,7 @@
#include "derived-path.hh" #include "derived-path.hh"
#include <nlohmann/json_fwd.hpp> #include <nlohmann/json_fwd.hpp>
#include "comparator.hh" #include "comparator.hh"
#include "crypto.hh" #include "signature/signer.hh"
namespace nix { namespace nix {
@ -64,7 +64,7 @@ struct Realisation {
static Realisation fromJSON(const nlohmann::json& json, const std::string& whence); static Realisation fromJSON(const nlohmann::json& json, const std::string& whence);
std::string fingerprint() const; std::string fingerprint() const;
void sign(const SecretKey &); void sign(const Signer &);
bool checkSignature(const PublicKeys & publicKeys, const std::string & sig) const; bool checkSignature(const PublicKeys & publicKeys, const std::string & sig) const;
size_t checkSignatures(const PublicKeys & publicKeys) const; size_t checkSignatures(const PublicKeys & publicKeys) const;

View File

@ -1,4 +1,4 @@
#include "crypto.hh" #include "signature/local-keys.hh"
#include "source-accessor.hh" #include "source-accessor.hh"
#include "globals.hh" #include "globals.hh"
#include "derived-path.hh" #include "derived-path.hh"

View File

@ -14,6 +14,8 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <fcntl.h> #include <fcntl.h>
#include <sodium.h>
namespace nix { namespace nix {
static size_t regularHashSize(HashAlgorithm type) { static size_t regularHashSize(HashAlgorithm type) {
@ -261,6 +263,13 @@ Hash::Hash(std::string_view rest, HashAlgorithm algo, bool isSRI)
throw BadHash("hash '%s' has wrong length for hash algorithm '%s'", rest, printHashAlgo(this->algo)); throw BadHash("hash '%s' has wrong length for hash algorithm '%s'", rest, printHashAlgo(this->algo));
} }
Hash Hash::random(HashAlgorithm algo)
{
Hash hash(algo);
randombytes_buf(hash.hash, hash.hashSize);
return hash;
}
Hash newHashAllowEmpty(std::string_view hashStr, std::optional<HashAlgorithm> ha) Hash newHashAllowEmpty(std::string_view hashStr, std::optional<HashAlgorithm> ha)
{ {
if (hashStr.empty()) { if (hashStr.empty()) {

View File

@ -5,7 +5,6 @@
#include "serialise.hh" #include "serialise.hh"
#include "file-system.hh" #include "file-system.hh"
namespace nix { namespace nix {
@ -143,6 +142,11 @@ public:
} }
static Hash dummy; static Hash dummy;
/**
* @return a random hash with hash algorithm `algo`
*/
static Hash random(HashAlgorithm algo);
}; };
/** /**

View File

@ -4,14 +4,17 @@ libutil_NAME = libnixutil
libutil_DIR := $(d) libutil_DIR := $(d)
libutil_SOURCES := $(wildcard $(d)/*.cc) libutil_SOURCES := $(wildcard $(d)/*.cc $(d)/signature/*.cc)
libutil_CXXFLAGS += -I src/libutil libutil_CXXFLAGS += -I src/libutil
libutil_LDFLAGS += -pthread $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context libutil_LDFLAGS += -pthread $(LIBCURL_LIBS) $(SODIUM_LIBS) $(OPENSSL_LIBS) $(LIBBROTLI_LIBS) $(LIBARCHIVE_LIBS) $(BOOST_LDFLAGS) -lboost_context
$(foreach i, $(wildcard $(d)/args/*.hh), \ $(foreach i, $(wildcard $(d)/args/*.hh), \
$(eval $(call install-file-in, $(i), $(includedir)/nix/args, 0644))) $(eval $(call install-file-in, $(i), $(includedir)/nix/args, 0644)))
$(foreach i, $(wildcard $(d)/signature/*.hh), \
$(eval $(call install-file-in, $(i), $(includedir)/nix/signature, 0644)))
ifeq ($(HAVE_LIBCPUID), 1) ifeq ($(HAVE_LIBCPUID), 1)
libutil_LDFLAGS += -lcpuid libutil_LDFLAGS += -lcpuid

View File

@ -1,13 +1,12 @@
#include "crypto.hh" #include "signature/local-keys.hh"
#include "file-system.hh" #include "file-system.hh"
#include "util.hh" #include "util.hh"
#include "globals.hh"
#include <sodium.h> #include <sodium.h>
namespace nix { namespace nix {
static std::pair<std::string_view, std::string_view> split(std::string_view s) BorrowedCryptoValue BorrowedCryptoValue::parse(std::string_view s)
{ {
size_t colon = s.find(':'); size_t colon = s.find(':');
if (colon == std::string::npos || colon == 0) if (colon == std::string::npos || colon == 0)
@ -17,10 +16,10 @@ static std::pair<std::string_view, std::string_view> split(std::string_view s)
Key::Key(std::string_view s) Key::Key(std::string_view s)
{ {
auto ss = split(s); auto ss = BorrowedCryptoValue::parse(s);
name = ss.first; name = ss.name;
key = ss.second; key = ss.payload;
if (name == "" || key == "") if (name == "" || key == "")
throw Error("secret key is corrupt"); throw Error("secret key is corrupt");
@ -73,45 +72,34 @@ PublicKey::PublicKey(std::string_view s)
throw Error("public key is not valid"); throw Error("public key is not valid");
} }
bool verifyDetached(const std::string & data, const std::string & sig, bool PublicKey::verifyDetached(std::string_view data, std::string_view sig) const
const PublicKeys & publicKeys)
{ {
auto ss = split(sig); auto ss = BorrowedCryptoValue::parse(sig);
auto key = publicKeys.find(std::string(ss.first)); if (ss.name != std::string_view { name }) return false;
if (key == publicKeys.end()) return false;
auto sig2 = base64Decode(ss.second); return verifyDetachedAnon(data, ss.payload);
}
bool PublicKey::verifyDetachedAnon(std::string_view data, std::string_view sig) const
{
auto sig2 = base64Decode(sig);
if (sig2.size() != crypto_sign_BYTES) if (sig2.size() != crypto_sign_BYTES)
throw Error("signature is not valid"); throw Error("signature is not valid");
return crypto_sign_verify_detached((unsigned char *) sig2.data(), return crypto_sign_verify_detached((unsigned char *) sig2.data(),
(unsigned char *) data.data(), data.size(), (unsigned char *) data.data(), data.size(),
(unsigned char *) key->second.key.data()) == 0; (unsigned char *) key.data()) == 0;
} }
PublicKeys getDefaultPublicKeys() bool verifyDetached(std::string_view data, std::string_view sig, const PublicKeys & publicKeys)
{ {
PublicKeys publicKeys; auto ss = BorrowedCryptoValue::parse(sig);
// FIXME: filter duplicates auto key = publicKeys.find(std::string(ss.name));
if (key == publicKeys.end()) return false;
for (auto s : settings.trustedPublicKeys.get()) { return key->second.verifyDetachedAnon(data, ss.payload);
PublicKey key(s);
publicKeys.emplace(key.name, key);
}
for (auto secretKeyFile : settings.secretKeyFiles.get()) {
try {
SecretKey secretKey(readFile(secretKeyFile));
publicKeys.emplace(secretKey.name, secretKey.toPublicKey());
} catch (SysError & e) {
/* Ignore unreadable key files. That's normal in a
multi-user installation. */
}
}
return publicKeys;
} }
} }

View File

@ -7,6 +7,25 @@
namespace nix { namespace nix {
/**
* Except where otherwise noted, Nix serializes keys and signatures in
* the form:
*
* ```
* <name>:<key/signature-in-Base64>
* ```
*/
struct BorrowedCryptoValue {
std::string_view name;
std::string_view payload;
/**
* This splits on the colon, the user can then separated decode the
* Base64 payload separately.
*/
static BorrowedCryptoValue parse(std::string_view);
};
struct Key struct Key
{ {
std::string name; std::string name;
@ -49,21 +68,36 @@ struct PublicKey : Key
{ {
PublicKey(std::string_view data); PublicKey(std::string_view data);
/**
* @return true iff `sig` and this key's names match, and `sig` is a
* correct signature over `data` using the given public key.
*/
bool verifyDetached(std::string_view data, std::string_view sigs) const;
/**
* @return true iff `sig` is a correct signature over `data` using the
* given public key.
*
* @param just the Base64 signature itself, not a colon-separated pair of a
* public key name and signature.
*/
bool verifyDetachedAnon(std::string_view data, std::string_view sigs) const;
private: private:
PublicKey(std::string_view name, std::string && key) PublicKey(std::string_view name, std::string && key)
: Key(name, std::move(key)) { } : Key(name, std::move(key)) { }
friend struct SecretKey; friend struct SecretKey;
}; };
/**
* Map from key names to public keys
*/
typedef std::map<std::string, PublicKey> PublicKeys; typedef std::map<std::string, PublicKey> PublicKeys;
/** /**
* @return true iff sig is a correct signature over data using one * @return true iff sig is a correct signature over data using one
* of the given public keys. * of the given public keys.
*/ */
bool verifyDetached(const std::string & data, const std::string & sig, bool verifyDetached(std::string_view data, std::string_view sig, const PublicKeys & publicKeys);
const PublicKeys & publicKeys);
PublicKeys getDefaultPublicKeys();
} }

View File

@ -0,0 +1,23 @@
#include "signature/signer.hh"
#include "error.hh"
#include <sodium.h>
namespace nix {
LocalSigner::LocalSigner(SecretKey && privateKey)
: privateKey(privateKey)
, publicKey(privateKey.toPublicKey())
{ }
std::string LocalSigner::signDetached(std::string_view s) const
{
return privateKey.signDetached(s);
}
const PublicKey & LocalSigner::getPublicKey()
{
return publicKey;
}
}

View File

@ -0,0 +1,61 @@
#pragma once
#include "types.hh"
#include "signature/local-keys.hh"
#include <map>
#include <optional>
namespace nix {
/**
* An abstract signer
*
* Derive from this class to implement a custom signature scheme.
*
* It is only necessary to implement signature of bytes and provide a
* public key.
*/
struct Signer
{
virtual ~Signer() = default;
/**
* Sign the given data, creating a (detached) signature.
*
* @param data data to be signed.
*
* @return the [detached
* signature](https://en.wikipedia.org/wiki/Detached_signature),
* i.e. just the signature itself without a copy of the signed data.
*/
virtual std::string signDetached(std::string_view data) const = 0;
/**
* View the public key associated with this `Signer`.
*/
virtual const PublicKey & getPublicKey() = 0;
};
using Signers = std::map<std::string, Signer*>;
/**
* Local signer
*
* The private key is held in this machine's RAM
*/
struct LocalSigner : Signer
{
LocalSigner(SecretKey && privateKey);
std::string signDetached(std::string_view s) const override;
const PublicKey & getPublicKey() override;
private:
SecretKey privateKey;
PublicKey publicKey;
};
}

View File

@ -7,6 +7,7 @@
#include <grp.h> #include <grp.h>
#include <regex> #include <regex>
#include <sodium.h>
namespace nix { namespace nix {
@ -28,6 +29,9 @@ void initLibUtil() {
} }
// This is not actually the main point of this check, but let's make sure anyway: // This is not actually the main point of this check, but let's make sure anyway:
assert(caught); assert(caught);
if (sodium_init() == -1)
throw Error("could not initialise libsodium");
} }
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////

View File

@ -112,7 +112,7 @@ struct CmdSign : StorePathsCommand
std::string description() override std::string description() override
{ {
return "sign store paths"; return "sign store paths with a local key";
} }
void run(ref<Store> store, StorePaths && storePaths) override void run(ref<Store> store, StorePaths && storePaths) override
@ -121,6 +121,7 @@ struct CmdSign : StorePathsCommand
throw UsageError("you must specify a secret key file using '-k'"); throw UsageError("you must specify a secret key file using '-k'");
SecretKey secretKey(readFile(secretKeyFile)); SecretKey secretKey(readFile(secretKeyFile));
LocalSigner signer(std::move(secretKey));
size_t added{0}; size_t added{0};
@ -129,7 +130,7 @@ struct CmdSign : StorePathsCommand
auto info2(*info); auto info2(*info);
info2.sigs.clear(); info2.sigs.clear();
info2.sign(*store, secretKey); info2.sign(*store, signer);
assert(!info2.sigs.empty()); assert(!info2.sigs.empty());
if (!info->sigs.count(*info2.sigs.begin())) { if (!info->sigs.count(*info2.sigs.begin())) {

View File

@ -5,6 +5,7 @@
#include "thread-pool.hh" #include "thread-pool.hh"
#include "references.hh" #include "references.hh"
#include "signals.hh" #include "signals.hh"
#include "keys.hh"
#include <atomic> #include <atomic>