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),
|
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
|
mode := providercache.InstallNewProvidersOnly
|
||||||
|
|
|
@ -945,6 +945,10 @@ func TestInit_providerSource(t *testing.T) {
|
||||||
t.Errorf("wrong version selections after upgrade\n%s", diff)
|
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) {
|
func TestInit_getUpgradePlugins(t *testing.T) {
|
||||||
|
@ -1101,7 +1105,7 @@ func TestInit_getProviderMissing(t *testing.T) {
|
||||||
|
|
||||||
args := []string{}
|
args := []string{}
|
||||||
if code := c.Run(args); code == 0 {
|
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") {
|
if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") {
|
||||||
|
@ -1619,7 +1623,7 @@ func installFakeProviderPackagesElsewhere(t *testing.T, cacheDir *providercache.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to prepare fake package for %s %s: %s", name, versionStr, err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("failed to install fake package for %s %s: %s", name, versionStr, err)
|
t.Fatalf("failed to install fake package for %s %s: %s", name, versionStr, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ package getproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"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)
|
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{
|
meta := PackageMeta{
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Version: version,
|
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
|
// (At the time of writing, no caller actually does that, but who
|
||||||
// knows what the future holds?)
|
// knows what the future holds?)
|
||||||
Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),
|
Filename: fmt.Sprintf("terraform-provider-%s_%s_%s.zip", provider.Type, version.String(), target.String()),
|
||||||
|
|
||||||
|
Authentication: NewArchiveChecksumAuthentication(checksum),
|
||||||
}
|
}
|
||||||
return meta, close, nil
|
return meta, close, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,60 @@ package getproviders
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"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
|
// PackageAuthentication is an interface implemented by the optional package
|
||||||
// authentication implementations a source may include on its PackageMeta
|
// authentication implementations a source may include on its PackageMeta
|
||||||
// objects.
|
// objects.
|
||||||
|
@ -16,15 +65,14 @@ import (
|
||||||
// that a package is what its distributor intended to distribute and that it
|
// that a package is what its distributor intended to distribute and that it
|
||||||
// has not been tampered with.
|
// has not been tampered with.
|
||||||
type PackageAuthentication interface {
|
type PackageAuthentication interface {
|
||||||
// AuthenticatePackage takes the metadata about the package as returned
|
// AuthenticatePackage takes the local location of a package (which may or
|
||||||
// by its original source, and also the "localLocation" where it has
|
// may not be the same as the original source location), and returns a
|
||||||
// been staged for local inspection (which may or may not be the same
|
// PackageAuthenticationResult, or an error if the authentication checks
|
||||||
// as the original source location) and returns an error if the
|
// fail.
|
||||||
// authentication checks fail.
|
|
||||||
//
|
//
|
||||||
// The localLocation is guaranteed not to be a PackageHTTPURL: a
|
// The local location is guaranteed not to be a PackageHTTPURL: a remote
|
||||||
// remote package will always be staged locally for inspection first.
|
// package will always be staged locally for inspection first.
|
||||||
AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error
|
AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type packageAuthenticationAll []PackageAuthentication
|
type packageAuthenticationAll []PackageAuthentication
|
||||||
|
@ -34,18 +82,23 @@ type packageAuthenticationAll []PackageAuthentication
|
||||||
//
|
//
|
||||||
// The checks are processed in the order given, so a failure of an earlier
|
// The checks are processed in the order given, so a failure of an earlier
|
||||||
// check will prevent execution of a later one.
|
// 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 {
|
func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentication {
|
||||||
return packageAuthenticationAll(checks)
|
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 {
|
for _, check := range checks {
|
||||||
err := check.AuthenticatePackage(meta, localLocation)
|
var err error
|
||||||
|
authResult, err = check.AuthenticatePackage(localLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return authResult, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return authResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type archiveHashAuthentication struct {
|
type archiveHashAuthentication struct {
|
||||||
|
@ -65,29 +118,222 @@ func NewArchiveChecksumAuthentication(wantSHA256Sum [sha256.Size]byte) PackageAu
|
||||||
return archiveHashAuthentication{wantSHA256Sum}
|
return archiveHashAuthentication{wantSHA256Sum}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a archiveHashAuthentication) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
|
func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||||||
archiveLocation, ok := localLocation.(PackageLocalArchive)
|
archiveLocation, ok := localLocation.(PackageLocalArchive)
|
||||||
if !ok {
|
if !ok {
|
||||||
// A source should not use this authentication type for non-archive
|
// A source should not use this authentication type for non-archive
|
||||||
// locations.
|
// 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))
|
f, err := os.Open(string(archiveLocation))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
_, err = io.Copy(h, f)
|
_, err = io.Copy(h, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
gotHash := h.Sum(nil)
|
gotHash := h.Sum(nil)
|
||||||
if !bytes.Equal(gotHash, a.WantSHA256Sum[:]) {
|
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"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
@ -172,6 +173,9 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
|
||||||
return PackageMeta{}, c.errQueryFailed(provider, errors.New(resp.Status))
|
return PackageMeta{}, c.errQueryFailed(provider, errors.New(resp.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SigningKeyList struct {
|
||||||
|
GPGPublicKeys []*SigningKey `json:"gpg_public_keys"`
|
||||||
|
}
|
||||||
type ResponseBody struct {
|
type ResponseBody struct {
|
||||||
Protocols []string `json:"protocols"`
|
Protocols []string `json:"protocols"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
|
@ -180,7 +184,10 @@ func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, t
|
||||||
DownloadURL string `json:"download_url"`
|
DownloadURL string `json:"download_url"`
|
||||||
SHA256Sum string `json:"shasum"`
|
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
|
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),
|
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var checksum [sha256.Size]byte
|
var checksum [sha256.Size]byte
|
||||||
_, err = hex.Decode(checksum[:], []byte(body.SHA256Sum))
|
_, err = hex.Decode(checksum[:], []byte(body.SHA256Sum))
|
||||||
if err != nil {
|
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),
|
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
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
@ -321,3 +370,22 @@ func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error {
|
||||||
HaveCredentials: c.creds != nil,
|
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
|
package getproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -91,6 +92,21 @@ func fakeRegistryHandler(resp http.ResponseWriter, req *http.Request) {
|
||||||
return
|
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/") {
|
if !strings.HasPrefix(path, "/providers/v1/") {
|
||||||
resp.WriteHeader(404)
|
resp.WriteHeader(404)
|
||||||
resp.Write([]byte(`not a provider registry endpoint`))
|
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`))
|
resp.Write([]byte(`unsupported OS`))
|
||||||
return
|
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.Header().Set("Content-Type", "application/json")
|
||||||
resp.WriteHeader(200)
|
resp.WriteHeader(200)
|
||||||
// Note that these version numbers are intentionally misordered
|
resp.Write(enc)
|
||||||
// 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"}`))
|
|
||||||
default:
|
default:
|
||||||
resp.WriteHeader(404)
|
resp.WriteHeader(404)
|
||||||
resp.Write([]byte(`unknown namespace/provider/version/architecture`))
|
resp.Write([]byte(`unknown namespace/provider/version/architecture`))
|
||||||
|
|
|
@ -23,7 +23,7 @@ func TestSourceAvailableVersions(t *testing.T) {
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
// These test cases are relying on behaviors of the fake provider
|
// 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",
|
"example.com/awesomesauce/happycloud",
|
||||||
[]string{"1.0.0", "1.2.0"},
|
[]string{"1.0.0", "1.2.0"},
|
||||||
|
@ -124,8 +124,22 @@ func TestSourcePackageMeta(t *testing.T) {
|
||||||
ProtocolVersions: VersionList{versions.MustParseVersion("5.0.0")},
|
ProtocolVersions: VersionList{versions.MustParseVersion("5.0.0")},
|
||||||
TargetPlatform: Platform{"linux", "amd64"},
|
TargetPlatform: Platform{"linux", "amd64"},
|
||||||
Filename: "happycloud_1.2.0.zip",
|
Filename: "happycloud_1.2.0.zip",
|
||||||
Location: PackageHTTPURL(baseURL + "/pkg/happycloud_1.2.0.zip"),
|
Location: PackageHTTPURL(baseURL + "/pkg/awesomesauce/happycloud_1.2.0.zip"),
|
||||||
Authentication: archiveHashAuthentication{[32]uint8{30: 0xf0, 31: 0x0d}}, // fake registry uses a memorable sum
|
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.
|
// concrete types: PackageLocalArchive, PackageLocalDir, or PackageHTTPURL.
|
||||||
type PackageLocation interface {
|
type PackageLocation interface {
|
||||||
packageLocation()
|
packageLocation()
|
||||||
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageLocalArchive is the location of a provider distribution archive file
|
// PackageLocalArchive is the location of a provider distribution archive file
|
||||||
|
@ -233,6 +234,7 @@ type PackageLocation interface {
|
||||||
type PackageLocalArchive string
|
type PackageLocalArchive string
|
||||||
|
|
||||||
func (p PackageLocalArchive) packageLocation() {}
|
func (p PackageLocalArchive) packageLocation() {}
|
||||||
|
func (p PackageLocalArchive) String() string { return string(p) }
|
||||||
|
|
||||||
// PackageLocalDir is the location of a directory containing an unpacked
|
// PackageLocalDir is the location of a directory containing an unpacked
|
||||||
// provider distribution archive in the local filesystem. Its value is a local
|
// provider distribution archive in the local filesystem. Its value is a local
|
||||||
|
@ -241,12 +243,14 @@ func (p PackageLocalArchive) packageLocation() {}
|
||||||
type PackageLocalDir string
|
type PackageLocalDir string
|
||||||
|
|
||||||
func (p PackageLocalDir) packageLocation() {}
|
func (p PackageLocalDir) packageLocation() {}
|
||||||
|
func (p PackageLocalDir) String() string { return string(p) }
|
||||||
|
|
||||||
// PackageHTTPURL is a provider package location accessible via HTTP.
|
// PackageHTTPURL is a provider package location accessible via HTTP.
|
||||||
// Its value is a URL string using either the http: scheme or the https: scheme.
|
// Its value is a URL string using either the http: scheme or the https: scheme.
|
||||||
type PackageHTTPURL string
|
type PackageHTTPURL string
|
||||||
|
|
||||||
func (p PackageHTTPURL) packageLocation() {}
|
func (p PackageHTTPURL) packageLocation() {}
|
||||||
|
func (p PackageHTTPURL) String() string { return string(p) }
|
||||||
|
|
||||||
// PackageMetaList is a list of PackageMeta. It's just []PackageMeta with
|
// PackageMetaList is a list of PackageMeta. It's just []PackageMeta with
|
||||||
// some methods for convenient sorting and filtering.
|
// some methods for convenient sorting and filtering.
|
||||||
|
|
|
@ -11,9 +11,9 @@ import (
|
||||||
// InstallPackage takes a metadata object describing a package available for
|
// InstallPackage takes a metadata object describing a package available for
|
||||||
// installation, retrieves that package, and installs it into the receiving
|
// installation, retrieves that package, and installs it into the receiving
|
||||||
// cache directory.
|
// 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 {
|
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(
|
newPath := getproviders.UnpackedDirectoryPathForPackage(
|
||||||
d.baseDir, meta.Provider, meta.Version, d.targetPlatform,
|
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.
|
// incorporate any changes we make here.
|
||||||
d.metaCache = nil
|
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)
|
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:
|
case getproviders.PackageHTTPURL:
|
||||||
return installFromHTTPURL(ctx, string(location), newPath)
|
return installFromHTTPURL(ctx, meta, newPath)
|
||||||
case getproviders.PackageLocalArchive:
|
case getproviders.PackageLocalArchive:
|
||||||
return installFromLocalArchive(ctx, string(location), newPath)
|
return installFromLocalArchive(ctx, meta, newPath)
|
||||||
case getproviders.PackageLocalDir:
|
case getproviders.PackageLocalDir:
|
||||||
return installFromLocalDir(ctx, string(location), newPath)
|
return installFromLocalDir(ctx, meta, newPath)
|
||||||
default:
|
default:
|
||||||
// Should not get here, because the above should be exhaustive for
|
// Should not get here, because the above should be exhaustive for
|
||||||
// all implementations of getproviders.Location.
|
// 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
|
// We re-use the process of installing from a local directory here, because
|
||||||
// the two operations are fundamentally the same: symlink if possible,
|
// the two operations are fundamentally the same: symlink if possible,
|
||||||
// deep-copy otherwise.
|
// 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
|
package providercache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -12,6 +13,61 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/getproviders"
|
"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) {
|
func TestLinkFromOtherCache(t *testing.T) {
|
||||||
srcDirPath := "testdata/cachedir"
|
srcDirPath := "testdata/cachedir"
|
||||||
tmpDirPath, err := ioutil.TempDir("", "terraform-test-providercache")
|
tmpDirPath, err := ioutil.TempDir("", "terraform-test-providercache")
|
||||||
|
|
|
@ -315,7 +315,7 @@ NeedProvider:
|
||||||
installTo = i.targetDir
|
installTo = i.targetDir
|
||||||
linkTo = nil // no linking needed
|
linkTo = nil // no linking needed
|
||||||
}
|
}
|
||||||
err = installTo.InstallPackage(ctx, meta)
|
authResult, err := installTo.InstallPackage(ctx, meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: Consider retrying for certain kinds of error that seem
|
// TODO: Consider retrying for certain kinds of error that seem
|
||||||
// likely to be transient. For now, we just treat all errors equally.
|
// likely to be transient. For now, we just treat all errors equally.
|
||||||
|
@ -350,7 +350,7 @@ NeedProvider:
|
||||||
}
|
}
|
||||||
selected[provider] = version
|
selected[provider] = version
|
||||||
if cb := evts.FetchPackageSuccess; cb != nil {
|
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.
|
// signals a failure that the installer is considering transient.
|
||||||
FetchPackageMeta func(provider addrs.Provider, version getproviders.Version) // fetching metadata prior to real download
|
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)
|
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)
|
FetchPackageRetry func(provider addrs.Provider, version getproviders.Version, err error)
|
||||||
FetchPackageFailure 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/httpclient"
|
||||||
"github.com/hashicorp/terraform/internal/copydir"
|
"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
|
// We borrow the "unpack a zip file into a target directory" logic from
|
||||||
|
@ -21,7 +22,9 @@ import (
|
||||||
// specific protocol and set of expectations.)
|
// specific protocol and set of expectations.)
|
||||||
var unzip = getter.ZipDecompressor{}
|
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
|
// 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
|
// a zip file. We'll fetch that into a temporary file here and then
|
||||||
// delegate to installFromLocalArchive below to actually extract it.
|
// 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()
|
httpClient := httpclient.New()
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != 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)
|
resp, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
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")
|
f, err := ioutil.TempFile("", "terraform-provider")
|
||||||
if err != nil {
|
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()
|
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)
|
err = fmt.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, n)
|
||||||
}
|
}
|
||||||
if err != nil {
|
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()
|
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 {
|
func installFromLocalArchive(ctx context.Context, meta getproviders.PackageMeta, targetDir string) (*getproviders.PackageAuthenticationResult, error) {
|
||||||
return unzip.Decompress(targetDir, filename, true)
|
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
|
// installFromLocalDir is the implementation of both installing a package from
|
||||||
// a local directory source _and_ of linking a package from another cache
|
// a local directory source _and_ of linking a package from another cache
|
||||||
// in LinkFromOtherCache, because they both do fundamentally the same
|
// in LinkFromOtherCache, because they both do fundamentally the same
|
||||||
// operation: symlink if possible, or deep-copy otherwise.
|
// 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)
|
absNew, err := filepath.Abs(targetDir)
|
||||||
if err != nil {
|
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)
|
absCurrent, err := filepath.Abs(sourceDir)
|
||||||
if err != nil {
|
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
|
// 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
|
// disk. This compares the files by their OS-level device and directory
|
||||||
// entry identifiers, not by their virtual filesystem paths.
|
// entry identifiers, not by their virtual filesystem paths.
|
||||||
if same, err := copydir.SameFile(absNew, absCurrent); same {
|
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 {
|
} 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.
|
// Delete anything that's already present at this path first.
|
||||||
err = os.RemoveAll(targetDir)
|
err = os.RemoveAll(targetDir)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
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
|
// 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)
|
parentDir := filepath.Dir(absNew)
|
||||||
err = os.MkdirAll(parentDir, 0755)
|
err = os.MkdirAll(parentDir, 0755)
|
||||||
if err != nil && os.IsExist(err) {
|
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)
|
err = os.Symlink(linkTarget, absNew)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Success, then!
|
// Success, then!
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get down here then symlinking failed and we need a deep copy
|
// If we get down here then symlinking failed and we need a deep copy
|
||||||
// instead.
|
// instead.
|
||||||
err = copydir.CopyDir(absNew, absCurrent)
|
err = copydir.CopyDir(absNew, absCurrent)
|
||||||
if err != nil {
|
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.
|
// 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