Skip to content

Commit

Permalink
internal: Verify provider signatures on install
Browse files Browse the repository at this point in the history
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 an untrusted community
  provider.
  • Loading branch information
alisdair committed Apr 9, 2020
1 parent 9c75cfd commit 2eb234b
Show file tree
Hide file tree
Showing 12 changed files with 509 additions and 53 deletions.
11 changes: 11 additions & 0 deletions command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion command/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1604,7 +1604,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)
}
Expand Down
212 changes: 201 additions & 11 deletions internal/getproviders/package_authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,36 @@ package getproviders
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"strings"

"golang.org/x/crypto/openpgp"
openpgpArmor "golang.org/x/crypto/openpgp/armor"
openpgpErrors "golang.org/x/crypto/openpgp/errors"
)

// FIXME docs
type PackageAuthenticationResult struct {
Result string
Warning string
}

func (t *PackageAuthenticationResult) String() string {
if t == nil {
return "Unauthenticated"
}
return t.Result
}

// FIXME docs
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.
Expand All @@ -24,7 +49,7 @@ type PackageAuthentication interface {
//
// 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
AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) (*PackageAuthenticationResult, error)
}

type packageAuthenticationAll []PackageAuthentication
Expand All @@ -38,14 +63,16 @@ func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentica
return packageAuthenticationAll(checks)
}

func (checks packageAuthenticationAll) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
func (checks packageAuthenticationAll) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) (*PackageAuthenticationResult, error) {
var authResult *PackageAuthenticationResult
for _, check := range checks {
err := check.AuthenticatePackage(meta, localLocation)
var err error
authResult, err = check.AuthenticatePackage(meta, localLocation)
if err != nil {
return err
return authResult, err
}
}
return nil
return authResult, nil
}

type archiveHashAuthentication struct {
Expand All @@ -65,29 +92,192 @@ func NewArchiveChecksumAuthentication(wantSHA256Sum [sha256.Size]byte) PackageAu
return archiveHashAuthentication{wantSHA256Sum}
}

func (a archiveHashAuthentication) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
func (a archiveHashAuthentication) AuthenticatePackage(meta PackageMeta, 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 &PackageAuthenticationResult{Result: "verified checksum"}, nil
}

type matchingChecksumAuthentication struct {
Document []byte
Filename string
WantSHA256Sum [sha256.Size]byte
}

// NewMatchingChecksumAuthentication FIXME
func NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication {
return matchingChecksumAuthentication{
Document: document,
Filename: filename,
WantSHA256Sum: wantSHA256Sum,
}
}

func (m matchingChecksumAuthentication) AuthenticatePackage(meta PackageMeta, location PackageLocation) (*PackageAuthenticationResult, error) {
if _, ok := meta.Location.(PackageHTTPURL); !ok {
// A source should not use this authentication type for non-HTTP
// source locations.
return nil, fmt.Errorf("cannot verify matching checksum for non-HTTP location %s", meta.Location)
}

// 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 thee 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 a given key.
func NewSignatureAuthentication(document, signature []byte, keys []SigningKey) PackageAuthentication {
return signatureAuthentication{
Document: document,
Signature: signature,
Keys: keys,
}
}

func (s signatureAuthentication) AuthenticatePackage(meta PackageMeta, location PackageLocation) (*PackageAuthenticationResult, error) {
if _, ok := location.(PackageLocalArchive); !ok {
// A source should not use this authentication type for non-archive
// locations.
return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", location)
}
return nil

if _, ok := meta.Location.(PackageHTTPURL); !ok {
// A source should not use this authentication type for non-HTTP source
// locations.
return nil, fmt.Errorf("cannot check archive hash for non-HTTP location %s", meta.Location)
}

// Attempt to verify the signature using each of the keys returned by the
// registry. Note: currently the registry only returns one key, but this
// may change in the future. We must check each key in turn to find the
// matching signing entity before proceeding.
var signingKey *SigningKey
for _, key := range s.Keys {
keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.ASCIIArmor))
if err != nil {
return nil, err
}

_, 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, err
}

signingKey = &key
break
}

// If none of the provided keys issued the signature, this package is
// unsigned. This is currently a terminal authentication error.
if signingKey == nil {
return nil, fmt.Errorf("Authentication signature from unknown issuer")
}

// 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 Partners keyring: %s", err)
}
_, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature))
if err == nil {
return &PackageAuthenticationResult{Result: "HashiCorp provider"}, 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, err
}

trustSignature, err := openpgpArmor.Decode(strings.NewReader(signingKey.TrustSignature))
if err != nil {
return nil, 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: "Partner provider"}, 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.
return &PackageAuthenticationResult{
Result: "community provider",
Warning: communityProviderWarning,
}, nil
}

const communityProviderWarning = `community providers are not trusted by HashiCorp. Use at your own risk.`
89 changes: 89 additions & 0 deletions internal/getproviders/public_keys.go
Original file line number Diff line number Diff line change
@@ -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-----`
Loading

0 comments on commit 2eb234b

Please sign in to comment.