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.
This commit is contained in:
Martin Atkins 2020-09-08 16:36:18 -07:00
parent 0b632c872e
commit e843097e52
2 changed files with 62 additions and 15 deletions

View File

@ -1,13 +1,19 @@
package getproviders package getproviders
import ( import (
"crypto/sha256"
"fmt" "fmt"
"io"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"golang.org/x/mod/sumdb/dirhash" "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 // PackageHash computes a hash of the contents of the package at the given
// location, using whichever hash algorithm is the current default. // 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. // a non-local location this function will always return an error.
func PackageMatchesHash(loc PackageLocation, want string) (bool, error) { func PackageMatchesHash(loc PackageLocation, want string) (bool, error) {
switch { switch {
case strings.HasPrefix(want, "h1"): case strings.HasPrefix(want, h1Prefix):
got, err := PackageHashV1(loc) got, err := PackageHashV1(loc)
if err != nil { if err != nil {
return false, err 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. // verification in order for a package to be considered valid.
func PreferredHash(given []string) string { func PreferredHash(given []string) string {
for _, s := range given { for _, s := range given {
if strings.HasPrefix(s, "h1:") { if strings.HasPrefix(s, h1Prefix) {
return s return s
} }
} }
return "" 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 // PackageHashV1 computes a hash of the contents of the package at the given
// location using hash algorithm 1. // location using hash algorithm 1.
// //

View File

@ -181,25 +181,23 @@ func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLoca
return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation) 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 { if err != nil {
return nil, err return nil, fmt.Errorf("failed to compute checksum for %s: %s", archiveLocation, err)
} }
defer f.Close() wantHash := HashLegacyZipSHAFromSHA(a.WantSHA256Sum)
if gotHash != wantHash {
h := sha256.New() return nil, fmt.Errorf("archive has incorrect checksum %s (expected %s)", gotHash, wantHash)
_, 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[:])
} }
return &PackageAuthenticationResult{result: verifiedChecksum}, nil return &PackageAuthenticationResult{result: verifiedChecksum}, nil
} }
func (a archiveHashAuthentication) AcceptableHashes() map[Platform][]string {
return map[Platform][]string{
a.Platform: {HashLegacyZipSHAFromSHA(a.WantSHA256Sum)},
}
}
type matchingChecksumAuthentication struct { type matchingChecksumAuthentication struct {
Document []byte Document []byte
Filename string Filename string