From 6024dc1d97212130c19d3ff5ce6b1d102837eee6 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Thu, 13 Dec 2018 14:30:52 +0100 Subject: [PATCH] Support SRI hashes 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 { url = https://nixos.org/releases/nix/nix-2.1.3/nix-2.1.3.tar.xz; sha256 = "5d22dad058d5c800d65a115f919da22938c50dd6ba98c5e3a183172d149840a4"; }; you can write import { 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 ":" 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. --- corepkgs/fetchurl.nix | 8 ++++++-- src/libexpr/primops.cc | 10 ++++------ src/libutil/hash.cc | 32 +++++++++++++++++++------------- src/libutil/hash.hh | 7 ++++--- src/nix-store/nix-store.cc | 3 +++ src/nix/hash.cc | 23 ++++++++++++++--------- tests/fetchurl.sh | 11 +++++++++++ tests/hash.sh | 12 +++++++++++- 8 files changed, 72 insertions(+), 34 deletions(-) diff --git a/corepkgs/fetchurl.nix b/corepkgs/fetchurl.nix index 0ce1bab11..a84777f57 100644 --- a/corepkgs/fetchurl.nix +++ b/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) diff --git a/src/libexpr/primops.cc b/src/libexpr/primops.cc index 7372134e2..60698f740 100644 --- a/src/libexpr/primops.cc +++ b/src/libexpr/primops.cc @@ -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 { diff --git a/src/libutil/hash.cc b/src/libutil/hash.cc index 9d82f13a5..1c14ebb18 100644 --- a/src/libutil/hash.cc +++ b/src/libutil/hash.cc @@ -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: @@ -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; } @@ -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; + bool isSRI = false; - if (colon == string::npos) { - if (type == htUnknown) + 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'; @@ -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]; @@ -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); } diff --git a/src/libutil/hash.hh b/src/libutil/hash.hh index fd7a61df8..2dbc3b630 100644 --- a/src/libutil/hash.hh +++ b/src/libutil/hash.hh @@ -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 @@ -38,8 +38,9 @@ struct Hash Hash(HashType type) : type(type) { init(); }; /* Initialize the hash from a string representation, in the format - "[:]". If the 'type' argument is - htUnknown, then the hash type must be specified in the + "[:]" or "-" (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); diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc index a9ad14762..5b37237eb 100644 --- a/src/nix-store/nix-store.cc +++ b/src/nix-store/nix-store.cc @@ -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; diff --git a/src/nix/hash.cc b/src/nix/hash.cc index 64062fb97..af4105e28 100644 --- a/src/nix/hash.cc +++ b/src/nix/hash.cc @@ -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 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); @@ -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); } } }; @@ -54,7 +55,7 @@ static RegisterCommand r2(make_ref(CmdHash::mPath)); struct CmdToBase : Command { Base base; - HashType ht = htSHA512; + HashType ht = htUnknown; std::vector args; CmdToBase(Base base) : base(base) @@ -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(Base16)); static RegisterCommand r4(make_ref(Base32)); static RegisterCommand r5(make_ref(Base64)); +static RegisterCommand r6(make_ref(SRI)); /* Legacy nix-hash command. */ static int compatNixHash(int argc, char * * argv) diff --git a/tests/fetchurl.sh b/tests/fetchurl.sh index 9bbf044f7..d51d081f5 100644 --- a/tests/fetchurl.sh +++ b/tests/fetchurl.sh @@ -18,6 +18,17 @@ outPath=$(nix-build '' --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 '' --argstr url file://$(pwd)/fetchurl.sh --argstr hash $hash --no-out-link --hashed-mirrors '') + +cmp $outPath fetchurl.sh + # Test the hashed mirror feature. clearStore diff --git a/tests/hash.sh b/tests/hash.sh index 9f234bc63..4cfc97901 100644 --- a/tests/hash.sh +++ b/tests/hash.sh @@ -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 @@ -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 @@ -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="