internal: Verify provider signatures on install
Providers installed from the registry are accompanied by a list of checksums (the "SHA256SUMS" file), which is cryptographically signed to allow package authentication. The process of verifying this has multiple steps: - First we must verify that the SHA256 hash of the package archive matches the expected hash. This could be done for local installations too, in the future. - Next we ensure that the expected hash returned as part of the registry API response matches an entry in the checksum list. - Finally we verify the cryptographic signature of the checksum list, using the public keys provided by the registry. Each of these steps is implemented as a separate PackageAuthentication type. The local archive installation mechanism uses only the archive checksum authenticator, and the HTTP installation uses all three in the order given. The package authentication system now also returns a result value, which is used by command/init to display the result of the authentication process. There are three tiers of signature, each of which is presented differently to the user: - Signatures from the embedded HashiCorp public key indicate that the provider is officially supported by HashiCorp; - If the signing key is not from HashiCorp, it may have an associated trust signature, which indicates that the provider is from one of HashiCorp's trusted partners; - Otherwise, if the signature is valid, this is a community provider.
This commit is contained in:
parent
f09ae6f862
commit
a5b3d497cc
|
@ -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