github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/getproviders/package_authentication.go (about) 1 package getproviders 2 3 import ( 4 "bufio" 5 "bytes" 6 "crypto/sha256" 7 "encoding/hex" 8 "fmt" 9 "log" 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" 15 ) 16 17 type packageAuthenticationResult int 18 19 const ( 20 verifiedChecksum packageAuthenticationResult = iota 21 officialProvider 22 partnerProvider 23 communityProvider 24 ) 25 26 // PackageAuthenticationResult is returned from a PackageAuthentication 27 // implementation. It is a mostly-opaque type intended for use in UI, which 28 // implements Stringer. 29 // 30 // A failed PackageAuthentication attempt will return an "unauthenticated" 31 // result, which is represented by nil. 32 type PackageAuthenticationResult struct { 33 result packageAuthenticationResult 34 KeyID string 35 } 36 37 func (t *PackageAuthenticationResult) String() string { 38 if t == nil { 39 return "unauthenticated" 40 } 41 return []string{ 42 "verified checksum", 43 "signed by HashiCorp", 44 "signed by a HashiCorp partner", 45 "self-signed", 46 }[t.result] 47 } 48 49 // SignedByHashiCorp returns whether the package was authenticated as signed 50 // by HashiCorp. 51 func (t *PackageAuthenticationResult) SignedByHashiCorp() bool { 52 if t == nil { 53 return false 54 } 55 if t.result == officialProvider { 56 return true 57 } 58 59 return false 60 } 61 62 // SignedByAnyParty returns whether the package was authenticated as signed 63 // by either HashiCorp or by a third-party. 64 func (t *PackageAuthenticationResult) SignedByAnyParty() bool { 65 if t == nil { 66 return false 67 } 68 if t.result == officialProvider || t.result == partnerProvider || t.result == communityProvider { 69 return true 70 } 71 72 return false 73 } 74 75 // ThirdPartySigned returns whether the package was authenticated as signed by a party 76 // other than HashiCorp. 77 func (t *PackageAuthenticationResult) ThirdPartySigned() bool { 78 if t == nil { 79 return false 80 } 81 if t.result == partnerProvider || t.result == communityProvider { 82 return true 83 } 84 85 return false 86 } 87 88 // SigningKey represents a key used to sign packages from a registry, along 89 // with an optional trust signature from the registry operator. These are 90 // both in ASCII armored OpenPGP format. 91 // 92 // The JSON struct tags represent the field names used by the Registry API. 93 type SigningKey struct { 94 ASCIIArmor string `json:"ascii_armor"` 95 TrustSignature string `json:"trust_signature"` 96 } 97 98 // PackageAuthentication is an interface implemented by the optional package 99 // authentication implementations a source may include on its PackageMeta 100 // objects. 101 // 102 // A PackageAuthentication implementation is responsible for authenticating 103 // that a package is what its distributor intended to distribute and that it 104 // has not been tampered with. 105 type PackageAuthentication interface { 106 // AuthenticatePackage takes the local location of a package (which may or 107 // may not be the same as the original source location), and returns a 108 // PackageAuthenticationResult, or an error if the authentication checks 109 // fail. 110 // 111 // The local location is guaranteed not to be a PackageHTTPURL: a remote 112 // package will always be staged locally for inspection first. 113 AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) 114 } 115 116 // PackageAuthenticationHashes is an optional interface implemented by 117 // PackageAuthentication implementations that are able to return a set of 118 // hashes they would consider valid if a given PackageLocation referred to 119 // a package that matched that hash string. 120 // 121 // This can be used to record a set of acceptable hashes for a particular 122 // package in a lock file so that future install operations can determine 123 // whether the package has changed since its initial installation. 124 type PackageAuthenticationHashes interface { 125 PackageAuthentication 126 127 // AcceptableHashes returns a set of hashes that this authenticator 128 // considers to be valid for the current package or, where possible, 129 // equivalent packages on other platforms. The order of the items in 130 // the result is not significant, and it may contain duplicates 131 // that are also not significant. 132 // 133 // This method's result should only be used to create a "lock" for a 134 // particular provider if an earlier call to AuthenticatePackage for 135 // the corresponding package succeeded. A caller might choose to apply 136 // differing levels of trust for the acceptable hashes depending on 137 // the authentication result: a "verified checksum" result only checked 138 // that the downloaded package matched what the source claimed, which 139 // could be considered to be less trustworthy than a check that includes 140 // verifying a signature from the origin registry, depending on what the 141 // hashes are going to be used for. 142 // 143 // Implementations of PackageAuthenticationHashes may return multiple 144 // hashes with different schemes, which means that all of them are equally 145 // acceptable. Implementors may also return hashes that use schemes the 146 // current version of the authenticator would not allow but that could be 147 // accepted by other versions of Terraform, e.g. if a particular hash 148 // scheme has been deprecated. 149 // 150 // Authenticators that don't use hashes as their authentication procedure 151 // will either not implement this interface or will have an implementation 152 // that returns an empty result. 153 AcceptableHashes() []Hash 154 } 155 156 type packageAuthenticationAll []PackageAuthentication 157 158 // PackageAuthenticationAll combines several authentications together into a 159 // single check value, which passes only if all of the given ones pass. 160 // 161 // The checks are processed in the order given, so a failure of an earlier 162 // check will prevent execution of a later one. 163 // 164 // The returned result is from the last authentication, so callers should 165 // take care to order the authentications such that the strongest is last. 166 // 167 // The returned object also implements the AcceptableHashes method from 168 // interface PackageAuthenticationHashes, returning the hashes from the 169 // last of the given checks that indicates at least one acceptable hash, 170 // or no hashes at all if none of the constituents indicate any. The result 171 // may therefore be incomplete if there is more than one check that can provide 172 // hashes and they disagree about which hashes are acceptable. 173 func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentication { 174 return packageAuthenticationAll(checks) 175 } 176 177 func (checks packageAuthenticationAll) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { 178 var authResult *PackageAuthenticationResult 179 for _, check := range checks { 180 var err error 181 authResult, err = check.AuthenticatePackage(localLocation) 182 if err != nil { 183 return authResult, err 184 } 185 } 186 return authResult, nil 187 } 188 189 func (checks packageAuthenticationAll) AcceptableHashes() []Hash { 190 // The elements of checks are expected to be ordered so that the strongest 191 // one is later in the list, so we'll visit them in reverse order and 192 // take the first one that implements the interface and returns a non-empty 193 // result. 194 for i := len(checks) - 1; i >= 0; i-- { 195 check, ok := checks[i].(PackageAuthenticationHashes) 196 if !ok { 197 continue 198 } 199 allHashes := check.AcceptableHashes() 200 if len(allHashes) > 0 { 201 return allHashes 202 } 203 } 204 return nil 205 } 206 207 type packageHashAuthentication struct { 208 RequiredHashes []Hash 209 AllHashes []Hash 210 Platform Platform 211 } 212 213 // NewPackageHashAuthentication returns a PackageAuthentication implementation 214 // that checks whether the contents of the package match whatever subset of the 215 // given hashes are considered acceptable by the current version of Terraform. 216 // 217 // This uses the hash algorithms implemented by functions PackageHash and 218 // MatchesHash. The PreferredHashes function will select which of the given 219 // hashes are considered by Terraform to be the strongest verification, and 220 // authentication succeeds as long as one of those matches. 221 func NewPackageHashAuthentication(platform Platform, validHashes []Hash) PackageAuthentication { 222 requiredHashes := PreferredHashes(validHashes) 223 return packageHashAuthentication{ 224 RequiredHashes: requiredHashes, 225 AllHashes: validHashes, 226 Platform: platform, 227 } 228 } 229 230 func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { 231 if len(a.RequiredHashes) == 0 { 232 // Indicates that none of the hashes given to 233 // NewPackageHashAuthentication were considered to be usable by this 234 // version of Terraform. 235 return nil, fmt.Errorf("this version of Terraform does not support any of the checksum formats given for this provider") 236 } 237 238 matches, err := PackageMatchesAnyHash(localLocation, a.RequiredHashes) 239 if err != nil { 240 return nil, fmt.Errorf("failed to verify provider package checksums: %s", err) 241 } 242 243 if matches { 244 return &PackageAuthenticationResult{result: verifiedChecksum}, nil 245 } 246 if len(a.RequiredHashes) == 1 { 247 return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHashes[0].String()) 248 } 249 // It's non-ideal that this doesn't actually list the expected checksums, 250 // but in the many-checksum case the message would get pretty unweildy. 251 // In practice today we typically use this authenticator only with a 252 // single hash returned from a network mirror, so the better message 253 // above will prevail in that case. Maybe we'll improve on this somehow 254 // if the future introduction of a new hash scheme causes there to more 255 // commonly be multiple hashes. 256 return nil, fmt.Errorf("provider package doesn't match the any of the expected checksums") 257 } 258 259 func (a packageHashAuthentication) AcceptableHashes() []Hash { 260 // In this case we include even hashes the current version of Terraform 261 // doesn't prefer, because this result is used for building a lock file 262 // and so it's helpful to include older hash formats that other Terraform 263 // versions might need in order to do authentication successfully. 264 return a.AllHashes 265 } 266 267 type archiveHashAuthentication struct { 268 Platform Platform 269 WantSHA256Sum [sha256.Size]byte 270 } 271 272 // NewArchiveChecksumAuthentication returns a PackageAuthentication 273 // implementation that checks that the original distribution archive matches 274 // the given hash. 275 // 276 // This authentication is suitable only for PackageHTTPURL and 277 // PackageLocalArchive source locations, because the unpacked layout 278 // (represented by PackageLocalDir) does not retain access to the original 279 // source archive. Therefore this authenticator will return an error if its 280 // given localLocation is not PackageLocalArchive. 281 // 282 // NewPackageHashAuthentication is preferable to use when possible because 283 // it uses the newer hashing scheme (implemented by function PackageHash) that 284 // can work with both packed and unpacked provider packages. 285 func NewArchiveChecksumAuthentication(platform Platform, wantSHA256Sum [sha256.Size]byte) PackageAuthentication { 286 return archiveHashAuthentication{platform, wantSHA256Sum} 287 } 288 289 func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { 290 archiveLocation, ok := localLocation.(PackageLocalArchive) 291 if !ok { 292 // A source should not use this authentication type for non-archive 293 // locations. 294 return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation) 295 } 296 297 gotHash, err := PackageHashLegacyZipSHA(archiveLocation) 298 if err != nil { 299 return nil, fmt.Errorf("failed to compute checksum for %s: %s", archiveLocation, err) 300 } 301 wantHash := HashLegacyZipSHAFromSHA(a.WantSHA256Sum) 302 if gotHash != wantHash { 303 return nil, fmt.Errorf("archive has incorrect checksum %s (expected %s)", gotHash, wantHash) 304 } 305 return &PackageAuthenticationResult{result: verifiedChecksum}, nil 306 } 307 308 func (a archiveHashAuthentication) AcceptableHashes() []Hash { 309 return []Hash{HashLegacyZipSHAFromSHA(a.WantSHA256Sum)} 310 } 311 312 type matchingChecksumAuthentication struct { 313 Document []byte 314 Filename string 315 WantSHA256Sum [sha256.Size]byte 316 } 317 318 // NewMatchingChecksumAuthentication returns a PackageAuthentication 319 // implementation that scans a registry-provided SHA256SUMS document for a 320 // specified filename, and compares the SHA256 hash against the expected hash. 321 // This is necessary to ensure that the signed SHA256SUMS document matches the 322 // declared SHA256 hash for the package, and therefore that a valid signature 323 // of this document authenticates the package. 324 // 325 // This authentication always returns a nil result, since it alone cannot offer 326 // any assertions about package integrity. It should be combined with other 327 // authentications to be useful. 328 func NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication { 329 return matchingChecksumAuthentication{ 330 Document: document, 331 Filename: filename, 332 WantSHA256Sum: wantSHA256Sum, 333 } 334 } 335 336 func (m matchingChecksumAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) { 337 // Find the checksum in the list with matching filename. The document is 338 // in the form "0123456789abcdef filename.zip". 339 filename := []byte(m.Filename) 340 var checksum []byte 341 for _, line := range bytes.Split(m.Document, []byte("\n")) { 342 parts := bytes.Fields(line) 343 if len(parts) > 1 && bytes.Equal(parts[1], filename) { 344 checksum = parts[0] 345 break 346 } 347 } 348 if checksum == nil { 349 return nil, fmt.Errorf("checksum list has no SHA-256 hash for %q", m.Filename) 350 } 351 352 // Decode the ASCII checksum into a byte array for comparison. 353 var gotSHA256Sum [sha256.Size]byte 354 if _, err := hex.Decode(gotSHA256Sum[:], checksum); err != nil { 355 return nil, fmt.Errorf("checksum list has invalid SHA256 hash %q: %s", string(checksum), err) 356 } 357 358 // If the checksums don't match, authentication fails. 359 if !bytes.Equal(gotSHA256Sum[:], m.WantSHA256Sum[:]) { 360 return nil, fmt.Errorf("checksum list has unexpected SHA-256 hash %x (expected %x)", gotSHA256Sum, m.WantSHA256Sum[:]) 361 } 362 363 // Success! But this doesn't result in any real authentication, only a 364 // lack of authentication errors, so we return a nil result. 365 return nil, nil 366 } 367 368 type signatureAuthentication struct { 369 Document []byte 370 Signature []byte 371 Keys []SigningKey 372 } 373 374 // NewSignatureAuthentication returns a PackageAuthentication implementation 375 // that verifies the cryptographic signature for a package against any of the 376 // provided keys. 377 // 378 // The signing key for a package will be auto detected by attempting each key 379 // in turn until one is successful. If such a key is found, there are three 380 // possible successful authentication results: 381 // 382 // 1. If the signing key is the HashiCorp official key, it is an official 383 // provider; 384 // 2. Otherwise, if the signing key has a trust signature from the HashiCorp 385 // Partners key, it is a partner provider; 386 // 3. If neither of the above is true, it is a community provider. 387 // 388 // Any failure in the process of validating the signature will result in an 389 // unauthenticated result. 390 func NewSignatureAuthentication(document, signature []byte, keys []SigningKey) PackageAuthentication { 391 return signatureAuthentication{ 392 Document: document, 393 Signature: signature, 394 Keys: keys, 395 } 396 } 397 398 func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) { 399 // Find the key that signed the checksum file. This can fail if there is no 400 // valid signature for any of the provided keys. 401 signingKey, keyID, err := s.findSigningKey() 402 if err != nil { 403 return nil, err 404 } 405 406 // Verify the signature using the HashiCorp public key. If this succeeds, 407 // this is an official provider. 408 hashicorpKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPublicKey)) 409 if err != nil { 410 return nil, fmt.Errorf("error creating HashiCorp keyring: %s", err) 411 } 412 _, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) 413 if err == nil { 414 return &PackageAuthenticationResult{result: officialProvider, KeyID: keyID}, nil 415 } 416 417 // If the signing key has a trust signature, attempt to verify it with the 418 // HashiCorp partners public key. 419 if signingKey.TrustSignature != "" { 420 hashicorpPartnersKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPartnersKey)) 421 if err != nil { 422 return nil, fmt.Errorf("error creating HashiCorp Partners keyring: %s", err) 423 } 424 425 authorKey, err := openpgpArmor.Decode(strings.NewReader(signingKey.ASCIIArmor)) 426 if err != nil { 427 return nil, fmt.Errorf("error decoding signing key: %s", err) 428 } 429 430 trustSignature, err := openpgpArmor.Decode(strings.NewReader(signingKey.TrustSignature)) 431 if err != nil { 432 return nil, fmt.Errorf("error decoding trust signature: %s", err) 433 } 434 435 _, err = openpgp.CheckDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body) 436 if err != nil { 437 return nil, fmt.Errorf("error verifying trust signature: %s", err) 438 } 439 440 return &PackageAuthenticationResult{result: partnerProvider, KeyID: keyID}, nil 441 } 442 443 // We have a valid signature, but it's not from the HashiCorp key, and it 444 // also isn't a trusted partner. This is a community provider. 445 return &PackageAuthenticationResult{result: communityProvider, KeyID: keyID}, nil 446 } 447 448 func (s signatureAuthentication) AcceptableHashes() []Hash { 449 // This is a bit of an abstraction leak because signatureAuthentication 450 // otherwise just treats the document as an opaque blob that's been 451 // signed, but here we're making assumptions about its format because 452 // we only want to trust that _all_ of the checksums are valid (rather 453 // than just the current platform's one) if we've also verified that the 454 // bag of checksums is signed. 455 // 456 // In recognition of that layering quirk this implementation is intended to 457 // be somewhat resilient to potentially using this authenticator with 458 // non-checksums files in future (in which case it'll return nothing at all) 459 // but it might be better in the long run to instead combine 460 // signatureAuthentication and matchingChecksumAuthentication together and 461 // be explicit that the resulting merged authenticator is exclusively for 462 // checksums files. 463 464 var ret []Hash 465 sc := bufio.NewScanner(bytes.NewReader(s.Document)) 466 for sc.Scan() { 467 parts := bytes.Fields(sc.Bytes()) 468 if len(parts) != 0 && len(parts) < 2 { 469 // Doesn't look like a valid sums file line, so we'll assume 470 // this whole thing isn't a checksums file. 471 return nil 472 } 473 474 // If this is a checksums file then the first part should be a 475 // hex-encoded SHA256 hash, so it should be 64 characters long 476 // and contain only hex digits. 477 hashStr := parts[0] 478 if len(hashStr) != 64 { 479 return nil // doesn't look like a checksums file 480 } 481 482 var gotSHA256Sum [sha256.Size]byte 483 if _, err := hex.Decode(gotSHA256Sum[:], hashStr); err != nil { 484 return nil // doesn't look like a checksums file 485 } 486 487 ret = append(ret, HashLegacyZipSHAFromSHA(gotSHA256Sum)) 488 } 489 490 return ret 491 } 492 493 // findSigningKey attempts to verify the signature using each of the keys 494 // returned by the registry. If a valid signature is found, it returns the 495 // signing key. 496 // 497 // Note: currently the registry only returns one key, but this may change in 498 // the future. 499 func (s signatureAuthentication) findSigningKey() (*SigningKey, string, error) { 500 for _, key := range s.Keys { 501 keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.ASCIIArmor)) 502 if err != nil { 503 return nil, "", fmt.Errorf("error decoding signing key: %s", err) 504 } 505 506 entity, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) 507 508 // If the signature issuer does not match the the key, keep trying the 509 // rest of the provided keys. 510 if err == openpgpErrors.ErrUnknownIssuer { 511 continue 512 } 513 514 // Any other signature error is terminal. 515 if err != nil { 516 return nil, "", fmt.Errorf("error checking signature: %s", err) 517 } 518 519 keyID := "n/a" 520 if entity.PrimaryKey != nil { 521 keyID = entity.PrimaryKey.KeyIdString() 522 } 523 524 log.Printf("[DEBUG] Provider signed by %s", entityString(entity)) 525 return &key, keyID, nil 526 } 527 528 // If none of the provided keys issued the signature, this package is 529 // unsigned. This is currently a terminal authentication error. 530 return nil, "", fmt.Errorf("authentication signature from unknown issuer") 531 } 532 533 // entityString extracts the key ID and identity name(s) from an openpgp.Entity 534 // for logging. 535 func entityString(entity *openpgp.Entity) string { 536 if entity == nil { 537 return "" 538 } 539 540 keyID := "n/a" 541 if entity.PrimaryKey != nil { 542 keyID = entity.PrimaryKey.KeyIdString() 543 } 544 545 var names []string 546 for _, identity := range entity.Identities { 547 names = append(names, identity.Name) 548 } 549 550 return fmt.Sprintf("%s %s", keyID, strings.Join(names, ", ")) 551 }