Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libstore: generalize signature scheme and introduce remote signing #9076

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/manual/src/SUMMARY.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
- [Protocols](protocols/index.md)
- [Serving Tarball Flakes](protocols/tarball-fetcher.md)
- [Derivation "ATerm" file format](protocols/derivation-aterm.md)
- [Signatures](protocols/signature.md)
- [Glossary](glossary.md)
- [Contributing](contributing/index.md)
- [Hacking](contributing/hacking.md)
Expand Down
47 changes: 47 additions & 0 deletions doc/manual/src/protocols/signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Signature
RaitoBezarius marked this conversation as resolved.
Show resolved Hide resolved

In Nix, signatures are used to trust store paths using asymmetric cryptography,
in this instance: Curve25519-based cryptography.

## Store path signature format

Store path signatures are ED25519 signatures, there are two types of
signatures:

- `ValidPathInfo` will assemble a fingerprint in the form of `1;<store
path>;<NAR hash in base32 including its own type>;<NAR size in bytes>;<NAR
references separated by ,>` and sign this information with the ED25519
private key.
- `Realisation` will assemble a fingerprint in the form of a JSON string: `{
id, outPath, dependentRealisations }` and sign this information with the
ED25519 private key.

# Remote signature protocol

The remote signature protocol is a mechanism to offload the signature of your
store paths to another machine that can possess the secret key material in a
secure way.

In this setup, Nix will contact a remote signing URL that you specified and ask
to sign fingerprints over the wire.

The protocol expects a UNIX domain socket to force you to handle proper
authentication and authorization. `socat` is a great tool to manipulate all
sorts of sockets.

## Semantics of the APIs

- `POST /sign`: expects a fingerprint as input and will return the signature
associated to that fingerprint.
- `POST /sign-store-path`: expects a store path as a parameter and will attempt
to sign that specific store path which is expected to be present on the
signer's machine and return the signature in the response.
- `GET /publickey`: receives the public key as a string in the response.

## A note on the security of that scheme

You are responsible to ensure that `/sign` cannot be abused to sign anything
and everything, for this, a simple setup could involve setting up a TCP service
that requires authentication, e.g. SSH or something on the HTTP level and you
can run a highly privileged daemon on the machine that wants to benefit from
signatures presenting a UNIX domain socket to Nix.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should be one sentence per line (or other punctuation / semantic boundary), not fixed line length wrapped.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that mean that you want to remove the fixed line length wrapping and leave it the existing punctuation or ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do it. @fricklerhandwerk wrote the non-fixed length lines somewhere in the contributions docs but I forget where. You can find it if you are curious.

1 change: 0 additions & 1 deletion perl/lib/Nix/Store.xs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
#include "realisation.hh"
#include "globals.hh"
#include "store-api.hh"
#include "crypto.hh"
#include "posix-source-accessor.hh"

#include <sodium.h>
Expand Down
13 changes: 10 additions & 3 deletions src/libstore/binary-cache-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ BinaryCacheStore::BinaryCacheStore(const Params & params)
: BinaryCacheStoreConfig(params)
, Store(params)
{
if (secretKeyFile != "")
secretKey = std::unique_ptr<SecretKey>(new SecretKey(readFile(secretKeyFile)));
if (remoteSigningPath != "")
signer = std::make_unique<RemoteSigner>(
remoteSigningPath
);
else if (secretKeyFile != "")
signer = std::make_unique<LocalSigner>(
SecretKey(readFile(secretKeyFile))
);


StringSink sink;
sink << narVersionMagic1;
Expand Down Expand Up @@ -274,7 +281,7 @@ ref<const ValidPathInfo> BinaryCacheStore::addToStoreCommon(
stats.narWriteCompressionTimeMs += duration;

/* Atomically write the NAR info file.*/
if (secretKey) narInfo->sign(*this, *secretKey);
if (signer) narInfo->sign(*this, *signer);

writeNarInfo(narInfo);

Expand Down
13 changes: 10 additions & 3 deletions src/libstore/binary-cache-store.hh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once
///@file

#include "crypto.hh"
#include "signature/local-keys.hh"
#include "store-api.hh"
#include "log-store.hh"

Expand Down Expand Up @@ -32,6 +32,14 @@ struct BinaryCacheStoreConfig : virtual StoreConfig
const Setting<Path> secretKeyFile{this, "", "secret-key",
"Path to the secret key used to sign the binary cache."};

const Setting<Path> remoteSigningPath{
this, {}, "remote-signing-path",
R"(
A path which is implementing the Nix remote signing protocol
pointing at a UNIX domain socket.
These are used to sign the binary cache paths.
)"};

const Setting<Path> 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`."};

Expand All @@ -57,8 +65,7 @@ class BinaryCacheStore : public virtual BinaryCacheStoreConfig,
{

private:

std::unique_ptr<SecretKey> secretKey;
std::unique_ptr<Signer> signer;

protected:

Expand Down
5 changes: 2 additions & 3 deletions src/libstore/filetransfer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "finally.hh"
#include "callback.hh"
#include "signals.hh"
#include "network/user-agent.hh"

#if ENABLE_S3
#include <aws/core/client/ClientConfiguration.h>
Expand Down Expand Up @@ -295,9 +296,7 @@ struct curlFileTransfer : public FileTransfer
curl_easy_setopt(req, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(req, CURLOPT_MAXREDIRS, 10);
curl_easy_setopt(req, CURLOPT_NOSIGNAL, 1);
curl_easy_setopt(req, CURLOPT_USERAGENT,
("curl/" LIBCURL_VERSION " Nix/" + nixVersion +
(fileTransferSettings.userAgentSuffix != "" ? " " + fileTransferSettings.userAgentSuffix.get() : "")).c_str());
set_user_agent(req, fileTransferSettings.userAgentSuffix.get());
#if LIBCURL_VERSION_NUM >= 0x072b00
curl_easy_setopt(req, CURLOPT_PIPEWAIT, 1);
#endif
Expand Down
5 changes: 0 additions & 5 deletions src/libstore/globals.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@

#include <nlohmann/json.hpp>

#include <sodium/core.h>

#ifdef __GLIBC__
# include <gnu/lib-names.h>
# include <nss.h>
Expand Down Expand Up @@ -409,9 +407,6 @@ void initLibStore() {

initLibUtil();

if (sodium_init() == -1)
throw Error("could not initialise libsodium");

loadConfFile();

preloadNSS();
Expand Down
7 changes: 7 additions & 0 deletions src/libstore/globals.hh
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,13 @@ public:
can add it to `trusted-public-keys` in their `nix.conf`.
)"};

Setting<Strings> remoteSigningPaths{
this, {}, "remote-signing-paths",
R"(
A whitespace-separated list of paths pointing at Nix remote signing
UNIX domain socket servers. These are used to sign locally-built paths.
)"};

Setting<unsigned int> tarballTtl{
this, 60 * 60, "tarball-ttl",
R"(
Expand Down
33 changes: 33 additions & 0 deletions src/libstore/keys.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#pragma once
///@file
RaitoBezarius marked this conversation as resolved.
Show resolved Hide resolved

#include "globals.hh"
#include "libutil/signature/local-keys.hh"

namespace nix {
RaitoBezarius marked this conversation as resolved.
Show resolved Hide resolved

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;
}

}
19 changes: 17 additions & 2 deletions src/libstore/local-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "signals.hh"
#include "posix-fs-canonicalise.hh"
#include "posix-source-accessor.hh"
#include "keys.hh"

#include <iostream>
#include <algorithm>
Expand Down Expand Up @@ -1575,22 +1576,36 @@ void LocalStore::signRealisation(Realisation & realisation)
// FIXME: keep secret keys in memory.

auto secretKeyFiles = settings.secretKeyFiles;
auto remoteSigningPaths = settings.remoteSigningPaths;

for (auto & secretKeyFile : secretKeyFiles.get()) {
SecretKey secretKey(readFile(secretKeyFile));
realisation.sign(secretKey);
LocalSigner signer(std::move(secretKey));
realisation.sign(signer);
}

for (auto & remoteSigningPath : remoteSigningPaths.get()) {
RemoteSigner signer(remoteSigningPath);
realisation.sign(signer);
}
}

void LocalStore::signPathInfo(ValidPathInfo & info)
{
// FIXME: keep secret keys in memory.

auto remoteSigningPaths = settings.remoteSigningPaths;
auto secretKeyFiles = settings.secretKeyFiles;

for (auto & secretKeyFile : secretKeyFiles.get()) {
SecretKey secretKey(readFile(secretKeyFile));
info.sign(*this, secretKey);
LocalSigner signer(std::move(secretKey));
info.sign(*this, signer);
}

for (auto & remoteSigningPath : remoteSigningPaths.get()) {
RemoteSigner signer(remoteSigningPath);
info.sign(*this, signer);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/libstore/local.mk
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ libstore_SOURCES := $(wildcard $(d)/*.cc $(d)/builtins/*.cc $(d)/build/*.cc)

libstore_LIBS = libutil

libstore_LDFLAGS += $(SQLITE3_LIBS) $(LIBCURL_LIBS) $(SODIUM_LIBS) -pthread
libstore_LDFLAGS += $(SQLITE3_LIBS) $(LIBCURL_LIBS) -pthread
ifdef HOST_LINUX
libstore_LDFLAGS += -ldl
endif
Expand Down
4 changes: 2 additions & 2 deletions src/libstore/path-info.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/libstore/path-info.hh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once
///@file

#include "crypto.hh"
#include "signature/signer.hh"
#include "path.hh"
#include "hash.hh"
#include "content-address.hh"
Expand Down Expand Up @@ -107,7 +107,7 @@ struct ValidPathInfo : UnkeyedValidPathInfo {
*/
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
Expand Down
6 changes: 2 additions & 4 deletions src/libstore/path.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "store-dir-config.hh"

#include <sodium.h>
#include "util.hh"

namespace nix {

Expand Down Expand Up @@ -49,9 +49,7 @@ StorePath StorePath::dummy("ffffffffffffffffffffffffffffffff-x");

StorePath StorePath::random(std::string_view name)
{
Hash hash(HashAlgorithm::SHA1);
randombytes_buf(hash.hash, hash.hashSize);
return StorePath(hash, name);
return StorePath(Hash::random(HashAlgorithm::SHA1), name);
}

StorePath StoreDirConfig::parseStorePath(std::string_view path) const
Expand Down
5 changes: 3 additions & 2 deletions src/libstore/realisation.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "realisation.hh"
#include "store-api.hh"
#include "closure.hh"
#include "signature/local-keys.hh"
#include <nlohmann/json.hpp>

namespace nix {
Expand Down Expand Up @@ -113,9 +114,9 @@ std::string Realisation::fingerprint() const
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
Expand Down
4 changes: 2 additions & 2 deletions src/libstore/realisation.hh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include "derived-path.hh"
#include <nlohmann/json_fwd.hpp>
#include "comparator.hh"
#include "crypto.hh"
#include "signature/signer.hh"

namespace nix {

Expand Down Expand Up @@ -64,7 +64,7 @@ struct Realisation {
static Realisation fromJSON(const nlohmann::json& json, const std::string& whence);

std::string fingerprint() const;
void sign(const SecretKey &);
void sign(const Signer&);
bool checkSignature(const PublicKeys & publicKeys, const std::string & sig) const;
size_t checkSignatures(const PublicKeys & publicKeys) const;

Expand Down
2 changes: 1 addition & 1 deletion src/libstore/store-api.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "crypto.hh"
#include "signature/local-keys.hh"
#include "source-accessor.hh"
#include "globals.hh"
#include "derived-path.hh"
Expand Down
9 changes: 9 additions & 0 deletions src/libutil/hash.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <sys/stat.h>
#include <fcntl.h>

#include <sodium.h>

namespace nix {

static size_t regularHashSize(HashAlgorithm type) {
Expand Down Expand Up @@ -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));
}

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)
{
if (hashStr.empty()) {
Expand Down
Loading
Loading