diff --git a/internal/getproviders/hash.go b/internal/getproviders/hash.go index 9fb63791c..4ada2514b 100644 --- a/internal/getproviders/hash.go +++ b/internal/getproviders/hash.go @@ -186,11 +186,77 @@ func PackageMatchesHash(loc PackageLocation, want Hash) (bool, error) { return false, err } return got == want, nil + case HashSchemeZip: + archiveLoc, ok := loc.(PackageLocalArchive) + if !ok { + return false, fmt.Errorf(`ziphash scheme ("zh:" prefix) is not supported for unpacked provider packages`) + } + got, err := PackageHashLegacyZipSHA(archiveLoc) + if err != nil { + return false, err + } + return got == want, nil default: return false, fmt.Errorf("unsupported hash format (this may require a newer version of Terraform)") } } +// PackageMatchesAnyHash returns true if the package at the given location +// matches at least one of the given hashes, or false otherwise. +// +// If it cannot read from the given location, PackageMatchesAnyHash returns an +// error. Unlike the singular PackageMatchesHash, PackageMatchesAnyHash +// considers unsupported hash formats as successfully non-matching, rather +// than returning an error. +// +// PackageMatchesAnyHash can be used only with the two local package location +// types 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 PackageMatchesAnyHash(loc PackageLocation, allowed []Hash) (bool, error) { + // It's likely that we'll have multiple hashes of the same scheme in + // the "allowed" set, in which case we'll avoid repeatedly re-reading the + // given package by caching its result for each of the two + // currently-supported hash formats. These will be NilHash until we + // encounter the first hash of the corresponding scheme. + var v1Hash, zipHash Hash + for _, want := range allowed { + switch want.Scheme() { + case HashScheme1: + if v1Hash == NilHash { + got, err := PackageHashV1(loc) + if err != nil { + return false, err + } + v1Hash = got + } + if v1Hash == want { + return true, nil + } + case HashSchemeZip: + archiveLoc, ok := loc.(PackageLocalArchive) + if !ok { + // A zip hash can never match an unpacked directory + continue + } + if zipHash == NilHash { + got, err := PackageHashLegacyZipSHA(archiveLoc) + if err != nil { + return false, err + } + zipHash = got + } + if zipHash == want { + return true, nil + } + default: + // If it's not a supported format then it can't match. + continue + } + } + return false, nil +} + // PreferredHashes examines all of the given hash strings and returns the one // that the current version of Terraform considers to provide the strongest // verification. @@ -200,10 +266,20 @@ func PackageMatchesHash(loc PackageLocation, want Hash) (bool, error) { // 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 []Hash) []Hash { + // For now this is just filtering for the two hash formats we support, + // both of which are considered equally "preferred". If we introduce + // a new scheme like "h2:" in future then, depending on the characteristics + // of that new version, it might make sense to rework this function so + // that it only returns "h1:" hashes if the input has no "h2:" hashes, + // so that h2: is preferred when possible and h1: is only a fallback for + // interacting with older systems that haven't been updated with the new + // scheme yet. + var ret []Hash for _, hash := range given { - if hash.Scheme() == HashScheme1 { - return append(ret, hash) + switch hash.Scheme() { + case HashScheme1, HashSchemeZip: + ret = append(ret, hash) } } return ret diff --git a/internal/getproviders/http_mirror_source_test.go b/internal/getproviders/http_mirror_source_test.go index c65e41cbe..20e63aaac 100644 --- a/internal/getproviders/http_mirror_source_test.go +++ b/internal/getproviders/http_mirror_source_test.go @@ -123,9 +123,9 @@ func TestHTTPMirrorSource(t *testing.T) { Filename: "terraform-provider-test_v1.0.0_tos_m68k.zip", Location: PackageHTTPURL(httpServer.URL + "/terraform.io/test/exists/terraform-provider-test_v1.0.0_tos_m68k.zip"), Authentication: packageHashAuthentication{ - RequiredHash: "h1:placeholder-hash", - ValidHashes: []Hash{"h1:placeholder-hash", "h0:unacceptable-hash"}, - Platform: Platform{"tos", "m68k"}, + RequiredHashes: []Hash{"h1:placeholder-hash"}, + AllHashes: []Hash{"h1:placeholder-hash", "h0:unacceptable-hash"}, + Platform: Platform{"tos", "m68k"}, }, } if diff := cmp.Diff(want, got); diff != "" { @@ -133,9 +133,7 @@ func TestHTTPMirrorSource(t *testing.T) { } gotHashes := got.AcceptableHashes() - wantHashes := map[Platform][]Hash{ - tosPlatform: {"h1:placeholder-hash", "h0:unacceptable-hash"}, - } + wantHashes := []Hash{"h1:placeholder-hash", "h0:unacceptable-hash"} if diff := cmp.Diff(wantHashes, gotHashes); diff != "" { t.Errorf("wrong acceptable hashes\n%s", diff) } diff --git a/internal/getproviders/package_authentication.go b/internal/getproviders/package_authentication.go index fec24b73f..32e907dee 100644 --- a/internal/getproviders/package_authentication.go +++ b/internal/getproviders/package_authentication.go @@ -1,6 +1,7 @@ package getproviders import ( + "bufio" "bytes" "crypto/sha256" "encoding/hex" @@ -123,9 +124,10 @@ type PackageAuthentication interface { type PackageAuthenticationHashes interface { PackageAuthentication - // AcceptableHashes returns a set of hash strings that this authenticator - // would accept as valid, grouped by platform. The order of the items - // in each of the slices is not significant, and may contain duplicates + // AcceptableHashes returns a set of hashes that this authenticator + // considers to be valid for the current package or, where possible, + // equivalent packages on other platforms. The order of the items in + // the result is not significant, and it may contain duplicates // that are also not significant. // // This method's result should only be used to create a "lock" for a @@ -138,16 +140,17 @@ type PackageAuthenticationHashes interface { // verifying a signature from the origin registry, depending on what the // hashes are going to be used for. // - // Hashes are returned as strings with hashing scheme prefixes like "h1:" - // to record which hashing scheme the hash was created with. // Implementations of PackageAuthenticationHashes may return multiple // hashes with different schemes, which means that all of them are equally - // acceptable. + // acceptable. Implementors may also return hashes that use schemes the + // current version of the authenticator would not allow but that could be + // accepted by other versions of Terraform, e.g. if a particular hash + // scheme has been deprecated. // // 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][]Hash + AcceptableHashes() []Hash } type packageAuthenticationAll []PackageAuthentication @@ -183,7 +186,7 @@ func (checks packageAuthenticationAll) AuthenticatePackage(localLocation Package return authResult, nil } -func (checks packageAuthenticationAll) AcceptableHashes() map[Platform][]Hash { +func (checks packageAuthenticationAll) AcceptableHashes() []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,42 +205,37 @@ func (checks packageAuthenticationAll) AcceptableHashes() map[Platform][]Hash { } type packageHashAuthentication struct { - RequiredHash Hash - ValidHashes []Hash - Platform Platform + RequiredHashes []Hash + AllHashes []Hash + Platform Platform } // NewPackageHashAuthentication returns a PackageAuthentication implementation -// that checks whether the contents of the package match whichever of the -// given hashes is most preferred by the current version of Terraform. +// that checks whether the contents of the package match whatever subset of the +// given hashes are considered acceptable by the current version of Terraform. // -// This uses the hash algorithms implemented by functions Hash and MatchesHash. -// 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. +// This uses the hash algorithms implemented by functions PackageHash and +// MatchesHash. The PreferredHashes function will select which of the given +// hashes are considered by Terraform to be the strongest verification, and +// authentication succeeds as long as one of those matches. func NewPackageHashAuthentication(platform Platform, validHashes []Hash) PackageAuthentication { requiredHashes := PreferredHashes(validHashes) - // TODO: Update to support multiple hashes - var requiredHash Hash - if len(requiredHashes) > 0 { - requiredHash = requiredHashes[0] - } return packageHashAuthentication{ - RequiredHash: requiredHash, - ValidHashes: validHashes, - Platform: platform, + RequiredHashes: requiredHashes, + AllHashes: validHashes, + Platform: platform, } } func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { - if a.RequiredHash == "" { + if len(a.RequiredHashes) == 0 { // Indicates that none of the hashes given to // NewPackageHashAuthentication were considered to be usable by this // version of Terraform. return nil, fmt.Errorf("this version of Terraform does not support any of the checksum formats given for this provider") } - matches, err := PackageMatchesHash(localLocation, a.RequiredHash) + matches, err := PackageMatchesAnyHash(localLocation, a.RequiredHashes) if err != nil { return nil, fmt.Errorf("failed to verify provider package checksums: %s", err) } @@ -245,13 +243,25 @@ func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLoca if matches { return &PackageAuthenticationResult{result: verifiedChecksum}, nil } - return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHash) + if len(a.RequiredHashes) == 1 { + return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHashes[0].String()) + } + // It's non-ideal that this doesn't actually list the expected checksums, + // but in the many-checksum case the message would get pretty unweildy. + // In practice today we typically use this authenticator only with a + // single hash returned from a network mirror, so the better message + // above will prevail in that case. Maybe we'll improve on this somehow + // if the future introduction of a new hash scheme causes there to more + // commonly be multiple hashes. + return nil, fmt.Errorf("provider package doesn't match the any of the expected checksums") } -func (a packageHashAuthentication) AcceptableHashes() map[Platform][]Hash { - return map[Platform][]Hash{ - a.Platform: a.ValidHashes, - } +func (a packageHashAuthentication) AcceptableHashes() []Hash { + // In this case we include even hashes the current version of Terraform + // doesn't prefer, because this result is used for building a lock file + // and so it's helpful to include older hash formats that other Terraform + // versions might need in order to do authentication successfully. + return a.AllHashes } type archiveHashAuthentication struct { @@ -270,7 +280,7 @@ type archiveHashAuthentication struct { // given localLocation is not PackageLocalArchive. // // NewPackageHashAuthentication is preferable to use when possible because -// it uses the newer hashing scheme (implemented by function Hash) that +// it uses the newer hashing scheme (implemented by function PackageHash) that // can work with both packed and unpacked provider packages. func NewArchiveChecksumAuthentication(platform Platform, wantSHA256Sum [sha256.Size]byte) PackageAuthentication { return archiveHashAuthentication{platform, wantSHA256Sum} @@ -295,10 +305,8 @@ func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLoca return &PackageAuthenticationResult{result: verifiedChecksum}, nil } -func (a archiveHashAuthentication) AcceptableHashes() map[Platform][]Hash { - return map[Platform][]Hash{ - a.Platform: {HashLegacyZipSHAFromSHA(a.WantSHA256Sum)}, - } +func (a archiveHashAuthentication) AcceptableHashes() []Hash { + return []Hash{HashLegacyZipSHAFromSHA(a.WantSHA256Sum)} } type matchingChecksumAuthentication struct { @@ -437,6 +445,51 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( return &PackageAuthenticationResult{result: communityProvider, KeyID: keyID}, nil } +func (s signatureAuthentication) AcceptableHashes() []Hash { + // This is a bit of an abstraction leak because signatureAuthentication + // otherwise just treats the document as an opaque blob that's been + // signed, but here we're making assumptions about its format because + // we only want to trust that _all_ of the checksums are valid (rather + // than just the current platform's one) if we've also verified that the + // bag of checksums is signed. + // + // In recognition of that layering quirk this implementation is intended to + // be somewhat resilient to potentially using this authenticator with + // non-checksums files in future (in which case it'll return nothing at all) + // but it might be better in the long run to instead combine + // signatureAuthentication and matchingChecksumAuthentication together and + // be explicit that the resulting merged authenticator is exclusively for + // checksums files. + + var ret []Hash + sc := bufio.NewScanner(bytes.NewReader(s.Document)) + for sc.Scan() { + parts := bytes.Fields(sc.Bytes()) + if len(parts) != 0 && len(parts) < 2 { + // Doesn't look like a valid sums file line, so we'll assume + // this whole thing isn't a checksums file. + return nil + } + + // If this is a checksums file then the first part should be a + // hex-encoded SHA256 hash, so it should be 64 characters long + // and contain only hex digits. + hashStr := parts[0] + if len(hashStr) != 64 { + return nil // doesn't look like a checksums file + } + + var gotSHA256Sum [sha256.Size]byte + if _, err := hex.Decode(gotSHA256Sum[:], hashStr); err != nil { + return nil // doesn't look like a checksums file + } + + ret = append(ret, HashLegacyZipSHAFromSHA(gotSHA256Sum)) + } + + return ret +} + // findSigningKey attempts to verify the signature using each of the keys // returned by the registry. If a valid signature is found, it returns the // signing key. diff --git a/internal/getproviders/package_authentication_test.go b/internal/getproviders/package_authentication_test.go index fdae37e3d..62d7bbddb 100644 --- a/internal/getproviders/package_authentication_test.go +++ b/internal/getproviders/package_authentication_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" "golang.org/x/crypto/openpgp" ) @@ -388,7 +389,7 @@ func TestSignatureAuthentication_success(t *testing.T) { t.Fatal(err) } - auth := NewSignatureAuthentication([]byte(testShaSums), signature, test.keys) + auth := NewSignatureAuthentication([]byte(testShaSumsPlaceholder), signature, test.keys) result, err := auth.AuthenticatePackage(location) if result == nil || *result != test.result { @@ -468,7 +469,7 @@ func TestSignatureAuthentication_failure(t *testing.T) { t.Fatal(err) } - auth := NewSignatureAuthentication([]byte(testShaSums), signature, test.keys) + auth := NewSignatureAuthentication([]byte(testShaSumsPlaceholder), signature, test.keys) result, err := auth.AuthenticatePackage(location) if result != nil { @@ -481,6 +482,33 @@ func TestSignatureAuthentication_failure(t *testing.T) { } } +func TestSignatureAuthentication_acceptableHashes(t *testing.T) { + auth := NewSignatureAuthentication([]byte(testShaSumsRealistic), nil, nil) + authWithHashes, ok := auth.(PackageAuthenticationHashes) + if !ok { + t.Fatalf("%T does not implement PackageAuthenticationHashes", auth) + } + got := authWithHashes.AcceptableHashes() + want := []Hash{ + // These are the hashes encoded in constant testShaSumsRealistic + "zh:7d7e888fdd28abfe00894f9055209b9eec785153641de98e6852aa071008d4ee", + "zh:f8b6cf9ade087c17826d49d89cef21261cdc22bd27065bbc5b27d7dbf7fbbf6c", + "zh:a5ba9945606bb7bfb821ba303957eeb40dd9ee4e706ba8da1eaf7cbeb0356e63", + "zh:df3a5a8d6ffff7bacf19c92d10d0d500f98169ea17b3764b01a789f563d1aad7", + "zh:086119a26576d06b8281a97e8644380da89ce16197cd955f74ea5ee664e9358b", + "zh:1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239", + "zh:0e9fd0f3e2254b526a0e81e0cfdfc82583b0cd343778c53ead21aa7d52f776d7", + "zh:66a947e7de1c74caf9f584c3ed4e91d2cb1af6fe5ce8abaf1cf8f7ff626a09d1", + "zh:def1b73849bec0dc57a04405847921bf9206c75b52ae9de195476facb26bd85e", + "zh:48f1826ec31d6f104e46cc2022b41f30cd1019ef48eaec9697654ef9ec37a879", + "zh:17e0b496022bc4e4137be15e96d2b051c8acd6e14cb48d9b13b262330464f6cc", + "zh:2696c86228f491bc5425561c45904c9ce39b1c676b1e17734cb2ee6b578c4bcd", + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + const testAuthorKeyID = `37A6AB3BCF2C170A` // testAuthorKeyArmor is test key ID 5BFEEC4317E746008621970637A6AB3BCF2C170A. @@ -554,9 +582,26 @@ SnHodBLlpKLyUXi36DCDy/iKVsieqLsAdcYe0nQFuhoQcOme33A= =aHOG -----END PGP SIGNATURE-----` -// testShaSums is a string that represents the SHA256SUMS file downloaded -// for a release. -const testShaSums = "example shasums data" +// testShaSumsPlaceholder is a string that represents a signed document that +// the signature authenticator will check. Some of the signature valuesin +// other constants in this file are signing this string. +const testShaSumsPlaceholder = "example shasums data" + +// testShaSumsRealistic is a more realistic SHA256SUMS document that we can use +// to test the AcceptableHashes method. The signature values in other constants +// in this file do not sign this string. +const testShaSumsRealistic = `7d7e888fdd28abfe00894f9055209b9eec785153641de98e6852aa071008d4ee terraform_0.14.0-alpha20200923_darwin_amd64.zip +f8b6cf9ade087c17826d49d89cef21261cdc22bd27065bbc5b27d7dbf7fbbf6c terraform_0.14.0-alpha20200923_freebsd_386.zip +a5ba9945606bb7bfb821ba303957eeb40dd9ee4e706ba8da1eaf7cbeb0356e63 terraform_0.14.0-alpha20200923_freebsd_amd64.zip +df3a5a8d6ffff7bacf19c92d10d0d500f98169ea17b3764b01a789f563d1aad7 terraform_0.14.0-alpha20200923_freebsd_arm.zip +086119a26576d06b8281a97e8644380da89ce16197cd955f74ea5ee664e9358b terraform_0.14.0-alpha20200923_linux_386.zip +1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239 terraform_0.14.0-alpha20200923_linux_amd64.zip +0e9fd0f3e2254b526a0e81e0cfdfc82583b0cd343778c53ead21aa7d52f776d7 terraform_0.14.0-alpha20200923_linux_arm.zip +66a947e7de1c74caf9f584c3ed4e91d2cb1af6fe5ce8abaf1cf8f7ff626a09d1 terraform_0.14.0-alpha20200923_openbsd_386.zip +def1b73849bec0dc57a04405847921bf9206c75b52ae9de195476facb26bd85e terraform_0.14.0-alpha20200923_openbsd_amd64.zip +48f1826ec31d6f104e46cc2022b41f30cd1019ef48eaec9697654ef9ec37a879 terraform_0.14.0-alpha20200923_solaris_amd64.zip +17e0b496022bc4e4137be15e96d2b051c8acd6e14cb48d9b13b262330464f6cc terraform_0.14.0-alpha20200923_windows_386.zip +2696c86228f491bc5425561c45904c9ce39b1c676b1e17734cb2ee6b578c4bcd terraform_0.14.0-alpha20200923_windows_amd64.zip` // testAuthorSignatureGoodBase64 is a signature of testShaSums signed with // testAuthorKeyArmor, which represents the SHA256SUMS.sig file downloaded for diff --git a/internal/getproviders/registry_client_test.go b/internal/getproviders/registry_client_test.go index b7cbb2ee8..2078105b6 100644 --- a/internal/getproviders/registry_client_test.go +++ b/internal/getproviders/registry_client_test.go @@ -174,7 +174,7 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) { case "/pkg/awesomesauce/happycloud_1.2.0.zip": resp.Write([]byte("some zip file")) case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS": - resp.Write([]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n")) + resp.Write([]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n000000000000000000000000000000000000000000000000000000000000face happycloud_1.2.0_face.zip\n")) case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS.sig": resp.Write([]byte("GPG signature")) default: diff --git a/internal/getproviders/registry_source_test.go b/internal/getproviders/registry_source_test.go index 58008e2fb..05ce65887 100644 --- a/internal/getproviders/registry_source_test.go +++ b/internal/getproviders/registry_source_test.go @@ -115,11 +115,11 @@ func TestSourcePackageMeta(t *testing.T) { version string os, arch string want PackageMeta - wantHashes map[Platform][]Hash + wantHashes []Hash wantErr string }{ // These test cases are relying on behaviors of the fake provider - // registry server implemented in client_test.go. + // registry server implemented in registry_client_test.go. { "example.com/awesomesauce/happycloud", "1.2.0", @@ -135,13 +135,13 @@ func TestSourcePackageMeta(t *testing.T) { Location: PackageHTTPURL(baseURL + "/pkg/awesomesauce/happycloud_1.2.0.zip"), Authentication: PackageAuthenticationAll( NewMatchingChecksumAuthentication( - []byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"), + []byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n000000000000000000000000000000000000000000000000000000000000face happycloud_1.2.0_face.zip\n"), "happycloud_1.2.0.zip", [32]byte{30: 0xf0, 31: 0x0d}, ), NewArchiveChecksumAuthentication(Platform{"linux", "amd64"}, [32]byte{30: 0xf0, 31: 0x0d}), NewSignatureAuthentication( - []byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"), + []byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n000000000000000000000000000000000000000000000000000000000000face happycloud_1.2.0_face.zip\n"), []byte("GPG signature"), []SigningKey{ {ASCIIArmor: HashicorpPublicKey}, @@ -149,10 +149,9 @@ func TestSourcePackageMeta(t *testing.T) { ), ), }, - map[Platform][]Hash{ - {"linux", "amd64"}: { - "zh:000000000000000000000000000000000000000000000000000000000000f00d", - }, + []Hash{ + "zh:000000000000000000000000000000000000000000000000000000000000f00d", + "zh:000000000000000000000000000000000000000000000000000000000000face", }, ``, }, diff --git a/internal/getproviders/types.go b/internal/getproviders/types.go index 57313e794..c3b93b36f 100644 --- a/internal/getproviders/types.go +++ b/internal/getproviders/types.go @@ -244,9 +244,19 @@ func (m PackageMeta) PackedFilePath(baseDir string) string { return PackedFilePathForPackage(baseDir, m.Provider, m.Version, m.TargetPlatform) } -// AcceptableHashes returns a set of hashes (grouped by target platform) that -// could be recorded for comparison to future results for the same provider -// version, to implement a "trust on first use" scheme. +// AcceptableHashes returns a set of hashes that could be recorded for +// comparison to future results for the same provider version, to implement a +// "trust on first use" scheme. +// +// The AcceptableHashes result is a platform-agnostic set of hashes, with the +// intent that in most cases it will be used as an additional cross-check in +// addition to a platform-specific hash check made during installation. However, +// there are some situations (such as verifying an already-installed package +// that's on local disk) where Terraform would check only against the results +// of this function, meaning that it would in principle accept another +// platform's package as a substitute for the correct platform. That's a +// pragmatic compromise to allow lock files derived from the result of this +// method to be portable across platforms. // // Callers of this method should typically also verify the package using the // object in the Authentication field, and consider how much trust to give @@ -259,7 +269,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][]Hash { +func (m PackageMeta) AcceptableHashes() []Hash { auth, ok := m.Authentication.(PackageAuthenticationHashes) if !ok { return nil