add signature verification

Fetch the SHA256SUMS file and verify it's signature before downloading
any plugins.

This embeds the hashicorp public key in the binary. If the publickey is
replaced, new releases will need to be cut anyway. A
--verify-plugin=false flag will be added to skip signature verification
in these cases.
This commit is contained in:
James Bardin 2017-06-17 10:52:30 -04:00
parent afe891a80e
commit 415d562d36
3 changed files with 141 additions and 6 deletions

View File

@ -38,6 +38,10 @@ func providerName(name string) string {
return "terraform-provider-" + name
}
func providerFileName(name, version string) string {
return fmt.Sprintf("%s_%s_%s_%s.zip", providerName(name), version, runtime.GOOS, runtime.GOARCH)
}
// providerVersionsURL returns the path to the released versions directory for the provider:
// https://releases.hashicorp.com/terraform-provider-name/
func providerVersionsURL(name string) string {
@ -48,7 +52,11 @@ func providerVersionsURL(name string) string {
// and ARCH:
// .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
func providerURL(name, version string) string {
fileName := fmt.Sprintf("%s_%s_%s_%s.zip", providerName(name), version, runtime.GOOS, runtime.GOARCH)
return fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, providerFileName(name, version))
}
func providerChecksumURL(name, version string) string {
fileName := fmt.Sprintf("%s_%s_SHA256SUMS", providerName(name), version)
u := fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, fileName)
return u
}
@ -69,6 +77,9 @@ type ProviderInstaller struct {
Dir string
PluginProtocolVersion uint
// Skip checksum and signature verification
SkipVerify bool
}
func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) {
@ -93,6 +104,19 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e
// take the first matching plugin we find
for _, v := range versions {
url := providerURL(provider, v.String())
if !i.SkipVerify {
sha256, err := getProviderChecksum(provider, v.String())
if err != nil {
return PluginMeta{}, nil
}
// add the checksum parameter for go-getter to verify the download for us.
if sha256 != "" {
url = url + "?checksum=sha256:" + sha256
}
}
log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
if checkPlugin(url, i.PluginProtocolVersion) {
log.Printf("[DEBUG] getting provider %q version %q at %s", provider, v, url)
@ -107,10 +131,10 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, e
// the archive directly into a shared dir here.)
log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, v)
metas := FindPlugins("provider", []string{i.Dir})
log.Printf("all plugins found %#v", metas)
log.Printf("[DEBUG] all plugins found %#v", metas)
metas, _ = metas.ValidateVersions()
metas = metas.WithName(provider).WithVersion(v)
log.Printf("filtered plugins %#v", metas)
log.Printf("[DEBUG] filtered plugins %#v", metas)
if metas.Count() == 0 {
// This should never happen. Suggests that the release archive
// contains an executable file whose name doesn't match the
@ -287,3 +311,61 @@ func versionsFromNames(names []string) []Version {
return versions
}
func getProviderChecksum(name, version string) (string, error) {
checksums, err := getPluginSHA256SUMs(providerChecksumURL(name, version))
if err != nil {
return "", err
}
return checksumForFile(checksums, providerFileName(name, version)), nil
}
func checksumForFile(sums []byte, name string) string {
for _, line := range strings.Split(string(sums), "\n") {
parts := strings.Fields(line)
if len(parts) > 1 && parts[1] == name {
return parts[0]
}
}
return ""
}
// fetch the SHA256SUMS file provided, and verify its signature.
func getPluginSHA256SUMs(sumsURL string) ([]byte, error) {
sigURL := sumsURL + ".sig"
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 {
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

@ -119,8 +119,8 @@ func TestProviderInstallerGet(t *testing.T) {
// attempt to use an incompatible protocol version
i := &ProviderInstaller{
Dir: tmpDir,
PluginProtocolVersion: 5,
SkipVerify: true,
}
_, err = i.Get("test", AllVersions)
if err == nil {
@ -129,8 +129,8 @@ func TestProviderInstallerGet(t *testing.T) {
i = &ProviderInstaller{
Dir: tmpDir,
PluginProtocolVersion: 3,
SkipVerify: true,
}
gotMeta, err := i.Get("test", AllVersions)
if err != nil {
@ -185,8 +185,8 @@ func TestProviderInstallerPurgeUnused(t *testing.T) {
i := &ProviderInstaller{
Dir: tmpDir,
PluginProtocolVersion: 3,
SkipVerify: true,
}
purged, err := i.PurgeUnused(map[string]PluginMeta{
"test": PluginMeta{

View File

@ -0,0 +1,53 @@
package discovery
import (
"bytes"
"log"
"strings"
"golang.org/x/crypto/openpgp"
)
// 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))
if err != nil {
log.Fatal(err)
}
_, err = openpgp.CheckDetachedSignature(el, bytes.NewReader(data), bytes.NewReader(sig))
return err
}
// 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-----`