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