diff --git a/command/providers_mirror.go b/command/providers_mirror.go index e9d904b13..6d381a6e4 100644 --- a/command/providers_mirror.go +++ b/command/providers_mirror.go @@ -269,8 +269,8 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { indexArchives[version] = map[string]interface{}{} } indexArchives[version][platform.String()] = map[string]interface{}{ - "url": archiveFilename, // a relative URL from the index file's URL - "hashes": []string{hash}, // an array to allow for additional hash formats in future + "url": archiveFilename, // a relative URL from the index file's URL + "hashes": []string{hash.String()}, // an array to allow for additional hash formats in future } } mainIndex := map[string]interface{}{ diff --git a/internal/depsfile/locks.go b/internal/depsfile/locks.go index d676f9cb8..34b34d99e 100644 --- a/internal/depsfile/locks.go +++ b/internal/depsfile/locks.go @@ -2,7 +2,6 @@ package depsfile import ( "fmt" - "sort" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/internal/getproviders" @@ -58,14 +57,11 @@ func (l *Locks) Provider(addr addrs.Provider) *ProviderLock { // non-lockable provider address then this function will panic. Use // function ProviderIsLockable to determine whether a particular provider // should participate in the version locking mechanism. -func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []string) *ProviderLock { +func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock { if !ProviderIsLockable(addr) { panic(fmt.Sprintf("Locks.SetProvider with non-lockable provider %s", addr)) } - // Normalize the hash lists into a consistent order. - sort.Strings(hashes) - new := &ProviderLock{ addr: addr, version: version, @@ -137,7 +133,7 @@ type ProviderLock struct { // means we can only populate the hash for the current platform, and so // it won't be possible to verify a subsequent installation of the same // provider on a different platform. - hashes []string + hashes []getproviders.Hash } // Provider returns the address of the provider this lock applies to. @@ -172,7 +168,7 @@ func (l *ProviderLock) VersionConstraints() getproviders.VersionConstraints { // of which must match in order for verification to be considered successful. // // Do not modify the backing array of the returned slice. -func (l *ProviderLock) AllHashes() []string { +func (l *ProviderLock) AllHashes() []getproviders.Hash { return l.hashes } @@ -183,6 +179,6 @@ func (l *ProviderLock) AllHashes() []string { // // At least one of the given hashes must match for a package to be considered // valud. -func (l *ProviderLock) PreferredHashes() []string { +func (l *ProviderLock) PreferredHashes() []getproviders.Hash { return getproviders.PreferredHashes(l.hashes) } diff --git a/internal/depsfile/locks_file.go b/internal/depsfile/locks_file.go index dd9fc1840..dfce807ed 100644 --- a/internal/depsfile/locks_file.go +++ b/internal/depsfile/locks_file.go @@ -102,8 +102,8 @@ func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics { } if len(lock.hashes) != 0 { hashVals := make([]cty.Value, 0, len(lock.hashes)) - for _, str := range lock.hashes { - hashVals = append(hashVals, cty.StringVal(str)) + for _, hash := range lock.hashes { + hashVals = append(hashVals, cty.StringVal(hash.String())) } // We're using a set rather than a list here because the order // isn't significant and SetAttributeValue will automatically @@ -369,7 +369,7 @@ func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl return constraints, diags } -func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) ([]string, tfdiags.Diagnostics) { +func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) ([]getproviders.Hash, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if attr == nil { // It's okay to omit this argument. @@ -396,7 +396,7 @@ func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) return nil, diags } - ret := make([]string, 0, len(hashExprs)) + ret := make([]getproviders.Hash, 0, len(hashExprs)) for _, hashExpr := range hashExprs { var raw string hclDiags := gohcl.DecodeExpression(hashExpr, nil, &raw) @@ -404,10 +404,19 @@ func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) if hclDiags.HasErrors() { continue } - // TODO: Validate the hash syntax, but not the actual hash schemes - // because we expect to support different hash formats over time and - // will silently ignore ones that we no longer prefer. - ret = append(ret, raw) + + hash, err := getproviders.ParseHash(raw) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider hash string", + Detail: fmt.Sprintf("Cannot interpret %q as a provider hash: %s.", raw, err), + Subject: expr.Range().Ptr(), + }) + continue + } + + ret = append(ret, hash) } return ret, diags diff --git a/internal/depsfile/locks_file_test.go b/internal/depsfile/locks_file_test.go index 2dd67bb6a..86b0a7058 100644 --- a/internal/depsfile/locks_file_test.go +++ b/internal/depsfile/locks_file_test.go @@ -144,10 +144,10 @@ func TestLoadLocksFromFile(t *testing.T) { if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ">= 3.0.2"; got != want { t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) } - wantHashes := []string{ - "test:placeholder-hash-1", - "test:placeholder-hash-2", - "test:placeholder-hash-3", + wantHashes := []getproviders.Hash{ + getproviders.MustParseHash("test:placeholder-hash-1"), + getproviders.MustParseHash("test:placeholder-hash-2"), + getproviders.MustParseHash("test:placeholder-hash-3"), } if diff := cmp.Diff(wantHashes, lock.hashes); diff != "" { t.Errorf("wrong hashes\n%s", diff) @@ -169,10 +169,10 @@ func TestSaveLocksToFile(t *testing.T) { oneDotTwo := getproviders.MustParseVersion("1.2.0") atLeastOneDotOh := getproviders.MustParseVersionConstraints(">= 1.0.0") pessimisticOneDotOh := getproviders.MustParseVersionConstraints("~> 1") - hashes := []string{ - "test:cccccccccccccccccccccccccccccccccccccccccccccccc", - "test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + hashes := []getproviders.Hash{ + getproviders.MustParseHash("test:cccccccccccccccccccccccccccccccccccccccccccccccc"), + getproviders.MustParseHash("test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + getproviders.MustParseHash("test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), } locks.SetProvider(fooProvider, oneDotOh, atLeastOneDotOh, hashes) locks.SetProvider(barProvider, oneDotTwo, pessimisticOneDotOh, nil) diff --git a/internal/getproviders/hash.go b/internal/getproviders/hash.go index dfbd6c15d..9fb63791c 100644 --- a/internal/getproviders/hash.go +++ b/internal/getproviders/hash.go @@ -11,8 +11,142 @@ import ( "golang.org/x/mod/sumdb/dirhash" ) -const h1Prefix = "h1:" -const zipHashPrefix = "zh:" +// Hash is a specially-formatted string representing a checksum of a package +// or the contents of the package. +// +// A Hash string is always starts with a scheme, which is a short series of +// alphanumeric characters followed by a colon, and then the remainder of the +// string has a different meaning depending on the scheme prefix. +// +// The currently-valid schemes are defined as the constants of type HashScheme +// in this package. +// +// Callers outside of this package must not create Hash values via direct +// conversion. Instead, use either the HashScheme.New method on one of the +// HashScheme contents (for a hash of a particular scheme) or the ParseHash +// function (if hashes of any scheme are acceptable). +type Hash string + +// NilHash is the zero value of Hash. It isn't a valid hash, so all of its +// methods will panic. +const NilHash = Hash("") + +// ParseHash parses the string representation of a Hash into a Hash value. +// +// A particular version of Terraform only supports a fixed set of hash schemes, +// but this function intentionally allows unrecognized schemes so that we can +// silently ignore other schemes that may be introduced in the future. For +// that reason, the Scheme method of the returned Hash may return a value that +// isn't in one of the HashScheme constants in this package. +// +// This function doesn't verify that the value portion of the given hash makes +// sense for the given scheme. Invalid values are just considered to not match +// any packages. +// +// If this function returns an error then the returned Hash is invalid and +// must not be used. +func ParseHash(s string) (Hash, error) { + colon := strings.Index(s, ":") + if colon < 1 { // 1 because a zero-length scheme is not allowed + return NilHash, fmt.Errorf("hash string must start with a scheme keyword followed by a colon") + } + return Hash(s), nil +} + +// MustParseHash is a wrapper around ParseHash that panics if it returns an +// error. +func MustParseHash(s string) Hash { + hash, err := ParseHash(s) + if err != nil { + panic(err.Error()) + } + return hash +} + +// Scheme returns the scheme of the recieving hash. If the receiver is not +// using valid syntax then this method will panic. +func (h Hash) Scheme() HashScheme { + colon := strings.Index(string(h), ":") + if colon < 0 { + panic(fmt.Sprintf("invalid hash string %q", h)) + } + return HashScheme(h[:colon+1]) +} + +// HasScheme returns true if the given scheme matches the receiver's scheme, +// or false otherwise. +// +// If the receiver is not using valid syntax then this method will panic. +func (h Hash) HasScheme(want HashScheme) bool { + return h.Scheme() == want +} + +// Value returns the scheme-specific value from the recieving hash. The +// meaning of this value depends on the scheme. +// +// If the receiver is not using valid syntax then this method will panic. +func (h Hash) Value() string { + colon := strings.Index(string(h), ":") + if colon < 0 { + panic(fmt.Sprintf("invalid hash string %q", h)) + } + return string(h[colon+1:]) +} + +// String returns a string representation of the receiving hash. +func (h Hash) String() string { + return string(h) +} + +// GoString returns a Go syntax representation of the receiving hash. +// +// This is here primarily to help with producing descriptive test failure +// output; these results are not particularly useful at runtime. +func (h Hash) GoString() string { + if h == NilHash { + return "getproviders.NilHash" + } + switch scheme := h.Scheme(); scheme { + case HashScheme1: + return fmt.Sprintf("getproviders.HashScheme1.New(%q)", h.Value()) + case HashSchemeZip: + return fmt.Sprintf("getproviders.HashSchemeZip.New(%q)", h.Value()) + default: + // This fallback is for when we encounter lock files or API responses + // with hash schemes that the current version of Terraform isn't + // familiar with. They were presumably introduced in a later version. + return fmt.Sprintf("getproviders.HashScheme(%q).New(%q)", scheme, h.Value()) + } +} + +// HashScheme is an enumeration of schemes that are allowed for values of type +// Hash. +type HashScheme string + +const ( + // HashScheme1 is the scheme identifier for the first hash scheme. + // + // Use HashV1 (or one of its wrapper functions) to calculate hashes with + // this scheme. + HashScheme1 HashScheme = HashScheme("h1:") + + // HashSchemeZip is the scheme identifier for the legacy hash scheme that + // applies to distribution archives (.zip files) rather than package + // contents, and can therefore only be verified against the original + // distribution .zip file, not an extracted directory. + // + // Use PackageHashLegacyZipSHA to calculate hashes with this scheme. + HashSchemeZip HashScheme = HashScheme("zh:") +) + +// New creates a new Hash value with the receiver as its scheme and the given +// raw string as its value. +// +// It's the caller's responsibility to make sure that the given value makes +// sense for the selected scheme. +func (hs HashScheme) New(value string) Hash { + return Hash(string(hs) + value) +} // PackageHash computes a hash of the contents of the package at the given // location, using whichever hash algorithm is the current default. @@ -26,7 +160,7 @@ const zipHashPrefix = "zh:" // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageHash(loc PackageLocation) (string, error) { +func PackageHash(loc PackageLocation) (Hash, error) { return PackageHashV1(loc) } @@ -44,9 +178,9 @@ func PackageHash(loc PackageLocation) (string, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageMatchesHash(loc PackageLocation, want string) (bool, error) { - switch { - case strings.HasPrefix(want, h1Prefix): +func PackageMatchesHash(loc PackageLocation, want Hash) (bool, error) { + switch want.Scheme() { + case HashScheme1: got, err := PackageHashV1(loc) if err != nil { return false, err @@ -65,11 +199,11 @@ func PackageMatchesHash(loc PackageLocation, want string) (bool, error) { // format. If PreferredHash returns a non-empty string then it will be one // of the hash strings in "given", and that hash is the one that must pass // verification in order for a package to be considered valid. -func PreferredHashes(given []string) []string { - var ret []string - for _, s := range given { - if strings.HasPrefix(s, h1Prefix) { - return append(ret, s) +func PreferredHashes(given []Hash) []Hash { + var ret []Hash + for _, hash := range given { + if hash.Scheme() == HashScheme1 { + return append(ret, hash) } } return ret @@ -87,7 +221,7 @@ func PreferredHashes(given []string) []string { // // Because this hashing scheme uses the official provider .zip file as its // input, it accepts only PackageLocalArchive locations. -func PackageHashLegacyZipSHA(loc PackageLocalArchive) (string, error) { +func PackageHashLegacyZipSHA(loc PackageLocalArchive) (Hash, error) { archivePath, err := filepath.EvalSymlinks(string(loc)) if err != nil { return "", err @@ -106,7 +240,7 @@ func PackageHashLegacyZipSHA(loc PackageLocalArchive) (string, error) { } gotHash := h.Sum(nil) - return fmt.Sprintf("%s%x", zipHashPrefix, gotHash), nil + return HashSchemeZip.New(fmt.Sprintf("%x", gotHash)), nil } // HashLegacyZipSHAFromSHA is a convenience method to produce the schemed-string @@ -114,12 +248,13 @@ func PackageHashLegacyZipSHA(loc PackageLocalArchive) (string, error) { // // 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[:]) +func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) Hash { + return HashSchemeZip.New(fmt.Sprintf("%x", sum[:])) } // PackageHashV1 computes a hash of the contents of the package at the given -// location using hash algorithm 1. +// location using hash algorithm 1. The resulting Hash is guaranteed to have +// the scheme HashScheme1. // // The hash covers the paths to files in the directory and the contents of // those files. It does not cover other metadata about the files, such as @@ -135,7 +270,7 @@ func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) string { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageHashV1(loc PackageLocation) (string, error) { +func PackageHashV1(loc PackageLocation) (Hash, error) { // Our HashV1 is really just the Go Modules hash version 1, which is // sufficient for our needs and already well-used for identity of // Go Modules distribution packages. It is also blocked from incompatible @@ -163,7 +298,10 @@ func PackageHashV1(loc PackageLocation) (string, error) { return "", err } - return dirhash.HashDir(packageDir, "", dirhash.Hash1) + // The dirhash.HashDir result is already in our expected h1:... + // format, so we can just convert directly to Hash. + s, err := dirhash.HashDir(packageDir, "", dirhash.Hash1) + return Hash(s), err case PackageLocalArchive: archivePath, err := filepath.EvalSymlinks(string(loc)) @@ -171,7 +309,10 @@ func PackageHashV1(loc PackageLocation) (string, error) { return "", err } - return dirhash.HashZip(archivePath, dirhash.Hash1) + // The dirhash.HashDir result is already in our expected h1:... + // format, so we can just convert directly to Hash. + s, err := dirhash.HashZip(archivePath, dirhash.Hash1) + return Hash(s), err default: return "", fmt.Errorf("cannot hash package at %s", loc.String()) @@ -190,7 +331,7 @@ func PackageHashV1(loc PackageLocation) (string, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func (m PackageMeta) Hash() (string, error) { +func (m PackageMeta) Hash() (Hash, error) { return PackageHash(m.Location) } @@ -204,7 +345,7 @@ func (m PackageMeta) Hash() (string, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func (m PackageMeta) MatchesHash(want string) (bool, error) { +func (m PackageMeta) MatchesHash(want Hash) (bool, error) { return PackageMatchesHash(m.Location, want) } @@ -219,6 +360,6 @@ func (m PackageMeta) MatchesHash(want string) (bool, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func (m PackageMeta) HashV1() (string, error) { +func (m PackageMeta) HashV1() (Hash, error) { return PackageHashV1(m.Location) } diff --git a/internal/getproviders/hash_test.go b/internal/getproviders/hash_test.go new file mode 100644 index 000000000..de7873498 --- /dev/null +++ b/internal/getproviders/hash_test.go @@ -0,0 +1,70 @@ +package getproviders + +import ( + "testing" +) + +func TestParseHash(t *testing.T) { + tests := []struct { + Input string + Want Hash + WantErr string + }{ + { + Input: "h1:foo", + Want: HashScheme1.New("foo"), + }, + { + Input: "zh:bar", + Want: HashSchemeZip.New("bar"), + }, + { + // A scheme we don't know is considered valid syntax, it just won't match anything. + Input: "unknown:baz", + Want: HashScheme("unknown:").New("baz"), + }, + { + // A scheme with an empty value is weird, but allowed. + Input: "unknown:", + Want: HashScheme("unknown:").New(""), + }, + { + Input: "", + WantErr: "hash string must start with a scheme keyword followed by a colon", + }, + { + // A naked SHA256 hash in hex format is not sufficient + Input: "1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239", + WantErr: "hash string must start with a scheme keyword followed by a colon", + }, + { + // An empty scheme is not allowed + Input: ":blah", + WantErr: "hash string must start with a scheme keyword followed by a colon", + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got, err := ParseHash(test.Input) + + if test.WantErr != "" { + if err == nil { + t.Fatalf("want error: %s", test.WantErr) + } + if got, want := err.Error(), test.WantErr; got != want { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + if got != test.Want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/internal/getproviders/http_mirror_source.go b/internal/getproviders/http_mirror_source.go index ada5287ff..0d191a31e 100644 --- a/internal/getproviders/http_mirror_source.go +++ b/internal/getproviders/http_mirror_source.go @@ -225,7 +225,18 @@ func (s *HTTPMirrorSource) PackageMeta(provider addrs.Provider, version Version, // A network mirror might not provide any hashes at all, in which case // the package has no source-defined authentication whatsoever. if len(archiveMeta.Hashes) > 0 { - ret.Authentication = NewPackageHashAuthentication(target, archiveMeta.Hashes) + hashes := make([]Hash, 0, len(archiveMeta.Hashes)) + for _, hashStr := range archiveMeta.Hashes { + hash, err := ParseHash(hashStr) + if err != nil { + return PackageMeta{}, s.errQueryFailed( + provider, + fmt.Errorf("provider mirror returned invalid provider hash %q: %s", hashStr, err), + ) + } + hashes = append(hashes, hash) + } + ret.Authentication = NewPackageHashAuthentication(target, hashes) } return ret, nil diff --git a/internal/getproviders/http_mirror_source_test.go b/internal/getproviders/http_mirror_source_test.go index 4e01fc520..c65e41cbe 100644 --- a/internal/getproviders/http_mirror_source_test.go +++ b/internal/getproviders/http_mirror_source_test.go @@ -124,7 +124,7 @@ func TestHTTPMirrorSource(t *testing.T) { Location: PackageHTTPURL(httpServer.URL + "/terraform.io/test/exists/terraform-provider-test_v1.0.0_tos_m68k.zip"), Authentication: packageHashAuthentication{ RequiredHash: "h1:placeholder-hash", - ValidHashes: []string{"h1:placeholder-hash", "h0:unacceptable-hash"}, + ValidHashes: []Hash{"h1:placeholder-hash", "h0:unacceptable-hash"}, Platform: Platform{"tos", "m68k"}, }, } @@ -133,7 +133,7 @@ func TestHTTPMirrorSource(t *testing.T) { } gotHashes := got.AcceptableHashes() - wantHashes := map[Platform][]string{ + wantHashes := map[Platform][]Hash{ tosPlatform: {"h1:placeholder-hash", "h0:unacceptable-hash"}, } if diff := cmp.Diff(wantHashes, gotHashes); diff != "" { diff --git a/internal/getproviders/package_authentication.go b/internal/getproviders/package_authentication.go index fd38f15b9..fec24b73f 100644 --- a/internal/getproviders/package_authentication.go +++ b/internal/getproviders/package_authentication.go @@ -147,7 +147,7 @@ type PackageAuthenticationHashes interface { // Authenticators that don't use hashes as their authentication procedure // will either not implement this interface or will have an implementation // that returns an empty result. - AcceptableHashes() map[Platform][]string + AcceptableHashes() map[Platform][]Hash } type packageAuthenticationAll []PackageAuthentication @@ -183,7 +183,7 @@ func (checks packageAuthenticationAll) AuthenticatePackage(localLocation Package return authResult, nil } -func (checks packageAuthenticationAll) AcceptableHashes() map[Platform][]string { +func (checks packageAuthenticationAll) AcceptableHashes() map[Platform][]Hash { // The elements of checks are expected to be ordered so that the strongest // one is later in the list, so we'll visit them in reverse order and // take the first one that implements the interface and returns a non-empty @@ -202,8 +202,8 @@ func (checks packageAuthenticationAll) AcceptableHashes() map[Platform][]string } type packageHashAuthentication struct { - RequiredHash string - ValidHashes []string + RequiredHash Hash + ValidHashes []Hash Platform Platform } @@ -215,10 +215,10 @@ type packageHashAuthentication struct { // The PreferredHash function will select which of the given hashes is // considered by Terraform to be the strongest verification, and authentication // succeeds as long as that chosen hash matches. -func NewPackageHashAuthentication(platform Platform, validHashes []string) PackageAuthentication { +func NewPackageHashAuthentication(platform Platform, validHashes []Hash) PackageAuthentication { requiredHashes := PreferredHashes(validHashes) // TODO: Update to support multiple hashes - var requiredHash string + var requiredHash Hash if len(requiredHashes) > 0 { requiredHash = requiredHashes[0] } @@ -248,8 +248,8 @@ func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLoca return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHash) } -func (a packageHashAuthentication) AcceptableHashes() map[Platform][]string { - return map[Platform][]string{ +func (a packageHashAuthentication) AcceptableHashes() map[Platform][]Hash { + return map[Platform][]Hash{ a.Platform: a.ValidHashes, } } @@ -295,8 +295,8 @@ func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLoca return &PackageAuthenticationResult{result: verifiedChecksum}, nil } -func (a archiveHashAuthentication) AcceptableHashes() map[Platform][]string { - return map[Platform][]string{ +func (a archiveHashAuthentication) AcceptableHashes() map[Platform][]Hash { + return map[Platform][]Hash{ a.Platform: {HashLegacyZipSHAFromSHA(a.WantSHA256Sum)}, } } diff --git a/internal/getproviders/package_authentication_test.go b/internal/getproviders/package_authentication_test.go index 683eda677..fdae37e3d 100644 --- a/internal/getproviders/package_authentication_test.go +++ b/internal/getproviders/package_authentication_test.go @@ -104,9 +104,9 @@ func TestPackageHashAuthentication_success(t *testing.T) { // Location must be a PackageLocalArchive path location := PackageLocalDir("testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/2.0.0/linux_amd64") - wantHashes := []string{ + wantHashes := []Hash{ // Known-good HashV1 result for this directory - "h1:qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g=", + Hash("h1:qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g="), } auth := NewPackageHashAuthentication(Platform{"linux", "amd64"}, wantHashes) @@ -145,7 +145,7 @@ func TestPackageHashAuthentication_failure(t *testing.T) { t.Run(name, func(t *testing.T) { // Invalid expected hash, either because we'll error before we // reach it, or we want to force a checksum mismatch. - auth := NewPackageHashAuthentication(Platform{"linux", "amd64"}, []string{"h1:invalid"}) + auth := NewPackageHashAuthentication(Platform{"linux", "amd64"}, []Hash{"h1:invalid"}) result, err := auth.AuthenticatePackage(test.location) if result != nil { diff --git a/internal/getproviders/registry_source_test.go b/internal/getproviders/registry_source_test.go index 232354528..58008e2fb 100644 --- a/internal/getproviders/registry_source_test.go +++ b/internal/getproviders/registry_source_test.go @@ -115,7 +115,7 @@ func TestSourcePackageMeta(t *testing.T) { version string os, arch string want PackageMeta - wantHashes map[Platform][]string + wantHashes map[Platform][]Hash wantErr string }{ // These test cases are relying on behaviors of the fake provider @@ -149,7 +149,7 @@ func TestSourcePackageMeta(t *testing.T) { ), ), }, - map[Platform][]string{ + map[Platform][]Hash{ {"linux", "amd64"}: { "zh:000000000000000000000000000000000000000000000000000000000000f00d", }, diff --git a/internal/getproviders/types.go b/internal/getproviders/types.go index 9c5e6285c..57313e794 100644 --- a/internal/getproviders/types.go +++ b/internal/getproviders/types.go @@ -259,7 +259,7 @@ func (m PackageMeta) PackedFilePath(baseDir string) string { // Authentication field. AcceptableHashes therefore returns an empty result // for a PackageMeta that has no authentication object, or has one that does // not make use of hashes. -func (m PackageMeta) AcceptableHashes() map[Platform][]string { +func (m PackageMeta) AcceptableHashes() map[Platform][]Hash { auth, ok := m.Authentication.(PackageAuthenticationHashes) if !ok { return nil diff --git a/internal/providercache/cached_provider.go b/internal/providercache/cached_provider.go index 25c864c00..55b6965f2 100644 --- a/internal/providercache/cached_provider.go +++ b/internal/providercache/cached_provider.go @@ -43,7 +43,7 @@ func (cp *CachedProvider) PackageLocation() getproviders.PackageLocalDir { // If you need a specific version of hash rather than just whichever one is // current default, call that version's corresponding method (e.g. HashV1) // directly instead. -func (cp *CachedProvider) Hash() (string, error) { +func (cp *CachedProvider) Hash() (getproviders.Hash, error) { return getproviders.PackageHash(cp.PackageLocation()) } @@ -54,7 +54,7 @@ func (cp *CachedProvider) Hash() (string, error) { // // MatchesHash may accept hashes in a number of different formats. Over time // the set of supported formats may grow and shrink. -func (cp *CachedProvider) MatchesHash(want string) (bool, error) { +func (cp *CachedProvider) MatchesHash(want getproviders.Hash) (bool, error) { return getproviders.PackageMatchesHash(cp.PackageLocation(), want) } @@ -69,7 +69,7 @@ func (cp *CachedProvider) MatchesHash(want string) (bool, error) { // being added (in a backward-compatible way) in future. The result from // HashV1 always begins with the prefix "h1:" so that callers can distinguish // the results of potentially multiple different hash algorithms in future. -func (cp *CachedProvider) HashV1() (string, error) { +func (cp *CachedProvider) HashV1() (getproviders.Hash, error) { return getproviders.PackageHashV1(cp.PackageLocation()) } diff --git a/internal/providercache/cached_provider_test.go b/internal/providercache/cached_provider_test.go index 84315cbcc..a3272abbf 100644 --- a/internal/providercache/cached_provider_test.go +++ b/internal/providercache/cached_provider_test.go @@ -18,7 +18,7 @@ func TestCachedProviderHash(t *testing.T) { PackageDir: "testdata/cachedir/registry.terraform.io/hashicorp/null/2.0.0/darwin_amd64", } - want := "h1:qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g=" + want := getproviders.MustParseHash("h1:qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g=") got, err := cp.Hash() if err != nil { t.Fatalf("unexpected error: %s", err) diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index bd1d95619..05878b064 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -418,7 +418,7 @@ NeedProvider: } lockEntries[provider] = lockFileEntry{ SelectedVersion: version, - PackageHash: hash, + PackageHash: hash.String(), } } err := i.lockFile().Write(lockEntries) @@ -471,7 +471,13 @@ func (i *Installer) SelectedPackages() (map[addrs.Provider]*CachedProvider, erro continue } - ok, err := cached.MatchesHash(entry.PackageHash) + hash, err := getproviders.ParseHash(entry.PackageHash) + if err != nil { + errs[provider] = fmt.Errorf("local cache for %s has invalid hash %q: %s", provider, entry.PackageHash, err) + continue + } + + ok, err := cached.MatchesHash(hash) if err != nil { errs[provider] = fmt.Errorf("failed to verify checksum for v%s package: %s", entry.SelectedVersion, err) continue