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  }