From 22ef5cc99cb8bf06040dd8e56b715a835e31b913 Mon Sep 17 00:00:00 2001 From: Paul Tyng Date: Tue, 12 May 2020 13:58:12 -0400 Subject: [PATCH] Modify language for reporting signing state Be more explicit about the signing status of fetched plugins and provide documentation about the different signing options. --- command/init.go | 26 +++++++--- .../getproviders/package_authentication.go | 50 ++++++++++++------- .../package_authentication_test.go | 31 +++++++++--- internal/providercache/installer.go | 9 +++- internal/providercache/installer_events.go | 4 ++ website/docs/plugins/signing.html.md | 26 ++++++++++ website/docs/registry/providers/tiers.html.md | 20 -------- website/layouts/docs.erb | 4 ++ 8 files changed, 119 insertions(+), 51 deletions(-) create mode 100644 website/docs/plugins/signing.html.md delete mode 100644 website/docs/registry/providers/tiers.html.md diff --git a/command/init.go b/command/init.go index 21e9959ca8..42d25dc85f 100644 --- a/command/init.go +++ b/command/init.go @@ -566,15 +566,29 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state } }, FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) { - var warning string - if authResult != nil { - warning = authResult.Warning + var keyID string + if authResult != nil && authResult.ThirdPartySigned() { + keyID = authResult.KeyID } - if warning != "" { - warning = c.Colorize().Color(fmt.Sprintf("\n [reset][yellow]Warning: %s[reset]", warning)) + if keyID != "" { + keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID)) } - c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s)%s", provider.ForDisplay(), version, authResult, warning)) + c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s%s)", provider.ForDisplay(), version, authResult, keyID)) + }, + ProvidersFetched: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) { + thirdPartySigned := false + for _, authResult := range authResults { + if authResult.ThirdPartySigned() { + thirdPartySigned = true + break + } + } + if thirdPartySigned { + c.Ui.Info(fmt.Sprintf("\nPartner and community providers are signed by their developers.\n" + + "If you'd like to know more about provider signing, you can read about it here:\n" + + "https://www.terraform.io/docs/plugins/signing.html")) + } }, } diff --git a/internal/getproviders/package_authentication.go b/internal/getproviders/package_authentication.go index 810363e4d2..987908df69 100644 --- a/internal/getproviders/package_authentication.go +++ b/internal/getproviders/package_authentication.go @@ -26,13 +26,13 @@ const ( // 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. +// implements Stringer. // // A failed PackageAuthentication attempt will return an "unauthenticated" // result, which is represented by nil. type PackageAuthenticationResult struct { - result packageAuthenticationResult - Warning string + result packageAuthenticationResult + KeyID string } func (t *PackageAuthenticationResult) String() string { @@ -41,12 +41,25 @@ func (t *PackageAuthenticationResult) String() string { } return []string{ "verified checksum", - "official provider", - "partner provider", - "community provider", + "signed by HashiCorp", + "signed by a HashiCorp partner", + "self-signed", }[t.result] } +// ThirdPartySigned returns whether the package was authenticated as signed by a party +// other than HashiCorp. +func (t *PackageAuthenticationResult) ThirdPartySigned() bool { + if t == nil { + return false + } + if t.result == partnerProvider || t.result == communityProvider { + return true + } + + return false +} + // 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. @@ -234,7 +247,7 @@ func NewSignatureAuthentication(document, signature []byte, keys []SigningKey) P 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() + signingKey, keyID, err := s.findSigningKey() if err != nil { return nil, err } @@ -247,7 +260,7 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( } _, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) if err == nil { - return &PackageAuthenticationResult{result: officialProvider}, nil + return &PackageAuthenticationResult{result: officialProvider, KeyID: keyID}, nil } // If the signing key has a trust signature, attempt to verify it with the @@ -273,14 +286,12 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( return nil, fmt.Errorf("error verifying trust signature: %s", err) } - return &PackageAuthenticationResult{result: partnerProvider}, nil + return &PackageAuthenticationResult{result: partnerProvider, KeyID: keyID}, 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 + return &PackageAuthenticationResult{result: communityProvider, KeyID: keyID}, nil } // findSigningKey attempts to verify the signature using each of the keys @@ -289,11 +300,11 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( // // Note: currently the registry only returns one key, but this may change in // the future. -func (s signatureAuthentication) findSigningKey() (*SigningKey, error) { +func (s signatureAuthentication) findSigningKey() (*SigningKey, string, 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) + return nil, "", fmt.Errorf("error decoding signing key: %s", err) } entity, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) @@ -306,16 +317,21 @@ func (s signatureAuthentication) findSigningKey() (*SigningKey, error) { // Any other signature error is terminal. if err != nil { - return nil, fmt.Errorf("error checking signature: %s", err) + return nil, "", fmt.Errorf("error checking signature: %s", err) + } + + keyID := "n/a" + if entity.PrimaryKey != nil { + keyID = entity.PrimaryKey.KeyIdString() } log.Printf("[DEBUG] Provider signed by %s", entityString(entity)) - return &key, nil + return &key, keyID, 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") + return nil, "", fmt.Errorf("authentication signature from unknown issuer") } // entityString extracts the key ID and identity name(s) from an openpgp.Entity diff --git a/internal/getproviders/package_authentication_test.go b/internal/getproviders/package_authentication_test.go index f14b2e0bbe..521306db5d 100644 --- a/internal/getproviders/package_authentication_test.go +++ b/internal/getproviders/package_authentication_test.go @@ -26,15 +26,15 @@ func TestPackageAuthenticationResult(t *testing.T) { }, { &PackageAuthenticationResult{result: officialProvider}, - "official provider", + "signed by HashiCorp", }, { &PackageAuthenticationResult{result: partnerProvider}, - "partner provider", + "signed by a HashiCorp partner", }, { &PackageAuthenticationResult{result: communityProvider}, - "community provider", + "self-signed", }, } for _, test := range tests { @@ -270,7 +270,10 @@ func TestSignatureAuthentication_success(t *testing.T) { ASCIIArmor: HashicorpPublicKey, }, }, - PackageAuthenticationResult{result: officialProvider}, + PackageAuthenticationResult{ + result: officialProvider, + KeyID: testHashiCorpPublicKeyID, + }, }, "partner provider": { testAuthorSignatureGoodBase64, @@ -280,7 +283,10 @@ func TestSignatureAuthentication_success(t *testing.T) { TrustSignature: testAuthorKeyTrustSignatureArmor, }, }, - PackageAuthenticationResult{result: partnerProvider}, + PackageAuthenticationResult{ + result: partnerProvider, + KeyID: testAuthorKeyID, + }, }, "community provider": { testAuthorSignatureGoodBase64, @@ -289,7 +295,10 @@ func TestSignatureAuthentication_success(t *testing.T) { ASCIIArmor: testAuthorKeyArmor, }, }, - PackageAuthenticationResult{result: communityProvider}, + PackageAuthenticationResult{ + result: communityProvider, + KeyID: testAuthorKeyID, + }, }, "multiple signing keys": { testAuthorSignatureGoodBase64, @@ -301,7 +310,10 @@ func TestSignatureAuthentication_success(t *testing.T) { ASCIIArmor: testAuthorKeyArmor, }, }, - PackageAuthenticationResult{result: communityProvider}, + PackageAuthenticationResult{ + result: communityProvider, + KeyID: testAuthorKeyID, + }, }, } @@ -408,6 +420,8 @@ func TestSignatureAuthentication_failure(t *testing.T) { } } +const testAuthorKeyID = `37A6AB3BCF2C170A` + // testAuthorKeyArmor is test key ID 5BFEEC4317E746008621970637A6AB3BCF2C170A. const testAuthorKeyArmor = `-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -500,6 +514,9 @@ const testSignatureBadBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` + `rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` + `n1ayZdaCIw/r4w==` +// testHashiCorpPublicKeyID is the Key ID of the HashiCorpPublicKey. +const testHashiCorpPublicKeyID = `51852D87348FFC4C` + // testHashicorpSignatureGoodBase64 is a signature of testShaSums signed with // HashicorpPublicKey, which represents the SHA256SUMS.sig file downloaded for // an official release. diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index 2c97eaa6b5..203eb0e354 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -249,7 +249,8 @@ NeedProvider: // Step 3: For each provider version we've decided we need to install, // install its package into our target cache (possibly via the global cache). - targetPlatform := i.targetDir.targetPlatform // we inherit this to behave correctly in unit tests + authResults := map[addrs.Provider]*getproviders.PackageAuthenticationResult{} // record auth results for all successfully fetched providers + targetPlatform := i.targetDir.targetPlatform // we inherit this to behave correctly in unit tests for provider, version := range need { if i.globalCacheDir != nil { // Step 3a: If our global cache already has this version available then @@ -348,12 +349,18 @@ NeedProvider: continue } } + authResults[provider] = authResult selected[provider] = version if cb := evts.FetchPackageSuccess; cb != nil { cb(provider, version, new.PackageDir, authResult) } } + // Emit final event for fetching if any were successfully fetched + if cb := evts.ProvidersFetched; cb != nil && len(authResults) > 0 { + cb(authResults) + } + // We'll remember our selections in a lock file inside the target directory, // so callers can recover those exact selections later by calling // SelectedPackages on the same installer. diff --git a/internal/providercache/installer_events.go b/internal/providercache/installer_events.go index 1902724524..d3a433853f 100644 --- a/internal/providercache/installer_events.go +++ b/internal/providercache/installer_events.go @@ -103,6 +103,10 @@ type InstallerEvents struct { FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) FetchPackageFailure func(provider addrs.Provider, version getproviders.Version, err error) + // The ProvidersFetched event is called after all fetch operations if at + // least one provider was fetched successfully. + ProvidersFetched func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) + // HashPackageFailure is called if the installer is unable to determine // the hash of the contents of an installed package after installation. // In that case, the selection will not be recorded in the target cache diff --git a/website/docs/plugins/signing.html.md b/website/docs/plugins/signing.html.md new file mode 100644 index 0000000000..f1cfa7c685 --- /dev/null +++ b/website/docs/plugins/signing.html.md @@ -0,0 +1,26 @@ +--- +layout: "registry" +page_title: "Plugin Signing" +sidebar_current: "docs-plugins-signing" +description: |- + Terraform plugin signing trust levels +--- + +# Plugin Signing + +~> **Note** Currently only provider plugins fetched from a registry are authenticated. + +Terraform providers installed from the Registry are cryptographically signed, and the signature is verified at time of installation. There are three types of provider signatures, each with different trust implications: + +* **Signed by HashiCorp** - are built, signed, and supported by HashiCorp. +* **Signed by Trusted Partners** - are built, signed, and supported by a third party. HashiCorp has +verified the ownership of the private key and we provide a chain of trust to the CLI to verify this +programatically. +* **Self-signed** - are built, signed, and supported by a third party. HashiCorp does not provide a +verification or chain of trust for the signature. You may obtain and validate fingerprints manually +if you want to ensure you are using a binary you can trust. + +Terraform does **NOT** support fetching and using unsigned binaries, but you can manually install +unsigned binaries. You should take extreme care when doing so as no programatic authentication is performed. + +Usage of plugins from the registry is subject to the Registry's [Terms of Use](https://registry.terraform.io/terms). diff --git a/website/docs/registry/providers/tiers.html.md b/website/docs/registry/providers/tiers.html.md deleted file mode 100644 index 591d7e54ed..0000000000 --- a/website/docs/registry/providers/tiers.html.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -layout: "registry" -page_title: "Terraform Registry - Provider Tiers -sidebar_current: "docs-registry-provider-tiers -description: |- - Published Provider tiers in the Terraform Registry ---- - -# Provider Tiers - -There are three tiers of providers in the Terraform Registry: - -* **Official Providers** - are built, signed, and supported by HashiCorp. Official Providers can typically be used without providing - provider source information in your Terraform configuration. -* **Partner Providers** - are built, signed, and supported by a third party. HashiCorp has verified the ownership of the private - key and we provide a chain of trust to the CLI to verify this programatically. To use Partner Providers in your Terraform - configuration, you need to specify the provider source, typically this is the namespace and name to download from the registry. -* **Community Providers** - are built, signed, and supported by a third party. HashiCorp does not provide a verification or chain - of trust for the signing. You will want to obtain and validate fingerprints manually if you want to ensure you are using a - binary you can trust. diff --git a/website/layouts/docs.erb b/website/layouts/docs.erb index beb61a7592..1742ea4d5d 100644 --- a/website/layouts/docs.erb +++ b/website/layouts/docs.erb @@ -424,6 +424,10 @@ Basics + > + Signing + + > Provider