github.com/goreleaser/goreleaser@v1.25.1/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/goreleaser/goreleaser/internal/artifact"
    18  	"github.com/goreleaser/goreleaser/internal/git"
    19  	"github.com/goreleaser/goreleaser/internal/skips"
    20  	"github.com/goreleaser/goreleaser/internal/testctx"
    21  	"github.com/goreleaser/goreleaser/internal/testlib"
    22  	"github.com/goreleaser/goreleaser/internal/tmpl"
    23  	"github.com/goreleaser/goreleaser/pkg/config"
    24  	"github.com/goreleaser/goreleaser/pkg/context"
    25  	"github.com/stretchr/testify/require"
    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  	code := m.Run()
    50  	_ = os.RemoveAll(keyring)
    51  	os.Exit(code)
    52  }
    53  
    54  func TestDescription(t *testing.T) {
    55  	require.NotEmpty(t, Pipe{}.String())
    56  }
    57  
    58  func TestSignDefault(t *testing.T) {
    59  	_ = testlib.Mktmp(t)
    60  	testlib.GitInit(t)
    61  
    62  	ctx := testctx.NewWithCfg(config.Project{
    63  		Signs: []config.Sign{{}},
    64  	})
    65  	setGpg(t, ctx, "") // force empty gpg.program
    66  
    67  	require.NoError(t, Pipe{}.Default(ctx))
    68  	require.Equal(t, "gpg", ctx.Config.Signs[0].Cmd)
    69  	require.Equal(t, "${artifact}.sig", ctx.Config.Signs[0].Signature)
    70  	require.Equal(t, []string{"--output", "$signature", "--detach-sig", "$artifact"}, ctx.Config.Signs[0].Args)
    71  	require.Equal(t, "none", ctx.Config.Signs[0].Artifacts)
    72  }
    73  
    74  func TestDefaultGpgFromGitConfig(t *testing.T) {
    75  	_ = testlib.Mktmp(t)
    76  	testlib.GitInit(t)
    77  
    78  	ctx := testctx.NewWithCfg(config.Project{
    79  		Signs: []config.Sign{{}},
    80  	})
    81  	setGpg(t, ctx, "not-really-gpg")
    82  
    83  	require.NoError(t, Pipe{}.Default(ctx))
    84  	require.Equal(t, "not-really-gpg", ctx.Config.Signs[0].Cmd)
    85  }
    86  
    87  func TestSignDisabled(t *testing.T) {
    88  	ctx := testctx.NewWithCfg(config.Project{Signs: []config.Sign{{Artifacts: "none"}}})
    89  	err := Pipe{}.Run(ctx)
    90  	require.EqualError(t, err, "artifact signing is disabled")
    91  }
    92  
    93  func TestSignInvalidArtifacts(t *testing.T) {
    94  	ctx := testctx.NewWithCfg(config.Project{Signs: []config.Sign{{Artifacts: "foo"}}})
    95  	err := Pipe{}.Run(ctx)
    96  	require.EqualError(t, err, "invalid list of artifacts to sign: foo")
    97  }
    98  
    99  func TestSignArtifacts(t *testing.T) {
   100  	stdin := passwordUser
   101  	tmplStdin := passwordUserTmpl
   102  	tests := []struct {
   103  		desc             string
   104  		ctx              *context.Context
   105  		signaturePaths   []string
   106  		signatureNames   []string
   107  		certificateNames []string
   108  		expectedErrMsg   string
   109  		expectedErrIs    error
   110  		expectedErrAs    any
   111  		user             string
   112  	}{
   113  		{
   114  			desc:          "sign cmd not found",
   115  			expectedErrIs: exec.ErrNotFound,
   116  			ctx: testctx.NewWithCfg(config.Project{
   117  				Signs: []config.Sign{
   118  					{
   119  						Artifacts: "all",
   120  						Cmd:       "not-a-valid-cmd",
   121  					},
   122  				},
   123  			}),
   124  		},
   125  		{
   126  			desc:           "sign errors",
   127  			expectedErrMsg: "sign: exit failed",
   128  			ctx: testctx.NewWithCfg(config.Project{
   129  				Signs: []config.Sign{
   130  					{
   131  						Artifacts: "all",
   132  						Cmd:       "exit",
   133  						Args:      []string{"1"},
   134  					},
   135  				},
   136  			}),
   137  		},
   138  		{
   139  			desc:          "invalid certificate template",
   140  			expectedErrAs: &tmpl.Error{},
   141  			ctx: testctx.NewWithCfg(config.Project{
   142  				Signs: []config.Sign{
   143  					{
   144  						Artifacts:   "all",
   145  						Cmd:         "exit",
   146  						Certificate: "{{ .blah }}",
   147  					},
   148  				},
   149  			}),
   150  		},
   151  		{
   152  			desc:          "invalid signature template",
   153  			expectedErrAs: &tmpl.Error{},
   154  			ctx: testctx.NewWithCfg(config.Project{
   155  				Signs: []config.Sign{
   156  					{
   157  						Artifacts: "all",
   158  						Cmd:       "exit",
   159  						Signature: "{{ .blah }}",
   160  					},
   161  				},
   162  			}),
   163  		},
   164  		{
   165  			desc:          "invalid args template",
   166  			expectedErrAs: &tmpl.Error{},
   167  			ctx: testctx.NewWithCfg(config.Project{
   168  				Signs: []config.Sign{
   169  					{
   170  						Artifacts: "all",
   171  						Cmd:       "exit",
   172  						Args:      []string{"${FOO}-{{ .foo }{{}}{"},
   173  					},
   174  				},
   175  				Env: []string{
   176  					"FOO=BAR",
   177  				},
   178  			}),
   179  		},
   180  		{
   181  			desc:          "invalid env template",
   182  			expectedErrAs: &tmpl.Error{},
   183  			ctx: testctx.NewWithCfg(config.Project{
   184  				Signs: []config.Sign{
   185  					{
   186  						Artifacts: "all",
   187  						Cmd:       "exit",
   188  						Env:       []string{"A={{ .blah }}"},
   189  					},
   190  				},
   191  			}),
   192  		},
   193  		{
   194  			desc: "sign all artifacts",
   195  			ctx: testctx.NewWithCfg(config.Project{
   196  				Signs: []config.Sign{
   197  					{
   198  						Artifacts: "all",
   199  					},
   200  				},
   201  			}),
   202  			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"},
   203  			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"},
   204  		},
   205  		{
   206  			desc: "sign archives",
   207  			ctx: testctx.NewWithCfg(config.Project{
   208  				Signs: []config.Sign{
   209  					{
   210  						Artifacts: "archive",
   211  					},
   212  				},
   213  			}),
   214  			signaturePaths: []string{"artifact1.sig", "artifact2.sig"},
   215  			signatureNames: []string{"artifact1.sig", "artifact2.sig"},
   216  		},
   217  		{
   218  			desc: "sign packages",
   219  			ctx: testctx.NewWithCfg(config.Project{
   220  				Signs: []config.Sign{
   221  					{
   222  						Artifacts: "package",
   223  					},
   224  				},
   225  			}),
   226  			signaturePaths: []string{"package1.deb.sig"},
   227  			signatureNames: []string{"package1.deb.sig"},
   228  		},
   229  		{
   230  			desc: "sign binaries",
   231  			ctx: testctx.NewWithCfg(config.Project{
   232  				Signs: []config.Sign{
   233  					{
   234  						Artifacts: "binary",
   235  					},
   236  				},
   237  			}),
   238  			signaturePaths: []string{"artifact3.sig", "linux_amd64/artifact4.sig"},
   239  			signatureNames: []string{"artifact3_1.0.0_linux_amd64.sig", "artifact4_1.0.0_linux_amd64.sig"},
   240  		},
   241  		{
   242  			desc: "multiple sign configs",
   243  			ctx: testctx.NewWithCfg(config.Project{
   244  				Env: []string{
   245  					"GPG_KEY_ID=" + fakeGPGKeyID,
   246  				},
   247  				Signs: []config.Sign{
   248  					{
   249  						ID:        "s1",
   250  						Artifacts: "checksum",
   251  					},
   252  					{
   253  						ID:        "s2",
   254  						Artifacts: "archive",
   255  						Signature: "${artifact}.{{ .Env.GPG_KEY_ID }}.sig",
   256  					},
   257  				},
   258  			}),
   259  			signaturePaths: []string{
   260  				"artifact1." + fakeGPGKeyID + ".sig",
   261  				"artifact2." + fakeGPGKeyID + ".sig",
   262  				"checksum.sig",
   263  				"checksum2.sig",
   264  			},
   265  			signatureNames: []string{
   266  				"artifact1." + fakeGPGKeyID + ".sig",
   267  				"artifact2." + fakeGPGKeyID + ".sig",
   268  				"checksum.sig",
   269  				"checksum2.sig",
   270  			},
   271  		},
   272  		{
   273  			desc: "sign filtered artifacts",
   274  			ctx: testctx.NewWithCfg(config.Project{
   275  				Signs: []config.Sign{
   276  					{
   277  						Artifacts: "all",
   278  						IDs:       []string{"foo"},
   279  					},
   280  				},
   281  			}),
   282  			signaturePaths: []string{"artifact1.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "artifact5.tar.gz.sig", "package1.deb.sig"},
   283  			signatureNames: []string{"artifact1.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact5.tar.gz.sig", "package1.deb.sig"},
   284  		},
   285  		{
   286  			desc: "sign only checksums",
   287  			ctx: testctx.NewWithCfg(config.Project{
   288  				Signs: []config.Sign{
   289  					{
   290  						Artifacts: "checksum",
   291  					},
   292  				},
   293  			}),
   294  			signaturePaths: []string{"checksum.sig", "checksum2.sig"},
   295  			signatureNames: []string{"checksum.sig", "checksum2.sig"},
   296  		},
   297  		{
   298  			desc: "sign only filtered checksums",
   299  			ctx: testctx.NewWithCfg(config.Project{
   300  				Signs: []config.Sign{
   301  					{
   302  						Artifacts: "checksum",
   303  						IDs:       []string{"foo"},
   304  					},
   305  				},
   306  			}),
   307  			signaturePaths: []string{"checksum.sig", "checksum2.sig"},
   308  			signatureNames: []string{"checksum.sig", "checksum2.sig"},
   309  		},
   310  		{
   311  			desc: "sign only source",
   312  			ctx: testctx.NewWithCfg(config.Project{
   313  				Signs: []config.Sign{
   314  					{
   315  						Artifacts: "source",
   316  					},
   317  				},
   318  			}),
   319  			signaturePaths: []string{"artifact5.tar.gz.sig"},
   320  			signatureNames: []string{"artifact5.tar.gz.sig"},
   321  		},
   322  		{
   323  			desc: "sign only source filter by id",
   324  			ctx: testctx.NewWithCfg(config.Project{
   325  				Signs: []config.Sign{
   326  					{
   327  						Artifacts: "source",
   328  						IDs:       []string{"should-not-be-used"},
   329  					},
   330  				},
   331  			}),
   332  			signaturePaths: []string{"artifact5.tar.gz.sig"},
   333  			signatureNames: []string{"artifact5.tar.gz.sig"},
   334  		},
   335  		{
   336  			desc: "sign only sbom",
   337  			ctx: testctx.NewWithCfg(config.Project{
   338  				Signs: []config.Sign{
   339  					{
   340  						Artifacts: "sbom",
   341  					},
   342  				},
   343  			}),
   344  			signaturePaths: []string{"artifact5.tar.gz.sbom.sig"},
   345  			signatureNames: []string{"artifact5.tar.gz.sbom.sig"},
   346  		},
   347  		{
   348  			desc: "sign all artifacts with env",
   349  			ctx: testctx.NewWithCfg(config.Project{
   350  				Signs: []config.Sign{
   351  					{
   352  						Artifacts: "all",
   353  						Args: []string{
   354  							"-u",
   355  							"${TEST_USER}",
   356  							"--output",
   357  							"${signature}",
   358  							"--detach-sign",
   359  							"${artifact}",
   360  						},
   361  					},
   362  				},
   363  				Env: []string{
   364  					fmt.Sprintf("TEST_USER=%s", user),
   365  				},
   366  			}),
   367  			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"},
   368  			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"},
   369  		},
   370  		{
   371  			desc: "sign all artifacts with template",
   372  			ctx: testctx.NewWithCfg(config.Project{
   373  				Signs: []config.Sign{
   374  					{
   375  						Artifacts: "all",
   376  						Args: []string{
   377  							"-u",
   378  							"{{ .Env.SOME_TEST_USER }}",
   379  							"--output",
   380  							"${signature}",
   381  							"--detach-sign",
   382  							"${artifact}",
   383  						},
   384  					},
   385  				},
   386  				Env: []string{
   387  					fmt.Sprintf("SOME_TEST_USER=%s", user),
   388  				},
   389  			}),
   390  			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"},
   391  			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"},
   392  		},
   393  		{
   394  			desc: "sign single with password from stdin",
   395  			ctx: testctx.NewWithCfg(config.Project{
   396  				Signs: []config.Sign{
   397  					{
   398  						Artifacts: "all",
   399  						Args: []string{
   400  							"-u",
   401  							passwordUser,
   402  							"--batch",
   403  							"--pinentry-mode",
   404  							"loopback",
   405  							"--passphrase-fd",
   406  							"0",
   407  							"--output",
   408  							"${signature}",
   409  							"--detach-sign",
   410  							"${artifact}",
   411  						},
   412  						Stdin: &stdin,
   413  					},
   414  				},
   415  			}),
   416  			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"},
   417  			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"},
   418  			user:           passwordUser,
   419  		},
   420  		{
   421  			desc: "sign single with password from templated stdin",
   422  			ctx: testctx.NewWithCfg(config.Project{
   423  				Env: []string{"GPG_PASSWORD=" + stdin},
   424  				Signs: []config.Sign{
   425  					{
   426  						Artifacts: "all",
   427  						Args: []string{
   428  							"-u",
   429  							passwordUser,
   430  							"--batch",
   431  							"--pinentry-mode",
   432  							"loopback",
   433  							"--passphrase-fd",
   434  							"0",
   435  							"--output",
   436  							"${signature}",
   437  							"--detach-sign",
   438  							"${artifact}",
   439  						},
   440  						Stdin: &tmplStdin,
   441  					},
   442  				},
   443  			}),
   444  			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"},
   445  			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"},
   446  			user:           passwordUser,
   447  		},
   448  		{
   449  			desc: "sign single with password from stdin_file",
   450  			ctx: testctx.NewWithCfg(config.Project{
   451  				Signs: []config.Sign{
   452  					{
   453  						Artifacts: "all",
   454  						Args: []string{
   455  							"-u",
   456  							passwordUser,
   457  							"--batch",
   458  							"--pinentry-mode",
   459  							"loopback",
   460  							"--passphrase-fd",
   461  							"0",
   462  							"--output",
   463  							"${signature}",
   464  							"--detach-sign",
   465  							"${artifact}",
   466  						},
   467  						StdinFile: filepath.Join(keyring, passwordUser),
   468  					},
   469  				},
   470  			}),
   471  			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"},
   472  			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"},
   473  			user:           passwordUser,
   474  		},
   475  		{
   476  			desc: "missing stdin_file",
   477  			ctx: testctx.NewWithCfg(config.Project{
   478  				Signs: []config.Sign{
   479  					{
   480  						Artifacts: "all",
   481  						Args: []string{
   482  							"--batch",
   483  							"--pinentry-mode",
   484  							"loopback",
   485  							"--passphrase-fd",
   486  							"0",
   487  						},
   488  						StdinFile: "/tmp/non-existing-file",
   489  					},
   490  				},
   491  			}),
   492  			expectedErrIs: os.ErrNotExist,
   493  		},
   494  		{
   495  			desc: "sign creating certificate",
   496  			ctx: testctx.NewWithCfg(config.Project{
   497  				Signs: []config.Sign{
   498  					{
   499  						Certificate: "${artifact}.pem",
   500  						Artifacts:   "checksum",
   501  					},
   502  				},
   503  			}),
   504  			signaturePaths:   []string{"checksum.sig", "checksum2.sig"},
   505  			signatureNames:   []string{"checksum.sig", "checksum2.sig"},
   506  			certificateNames: []string{"checksum.pem", "checksum2.pem"},
   507  		},
   508  		{
   509  			desc: "sign all artifacts with env and certificate",
   510  			ctx: testctx.NewWithCfg(config.Project{
   511  				Signs: []config.Sign{
   512  					{
   513  						Env:         []string{"NOT_HONK=honk", "HONK={{ .Env.NOT_HONK }}"},
   514  						Certificate: `{{ trimsuffix (trimsuffix .Env.artifact ".tar.gz") ".deb" }}_${HONK}.pem`,
   515  						Artifacts:   "all",
   516  					},
   517  				},
   518  			}),
   519  			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"},
   520  			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"},
   521  			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"},
   522  		},
   523  	}
   524  
   525  	for _, test := range tests {
   526  		if test.user == "" {
   527  			test.user = user
   528  		}
   529  
   530  		t.Run(test.desc, func(t *testing.T) {
   531  			testSign(
   532  				t,
   533  				test.ctx,
   534  				test.certificateNames,
   535  				test.signaturePaths,
   536  				test.signatureNames,
   537  				test.user,
   538  				test.expectedErrMsg,
   539  				test.expectedErrIs,
   540  				test.expectedErrAs,
   541  			)
   542  		})
   543  	}
   544  }
   545  
   546  func testSign(
   547  	tb testing.TB,
   548  	ctx *context.Context,
   549  	certificateNames, signaturePaths, signatureNames []string,
   550  	user, expectedErrMsg string,
   551  	expectedErrIs error,
   552  	expectedErrAs any,
   553  ) {
   554  	tb.Helper()
   555  	tmpdir := tb.TempDir()
   556  
   557  	ctx.Config.Dist = tmpdir
   558  
   559  	// create some fake artifacts
   560  	artifacts := []string{"artifact1", "artifact2", "artifact3", "checksum", "checksum2", "package1.deb"}
   561  	require.NoError(tb, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm))
   562  	for _, f := range artifacts {
   563  		file := filepath.Join(tmpdir, f)
   564  		require.NoError(tb, os.WriteFile(file, []byte("foo"), 0o644))
   565  	}
   566  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0o644))
   567  	artifacts = append(artifacts, "linux_amd64/artifact4")
   568  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0o644))
   569  	artifacts = append(artifacts, "artifact5.tar.gz")
   570  	require.NoError(tb, os.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz.sbom"), []byte("sbom(foo)"), 0o644))
   571  	artifacts = append(artifacts, "artifact5.tar.gz.sbom")
   572  	ctx.Artifacts.Add(&artifact.Artifact{
   573  		Name: "artifact1",
   574  		Path: filepath.Join(tmpdir, "artifact1"),
   575  		Type: artifact.UploadableArchive,
   576  		Extra: map[string]interface{}{
   577  			artifact.ExtraID: "foo",
   578  		},
   579  	})
   580  	ctx.Artifacts.Add(&artifact.Artifact{
   581  		Name: "artifact2",
   582  		Path: filepath.Join(tmpdir, "artifact2"),
   583  		Type: artifact.UploadableArchive,
   584  		Extra: map[string]interface{}{
   585  			artifact.ExtraID: "foo3",
   586  		},
   587  	})
   588  	ctx.Artifacts.Add(&artifact.Artifact{
   589  		Name: "artifact3_1.0.0_linux_amd64",
   590  		Path: filepath.Join(tmpdir, "artifact3"),
   591  		Type: artifact.UploadableBinary,
   592  		Extra: map[string]interface{}{
   593  			artifact.ExtraID: "foo",
   594  		},
   595  	})
   596  	ctx.Artifacts.Add(&artifact.Artifact{
   597  		Name: "checksum",
   598  		Path: filepath.Join(tmpdir, "checksum"),
   599  		Type: artifact.Checksum,
   600  	})
   601  	ctx.Artifacts.Add(&artifact.Artifact{
   602  		Name: "checksum2",
   603  		Path: filepath.Join(tmpdir, "checksum2"),
   604  		Type: artifact.Checksum,
   605  		Extra: map[string]interface{}{
   606  			"Refresh": func() error {
   607  				file := filepath.Join(tmpdir, "checksum2")
   608  				return os.WriteFile(file, []byte("foo"), 0o644)
   609  			},
   610  		},
   611  	})
   612  	ctx.Artifacts.Add(&artifact.Artifact{
   613  		Name: "artifact4_1.0.0_linux_amd64",
   614  		Path: filepath.Join(tmpdir, "linux_amd64", "artifact4"),
   615  		Type: artifact.UploadableBinary,
   616  		Extra: map[string]interface{}{
   617  			artifact.ExtraID: "foo3",
   618  		},
   619  	})
   620  	ctx.Artifacts.Add(&artifact.Artifact{
   621  		Name: "artifact5.tar.gz",
   622  		Path: filepath.Join(tmpdir, "artifact5.tar.gz"),
   623  		Type: artifact.UploadableSourceArchive,
   624  	})
   625  	ctx.Artifacts.Add(&artifact.Artifact{
   626  		Name: "artifact5.tar.gz.sbom",
   627  		Path: filepath.Join(tmpdir, "artifact5.tar.gz.sbom"),
   628  		Type: artifact.SBOM,
   629  	})
   630  	ctx.Artifacts.Add(&artifact.Artifact{
   631  		Name: "package1.deb",
   632  		Path: filepath.Join(tmpdir, "package1.deb"),
   633  		Type: artifact.LinuxPackage,
   634  		Extra: map[string]interface{}{
   635  			artifact.ExtraID: "foo",
   636  		},
   637  	})
   638  
   639  	// configure the pipeline
   640  	// make sure we are using the test keyring
   641  	require.NoError(tb, Pipe{}.Default(ctx))
   642  	for i := range ctx.Config.Signs {
   643  		ctx.Config.Signs[i].Args = append(
   644  			[]string{"--homedir", keyring},
   645  			ctx.Config.Signs[i].Args...,
   646  		)
   647  	}
   648  
   649  	err := Pipe{}.Run(ctx)
   650  
   651  	// run the pipeline
   652  	if expectedErrMsg != "" {
   653  		require.ErrorContains(tb, err, 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  }