From 146e983c3666913194135860e5284767db0986d9 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 24 Aug 2020 18:17:26 -0700 Subject: [PATCH] internal/getproviders: package authenticator for our new-style hashes Earlier we introduced a new package hashing mechanism that is compatible with both packed and unpacked packages, because it's a hash of the contents of the package rather than of the archive it's delivered in. However, we were using that only for the local selections file and not for any remote package authentication yet. The provider network mirrors protocol includes new-style hashes as a step towards transitioning over to the new hash format in all cases, so this new authenticator is here in preparation for verifying the checksums of packages coming from network mirrors, for mirrors that support them. For now this leaves us in a kinda confusing situation where we have both NewPackageHashAuthentication for the new style and NewArchiveChecksumAuthentication for the old style, which for the moment is represented only by a doc comment on the latter. Hopefully we can remove NewArchiveChecksumAuthentication in a future commit, if we can get the registry updated to use the new hashing format. --- .../getproviders/package_authentication.go | 42 +++++++++++++ .../package_authentication_test.go | 61 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/internal/getproviders/package_authentication.go b/internal/getproviders/package_authentication.go index 987908df6..10f000a26 100644 --- a/internal/getproviders/package_authentication.go +++ b/internal/getproviders/package_authentication.go @@ -114,6 +114,44 @@ func (checks packageAuthenticationAll) AuthenticatePackage(localLocation Package return authResult, nil } +type packageHashAuthentication struct { + RequiredHash string +} + +// 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. +// +// 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. +func NewPackageHashAuthentication(validHashes []string) PackageAuthentication { + requiredHash := PreferredHash(validHashes) + return packageHashAuthentication{ + RequiredHash: requiredHash, + } +} + +func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { + if a.RequiredHash == "" { + // 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) + if err != nil { + return nil, fmt.Errorf("failed to verify provider package checksums: %s", err) + } + + if matches { + return &PackageAuthenticationResult{result: verifiedChecksum}, nil + } + return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHash) +} + type archiveHashAuthentication struct { WantSHA256Sum [sha256.Size]byte } @@ -127,6 +165,10 @@ type archiveHashAuthentication struct { // (represented by PackageLocalDir) does not retain access to the original // source archive. Therefore this authenticator will return an error if its // given localLocation is not PackageLocalArchive. +// +// NewPackageHashAuthentication is preferable to use when possible because +// it uses the newer hashing scheme (implemented by function Hash) that +// can work with both packed and unpacked provider packages. func NewArchiveChecksumAuthentication(wantSHA256Sum [sha256.Size]byte) PackageAuthentication { return archiveHashAuthentication{wantSHA256Sum} } diff --git a/internal/getproviders/package_authentication_test.go b/internal/getproviders/package_authentication_test.go index 521306db5..5b734183d 100644 --- a/internal/getproviders/package_authentication_test.go +++ b/internal/getproviders/package_authentication_test.go @@ -97,6 +97,67 @@ func TestPackageAuthenticationAll_failure(t *testing.T) { } } +// Package hash authentication requires a zip file or directory fixture and a +// known-good set of hashes, of which the authenticator will pick one. The +// result should be "verified checksum". +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{ + // Known-good HashV1 result for this directory + "h1:qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g=", + } + + auth := NewPackageHashAuthentication(wantHashes) + result, err := auth.AuthenticatePackage(location) + + wantResult := PackageAuthenticationResult{result: verifiedChecksum} + if result == nil || *result != wantResult { + t.Errorf("wrong result: got %#v, want %#v", result, wantResult) + } + if err != nil { + t.Errorf("wrong err: got %s, want nil", err) + } +} + +// Package has authentication can fail for various reasons. +func TestPackageHashAuthentication_failure(t *testing.T) { + tests := map[string]struct { + location PackageLocation + err string + }{ + "missing file": { + PackageLocalArchive("testdata/no-package-here.zip"), + "failed to verify provider package checksums: lstat testdata/no-package-here.zip: no such file or directory", + }, + "checksum mismatch": { + PackageLocalDir("testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/2.0.0/linux_amd64"), + "provider package doesn't match the expected checksum \"h1:invalid\"", + }, + "invalid zip file": { + PackageLocalArchive("testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip"), + "failed to verify provider package checksums: zip: not a valid zip file", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + // Empty expected hash, either because we'll error before we + // reach it, or we want to force a checksum mismatch. + auth := NewPackageHashAuthentication([]string{"h1:invalid"}) + result, err := auth.AuthenticatePackage(test.location) + + if result != nil { + t.Errorf("wrong result: got %#v, want nil", result) + } + if gotErr := err.Error(); gotErr != test.err { + t.Errorf("wrong err: got %q, want %q", gotErr, test.err) + } + }) + } +} + // Archive checksum authentication requires a file fixture and a known-good // SHA256 hash. The result should be "verified checksum". func TestArchiveChecksumAuthentication_success(t *testing.T) {