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  }