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:
Alisdair McDiarmid 2020-04-08 16:22:07 -04:00
parent f09ae6f862
commit a5b3d497cc
16 changed files with 1222 additions and 66 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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