Skip to content

Commit

Permalink
Support SRI hashes
Browse files Browse the repository at this point in the history
SRI hashes (https://www.w3.org/TR/SRI/) combine the hash algorithm and
a base-64 hash. This allows more concise and standard hash
specifications. For example, instead of

  import <nix/fetchurl.nl> {
    url = https://nixos.org/releases/nix/nix-2.1.3/nix-2.1.3.tar.xz;
    sha256 = "5d22dad058d5c800d65a115f919da22938c50dd6ba98c5e3a183172d149840a4";
  };

you can write

  import <nix/fetchurl.nl> {
    url = https://nixos.org/releases/nix/nix-2.1.3/nix-2.1.3.tar.xz;
    hash = "sha256-XSLa0FjVyADWWhFfkZ2iKTjFDda6mMXjoYMXLRSYQKQ=";
  };

In fixed-output derivations, the outputHashAlgo is no longer mandatory
if outputHash specifies the hash (either as an SRI or in the old
"<type>:<hash>" format).

'nix hash-{file,path}' now print hashes in SRI format by default. I
also reverted them to use SHA-256 by default because that's what we're
using most of the time in Nixpkgs.

Suggested by @zimbatm.
  • Loading branch information
edolstra committed Dec 13, 2018
1 parent c37e6d7 commit 6024dc1
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 35 deletions.
8 changes: 6 additions & 2 deletions corepkgs/fetchurl.nix
@@ -1,10 +1,14 @@
{ system ? "" # obsolete
, url
, hash ? "" # an SRI ash

# Legacy hash specification
, md5 ? "", sha1 ? "", sha256 ? "", sha512 ? ""
, outputHash ?
if sha512 != "" then sha512 else if sha1 != "" then sha1 else if md5 != "" then md5 else sha256
if hash != "" then hash else if sha512 != "" then sha512 else if sha1 != "" then sha1 else if md5 != "" then md5 else sha256
, outputHashAlgo ?
if sha512 != "" then "sha512" else if sha1 != "" then "sha1" else if md5 != "" then "md5" else "sha256"
if hash != "" then "" else if sha512 != "" then "sha512" else if sha1 != "" then "sha1" else if md5 != "" then "md5" else "sha256"

, executable ? false
, unpack ? false
, name ? baseNameOf (toString url)
Expand Down
10 changes: 4 additions & 6 deletions src/libexpr/primops.cc
Expand Up @@ -724,16 +724,14 @@ static void prim_derivationStrict(EvalState & state, const Pos & pos, Value * *
if (outputs.size() != 1 || *(outputs.begin()) != "out")
throw Error(format("multiple outputs are not supported in fixed-output derivations, at %1%") % posDrvName);

HashType ht = parseHashType(outputHashAlgo);
if (ht == htUnknown)
throw EvalError(format("unknown hash algorithm '%1%', at %2%") % outputHashAlgo % posDrvName);
HashType ht = outputHashAlgo.empty() ? htUnknown : parseHashType(outputHashAlgo);
Hash h(*outputHash, ht);
outputHash = h.to_string(Base16, false);
if (outputHashRecursive) outputHashAlgo = "r:" + outputHashAlgo;

Path outPath = state.store->makeFixedOutputPath(outputHashRecursive, h, drvName);
if (!jsonObject) drv.env["out"] = outPath;
drv.outputs["out"] = DerivationOutput(outPath, outputHashAlgo, *outputHash);
drv.outputs["out"] = DerivationOutput(outPath,
(outputHashRecursive ? "r:" : "") + printHashType(h.type),
h.to_string(Base16, false));
}

else {
Expand Down
34 changes: 20 additions & 14 deletions src/libutil/hash.cc
Expand Up @@ -105,9 +105,9 @@ string printHash16or32(const Hash & hash)
std::string Hash::to_string(Base base, bool includeType) const
{
std::string s;
if (includeType) {
if (base == SRI || includeType) {
s += printHashType(type);
s += ':';
s += base == SRI ? '-' : ':';
}
switch (base) {
case Base16:
Expand All @@ -117,6 +117,7 @@ std::string Hash::to_string(Base base, bool includeType) const
s += printHash32(*this);
break;
case Base64:
case SRI:
s += base64Encode(std::string((const char *) hash, hashSize));
break;
}
Expand All @@ -127,28 +128,33 @@ std::string Hash::to_string(Base base, bool includeType) const
Hash::Hash(const std::string & s, HashType type)
: type(type)
{
auto colon = s.find(':');

size_t pos = 0;

if (colon == string::npos) {
if (type == htUnknown)
bool isSRI = false;

auto sep = s.find(':');
if (sep == string::npos) {
sep = s.find('-');
if (sep != string::npos) {
isSRI = true;
} else if (type == htUnknown)
throw BadHash("hash '%s' does not include a type", s);
} else {
string hts = string(s, 0, colon);
}

if (sep != string::npos) {
string hts = string(s, 0, sep);
this->type = parseHashType(hts);
if (this->type == htUnknown)
throw BadHash("unknown hash type '%s'", hts);
if (type != htUnknown && type != this->type)
throw BadHash("hash '%s' should have type '%s'", s, printHashType(type));
pos = colon + 1;
pos = sep + 1;
}

init();

size_t size = s.size() - pos;

if (size == base16Len()) {
if (!isSRI && size == base16Len()) {

auto parseHexDigit = [&](char c) {
if (c >= '0' && c <= '9') return c - '0';
Expand All @@ -164,7 +170,7 @@ Hash::Hash(const std::string & s, HashType type)
}
}

else if (size == base32Len()) {
else if (!isSRI && size == base32Len()) {

for (unsigned int n = 0; n < size; ++n) {
char c = s[pos + size - n - 1];
Expand All @@ -187,10 +193,10 @@ Hash::Hash(const std::string & s, HashType type)
}
}

else if (size == base64Len()) {
else if (isSRI || size == base64Len()) {
auto d = base64Decode(std::string(s, pos));
if (d.size() != hashSize)
throw BadHash("invalid base-64 hash '%s'", s);
throw BadHash("invalid %s hash '%s'", isSRI ? "SRI" : "base-64", s);
assert(hashSize);
memcpy(hash, d.data(), hashSize);
}
Expand Down
7 changes: 4 additions & 3 deletions src/libutil/hash.hh
Expand Up @@ -20,7 +20,7 @@ const int sha512HashSize = 64;

extern const string base32Chars;

enum Base : int { Base64, Base32, Base16 };
enum Base : int { Base64, Base32, Base16, SRI };


struct Hash
Expand All @@ -38,8 +38,9 @@ struct Hash
Hash(HashType type) : type(type) { init(); };

/* Initialize the hash from a string representation, in the format
"[<type>:]<base16|base32|base64>". If the 'type' argument is
htUnknown, then the hash type must be specified in the
"[<type>:]<base16|base32|base64>" or "<type>-<base64>" (a
Subresource Integrity hash expression). If the 'type' argument
is htUnknown, then the hash type must be specified in the
string. */
Hash(const std::string & s, HashType type = htUnknown);

Expand Down
3 changes: 3 additions & 0 deletions src/nix-store/nix-store.cc
Expand Up @@ -1000,6 +1000,9 @@ static int _main(int argc, char * * argv)
Strings opFlags, opArgs;
Operation op = 0;

Hash h("sha512-Q2bFTOhEALkN8hOms2FKTDLy7eugP2zFZ1T8LCvX42Fp3WoNr3bjZSAHeOsHrbV1Fu9/A0EzCinRE7Af1ofPrw==");
printError("GOT HASH %s", h.to_string(Base64));

parseCmdLine(argc, argv, [&](Strings::iterator & arg, const Strings::iterator & end) {
Operation oldOp = op;

Expand Down
23 changes: 14 additions & 9 deletions src/nix/hash.cc
Expand Up @@ -9,13 +9,14 @@ struct CmdHash : Command
{
enum Mode { mFile, mPath };
Mode mode;
Base base = Base16;
Base base = SRI;
bool truncate = false;
HashType ht = htSHA512;
HashType ht = htSHA256;
std::vector<std::string> paths;

CmdHash(Mode mode) : mode(mode)
{
mkFlag(0, "sri", "print hash in SRI format", &base, SRI);
mkFlag(0, "base64", "print hash in base-64", &base, Base64);
mkFlag(0, "base32", "print hash in base-32 (Nix-specific)", &base, Base32);
mkFlag(0, "base16", "print hash in base-16", &base, Base16);
Expand Down Expand Up @@ -43,7 +44,7 @@ struct CmdHash : Command
Hash h = mode == mFile ? hashFile(ht, path) : hashPath(ht, path).first;
if (truncate && h.hashSize > 20) h = compressHash(h, 20);
std::cout << format("%1%\n") %
h.to_string(base, false);
h.to_string(base, base == SRI);
}
}
};
Expand All @@ -54,7 +55,7 @@ static RegisterCommand r2(make_ref<CmdHash>(CmdHash::mPath));
struct CmdToBase : Command
{
Base base;
HashType ht = htSHA512;
HashType ht = htUnknown;
std::vector<std::string> args;

CmdToBase(Base base) : base(base)
Expand All @@ -70,26 +71,30 @@ struct CmdToBase : Command
return
base == Base16 ? "to-base16" :
base == Base32 ? "to-base32" :
"to-base64";
base == Base64 ? "to-base64" :
"to-sri";
}

std::string description() override
{
return fmt("convert a hash to base-%d representation",
base == Base16 ? 16 :
base == Base32 ? 32 : 64);
return fmt("convert a hash to %s representation",
base == Base16 ? "base-16" :
base == Base32 ? "base-32" :
base == Base64 ? "base-64" :
"SRI");
}

void run() override
{
for (auto s : args)
std::cout << fmt("%s\n", Hash(s, ht).to_string(base, false));
std::cout << fmt("%s\n", Hash(s, ht).to_string(base, base == SRI));
}
};

static RegisterCommand r3(make_ref<CmdToBase>(Base16));
static RegisterCommand r4(make_ref<CmdToBase>(Base32));
static RegisterCommand r5(make_ref<CmdToBase>(Base64));
static RegisterCommand r6(make_ref<CmdToBase>(SRI));

/* Legacy nix-hash command. */
static int compatNixHash(int argc, char * * argv)
Expand Down
11 changes: 11 additions & 0 deletions tests/fetchurl.sh
Expand Up @@ -18,6 +18,17 @@ outPath=$(nix-build '<nix/fetchurl.nix>' --argstr url file://$(pwd)/fetchurl.sh

cmp $outPath fetchurl.sh

# Now using an SRI hash.
clearStore

hash=$(nix hash-file ./fetchurl.sh)

[[ $hash =~ ^sha512- ]]

outPath=$(nix-build '<nix/fetchurl.nix>' --argstr url file://$(pwd)/fetchurl.sh --argstr hash $hash --no-out-link --hashed-mirrors '')

cmp $outPath fetchurl.sh

# Test the hashed mirror feature.
clearStore

Expand Down
12 changes: 11 additions & 1 deletion tests/hash.sh
Expand Up @@ -2,7 +2,7 @@ source common.sh

try () {
printf "%s" "$2" > $TEST_ROOT/vector
hash=$(nix-hash $EXTRA --flat --type "$1" $TEST_ROOT/vector)
hash=$(nix hash-file --base16 $EXTRA --type "$1" $TEST_ROOT/vector)
if test "$hash" != "$3"; then
echo "hash $1, expected $3, got $hash"
exit 1
Expand Down Expand Up @@ -33,6 +33,12 @@ EXTRA=--base32
try sha256 "abc" "1b8m03r63zqhnjf7l5wnldhh7c134ap5vpj0850ymkq1iyzicy5s"
EXTRA=

EXTRA=--sri
try sha512 "" "sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="
try sha512 "abc" "sha512-3a81oZNherrMQXNJriBBMRLm+k6JqX6iCp7u5ktV05ohkpkqJ0/BqDa6PCOj/uu9RU1EI2Q86A4qmslPpUyknw=="
try sha512 "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" "sha512-IEqPxt2oLwoM7XvrjgikFlfBbvRosiioJ5vjMacDwzWW/RXBOxsH+aodO+pXeJygMa2Fx6cd1wNU7GMSOMo0RQ=="
try sha256 "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" "sha256-JI1qYdIGOLjlwCaTDD5gOaM85Flk/yFn9uzt1BnbBsE="

try2 () {
hash=$(nix-hash --type "$1" $TEST_ROOT/hash-path)
if test "$hash" != "$2"; then
Expand Down Expand Up @@ -65,12 +71,16 @@ try2 md5 "f78b733a68f5edbdf9413899339eaa4a"
try3() {
h64=$(nix to-base64 --type "$1" "$2")
[ "$h64" = "$4" ]
sri=$(nix to-sri --type "$1" "$2")
[ "$sri" = "$1-$4" ]
h32=$(nix-hash --type "$1" --to-base32 "$2")
[ "$h32" = "$3" ]
h16=$(nix-hash --type "$1" --to-base16 "$h32")
[ "$h16" = "$2" ]
h16=$(nix to-base16 --type "$1" "$h64")
[ "$h16" = "$2" ]
h16=$(nix to-base16 "$sri")
[ "$h16" = "$2" ]
}
try3 sha1 "800d59cfcd3c05e900cb4e214be48f6b886a08df" "vw46m23bizj4n8afrc0fj19wrp7mj3c0" "gA1Zz808BekAy04hS+SPa4hqCN8="
try3 sha256 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" "1b8m03r63zqhnjf7l5wnldhh7c134ap5vpj0850ymkq1iyzicy5s" "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0="
Expand Down

8 comments on commit 6024dc1

@copumpkin
Copy link
Member

Choose a reason for hiding this comment

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

Nice!

@knedlsepp
Copy link
Member

Choose a reason for hiding this comment

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

Could we maybe use this format for 5e6fa90

@zimbatm
Copy link
Member

Choose a reason for hiding this comment

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

We need to come-up with a migration path now

@AmineChikhaoui
Copy link
Member

Choose a reason for hiding this comment

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

@Ekleog
Copy link
Member

@Ekleog Ekleog commented on 6024dc1 Dec 14, 2018

Choose a reason for hiding this comment

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

@edolstra Changing the default output of the failure message when a package's hash mismatches sounds premature to me, as people are no longer able to rely on the “hash mismatch” message to commit hashes to nixpkgs (which is not yet ready to require the latest version of nix).

Maybe it would make sense to revert the default output change, and do it again in ~6 months / a year, when nixpkgs will be ready to require a version of nix that contains this commit?

@zimbatm
Copy link
Member

Choose a reason for hiding this comment

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

@Ekleog it doesn't change the default output format AFAIK

@Ekleog
Copy link
Member

@Ekleog Ekleog commented on 6024dc1 Dec 15, 2018

Choose a reason for hiding this comment

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

@zimbatm
Oh indeed, I have been tricked by another recent change in the hash output that makes hashes look like sha256:0y6gwmn44svm9655hbq5if38sl1xd5qa265ikn8v8rbzkppyjr6g (so adding the sha256:) ; coupled with “'nix hash-{file,path}' now print hashes in SRI format by default” from the commit message I assumed it was due to this commit. Didn't notice it was sha256: and not sha256-.

@dtzWill
Copy link
Member

Choose a reason for hiding this comment

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

@edolstra FYI https://hydra.nixos.org/build/85858916

Since I ran into this as well, submitted a quick fix for this: #2584

Please sign in to comment.