Skip to content

Commit d6a5225

Browse files
committed
internal: Verify provider signatures on install
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.
1 parent 9c75cfd commit d6a5225

File tree

11 files changed

+505
-52
lines changed

11 files changed

+505
-52
lines changed

command/init.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,14 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state
508508
fmt.Sprintf("Error while installing %s v%s: %s.", provider.ForDisplay(), version, err),
509509
))
510510
},
511+
FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) {
512+
warning := authResult.Warning
513+
if warning != "" {
514+
warning = c.Colorize().Color(fmt.Sprintf("\n [reset][yellow]Warning: %s[reset]", warning))
515+
}
516+
517+
c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s)%s", provider.ForDisplay(), version, authResult, warning))
518+
},
511519
}
512520

513521
mode := providercache.InstallNewProvidersOnly

internal/getproviders/package_authentication.go

Lines changed: 201 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,36 @@ package getproviders
33
import (
44
"bytes"
55
"crypto/sha256"
6+
"encoding/hex"
67
"fmt"
78
"io"
89
"os"
10+
"strings"
11+
12+
"golang.org/x/crypto/openpgp"
13+
openpgpArmor "golang.org/x/crypto/openpgp/armor"
14+
openpgpErrors "golang.org/x/crypto/openpgp/errors"
915
)
1016

17+
// FIXME docs
18+
type PackageAuthenticationResult struct {
19+
Result string
20+
Warning string
21+
}
22+
23+
func (t *PackageAuthenticationResult) String() string {
24+
if t == nil {
25+
return "Unauthenticated"
26+
}
27+
return t.Result
28+
}
29+
30+
// FIXME docs
31+
type SigningKey struct {
32+
ASCIIArmor string `json:"ascii_armor"`
33+
TrustSignature string `json:"trust_signature"`
34+
}
35+
1136
// PackageAuthentication is an interface implemented by the optional package
1237
// authentication implementations a source may include on its PackageMeta
1338
// objects.
@@ -24,7 +49,7 @@ type PackageAuthentication interface {
2449
//
2550
// The localLocation is guaranteed not to be a PackageHTTPURL: a
2651
// remote package will always be staged locally for inspection first.
27-
AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error
52+
AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) (*PackageAuthenticationResult, error)
2853
}
2954

3055
type packageAuthenticationAll []PackageAuthentication
@@ -38,14 +63,16 @@ func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentica
3863
return packageAuthenticationAll(checks)
3964
}
4065

41-
func (checks packageAuthenticationAll) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
66+
func (checks packageAuthenticationAll) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) (*PackageAuthenticationResult, error) {
67+
var authResult *PackageAuthenticationResult
4268
for _, check := range checks {
43-
err := check.AuthenticatePackage(meta, localLocation)
69+
var err error
70+
authResult, err = check.AuthenticatePackage(meta, localLocation)
4471
if err != nil {
45-
return err
72+
return authResult, err
4673
}
4774
}
48-
return nil
75+
return authResult, nil
4976
}
5077

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

68-
func (a archiveHashAuthentication) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) error {
95+
func (a archiveHashAuthentication) AuthenticatePackage(meta PackageMeta, localLocation PackageLocation) (*PackageAuthenticationResult, error) {
6996
archiveLocation, ok := localLocation.(PackageLocalArchive)
7097
if !ok {
7198
// A source should not use this authentication type for non-archive
7299
// locations.
73-
return fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation)
100+
return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation)
74101
}
75102

76103
f, err := os.Open(string(archiveLocation))
77104
if err != nil {
78-
return err
105+
return nil, err
79106
}
80107
defer f.Close()
81108

82109
h := sha256.New()
83110
_, err = io.Copy(h, f)
84111
if err != nil {
85-
return err
112+
return nil, err
86113
}
87114

88115
gotHash := h.Sum(nil)
89116
if !bytes.Equal(gotHash, a.WantSHA256Sum[:]) {
90-
return fmt.Errorf("archive has incorrect SHA-256 checksum %x (expected %x)", gotHash, a.WantSHA256Sum[:])
117+
return nil, fmt.Errorf("archive has incorrect SHA-256 checksum %x (expected %x)", gotHash, a.WantSHA256Sum[:])
118+
}
119+
return &PackageAuthenticationResult{Result: "verified checksum"}, nil
120+
}
121+
122+
type matchingChecksumAuthentication struct {
123+
Document []byte
124+
Filename string
125+
WantSHA256Sum [sha256.Size]byte
126+
}
127+
128+
// NewMatchingChecksumAuthentication FIXME
129+
func NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication {
130+
return matchingChecksumAuthentication{
131+
Document: document,
132+
Filename: filename,
133+
WantSHA256Sum: wantSHA256Sum,
134+
}
135+
}
136+
137+
func (m matchingChecksumAuthentication) AuthenticatePackage(meta PackageMeta, location PackageLocation) (*PackageAuthenticationResult, error) {
138+
if _, ok := meta.Location.(PackageHTTPURL); !ok {
139+
// A source should not use this authentication type for non-HTTP
140+
// source locations.
141+
return nil, fmt.Errorf("cannot verify matching checksum for non-HTTP location %s", meta.Location)
142+
}
143+
144+
// Find the checksum in the list with matching filename. The document is
145+
// in the form "0123456789abcdef filename.zip".
146+
filename := []byte(m.Filename)
147+
var checksum []byte
148+
for _, line := range bytes.Split(m.Document, []byte("\n")) {
149+
parts := bytes.Fields(line)
150+
if len(parts) > 1 && bytes.Equal(parts[1], filename) {
151+
checksum = parts[0]
152+
break
153+
}
154+
}
155+
if checksum == nil {
156+
return nil, fmt.Errorf("checksum list has no SHA-256 hash for %q", m.Filename)
157+
}
158+
159+
// Decode the ASCII checksum into a byte array for comparison.
160+
var gotSHA256Sum [sha256.Size]byte
161+
if _, err := hex.Decode(gotSHA256Sum[:], checksum); err != nil {
162+
return nil, fmt.Errorf("checksum list has invalid SHA256 hash %q: %s", string(checksum), err)
163+
}
164+
165+
// If thee checksums don't match, authentication fails.
166+
if !bytes.Equal(gotSHA256Sum[:], m.WantSHA256Sum[:]) {
167+
return nil, fmt.Errorf("checksum list has unexpected SHA-256 hash %x (expected %x)", gotSHA256Sum, m.WantSHA256Sum[:])
168+
}
169+
170+
// Success! But this doesn't result in any real authentication, only a
171+
// lack of authentication errors, so we return a nil result.
172+
return nil, nil
173+
}
174+
175+
type signatureAuthentication struct {
176+
Document []byte
177+
Signature []byte
178+
Keys []SigningKey
179+
}
180+
181+
// NewSignatureAuthentication returns a PackageAuthentication implementation
182+
// that verifies the cryptographic signature for a package against a given key.
183+
func NewSignatureAuthentication(document, signature []byte, keys []SigningKey) PackageAuthentication {
184+
return signatureAuthentication{
185+
Document: document,
186+
Signature: signature,
187+
Keys: keys,
188+
}
189+
}
190+
191+
func (s signatureAuthentication) AuthenticatePackage(meta PackageMeta, location PackageLocation) (*PackageAuthenticationResult, error) {
192+
if _, ok := location.(PackageLocalArchive); !ok {
193+
// A source should not use this authentication type for non-archive
194+
// locations.
195+
return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", location)
91196
}
92-
return nil
197+
198+
if _, ok := meta.Location.(PackageHTTPURL); !ok {
199+
// A source should not use this authentication type for non-HTTP source
200+
// locations.
201+
return nil, fmt.Errorf("cannot check archive hash for non-HTTP location %s", meta.Location)
202+
}
203+
204+
// Attempt to verify the signature using each of the keys returned by the
205+
// registry. Note: currently the registry only returns one key, but this
206+
// may change in the future. We must check each key in turn to find the
207+
// matching signing entity before proceeding.
208+
var signingKey *SigningKey
209+
for _, key := range s.Keys {
210+
keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.ASCIIArmor))
211+
if err != nil {
212+
return nil, err
213+
}
214+
215+
_, err = openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature))
216+
217+
// If the signature issuer does not match the the key, keep trying the
218+
// rest of the provided keys.
219+
if err == openpgpErrors.ErrUnknownIssuer {
220+
continue
221+
}
222+
223+
// Any other signature error is terminal.
224+
if err != nil {
225+
return nil, err
226+
}
227+
228+
signingKey = &key
229+
break
230+
}
231+
232+
// If none of the provided keys issued the signature, this package is
233+
// unsigned. This is currently a terminal authentication error.
234+
if signingKey == nil {
235+
return nil, fmt.Errorf("Authentication signature from unknown issuer")
236+
}
237+
238+
// Verify the signature using the HashiCorp public key. If this succeeds,
239+
// this is an official provider.
240+
hashicorpKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPublicKey))
241+
if err != nil {
242+
return nil, fmt.Errorf("Error creating HashiCorp Partners keyring: %s", err)
243+
}
244+
_, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature))
245+
if err == nil {
246+
return &PackageAuthenticationResult{Result: "HashiCorp provider"}, nil
247+
}
248+
249+
// If the signing key has a trust signature, attempt to verify it with the
250+
// HashiCorp partners public key.
251+
if signingKey.TrustSignature != "" {
252+
hashicorpPartnersKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPartnersKey))
253+
if err != nil {
254+
return nil, fmt.Errorf("Error creating HashiCorp Partners keyring: %s", err)
255+
}
256+
257+
authorKey, err := openpgpArmor.Decode(strings.NewReader(signingKey.ASCIIArmor))
258+
if err != nil {
259+
return nil, err
260+
}
261+
262+
trustSignature, err := openpgpArmor.Decode(strings.NewReader(signingKey.TrustSignature))
263+
if err != nil {
264+
return nil, err
265+
}
266+
267+
_, err = openpgp.CheckDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body)
268+
if err != nil {
269+
return nil, fmt.Errorf("Error verifying trust signature: %s", err)
270+
}
271+
272+
return &PackageAuthenticationResult{Result: "Partner provider"}, nil
273+
}
274+
275+
// We have a valid signature, but it's not from the HashiCorp key, and it
276+
// also isn't a trusted partner. This is a community provider.
277+
return &PackageAuthenticationResult{
278+
Result: "community provider",
279+
Warning: communityProviderWarning,
280+
}, nil
93281
}
282+
283+
const communityProviderWarning = `community providers are not trusted by HashiCorp. Use at your own risk.`
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package getproviders
2+
3+
// HashicorpPublicKey is the HashiCorp public key, also available at
4+
// https://www.hashicorp.com/security
5+
const HashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
6+
Version: GnuPG v1
7+
8+
mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
9+
W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
10+
fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
11+
3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
12+
KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
13+
SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
14+
cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JATgEEwECACIFAlMORM0CGwMG
15+
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFGFLYc0j/xMyWIIAIPhcVqiQ59n
16+
Jc07gjUX0SWBJAxEG1lKxfzS4Xp+57h2xxTpdotGQ1fZwsihaIqow337YHQI3q0i
17+
SqV534Ms+j/tU7X8sq11xFJIeEVG8PASRCwmryUwghFKPlHETQ8jJ+Y8+1asRydi
18+
psP3B/5Mjhqv/uOK+Vy3zAyIpyDOMtIpOVfjSpCplVRdtSTFWBu9Em7j5I2HMn1w
19+
sJZnJgXKpybpibGiiTtmnFLOwibmprSu04rsnP4ncdC2XRD4wIjoyA+4PKgX3sCO
20+
klEzKryWYBmLkJOMDdo52LttP3279s7XrkLEE7ia0fXa2c12EQ0f0DQ1tGUvyVEW
21+
WmJVccm5bq25AQ0EUw5EzQEIANaPUY04/g7AmYkOMjaCZ6iTp9hB5Rsj/4ee/ln9
22+
wArzRO9+3eejLWh53FoN1rO+su7tiXJA5YAzVy6tuolrqjM8DBztPxdLBbEi4V+j
23+
2tK0dATdBQBHEh3OJApO2UBtcjaZBT31zrG9K55D+CrcgIVEHAKY8Cb4kLBkb5wM
24+
skn+DrASKU0BNIV1qRsxfiUdQHZfSqtp004nrql1lbFMLFEuiY8FZrkkQ9qduixo
25+
mTT6f34/oiY+Jam3zCK7RDN/OjuWheIPGj/Qbx9JuNiwgX6yRj7OE1tjUx6d8g9y
26+
0H1fmLJbb3WZZbuuGFnK6qrE3bGeY8+AWaJAZ37wpWh1p0cAEQEAAYkBHwQYAQIA
27+
CQUCUw5EzQIbDAAKCRBRhS2HNI/8TJntCAClU7TOO/X053eKF1jqNW4A1qpxctVc
28+
z8eTcY8Om5O4f6a/rfxfNFKn9Qyja/OG1xWNobETy7MiMXYjaa8uUx5iFy6kMVaP
29+
0BXJ59NLZjMARGw6lVTYDTIvzqqqwLxgliSDfSnqUhubGwvykANPO+93BBx89MRG
30+
unNoYGXtPlhNFrAsB1VR8+EyKLv2HQtGCPSFBhrjuzH3gxGibNDDdFQLxxuJWepJ
31+
EK1UbTS4ms0NgZ2Uknqn1WRU1Ki7rE4sTy68iZtWpKQXZEJa0IGnuI2sSINGcXCJ
32+
oEIgXTMyCILo34Fa/C6VCm2WBgz9zZO8/rHIiQm1J5zqz0DrDwKBUM9C
33+
=LYpS
34+
-----END PGP PUBLIC KEY BLOCK-----`
35+
36+
// HashicorpPartnersKey is a key created by HashiCorp, used to generate and
37+
// verify trust signatures for Partner tier providers.
38+
const HashicorpPartnersKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
39+
40+
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
88+
=5/s+
89+
-----END PGP PUBLIC KEY BLOCK-----`

0 commit comments

Comments
 (0)