Merge pull request #19389 from hashicorp/f-registry-provider-signing-keys

Terraform Registry: Use signing keys provided from the Registry
This commit is contained in:
Justin Campbell 2018-11-20 14:28:41 -05:00 committed by GitHub
commit bf301d7abd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 192 additions and 68 deletions

View File

@ -15,7 +15,6 @@ import (
getter "github.com/hashicorp/go-getter"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/httpclient"
"github.com/hashicorp/terraform/registry"
"github.com/hashicorp/terraform/registry/regsrc"
@ -28,6 +27,12 @@ import (
const protocolVersionHeader = "x-terraform-protocol-version"
const gpgVerificationError = `GPG signature verification error:
Terraform was unable to verify the GPG signature of the downloaded provider
files using the keys downloaded from the Terraform Registry. This may mean that
the publisher of the provider removed the key it was signed with, or that the
distributed files were changed after this version was released.`
var httpClient *http.Client
var errVersionNotFound = errors.New("version not found")
@ -173,6 +178,9 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e
downloadURLs, err := i.listProviderDownloadURLs(provider, versionMeta.Version)
providerURL := downloadURLs.DownloadURL
i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %q (%s)...", provider, versionMeta.Version))
log.Printf("[DEBUG] getting provider %q version %q", provider, versionMeta.Version)
if !i.SkipVerify {
sha256, err := i.getProviderChecksum(downloadURLs)
if err != nil {
@ -185,8 +193,6 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e
}
}
i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %q (%s)...", provider, versionMeta.Version))
log.Printf("[DEBUG] getting provider %q version %q", provider, versionMeta.Version)
err = i.install(provider, v, providerURL)
if err != nil {
return PluginMeta{}, err
@ -353,12 +359,37 @@ func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaS
}
func (i *ProviderInstaller) getProviderChecksum(urls *response.TerraformProviderPlatformLocation) (string, error) {
checksums, err := getPluginSHA256SUMs(urls.ShasumsURL, urls.ShasumsSignatureURL)
// Get SHA256SUMS file.
shasums, err := getFile(urls.ShasumsURL)
if err != nil {
return "", err
return "", fmt.Errorf("error fetching checksums: %s", err)
}
return checksumForFile(checksums, urls.Filename), nil
// Get SHA256SUMS.sig file.
signature, err := getFile(urls.ShasumsSignatureURL)
if err != nil {
return "", fmt.Errorf("error fetching checksums signature: %s", err)
}
// Verify GPG signature.
asciiArmor := urls.SigningKeys.GPGASCIIArmor()
signer, err := verifySig(shasums, signature, asciiArmor)
if err != nil {
log.Printf("[ERROR] error verifying signature: %s", err)
return "", fmt.Errorf(gpgVerificationError)
}
// Display identity for GPG key which succeeded verifying the signature.
// This could also be used to display to the user with i.Ui.Info().
identities := []string{}
for k := range signer.Identities {
identities = append(identities, k)
}
identity := strings.Join(identities, ", ")
log.Printf("[DEBUG] verified GPG signature with key from %s", identity)
// Extract checksum for this os/arch platform binary.
return checksumForFile(shasums, urls.Filename), nil
}
// list all versions available for the named provider
@ -487,25 +518,6 @@ func checksumForFile(sums []byte, name string) string {
return ""
}
// fetch the SHA256SUMS file provided, and verify its signature.
func getPluginSHA256SUMs(sumsURL, sigURL string) ([]byte, error) {
sums, err := getFile(sumsURL)
if err != nil {
return nil, fmt.Errorf("error fetching checksums: %s", err)
}
sig, err := getFile(sigURL)
if err != nil {
return nil, fmt.Errorf("error fetching checksums signature: %s", err)
}
if err := verifySig(sums, sig); err != nil {
return nil, err
}
return sums, nil
}
func getFile(url string) ([]byte, error) {
resp, err := httpClient.Get(url)
if err != nil {

View File

@ -369,23 +369,57 @@ func TestProviderInstallerPurgeUnused(t *testing.T) {
// Test fetching a provider's checksum file while verifying its signature.
func TestProviderChecksum(t *testing.T) {
hashicorpKey, err := ioutil.ReadFile("testdata/hashicorp.asc")
if err != nil {
t.Fatal(err)
}
tests := []struct {
Name string
URLs *response.TerraformProviderPlatformLocation
Err bool
}{
{
"good",
&response.TerraformProviderPlatformLocation{
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS",
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig",
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
SigningKeys: response.SigningKeyList{
GPGKeys: []*response.GPGKey{
&response.GPGKey{
ASCIIArmor: string(hashicorpKey),
},
},
},
},
false,
},
{
"bad",
&response.TerraformProviderPlatformLocation{
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS",
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS.sig",
SigningKeys: response.SigningKeyList{
GPGKeys: []*response.GPGKey{
&response.GPGKey{
ASCIIArmor: string(hashicorpKey),
},
},
},
},
true,
},
{
"no keys",
&response.TerraformProviderPlatformLocation{
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS",
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig",
SigningKeys: response.SigningKeyList{
GPGKeys: []*response.GPGKey{},
},
},
true,
},

View File

@ -10,44 +10,11 @@ import (
// Verify the data using the provided openpgp detached signature and the
// embedded hashicorp public key.
func verifySig(data, sig []byte) error {
el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(hashiPublicKey))
func verifySig(data, sig []byte, armor string) (*openpgp.Entity, error) {
el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(armor))
if err != nil {
log.Fatal(err)
}
_, err = openpgp.CheckDetachedSignature(el, bytes.NewReader(data), bytes.NewReader(sig))
return err
return openpgp.CheckDetachedSignature(el, bytes.NewReader(data), bytes.NewReader(sig))
}
// this is the public key that signs the checksums file for releases.
const hashiPublicKey = `-----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-----`

30
plugin/discovery/testdata/hashicorp.asc vendored Normal file
View File

@ -0,0 +1,30 @@
-----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-----

View File

@ -2,6 +2,7 @@ package response
import (
"sort"
"strings"
version "github.com/hashicorp/go-version"
)
@ -44,17 +45,45 @@ type TerraformProviderPlatform struct {
// structure for a provider platform with all details required to perform a
// download.
type TerraformProviderPlatformLocation struct {
OS string `json:"os"`
Arch string `json:"arch"`
Filename string `json:"filename"`
DownloadURL string `json:"download_url"`
ShasumsURL string `json:"shasums_url"`
ShasumsSignatureURL string `json:"shasums_signature_url"`
Protocols []string `json:"protocols"`
OS string `json:"os"`
Arch string `json:"arch"`
Filename string `json:"filename"`
DownloadURL string `json:"download_url"`
ShasumsURL string `json:"shasums_url"`
ShasumsSignatureURL string `json:"shasums_signature_url"`
Shasum string `json:"shasum"`
SigningKeys SigningKeyList `json:"signing_keys"`
}
// SigningKeyList is the response structure for a list of signing keys.
type SigningKeyList struct {
GPGKeys []*GPGKey `json:"gpg_public_keys"`
}
// GPGKey is the response structure for a GPG key.
type GPGKey struct {
ASCIIArmor string `json:"ascii_armor"`
Source string `json:"source"`
SourceURL *string `json:"source_url"`
}
// Collection type for TerraformProviderVersion
type ProviderVersionCollection []*TerraformProviderVersion
// GPGASCIIArmor returns an ASCII-armor-formatted string for all of the gpg
// keys in the response.
func (signingKeys *SigningKeyList) GPGASCIIArmor() string {
keys := []string{}
for _, gpgKey := range signingKeys.GPGKeys {
keys = append(keys, gpgKey.ASCIIArmor)
}
return strings.Join(keys, "\n")
}
// Sort sorts versions from newest to oldest.
func (v ProviderVersionCollection) Sort() {
sort.Slice(v, func(i, j int) bool {

View File

@ -0,0 +1,52 @@
package response
import (
"fmt"
"testing"
)
var (
testGPGKeyOne = &GPGKey{
ASCIIArmor: "---\none\n---",
}
testGPGKeyTwo = &GPGKey{
ASCIIArmor: "---\ntwo\n---",
}
)
func TestSigningKeyList_GPGASCIIArmor(t *testing.T) {
var tests = []struct {
name string
gpgKeys []*GPGKey
expected string
}{
{
name: "no keys",
gpgKeys: []*GPGKey{},
expected: "",
},
{
name: "one key",
gpgKeys: []*GPGKey{testGPGKeyOne},
expected: testGPGKeyOne.ASCIIArmor,
},
{
name: "two keys",
gpgKeys: []*GPGKey{testGPGKeyOne, testGPGKeyTwo},
expected: fmt.Sprintf("%s\n%s",
testGPGKeyOne.ASCIIArmor, testGPGKeyTwo.ASCIIArmor),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
signingKeys := &SigningKeyList{
GPGKeys: tt.gpgKeys,
}
actual := signingKeys.GPGASCIIArmor()
if actual != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, actual)
}
})
}
}