package getproviders import ( "bufio" "bytes" "crypto/sha256" "encoding/hex" "fmt" "log" "strings" // TODO: replace crypto/openpgp since it is deprecated // https://github.com/golang/go/issues/44226 //lint:file-ignore SA1019 openpgp is deprecated but there are no good alternatives yet "golang.org/x/crypto/openpgp" openpgpArmor "golang.org/x/crypto/openpgp/armor" openpgpErrors "golang.org/x/crypto/openpgp/errors" ) type packageAuthenticationResult int const ( verifiedChecksum packageAuthenticationResult = iota officialProvider partnerProvider communityProvider ) // PackageAuthenticationResult is returned from a PackageAuthentication // implementation. It is a mostly-opaque type intended for use in UI, which // implements Stringer. // // A failed PackageAuthentication attempt will return an "unauthenticated" // result, which is represented by nil. type PackageAuthenticationResult struct { result packageAuthenticationResult KeyID string } func (t *PackageAuthenticationResult) String() string { if t == nil { return "unauthenticated" } return []string{ "verified checksum", "signed by HashiCorp", "signed by a HashiCorp partner", "self-signed", }[t.result] } // SignedByHashiCorp returns whether the package was authenticated as signed // by HashiCorp. func (t *PackageAuthenticationResult) SignedByHashiCorp() bool { if t == nil { return false } if t.result == officialProvider { return true } return false } // SignedByAnyParty returns whether the package was authenticated as signed // by either HashiCorp or by a third-party. func (t *PackageAuthenticationResult) SignedByAnyParty() bool { if t == nil { return false } if t.result == officialProvider || t.result == partnerProvider || t.result == communityProvider { return true } return false } // 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. // // The JSON struct tags represent the field names used by the Registry API. type SigningKey struct { ASCIIArmor string `json:"ascii_armor"` TrustSignature string `json:"trust_signature"` } // PackageAuthentication is an interface implemented by the optional package // authentication implementations a source may include on its PackageMeta // objects. // // A PackageAuthentication implementation is responsible for authenticating // that a package is what its distributor intended to distribute and that it // has not been tampered with. type PackageAuthentication interface { // AuthenticatePackage takes the local location of a package (which may or // may not be the same as the original source location), and returns a // PackageAuthenticationResult, or an error if the authentication checks // fail. // // The local location is guaranteed not to be a PackageHTTPURL: a remote // package will always be staged locally for inspection first. AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) } // PackageAuthenticationHashes is an optional interface implemented by // PackageAuthentication implementations that are able to return a set of // hashes they would consider valid if a given PackageLocation referred to // a package that matched that hash string. // // This can be used to record a set of acceptable hashes for a particular // package in a lock file so that future install operations can determine // whether the package has changed since its initial installation. type PackageAuthenticationHashes interface { PackageAuthentication // AcceptableHashes returns a set of hashes that this authenticator // considers to be valid for the current package or, where possible, // equivalent packages on other platforms. The order of the items in // the result is not significant, and it may contain duplicates // that are also not significant. // // This method's result should only be used to create a "lock" for a // particular provider if an earlier call to AuthenticatePackage for // the corresponding package succeeded. A caller might choose to apply // differing levels of trust for the acceptable hashes depending on // the authentication result: a "verified checksum" result only checked // that the downloaded package matched what the source claimed, which // could be considered to be less trustworthy than a check that includes // verifying a signature from the origin registry, depending on what the // hashes are going to be used for. // // Implementations of PackageAuthenticationHashes may return multiple // hashes with different schemes, which means that all of them are equally // acceptable. Implementors may also return hashes that use schemes the // current version of the authenticator would not allow but that could be // accepted by other versions of Terraform, e.g. if a particular hash // scheme has been deprecated. // // Authenticators that don't use hashes as their authentication procedure // will either not implement this interface or will have an implementation // that returns an empty result. AcceptableHashes() []Hash } type packageAuthenticationAll []PackageAuthentication // PackageAuthenticationAll combines several authentications together into a // single check value, which passes only if all of the given ones pass. // // The checks are processed in the order given, so a failure of an earlier // check will prevent execution of a later one. // // The returned result is from the last authentication, so callers should // take care to order the authentications such that the strongest is last. // // The returned object also implements the AcceptableHashes method from // interface PackageAuthenticationHashes, returning the hashes from the // last of the given checks that indicates at least one acceptable hash, // or no hashes at all if none of the constituents indicate any. The result // may therefore be incomplete if there is more than one check that can provide // hashes and they disagree about which hashes are acceptable. func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentication { return packageAuthenticationAll(checks) } func (checks packageAuthenticationAll) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { var authResult *PackageAuthenticationResult for _, check := range checks { var err error authResult, err = check.AuthenticatePackage(localLocation) if err != nil { return authResult, err } } return authResult, nil } func (checks packageAuthenticationAll) AcceptableHashes() []Hash { // The elements of checks are expected to be ordered so that the strongest // one is later in the list, so we'll visit them in reverse order and // take the first one that implements the interface and returns a non-empty // result. for i := len(checks) - 1; i >= 0; i-- { check, ok := checks[i].(PackageAuthenticationHashes) if !ok { continue } allHashes := check.AcceptableHashes() if len(allHashes) > 0 { return allHashes } } return nil } type packageHashAuthentication struct { RequiredHashes []Hash AllHashes []Hash Platform Platform } // NewPackageHashAuthentication returns a PackageAuthentication implementation // that checks whether the contents of the package match whatever subset of the // given hashes are considered acceptable by the current version of Terraform. // // This uses the hash algorithms implemented by functions PackageHash and // MatchesHash. The PreferredHashes function will select which of the given // hashes are considered by Terraform to be the strongest verification, and // authentication succeeds as long as one of those matches. func NewPackageHashAuthentication(platform Platform, validHashes []Hash) PackageAuthentication { requiredHashes := PreferredHashes(validHashes) return packageHashAuthentication{ RequiredHashes: requiredHashes, AllHashes: validHashes, Platform: platform, } } func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { if len(a.RequiredHashes) == 0 { // Indicates that none of the hashes given to // NewPackageHashAuthentication were considered to be usable by this // version of Terraform. return nil, fmt.Errorf("this version of Terraform does not support any of the checksum formats given for this provider") } matches, err := PackageMatchesAnyHash(localLocation, a.RequiredHashes) if err != nil { return nil, fmt.Errorf("failed to verify provider package checksums: %s", err) } if matches { return &PackageAuthenticationResult{result: verifiedChecksum}, nil } if len(a.RequiredHashes) == 1 { return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHashes[0].String()) } // It's non-ideal that this doesn't actually list the expected checksums, // but in the many-checksum case the message would get pretty unweildy. // In practice today we typically use this authenticator only with a // single hash returned from a network mirror, so the better message // above will prevail in that case. Maybe we'll improve on this somehow // if the future introduction of a new hash scheme causes there to more // commonly be multiple hashes. return nil, fmt.Errorf("provider package doesn't match the any of the expected checksums") } func (a packageHashAuthentication) AcceptableHashes() []Hash { // In this case we include even hashes the current version of Terraform // doesn't prefer, because this result is used for building a lock file // and so it's helpful to include older hash formats that other Terraform // versions might need in order to do authentication successfully. return a.AllHashes } type archiveHashAuthentication struct { Platform Platform WantSHA256Sum [sha256.Size]byte } // NewArchiveChecksumAuthentication returns a PackageAuthentication // implementation that checks that the original distribution archive matches // the given hash. // // This authentication is suitable only for PackageHTTPURL and // PackageLocalArchive source locations, because the unpacked layout // (represented by PackageLocalDir) does not retain access to the original // source archive. Therefore this authenticator will return an error if its // given localLocation is not PackageLocalArchive. // // NewPackageHashAuthentication is preferable to use when possible because // it uses the newer hashing scheme (implemented by function PackageHash) that // can work with both packed and unpacked provider packages. func NewArchiveChecksumAuthentication(platform Platform, wantSHA256Sum [sha256.Size]byte) PackageAuthentication { return archiveHashAuthentication{platform, wantSHA256Sum} } func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) { archiveLocation, ok := localLocation.(PackageLocalArchive) if !ok { // A source should not use this authentication type for non-archive // locations. return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation) } gotHash, err := PackageHashLegacyZipSHA(archiveLocation) if err != nil { return nil, fmt.Errorf("failed to compute checksum for %s: %s", archiveLocation, err) } wantHash := HashLegacyZipSHAFromSHA(a.WantSHA256Sum) if gotHash != wantHash { return nil, fmt.Errorf("archive has incorrect checksum %s (expected %s)", gotHash, wantHash) } return &PackageAuthenticationResult{result: verifiedChecksum}, nil } func (a archiveHashAuthentication) AcceptableHashes() []Hash { return []Hash{HashLegacyZipSHAFromSHA(a.WantSHA256Sum)} } type matchingChecksumAuthentication struct { Document []byte Filename string WantSHA256Sum [sha256.Size]byte } // NewMatchingChecksumAuthentication returns a PackageAuthentication // implementation that scans a registry-provided SHA256SUMS document for a // specified filename, and compares the SHA256 hash against the expected hash. // This is necessary to ensure that the signed SHA256SUMS document matches the // declared SHA256 hash for the package, and therefore that a valid signature // of this document authenticates the package. // // This authentication always returns a nil result, since it alone cannot offer // any assertions about package integrity. It should be combined with other // authentications to be useful. func NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication { return matchingChecksumAuthentication{ Document: document, Filename: filename, WantSHA256Sum: wantSHA256Sum, } } func (m matchingChecksumAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) { // Find the checksum in the list with matching filename. The document is // in the form "0123456789abcdef filename.zip". filename := []byte(m.Filename) var checksum []byte for _, line := range bytes.Split(m.Document, []byte("\n")) { parts := bytes.Fields(line) if len(parts) > 1 && bytes.Equal(parts[1], filename) { checksum = parts[0] break } } if checksum == nil { return nil, fmt.Errorf("checksum list has no SHA-256 hash for %q", m.Filename) } // Decode the ASCII checksum into a byte array for comparison. var gotSHA256Sum [sha256.Size]byte if _, err := hex.Decode(gotSHA256Sum[:], checksum); err != nil { return nil, fmt.Errorf("checksum list has invalid SHA256 hash %q: %s", string(checksum), err) } // If the checksums don't match, authentication fails. if !bytes.Equal(gotSHA256Sum[:], m.WantSHA256Sum[:]) { return nil, fmt.Errorf("checksum list has unexpected SHA-256 hash %x (expected %x)", gotSHA256Sum, m.WantSHA256Sum[:]) } // Success! But this doesn't result in any real authentication, only a // lack of authentication errors, so we return a nil result. return nil, nil } type signatureAuthentication struct { Document []byte Signature []byte Keys []SigningKey } // NewSignatureAuthentication returns a PackageAuthentication implementation // that verifies the cryptographic signature for a package against any of the // provided keys. // // The signing key for a package will be auto detected by attempting each key // in turn until one is successful. If such a key is found, there are three // possible successful authentication results: // // 1. If the signing key is the HashiCorp official key, it is an official // provider; // 2. Otherwise, if the signing key has a trust signature from the HashiCorp // Partners key, it is a partner provider; // 3. If neither of the above is true, it is a community provider. // // Any failure in the process of validating the signature will result in an // unauthenticated result. func NewSignatureAuthentication(document, signature []byte, keys []SigningKey) PackageAuthentication { return signatureAuthentication{ Document: document, Signature: signature, Keys: keys, } } 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, keyID, err := s.findSigningKey() if err != nil { return nil, err } // Verify the signature using the HashiCorp public key. If this succeeds, // this is an official provider. hashicorpKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPublicKey)) if err != nil { return nil, fmt.Errorf("error creating HashiCorp keyring: %s", err) } _, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) if err == nil { return &PackageAuthenticationResult{result: officialProvider, KeyID: keyID}, nil } // If the signing key has a trust signature, attempt to verify it with the // HashiCorp partners public key. if signingKey.TrustSignature != "" { hashicorpPartnersKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPartnersKey)) if err != nil { return nil, fmt.Errorf("error creating HashiCorp Partners keyring: %s", err) } authorKey, err := openpgpArmor.Decode(strings.NewReader(signingKey.ASCIIArmor)) if err != nil { return nil, fmt.Errorf("error decoding signing key: %s", err) } trustSignature, err := openpgpArmor.Decode(strings.NewReader(signingKey.TrustSignature)) if err != nil { return nil, fmt.Errorf("error decoding trust signature: %s", err) } _, err = openpgp.CheckDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body) if err != nil { return nil, fmt.Errorf("error verifying trust signature: %s", err) } 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. return &PackageAuthenticationResult{result: communityProvider, KeyID: keyID}, nil } func (s signatureAuthentication) AcceptableHashes() []Hash { // This is a bit of an abstraction leak because signatureAuthentication // otherwise just treats the document as an opaque blob that's been // signed, but here we're making assumptions about its format because // we only want to trust that _all_ of the checksums are valid (rather // than just the current platform's one) if we've also verified that the // bag of checksums is signed. // // In recognition of that layering quirk this implementation is intended to // be somewhat resilient to potentially using this authenticator with // non-checksums files in future (in which case it'll return nothing at all) // but it might be better in the long run to instead combine // signatureAuthentication and matchingChecksumAuthentication together and // be explicit that the resulting merged authenticator is exclusively for // checksums files. var ret []Hash sc := bufio.NewScanner(bytes.NewReader(s.Document)) for sc.Scan() { parts := bytes.Fields(sc.Bytes()) if len(parts) != 0 && len(parts) < 2 { // Doesn't look like a valid sums file line, so we'll assume // this whole thing isn't a checksums file. return nil } // If this is a checksums file then the first part should be a // hex-encoded SHA256 hash, so it should be 64 characters long // and contain only hex digits. hashStr := parts[0] if len(hashStr) != 64 { return nil // doesn't look like a checksums file } var gotSHA256Sum [sha256.Size]byte if _, err := hex.Decode(gotSHA256Sum[:], hashStr); err != nil { return nil // doesn't look like a checksums file } ret = append(ret, HashLegacyZipSHAFromSHA(gotSHA256Sum)) } return ret } // findSigningKey attempts to verify the signature using each of the keys // returned by the registry. If a valid signature is found, it returns the // signing key. // // Note: currently the registry only returns one key, but this may change in // the future. 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) } entity, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) // If the signature issuer does not match the the key, keep trying the // rest of the provided keys. if err == openpgpErrors.ErrUnknownIssuer { continue } // Any other signature error is terminal. if err != nil { 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, 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") } // entityString extracts the key ID and identity name(s) from an openpgp.Entity // for logging. func entityString(entity *openpgp.Entity) string { if entity == nil { return "" } keyID := "n/a" if entity.PrimaryKey != nil { keyID = entity.PrimaryKey.KeyIdString() } var names []string for _, identity := range entity.Identities { names = append(names, identity.Name) } return fmt.Sprintf("%s %s", keyID, strings.Join(names, ", ")) }