github.com/argoproj/argo-cd/v3@v3.2.1/util/gpg/gpg_test.go (about)

     1  package gpg
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"os/exec"
     8  	"path"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/argoproj/argo-cd/v3/common"
    15  	"github.com/argoproj/argo-cd/v3/test"
    16  )
    17  
    18  const (
    19  	longKeyID  = "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23"
    20  	shortKeyID = "4AEE18F83AFDEB23"
    21  )
    22  
    23  var syncTestSources = map[string]string{
    24  	"F7842A5CEAA9C0B1": "testdata/janedoe.asc",
    25  	"FDC79815400D88A9": "testdata/johndoe.asc",
    26  	"4AEE18F83AFDEB23": "testdata/github.asc",
    27  }
    28  
    29  // Helper function to create temporary GNUPGHOME
    30  func initTempDir(t *testing.T) string {
    31  	t.Helper()
    32  	// Intentionally avoid using t.TempDir. That function creates really long paths, which can exceed the socket file
    33  	// path length on some OSes. The GPG tests rely on sockets.
    34  	p, err := os.MkdirTemp(os.TempDir(), "")
    35  	if err != nil {
    36  		panic(err)
    37  	}
    38  	fmt.Printf("-> Using %s as GNUPGHOME\n", p)
    39  	t.Setenv(common.EnvGnuPGHome, p)
    40  	t.Cleanup(func() {
    41  		err := os.RemoveAll(p)
    42  		if err != nil {
    43  			panic(err)
    44  		}
    45  	})
    46  	return p
    47  }
    48  
    49  func Test_IsGPGEnabled(t *testing.T) {
    50  	t.Run("true", func(t *testing.T) {
    51  		t.Setenv("ARGOCD_GPG_ENABLED", "true")
    52  		assert.True(t, IsGPGEnabled())
    53  	})
    54  
    55  	t.Run("false", func(t *testing.T) {
    56  		t.Setenv("ARGOCD_GPG_ENABLED", "false")
    57  		assert.False(t, IsGPGEnabled())
    58  	})
    59  
    60  	t.Run("empty", func(t *testing.T) {
    61  		t.Setenv("ARGOCD_GPG_ENABLED", "")
    62  		assert.True(t, IsGPGEnabled())
    63  	})
    64  }
    65  
    66  func Test_GPG_InitializeGnuPG(t *testing.T) {
    67  	p := initTempDir(t)
    68  
    69  	// First run should initialize fine
    70  	err := InitializeGnuPG()
    71  	require.NoError(t, err)
    72  
    73  	// We should have exactly one public key with ultimate trust (our own) in the keyring
    74  	keys, err := GetInstalledPGPKeys(nil)
    75  	require.NoError(t, err)
    76  	assert.Len(t, keys, 1)
    77  	assert.Equal(t, "ultimate", keys[0].Trust)
    78  
    79  	// During unit-tests, we need to also kill gpg-agent so we can create a new key.
    80  	// In real world scenario -- i.e. container crash -- gpg-agent is not running yet.
    81  	cmd := exec.Command("gpgconf", "--kill", "gpg-agent")
    82  	cmd.Env = []string{"GNUPGHOME=" + p}
    83  	err = cmd.Run()
    84  	require.NoError(t, err)
    85  
    86  	// Second run should not return error
    87  	err = InitializeGnuPG()
    88  	require.NoError(t, err)
    89  	keys, err = GetInstalledPGPKeys(nil)
    90  	require.NoError(t, err)
    91  	assert.Len(t, keys, 1)
    92  	assert.Equal(t, "ultimate", keys[0].Trust)
    93  
    94  	t.Run("GNUPGHOME is a file", func(t *testing.T) {
    95  		f, err := os.CreateTemp(t.TempDir(), "gpg-test")
    96  		require.NoError(t, err)
    97  		defer os.Remove(f.Name())
    98  
    99  		// we need to error out
   100  		t.Setenv(common.EnvGnuPGHome, f.Name())
   101  		err = InitializeGnuPG()
   102  		assert.ErrorContains(t, err, "does not point to a directory")
   103  	})
   104  
   105  	t.Run("Unaccessible GNUPGHOME", func(t *testing.T) {
   106  		p := initTempDir(t)
   107  		fp := p + "/gpg"
   108  		err = os.Mkdir(fp, 0o000)
   109  		if err != nil {
   110  			panic(err.Error())
   111  		}
   112  		if err != nil {
   113  			panic(err.Error())
   114  		}
   115  		t.Setenv(common.EnvGnuPGHome, fp)
   116  		err := InitializeGnuPG()
   117  		require.Error(t, err)
   118  		// Restore permissions so path can be deleted
   119  		err = os.Chmod(fp, 0o700)
   120  		if err != nil {
   121  			panic(err.Error())
   122  		}
   123  	})
   124  
   125  	t.Run("GNUPGHOME with too wide permissions", func(t *testing.T) {
   126  		// We do not expect an error here, because of openshift's random UIDs that
   127  		// forced us to use an emptyDir mount (#4127)
   128  		p := initTempDir(t)
   129  		err := os.Chmod(p, 0o777)
   130  		if err != nil {
   131  			panic(err.Error())
   132  		}
   133  		t.Setenv(common.EnvGnuPGHome, p)
   134  		err = InitializeGnuPG()
   135  		require.NoError(t, err)
   136  	})
   137  }
   138  
   139  func Test_GPG_KeyManagement(t *testing.T) {
   140  	initTempDir(t)
   141  
   142  	err := InitializeGnuPG()
   143  	require.NoError(t, err)
   144  
   145  	// Import a single good key
   146  	keys, err := ImportPGPKeys("testdata/github.asc")
   147  	require.NoError(t, err)
   148  	assert.Len(t, keys, 1)
   149  	assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
   150  	assert.Contains(t, keys[0].Owner, "noreply@github.com")
   151  	assert.Equal(t, "unknown", keys[0].Trust)
   152  	assert.Equal(t, "unknown", keys[0].SubType)
   153  
   154  	kids := make([]string, 0)
   155  	importedKeyId := keys[0].KeyID
   156  
   157  	// We should have a total of 2 keys in the keyring now
   158  	{
   159  		keys, err := GetInstalledPGPKeys(nil)
   160  		require.NoError(t, err)
   161  		assert.Len(t, keys, 2)
   162  	}
   163  
   164  	// We should now have that key in our keyring with unknown trust (trustdb not updated)
   165  	{
   166  		keys, err := GetInstalledPGPKeys([]string{importedKeyId})
   167  		require.NoError(t, err)
   168  		assert.Len(t, keys, 1)
   169  		assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
   170  		assert.Contains(t, keys[0].Owner, "noreply@github.com")
   171  		assert.Equal(t, "unknown", keys[0].Trust)
   172  		assert.Equal(t, "rsa2048", keys[0].SubType)
   173  		kids = append(kids, keys[0].Fingerprint)
   174  	}
   175  
   176  	assert.Len(t, kids, 1)
   177  
   178  	// Set trust level for our key and check the result
   179  	{
   180  		err := SetPGPTrustLevelById(kids, "ultimate")
   181  		require.NoError(t, err)
   182  		keys, err := GetInstalledPGPKeys(kids)
   183  		require.NoError(t, err)
   184  		assert.Len(t, keys, 1)
   185  		assert.Equal(t, kids[0], keys[0].Fingerprint)
   186  		assert.Equal(t, "ultimate", keys[0].Trust)
   187  	}
   188  
   189  	// Import garbage - error expected
   190  	keys, err = ImportPGPKeys("testdata/garbage.asc")
   191  	require.Error(t, err)
   192  	assert.Empty(t, keys)
   193  
   194  	// We should still have a total of 2 keys in the keyring now
   195  	{
   196  		keys, err := GetInstalledPGPKeys(nil)
   197  		require.NoError(t, err)
   198  		assert.Len(t, keys, 2)
   199  	}
   200  
   201  	// Delete previously imported public key
   202  	{
   203  		err := DeletePGPKey(importedKeyId)
   204  		require.NoError(t, err)
   205  		keys, err := GetInstalledPGPKeys(nil)
   206  		require.NoError(t, err)
   207  		assert.Len(t, keys, 1)
   208  	}
   209  
   210  	// Delete non-existing key
   211  	{
   212  		err := DeletePGPKey(importedKeyId)
   213  		require.Error(t, err)
   214  	}
   215  
   216  	// Import multiple keys
   217  	{
   218  		keys, err := ImportPGPKeys("testdata/multi.asc")
   219  		require.NoError(t, err)
   220  		assert.Len(t, keys, 2)
   221  		assert.Contains(t, keys[0].Owner, "john.doe@example.com")
   222  		assert.Contains(t, keys[1].Owner, "jane.doe@example.com")
   223  	}
   224  
   225  	// Check if they were really imported
   226  	{
   227  		keys, err := GetInstalledPGPKeys(nil)
   228  		require.NoError(t, err)
   229  		assert.Len(t, keys, 3)
   230  	}
   231  }
   232  
   233  func Test_ImportPGPKeysFromString(t *testing.T) {
   234  	initTempDir(t)
   235  
   236  	err := InitializeGnuPG()
   237  	require.NoError(t, err)
   238  
   239  	// Import a single good key
   240  	keys, err := ImportPGPKeysFromString(test.MustLoadFileToString("testdata/github.asc"))
   241  	require.NoError(t, err)
   242  	assert.Len(t, keys, 1)
   243  	assert.Equal(t, "4AEE18F83AFDEB23", keys[0].KeyID)
   244  	assert.Contains(t, keys[0].Owner, "noreply@github.com")
   245  	assert.Equal(t, "unknown", keys[0].Trust)
   246  	assert.Equal(t, "unknown", keys[0].SubType)
   247  }
   248  
   249  func Test_ValidateGPGKeysFromString(t *testing.T) {
   250  	initTempDir(t)
   251  
   252  	err := InitializeGnuPG()
   253  	require.NoError(t, err)
   254  
   255  	{
   256  		keyData := test.MustLoadFileToString("testdata/github.asc")
   257  		keys, err := ValidatePGPKeysFromString(keyData)
   258  		require.NoError(t, err)
   259  		assert.Len(t, keys, 1)
   260  	}
   261  
   262  	{
   263  		keyData := test.MustLoadFileToString("testdata/multi.asc")
   264  		keys, err := ValidatePGPKeysFromString(keyData)
   265  		require.NoError(t, err)
   266  		assert.Len(t, keys, 2)
   267  	}
   268  }
   269  
   270  func Test_ValidateGPGKeys(t *testing.T) {
   271  	initTempDir(t)
   272  
   273  	err := InitializeGnuPG()
   274  	require.NoError(t, err)
   275  
   276  	// Validation good case - 1 key
   277  	{
   278  		keys, err := ValidatePGPKeys("testdata/github.asc")
   279  		require.NoError(t, err)
   280  		assert.Len(t, keys, 1)
   281  		assert.Contains(t, keys, "4AEE18F83AFDEB23")
   282  	}
   283  
   284  	// Validation bad case
   285  	{
   286  		keys, err := ValidatePGPKeys("testdata/garbage.asc")
   287  		require.Error(t, err)
   288  		assert.Empty(t, keys)
   289  	}
   290  
   291  	// We should still have a total of 1 keys in the keyring now
   292  	{
   293  		keys, err := GetInstalledPGPKeys(nil)
   294  		require.NoError(t, err)
   295  		assert.Len(t, keys, 1)
   296  	}
   297  }
   298  
   299  func Test_GPG_ParseGitCommitVerification(t *testing.T) {
   300  	initTempDir(t)
   301  
   302  	err := InitializeGnuPG()
   303  	require.NoError(t, err)
   304  
   305  	keys, err := ImportPGPKeys("testdata/github.asc")
   306  	require.NoError(t, err)
   307  	assert.Len(t, keys, 1)
   308  
   309  	// Good case
   310  	{
   311  		c, err := os.ReadFile("testdata/good_signature.txt")
   312  		if err != nil {
   313  			panic(err.Error())
   314  		}
   315  		res := ParseGitCommitVerification(string(c))
   316  		assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
   317  		assert.Equal(t, "RSA", res.Cipher)
   318  		assert.Equal(t, "ultimate", res.Trust)
   319  		assert.Equal(t, "Wed Feb 26 23:22:34 2020 CET", res.Date)
   320  		assert.Equal(t, VerifyResultGood, res.Result)
   321  	}
   322  
   323  	// Signature with unknown key - considered invalid
   324  	{
   325  		c, err := os.ReadFile("testdata/unknown_signature1.txt")
   326  		if err != nil {
   327  			panic(err.Error())
   328  		}
   329  		res := ParseGitCommitVerification(string(c))
   330  		assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
   331  		assert.Equal(t, "RSA", res.Cipher)
   332  		assert.Equal(t, TrustUnknown, res.Trust)
   333  		assert.Equal(t, "Mon Aug 26 20:59:48 2019 CEST", res.Date)
   334  		assert.Equal(t, VerifyResultInvalid, res.Result)
   335  	}
   336  
   337  	// Signature with unknown key and additional fields - considered invalid
   338  	{
   339  		c, err := os.ReadFile("testdata/unknown_signature2.txt")
   340  		if err != nil {
   341  			panic(err.Error())
   342  		}
   343  		res := ParseGitCommitVerification(string(c))
   344  		assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
   345  		assert.Equal(t, "RSA", res.Cipher)
   346  		assert.Equal(t, TrustUnknown, res.Trust)
   347  		assert.Equal(t, "Mon Aug 26 20:59:48 2019 CEST", res.Date)
   348  		assert.Equal(t, VerifyResultInvalid, res.Result)
   349  	}
   350  
   351  	// Bad signature with known key
   352  	{
   353  		c, err := os.ReadFile("testdata/bad_signature_bad.txt")
   354  		if err != nil {
   355  			panic(err.Error())
   356  		}
   357  		res := ParseGitCommitVerification(string(c))
   358  		assert.Equal(t, "4AEE18F83AFDEB23", res.KeyID)
   359  		assert.Equal(t, "RSA", res.Cipher)
   360  		assert.Equal(t, "ultimate", res.Trust)
   361  		assert.Equal(t, "Wed Feb 26 23:22:34 2020 CET", res.Date)
   362  		assert.Equal(t, VerifyResultBad, res.Result)
   363  	}
   364  
   365  	// Bad case: Manipulated/invalid clear text signature
   366  	{
   367  		c, err := os.ReadFile("testdata/bad_signature_manipulated.txt")
   368  		if err != nil {
   369  			panic(err.Error())
   370  		}
   371  		res := ParseGitCommitVerification(string(c))
   372  		assert.Equal(t, VerifyResultUnknown, res.Result)
   373  		assert.Contains(t, res.Message, "Could not parse output")
   374  	}
   375  
   376  	// Bad case: Incomplete signature data #1
   377  	{
   378  		c, err := os.ReadFile("testdata/bad_signature_preeof1.txt")
   379  		if err != nil {
   380  			panic(err.Error())
   381  		}
   382  		res := ParseGitCommitVerification(string(c))
   383  		assert.Equal(t, VerifyResultUnknown, res.Result)
   384  		assert.Contains(t, res.Message, "end-of-file")
   385  	}
   386  
   387  	// Bad case: Incomplete signature data #2
   388  	{
   389  		c, err := os.ReadFile("testdata/bad_signature_preeof2.txt")
   390  		if err != nil {
   391  			panic(err.Error())
   392  		}
   393  		res := ParseGitCommitVerification(string(c))
   394  		assert.Equal(t, VerifyResultUnknown, res.Result)
   395  		assert.Contains(t, res.Message, "end-of-file")
   396  	}
   397  
   398  	// Bad case: No signature data #1
   399  	{
   400  		c, err := os.ReadFile("testdata/bad_signature_nodata.txt")
   401  		if err != nil {
   402  			panic(err.Error())
   403  		}
   404  		res := ParseGitCommitVerification(string(c))
   405  		assert.Equal(t, VerifyResultUnknown, res.Result)
   406  		assert.Contains(t, res.Message, "no verification data found")
   407  	}
   408  
   409  	// Bad case: Malformed signature data #1
   410  	{
   411  		c, err := os.ReadFile("testdata/bad_signature_malformed1.txt")
   412  		if err != nil {
   413  			panic(err.Error())
   414  		}
   415  		res := ParseGitCommitVerification(string(c))
   416  		assert.Equal(t, VerifyResultUnknown, res.Result)
   417  		assert.Contains(t, res.Message, "no verification data found")
   418  	}
   419  
   420  	// Bad case: Malformed signature data #2
   421  	{
   422  		c, err := os.ReadFile("testdata/bad_signature_malformed2.txt")
   423  		if err != nil {
   424  			panic(err.Error())
   425  		}
   426  		res := ParseGitCommitVerification(string(c))
   427  		assert.Equal(t, VerifyResultUnknown, res.Result)
   428  		assert.Contains(t, res.Message, "Could not parse key ID")
   429  	}
   430  
   431  	// Bad case: Malformed signature data #3
   432  	{
   433  		c, err := os.ReadFile("testdata/bad_signature_malformed3.txt")
   434  		if err != nil {
   435  			panic(err.Error())
   436  		}
   437  		res := ParseGitCommitVerification(string(c))
   438  		assert.Equal(t, VerifyResultUnknown, res.Result)
   439  		assert.Contains(t, res.Message, "Could not parse result of verify")
   440  	}
   441  
   442  	// Bad case: Invalid key ID in signature
   443  	{
   444  		c, err := os.ReadFile("testdata/bad_signature_badkeyid.txt")
   445  		if err != nil {
   446  			panic(err.Error())
   447  		}
   448  		res := ParseGitCommitVerification(string(c))
   449  		assert.Equal(t, VerifyResultUnknown, res.Result)
   450  		assert.Contains(t, res.Message, "Invalid PGP key ID")
   451  	}
   452  }
   453  
   454  func Test_GetGnuPGHomePath(t *testing.T) {
   455  	t.Run("empty", func(t *testing.T) {
   456  		t.Setenv(common.EnvGnuPGHome, "")
   457  		p := common.GetGnuPGHomePath()
   458  		assert.Equal(t, common.DefaultGnuPgHomePath, p)
   459  	})
   460  
   461  	t.Run("tempdir", func(t *testing.T) {
   462  		t.Setenv(common.EnvGnuPGHome, "/tmp/gpghome")
   463  		p := common.GetGnuPGHomePath()
   464  		assert.Equal(t, "/tmp/gpghome", p)
   465  	})
   466  }
   467  
   468  func Test_KeyID(t *testing.T) {
   469  	// Good case - long key ID (aka fingerprint) to short key ID
   470  	{
   471  		res := KeyID(longKeyID)
   472  		assert.Equal(t, shortKeyID, res)
   473  	}
   474  	// Good case - short key ID remains same
   475  	{
   476  		res := KeyID(shortKeyID)
   477  		assert.Equal(t, shortKeyID, res)
   478  	}
   479  	// Bad case - key ID too short
   480  	{
   481  		keyID := "AEE18F83AFDEB23"
   482  		res := KeyID(keyID)
   483  		assert.Empty(t, res)
   484  	}
   485  	// Bad case - key ID too long
   486  	{
   487  		keyID := "5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB2323"
   488  		res := KeyID(keyID)
   489  		assert.Empty(t, res)
   490  	}
   491  	// Bad case - right length, but not hex string
   492  	{
   493  		keyID := "abcdefghijklmn"
   494  		res := KeyID(keyID)
   495  		assert.Empty(t, res)
   496  	}
   497  }
   498  
   499  func Test_IsShortKeyID(t *testing.T) {
   500  	assert.True(t, IsShortKeyID(shortKeyID))
   501  	assert.False(t, IsShortKeyID(longKeyID))
   502  	assert.False(t, IsShortKeyID("ab"))
   503  }
   504  
   505  func Test_IsLongKeyID(t *testing.T) {
   506  	assert.True(t, IsLongKeyID(longKeyID))
   507  	assert.False(t, IsLongKeyID(shortKeyID))
   508  	assert.False(t, IsLongKeyID(longKeyID+"a"))
   509  }
   510  
   511  func Test_isHexString(t *testing.T) {
   512  	assert.True(t, isHexString("ab0099"))
   513  	assert.True(t, isHexString("AB0099"))
   514  	assert.False(t, isHexString("foobar"))
   515  }
   516  
   517  func Test_IsSecretKey(t *testing.T) {
   518  	initTempDir(t)
   519  
   520  	// First run should initialize fine
   521  	err := InitializeGnuPG()
   522  	require.NoError(t, err)
   523  
   524  	// We should have exactly one public key with ultimate trust (our own) in the keyring
   525  	keys, err := GetInstalledPGPKeys(nil)
   526  	require.NoError(t, err)
   527  	assert.Len(t, keys, 1)
   528  	assert.Equal(t, "ultimate", keys[0].Trust)
   529  
   530  	{
   531  		secret, err := IsSecretKey(keys[0].KeyID)
   532  		require.NoError(t, err)
   533  		assert.True(t, secret)
   534  	}
   535  
   536  	{
   537  		secret, err := IsSecretKey("invalid")
   538  		require.NoError(t, err)
   539  		assert.False(t, secret)
   540  	}
   541  }
   542  
   543  func Test_SyncKeyRingFromDirectory(t *testing.T) {
   544  	initTempDir(t)
   545  
   546  	// First run should initialize fine
   547  	err := InitializeGnuPG()
   548  	require.NoError(t, err)
   549  
   550  	tempDir := t.TempDir()
   551  
   552  	{
   553  		newKeys, removed, err := SyncKeyRingFromDirectory(tempDir)
   554  		require.NoError(t, err)
   555  		assert.Empty(t, newKeys)
   556  		assert.Empty(t, removed)
   557  	}
   558  
   559  	{
   560  		for k, v := range syncTestSources {
   561  			src, err := os.Open(v)
   562  			if err != nil {
   563  				panic(err.Error())
   564  			}
   565  			defer src.Close()
   566  			dst, err := os.Create(path.Join(tempDir, k))
   567  			if err != nil {
   568  				panic(err.Error())
   569  			}
   570  			defer dst.Close()
   571  			_, err = io.Copy(dst, src)
   572  			if err != nil {
   573  				panic(err.Error())
   574  			}
   575  			dst.Close()
   576  		}
   577  
   578  		newKeys, removed, err := SyncKeyRingFromDirectory(tempDir)
   579  		require.NoError(t, err)
   580  		assert.Len(t, newKeys, 3)
   581  		assert.Empty(t, removed)
   582  
   583  		installed, err := GetInstalledPGPKeys(newKeys)
   584  		require.NoError(t, err)
   585  		for _, k := range installed {
   586  			assert.Contains(t, newKeys, k.KeyID)
   587  		}
   588  	}
   589  
   590  	{
   591  		err := os.Remove(path.Join(tempDir, "4AEE18F83AFDEB23"))
   592  		if err != nil {
   593  			panic(err.Error())
   594  		}
   595  		newKeys, removed, err := SyncKeyRingFromDirectory(tempDir)
   596  		require.NoError(t, err)
   597  		assert.Empty(t, newKeys)
   598  		assert.Len(t, removed, 1)
   599  
   600  		installed, err := GetInstalledPGPKeys(newKeys)
   601  		require.NoError(t, err)
   602  		for _, k := range installed {
   603  			assert.NotEqual(t, k.KeyID, removed[0])
   604  		}
   605  	}
   606  }