Merge pull request #24617 from hashicorp/alisdair/provider-installer-signature-verification
internal: Verify provider signatures on install
This commit is contained in:
commit
e32e7e2c4b
|
@ -508,6 +508,17 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state
|
|||
fmt.Sprintf("Error while installing %s v%s: %s.", provider.ForDisplay(), version, err),
|
||||
))
|
||||
},
|
||||
FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
|
||||
var warning string
|
||||
if authResult != nil {
|
||||
warning = authResult.Warning
|
||||
}
|
||||
if warning != "" {
|
||||
warning = c.Colorize().Color(fmt.Sprintf("\n [reset][yellow]Warning: %s[reset]", warning))
|
||||
}
|
||||
|
||||
c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s)%s", provider.ForDisplay(), version, authResult, warning))
|
||||
},
|
||||
}
|
||||
|
||||
mode := providercache.InstallNewProvidersOnly
|
||||
|
|
|
@ -945,6 +945,10 @@ func TestInit_providerSource(t *testing.T) {
|
|||
t.Errorf("wrong version selections after upgrade\n%s", diff)
|
||||
}
|
||||
|
||||
outputStr := ui.OutputWriter.String()
|
||||
if want := "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(outputStr, want) {
|
||||
t.Fatalf("unexpected output: %s\nexpected to include %q", outputStr, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit_getUpgradePlugins(t *testing.T) {
|
||||
|
@ -1101,7 +1105,7 @@ func TestInit_getProviderMissing(t *testing.T) {
|
|||
|
||||
args := []string{}
|
||||
if code := c.Run(args); code == 0 {
|
||||
t.Fatalf("expceted error, got output: \n%s", ui.OutputWriter.String())
|
||||
t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String())
|
||||
}
|
||||
|
||||
if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") {
|
||||
|
@ -1619,7 +1623,7 @@ func installFakeProviderPackagesElsewhere(t *testing.T, cacheDir *providercache.
|
|||
if err != nil {
|
||||
t.Fatalf("failed to prepare fake package for %s %s: %s", name, versionStr, err)
|
||||
}
|
||||
err = cacheDir.InstallPackage(context.Background(), meta)
|
||||
_, err = cacheDir.InstallPackage(context.Background(), meta)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to install fake package for %s %s: %s", name, versionStr, err)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ package getproviders
|
|||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
|
@ -168,6 +170,14 @@ func FakeInstallablePackageMeta(provider addrs.Provider, version Version, target
|
|||
return PackageMeta{}, close, fmt.Errorf("failed to close the mock zip file: %s", err)
|
||||
}
|
||||
|
||||
// Compute the SHA256 checksum of the generated file, to allow package
|
||||
// authentication code to be exercised.
|
||||
f.Seek(0, io.SeekStart)
|
||||
h := sha256.New()
|
||||
io.Copy(h, f)
|
||||
checksum := [32]byte{}
|
||||
h.Sum(checksum[:0])
|
||||
|
||||
meta := PackageMeta{
|
||||
Provider: provider,
|
||||
Version: version,
|
||||
|
@ -181,6 +191,8 @@ func FakeInstallablePackageMeta(provider addrs.Provider, version Version, target
|
|||
// (At the time of writing, no caller actually does that, but who
|
||||
// knows what the future holds?)
|
||||
Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),
|
||||
|
||||
Authentication: NewArchiveChecksumAuthentication(checksum),
|
||||
}
|
||||
return meta, close, nil
|
||||
}
|
||||
|
|
|
@ -3,11 +3,60 @@ package getproviders
|
|||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
openpgpArmor "golang.org/x/crypto/openpgp/armor"
|
||||
openpgpErrors "golang.org/x/crypto/openpgp/errors"
|
||||
)
|
||||
|
||||
type packageAuthenticationResult int
|
||||
|
||||
const (
|
||||
verifiedChecksum packageAuthenticationResult = iota
|
||||
officialProvider
|
||||
partnerProvider
|
||||
communityProvider
|
||||
)
|
||||
|
||||
// PackageAuthenticationResult is returned from a PackageAuthentication
|
||||
// implementation. It is a mostly-opaque type intended for use in UI, which
|
||||
// implements Stringer and includes an optional Warning field.
|
||||
//
|
||||
// A failed PackageAuthentication attempt will return an "unauthenticated"
|
||||
// result, which is represented by nil.
|
||||
type PackageAuthenticationResult struct {
|
||||
result packageAuthenticationResult
|
||||
Warning string
|
||||
}
|
||||
|
||||
func (t *PackageAuthenticationResult) String() string {
|
||||
if t == nil {
|
||||
return "unauthenticated"
|
||||
}
|
||||
return []string{
|
||||
"verified checksum",
|
||||
"official provider",
|
||||
"partner provider",
|
||||
"community provider",
|
||||
}[t.result]
|
||||
}
|
||||
|
||||
// SigningKey represents a key used to sign packages from a registry, along
|
||||
// with an optional trust signature from the registry operator. These are
|
||||
// both in ASCII armored OpenPGP format.
|
||||
//
|
||||
// The JSON struct tags represent the field names used by the Registry API.
|
||||
type SigningKey struct {
|
||||
ASCIIArmor string `json:"ascii_armor"`
|
||||
TrustSignature string `json:"trust_signature"`
|
||||
}
|
||||
|
||||
// PackageAuthentication is an interface implemented by the optional package
|
||||
// authentication implementations a source may include on its PackageMeta
|
||||
// objects.
|
||||
|
@ -16,15 +65,14 @@ import (
|
|||
// that a package is what its distributor intended to distribute and that it
|
||||
// has not been tampered with.
|
||||
type PackageAuthentication interface {
|
||||
// AuthenticatePackage takes the metadata about the package as returned
|
||||
// by its original source, and also the "localLocation" where it has
|
||||
// been staged for local inspection (which may or may not be the same
|
||||
// as the original source location) and returns an error if the
|
||||
// authentication checks fail.
|
||||
// AuthenticatePackage takes the local location of a package (which may or
|
||||
// may not be the same as the original source location), and returns a
|
||||
// PackageAuthenticationResult, or an error if the authentication checks
|
||||
// fail.
|
||||
//
|
||||
// The localLocation is guaranteed not to be a PackageHTTPURL: a
|
||||
// remote package will always be staged locally for inspection first.
|
||||
AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error
|
||||
// The local location is guaranteed not to be a PackageHTTPURL: a remote
|
||||
// package will always be staged locally for inspection first.
|
||||
AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error)
|
||||
}
|
||||
|
||||
type packageAuthenticationAll []PackageAuthentication
|
||||
|
@ -34,18 +82,23 @@ type packageAuthenticationAll []PackageAuthentication
|
|||
//
|
||||
// The checks are processed in the order given, so a failure of an earlier
|
||||
// check will prevent execution of a later one.
|
||||
//
|
||||
// The returned result is from the last authentication, so callers should
|
||||
// take care to order the authentications such that the strongest is last.
|
||||
func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentication {
|
||||
return packageAuthenticationAll(checks)
|
||||
}
|
||||
|
||||
func (checks packageAuthenticationAll) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
|
||||
func (checks packageAuthenticationAll) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||||
var authResult *PackageAuthenticationResult
|
||||
for _, check := range checks {
|
||||
err := check.AuthenticatePackage(meta, localLocation)
|
||||
var err error
|
||||
authResult, err = check.AuthenticatePackage(localLocation)
|
||||
if err != nil {
|
||||
return err
|
||||
return authResult, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return authResult, nil
|
||||
}
|
||||
|
||||
type archiveHashAuthentication struct {
|
||||
|
@ -65,29 +118,222 @@ func NewArchiveChecksumAuthentication(wantSHA256Sum [sha256.Size]byte) PackageAu
|
|||
return archiveHashAuthentication{wantSHA256Sum}
|
||||
}
|
||||
|
||||
func (a archiveHashAuthentication) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
|
||||
func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||||
archiveLocation, ok := localLocation.(PackageLocalArchive)
|
||||
if !ok {
|
||||
// A source should not use this authentication type for non-archive
|
||||
// locations.
|
||||
return 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))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
_, err = io.Copy(h, f)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gotHash := h.Sum(nil)
|
||||
if !bytes.Equal(gotHash, a.WantSHA256Sum[:]) {
|
||||
return fmt.Errorf("archive has incorrect SHA-256 checksum %x (expected %x)", gotHash, a.WantSHA256Sum[:])
|
||||
return nil, fmt.Errorf("archive has incorrect SHA-256 checksum %x (expected %x)", gotHash, a.WantSHA256Sum[:])
|
||||
}
|
||||
return nil
|
||||
return &PackageAuthenticationResult{result: verifiedChecksum}, nil
|
||||
}
|
||||
|
||||
type matchingChecksumAuthentication struct {
|
||||
Document []byte
|
||||
Filename string
|
||||
WantSHA256Sum [sha256.Size]byte
|
||||
}
|
||||
|
||||
// NewMatchingChecksumAuthentication returns a PackageAuthentication
|
||||
// implementation that scans a registry-provided SHA256SUMS document for a
|
||||
// specified filename, and compares the SHA256 hash against the expected hash.
|
||||
// This is necessary to ensure that the signed SHA256SUMS document matches the
|
||||
// declared SHA256 hash for the package, and therefore that a valid signature
|
||||
// of this document authenticates the package.
|
||||
//
|
||||
// This authentication always returns a nil result, since it alone cannot offer
|
||||
// any assertions about package integrity. It should be combined with other
|
||||
// authentications to be useful.
|
||||
func NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication {
|
||||
return matchingChecksumAuthentication{
|
||||
Document: document,
|
||||
Filename: filename,
|
||||
WantSHA256Sum: wantSHA256Sum,
|
||||
}
|
||||
}
|
||||
|
||||
func (m matchingChecksumAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) {
|
||||
// Find the checksum in the list with matching filename. The document is
|
||||
// in the form "0123456789abcdef filename.zip".
|
||||
filename := []byte(m.Filename)
|
||||
var checksum []byte
|
||||
for _, line := range bytes.Split(m.Document, []byte("\n")) {
|
||||
parts := bytes.Fields(line)
|
||||
if len(parts) > 1 && bytes.Equal(parts[1], filename) {
|
||||
checksum = parts[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if checksum == nil {
|
||||
return nil, fmt.Errorf("checksum list has no SHA-256 hash for %q", m.Filename)
|
||||
}
|
||||
|
||||
// Decode the ASCII checksum into a byte array for comparison.
|
||||
var gotSHA256Sum [sha256.Size]byte
|
||||
if _, err := hex.Decode(gotSHA256Sum[:], checksum); err != nil {
|
||||
return nil, fmt.Errorf("checksum list has invalid SHA256 hash %q: %s", string(checksum), err)
|
||||
}
|
||||
|
||||
// If the checksums don't match, authentication fails.
|
||||
if !bytes.Equal(gotSHA256Sum[:], m.WantSHA256Sum[:]) {
|
||||
return nil, fmt.Errorf("checksum list has unexpected SHA-256 hash %x (expected %x)", gotSHA256Sum, m.WantSHA256Sum[:])
|
||||
}
|
||||
|
||||
// Success! But this doesn't result in any real authentication, only a
|
||||
// lack of authentication errors, so we return a nil result.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type signatureAuthentication struct {
|
||||
Document []byte
|
||||
Signature []byte
|
||||
Keys []SigningKey
|
||||
}
|
||||
|
||||
// NewSignatureAuthentication returns a PackageAuthentication implementation
|
||||
// that verifies the cryptographic signature for a package against any of the
|
||||
// provided keys.
|
||||
//
|
||||
// The signing key for a package will be auto detected by attempting each key
|
||||
// in turn until one is successful. If such a key is found, there are three
|
||||
// possible successful authentication results:
|
||||
//
|
||||
// 1. If the signing key is the HashiCorp official key, it is an official
|
||||
// provider;
|
||||
// 2. Otherwise, if the signing key has a trust signature from the HashiCorp
|
||||
// Partners key, it is a partner provider;
|
||||
// 3. If neither of the above is true, it is a community provider.
|
||||
//
|
||||
// Any failure in the process of validating the signature will result in an
|
||||
// unauthenticated result.
|
||||
func NewSignatureAuthentication(document, signature []byte, keys []SigningKey) PackageAuthentication {
|
||||
return signatureAuthentication{
|
||||
Document: document,
|
||||
Signature: signature,
|
||||
Keys: keys,
|
||||
}
|
||||
}
|
||||
|
||||
func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) {
|
||||
// Find the key that signed the checksum file. This can fail if there is no
|
||||
// valid signature for any of the provided keys.
|
||||
signingKey, err := s.findSigningKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Verify the signature using the HashiCorp public key. If this succeeds,
|
||||
// this is an official provider.
|
||||
hashicorpKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPublicKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating HashiCorp keyring: %s", err)
|
||||
}
|
||||
_, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature))
|
||||
if err == nil {
|
||||
return &PackageAuthenticationResult{result: officialProvider}, nil
|
||||
}
|
||||
|
||||
// If the signing key has a trust signature, attempt to verify it with the
|
||||
// HashiCorp partners public key.
|
||||
if signingKey.TrustSignature != "" {
|
||||
hashicorpPartnersKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPartnersKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating HashiCorp Partners keyring: %s", err)
|
||||
}
|
||||
|
||||
authorKey, err := openpgpArmor.Decode(strings.NewReader(signingKey.ASCIIArmor))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding signing key: %s", err)
|
||||
}
|
||||
|
||||
trustSignature, err := openpgpArmor.Decode(strings.NewReader(signingKey.TrustSignature))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding trust signature: %s", err)
|
||||
}
|
||||
|
||||
_, err = openpgp.CheckDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error verifying trust signature: %s", err)
|
||||
}
|
||||
|
||||
return &PackageAuthenticationResult{result: partnerProvider}, nil
|
||||
}
|
||||
|
||||
// We have a valid signature, but it's not from the HashiCorp key, and it
|
||||
// also isn't a trusted partner. This is a community provider.
|
||||
// FIXME: we may want to add a more detailed warning here explaining the
|
||||
// difference between partner and community providers.
|
||||
return &PackageAuthenticationResult{result: communityProvider}, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Note: currently the registry only returns one key, but this may change in
|
||||
// the future.
|
||||
func (s signatureAuthentication) findSigningKey() (*SigningKey, error) {
|
||||
for _, key := range s.Keys {
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.ASCIIArmor))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding signing key: %s", err)
|
||||
}
|
||||
|
||||
entity, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature))
|
||||
|
||||
// If the signature issuer does not match the the key, keep trying the
|
||||
// rest of the provided keys.
|
||||
if err == openpgpErrors.ErrUnknownIssuer {
|
||||
continue
|
||||
}
|
||||
|
||||
// Any other signature error is terminal.
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking signature: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Provider signed by %s", entityString(entity))
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
// If none of the provided keys issued the signature, this package is
|
||||
// unsigned. This is currently a terminal authentication error.
|
||||
return nil, fmt.Errorf("authentication signature from unknown issuer")
|
||||
}
|
||||
|
||||
// entityString extracts the key ID and identity name(s) from an openpgp.Entity
|
||||
// for logging.
|
||||
func entityString(entity *openpgp.Entity) string {
|
||||
if entity == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
keyID := "n/a"
|
||||
if entity.PrimaryKey != nil {
|
||||
keyID = entity.PrimaryKey.KeyIdString()
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, identity := range entity.Identities {
|
||||
names = append(names, identity.Name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", keyID, strings.Join(names, ", "))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,566 @@
|
|||
package getproviders
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
)
|
||||
|
||||
func TestPackageAuthenticationResult(t *testing.T) {
|
||||
tests := []struct {
|
||||
result *PackageAuthenticationResult
|
||||
want string
|
||||
}{
|
||||
{
|
||||
nil,
|
||||
"unauthenticated",
|
||||
},
|
||||
{
|
||||
&PackageAuthenticationResult{result: verifiedChecksum},
|
||||
"verified checksum",
|
||||
},
|
||||
{
|
||||
&PackageAuthenticationResult{result: officialProvider},
|
||||
"official provider",
|
||||
},
|
||||
{
|
||||
&PackageAuthenticationResult{result: partnerProvider},
|
||||
"partner provider",
|
||||
},
|
||||
{
|
||||
&PackageAuthenticationResult{result: communityProvider},
|
||||
"community provider",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := test.result.String(); got != test.want {
|
||||
t.Errorf("wrong value: got %q, want %q", got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mockAuthentication is an implementation of the PackageAuthentication
|
||||
// interface which returns fixed values. This is used to test the combining
|
||||
// logic of PackageAuthenticationAll.
|
||||
type mockAuthentication struct {
|
||||
result packageAuthenticationResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (m mockAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||||
if m.err == nil {
|
||||
return &PackageAuthenticationResult{result: m.result}, nil
|
||||
} else {
|
||||
return nil, m.err
|
||||
}
|
||||
}
|
||||
|
||||
var _ PackageAuthentication = (*mockAuthentication)(nil)
|
||||
|
||||
// If all authentications succeed, the returned result should come from the
|
||||
// last authentication.
|
||||
func TestPackageAuthenticationAll_success(t *testing.T) {
|
||||
result, err := PackageAuthenticationAll(
|
||||
&mockAuthentication{result: verifiedChecksum},
|
||||
&mockAuthentication{result: communityProvider},
|
||||
).AuthenticatePackage(nil)
|
||||
|
||||
want := PackageAuthenticationResult{result: communityProvider}
|
||||
if result == nil || *result != want {
|
||||
t.Errorf("wrong result: want %#v, got %#v", want, result)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("wrong err: got %#v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If an authentication fails, its error should be returned along with a nil
|
||||
// result.
|
||||
func TestPackageAuthenticationAll_failure(t *testing.T) {
|
||||
someError := errors.New("some error")
|
||||
result, err := PackageAuthenticationAll(
|
||||
&mockAuthentication{result: verifiedChecksum},
|
||||
&mockAuthentication{err: someError},
|
||||
&mockAuthentication{result: communityProvider},
|
||||
).AuthenticatePackage(nil)
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("wrong result: got %#v, want nil", result)
|
||||
}
|
||||
if err != someError {
|
||||
t.Errorf("wrong err: got %#v, want %#v", err, someError)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Location must be a PackageLocalArchive path
|
||||
location := PackageLocalArchive("testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip")
|
||||
|
||||
// Known-good SHA256 hash for this archive
|
||||
wantSHA256Sum := [sha256.Size]byte{
|
||||
0x4f, 0xb3, 0x98, 0x49, 0xf2, 0xe1, 0x38, 0xeb,
|
||||
0x16, 0xa1, 0x8b, 0xa0, 0xc6, 0x82, 0x63, 0x5d,
|
||||
0x78, 0x1c, 0xb8, 0xc3, 0xb2, 0x59, 0x01, 0xdd,
|
||||
0x5a, 0x79, 0x2a, 0xde, 0x97, 0x11, 0xf5, 0x01,
|
||||
}
|
||||
|
||||
auth := NewArchiveChecksumAuthentication(wantSHA256Sum)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Archive checksum authentication can fail for various reasons. These test
|
||||
// cases are almost exhaustive, missing only an io.Copy error which is
|
||||
// difficult to induce.
|
||||
func TestArchiveChecksumAuthentication_failure(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
location PackageLocation
|
||||
err string
|
||||
}{
|
||||
"missing file": {
|
||||
PackageLocalArchive("testdata/no-package-here.zip"),
|
||||
"open testdata/no-package-here.zip: no such file or directory",
|
||||
},
|
||||
"checksum mismatch": {
|
||||
PackageLocalArchive("testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip"),
|
||||
"archive has incorrect SHA-256 checksum 4fb39849f2e138eb16a18ba0c682635d781cb8c3b25901dd5a792ade9711f501 (expected 0000000000000000000000000000000000000000000000000000000000000000)",
|
||||
},
|
||||
"invalid location": {
|
||||
PackageLocalDir("testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64"),
|
||||
"cannot check archive hash for non-archive location testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Zero expected checksum, either because we'll error before we
|
||||
// reach it, or we want to force a checksum mismatch
|
||||
auth := NewArchiveChecksumAuthentication([sha256.Size]byte{0})
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Matching checksum authentication takes a SHA256SUMS document, an archive
|
||||
// filename, and an expected SHA256 hash. On success both return values should
|
||||
// be nil.
|
||||
func TestMatchingChecksumAuthentication_success(t *testing.T) {
|
||||
// Location is unused
|
||||
location := PackageLocalArchive("testdata/my-package.zip")
|
||||
|
||||
// Two different checksums for other files
|
||||
wantSHA256Sum := [sha256.Size]byte{0xde, 0xca, 0xde}
|
||||
otherSHA256Sum := [sha256.Size]byte{0xc0, 0xff, 0xee}
|
||||
|
||||
document := []byte(
|
||||
fmt.Sprintf(
|
||||
"%x README.txt\n%x my-package.zip\n",
|
||||
otherSHA256Sum,
|
||||
wantSHA256Sum,
|
||||
),
|
||||
)
|
||||
filename := "my-package.zip"
|
||||
|
||||
auth := NewMatchingChecksumAuthentication(document, filename, wantSHA256Sum)
|
||||
result, err := auth.AuthenticatePackage(location)
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("wrong result: got %#v, want nil", result)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("wrong err: got %s, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Matching checksum authentication can fail for three reasons: no checksum
|
||||
// in the document for the filename, invalid checksum value, and non-matching
|
||||
// checksum value.
|
||||
func TestMatchingChecksumAuthentication_failure(t *testing.T) {
|
||||
wantSHA256Sum := [sha256.Size]byte{0xde, 0xca, 0xde}
|
||||
filename := "my-package.zip"
|
||||
|
||||
tests := map[string]struct {
|
||||
document []byte
|
||||
err string
|
||||
}{
|
||||
"no checksum for filename": {
|
||||
[]byte(
|
||||
fmt.Sprintf(
|
||||
"%x README.txt",
|
||||
[sha256.Size]byte{0xbe, 0xef},
|
||||
),
|
||||
),
|
||||
`checksum list has no SHA-256 hash for "my-package.zip"`,
|
||||
},
|
||||
"invalid checksum": {
|
||||
[]byte(
|
||||
fmt.Sprintf(
|
||||
"%s README.txt\n%s my-package.zip",
|
||||
"horses",
|
||||
"chickens",
|
||||
),
|
||||
),
|
||||
`checksum list has invalid SHA256 hash "chickens": encoding/hex: invalid byte: U+0068 'h'`,
|
||||
},
|
||||
"checksum mismatch": {
|
||||
[]byte(
|
||||
fmt.Sprintf(
|
||||
"%x README.txt\n%x my-package.zip",
|
||||
[sha256.Size]byte{0xbe, 0xef},
|
||||
[sha256.Size]byte{0xc0, 0xff, 0xee},
|
||||
),
|
||||
),
|
||||
"checksum list has unexpected SHA-256 hash c0ffee0000000000000000000000000000000000000000000000000000000000 (expected decade0000000000000000000000000000000000000000000000000000000000)",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Location is unused
|
||||
location := PackageLocalArchive("testdata/my-package.zip")
|
||||
|
||||
auth := NewMatchingChecksumAuthentication(test.document, filename, wantSHA256Sum)
|
||||
result, err := auth.AuthenticatePackage(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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Signature authentication takes a checksum document, a signature, and a list
|
||||
// of signing keys. If the document is signed by one of the given keys, the
|
||||
// authentication is successful. The value of the result depends on the signing
|
||||
// key and its trust signature.
|
||||
func TestSignatureAuthentication_success(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
signature string
|
||||
keys []SigningKey
|
||||
result PackageAuthenticationResult
|
||||
}{
|
||||
"official provider": {
|
||||
testHashicorpSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: HashicorpPublicKey,
|
||||
},
|
||||
},
|
||||
PackageAuthenticationResult{result: officialProvider},
|
||||
},
|
||||
"partner provider": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: testAuthorKeyArmor,
|
||||
TrustSignature: testAuthorKeyTrustSignatureArmor,
|
||||
},
|
||||
},
|
||||
PackageAuthenticationResult{result: partnerProvider},
|
||||
},
|
||||
"community provider": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: testAuthorKeyArmor,
|
||||
},
|
||||
},
|
||||
PackageAuthenticationResult{result: communityProvider},
|
||||
},
|
||||
"multiple signing keys": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: HashicorpPartnersKey,
|
||||
},
|
||||
{
|
||||
ASCIIArmor: testAuthorKeyArmor,
|
||||
},
|
||||
},
|
||||
PackageAuthenticationResult{result: communityProvider},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Location is unused
|
||||
location := PackageLocalArchive("testdata/my-package.zip")
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(test.signature)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auth := NewSignatureAuthentication([]byte(testShaSums), signature, test.keys)
|
||||
result, err := auth.AuthenticatePackage(location)
|
||||
|
||||
if result == nil || *result != test.result {
|
||||
t.Errorf("wrong result: got %#v, want %#v", result, test.result)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("wrong err: got %s, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Signature authentication can fail for many reasons, most of which are due
|
||||
// to OpenPGP failures from malformed keys or signatures.
|
||||
func TestSignatureAuthentication_failure(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
signature string
|
||||
keys []SigningKey
|
||||
err string
|
||||
}{
|
||||
"invalid key": {
|
||||
testHashicorpSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: "invalid PGP armor value",
|
||||
},
|
||||
},
|
||||
"error decoding signing key: openpgp: invalid argument: no armored data found",
|
||||
},
|
||||
"invalid signature": {
|
||||
testSignatureBadBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: testAuthorKeyArmor,
|
||||
},
|
||||
},
|
||||
"error checking signature: openpgp: invalid data: signature subpacket truncated",
|
||||
},
|
||||
"no keys match signature": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: HashicorpPublicKey,
|
||||
},
|
||||
},
|
||||
"authentication signature from unknown issuer",
|
||||
},
|
||||
"invalid trust signature": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: testAuthorKeyArmor,
|
||||
TrustSignature: "invalid PGP armor value",
|
||||
},
|
||||
},
|
||||
"error decoding trust signature: EOF",
|
||||
},
|
||||
"unverified trust signature": {
|
||||
testAuthorSignatureGoodBase64,
|
||||
[]SigningKey{
|
||||
{
|
||||
ASCIIArmor: testAuthorKeyArmor,
|
||||
TrustSignature: testOtherKeyTrustSignatureArmor,
|
||||
},
|
||||
},
|
||||
"error verifying trust signature: openpgp: invalid signature: hash tag doesn't match",
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Location is unused
|
||||
location := PackageLocalArchive("testdata/my-package.zip")
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(test.signature)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auth := NewSignatureAuthentication([]byte(testShaSums), signature, test.keys)
|
||||
result, err := auth.AuthenticatePackage(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 %s, want %s", gotErr, test.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testAuthorKeyArmor is test key ID 5BFEEC4317E746008621970637A6AB3BCF2C170A.
|
||||
const testAuthorKeyArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBF5vhgYBCAC40OcC2hEx3yGiLhHMbt7DAVEQ0nZwAWy6oL98niknLumBa1VO
|
||||
nMYshP+o/FKOFatBl8aXhmDo606P6pD9d4Pg/WNehqT7hGNHcAFlm+8qjQAvE5uX
|
||||
Z/na/Np7dmWasCiL5hYyHEnKU/XFpc9KyicbkS7n8igP1LEb8xDD1pMLULQsQHA4
|
||||
258asvtwjoYTZIij1I6bUE178bGFPNCfj+FzQM8nKzPpDVxZ7njN9c2sB9FEdJ1+
|
||||
S9mZQNK5PbJuEAOpD5Jp9BnGE16jsLUhDmvGHBjFZAXMBkNSloEMHhs2ty9lEzoF
|
||||
eJmJx7XCGw+ds1SWp4MsHQPWzXxAlrfa4GMlABEBAAG0R1RlcnJhZm9ybSBUZXN0
|
||||
aW5nIChwbHVnaW4vZGlzY292ZXJ5LykgPHRlcnJhZm9ybSt0ZXN0aW5nQGhhc2hp
|
||||
Y29ycC5jb20+iQFOBBMBCAA4FiEEW/7sQxfnRgCGIZcGN6arO88sFwoFAl5vhgYC
|
||||
GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQN6arO88sFwpWvQf/apaMu4Bm
|
||||
ea8AGjdl9acQhHBpWsyiHLIfZvN11xxN/f3+YITvPXIe2PMgveqNfXxu6PIeZGDb
|
||||
0DBvnBQy/vqmA+sCQ8t8+kIWdfZ1EeM2YcXdmAEtriooLvc85JFYjafLIKSj9N7o
|
||||
V/R/e1BCW/v1/7Je47c+6FSt3HHhwyT5AZ3BCq1zpw6PeCDSQ/gZr3Mvq4CjeLA/
|
||||
K+8TM3KyOF4qBGDvzGzp/t9umQSS2L0ozd90lxJtf5Q8ozqDaBiDo+f/osXT2EvN
|
||||
VwPP/xh/gABkXiNrPylFbeD+XPAC4N7NmYK5aPDzRYXXknP8e9PDMykoJKZ+bSdz
|
||||
F3IZ4q5RDHmmNbkBDQReb4YGAQgAt15e1F8TPQQm1jK8+scypHgfmPHbp7Qsulo1
|
||||
GTcUd8QmhbR4kayuLDEpJYzq6+IoTM4TPqsdVuq/1Nwey9oyK0wXk/SUR29nRIQh
|
||||
3GBg7JVg1YsObsfVTvEflYOdjk8T/Udqs4I6HnmSbtzsaohzybutpWXPUkW8OzFI
|
||||
ATwfVTrrz70Yxs+ly0nSEH2Yf+kg2uYZvv5KsJ3MNENhXnHnlaTy2IfhsxAX0xOG
|
||||
pa9fXV3NzdEbl0mYaEzMi77qRAyIQ9VrIL5F0yY/LlbpLSl6xk2+BB2v3a1Ey6SJ
|
||||
w4/le6AM0wlH2hKPCTlkvM0IvUWjlzrPzCkeu027iVc+fqdyiQARAQABiQE2BBgB
|
||||
CAAgFiEEW/7sQxfnRgCGIZcGN6arO88sFwoFAl5vhgYCGwwACgkQN6arO88sFwqz
|
||||
nAf/eF4oZG9F8sJX01mVdDm/L7Uthe4xjTdl7jwV4ygNX+pCyWrww3qc3qbd3QKg
|
||||
CFqIt/TAPE/OxHxCFuxalQefpOqfxjKzvcktxzWmpgxaWsvHaXiS4bKBPz78N/Ke
|
||||
MUtcjGHyLeSzYPUfjquqDzQxqXidRYhyHGSy9c0NKZ6wCElLZ6KcmCQb4sZxVwfu
|
||||
ssjwAFbPMp1nr0f5SWCJfhTh7QF7lO2ldJaKMlcBM8aebmqFQ52P7ZWOFcgeerng
|
||||
G7Zdrci1KEd943HhzDCsUFz4gJwbvUyiAYb2ddndpUBkYwCB/XrHWPOSnGxHgZoo
|
||||
1gIqed9OV/+s5wKxZPjL0pCStQ==
|
||||
=mYqJ
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
// testAuthorKeyTrustSignatureArmor is a trust signature of the data in
|
||||
// testAuthorKeyArmor signed with HashicorpPartnersKey.
|
||||
const testAuthorKeyTrustSignatureArmor = `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCAAdFiEEUYkGV8Ws20uCMIZWfXLUJo5GYPwFAl5w9+YACgkQfXLUJo5G
|
||||
YPwjRBAAvy9jo3vvetb4qx/z2qhbRH2JbZN9byKuqlIggPzDhhaIsVJVZ9L6H6bE
|
||||
AMgPe/NaH58wfiqMYenulYxj9tZwJORT/OK0Y9ZFXXZk6kWPMNv7TEppyB0wKgqq
|
||||
ORKf07KjDcVQslDG9ARgnvDq2GA4UTHxhT0chKHdIKeDLmTm0VSkfNeOhQIkW7vB
|
||||
S/WT9y78319QJek8OKwJo0Jv0O93rvZZI0JFjXGtP15XNBfObMtPXn3l8qoLzhsv
|
||||
pJJG/u+BsVZ+y1JDQQlHaD1P2TLW/nGymFq12k693IOCmNyaIOa01Wa9B/j3a3RY
|
||||
v4SdkULvJKbttNMNBgIMJ74wZp5EUhEFs68sllrIrmthH8bW2fbcHEQ1g/MJCe3+
|
||||
43c9aoW8yNQmuEe7yre9lgqcJOIOxlb5XEJhH0Lh+8OBi5aHA/5wXGU5WrhWqHCR
|
||||
npXBsNqy2sKUuVkEzvn3Hd6aoKncVLrgNR8xA3VP86jJhawvO+M+YYMr1wOVHc/I
|
||||
PYq9hlyUR8qJ/0RpnaIE1iLbPYfEpGTg7oHORpbQVoZAUwMN/Sdox7sMkqCOb1RJ
|
||||
Cmy9J5o7iiNOoshvps5cxcbsM7LNfbf0vDhWpckAvsQehrS1mfVuFHkIiotVQhH1
|
||||
QXPfvB2cVF/SxMqqHWpnT+8c8klfS03kXSb0BdknrQ4DNPq1H5A=
|
||||
=3A1s
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
// testOtherKeyTrustSignatureArmor is a trust signature of another key (not the
|
||||
// author key), signed with HashicorpPartnersKey.
|
||||
const testOtherKeyTrustSignatureArmor = `-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCAAdFiEEUYkGV8Ws20uCMIZWfXLUJo5GYPwFAl6POvsACgkQfXLUJo5G
|
||||
YPyGihAAomM1kGmrC5KRgWQ+V47r8wFoIkhsTgAYb9ENOzn/RVJt3SJSstcKxfA3
|
||||
7HW5R4kqAoXH1hcPYpUcOcdeAvtZxjGRQ9JgErV8NBg6sR11aQccCzAG4Hy0hWav
|
||||
/jB5NzTEX5JFEXH6WhpWI1avh0l2j6JxO1K1s+5+5PI3KbuO+XSqeZ3QmUz9FwGu
|
||||
pr0J6oYcERupzrpnmgMb5fbkpHfzffR2/MOYdF9Hae4EvDS1b7tokuuKsStNnCm0
|
||||
ge7PFdekwbj/OiQrQlqM1pOw2siPX3ouWCtW8oExm9tAxNw31Bn2g3oaNMkHMqJd
|
||||
hlVUZlqeJMyylUat3cY7GTQONfCnoyUHe/wv8exBUbV3v2glp9y2g9i2XmXkHOrV
|
||||
Z+pnNBc+jdp3a4O0Y8fXXZdjiIolZKY8BbvzheuMrQQIOmw4N3KrZbTpLKuqz8rb
|
||||
h8bqUbU42oWcJmBvzF4NZ4tQ+aFHs4CbOnjfDfS14baQr2Gqo9BqTfrzS5Pbs8lq
|
||||
AhY0r+zi71lQ1rBfgZfjd8zWlOzpDO//nwKhGCqYOWke/C/T6o0zxM0R4uR4zXwT
|
||||
KhvXK8/kK/L8Flaxqme0d5bzXLbsMe9I6I76DY5iNhkiFnnWt4+FhGoIDR03MTKS
|
||||
SnHodBLlpKLyUXi36DCDy/iKVsieqLsAdcYe0nQFuhoQcOme33A=
|
||||
=aHOG
|
||||
-----END PGP SIGNATURE-----`
|
||||
|
||||
// testShaSums is a string that represents the SHA256SUMS file downloaded
|
||||
// for a release.
|
||||
const testShaSums = "example shasums data"
|
||||
|
||||
// testAuthorSignatureGoodBase64 is a signature of testShaSums signed with
|
||||
// testAuthorKeyArmor, which represents the SHA256SUMS.sig file downloaded for
|
||||
// a release.
|
||||
const testAuthorSignatureGoodBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +
|
||||
`FwoFAl5vh7gACgkQN6arO88sFwrAlQf6Al77qzjxNIj+NQNJfBGYUE5jHIgcuWOs1IPRTYUI` +
|
||||
`rHQIUU2RVrdHoAefKTKNzGde653JK/pYTflSV+6ini3/aZZnXlF6t001w3wswmakdwTr0hXx` +
|
||||
`Ez/hHYio72Gpn7+T/L+nl6dKkjeGqd/Kor5x2TY9uYB737ESmAe5T8ZlPaGMFHh0mYlNTeRq` +
|
||||
`4qIKqL6DwddBF4Ju2svn2MeNMGfE358H31mxAl2k4PPrwBTR1sFUCUOzAXVA/g9Ov5Y9ni2G` +
|
||||
`rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` +
|
||||
`n1ayZdaCIw/r4w==`
|
||||
|
||||
// testSignatureBadBase64 is an invalid signature.
|
||||
const testSignatureBadBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +
|
||||
`4qIKqL6DwddBF4Ju2svn2MeNMGfE358H31mxAl2k4PPrwBTR1sFUCUOzAXVA/g9Ov5Y9ni2G` +
|
||||
`rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` +
|
||||
`n1ayZdaCIw/r4w==`
|
||||
|
||||
// testHashicorpSignatureGoodBase64 is a signature of testShaSums signed with
|
||||
// HashicorpPublicKey, which represents the SHA256SUMS.sig file downloaded for
|
||||
// an official release.
|
||||
const testHashicorpSignatureGoodBase64 = `iQFLBAABCAA1FiEEkabn+F0FxlYwvvGJUYUth` +
|
||||
`zSP/EwFAl5w784XHHNlY3VyaXR5QGhhc2hpY29ycC5jb20ACgkQUYUthzSP/EyB8QgAv9ijp` +
|
||||
`kTcoFwDAs+1iEUrcW18h/2cU+bvFtdqNDiffzk7+YJ9ioxeWisPta/Z6hEyhdss2+5L1MNbo` +
|
||||
`oUBLABI+Aebfxa/uYFT2kX6r/eySmlY9kqNVpjXdemOQutS4NNZxdJL7CEbh2qIKCVuyo0ul` +
|
||||
`YrTdDH35vwVyLXImWiZLnrXcT/fXLpQGx/N8PDy6WmCeju5Y5RD7TuntB71eCaCZi7wFe1tR` +
|
||||
`qSoe9tD9A7ONB0rGuCY7BxqUj0S81hhz960YbNR9Q81WoNvF7b5SmcLJ1qJx1yvBLyqya6Su` +
|
||||
`DKjU/YYCh7bwHIYzpk1/nK/7SaTHpisekqojVsfDth4TA+jGA==`
|
||||
|
||||
// entityString function is used for logging the signing key.
|
||||
func TestEntityString(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
entity *openpgp.Entity
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"nil",
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"testAuthorKeyArmor",
|
||||
testReadArmoredEntity(t, testAuthorKeyArmor),
|
||||
"37A6AB3BCF2C170A Terraform Testing (plugin/discovery/) <terraform+testing@hashicorp.com>",
|
||||
},
|
||||
{
|
||||
"HashicorpPublicKey",
|
||||
testReadArmoredEntity(t, HashicorpPublicKey),
|
||||
"51852D87348FFC4C HashiCorp Security <security@hashicorp.com>",
|
||||
},
|
||||
{
|
||||
"HashicorpPartnersKey",
|
||||
testReadArmoredEntity(t, HashicorpPartnersKey),
|
||||
"7D72D4268E4660FC HashiCorp Security (Terraform Partner Signing) <security+terraform@hashicorp.com>",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := entityString(tt.entity)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("expected %s, actual %s", tt.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testReadArmoredEntity(t *testing.T, armor string) *openpgp.Entity {
|
||||
data := strings.NewReader(armor)
|
||||
|
||||
el, err := openpgp.ReadArmoredKeyRing(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if count := len(el); count != 1 {
|
||||
t.Fatalf("expected 1 entity, got %d", count)
|
||||
}
|
||||
|
||||
return el[0]
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package getproviders
|
||||
|
||||
// HashicorpPublicKey is the HashiCorp public key, also available at
|
||||
// https://www.hashicorp.com/security
|
||||
const HashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
|
||||
W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
|
||||
fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
|
||||
3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
|
||||
KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
|
||||
SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
|
||||
cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JATgEEwECACIFAlMORM0CGwMG
|
||||
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFGFLYc0j/xMyWIIAIPhcVqiQ59n
|
||||
Jc07gjUX0SWBJAxEG1lKxfzS4Xp+57h2xxTpdotGQ1fZwsihaIqow337YHQI3q0i
|
||||
SqV534Ms+j/tU7X8sq11xFJIeEVG8PASRCwmryUwghFKPlHETQ8jJ+Y8+1asRydi
|
||||
psP3B/5Mjhqv/uOK+Vy3zAyIpyDOMtIpOVfjSpCplVRdtSTFWBu9Em7j5I2HMn1w
|
||||
sJZnJgXKpybpibGiiTtmnFLOwibmprSu04rsnP4ncdC2XRD4wIjoyA+4PKgX3sCO
|
||||
klEzKryWYBmLkJOMDdo52LttP3279s7XrkLEE7ia0fXa2c12EQ0f0DQ1tGUvyVEW
|
||||
WmJVccm5bq25AQ0EUw5EzQEIANaPUY04/g7AmYkOMjaCZ6iTp9hB5Rsj/4ee/ln9
|
||||
wArzRO9+3eejLWh53FoN1rO+su7tiXJA5YAzVy6tuolrqjM8DBztPxdLBbEi4V+j
|
||||
2tK0dATdBQBHEh3OJApO2UBtcjaZBT31zrG9K55D+CrcgIVEHAKY8Cb4kLBkb5wM
|
||||
skn+DrASKU0BNIV1qRsxfiUdQHZfSqtp004nrql1lbFMLFEuiY8FZrkkQ9qduixo
|
||||
mTT6f34/oiY+Jam3zCK7RDN/OjuWheIPGj/Qbx9JuNiwgX6yRj7OE1tjUx6d8g9y
|
||||
0H1fmLJbb3WZZbuuGFnK6qrE3bGeY8+AWaJAZ37wpWh1p0cAEQEAAYkBHwQYAQIA
|
||||
CQUCUw5EzQIbDAAKCRBRhS2HNI/8TJntCAClU7TOO/X053eKF1jqNW4A1qpxctVc
|
||||
z8eTcY8Om5O4f6a/rfxfNFKn9Qyja/OG1xWNobETy7MiMXYjaa8uUx5iFy6kMVaP
|
||||
0BXJ59NLZjMARGw6lVTYDTIvzqqqwLxgliSDfSnqUhubGwvykANPO+93BBx89MRG
|
||||
unNoYGXtPlhNFrAsB1VR8+EyKLv2HQtGCPSFBhrjuzH3gxGibNDDdFQLxxuJWepJ
|
||||
EK1UbTS4ms0NgZ2Uknqn1WRU1Ki7rE4sTy68iZtWpKQXZEJa0IGnuI2sSINGcXCJ
|
||||
oEIgXTMyCILo34Fa/C6VCm2WBgz9zZO8/rHIiQm1J5zqz0DrDwKBUM9C
|
||||
=LYpS
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
|
||||
// HashicorpPartnersKey is a key created by HashiCorp, used to generate and
|
||||
// verify trust signatures for Partner tier providers.
|
||||
const HashicorpPartnersKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=5/s+
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
@ -172,6 +173,9 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
|
|||
return PackageMeta{}, c.errQueryFailed(provider, errors.New(resp.Status))
|
||||
}
|
||||
|
||||
type SigningKeyList struct {
|
||||
GPGPublicKeys []*SigningKey `json:"gpg_public_keys"`
|
||||
}
|
||||
type ResponseBody struct {
|
||||
Protocols []string `json:"protocols"`
|
||||
OS string `json:"os"`
|
||||
|
@ -180,7 +184,10 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
|
|||
DownloadURL string `json:"download_url"`
|
||||
SHA256Sum string `json:"shasum"`
|
||||
|
||||
// TODO: Other metadata for signature checking
|
||||
SHA256SumsURL string `json:"shasums_url"`
|
||||
SHA256SumsSignatureURL string `json:"shasums_signature_url"`
|
||||
|
||||
SigningKeys SigningKeyList `json:"signing_keys"`
|
||||
}
|
||||
var body ResponseBody
|
||||
|
||||
|
@ -230,6 +237,7 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
|
|||
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
|
||||
)
|
||||
}
|
||||
|
||||
var checksum [sha256.Size]byte
|
||||
_, err = hex.Decode(checksum[:], []byte(body.SHA256Sum))
|
||||
if err != nil {
|
||||
|
@ -238,7 +246,48 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
|
|||
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
|
||||
)
|
||||
}
|
||||
ret.Authentication = NewArchiveChecksumAuthentication(checksum)
|
||||
|
||||
shasumsURL, err := url.Parse(body.SHA256SumsURL)
|
||||
if err != nil {
|
||||
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS URL: %s", err)
|
||||
}
|
||||
shasumsURL = resp.Request.URL.ResolveReference(shasumsURL)
|
||||
if shasumsURL.Scheme != "http" && shasumsURL.Scheme != "https" {
|
||||
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS URL: must use http or https scheme")
|
||||
}
|
||||
document, err := c.getFile(shasumsURL)
|
||||
if err != nil {
|
||||
return PackageMeta{}, c.errQueryFailed(
|
||||
provider,
|
||||
fmt.Errorf("failed to retrieve authentication checksums for provider: %s", err),
|
||||
)
|
||||
}
|
||||
signatureURL, err := url.Parse(body.SHA256SumsSignatureURL)
|
||||
if err != nil {
|
||||
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS signature URL: %s", err)
|
||||
}
|
||||
signatureURL = resp.Request.URL.ResolveReference(signatureURL)
|
||||
if signatureURL.Scheme != "http" && signatureURL.Scheme != "https" {
|
||||
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS signature URL: must use http or https scheme")
|
||||
}
|
||||
signature, err := c.getFile(signatureURL)
|
||||
if err != nil {
|
||||
return PackageMeta{}, c.errQueryFailed(
|
||||
provider,
|
||||
fmt.Errorf("failed to retrieve cryptographic signature for provider: %s", err),
|
||||
)
|
||||
}
|
||||
|
||||
keys := make([]SigningKey, len(body.SigningKeys.GPGPublicKeys))
|
||||
for i, key := range body.SigningKeys.GPGPublicKeys {
|
||||
keys[i] = *key
|
||||
}
|
||||
|
||||
ret.Authentication = PackageAuthenticationAll(
|
||||
NewMatchingChecksumAuthentication(document, body.Filename, checksum),
|
||||
NewArchiveChecksumAuthentication(checksum),
|
||||
NewSignatureAuthentication(document, signature, keys),
|
||||
)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
@ -321,3 +370,22 @@ func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error {
|
|||
HaveCredentials: c.creds != nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *registryClient) getFile(url *url.URL) ([]byte, error) {
|
||||
resp, err := c.httpClient.Get(url.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s", resp.Status)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package getproviders
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -91,6 +92,21 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/pkg/") {
|
||||
switch path {
|
||||
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"))
|
||||
case "/pkg/awesomesauce/happycloud_1.2.0_SHA256SUMS.sig":
|
||||
resp.Write([]byte("GPG signature"))
|
||||
default:
|
||||
resp.WriteHeader(404)
|
||||
resp.Write([]byte("unknown package file download"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "/providers/v1/") {
|
||||
resp.WriteHeader(404)
|
||||
resp.Write([]byte(`not a provider registry endpoint`))
|
||||
|
@ -161,12 +177,31 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
|
|||
resp.Write([]byte(`unsupported OS`))
|
||||
return
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"protocols": []string{"5.0"},
|
||||
"os": pathParts[4],
|
||||
"arch": pathParts[5],
|
||||
"filename": "happycloud_" + pathParts[2] + ".zip",
|
||||
"shasum": "000000000000000000000000000000000000000000000000000000000000f00d",
|
||||
"download_url": "/pkg/awesomesauce/happycloud_" + pathParts[2] + ".zip",
|
||||
"shasums_url": "/pkg/awesomesauce/happycloud_" + pathParts[2] + "_SHA256SUMS",
|
||||
"shasums_signature_url": "/pkg/awesomesauce/happycloud_" + pathParts[2] + "_SHA256SUMS.sig",
|
||||
"signing_keys": map[string]interface{}{
|
||||
"gpg_public_keys": []map[string]interface{}{
|
||||
{
|
||||
"ascii_armor": HashicorpPublicKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
enc, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
resp.WriteHeader(500)
|
||||
resp.Write([]byte("failed to encode body"))
|
||||
}
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
resp.WriteHeader(200)
|
||||
// Note that these version numbers are intentionally misordered
|
||||
// so we can test that the client-side code places them in the
|
||||
// correct order (lowest precedence first).
|
||||
resp.Write([]byte(`{"protocols":["5.0"],"os":"` + pathParts[4] + `","arch":"` + pathParts[5] + `","filename":"happycloud_` + pathParts[2] + `.zip","download_url":"/pkg/happycloud_` + pathParts[2] + `.zip","shasum":"000000000000000000000000000000000000000000000000000000000000f00d"}`))
|
||||
resp.Write(enc)
|
||||
default:
|
||||
resp.WriteHeader(404)
|
||||
resp.Write([]byte(`unknown namespace/provider/version/architecture`))
|
||||
|
|
|
@ -23,7 +23,7 @@ func TestSourceAvailableVersions(t *testing.T) {
|
|||
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",
|
||||
[]string{"1.0.0", "1.2.0"},
|
||||
|
@ -124,8 +124,22 @@ func TestSourcePackageMeta(t *testing.T) {
|
|||
ProtocolVersions: VersionList{versions.MustParseVersion("5.0.0")},
|
||||
TargetPlatform: Platform{"linux", "amd64"},
|
||||
Filename: "happycloud_1.2.0.zip",
|
||||
Location: PackageHTTPURL(baseURL + "/pkg/happycloud_1.2.0.zip"),
|
||||
Authentication: archiveHashAuthentication{[32]uint8{30: 0xf0, 31: 0x0d}}, // fake registry uses a memorable sum
|
||||
Location: PackageHTTPURL(baseURL + "/pkg/awesomesauce/happycloud_1.2.0.zip"),
|
||||
Authentication: PackageAuthenticationAll(
|
||||
NewMatchingChecksumAuthentication(
|
||||
[]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"),
|
||||
"happycloud_1.2.0.zip",
|
||||
[32]byte{30: 0xf0, 31: 0x0d},
|
||||
),
|
||||
NewArchiveChecksumAuthentication([32]byte{30: 0xf0, 31: 0x0d}),
|
||||
NewSignatureAuthentication(
|
||||
[]byte("000000000000000000000000000000000000000000000000000000000000f00d happycloud_1.2.0.zip\n"),
|
||||
[]byte("GPG signature"),
|
||||
[]SigningKey{
|
||||
{ASCIIArmor: HashicorpPublicKey},
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
``,
|
||||
},
|
||||
|
|
|
@ -224,6 +224,7 @@ func (m PackageMeta) UnpackedDirectoryPath(baseDir string) string {
|
|||
// concrete types: PackageLocalArchive, PackageLocalDir, or PackageHTTPURL.
|
||||
type PackageLocation interface {
|
||||
packageLocation()
|
||||
String() string
|
||||
}
|
||||
|
||||
// PackageLocalArchive is the location of a provider distribution archive file
|
||||
|
@ -233,6 +234,7 @@ type PackageLocation interface {
|
|||
type PackageLocalArchive string
|
||||
|
||||
func (p PackageLocalArchive) packageLocation() {}
|
||||
func (p PackageLocalArchive) String() string { return string(p) }
|
||||
|
||||
// PackageLocalDir is the location of a directory containing an unpacked
|
||||
// provider distribution archive in the local filesystem. Its value is a local
|
||||
|
@ -241,12 +243,14 @@ func (p PackageLocalArchive) packageLocation() {}
|
|||
type PackageLocalDir string
|
||||
|
||||
func (p PackageLocalDir) packageLocation() {}
|
||||
func (p PackageLocalDir) String() string { return string(p) }
|
||||
|
||||
// PackageHTTPURL is a provider package location accessible via HTTP.
|
||||
// Its value is a URL string using either the http: scheme or the https: scheme.
|
||||
type PackageHTTPURL string
|
||||
|
||||
func (p PackageHTTPURL) packageLocation() {}
|
||||
func (p PackageHTTPURL) String() string { return string(p) }
|
||||
|
||||
// PackageMetaList is a list of PackageMeta. It's just []PackageMeta with
|
||||
// some methods for convenient sorting and filtering.
|
||||
|
|
|
@ -11,9 +11,9 @@ import (
|
|||
// InstallPackage takes a metadata object describing a package available for
|
||||
// installation, retrieves that package, and installs it into the receiving
|
||||
// cache directory.
|
||||
func (d *Dir) InstallPackage(ctx context.Context, meta getproviders.PackageMeta) error {
|
||||
func (d *Dir) InstallPackage(ctx context.Context, meta getproviders.PackageMeta) (*getproviders.PackageAuthenticationResult, error) {
|
||||
if meta.TargetPlatform != d.targetPlatform {
|
||||
return fmt.Errorf("can't install %s package into cache directory expecting %s", meta.TargetPlatform, d.targetPlatform)
|
||||
return nil, fmt.Errorf("can't install %s package into cache directory expecting %s", meta.TargetPlatform, d.targetPlatform)
|
||||
}
|
||||
newPath := getproviders.UnpackedDirectoryPathForPackage(
|
||||
d.baseDir, meta.Provider, meta.Version, d.targetPlatform,
|
||||
|
@ -23,23 +23,18 @@ func (d *Dir) InstallPackage(ctx context.Context, meta getproviders.PackageMeta)
|
|||
// incorporate any changes we make here.
|
||||
d.metaCache = nil
|
||||
|
||||
// TODO: If meta.Authentication is non-nil, we should call it at some point
|
||||
// in the rest of this process (perhaps inside installFromLocalArchive and
|
||||
// installFromLocalDir, so we already have the local copy?) and return an
|
||||
// error if the authentication fails.
|
||||
|
||||
log.Printf("[TRACE] providercache.Dir.InstallPackage: installing %s v%s from %s", meta.Provider, meta.Version, meta.Location)
|
||||
switch location := meta.Location.(type) {
|
||||
switch meta.Location.(type) {
|
||||
case getproviders.PackageHTTPURL:
|
||||
return installFromHTTPURL(ctx, string(location), newPath)
|
||||
return installFromHTTPURL(ctx, meta, newPath)
|
||||
case getproviders.PackageLocalArchive:
|
||||
return installFromLocalArchive(ctx, string(location), newPath)
|
||||
return installFromLocalArchive(ctx, meta, newPath)
|
||||
case getproviders.PackageLocalDir:
|
||||
return installFromLocalDir(ctx, string(location), newPath)
|
||||
return installFromLocalDir(ctx, meta, newPath)
|
||||
default:
|
||||
// Should not get here, because the above should be exhaustive for
|
||||
// all implementations of getproviders.Location.
|
||||
return fmt.Errorf("don't know how to install from a %T location", location)
|
||||
return nil, fmt.Errorf("don't know how to install from a %T location", meta.Location)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,5 +62,20 @@ func (d *Dir) LinkFromOtherCache(entry *CachedProvider) error {
|
|||
// We re-use the process of installing from a local directory here, because
|
||||
// the two operations are fundamentally the same: symlink if possible,
|
||||
// deep-copy otherwise.
|
||||
return installFromLocalDir(context.TODO(), currentPath, newPath)
|
||||
meta := getproviders.PackageMeta{
|
||||
Provider: entry.Provider,
|
||||
Version: entry.Version,
|
||||
|
||||
// FIXME: How do we populate this?
|
||||
ProtocolVersions: nil,
|
||||
TargetPlatform: d.targetPlatform,
|
||||
|
||||
// Because this is already unpacked, the filename is synthetic
|
||||
// based on the standard naming scheme.
|
||||
Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip",
|
||||
entry.Provider.Type, entry.Version, d.targetPlatform),
|
||||
Location: getproviders.PackageLocalDir(currentPath),
|
||||
}
|
||||
_, err := installFromLocalDir(context.TODO(), meta, newPath)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package providercache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
@ -12,6 +13,61 @@ import (
|
|||
"github.com/hashicorp/terraform/internal/getproviders"
|
||||
)
|
||||
|
||||
func TestInstallPackage(t *testing.T) {
|
||||
tmpDirPath, err := ioutil.TempDir("", "terraform-test-providercache")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDirPath)
|
||||
|
||||
linuxPlatform := getproviders.Platform{
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
}
|
||||
nullProvider := addrs.NewProvider(
|
||||
addrs.DefaultRegistryHost, "hashicorp", "null",
|
||||
)
|
||||
|
||||
tmpDir := newDirWithPlatform(tmpDirPath, linuxPlatform)
|
||||
|
||||
meta := getproviders.PackageMeta{
|
||||
Provider: nullProvider,
|
||||
Version: versions.MustParseVersion("2.1.0"),
|
||||
|
||||
ProtocolVersions: getproviders.VersionList{versions.MustParseVersion("5.0.0")},
|
||||
TargetPlatform: linuxPlatform,
|
||||
|
||||
Filename: "terraform-provider-null_2.1.0_linux_amd64.zip",
|
||||
Location: getproviders.PackageLocalArchive("testdata/terraform-provider-null_2.1.0_linux_amd64.zip"),
|
||||
}
|
||||
|
||||
result, err := tmpDir.InstallPackage(context.TODO(), meta)
|
||||
if err != nil {
|
||||
t.Fatalf("InstallPackage failed: %s", err)
|
||||
}
|
||||
if result != nil {
|
||||
t.Errorf("unexpected result %#v, wanted nil", result)
|
||||
}
|
||||
|
||||
// Now we should see the same version reflected in the temporary directory.
|
||||
got := tmpDir.AllAvailablePackages()
|
||||
want := map[addrs.Provider][]CachedProvider{
|
||||
nullProvider: {
|
||||
CachedProvider{
|
||||
Provider: nullProvider,
|
||||
|
||||
Version: versions.MustParseVersion("2.1.0"),
|
||||
|
||||
PackageDir: tmpDirPath + "/registry.terraform.io/hashicorp/null/2.1.0/linux_amd64",
|
||||
ExecutableFile: tmpDirPath + "/registry.terraform.io/hashicorp/null/2.1.0/linux_amd64/terraform-provider-null",
|
||||
},
|
||||
},
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("wrong cache contents after install\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkFromOtherCache(t *testing.T) {
|
||||
srcDirPath := "testdata/cachedir"
|
||||
tmpDirPath, err := ioutil.TempDir("", "terraform-test-providercache")
|
||||
|
|
|
@ -315,7 +315,7 @@ NeedProvider:
|
|||
installTo = i.targetDir
|
||||
linkTo = nil // no linking needed
|
||||
}
|
||||
err = installTo.InstallPackage(ctx, meta)
|
||||
authResult, err := installTo.InstallPackage(ctx, meta)
|
||||
if err != nil {
|
||||
// TODO: Consider retrying for certain kinds of error that seem
|
||||
// likely to be transient. For now, we just treat all errors equally.
|
||||
|
@ -350,7 +350,7 @@ NeedProvider:
|
|||
}
|
||||
selected[provider] = version
|
||||
if cb := evts.FetchPackageSuccess; cb != nil {
|
||||
cb(provider, version, new.PackageDir)
|
||||
cb(provider, version, new.PackageDir, authResult)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ type InstallerEvents struct {
|
|||
// signals a failure that the installer is considering transient.
|
||||
FetchPackageMeta func(provider addrs.Provider, version getproviders.Version) // fetching metadata prior to real download
|
||||
FetchPackageBegin func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation)
|
||||
FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string)
|
||||
FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult)
|
||||
FetchPackageRetry func(provider addrs.Provider, version getproviders.Version, err error)
|
||||
FetchPackageFailure func(provider addrs.Provider, version getproviders.Version, err error)
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/hashicorp/terraform/httpclient"
|
||||
"github.com/hashicorp/terraform/internal/copydir"
|
||||
"github.com/hashicorp/terraform/internal/getproviders"
|
||||
)
|
||||
|
||||
// We borrow the "unpack a zip file into a target directory" logic from
|
||||
|
@ -21,7 +22,9 @@ import (
|
|||
// specific protocol and set of expectations.)
|
||||
var unzip = getter.ZipDecompressor{}
|
||||
|
||||
func installFromHTTPURL(ctx context.Context, url string, targetDir string) error {
|
||||
func installFromHTTPURL(ctx context.Context, meta getproviders.PackageMeta, targetDir string) (*getproviders.PackageAuthenticationResult, error) {
|
||||
url := meta.Location.String()
|
||||
|
||||
// When we're installing from an HTTP URL we expect the URL to refer to
|
||||
// a zip file. We'll fetch that into a temporary file here and then
|
||||
// delegate to installFromLocalArchive below to actually extract it.
|
||||
|
@ -33,21 +36,21 @@ func installFromHTTPURL(ctx context.Context, url string, targetDir string) error
|
|||
httpClient := httpclient.New()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid provider download request: %s", err)
|
||||
return nil, fmt.Errorf("invalid provider download request: %s", err)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unsuccessful request to %s: %s", url, resp.Status)
|
||||
return nil, fmt.Errorf("unsuccessful request to %s: %s", url, resp.Status)
|
||||
}
|
||||
|
||||
f, err := ioutil.TempFile("", "terraform-provider")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open temporary file to download from %s", url)
|
||||
return nil, fmt.Errorf("failed to open temporary file to download from %s", url)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
@ -58,31 +61,69 @@ func installFromHTTPURL(ctx context.Context, url string, targetDir string) error
|
|||
err = fmt.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, n)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we managed to download successfully then we can now delegate to
|
||||
// installFromLocalArchive for extraction.
|
||||
archiveFilename := f.Name()
|
||||
return installFromLocalArchive(ctx, archiveFilename, targetDir)
|
||||
localLocation := getproviders.PackageLocalArchive(archiveFilename)
|
||||
|
||||
var authResult *getproviders.PackageAuthenticationResult
|
||||
if meta.Authentication != nil {
|
||||
if authResult, err = meta.Authentication.AuthenticatePackage(localLocation); err != nil {
|
||||
return authResult, err
|
||||
}
|
||||
}
|
||||
|
||||
// We can now delegate to installFromLocalArchive for extraction. To do so,
|
||||
// we construct a new package meta description using the local archive
|
||||
// path as the location, and skipping authentication.
|
||||
localMeta := getproviders.PackageMeta{
|
||||
Provider: meta.Provider,
|
||||
Version: meta.Version,
|
||||
ProtocolVersions: meta.ProtocolVersions,
|
||||
TargetPlatform: meta.TargetPlatform,
|
||||
Filename: meta.Filename,
|
||||
Location: localLocation,
|
||||
Authentication: nil,
|
||||
}
|
||||
if _, err := installFromLocalArchive(ctx, localMeta, targetDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authResult, nil
|
||||
}
|
||||
|
||||
func installFromLocalArchive(ctx context.Context, filename string, targetDir string) error {
|
||||
return unzip.Decompress(targetDir, filename, true)
|
||||
func installFromLocalArchive(ctx context.Context, meta getproviders.PackageMeta, targetDir string) (*getproviders.PackageAuthenticationResult, error) {
|
||||
var authResult *getproviders.PackageAuthenticationResult
|
||||
if meta.Authentication != nil {
|
||||
var err error
|
||||
if authResult, err = meta.Authentication.AuthenticatePackage(meta.Location); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
filename := meta.Location.String()
|
||||
|
||||
err := unzip.Decompress(targetDir, filename, true)
|
||||
if err != nil {
|
||||
return authResult, err
|
||||
}
|
||||
|
||||
return authResult, nil
|
||||
}
|
||||
|
||||
// installFromLocalDir is the implementation of both installing a package from
|
||||
// a local directory source _and_ of linking a package from another cache
|
||||
// in LinkFromOtherCache, because they both do fundamentally the same
|
||||
// operation: symlink if possible, or deep-copy otherwise.
|
||||
func installFromLocalDir(ctx context.Context, sourceDir string, targetDir string) error {
|
||||
func installFromLocalDir(ctx context.Context, meta getproviders.PackageMeta, targetDir string) (*getproviders.PackageAuthenticationResult, error) {
|
||||
sourceDir := meta.Location.String()
|
||||
|
||||
absNew, err := filepath.Abs(targetDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make target path %s absolute: %s", targetDir, err)
|
||||
return nil, fmt.Errorf("failed to make target path %s absolute: %s", targetDir, err)
|
||||
}
|
||||
absCurrent, err := filepath.Abs(sourceDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make source path %s absolute: %s", sourceDir, err)
|
||||
return nil, fmt.Errorf("failed to make source path %s absolute: %s", sourceDir, err)
|
||||
}
|
||||
|
||||
// Before we do anything else, we'll do a quick check to make sure that
|
||||
|
@ -90,15 +131,15 @@ func installFromLocalDir(ctx context.Context, sourceDir string, targetDir string
|
|||
// disk. This compares the files by their OS-level device and directory
|
||||
// entry identifiers, not by their virtual filesystem paths.
|
||||
if same, err := copydir.SameFile(absNew, absCurrent); same {
|
||||
return fmt.Errorf("cannot install existing provider directory %s to itself", targetDir)
|
||||
return nil, fmt.Errorf("cannot install existing provider directory %s to itself", targetDir)
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to determine if %s and %s are the same: %s", sourceDir, targetDir, err)
|
||||
return nil, fmt.Errorf("failed to determine if %s and %s are the same: %s", sourceDir, targetDir, err)
|
||||
}
|
||||
|
||||
// Delete anything that's already present at this path first.
|
||||
err = os.RemoveAll(targetDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove existing %s before linking it to %s: %s", sourceDir, targetDir, err)
|
||||
return nil, fmt.Errorf("failed to remove existing %s before linking it to %s: %s", sourceDir, targetDir, err)
|
||||
}
|
||||
|
||||
// We'll prefer to create a symlink if possible, but we'll fall back to
|
||||
|
@ -117,22 +158,22 @@ func installFromLocalDir(ctx context.Context, sourceDir string, targetDir string
|
|||
parentDir := filepath.Dir(absNew)
|
||||
err = os.MkdirAll(parentDir, 0755)
|
||||
if err != nil && os.IsExist(err) {
|
||||
return fmt.Errorf("failed to create parent directories leading to %s: %s", targetDir, err)
|
||||
return nil, fmt.Errorf("failed to create parent directories leading to %s: %s", targetDir, err)
|
||||
}
|
||||
|
||||
err = os.Symlink(linkTarget, absNew)
|
||||
if err == nil {
|
||||
// Success, then!
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If we get down here then symlinking failed and we need a deep copy
|
||||
// instead.
|
||||
err = copydir.CopyDir(absNew, absCurrent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to either symlink or copy %s to %s: %s", absCurrent, absNew, err)
|
||||
return nil, fmt.Errorf("failed to either symlink or copy %s to %s: %s", absCurrent, absNew, err)
|
||||
}
|
||||
|
||||
// If we got here then apparently our copy succeeded, so we're done.
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
|
BIN
internal/providercache/testdata/terraform-provider-null_2.1.0_linux_amd64.zip
vendored
Normal file
BIN
internal/providercache/testdata/terraform-provider-null_2.1.0_linux_amd64.zip
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue