From e843097e52d5e37c6df10feea0c23f7dd1feb854 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Tue, 8 Sep 2020 16:36:18 -0700 Subject: [PATCH] internal/getproviders: Formalize the "ziphash" hashing scheme This is the pre-existing hashing scheme that was initially built for releases.hashicorp.com and then later reused for the provider registry protocol, which takes a SHA256 hash of the official distribution .zip file and formats it as lowercase hex. This is a non-ideal hash scheme because it works only for PackageLocalArchive locations, and so we can't verify package directories on local disk against such hashes. However, the registry protocol is now a compatibility constraint and so we're going to need to support this hashing scheme for the foreseeable future. --- internal/getproviders/hash.go | 53 ++++++++++++++++++- .../getproviders/package_authentication.go | 24 ++++----- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/internal/getproviders/hash.go b/internal/getproviders/hash.go index f2d9b7b9d..2fbf7f615 100644 --- a/internal/getproviders/hash.go +++ b/internal/getproviders/hash.go @@ -1,13 +1,19 @@ package getproviders import ( + "crypto/sha256" "fmt" + "io" + "os" "path/filepath" "strings" "golang.org/x/mod/sumdb/dirhash" ) +const h1Prefix = "h1:" +const zipHashPrefix = "zh:" + // PackageHash computes a hash of the contents of the package at the given // location, using whichever hash algorithm is the current default. // @@ -40,7 +46,7 @@ func PackageHash(loc PackageLocation) (string, error) { // a non-local location this function will always return an error. func PackageMatchesHash(loc PackageLocation, want string) (bool, error) { switch { - case strings.HasPrefix(want, "h1"): + case strings.HasPrefix(want, h1Prefix): got, err := PackageHashV1(loc) if err != nil { return false, err @@ -61,13 +67,56 @@ func PackageMatchesHash(loc PackageLocation, want string) (bool, error) { // verification in order for a package to be considered valid. func PreferredHash(given []string) string { for _, s := range given { - if strings.HasPrefix(s, "h1:") { + if strings.HasPrefix(s, h1Prefix) { return s } } return "" } +// PackageHashLegacyZipSHA implements the old provider package hashing scheme +// of taking a SHA256 hash of the containing .zip archive itself, rather than +// of the contents of the archive. +// +// The result is a hash string with the "zh:" prefix, which is intended to +// represent "zip hash". After the prefix is a lowercase-hex encoded SHA256 +// checksum, intended to exactly match the formatting used in the registry +// API (apart from the prefix) so that checksums can be more conveniently +// compared by humans. +// +// Because this hashing scheme uses the official provider .zip file as its +// input, it accepts only PackageLocalArchive locations. +func PackageHashLegacyZipSHA(loc PackageLocalArchive) (string, error) { + archivePath, err := filepath.EvalSymlinks(string(loc)) + if err != nil { + return "", err + } + + f, err := os.Open(archivePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + _, err = io.Copy(h, f) + if err != nil { + return "", err + } + + gotHash := h.Sum(nil) + return fmt.Sprintf("%s%x", zipHashPrefix, gotHash), nil +} + +// HashLegacyZipSHAFromSHA is a convenience method to produce the schemed-string +// hash format from an already-calculated hash of a provider .zip archive. +// +// This just adds the "zh:" prefix and encodes the string in hex, so that the +// result is in the same format as PackageHashLegacyZipSHA. +func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) string { + return fmt.Sprintf("%s%x", zipHashPrefix, sum[:]) +} + // PackageHashV1 computes a hash of the contents of the package at the given // location using hash algorithm 1. // diff --git a/internal/getproviders/package_authentication.go b/internal/getproviders/package_authentication.go index 10f000a26..a87a624ac 100644 --- a/internal/getproviders/package_authentication.go +++ b/internal/getproviders/package_authentication.go @@ -181,25 +181,23 @@ func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLoca return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation) } - f, err := os.Open(string(archiveLocation)) + gotHash, err := PackageHashLegacyZipSHA(archiveLocation) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to compute checksum for %s: %s", archiveLocation, err) } - defer f.Close() - - h := sha256.New() - _, err = io.Copy(h, f) - if err != nil { - return nil, err - } - - gotHash := h.Sum(nil) - if !bytes.Equal(gotHash, a.WantSHA256Sum[:]) { - return nil, fmt.Errorf("archive has incorrect SHA-256 checksum %x (expected %x)", gotHash, a.WantSHA256Sum[:]) + wantHash := HashLegacyZipSHAFromSHA(a.WantSHA256Sum) + if gotHash != wantHash { + return nil, fmt.Errorf("archive has incorrect checksum %s (expected %s)", gotHash, wantHash) } return &PackageAuthenticationResult{result: verifiedChecksum}, nil } +func (a archiveHashAuthentication) AcceptableHashes() map[Platform][]string { + return map[Platform][]string{ + a.Platform: {HashLegacyZipSHAFromSHA(a.WantSHA256Sum)}, + } +} + type matchingChecksumAuthentication struct { Document []byte Filename string