mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #20781 from findkim/registry-checksum
plugin/discovery: verify registry checksum
This commit is contained in:
commit
479964ad66
@ -532,6 +532,11 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state
|
||||
case err == discovery.ErrorNoVersionCompatible:
|
||||
// Generic version incompatible msg
|
||||
c.Ui.Error(fmt.Sprintf(errProviderIncompatible, provider, constraint))
|
||||
case err == discovery.ErrorSignatureVerification:
|
||||
c.Ui.Error(fmt.Sprintf(errSignatureVerification, provider))
|
||||
case err == discovery.ErrorChecksumVerification,
|
||||
err == discovery.ErrorMissingChecksumVerification:
|
||||
c.Ui.Error(fmt.Sprintf(errChecksumVerification, provider))
|
||||
default:
|
||||
c.Ui.Error(fmt.Sprintf(errProviderInstallError, provider, err.Error(), DefaultPluginVendorDir))
|
||||
}
|
||||
@ -928,7 +933,7 @@ const errProviderInstallError = `
|
||||
|
||||
Terraform analyses the configuration and state and automatically downloads
|
||||
plugins for the providers used. However, when attempting to download this
|
||||
plugin an unexpected error occured.
|
||||
plugin an unexpected error occurred.
|
||||
|
||||
This may be caused if for some reason Terraform is unable to reach the
|
||||
plugin repository. The repository may be unreachable if access is blocked
|
||||
@ -959,3 +964,18 @@ by -plugin-dir on the command line, or in the following directory if custom
|
||||
plugin directories are not set:
|
||||
%[2]s
|
||||
`
|
||||
|
||||
const errChecksumVerification = `
|
||||
[reset][bold][red]Error verifying checksum for provider %[1]q[reset][red]
|
||||
The checksum for provider distribution from the Terraform Registry
|
||||
did not match the source. This may mean that the distributed files
|
||||
were changed after this version was released to the Registry.
|
||||
`
|
||||
|
||||
const errSignatureVerification = `
|
||||
[reset][bold][red]Error verifying GPG signature for provider %[1]q[reset][red]
|
||||
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.
|
||||
`
|
||||
|
@ -35,6 +35,22 @@ const ErrorNoSuchProvider = Error("no provider exists with the given name")
|
||||
// requested platform
|
||||
const ErrorNoVersionCompatibleWithPlatform = Error("no available version is compatible for the requested platform")
|
||||
|
||||
// ErrorMissingChecksumVerification indicates that either the provider
|
||||
// distribution is missing the SHA256SUMS file or the checksum file does
|
||||
// not contain a checksum for the binary plugin
|
||||
const ErrorMissingChecksumVerification = Error("unable to verify checksum")
|
||||
|
||||
// ErrorChecksumVerification indicates that the current checksum of the
|
||||
// provider plugin has changed since the initial release and is not trusted
|
||||
// to download
|
||||
const ErrorChecksumVerification = Error("unexpected plugin checksum")
|
||||
|
||||
// ErrorSignatureVerification indicates that the digital signature for a
|
||||
// provider distribution could not be verified for one of the following
|
||||
// reasons: missing signature file, missing public key, or the signature
|
||||
// was not signed by any known key for the publisher
|
||||
const ErrorSignatureVerification = Error("unable to verify signature")
|
||||
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
||||
|
@ -30,12 +30,6 @@ 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")
|
||||
@ -205,6 +199,16 @@ func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, t
|
||||
providerURL := downloadURLs.DownloadURL
|
||||
|
||||
if !i.SkipVerify {
|
||||
// Terraform verifies the integrity of a provider release before downloading
|
||||
// the plugin binary. The digital signature (SHA256SUMS.sig) on the
|
||||
// release distribution (SHA256SUMS) is verified with the public key of the
|
||||
// publisher provided in the Terraform Registry response, ensuring that
|
||||
// everything is as intended by the publisher. The checksum of the provider
|
||||
// plugin is expected in the SHA256SUMS file and is double checked to match
|
||||
// the checksum of the original published release to the Registry. This
|
||||
// enforces immutability of releases between the Registry and the plugin's
|
||||
// host location. Lastly, the integrity of the binary is verified upon
|
||||
// download matches the Registry and signed checksum.
|
||||
sha256, err := i.getProviderChecksum(downloadURLs)
|
||||
if err != nil {
|
||||
return PluginMeta{}, diags, err
|
||||
@ -391,25 +395,27 @@ func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaS
|
||||
return removed, errs
|
||||
}
|
||||
|
||||
func (i *ProviderInstaller) getProviderChecksum(urls *response.TerraformProviderPlatformLocation) (string, error) {
|
||||
func (i *ProviderInstaller) getProviderChecksum(resp *response.TerraformProviderPlatformLocation) (string, error) {
|
||||
// Get SHA256SUMS file.
|
||||
shasums, err := getFile(urls.ShasumsURL)
|
||||
shasums, err := getFile(resp.ShasumsURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching checksums: %s", err)
|
||||
log.Printf("[ERROR] error fetching checksums from %q: %s", resp.ShasumsURL, err)
|
||||
return "", ErrorMissingChecksumVerification
|
||||
}
|
||||
|
||||
// Get SHA256SUMS.sig file.
|
||||
signature, err := getFile(urls.ShasumsSignatureURL)
|
||||
signature, err := getFile(resp.ShasumsSignatureURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching checksums signature: %s", err)
|
||||
log.Printf("[ERROR] error fetching checksums signature from %q: %s", resp.ShasumsSignatureURL, err)
|
||||
return "", ErrorSignatureVerification
|
||||
}
|
||||
|
||||
// Verify the GPG signature returned from the Registry.
|
||||
asciiArmor := urls.SigningKeys.GPGASCIIArmor()
|
||||
asciiArmor := resp.SigningKeys.GPGASCIIArmor()
|
||||
signer, err := verifySig(shasums, signature, asciiArmor)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] error verifying signature: %s", err)
|
||||
return "", fmt.Errorf(gpgVerificationError)
|
||||
return "", ErrorSignatureVerification
|
||||
}
|
||||
|
||||
// Also verify the GPG signature against the HashiCorp public key. This is
|
||||
@ -418,7 +424,7 @@ func (i *ProviderInstaller) getProviderChecksum(urls *response.TerraformProvider
|
||||
_, err = verifySig(shasums, signature, HashicorpPublicKey)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] error verifying signature against HashiCorp public key: %s", err)
|
||||
return "", fmt.Errorf(gpgVerificationError)
|
||||
return "", ErrorSignatureVerification
|
||||
}
|
||||
|
||||
// Display identity for GPG key which succeeded verifying the signature.
|
||||
@ -430,8 +436,17 @@ func (i *ProviderInstaller) getProviderChecksum(urls *response.TerraformProvider
|
||||
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
|
||||
// Extract checksum for this os/arch platform binary and verify against Registry
|
||||
checksum := checksumForFile(shasums, resp.Filename)
|
||||
if checksum == "" {
|
||||
log.Printf("[ERROR] missing checksum for %s from source %s", resp.Filename, resp.ShasumsURL)
|
||||
return "", ErrorMissingChecksumVerification
|
||||
} else if checksum != resp.Shasum {
|
||||
log.Printf("[ERROR] unexpected checksum for %s from source %q", resp.Filename, resp.ShasumsURL)
|
||||
return "", ErrorChecksumVerification
|
||||
}
|
||||
|
||||
return checksum, nil
|
||||
}
|
||||
|
||||
func (i *ProviderInstaller) hostname() (string, error) {
|
||||
|
@ -606,13 +606,14 @@ func TestProviderChecksum(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
URLs *response.TerraformProviderPlatformLocation
|
||||
Resp *response.TerraformProviderPlatformLocation
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"good",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
|
||||
Shasum: "3c3e7df78b1f0161a3f941c271d5501f7b5e5f2c53738e7a371459712f5d4726",
|
||||
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{
|
||||
@ -653,32 +654,68 @@ func TestProviderChecksum(t *testing.T) {
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"mismatch checksum",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
|
||||
Shasum: "force mismatch",
|
||||
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{
|
||||
&response.GPGKey{
|
||||
ASCIIArmor: string(hashicorpKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"missing checksum for file",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64_missing_checksum.zip",
|
||||
Shasum: "checksum",
|
||||
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{
|
||||
&response.GPGKey{
|
||||
ASCIIArmor: string(hashicorpKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
i := ProviderInstaller{}
|
||||
|
||||
for _, test := range tests {
|
||||
sha256sum, err := i.getProviderChecksum(test.URLs)
|
||||
if test.Err {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
sha256sum, err := i.getProviderChecksum(test.Resp)
|
||||
if test.Err {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// get the expected checksum for our os/arch
|
||||
sumData, err := ioutil.ReadFile("testdata/terraform-provider-template_0.1.0_SHA256SUMS")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// get the expected checksum for our os/arch
|
||||
sumData, err := ioutil.ReadFile("testdata/terraform-provider-template_0.1.0_SHA256SUMS")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := checksumForFile(sumData, test.URLs.Filename)
|
||||
expected := checksumForFile(sumData, test.Resp.Filename)
|
||||
|
||||
if sha256sum != expected {
|
||||
t.Fatalf("expected: %s\ngot %s\n", sha256sum, expected)
|
||||
}
|
||||
if sha256sum != expected {
|
||||
t.Fatalf("expected: %s\ngot %s\n", sha256sum, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package discovery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
@ -13,7 +12,7 @@ import (
|
||||
func verifySig(data, sig []byte, armor string) (*openpgp.Entity, error) {
|
||||
el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(armor))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openpgp.CheckDetachedSignature(el, bytes.NewReader(data), bytes.NewReader(sig))
|
||||
|
Loading…
Reference in New Issue
Block a user