Use libsodium instead of OpenSSL for binary cache signing

Sodium's Ed25519 signatures are much shorter than OpenSSL's RSA
signatures. Public keys are also much shorter, so they're now
specified directly in the nix.conf option ‘binary-cache-public-keys’.

The new command ‘nix-store --generate-binary-cache-key’ generates and
prints a public and secret key.
This commit is contained in:
Eelco Dolstra 2015-02-04 16:43:32 +01:00
parent 0d1dafa0c4
commit e0def5bc4b
15 changed files with 196 additions and 91 deletions

View file

@ -7,6 +7,7 @@ HAVE_OPENSSL = @HAVE_OPENSSL@
OPENSSL_LIBS = @OPENSSL_LIBS@
PACKAGE_NAME = @PACKAGE_NAME@
PACKAGE_VERSION = @PACKAGE_VERSION@
SODIUM_LIBS = @SODIUM_LIBS@
bash = @bash@
bindir = @bindir@
bsddiff_compat_include = @bsddiff_compat_include@

View file

@ -205,6 +205,10 @@ AC_CHECK_HEADERS([bzlib.h], [true],
PKG_CHECK_MODULES([SQLITE3], [sqlite3 >= 3.6.19], [CXXFLAGS="$SQLITE3_CFLAGS $CXXFLAGS"])
# Look for libsodium, a required dependency.
PKG_CHECK_MODULES([SODIUM], [libsodium], [CXXFLAGS="$SODIUM_CFLAGS $CXXFLAGS"])
# Whether to use the Boehm garbage collector.
AC_ARG_ENABLE(gc, AC_HELP_STRING([--enable-gc],
[enable garbage collection in the Nix expression evaluator (requires Boehm GC) [default=no]]),

View file

@ -1,5 +1,7 @@
package Nix::Config;
use MIME::Base64;
$version = "@PACKAGE_VERSION@";
$binDir = $ENV{"NIX_BIN_DIR"} || "@bindir@";
@ -19,15 +21,15 @@ $useBindings = "@perlbindings@" eq "yes";
%config = ();
%binaryCachePublicKeys = ();
sub readConfig {
if (defined $ENV{'_NIX_OPTIONS'}) {
foreach my $s (split '\n', $ENV{'_NIX_OPTIONS'}) {
my ($n, $v) = split '=', $s, 2;
$config{$n} = $v;
}
return;
}
} else {
my $config = "$confDir/nix.conf";
return unless -f $config;
@ -37,6 +39,13 @@ sub readConfig {
$config{$1} = $2;
}
close CONFIG;
}
foreach my $s (split(/ /, $config{"binary-cache-public-keys"} // "")) {
my ($keyName, $publicKey) = split ":", $s;
next unless defined $keyName && defined $publicKey;
$binaryCachePublicKeys{$keyName} = decode_base64($publicKey);
}
}
return 1;

View file

@ -1,42 +0,0 @@
package Nix::Crypto;
use strict;
use MIME::Base64;
use Nix::Store;
use Nix::Config;
use IPC::Open2;
our @ISA = qw(Exporter);
our @EXPORT = qw(signString isValidSignature);
sub signString {
my ($privateKeyFile, $s) = @_;
my $hash = hashString("sha256", 0, $s);
my ($from, $to);
my $pid = open2($from, $to, $Nix::Config::openssl, "rsautl", "-sign", "-inkey", $privateKeyFile);
print $to $hash;
close $to;
local $/ = undef;
my $sig = <$from>;
close $from;
waitpid($pid, 0);
die "$0: OpenSSL returned exit code $? while signing hash\n" if $? != 0;
my $sig64 = encode_base64($sig, "");
return $sig64;
}
sub isValidSignature {
my ($publicKeyFile, $sig64, $s) = @_;
my ($from, $to);
my $pid = open2($from, $to, $Nix::Config::openssl, "rsautl", "-verify", "-inkey", $publicKeyFile, "-pubin");
print $to decode_base64($sig64);
close $to;
my $decoded = <$from>;
close $from;
waitpid($pid, 0);
return 0 if $? != 0;
my $hash = hashString("sha256", 0, $s);
return $decoded eq $hash;
}
1;

View file

@ -8,8 +8,9 @@ use Cwd;
use File::stat;
use File::Path;
use Fcntl ':flock';
use MIME::Base64;
use Nix::Config;
use Nix::Crypto;
use Nix::Store;
our @ISA = qw(Exporter);
our @EXPORT = qw(readManifest writeManifest updateManifestDB addPatch deleteOldManifests parseNARInfo);
@ -440,22 +441,20 @@ sub parseNARInfo {
}
my ($sigVersion, $keyName, $sig64) = split ";", $sig;
$sigVersion //= 0;
if ($sigVersion != 1) {
if ($sigVersion != 2) {
warn "NAR info file $location has unsupported version $sigVersion; ignoring\n";
return undef;
}
return undef unless defined $keyName && defined $sig64;
my $publicKeyFile = $Nix::Config::config{"binary-cache-public-key-$keyName"};
if (!defined $publicKeyFile) {
my $publicKey = $Nix::Config::binaryCachePublicKeys{$keyName};
if (!defined $publicKey) {
warn "NAR info file $location is signed by unknown key $keyName; ignoring\n";
return undef;
}
if (! -f $publicKeyFile) {
die "binary cache public key file $publicKeyFile does not exist\n";
return undef;
}
if (!isValidSignature($publicKeyFile, $sig64, $signedData)) {
warn "NAR info file $location has an invalid signature; ignoring\n";
if (!checkSignature($publicKey, decode_base64($sig64), $signedData)) {
warn "NAR info file $location has an incorrect signature; ignoring\n";
return undef;
}
$res->{signedBy} = $keyName;

View file

@ -17,6 +17,7 @@ our @EXPORT = qw(
queryPathFromHashPart
topoSortPaths computeFSClosure followLinksToStorePath exportPaths importPaths
hashPath hashFile hashString
signString checkSignature
addToStore makeFixedOutputPath
derivationFromPath
);

View file

@ -11,6 +11,8 @@
#include <misc.hh>
#include <util.hh>
#include <sodium.h>
using namespace nix;
@ -223,6 +225,44 @@ SV * hashString(char * algo, int base32, char * s)
}
SV * signString(SV * secretKey_, char * msg)
PPCODE:
try {
STRLEN secretKeyLen;
unsigned char * secretKey = (unsigned char *) SvPV(secretKey_, secretKeyLen);
if (secretKeyLen != crypto_sign_SECRETKEYBYTES)
throw Error("secret key is not valid");
unsigned char sig[crypto_sign_BYTES];
unsigned long long sigLen;
crypto_sign_detached(sig, &sigLen, (unsigned char *) msg, strlen(msg), secretKey);
XPUSHs(sv_2mortal(newSVpv((char *) sig, sigLen)));
} catch (Error & e) {
croak(e.what());
}
int checkSignature(SV * publicKey_, SV * sig_, char * msg)
CODE:
try {
STRLEN publicKeyLen;
unsigned char * publicKey = (unsigned char *) SvPV(publicKey_, publicKeyLen);
if (publicKeyLen != crypto_sign_PUBLICKEYBYTES)
throw Error("public key is not valid");
STRLEN sigLen;
unsigned char * sig = (unsigned char *) SvPV(sig_, sigLen);
if (sigLen != crypto_sign_BYTES)
throw Error("signature is not valid");
RETVAL = crypto_sign_verify_detached(sig, (unsigned char *) msg, strlen(msg), publicKey) == 0;
} catch (Error & e) {
croak(e.what());
}
OUTPUT:
RETVAL
SV * addToStore(char * srcPath, int recursive, char * algo)
PPCODE:
try {

View file

@ -5,8 +5,7 @@ nix_perl_sources := \
$(d)/lib/Nix/SSH.pm \
$(d)/lib/Nix/CopyClosure.pm \
$(d)/lib/Nix/Config.pm.in \
$(d)/lib/Nix/Utils.pm \
$(d)/lib/Nix/Crypto.pm
$(d)/lib/Nix/Utils.pm
nix_perl_modules := $(nix_perl_sources:.in=)
@ -23,16 +22,18 @@ ifeq ($(perlbindings), yes)
Store_SOURCES := $(Store_DIR)/Store.cc
Store_LIBS = libstore libutil
Store_CXXFLAGS = \
-I$(shell $(perl) -e 'use Config; print $$Config{archlibexp};')/CORE \
-D_FILE_OFFSET_BITS=64 -Wno-unused-variable -Wno-literal-suffix -Wno-reserved-user-defined-literal
Store_LIBS = libstore libutil
Store_LDFLAGS := $(SODIUM_LIBS)
ifeq (CYGWIN,$(findstring CYGWIN,$(OS)))
archlib = $(shell perl -E 'use Config; print $$Config{archlib};')
libperl = $(shell perl -E 'use Config; print $$Config{libperl};')
Store_LDFLAGS = $(shell find ${archlib} -name ${libperl})
Store_LDFLAGS += $(shell find ${archlib} -name ${libperl})
endif
Store_ALLOW_UNDEFINED = 1

View file

@ -24,7 +24,7 @@ let
buildInputs =
[ curl bison flex perl libxml2 libxslt bzip2
tetex dblatex nukeReferences pkgconfig sqlite
tetex dblatex nukeReferences pkgconfig sqlite libsodium
docbook5 docbook5_xsl
] ++ lib.optional (!lib.inNixShell) git;
@ -80,7 +80,7 @@ let
name = "nix";
src = tarball;
buildInputs = [ curl perl bzip2 openssl pkgconfig sqlite boehmgc ];
buildInputs = [ curl perl bzip2 openssl pkgconfig sqlite boehmgc libsodium ];
configureFlags = ''
--disable-init-state

View file

@ -6,11 +6,11 @@ use File::Basename;
use File::Path qw(mkpath);
use File::stat;
use File::Copy;
use MIME::Base64;
use Nix::Config;
use Nix::Store;
use Nix::Manifest;
use Nix::Utils;
use Nix::Crypto;
binmode STDERR, ":encoding(utf8)";
@ -27,8 +27,7 @@ my $writeManifest = 0;
my $manifestPath;
my $archivesURL;
my $link = 0;
my $privateKeyFile;
my $keyName;
my $secretKeyFile;
my @roots;
for (my $n = 0; $n < scalar @ARGV; $n++) {
@ -61,14 +60,10 @@ for (my $n = 0; $n < scalar @ARGV; $n++) {
$archivesURL = $ARGV[$n];
} elsif ($arg eq "--link") {
$link = 1;
} elsif ($arg eq "--key") {
} elsif ($arg eq "--key-file") {
$n++;
die "$0: $arg requires an argument\n" unless $n < scalar @ARGV;
$privateKeyFile = $ARGV[$n];
} elsif ($arg eq "--key-name") {
$n++;
die "$0: $arg requires an argument\n" unless $n < scalar @ARGV;
$keyName = $ARGV[$n];
$secretKeyFile = $ARGV[$n];
} elsif (substr($arg, 0, 1) eq "-") {
die "$0: unknown flag $arg\n";
} else {
@ -110,7 +105,7 @@ my %narFiles;
foreach my $storePath (@storePaths) {
my $pathHash = substr(basename($storePath), 0, 32);
my $narInfoFile = "$destDir/$pathHash.narinfo";
if (-e $narInfoFile) {
if (!$force && -e $narInfoFile) {
my $narInfo = parseNARInfo($storePath, readFile($narInfoFile), 0, $narInfoFile) or die "cannot read $narInfoFile\n";
my $narFile = "$destDir/$narInfo->{url}";
if (-e $narFile) {
@ -257,9 +252,13 @@ for (my $n = 0; $n < scalar @storePaths2; $n++) {
}
}
if (defined $privateKeyFile && defined $keyName) {
my $sig = signString($privateKeyFile, $info);
$info .= "Signature: 1;$keyName;$sig\n";
if (defined $secretKeyFile) {
my $s = readFile $secretKeyFile;
chomp $s;
my ($keyName, $secretKey) = split ":", $s;
die "invalid secret key file $secretKeyFile\n" unless defined $keyName && defined $secretKey;
my $sig = encode_base64(signString(decode_base64($secretKey), $info), "");
$info .= "Signature: 2;$keyName;$sig\n";
}
my $pathHash = substr(basename($storePath), 0, 32);

View file

@ -925,18 +925,24 @@ std::vector<const char *> stringsToCharPtrs(const Strings & ss)
}
string runProgram(Path program, bool searchPath, const Strings & args)
string runProgram(Path program, bool searchPath, const Strings & args,
const string & input)
{
checkInterrupt();
/* Create a pipe. */
Pipe pipe;
pipe.create();
Pipe stdout, stdin;
stdout.create();
if (!input.empty()) stdin.create();
/* Fork. */
Pid pid = startProcess([&]() {
if (dup2(pipe.writeSide, STDOUT_FILENO) == -1)
if (dup2(stdout.writeSide, STDOUT_FILENO) == -1)
throw SysError("dupping stdout");
if (!input.empty()) {
if (dup2(stdin.readSide, STDIN_FILENO) == -1)
throw SysError("dupping stdin");
}
Strings args_(args);
args_.push_front(program);
@ -950,9 +956,16 @@ string runProgram(Path program, bool searchPath, const Strings & args)
throw SysError(format("executing %1%") % program);
});
pipe.writeSide.close();
stdout.writeSide.close();
string result = drainFD(pipe.readSide);
/* FIXME: This can deadlock if the input is too long. */
if (!input.empty()) {
stdin.readSide.close();
writeFull(stdin.writeSide, input);
stdin.writeSide.close();
}
string result = drainFD(stdout.readSide);
/* Wait for the child to finish. */
int status = pid.wait(true);

View file

@ -285,7 +285,7 @@ pid_t startProcess(std::function<void()> fun, const ProcessOptions & options = P
/* Run a program and return its stdout in a string (i.e., like the
shell backtick operator). */
string runProgram(Path program, bool searchPath = false,
const Strings & args = Strings());
const Strings & args = Strings(), const string & input = "");
MakeError(ExecError, Error)

View file

@ -6,6 +6,6 @@ nix-store_SOURCES := $(wildcard $(d)/*.cc)
nix-store_LIBS = libmain libstore libutil libformat
nix-store_LDFLAGS = -lbz2 -pthread
nix-store_LDFLAGS = -lbz2 -pthread $(SODIUM_LIBS)
nix-store_CXXFLAGS = -DCURL=\"$(curl)\"

View file

@ -20,6 +20,8 @@
#include <bzlib.h>
#include <sodium.h>
using namespace nix;
using std::cin;
@ -1006,6 +1008,32 @@ static void opServe(Strings opFlags, Strings opArgs)
}
static void opGenerateBinaryCacheKey(Strings opFlags, Strings opArgs)
{
foreach (Strings::iterator, i, opFlags)
throw UsageError(format("unknown flag %1%") % *i);
if (opArgs.size() != 1) throw UsageError("one argument expected");
string keyName = opArgs.front();
sodium_init();
unsigned char pk[crypto_sign_PUBLICKEYBYTES];
unsigned char sk[crypto_sign_SECRETKEYBYTES];
if (crypto_sign_keypair(pk, sk) != 0)
throw Error("key generation failed");
// FIXME: super ugly way to do base64 encoding.
auto args = Strings({"-MMIME::Base64", "-0777", "-ne", "print encode_base64($_, '')"});
string pk64 = runProgram("perl", true, args, string((char *) pk, crypto_sign_PUBLICKEYBYTES));
std::cout << keyName << ":" << pk64 << std::endl;
string sk64 = runProgram("perl", true, args, string((char *) sk, crypto_sign_SECRETKEYBYTES));
std::cout << keyName << ":" << sk64 << std::endl;
}
/* Scan the arguments; find the operation, set global flags, put all
other flags in a list, and put all other arguments in another
list. */
@ -1072,14 +1100,16 @@ int main(int argc, char * * argv)
op = opQueryFailedPaths;
else if (*arg == "--clear-failed-paths")
op = opClearFailedPaths;
else if (*arg == "--serve")
op = opServe;
else if (*arg == "--generate-binary-cache-key")
op = opGenerateBinaryCacheKey;
else if (*arg == "--add-root")
gcRoot = absPath(getArg(*arg, arg, end));
else if (*arg == "--indirect")
indirectRoot = true;
else if (*arg == "--no-output")
noOutput = true;
else if (*arg == "--serve")
op = opServe;
else if (*arg != "" && arg->at(0) == '-') {
opFlags.push_back(*arg);
if (*arg == "--max-freed" || *arg == "--max-links" || *arg == "--max-atime") /* !!! hack */

View file

@ -87,3 +87,53 @@ rm $(grep -l "StorePath:.*dependencies-input-2" $cacheDir/*.narinfo)
nix-build --option binary-caches "file://$cacheDir" dependencies.nix -o $TEST_ROOT/result 2>&1 | tee $TEST_ROOT/log
grep -q "Downloading" $TEST_ROOT/log
# Create a signed binary cache.
clearCache
declare -a res=($(nix-store --generate-binary-cache-key test.nixos.org-1))
publicKey="${res[0]}"
secretKey="${res[1]}"
echo "$secretKey" > $TEST_ROOT/secret-key
res=($(nix-store --generate-binary-cache-key test.nixos.org-1))
badKey="${res[0]}"
res=($(nix-store --generate-binary-cache-key foo.nixos.org-1))
otherKey="${res[0]}"
nix-push --dest $cacheDir --key-file $TEST_ROOT/secret-key $outPath
# Downloading should fail if we don't provide a key.
clearStore
rm -f $NIX_STATE_DIR/binary-cache*
(! nix-store -r $outPath --option binary-caches "file://$cacheDir" --option signed-binary-caches '*' )
# And it should fail if we provide an incorrect key.
clearStore
rm -f $NIX_STATE_DIR/binary-cache*
(! nix-store -r $outPath --option binary-caches "file://$cacheDir" --option signed-binary-caches '*' --option binary-cache-public-keys "$badKey")
# It should succeed if we provide the correct key.
nix-store -r $outPath --option binary-caches "file://$cacheDir" --option signed-binary-caches '*' --option binary-cache-public-keys "$otherKey $publicKey"
# It should fail if we corrupt the .narinfo.
clearStore
for i in $cacheDir/*.narinfo; do
grep -v References $i > $i.tmp
mv $i.tmp $i
done
rm -f $NIX_STATE_DIR/binary-cache*
(! nix-store -r $outPath --option binary-caches "file://$cacheDir" --option signed-binary-caches '*' --option binary-cache-public-keys "$publicKey")