github.com/opentofu/opentofu@v1.7.1/internal/getproviders/package_authentication_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package getproviders
     7  
     8  import (
     9  	"crypto/sha256"
    10  	"encoding/base64"
    11  	"errors"
    12  	"fmt"
    13  	"strings"
    14  	"testing"
    15  
    16  	openpgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors"
    17  	tfaddr "github.com/opentofu/registry-address"
    18  
    19  	"github.com/google/go-cmp/cmp"
    20  
    21  	"github.com/ProtonMail/go-crypto/openpgp"
    22  )
    23  
    24  func TestPackageAuthenticationResult(t *testing.T) {
    25  	tests := []struct {
    26  		result *PackageAuthenticationResult
    27  		want   string
    28  	}{
    29  		{
    30  			nil,
    31  			"unauthenticated",
    32  		},
    33  		{
    34  			&PackageAuthenticationResult{result: signed},
    35  			"signed",
    36  		},
    37  	}
    38  	for _, test := range tests {
    39  		if got := test.result.String(); got != test.want {
    40  			t.Errorf("wrong value: got %q, want %q", got, test.want)
    41  		}
    42  	}
    43  }
    44  
    45  // mockAuthentication is an implementation of the PackageAuthentication
    46  // interface which returns fixed values. This is used to test the combining
    47  // logic of PackageAuthenticationAll.
    48  type mockAuthentication struct {
    49  	result packageAuthenticationResult
    50  	err    error
    51  }
    52  
    53  func (m mockAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
    54  	if m.err == nil {
    55  		return &PackageAuthenticationResult{result: m.result}, nil
    56  	} else {
    57  		return nil, m.err
    58  	}
    59  }
    60  
    61  var _ PackageAuthentication = (*mockAuthentication)(nil)
    62  
    63  // If all authentications succeed, the returned result should come from the
    64  // last authentication.
    65  func TestPackageAuthenticationAll_success(t *testing.T) {
    66  	result, err := PackageAuthenticationAll(
    67  		&mockAuthentication{result: verifiedChecksum},
    68  		&mockAuthentication{result: signed},
    69  	).AuthenticatePackage(nil)
    70  
    71  	want := PackageAuthenticationResult{result: signed}
    72  	if result == nil || *result != want {
    73  		t.Errorf("wrong result: want %#v, got %#v", want, result)
    74  	}
    75  	if err != nil {
    76  		t.Errorf("wrong err: got %#v, want nil", err)
    77  	}
    78  }
    79  
    80  // If an authentication fails, its error should be returned along with a nil
    81  // result.
    82  func TestPackageAuthenticationAll_failure(t *testing.T) {
    83  	someError := errors.New("some error")
    84  	result, err := PackageAuthenticationAll(
    85  		&mockAuthentication{result: verifiedChecksum},
    86  		&mockAuthentication{err: someError},
    87  		&mockAuthentication{result: signed},
    88  	).AuthenticatePackage(nil)
    89  
    90  	if result != nil {
    91  		t.Errorf("wrong result: got %#v, want nil", result)
    92  	}
    93  	if err != someError {
    94  		t.Errorf("wrong err: got %#v, want %#v", err, someError)
    95  	}
    96  }
    97  
    98  // Package hash authentication requires a zip file or directory fixture and a
    99  // known-good set of hashes, of which the authenticator will pick one. The
   100  // result should be "verified checksum".
   101  func TestPackageHashAuthentication_success(t *testing.T) {
   102  	// Location must be a PackageLocalArchive path
   103  	location := PackageLocalDir("testdata/filesystem-mirror/registry.opentofu.org/hashicorp/null/2.0.0/linux_amd64")
   104  
   105  	wantHashes := []Hash{
   106  		// Known-good HashV1 result for this directory
   107  		Hash("h1:qjsREM4DqEWECD43FcPqddZ9oxCG+IaMTxvWPciS05g="),
   108  	}
   109  
   110  	auth := NewPackageHashAuthentication(Platform{"linux", "amd64"}, wantHashes)
   111  	result, err := auth.AuthenticatePackage(location)
   112  
   113  	wantResult := PackageAuthenticationResult{result: verifiedChecksum}
   114  	if result == nil || *result != wantResult {
   115  		t.Errorf("wrong result: got %#v, want %#v", result, wantResult)
   116  	}
   117  	if err != nil {
   118  		t.Errorf("wrong err: got %s, want nil", err)
   119  	}
   120  }
   121  
   122  // Package has authentication can fail for various reasons.
   123  func TestPackageHashAuthentication_failure(t *testing.T) {
   124  	tests := map[string]struct {
   125  		location PackageLocation
   126  		err      string
   127  	}{
   128  		"missing file": {
   129  			PackageLocalArchive("testdata/no-package-here.zip"),
   130  			"failed to verify provider package checksums: lstat testdata/no-package-here.zip: no such file or directory",
   131  		},
   132  		"checksum mismatch": {
   133  			PackageLocalDir("testdata/filesystem-mirror/registry.opentofu.org/hashicorp/null/2.0.0/linux_amd64"),
   134  			"provider package doesn't match the expected checksum \"h1:invalid\"",
   135  		},
   136  		"invalid zip file": {
   137  			PackageLocalArchive("testdata/filesystem-mirror/registry.opentofu.org/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip"),
   138  			"failed to verify provider package checksums: zip: not a valid zip file",
   139  		},
   140  	}
   141  
   142  	for name, test := range tests {
   143  		t.Run(name, func(t *testing.T) {
   144  			// Invalid expected hash, either because we'll error before we
   145  			// reach it, or we want to force a checksum mismatch.
   146  			auth := NewPackageHashAuthentication(Platform{"linux", "amd64"}, []Hash{"h1:invalid"})
   147  			result, err := auth.AuthenticatePackage(test.location)
   148  
   149  			if result != nil {
   150  				t.Errorf("wrong result: got %#v, want nil", result)
   151  			}
   152  			if gotErr := err.Error(); gotErr != test.err {
   153  				t.Errorf("wrong err: got %q, want %q", gotErr, test.err)
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  // Archive checksum authentication requires a file fixture and a known-good
   160  // SHA256 hash. The result should be "verified checksum".
   161  func TestArchiveChecksumAuthentication_success(t *testing.T) {
   162  	// Location must be a PackageLocalArchive path
   163  	location := PackageLocalArchive("testdata/filesystem-mirror/registry.opentofu.org/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip")
   164  
   165  	// Known-good SHA256 hash for this archive
   166  	wantSHA256Sum := [sha256.Size]byte{
   167  		0x4f, 0xb3, 0x98, 0x49, 0xf2, 0xe1, 0x38, 0xeb,
   168  		0x16, 0xa1, 0x8b, 0xa0, 0xc6, 0x82, 0x63, 0x5d,
   169  		0x78, 0x1c, 0xb8, 0xc3, 0xb2, 0x59, 0x01, 0xdd,
   170  		0x5a, 0x79, 0x2a, 0xde, 0x97, 0x11, 0xf5, 0x01,
   171  	}
   172  
   173  	auth := NewArchiveChecksumAuthentication(Platform{"linux", "amd64"}, wantSHA256Sum)
   174  	result, err := auth.AuthenticatePackage(location)
   175  
   176  	wantResult := PackageAuthenticationResult{result: verifiedChecksum}
   177  	if result == nil || *result != wantResult {
   178  		t.Errorf("wrong result: got %#v, want %#v", result, wantResult)
   179  	}
   180  	if err != nil {
   181  		t.Errorf("wrong err: got %s, want nil", err)
   182  	}
   183  }
   184  
   185  // Archive checksum authentication can fail for various reasons. These test
   186  // cases are almost exhaustive, missing only an io.Copy error which is
   187  // difficult to induce.
   188  func TestArchiveChecksumAuthentication_failure(t *testing.T) {
   189  	tests := map[string]struct {
   190  		location PackageLocation
   191  		err      string
   192  	}{
   193  		"missing file": {
   194  			PackageLocalArchive("testdata/no-package-here.zip"),
   195  			"failed to compute checksum for testdata/no-package-here.zip: lstat testdata/no-package-here.zip: no such file or directory",
   196  		},
   197  		"checksum mismatch": {
   198  			PackageLocalArchive("testdata/filesystem-mirror/registry.opentofu.org/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip"),
   199  			"archive has incorrect checksum zh:4fb39849f2e138eb16a18ba0c682635d781cb8c3b25901dd5a792ade9711f501 (expected zh:0000000000000000000000000000000000000000000000000000000000000000)",
   200  		},
   201  		"invalid location": {
   202  			PackageLocalDir("testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64"),
   203  			"cannot check archive hash for non-archive location testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64",
   204  		},
   205  	}
   206  
   207  	for name, test := range tests {
   208  		t.Run(name, func(t *testing.T) {
   209  			// Zero expected checksum, either because we'll error before we
   210  			// reach it, or we want to force a checksum mismatch
   211  			auth := NewArchiveChecksumAuthentication(Platform{"linux", "amd64"}, [sha256.Size]byte{0})
   212  			result, err := auth.AuthenticatePackage(test.location)
   213  
   214  			if result != nil {
   215  				t.Errorf("wrong result: got %#v, want nil", result)
   216  			}
   217  			if gotErr := err.Error(); gotErr != test.err {
   218  				t.Errorf("wrong err: got %q, want %q", gotErr, test.err)
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  // Matching checksum authentication takes a SHA256SUMS document, an archive
   225  // filename, and an expected SHA256 hash. On success both return values should
   226  // be nil.
   227  func TestMatchingChecksumAuthentication_success(t *testing.T) {
   228  	// Location is unused
   229  	location := PackageLocalArchive("testdata/my-package.zip")
   230  
   231  	// Two different checksums for other files
   232  	wantSHA256Sum := [sha256.Size]byte{0xde, 0xca, 0xde}
   233  	otherSHA256Sum := [sha256.Size]byte{0xc0, 0xff, 0xee}
   234  
   235  	document := []byte(
   236  		fmt.Sprintf(
   237  			"%x README.txt\n%x my-package.zip\n",
   238  			otherSHA256Sum,
   239  			wantSHA256Sum,
   240  		),
   241  	)
   242  	filename := "my-package.zip"
   243  
   244  	auth := NewMatchingChecksumAuthentication(document, filename, wantSHA256Sum)
   245  	result, err := auth.AuthenticatePackage(location)
   246  
   247  	// NOTE: This also tests the expired key ignore logic as they key in the test is expired
   248  	if result != nil {
   249  		t.Errorf("wrong result: got %#v, want nil", result)
   250  	}
   251  	if err != nil {
   252  		t.Errorf("wrong err: got %s, want nil", err)
   253  	}
   254  }
   255  
   256  // Matching checksum authentication can fail for three reasons: no checksum
   257  // in the document for the filename, invalid checksum value, and non-matching
   258  // checksum value.
   259  func TestMatchingChecksumAuthentication_failure(t *testing.T) {
   260  	wantSHA256Sum := [sha256.Size]byte{0xde, 0xca, 0xde}
   261  	filename := "my-package.zip"
   262  
   263  	tests := map[string]struct {
   264  		document []byte
   265  		err      string
   266  	}{
   267  		"no checksum for filename": {
   268  			[]byte(
   269  				fmt.Sprintf(
   270  					"%x README.txt",
   271  					[sha256.Size]byte{0xbe, 0xef},
   272  				),
   273  			),
   274  			`checksum list has no SHA-256 hash for "my-package.zip"`,
   275  		},
   276  		"invalid checksum": {
   277  			[]byte(
   278  				fmt.Sprintf(
   279  					"%s README.txt\n%s my-package.zip",
   280  					"horses",
   281  					"chickens",
   282  				),
   283  			),
   284  			`checksum list has invalid SHA256 hash "chickens": encoding/hex: invalid byte: U+0068 'h'`,
   285  		},
   286  		"checksum mismatch": {
   287  			[]byte(
   288  				fmt.Sprintf(
   289  					"%x README.txt\n%x my-package.zip",
   290  					[sha256.Size]byte{0xbe, 0xef},
   291  					[sha256.Size]byte{0xc0, 0xff, 0xee},
   292  				),
   293  			),
   294  			"checksum list has unexpected SHA-256 hash c0ffee0000000000000000000000000000000000000000000000000000000000 (expected decade0000000000000000000000000000000000000000000000000000000000)",
   295  		},
   296  	}
   297  
   298  	for name, test := range tests {
   299  		t.Run(name, func(t *testing.T) {
   300  			// Location is unused
   301  			location := PackageLocalArchive("testdata/my-package.zip")
   302  
   303  			auth := NewMatchingChecksumAuthentication(test.document, filename, wantSHA256Sum)
   304  			result, err := auth.AuthenticatePackage(location)
   305  
   306  			if result != nil {
   307  				t.Errorf("wrong result: got %#v, want nil", result)
   308  			}
   309  			if gotErr := err.Error(); gotErr != test.err {
   310  				t.Errorf("wrong err: got %q, want %q", gotErr, test.err)
   311  			}
   312  		})
   313  	}
   314  }
   315  
   316  // Signature authentication takes a checksum document, a signature, and a list
   317  // of signing keys. If the document is signed by one of the given keys, the
   318  // authentication is successful. The value of the result depends on the signing
   319  // key.
   320  func TestSignatureAuthentication_success(t *testing.T) {
   321  	tests := map[string]struct {
   322  		signature string
   323  		keys      []SigningKey
   324  		result    PackageAuthenticationResult
   325  	}{
   326  		"community provider": {
   327  			testAuthorSignatureGoodBase64,
   328  			[]SigningKey{
   329  				{
   330  					ASCIIArmor: testAuthorKeyArmor,
   331  				},
   332  			},
   333  			PackageAuthenticationResult{
   334  				result: signed,
   335  				KeyID:  testAuthorKeyID,
   336  			},
   337  		},
   338  		"multiple signing keys": {
   339  			testAuthorSignatureGoodBase64,
   340  			[]SigningKey{
   341  				{
   342  					ASCIIArmor: anotherPublicKey,
   343  				},
   344  				{
   345  					ASCIIArmor: testAuthorKeyArmor,
   346  				},
   347  			},
   348  			PackageAuthenticationResult{
   349  				result: signed,
   350  				KeyID:  testAuthorKeyID,
   351  			},
   352  		},
   353  	}
   354  
   355  	for name, test := range tests {
   356  		t.Run(name, func(t *testing.T) {
   357  			// Location is unused
   358  			location := PackageLocalArchive("testdata/my-package.zip")
   359  			//
   360  			//providerSource, err := tfaddr.ParseProviderSource("testdata/my-package.zip")
   361  			//if err != nil {
   362  			//	t.Fatal(err)
   363  			//}
   364  
   365  			signature, err := base64.StdEncoding.DecodeString(test.signature)
   366  			if err != nil {
   367  				t.Fatal(err)
   368  			}
   369  
   370  			auth := NewSignatureAuthentication(PackageMeta{Location: location}, []byte(testShaSumsPlaceholder), signature, test.keys, nil)
   371  			result, err := auth.AuthenticatePackage(location)
   372  
   373  			if result == nil || *result != test.result {
   374  				t.Errorf("wrong result: got %#v, want %#v", result, test.result)
   375  			}
   376  			if err != nil {
   377  				t.Errorf("wrong err: got %s, want nil", err)
   378  			}
   379  		})
   380  	}
   381  }
   382  
   383  func TestNewSignatureAuthentication_success(t *testing.T) {
   384  	tests := map[string]struct {
   385  		signature string
   386  		keys      []SigningKey
   387  		result    PackageAuthenticationResult
   388  	}{
   389  		"official provider": {
   390  			testHashicorpSignatureGoodBase64,
   391  			[]SigningKey{
   392  				{
   393  					ASCIIArmor: TestingPublicKey,
   394  				},
   395  			},
   396  			PackageAuthenticationResult{
   397  				result: signed,
   398  				KeyID:  testHashiCorpPublicKeyID,
   399  			},
   400  		},
   401  	}
   402  
   403  	for name, test := range tests {
   404  		t.Run(name, func(t *testing.T) {
   405  			// Location is unused
   406  			location := PackageLocalArchive("testdata/my-package.zip")
   407  
   408  			signature, err := base64.StdEncoding.DecodeString(test.signature)
   409  			if err != nil {
   410  				t.Fatal(err)
   411  			}
   412  
   413  			auth := NewSignatureAuthentication(PackageMeta{Location: location}, []byte(testProviderShaSums), signature, test.keys, nil)
   414  			result, err := auth.AuthenticatePackage(location)
   415  
   416  			if result == nil || *result != test.result {
   417  				t.Errorf("wrong result: got %#v, want %#v", result, test.result)
   418  			}
   419  			if err != nil {
   420  				t.Errorf("wrong err: got %s, want nil", err)
   421  			}
   422  		})
   423  	}
   424  }
   425  func TestNewSignatureAuthentication_expired(t *testing.T) {
   426  	tests := map[string]struct {
   427  		signature string
   428  		keys      []SigningKey
   429  	}{
   430  		"official provider": {
   431  			testHashicorpSignatureGoodBase64,
   432  			[]SigningKey{
   433  				{
   434  					ASCIIArmor: TestingPublicKey,
   435  				},
   436  			},
   437  		},
   438  	}
   439  	t.Setenv(enforceGPGExpirationEnvName, "true")
   440  
   441  	for name, test := range tests {
   442  		t.Run(name, func(t *testing.T) {
   443  			// Location is unused
   444  			location := PackageLocalArchive("testdata/my-package.zip")
   445  
   446  			signature, err := base64.StdEncoding.DecodeString(test.signature)
   447  			if err != nil {
   448  				t.Fatal(err)
   449  			}
   450  
   451  			auth := NewSignatureAuthentication(PackageMeta{Location: location}, []byte(testProviderShaSums), signature, test.keys, nil)
   452  			_, err = auth.AuthenticatePackage(location)
   453  
   454  			if err == nil {
   455  				t.Errorf("wrong err: got %s, want %s", err, openpgpErrors.ErrKeyExpired)
   456  			}
   457  		})
   458  	}
   459  	t.Setenv(enforceGPGExpirationEnvName, "")
   460  }
   461  
   462  // Signature authentication can fail for many reasons, most of which are due
   463  // to OpenPGP failures from malformed keys or signatures.
   464  func TestSignatureAuthentication_failure(t *testing.T) {
   465  	tests := map[string]struct {
   466  		signature string
   467  		keys      []SigningKey
   468  		err       string
   469  	}{
   470  		"invalid key": {
   471  			testHashicorpSignatureGoodBase64,
   472  			[]SigningKey{
   473  				{
   474  					ASCIIArmor: "invalid PGP armor value",
   475  				},
   476  			},
   477  			"error decoding signing key: openpgp: invalid argument: no armored data found",
   478  		},
   479  		"invalid signature": {
   480  			testSignatureBadBase64,
   481  			[]SigningKey{
   482  				{
   483  					ASCIIArmor: testAuthorKeyArmor,
   484  				},
   485  			},
   486  			"error checking signature: openpgp: invalid data: signature subpacket truncated",
   487  		},
   488  		"no keys match signature": {
   489  			testAuthorSignatureGoodBase64,
   490  			[]SigningKey{
   491  				{
   492  					ASCIIArmor: TestingPublicKey,
   493  				},
   494  			},
   495  			"authentication signature from unknown issuer",
   496  		},
   497  	}
   498  
   499  	for name, test := range tests {
   500  		t.Run(name, func(t *testing.T) {
   501  			// Location is unused
   502  			location := PackageLocalArchive("testdata/my-package.zip")
   503  
   504  			signature, err := base64.StdEncoding.DecodeString(test.signature)
   505  			if err != nil {
   506  				t.Fatal(err)
   507  			}
   508  
   509  			auth := NewSignatureAuthentication(PackageMeta{Location: location}, []byte(testShaSumsPlaceholder), signature, test.keys, nil)
   510  			result, err := auth.AuthenticatePackage(location)
   511  
   512  			if result != nil {
   513  				t.Errorf("wrong result: got %#v, want nil", result)
   514  			}
   515  			if gotErr := err.Error(); gotErr != test.err {
   516  				t.Errorf("wrong err: got %s, want %s", gotErr, test.err)
   517  			}
   518  		})
   519  	}
   520  }
   521  
   522  func TestSignatureAuthentication_acceptableHashes(t *testing.T) {
   523  	auth := NewSignatureAuthentication(PackageMeta{}, []byte(testShaSumsRealistic), nil, nil, nil)
   524  	authWithHashes, ok := auth.(PackageAuthenticationHashes)
   525  	if !ok {
   526  		t.Fatalf("%T does not implement PackageAuthenticationHashes", auth)
   527  	}
   528  	got := authWithHashes.AcceptableHashes()
   529  	want := []Hash{
   530  		// These are the hashes encoded in constant testShaSumsRealistic
   531  		"zh:7d7e888fdd28abfe00894f9055209b9eec785153641de98e6852aa071008d4ee",
   532  		"zh:f8b6cf9ade087c17826d49d89cef21261cdc22bd27065bbc5b27d7dbf7fbbf6c",
   533  		"zh:a5ba9945606bb7bfb821ba303957eeb40dd9ee4e706ba8da1eaf7cbeb0356e63",
   534  		"zh:df3a5a8d6ffff7bacf19c92d10d0d500f98169ea17b3764b01a789f563d1aad7",
   535  		"zh:086119a26576d06b8281a97e8644380da89ce16197cd955f74ea5ee664e9358b",
   536  		"zh:1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239",
   537  		"zh:0e9fd0f3e2254b526a0e81e0cfdfc82583b0cd343778c53ead21aa7d52f776d7",
   538  		"zh:66a947e7de1c74caf9f584c3ed4e91d2cb1af6fe5ce8abaf1cf8f7ff626a09d1",
   539  		"zh:def1b73849bec0dc57a04405847921bf9206c75b52ae9de195476facb26bd85e",
   540  		"zh:48f1826ec31d6f104e46cc2022b41f30cd1019ef48eaec9697654ef9ec37a879",
   541  		"zh:17e0b496022bc4e4137be15e96d2b051c8acd6e14cb48d9b13b262330464f6cc",
   542  		"zh:2696c86228f491bc5425561c45904c9ce39b1c676b1e17734cb2ee6b578c4bcd",
   543  	}
   544  	if diff := cmp.Diff(want, got); diff != "" {
   545  		t.Errorf("wrong result\n%s", diff)
   546  	}
   547  }
   548  
   549  const testAuthorKeyID = `37A6AB3BCF2C170A`
   550  
   551  // testAuthorKeyArmor is test key ID 5BFEEC4317E746008621970637A6AB3BCF2C170A.
   552  const testAuthorKeyArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
   553  
   554  mQENBF5vhgYBCAC40OcC2hEx3yGiLhHMbt7DAVEQ0nZwAWy6oL98niknLumBa1VO
   555  nMYshP+o/FKOFatBl8aXhmDo606P6pD9d4Pg/WNehqT7hGNHcAFlm+8qjQAvE5uX
   556  Z/na/Np7dmWasCiL5hYyHEnKU/XFpc9KyicbkS7n8igP1LEb8xDD1pMLULQsQHA4
   557  258asvtwjoYTZIij1I6bUE178bGFPNCfj+FzQM8nKzPpDVxZ7njN9c2sB9FEdJ1+
   558  S9mZQNK5PbJuEAOpD5Jp9BnGE16jsLUhDmvGHBjFZAXMBkNSloEMHhs2ty9lEzoF
   559  eJmJx7XCGw+ds1SWp4MsHQPWzXxAlrfa4GMlABEBAAG0R1RlcnJhZm9ybSBUZXN0
   560  aW5nIChwbHVnaW4vZGlzY292ZXJ5LykgPHRlcnJhZm9ybSt0ZXN0aW5nQGhhc2hp
   561  Y29ycC5jb20+iQFOBBMBCAA4FiEEW/7sQxfnRgCGIZcGN6arO88sFwoFAl5vhgYC
   562  GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQN6arO88sFwpWvQf/apaMu4Bm
   563  ea8AGjdl9acQhHBpWsyiHLIfZvN11xxN/f3+YITvPXIe2PMgveqNfXxu6PIeZGDb
   564  0DBvnBQy/vqmA+sCQ8t8+kIWdfZ1EeM2YcXdmAEtriooLvc85JFYjafLIKSj9N7o
   565  V/R/e1BCW/v1/7Je47c+6FSt3HHhwyT5AZ3BCq1zpw6PeCDSQ/gZr3Mvq4CjeLA/
   566  K+8TM3KyOF4qBGDvzGzp/t9umQSS2L0ozd90lxJtf5Q8ozqDaBiDo+f/osXT2EvN
   567  VwPP/xh/gABkXiNrPylFbeD+XPAC4N7NmYK5aPDzRYXXknP8e9PDMykoJKZ+bSdz
   568  F3IZ4q5RDHmmNbkBDQReb4YGAQgAt15e1F8TPQQm1jK8+scypHgfmPHbp7Qsulo1
   569  GTcUd8QmhbR4kayuLDEpJYzq6+IoTM4TPqsdVuq/1Nwey9oyK0wXk/SUR29nRIQh
   570  3GBg7JVg1YsObsfVTvEflYOdjk8T/Udqs4I6HnmSbtzsaohzybutpWXPUkW8OzFI
   571  ATwfVTrrz70Yxs+ly0nSEH2Yf+kg2uYZvv5KsJ3MNENhXnHnlaTy2IfhsxAX0xOG
   572  pa9fXV3NzdEbl0mYaEzMi77qRAyIQ9VrIL5F0yY/LlbpLSl6xk2+BB2v3a1Ey6SJ
   573  w4/le6AM0wlH2hKPCTlkvM0IvUWjlzrPzCkeu027iVc+fqdyiQARAQABiQE2BBgB
   574  CAAgFiEEW/7sQxfnRgCGIZcGN6arO88sFwoFAl5vhgYCGwwACgkQN6arO88sFwqz
   575  nAf/eF4oZG9F8sJX01mVdDm/L7Uthe4xjTdl7jwV4ygNX+pCyWrww3qc3qbd3QKg
   576  CFqIt/TAPE/OxHxCFuxalQefpOqfxjKzvcktxzWmpgxaWsvHaXiS4bKBPz78N/Ke
   577  MUtcjGHyLeSzYPUfjquqDzQxqXidRYhyHGSy9c0NKZ6wCElLZ6KcmCQb4sZxVwfu
   578  ssjwAFbPMp1nr0f5SWCJfhTh7QF7lO2ldJaKMlcBM8aebmqFQ52P7ZWOFcgeerng
   579  G7Zdrci1KEd943HhzDCsUFz4gJwbvUyiAYb2ddndpUBkYwCB/XrHWPOSnGxHgZoo
   580  1gIqed9OV/+s5wKxZPjL0pCStQ==
   581  =mYqJ
   582  -----END PGP PUBLIC KEY BLOCK-----`
   583  
   584  // testAuthorEccKeyArmor uses Curve 25519 and has test key ID D01ED5C4BB1ED36A014B0D376540DDA046E5E135
   585  const testAuthorEccKeyArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
   586  
   587  mDMEY1B7+hYJKwYBBAHaRw8BAQdAFRDpASP+iDY+QotOBP9DF5CfuhSBD8Dl0hSG
   588  D7plEsO0M1RlcnJhZm9ybSBUZXN0aW5nIDx0ZXJyYWZvcm0rdGVzdGluZ0BoYXNo
   589  aWNvcnAuY29tPoiTBBMWCgA7FiEE0B7VxLse02oBSw03ZUDdoEbl4TUFAmNQe/oC
   590  GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQZUDdoEbl4TWhwwD+N/BR
   591  pR9NhRFDm+JRhA3saKmpTSRo9yJnr6tRlumE4KQA/A2cOCDeezf6t3SXltoYUKIt
   592  EYmbLxgMDlffVkFyC8IMuDgEY1B7+hIKKwYBBAGXVQEFAQEHQJ7frE76Le1qI1Go
   593  dfrVIzEgAcYjDW6T01/V95wgqPIuAwEIB4h4BBgWCgAgFiEE0B7VxLse02oBSw03
   594  ZUDdoEbl4TUFAmNQe/oCGwwACgkQZUDdoEbl4TWvsAD/YSQAigAH5hq4OmK4gs0J
   595  O74RFokGZzbPtoIvutb8eYoA/1QxxyqE/8A4Z21azYEO0j563LRa8SkZcB5UPDy3
   596  7ngJ
   597  =Xb0o
   598  -----END PGP PUBLIC KEY BLOCK-----`
   599  
   600  // testShaSumsPlaceholder is a string that represents a signed document that
   601  // the signature authenticator will check. Some of the signature values in
   602  // other constants in this file are signing this string.
   603  const testShaSumsPlaceholder = "example shasums data"
   604  
   605  // testShaSumsRealistic is a more realistic SHA256SUMS document that we can use
   606  // to test the AcceptableHashes method. The signature values in other constants
   607  // in this file do not sign this string.
   608  const testShaSumsRealistic = `7d7e888fdd28abfe00894f9055209b9eec785153641de98e6852aa071008d4ee  terraform_0.14.0-alpha20200923_darwin_amd64.zip
   609  f8b6cf9ade087c17826d49d89cef21261cdc22bd27065bbc5b27d7dbf7fbbf6c  terraform_0.14.0-alpha20200923_freebsd_386.zip
   610  a5ba9945606bb7bfb821ba303957eeb40dd9ee4e706ba8da1eaf7cbeb0356e63  terraform_0.14.0-alpha20200923_freebsd_amd64.zip
   611  df3a5a8d6ffff7bacf19c92d10d0d500f98169ea17b3764b01a789f563d1aad7  terraform_0.14.0-alpha20200923_freebsd_arm.zip
   612  086119a26576d06b8281a97e8644380da89ce16197cd955f74ea5ee664e9358b  terraform_0.14.0-alpha20200923_linux_386.zip
   613  1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239  terraform_0.14.0-alpha20200923_linux_amd64.zip
   614  0e9fd0f3e2254b526a0e81e0cfdfc82583b0cd343778c53ead21aa7d52f776d7  terraform_0.14.0-alpha20200923_linux_arm.zip
   615  66a947e7de1c74caf9f584c3ed4e91d2cb1af6fe5ce8abaf1cf8f7ff626a09d1  terraform_0.14.0-alpha20200923_openbsd_386.zip
   616  def1b73849bec0dc57a04405847921bf9206c75b52ae9de195476facb26bd85e  terraform_0.14.0-alpha20200923_openbsd_amd64.zip
   617  48f1826ec31d6f104e46cc2022b41f30cd1019ef48eaec9697654ef9ec37a879  terraform_0.14.0-alpha20200923_solaris_amd64.zip
   618  17e0b496022bc4e4137be15e96d2b051c8acd6e14cb48d9b13b262330464f6cc  terraform_0.14.0-alpha20200923_windows_386.zip
   619  2696c86228f491bc5425561c45904c9ce39b1c676b1e17734cb2ee6b578c4bcd  terraform_0.14.0-alpha20200923_windows_amd64.zip`
   620  
   621  // testAuthorSignatureGoodBase64 is a signature of testShaSums signed with
   622  // testAuthorKeyArmor, which represents the SHA256SUMS.sig file downloaded for
   623  // a release.
   624  const testAuthorSignatureGoodBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +
   625  	`FwoFAl5vh7gACgkQN6arO88sFwrAlQf6Al77qzjxNIj+NQNJfBGYUE5jHIgcuWOs1IPRTYUI` +
   626  	`rHQIUU2RVrdHoAefKTKNzGde653JK/pYTflSV+6ini3/aZZnXlF6t001w3wswmakdwTr0hXx` +
   627  	`Ez/hHYio72Gpn7+T/L+nl6dKkjeGqd/Kor5x2TY9uYB737ESmAe5T8ZlPaGMFHh0mYlNTeRq` +
   628  	`4qIKqL6DwddBF4Ju2svn2MeNMGfE358H31mxAl2k4PPrwBTR1sFUCUOzAXVA/g9Ov5Y9ni2G` +
   629  	`rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` +
   630  	`n1ayZdaCIw/r4w==`
   631  
   632  // testSignatureBadBase64 is an invalid signature.
   633  const testSignatureBadBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +
   634  	`4qIKqL6DwddBF4Ju2svn2MeNMGfE358H31mxAl2k4PPrwBTR1sFUCUOzAXVA/g9Ov5Y9ni2G` +
   635  	`rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` +
   636  	`n1ayZdaCIw/r4w==`
   637  
   638  // testHashiCorpPublicKeyID is the Key ID of the HashiCorpPublicKey.
   639  const testHashiCorpPublicKeyID = `34365D9472D7468F`
   640  
   641  const testProviderShaSums = `fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e  terraform-provider-null_3.1.0_darwin_amd64.zip
   642  9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2  terraform-provider-null_3.1.0_darwin_arm64.zip
   643  a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e  terraform-provider-null_3.1.0_freebsd_386.zip
   644  5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521  terraform-provider-null_3.1.0_freebsd_amd64.zip
   645  fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b  terraform-provider-null_3.1.0_freebsd_arm.zip
   646  c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d  terraform-provider-null_3.1.0_linux_386.zip
   647  53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515  terraform-provider-null_3.1.0_linux_amd64.zip
   648  cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8  terraform-provider-null_3.1.0_linux_arm64.zip
   649  e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70  terraform-provider-null_3.1.0_linux_arm.zip
   650  a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53  terraform-provider-null_3.1.0_windows_386.zip
   651  02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2  terraform-provider-null_3.1.0_windows_amd64.zip
   652  `
   653  
   654  // testHashicorpSignatureGoodBase64 is a signature of testProviderShaSums signed with
   655  // HashicorpPublicKey, which represents the SHA256SUMS.sig file downloaded for
   656  // an official release.
   657  const testHashicorpSignatureGoodBase64 = `wsFcBAABCAAQBQJgga+GCRCwtEEJdoW2dgAA` +
   658  	`o0YQAAW911BGDr2WHLo5NwcZenwHyxL5DX9g+4BknKbc/WxRC1hD8Afi3eygZk1yR6eT4Gp2H` +
   659  	`yNOwCjGL1PTONBumMfj9udIeuX8onrJMMvjFHh+bORGxBi4FKr4V3b2ZV1IYOjWMEyyTGRDvw` +
   660  	`SCdxBkp3apH3s2xZLmRoAj84JZ4KaxGF7hlT0j4IkNyQKd2T5cCByN9DV80+x+HtzaOieFwJL` +
   661  	`97iyGj6aznXfKfslK6S4oIrVTwyLTrQbxSxA0LsdUjRPHnJamL3sFOG77qUEUoXG3r61yi5vW` +
   662  	`V4P5gCH/+C+VkfGHqaB1s0jHYLxoTEXtwthe66MydDBPe2Hd0J12u9ppOIeK3leeb4uiixWIi` +
   663  	`rNdpWyjr/LU1KKWPxsDqMGYJ9TexyWkXjEpYmIEiY1Rxar8jrLh+FqVAhxRJajjgSRu5pZj50` +
   664  	`CNeKmmbyolLhPCmICjYYU/xKPGXSyDFqonVVyMWCSpO+8F38OmwDQHIk5AWyc8hPOAZ+g5N95` +
   665  	`cfUAzEqlvmNvVHQIU40Y6/Ip2HZzzFCLKQkMP1aDakYHq5w4ZO/ucjhKuoh1HDQMuMnZSu4eo` +
   666  	`2nMTBzYZnUxwtROrJZF1t103avbmP2QE/GaPvLIQn7o5WMV3ZcPCJ+szzzby7H2e33WIynrY/` +
   667  	`95ensBxh7mGFbcQ1C59b5o7viwIaaY2`
   668  
   669  // entityString function is used for logging the signing key.
   670  func TestEntityString(t *testing.T) {
   671  	var tests = []struct {
   672  		name     string
   673  		entity   *openpgp.Entity
   674  		expected string
   675  	}{
   676  		{
   677  			"nil",
   678  			nil,
   679  			"",
   680  		},
   681  		{
   682  			"testAuthorEccKeyArmor",
   683  			testReadArmoredEntity(t, testAuthorEccKeyArmor),
   684  			"6540DDA046E5E135 Terraform Testing <terraform+testing@hashicorp.com>",
   685  		},
   686  		{
   687  			"testAuthorKeyArmor",
   688  			testReadArmoredEntity(t, testAuthorKeyArmor),
   689  			"37A6AB3BCF2C170A Terraform Testing (plugin/discovery/) <terraform+testing@hashicorp.com>",
   690  		},
   691  		{
   692  			"HashicorpPublicKey",
   693  			testReadArmoredEntity(t, TestingPublicKey),
   694  			"34365D9472D7468F HashiCorp Security (hashicorp.com/security) <security@hashicorp.com>",
   695  		},
   696  		{
   697  			"HashicorpPartnersKey",
   698  			testReadArmoredEntity(t, anotherPublicKey),
   699  			"7D72D4268E4660FC HashiCorp Security (Terraform Partner Signing) <security+terraform@hashicorp.com>",
   700  		},
   701  	}
   702  
   703  	for _, tt := range tests {
   704  		t.Run(tt.name, func(t *testing.T) {
   705  			actual := entityString(tt.entity)
   706  			if actual != tt.expected {
   707  				t.Errorf("expected %s, actual %s", tt.expected, actual)
   708  			}
   709  		})
   710  	}
   711  }
   712  
   713  func testReadArmoredEntity(t *testing.T, armor string) *openpgp.Entity {
   714  	data := strings.NewReader(armor)
   715  
   716  	el, err := openpgp.ReadArmoredKeyRing(data)
   717  	if err != nil {
   718  		t.Fatal(err)
   719  	}
   720  
   721  	if count := len(el); count != 1 {
   722  		t.Fatalf("expected 1 entity, got %d", count)
   723  	}
   724  
   725  	return el[0]
   726  }
   727  
   728  func TestShouldEnforceGPGValidation(t *testing.T) {
   729  	tests := []struct {
   730  		name           string
   731  		providerSource *tfaddr.Provider
   732  		keys           []SigningKey
   733  		envVarValue    string
   734  		expected       bool
   735  	}{
   736  		{
   737  			name: "default provider registry, no keys",
   738  			providerSource: &tfaddr.Provider{
   739  				Hostname: tfaddr.DefaultProviderRegistryHost,
   740  			},
   741  			keys:        []SigningKey{},
   742  			envVarValue: "",
   743  			expected:    false,
   744  		},
   745  		{
   746  			name: "default provider registry, some keys",
   747  			providerSource: &tfaddr.Provider{
   748  				Hostname: tfaddr.DefaultProviderRegistryHost,
   749  			},
   750  			keys: []SigningKey{
   751  				{
   752  					ASCIIArmor: testAuthorKeyArmor,
   753  				},
   754  			},
   755  			envVarValue: "",
   756  			expected:    true,
   757  		},
   758  		{
   759  			name: "non-default provider registry, no keys",
   760  			providerSource: &tfaddr.Provider{
   761  				Hostname: "my-registry.com",
   762  			},
   763  			keys:        []SigningKey{},
   764  			envVarValue: "",
   765  			expected:    true,
   766  		},
   767  		{
   768  			name: "non-default provider registry, some keys",
   769  			providerSource: &tfaddr.Provider{
   770  				Hostname: "my-registry.com",
   771  			},
   772  			keys: []SigningKey{
   773  				{
   774  					ASCIIArmor: testAuthorKeyArmor,
   775  				},
   776  			},
   777  			envVarValue: "",
   778  			expected:    true,
   779  		},
   780  		// env var "true"
   781  		{
   782  			name: "default provider registry, no keys, env var true",
   783  			providerSource: &tfaddr.Provider{
   784  				Hostname: tfaddr.DefaultProviderRegistryHost,
   785  			},
   786  			keys:        []SigningKey{},
   787  			envVarValue: "true",
   788  			expected:    true,
   789  		},
   790  		{
   791  			name: "default provider registry, some keys, env var true",
   792  			providerSource: &tfaddr.Provider{
   793  				Hostname: tfaddr.DefaultProviderRegistryHost,
   794  			},
   795  			keys: []SigningKey{
   796  				{
   797  					ASCIIArmor: testAuthorKeyArmor,
   798  				},
   799  			},
   800  			envVarValue: "true",
   801  			expected:    true,
   802  		}, {
   803  			name: "non-default provider registry, no keys, env var true",
   804  			providerSource: &tfaddr.Provider{
   805  				Hostname: "my-registry.com",
   806  			},
   807  			keys:        []SigningKey{},
   808  			envVarValue: "true",
   809  			expected:    true,
   810  		},
   811  		{
   812  			name: "non-default provider registry, some keys, env var true",
   813  			providerSource: &tfaddr.Provider{
   814  				Hostname: "my-registry.com",
   815  			},
   816  			keys: []SigningKey{
   817  				{
   818  					ASCIIArmor: testAuthorKeyArmor,
   819  				},
   820  			},
   821  			envVarValue: "true",
   822  			expected:    true,
   823  		},
   824  		// env var "false"
   825  		{
   826  			name: "default provider registry, no keys, env var false",
   827  			providerSource: &tfaddr.Provider{
   828  				Hostname: tfaddr.DefaultProviderRegistryHost,
   829  			},
   830  			keys:        []SigningKey{},
   831  			envVarValue: "false",
   832  			expected:    false,
   833  		},
   834  		{
   835  			name: "default provider registry, some keys, env var false",
   836  			providerSource: &tfaddr.Provider{
   837  				Hostname: tfaddr.DefaultProviderRegistryHost,
   838  			},
   839  			keys: []SigningKey{
   840  				{
   841  					ASCIIArmor: testAuthorKeyArmor,
   842  				},
   843  			},
   844  			envVarValue: "false",
   845  			expected:    true,
   846  		}, {
   847  			name: "non-default provider registry, no keys, env var false",
   848  			providerSource: &tfaddr.Provider{
   849  				Hostname: "my-registry.com",
   850  			},
   851  			keys:        []SigningKey{},
   852  			envVarValue: "false",
   853  			expected:    true,
   854  		},
   855  		{
   856  			name: "non-default provider registry, some keys, env var false",
   857  			providerSource: &tfaddr.Provider{
   858  				Hostname: "my-registry.com",
   859  			},
   860  			keys: []SigningKey{
   861  				{
   862  					ASCIIArmor: testAuthorKeyArmor,
   863  				},
   864  			},
   865  			envVarValue: "false",
   866  			expected:    true,
   867  		},
   868  	}
   869  
   870  	for _, tt := range tests {
   871  		t.Run(tt.name, func(t *testing.T) {
   872  
   873  			sigAuth := signatureAuthentication{
   874  				ProviderSource: tt.providerSource,
   875  				Keys:           tt.keys,
   876  			}
   877  
   878  			if tt.envVarValue != "" {
   879  				t.Setenv(enforceGPGValidationEnvName, tt.envVarValue)
   880  			}
   881  
   882  			actual, err := sigAuth.shouldEnforceGPGValidation()
   883  			if err != nil {
   884  				t.Fatal(err)
   885  			}
   886  			if actual != tt.expected {
   887  				t.Errorf("expected %t, actual %t", tt.expected, actual)
   888  			}
   889  		})
   890  	}
   891  }