Merge pull request #24617 from hashicorp/alisdair/provider-installer-signature-verification

internal: Verify provider signatures on install
This commit is contained in:
Alisdair McDiarmid 2020-04-20 12:11:40 -04:00 committed by GitHub
commit e32e7e2c4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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
}
}
func installFromLocalArchive(ctx context.Context, filename string, targetDir string) error {
return unzip.Decompress(targetDir, filename, true)
// 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, 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
}