getproviders: Prepare for having multiple valid hashes per package

As we continue iterating towards saving valid hashes for a package in a
depsfile lock file after installation and verifying them on future
installation, this prepares getproviders for the possibility of having
multiple valid hashes per package.

This will arise in future commits for two reasons:
- We will need to support both the legacy "zip hash" hashing scheme and
  the new-style content-based hashing scheme because currently the
  registry protocol is only able to produce the legacy scheme, but our
  other installation sources prefer the content-based scheme. Therefore
  packages will typically have a mixture of hashes of both types.
- Installing from an upstream registry will save the hashes for the
  packages across all supported platforms, rather than just the current
  platform, and we'll consider all of those valid for future installation
  if we see both successful matching of the current platform checksum and
  a signature verification for the checksums file as a whole.

This also includes some more preparation for the second case above in that
signatureAuthentication now supports AcceptableHashes and returns all of
the zip-based hashes it can find in the checksums file. This is a bit of
an abstraction leak because previously that authenticator considered its
"document" to just be opaque bytes, but we want to make sure that we can
only end up trusting _all_ of the hashes if we've verified that the
document is signed. Hopefully we'll make this better in a future commit
with some refactoring, but that's deferred for now in order to minimize
disruption to existing codepaths while we work towards a provider locking
MVP.
This commit is contained in:
Martin Atkins 2020-09-23 16:23:00 -07:00
parent 6694cfaa0e
commit ef64df950c
7 changed files with 244 additions and 63 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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",
},
``,
},

View File

@ -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