github.com/windmeup/goreleaser@v1.21.95/internal/pipe/sign/sign_test.go (about)

     1  package sign
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"math/rand"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/stretchr/testify/assert"
    16  
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/windmeup/goreleaser/internal/artifact"
    19  	"github.com/windmeup/goreleaser/internal/git"
    20  	"github.com/windmeup/goreleaser/internal/skips"
    21  	"github.com/windmeup/goreleaser/internal/testctx"
    22  	"github.com/windmeup/goreleaser/internal/testlib"
    23  	"github.com/windmeup/goreleaser/internal/tmpl"
    24  	"github.com/windmeup/goreleaser/pkg/config"
    25  	"github.com/windmeup/goreleaser/pkg/context"
    26  )
    27  
    28  var (
    29  	originKeyring = "testdata/gnupg"
    30  	keyring       string
    31  )
    32  
    33  const (
    34  	user             = "nopass"
    35  	passwordUser     = "password"
    36  	passwordUserTmpl = "{{ .Env.GPG_PASSWORD }}"
    37  	fakeGPGKeyID     = "23E7505E"
    38  )
    39  
    40  func TestMain(m *testing.M) {
    41  	rand := rand.New(rand.NewSource(time.Now().UnixNano()))
    42  	keyring = fmt.Sprintf("/tmp/gorel_gpg_test.%d", rand.Int())
    43  	fmt.Println("copying", originKeyring, "to", keyring)
    44  	if err := exec.Command("cp", "-Rf", originKeyring, keyring).Run(); err != nil {
    45  		fmt.Printf("failed to copy %s to %s: %s", originKeyring, keyring, err)
    46  		os.Exit(1)
    47  	}
    48  
    49  	defer os.RemoveAll(keyring)
    50  	os.Exit(m.Run())
    51  }
    52  
    53  func TestDescription(t *testing.T) {
    54  	require.NotEmpty(t, Pipe{}.String())
    55  }
    56  
    57  func TestSignDefault(t *testing.T) {
    58  	_ = testlib.Mktmp(t)
    59  	testlib.GitInit(t)
    60  
    61  	ctx := testctx.NewWithCfg(config.Project{
    62  		Signs: []config.Sign{{}},
    63  	})
    64  	setGpg(t, ctx, "") // force empty gpg.program
    65  
    66  	require.NoError(t, Pipe{}.Default(ctx))
    67  	require.Equal(t, ctx.Config.Signs[0].Cmd, "gpg")
    68  	require.Equal(t, ctx.Config.Signs[0].Signature, "${artifact}.sig")
    69  	require.Equal(t, ctx.Config.Signs[0].Args, []string{"--output", "$signature", "--detach-sig", "$artifact"})
    70  	require.Equal(t, ctx.Config.Signs[0].Artifacts, "none")
    71  }
    72  
    73  func TestDefaultGpgFromGitConfig(t *testing.T) {
    74  	_ = testlib.Mktmp(t)
    75  	testlib.GitInit(t)
    76  
    77  	ctx := testctx.NewWithCfg(config.Project{
    78  		Signs: []config.Sign{{}},
    79  	})
    80  	setGpg(t, ctx, "not-really-gpg")
    81  
    82  	require.NoError(t, Pipe{}.Default(ctx))
    83  	require.Equal(t, ctx.Config.Signs[0].Cmd, "not-really-gpg")
    84  }
    85  
    86  func TestSignDisabled(t *testing.T) {
    87  	ctx := testctx.NewWithCfg(config.Project{Signs: []config.Sign{{Artifacts: "none"}}})
    88  	err := Pipe{}.Run(ctx)
    89  	require.EqualError(t, err, "artifact signing is disabled")
    90  }
    91  
    92  func TestSignInvalidArtifacts(t *testing.T) {
    93  	ctx := testctx.NewWithCfg(config.Project{Signs: []config.Sign{{Artifacts: "foo"}}})
    94  	err := Pipe{}.Run(ctx)
    95  	require.EqualError(t, err, "invalid list of artifacts to sign: foo")
    96  }
    97  
    98  func TestSignArtifacts(t *testing.T) {
    99  	stdin := passwordUser
   100  	tmplStdin := passwordUserTmpl
   101  	tests := []struct {
   102  		desc             string
   103  		ctx              *context.Context
   104  		signaturePaths   []string
   105  		signatureNames   []string
   106  		certificateNames []string
   107  		expectedErrMsg   string
   108  		expectedErrIs    error
   109  		expectedErrAs    any
   110  		user             string
   111  	}{
   112  		{
   113  			desc:          "sign cmd not found",
   114  			expectedErrIs: exec.ErrNotFound,
   115  			ctx: testctx.NewWithCfg(config.Project{
   116  				Signs: []config.Sign{
   117  					{
   118  						Artifacts: "all",
   119  						Cmd:       "not-a-valid-cmd",
   120  					},
   121  				},
   122  			}),
   123  		},
   124  		{
   125  			desc:           "sign errors",
   126  			expectedErrMsg: "sign: exit failed",
   127  			ctx: testctx.NewWithCfg(config.Project{
   128  				Signs: []config.Sign{
   129  					{
   130  						Artifacts: "all",
   131  						Cmd:       "exit",
   132  						Args:      []string{"1"},
   133  					},
   134  				},
   135  			}),
   136  		},
   137  		{
   138  			desc:          "invalid certificate template",
   139  			expectedErrAs: &tmpl.Error{},
   140  			ctx: testctx.NewWithCfg(config.Project{
   141  				Signs: []config.Sign{
   142  					{
   143  						Artifacts:   "all",
   144  						Cmd:         "exit",
   145  						Certificate: "{{ .blah }}",
   146  					},
   147  				},
   148  			}),
   149  		},
   150  		{
   151  			desc:          "invalid signature template",
   152  			expectedErrAs: &tmpl.Error{},
   153  			ctx: testctx.NewWithCfg(config.Project{
   154  				Signs: []config.Sign{
   155  					{
   156  						Artifacts: "all",
   157  						Cmd:       "exit",
   158  						Signature: "{{ .blah }}",
   159  					},
   160  				},
   161  			}),
   162  		},
   163  		{
   164  			desc:          "invalid args template",
   165  			expectedErrAs: &tmpl.Error{},
   166  			ctx: testctx.NewWithCfg(config.Project{
   167  				Signs: []config.Sign{
   168  					{
   169  						Artifacts: "all",
   170  						Cmd:       "exit",
   171  						Args:      []string{"${FOO}-{{ .foo }{{}}{"},
   172  					},
   173  				},
   174  				Env: []string{
   175  					"FOO=BAR",
   176  				},
   177  			}),
   178  		},
   179  		{
   180  			desc:          "invalid env template",
   181  			expectedErrAs: &tmpl.Error{},
   182  			ctx: testctx.NewWithCfg(config.Project{
   183  				Signs: []config.Sign{
   184  					{
   185  						Artifacts: "all",
   186  						Cmd:       "exit",
   187  						Env:       []string{"A={{ .blah }}"},
   188  					},
   189  				},
   190  			}),
   191  		},
   192  		{
   193  			desc: "sign all artifacts",
   194  			ctx: testctx.NewWithCfg(config.Project{
   195  				Signs: []config.Sign{
   196  					{
   197  						Artifacts: "all",
   198  					},
   199  				},
   200  			}),
   201  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   202  			signatureNames: []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   203  		},
   204  		{
   205  			desc: "sign archives",
   206  			ctx: testctx.NewWithCfg(config.Project{
   207  				Signs: []config.Sign{
   208  					{
   209  						Artifacts: "archive",
   210  					},
   211  				},
   212  			}),
   213  			signaturePaths: []string{"artifact1.sig", "artifact2.sig"},
   214  			signatureNames: []string{"artifact1.sig", "artifact2.sig"},
   215  		},
   216  		{
   217  			desc: "sign packages",
   218  			ctx: testctx.NewWithCfg(config.Project{
   219  				Signs: []config.Sign{
   220  					{
   221  						Artifacts: "package",
   222  					},
   223  				},
   224  			}),
   225  			signaturePaths: []string{"package1.deb.sig"},
   226  			signatureNames: []string{"package1.deb.sig"},
   227  		},
   228  		{
   229  			desc: "sign binaries",
   230  			ctx: testctx.NewWithCfg(config.Project{
   231  				Signs: []config.Sign{
   232  					{
   233  						Artifacts: "binary",
   234  					},
   235  				},
   236  			}),
   237  			signaturePaths: []string{"artifact3.sig", "linux_amd64/artifact4.sig"},
   238  			signatureNames: []string{"artifact3_1.0.0_linux_amd64.sig", "artifact4_1.0.0_linux_amd64.sig"},
   239  		},
   240  		{
   241  			desc: "multiple sign configs",
   242  			ctx: testctx.NewWithCfg(config.Project{
   243  				Env: []string{
   244  					"GPG_KEY_ID=" + fakeGPGKeyID,
   245  				},
   246  				Signs: []config.Sign{
   247  					{
   248  						ID:        "s1",
   249  						Artifacts: "checksum",
   250  					},
   251  					{
   252  						ID:        "s2",
   253  						Artifacts: "archive",
   254  						Signature: "${artifact}.{{ .Env.GPG_KEY_ID }}.sig",
   255  					},
   256  				},
   257  			}),
   258  			signaturePaths: []string{
   259  				"artifact1." + fakeGPGKeyID + ".sig",
   260  				"artifact2." + fakeGPGKeyID + ".sig",
   261  				"checksum.sig",
   262  				"checksum2.sig",
   263  			},
   264  			signatureNames: []string{
   265  				"artifact1." + fakeGPGKeyID + ".sig",
   266  				"artifact2." + fakeGPGKeyID + ".sig",
   267  				"checksum.sig",
   268  				"checksum2.sig",
   269  			},
   270  		},
   271  		{
   272  			desc: "sign filtered artifacts",
   273  			ctx: testctx.NewWithCfg(config.Project{
   274  				Signs: []config.Sign{
   275  					{
   276  						Artifacts: "all",
   277  						IDs:       []string{"foo"},
   278  					},
   279  				},
   280  			}),
   281  			signaturePaths: []string{"artifact1.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "artifact5.tar.gz.sig", "package1.deb.sig"},
   282  			signatureNames: []string{"artifact1.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact5.tar.gz.sig", "package1.deb.sig"},
   283  		},
   284  		{
   285  			desc: "sign only checksums",
   286  			ctx: testctx.NewWithCfg(config.Project{
   287  				Signs: []config.Sign{
   288  					{
   289  						Artifacts: "checksum",
   290  					},
   291  				},
   292  			}),
   293  			signaturePaths: []string{"checksum.sig", "checksum2.sig"},
   294  			signatureNames: []string{"checksum.sig", "checksum2.sig"},
   295  		},
   296  		{
   297  			desc: "sign only filtered checksums",
   298  			ctx: testctx.NewWithCfg(config.Project{
   299  				Signs: []config.Sign{
   300  					{
   301  						Artifacts: "checksum",
   302  						IDs:       []string{"foo"},
   303  					},
   304  				},
   305  			}),
   306  			signaturePaths: []string{"checksum.sig", "checksum2.sig"},
   307  			signatureNames: []string{"checksum.sig", "checksum2.sig"},
   308  		},
   309  		{
   310  			desc: "sign only source",
   311  			ctx: testctx.NewWithCfg(config.Project{
   312  				Signs: []config.Sign{
   313  					{
   314  						Artifacts: "source",
   315  					},
   316  				},
   317  			}),
   318  			signaturePaths: []string{"artifact5.tar.gz.sig"},
   319  			signatureNames: []string{"artifact5.tar.gz.sig"},
   320  		},
   321  		{
   322  			desc: "sign only source filter by id",
   323  			ctx: testctx.NewWithCfg(config.Project{
   324  				Signs: []config.Sign{
   325  					{
   326  						Artifacts: "source",
   327  						IDs:       []string{"should-not-be-used"},
   328  					},
   329  				},
   330  			}),
   331  			signaturePaths: []string{"artifact5.tar.gz.sig"},
   332  			signatureNames: []string{"artifact5.tar.gz.sig"},
   333  		},
   334  		{
   335  			desc: "sign only sbom",
   336  			ctx: testctx.NewWithCfg(config.Project{
   337  				Signs: []config.Sign{
   338  					{
   339  						Artifacts: "sbom",
   340  					},
   341  				},
   342  			}),
   343  			signaturePaths: []string{"artifact5.tar.gz.sbom.sig"},
   344  			signatureNames: []string{"artifact5.tar.gz.sbom.sig"},
   345  		},
   346  		{
   347  			desc: "sign all artifacts with env",
   348  			ctx: testctx.NewWithCfg(config.Project{
   349  				Signs: []config.Sign{
   350  					{
   351  						Artifacts: "all",
   352  						Args: []string{
   353  							"-u",
   354  							"${TEST_USER}",
   355  							"--output",
   356  							"${signature}",
   357  							"--detach-sign",
   358  							"${artifact}",
   359  						},
   360  					},
   361  				},
   362  				Env: []string{
   363  					fmt.Sprintf("TEST_USER=%s", user),
   364  				},
   365  			}),
   366  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   367  			signatureNames: []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   368  		},
   369  		{
   370  			desc: "sign all artifacts with template",
   371  			ctx: testctx.NewWithCfg(config.Project{
   372  				Signs: []config.Sign{
   373  					{
   374  						Artifacts: "all",
   375  						Args: []string{
   376  							"-u",
   377  							"{{ .Env.SOME_TEST_USER }}",
   378  							"--output",
   379  							"${signature}",
   380  							"--detach-sign",
   381  							"${artifact}",
   382  						},
   383  					},
   384  				},
   385  				Env: []string{
   386  					fmt.Sprintf("SOME_TEST_USER=%s", user),
   387  				},
   388  			}),
   389  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   390  			signatureNames: []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   391  		},
   392  		{
   393  			desc: "sign single with password from stdin",
   394  			ctx: testctx.NewWithCfg(config.Project{
   395  				Signs: []config.Sign{
   396  					{
   397  						Artifacts: "all",
   398  						Args: []string{
   399  							"-u",
   400  							passwordUser,
   401  							"--batch",
   402  							"--pinentry-mode",
   403  							"loopback",
   404  							"--passphrase-fd",
   405  							"0",
   406  							"--output",
   407  							"${signature}",
   408  							"--detach-sign",
   409  							"${artifact}",
   410  						},
   411  						Stdin: &stdin,
   412  					},
   413  				},
   414  			}),
   415  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   416  			signatureNames: []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   417  			user:           passwordUser,
   418  		},
   419  		{
   420  			desc: "sign single with password from templated stdin",
   421  			ctx: testctx.NewWithCfg(config.Project{
   422  				Env: []string{"GPG_PASSWORD=" + stdin},
   423  				Signs: []config.Sign{
   424  					{
   425  						Artifacts: "all",
   426  						Args: []string{
   427  							"-u",
   428  							passwordUser,
   429  							"--batch",
   430  							"--pinentry-mode",
   431  							"loopback",
   432  							"--passphrase-fd",
   433  							"0",
   434  							"--output",
   435  							"${signature}",
   436  							"--detach-sign",
   437  							"${artifact}",
   438  						},
   439  						Stdin: &tmplStdin,
   440  					},
   441  				},
   442  			}),
   443  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   444  			signatureNames: []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   445  			user:           passwordUser,
   446  		},
   447  		{
   448  			desc: "sign single with password from stdin_file",
   449  			ctx: testctx.NewWithCfg(config.Project{
   450  				Signs: []config.Sign{
   451  					{
   452  						Artifacts: "all",
   453  						Args: []string{
   454  							"-u",
   455  							passwordUser,
   456  							"--batch",
   457  							"--pinentry-mode",
   458  							"loopback",
   459  							"--passphrase-fd",
   460  							"0",
   461  							"--output",
   462  							"${signature}",
   463  							"--detach-sign",
   464  							"${artifact}",
   465  						},
   466  						StdinFile: filepath.Join(keyring, passwordUser),
   467  					},
   468  				},
   469  			}),
   470  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   471  			signatureNames: []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   472  			user:           passwordUser,
   473  		},
   474  		{
   475  			desc: "missing stdin_file",
   476  			ctx: testctx.NewWithCfg(config.Project{
   477  				Signs: []config.Sign{
   478  					{
   479  						Artifacts: "all",
   480  						Args: []string{
   481  							"--batch",
   482  							"--pinentry-mode",
   483  							"loopback",
   484  							"--passphrase-fd",
   485  							"0",
   486  						},
   487  						StdinFile: "/tmp/non-existing-file",
   488  					},
   489  				},
   490  			}),
   491  			expectedErrIs: os.ErrNotExist,
   492  		},
   493  		{
   494  			desc: "sign creating certificate",
   495  			ctx: testctx.NewWithCfg(config.Project{
   496  				Signs: []config.Sign{
   497  					{
   498  						Certificate: "${artifact}.pem",
   499  						Artifacts:   "checksum",
   500  					},
   501  				},
   502  			}),
   503  			signaturePaths:   []string{"checksum.sig", "checksum2.sig"},
   504  			signatureNames:   []string{"checksum.sig", "checksum2.sig"},
   505  			certificateNames: []string{"checksum.pem", "checksum2.pem"},
   506  		},
   507  		{
   508  			desc: "sign all artifacts with env and certificate",
   509  			ctx: testctx.NewWithCfg(config.Project{
   510  				Signs: []config.Sign{
   511  					{
   512  						Env:         []string{"NOT_HONK=honk", "HONK={{ .Env.NOT_HONK }}"},
   513  						Certificate: `{{ trimsuffix (trimsuffix .Env.artifact ".tar.gz") ".deb" }}_${HONK}.pem`,
   514  						Artifacts:   "all",
   515  					},
   516  				},
   517  			}),
   518  			signaturePaths:   []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   519  			signatureNames:   []string{"artifact1.sig", "artifact2.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact4_1.0.0_linux_amd64.sig", "artifact5.tar.gz.sig", "artifact5.tar.gz.sbom.sig", "package1.deb.sig"},
   520  			certificateNames: []string{"artifact1_honk.pem", "artifact2_honk.pem", "artifact3_1.0.0_linux_amd64_honk.pem", "checksum_honk.pem", "checksum2_honk.pem", "artifact4_1.0.0_linux_amd64_honk.pem", "artifact5_honk.pem", "artifact5.tar.gz.sbom_honk.pem", "package1_honk.pem"},
   521  		},
   522  	}
   523  
   524  	for _, test := range tests {
   525  		if test.user == "" {
   526  			test.user = user
   527  		}
   528  
   529  		t.Run(test.desc, func(t *testing.T) {
   530  			testSign(
   531  				t,
   532  				test.ctx,
   533  				test.certificateNames,
   534  				test.signaturePaths,
   535  				test.signatureNames,
   536  				test.user,
   537  				test.expectedErrMsg,
   538  				test.expectedErrIs,
   539  				test.expectedErrAs,
   540  			)
   541  		})
   542  	}
   543  }
   544  
   545  func testSign(
   546  	tb testing.TB,
   547  	ctx *context.Context,
   548  	certificateNames, signaturePaths, signatureNames []string,
   549  	user, expectedErrMsg string,
   550  	expectedErrIs error,
   551  	expectedErrAs any,
   552  ) {
   553  	tb.Helper()
   554  	tmpdir := tb.TempDir()
   555  
   556  	ctx.Config.Dist = tmpdir
   557  
   558  	// create some fake artifacts
   559  	artifacts := []string{"artifact1", "artifact2", "artifact3", "checksum", "checksum2", "package1.deb"}
   560  	require.NoError(tb, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm))
   561  	for _, f := range artifacts {
   562  		file := filepath.Join(tmpdir, f)
   563  		require.NoError(tb, os.WriteFile(file, []byte("foo"), 0o644))
   564  	}
   565  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0o644))
   566  	artifacts = append(artifacts, "linux_amd64/artifact4")
   567  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0o644))
   568  	artifacts = append(artifacts, "artifact5.tar.gz")
   569  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz.sbom"), []byte("sbom(foo)"), 0o644))
   570  	artifacts = append(artifacts, "artifact5.tar.gz.sbom")
   571  	ctx.Artifacts.Add(&artifact.Artifact{
   572  		Name: "artifact1",
   573  		Path: filepath.Join(tmpdir, "artifact1"),
   574  		Type: artifact.UploadableArchive,
   575  		Extra: map[string]interface{}{
   576  			artifact.ExtraID: "foo",
   577  		},
   578  	})
   579  	ctx.Artifacts.Add(&artifact.Artifact{
   580  		Name: "artifact2",
   581  		Path: filepath.Join(tmpdir, "artifact2"),
   582  		Type: artifact.UploadableArchive,
   583  		Extra: map[string]interface{}{
   584  			artifact.ExtraID: "foo3",
   585  		},
   586  	})
   587  	ctx.Artifacts.Add(&artifact.Artifact{
   588  		Name: "artifact3_1.0.0_linux_amd64",
   589  		Path: filepath.Join(tmpdir, "artifact3"),
   590  		Type: artifact.UploadableBinary,
   591  		Extra: map[string]interface{}{
   592  			artifact.ExtraID: "foo",
   593  		},
   594  	})
   595  	ctx.Artifacts.Add(&artifact.Artifact{
   596  		Name: "checksum",
   597  		Path: filepath.Join(tmpdir, "checksum"),
   598  		Type: artifact.Checksum,
   599  	})
   600  	ctx.Artifacts.Add(&artifact.Artifact{
   601  		Name: "checksum2",
   602  		Path: filepath.Join(tmpdir, "checksum2"),
   603  		Type: artifact.Checksum,
   604  		Extra: map[string]interface{}{
   605  			"Refresh": func() error {
   606  				file := filepath.Join(tmpdir, "checksum2")
   607  				return os.WriteFile(file, []byte("foo"), 0o644)
   608  			},
   609  		},
   610  	})
   611  	ctx.Artifacts.Add(&artifact.Artifact{
   612  		Name: "artifact4_1.0.0_linux_amd64",
   613  		Path: filepath.Join(tmpdir, "linux_amd64", "artifact4"),
   614  		Type: artifact.UploadableBinary,
   615  		Extra: map[string]interface{}{
   616  			artifact.ExtraID: "foo3",
   617  		},
   618  	})
   619  	ctx.Artifacts.Add(&artifact.Artifact{
   620  		Name: "artifact5.tar.gz",
   621  		Path: filepath.Join(tmpdir, "artifact5.tar.gz"),
   622  		Type: artifact.UploadableSourceArchive,
   623  	})
   624  	ctx.Artifacts.Add(&artifact.Artifact{
   625  		Name: "artifact5.tar.gz.sbom",
   626  		Path: filepath.Join(tmpdir, "artifact5.tar.gz.sbom"),
   627  		Type: artifact.SBOM,
   628  	})
   629  	ctx.Artifacts.Add(&artifact.Artifact{
   630  		Name: "package1.deb",
   631  		Path: filepath.Join(tmpdir, "package1.deb"),
   632  		Type: artifact.LinuxPackage,
   633  		Extra: map[string]interface{}{
   634  			artifact.ExtraID: "foo",
   635  		},
   636  	})
   637  
   638  	// configure the pipeline
   639  	// make sure we are using the test keyring
   640  	require.NoError(tb, Pipe{}.Default(ctx))
   641  	for i := range ctx.Config.Signs {
   642  		ctx.Config.Signs[i].Args = append(
   643  			[]string{"--homedir", keyring},
   644  			ctx.Config.Signs[i].Args...,
   645  		)
   646  	}
   647  
   648  	err := Pipe{}.Run(ctx)
   649  
   650  	// run the pipeline
   651  	if expectedErrMsg != "" {
   652  		require.Error(tb, err)
   653  		require.Contains(tb, err.Error(), expectedErrMsg)
   654  		return
   655  	}
   656  
   657  	if expectedErrIs != nil {
   658  		require.ErrorIs(tb, err, expectedErrIs)
   659  		return
   660  	}
   661  
   662  	if expectedErrAs != nil {
   663  		require.ErrorAs(tb, err, expectedErrAs)
   664  		return
   665  	}
   666  
   667  	require.NoError(tb, err)
   668  
   669  	// ensure all artifacts have an ID
   670  	for _, arti := range ctx.Artifacts.Filter(
   671  		artifact.Or(
   672  			artifact.ByType(artifact.Signature),
   673  			artifact.ByType(artifact.Certificate),
   674  		),
   675  	).List() {
   676  		require.NotEmptyf(tb, arti.ID(), ".Extra.ID on %s", arti.Path)
   677  	}
   678  
   679  	certificates := ctx.Artifacts.Filter(artifact.ByType(artifact.Certificate)).List()
   680  	certNames := []string{}
   681  	for _, cert := range certificates {
   682  		certNames = append(certNames, cert.Name)
   683  		require.True(tb, strings.HasPrefix(cert.Path, ctx.Config.Dist))
   684  	}
   685  
   686  	assert.ElementsMatch(tb, certificateNames, certNames)
   687  
   688  	// verify that only the artifacts and the signatures are in the dist dir
   689  	gotFiles := []string{}
   690  
   691  	require.NoError(tb, filepath.Walk(tmpdir,
   692  		func(path string, info os.FileInfo, err error) error {
   693  			if err != nil {
   694  				return err
   695  			}
   696  			if info.IsDir() {
   697  				return nil
   698  			}
   699  			relPath, err := filepath.Rel(tmpdir, path)
   700  			if err != nil {
   701  				return err
   702  			}
   703  			gotFiles = append(gotFiles, relPath)
   704  			return nil
   705  		}),
   706  	)
   707  
   708  	wantFiles := append(artifacts, signaturePaths...)
   709  	sort.Strings(wantFiles)
   710  	require.ElementsMatch(tb, wantFiles, gotFiles)
   711  
   712  	// verify the signatures
   713  	for _, sig := range signaturePaths {
   714  		verifySignature(tb, ctx, sig, user)
   715  	}
   716  
   717  	var signArtifacts []string
   718  	for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.Signature)).List() {
   719  		signArtifacts = append(signArtifacts, sig.Name)
   720  	}
   721  	// check signature is an artifact
   722  	require.ElementsMatch(tb, signArtifacts, signatureNames)
   723  }
   724  
   725  func verifySignature(tb testing.TB, ctx *context.Context, sig string, user string) {
   726  	tb.Helper()
   727  	artifact := strings.TrimSuffix(sig, filepath.Ext(sig))
   728  	artifact = strings.TrimSuffix(artifact, "."+fakeGPGKeyID)
   729  
   730  	// verify signature was made with key for usesr 'nopass'
   731  	cmd := exec.Command("gpg", "--homedir", keyring, "--verify", filepath.Join(ctx.Config.Dist, sig), filepath.Join(ctx.Config.Dist, artifact))
   732  	out, err := cmd.CombinedOutput()
   733  	require.NoError(tb, err, string(out))
   734  
   735  	// check if the signature matches the user we expect to do this properly we
   736  	// might need to have either separate keyrings or export the key from the
   737  	// keyring before we do the verification. For now we punt and look in the
   738  	// output.
   739  	if !bytes.Contains(out, []byte(user)) {
   740  		tb.Fatalf("%s: signature is not from %s: %s", sig, user, string(out))
   741  	}
   742  }
   743  
   744  func TestSeveralSignsWithTheSameID(t *testing.T) {
   745  	ctx := testctx.NewWithCfg(config.Project{
   746  		Signs: []config.Sign{
   747  			{
   748  				ID: "a",
   749  			},
   750  			{
   751  				ID: "a",
   752  			},
   753  		},
   754  	})
   755  	require.EqualError(t, Pipe{}.Default(ctx), "found 2 signs with the ID 'a', please fix your config")
   756  }
   757  
   758  func TestSkip(t *testing.T) {
   759  	t.Run("skip", func(t *testing.T) {
   760  		require.True(t, Pipe{}.Skip(testctx.New()))
   761  	})
   762  
   763  	t.Run("skip sign", func(t *testing.T) {
   764  		ctx := testctx.New(testctx.Skip(skips.Sign))
   765  		require.True(t, Pipe{}.Skip(ctx))
   766  	})
   767  
   768  	t.Run("dont skip", func(t *testing.T) {
   769  		ctx := testctx.NewWithCfg(config.Project{
   770  			Signs: []config.Sign{
   771  				{},
   772  			},
   773  		})
   774  		require.False(t, Pipe{}.Skip(ctx))
   775  	})
   776  }
   777  
   778  func TestDependencies(t *testing.T) {
   779  	ctx := testctx.NewWithCfg(config.Project{
   780  		Signs: []config.Sign{
   781  			{Cmd: "cosign"},
   782  			{Cmd: "gpg2"},
   783  		},
   784  	})
   785  	require.Equal(t, []string{"cosign", "gpg2"}, Pipe{}.Dependencies(ctx))
   786  }
   787  
   788  func setGpg(tb testing.TB, ctx *context.Context, p string) {
   789  	tb.Helper()
   790  	_, err := git.Run(ctx, "config", "--local", "--add", "gpg.program", p)
   791  	require.NoError(tb, err)
   792  }