From e0def5bc4b41ad09ce3f188bf522814ef3389e1f Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Wed, 4 Feb 2015 16:43:32 +0100 Subject: [PATCH] Use libsodium instead of OpenSSL for binary cache signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile.config.in | 1 + configure.ac | 4 +++ perl/lib/Nix/Config.pm.in | 27 +++++++++++++------- perl/lib/Nix/Crypto.pm | 42 -------------------------------- perl/lib/Nix/Manifest.pm | 19 +++++++-------- perl/lib/Nix/Store.pm | 1 + perl/lib/Nix/Store.xs | 40 ++++++++++++++++++++++++++++++ perl/local.mk | 11 +++++---- release.nix | 4 +-- scripts/nix-push.in | 25 +++++++++---------- src/libutil/util.cc | 25 ++++++++++++++----- src/libutil/util.hh | 2 +- src/nix-store/local.mk | 2 +- src/nix-store/nix-store.cc | 34 ++++++++++++++++++++++++-- tests/binary-cache.sh | 50 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 196 insertions(+), 91 deletions(-) delete mode 100644 perl/lib/Nix/Crypto.pm diff --git a/Makefile.config.in b/Makefile.config.in index 797b99f80..5b7bf297e 100644 --- a/Makefile.config.in +++ b/Makefile.config.in @@ -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@ diff --git a/configure.ac b/configure.ac index 622cf1e20..00bffd8b4 100644 --- a/configure.ac +++ b/configure.ac @@ -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]]), diff --git a/perl/lib/Nix/Config.pm.in b/perl/lib/Nix/Config.pm.in index bc51310e5..388acd2e6 100644 --- a/perl/lib/Nix/Config.pm.in +++ b/perl/lib/Nix/Config.pm.in @@ -1,5 +1,7 @@ package Nix::Config; +use MIME::Base64; + $version = "@PACKAGE_VERSION@"; $binDir = $ENV{"NIX_BIN_DIR"} || "@bindir@"; @@ -19,24 +21,31 @@ $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; + + open CONFIG, "<$config" or die "cannot open ‘$config’"; + while () { + /^\s*([\w\-\.]+)\s*=\s*(.*)$/ or next; + $config{$1} = $2; + } + close CONFIG; } - my $config = "$confDir/nix.conf"; - return unless -f $config; - - open CONFIG, "<$config" or die "cannot open ‘$config’"; - while () { - /^\s*([\w\-\.]+)\s*=\s*(.*)$/ or next; - $config{$1} = $2; + 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); } - close CONFIG; } return 1; diff --git a/perl/lib/Nix/Crypto.pm b/perl/lib/Nix/Crypto.pm deleted file mode 100644 index 0286e88d3..000000000 --- a/perl/lib/Nix/Crypto.pm +++ /dev/null @@ -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; diff --git a/perl/lib/Nix/Manifest.pm b/perl/lib/Nix/Manifest.pm index 9b7e89fa4..ec3e48fcf 100644 --- a/perl/lib/Nix/Manifest.pm +++ b/perl/lib/Nix/Manifest.pm @@ -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; diff --git a/perl/lib/Nix/Store.pm b/perl/lib/Nix/Store.pm index 89cfaefa5..233a432ee 100644 --- a/perl/lib/Nix/Store.pm +++ b/perl/lib/Nix/Store.pm @@ -17,6 +17,7 @@ our @EXPORT = qw( queryPathFromHashPart topoSortPaths computeFSClosure followLinksToStorePath exportPaths importPaths hashPath hashFile hashString + signString checkSignature addToStore makeFixedOutputPath derivationFromPath ); diff --git a/perl/lib/Nix/Store.xs b/perl/lib/Nix/Store.xs index ff90616d3..792d2f649 100644 --- a/perl/lib/Nix/Store.xs +++ b/perl/lib/Nix/Store.xs @@ -11,6 +11,8 @@ #include #include +#include + 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 { diff --git a/perl/local.mk b/perl/local.mk index 73d8a7c95..132676f53 100644 --- a/perl/local.mk +++ b/perl/local.mk @@ -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 diff --git a/release.nix b/release.nix index a08cf7a96..5b6ff2284 100644 --- a/release.nix +++ b/release.nix @@ -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 diff --git a/scripts/nix-push.in b/scripts/nix-push.in index c6d187704..0e90ab3c2 100755 --- a/scripts/nix-push.in +++ b/scripts/nix-push.in @@ -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); diff --git a/src/libutil/util.cc b/src/libutil/util.cc index 0d903f2f0..4f3010880 100644 --- a/src/libutil/util.cc +++ b/src/libutil/util.cc @@ -925,18 +925,24 @@ std::vector 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); diff --git a/src/libutil/util.hh b/src/libutil/util.hh index 186ee71f4..1a2dda527 100644 --- a/src/libutil/util.hh +++ b/src/libutil/util.hh @@ -285,7 +285,7 @@ pid_t startProcess(std::function 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) diff --git a/src/nix-store/local.mk b/src/nix-store/local.mk index b887fe033..84ff15b24 100644 --- a/src/nix-store/local.mk +++ b/src/nix-store/local.mk @@ -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)\" diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index 87bc8c379..187397368 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -20,6 +20,8 @@ #include +#include + 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 */ diff --git a/tests/binary-cache.sh b/tests/binary-cache.sh index 6f0c36f63..b0e7f63ae 100644 --- a/tests/binary-cache.sh +++ b/tests/binary-cache.sh @@ -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")