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