github.com/amane3/goreleaser@v0.182.0/internal/pipe/sign/sign_test.go (about)

     1  package sign
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"math/rand"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/amane3/goreleaser/internal/artifact"
    17  	"github.com/amane3/goreleaser/pkg/config"
    18  	"github.com/amane3/goreleaser/pkg/context"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  var originKeyring = "testdata/gnupg"
    23  var keyring string
    24  
    25  const user = "nopass"
    26  const passwordUser = "password"
    27  
    28  func TestMain(m *testing.M) {
    29  	rand.Seed(time.Now().UnixNano())
    30  	keyring = fmt.Sprintf("/tmp/gorel_gpg_test.%d", rand.Int())
    31  	fmt.Println("copying", originKeyring, "to", keyring)
    32  	if err := exec.Command("cp", "-Rf", originKeyring, keyring).Run(); err != nil {
    33  		fmt.Printf("failed to copy %s to %s: %s", originKeyring, keyring, err)
    34  		os.Exit(1)
    35  	}
    36  
    37  	defer os.RemoveAll(keyring)
    38  	os.Exit(m.Run())
    39  }
    40  
    41  func TestDescription(t *testing.T) {
    42  	require.NotEmpty(t, Pipe{}.String())
    43  }
    44  
    45  func TestSignDefault(t *testing.T) {
    46  	ctx := &context.Context{
    47  		Config: config.Project{
    48  			Signs: []config.Sign{{}},
    49  		},
    50  	}
    51  	err := Pipe{}.Default(ctx)
    52  	require.NoError(t, err)
    53  	require.Equal(t, ctx.Config.Signs[0].Cmd, "gpg")
    54  	require.Equal(t, ctx.Config.Signs[0].Signature, "${artifact}.sig")
    55  	require.Equal(t, ctx.Config.Signs[0].Args, []string{"--output", "$signature", "--detach-sig", "$artifact"})
    56  	require.Equal(t, ctx.Config.Signs[0].Artifacts, "none")
    57  }
    58  
    59  func TestSignDisabled(t *testing.T) {
    60  	ctx := context.New(config.Project{})
    61  	ctx.Config.Signs = []config.Sign{
    62  		{Artifacts: "none"},
    63  	}
    64  	err := Pipe{}.Run(ctx)
    65  	require.EqualError(t, err, "artifact signing is disabled")
    66  }
    67  
    68  func TestSignSkipped(t *testing.T) {
    69  	ctx := context.New(config.Project{})
    70  	ctx.SkipSign = true
    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  	tests := []struct {
    87  		desc           string
    88  		ctx            *context.Context
    89  		signaturePaths []string
    90  		signatureNames []string
    91  		expectedErrMsg string
    92  		user           string
    93  	}{
    94  		{
    95  			desc:           "sign errors",
    96  			expectedErrMsg: "sign: exit failed",
    97  			ctx: context.New(
    98  				config.Project{
    99  					Signs: []config.Sign{
   100  						{
   101  							Artifacts: "all",
   102  							Cmd:       "exit",
   103  							Args:      []string{"1"},
   104  						},
   105  					},
   106  				},
   107  			),
   108  		},
   109  		{
   110  			desc:           "invalid args template",
   111  			expectedErrMsg: `sign failed: ${FOO}-{{ .foo }{{}}{: invalid template: template: tmpl:1: unexpected "}" in operand`,
   112  			ctx: context.New(
   113  				config.Project{
   114  					Signs: []config.Sign{
   115  						{
   116  							Artifacts: "all",
   117  							Cmd:       "exit",
   118  							Args:      []string{"${FOO}-{{ .foo }{{}}{"},
   119  						},
   120  					},
   121  					Env: []string{
   122  						"FOO=BAR",
   123  					},
   124  				},
   125  			),
   126  		},
   127  		{
   128  			desc: "sign single",
   129  			ctx: context.New(
   130  				config.Project{
   131  					Signs: []config.Sign{
   132  						{Artifacts: "all"},
   133  					},
   134  				},
   135  			),
   136  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"},
   137  			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"},
   138  		},
   139  		{
   140  			desc: "sign all artifacts",
   141  			ctx: context.New(
   142  				config.Project{
   143  					Signs: []config.Sign{
   144  						{
   145  							Artifacts: "all",
   146  						},
   147  					},
   148  				},
   149  			),
   150  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"},
   151  			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"},
   152  		},
   153  		{
   154  			desc: "multiple sign configs",
   155  			ctx: context.New(
   156  				config.Project{
   157  					Signs: []config.Sign{
   158  						{
   159  							ID:        "s1",
   160  							Artifacts: "checksum",
   161  						},
   162  						{
   163  							ID:        "s2",
   164  							Artifacts: "checksum",
   165  							Signature: "${artifact}.sog",
   166  						},
   167  					},
   168  				},
   169  			),
   170  			signaturePaths: []string{
   171  				"checksum.sig",
   172  				"checksum2.sig",
   173  				"checksum.sog",
   174  				"checksum2.sog",
   175  			},
   176  			signatureNames: []string{
   177  				"checksum.sig",
   178  				"checksum2.sig",
   179  				"checksum.sog",
   180  				"checksum2.sog",
   181  			},
   182  		},
   183  		{
   184  			desc: "sign filtered artifacts",
   185  			ctx: context.New(
   186  				config.Project{
   187  					Signs: []config.Sign{
   188  						{
   189  							Artifacts: "all",
   190  							IDs:       []string{"foo"},
   191  						},
   192  					},
   193  				},
   194  			),
   195  			signaturePaths: []string{"artifact1.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "artifact5.tar.gz.sig"},
   196  			signatureNames: []string{"artifact1.sig", "artifact3_1.0.0_linux_amd64.sig", "checksum.sig", "checksum2.sig", "artifact5.tar.gz.sig"},
   197  		},
   198  		{
   199  			desc: "sign only checksums",
   200  			ctx: context.New(
   201  				config.Project{
   202  					Signs: []config.Sign{
   203  						{
   204  							Artifacts: "checksum",
   205  						},
   206  					},
   207  				},
   208  			),
   209  			signaturePaths: []string{"checksum.sig", "checksum2.sig"},
   210  			signatureNames: []string{"checksum.sig", "checksum2.sig"},
   211  		},
   212  		{
   213  			desc: "sign only filtered checksums",
   214  			ctx: context.New(
   215  				config.Project{
   216  					Signs: []config.Sign{
   217  						{
   218  							Artifacts: "checksum",
   219  							IDs:       []string{"foo"},
   220  						},
   221  					},
   222  				},
   223  			),
   224  			signaturePaths: []string{"checksum.sig", "checksum2.sig"},
   225  			signatureNames: []string{"checksum.sig", "checksum2.sig"},
   226  		},
   227  		{
   228  			desc: "sign only source",
   229  			ctx: context.New(
   230  				config.Project{
   231  					Signs: []config.Sign{
   232  						{
   233  							Artifacts: "source",
   234  						},
   235  					},
   236  				},
   237  			),
   238  			signaturePaths: []string{"artifact5.tar.gz.sig"},
   239  			signatureNames: []string{"artifact5.tar.gz.sig"},
   240  		},
   241  		{
   242  			desc: "sign all artifacts with env",
   243  			ctx: context.New(
   244  				config.Project{
   245  					Signs: []config.Sign{
   246  						{
   247  							Artifacts: "all",
   248  							Args: []string{
   249  								"-u",
   250  								"${TEST_USER}",
   251  								"--output",
   252  								"${signature}",
   253  								"--detach-sign",
   254  								"${artifact}",
   255  							},
   256  						},
   257  					},
   258  					Env: []string{
   259  						fmt.Sprintf("TEST_USER=%s", user),
   260  					},
   261  				},
   262  			),
   263  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"},
   264  			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"},
   265  		},
   266  		{
   267  			desc: "sign all artifacts with template",
   268  			ctx: context.New(
   269  				config.Project{
   270  					Signs: []config.Sign{
   271  						{
   272  							Artifacts: "all",
   273  							Args: []string{
   274  								"-u",
   275  								"{{ .Env.SOME_TEST_USER }}",
   276  								"--output",
   277  								"${signature}",
   278  								"--detach-sign",
   279  								"${artifact}",
   280  							},
   281  						},
   282  					},
   283  					Env: []string{
   284  						fmt.Sprintf("SOME_TEST_USER=%s", user),
   285  					},
   286  				},
   287  			),
   288  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"},
   289  			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"},
   290  		},
   291  		{
   292  			desc: "sign single with password from stdin",
   293  			ctx: context.New(
   294  				config.Project{
   295  					Signs: []config.Sign{
   296  						{
   297  							Artifacts: "all",
   298  							Args: []string{
   299  								"-u",
   300  								passwordUser,
   301  								"--batch",
   302  								"--pinentry-mode",
   303  								"loopback",
   304  								"--passphrase-fd",
   305  								"0",
   306  								"--output",
   307  								"${signature}",
   308  								"--detach-sign",
   309  								"${artifact}",
   310  							},
   311  							Stdin: &stdin,
   312  						},
   313  					},
   314  				},
   315  			),
   316  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"},
   317  			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"},
   318  			user:           passwordUser,
   319  		},
   320  		{
   321  			desc: "sign single with password from stdin_file",
   322  			ctx: context.New(
   323  				config.Project{
   324  					Signs: []config.Sign{
   325  						{
   326  							Artifacts: "all",
   327  							Args: []string{
   328  								"-u",
   329  								passwordUser,
   330  								"--batch",
   331  								"--pinentry-mode",
   332  								"loopback",
   333  								"--passphrase-fd",
   334  								"0",
   335  								"--output",
   336  								"${signature}",
   337  								"--detach-sign",
   338  								"${artifact}",
   339  							},
   340  							StdinFile: filepath.Join(keyring, passwordUser),
   341  						},
   342  					},
   343  				},
   344  			),
   345  			signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"},
   346  			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"},
   347  			user:           passwordUser,
   348  		},
   349  		{
   350  			desc: "missing stdin_file",
   351  			ctx: context.New(
   352  				config.Project{
   353  					Signs: []config.Sign{
   354  						{
   355  							Artifacts: "all",
   356  							Args: []string{
   357  								"--batch",
   358  								"--pinentry-mode",
   359  								"loopback",
   360  								"--passphrase-fd",
   361  								"0",
   362  							},
   363  							StdinFile: "/tmp/non-existing-file",
   364  						},
   365  					},
   366  				},
   367  			),
   368  			expectedErrMsg: `sign failed: cannot open file /tmp/non-existing-file: open /tmp/non-existing-file: no such file or directory`,
   369  		},
   370  	}
   371  
   372  	for _, test := range tests {
   373  		if test.user == "" {
   374  			test.user = user
   375  		}
   376  
   377  		t.Run(test.desc, func(tt *testing.T) {
   378  			testSign(tt, test.ctx, test.signaturePaths, test.signatureNames, test.user, test.expectedErrMsg)
   379  		})
   380  	}
   381  }
   382  
   383  func testSign(t testing.TB, ctx *context.Context, signaturePaths []string, signatureNames []string, user, expectedErrMsg string) {
   384  	var tmpdir = t.TempDir()
   385  
   386  	ctx.Config.Dist = tmpdir
   387  
   388  	// create some fake artifacts
   389  	var artifacts = []string{"artifact1", "artifact2", "artifact3", "checksum", "checksum2"}
   390  	require.NoError(t, os.Mkdir(filepath.Join(tmpdir, "linux_amd64"), os.ModePerm))
   391  	for _, f := range artifacts {
   392  		file := filepath.Join(tmpdir, f)
   393  		require.NoError(t, ioutil.WriteFile(file, []byte("foo"), 0644))
   394  	}
   395  	require.NoError(t, ioutil.WriteFile(filepath.Join(tmpdir, "linux_amd64", "artifact4"), []byte("foo"), 0644))
   396  	artifacts = append(artifacts, "linux_amd64/artifact4")
   397  	require.NoError(t, ioutil.WriteFile(filepath.Join(tmpdir, "artifact5.tar.gz"), []byte("foo"), 0644))
   398  	artifacts = append(artifacts, "artifact5.tar.gz")
   399  	ctx.Artifacts.Add(&artifact.Artifact{
   400  		Name: "artifact1",
   401  		Path: filepath.Join(tmpdir, "artifact1"),
   402  		Type: artifact.UploadableArchive,
   403  		Extra: map[string]interface{}{
   404  			"ID": "foo",
   405  		},
   406  	})
   407  	ctx.Artifacts.Add(&artifact.Artifact{
   408  		Name: "artifact2",
   409  		Path: filepath.Join(tmpdir, "artifact2"),
   410  		Type: artifact.UploadableArchive,
   411  		Extra: map[string]interface{}{
   412  			"ID": "foo3",
   413  		},
   414  	})
   415  	ctx.Artifacts.Add(&artifact.Artifact{
   416  		Name: "artifact3_1.0.0_linux_amd64",
   417  		Path: filepath.Join(tmpdir, "artifact3"),
   418  		Type: artifact.UploadableBinary,
   419  		Extra: map[string]interface{}{
   420  			"ID": "foo",
   421  		},
   422  	})
   423  	ctx.Artifacts.Add(&artifact.Artifact{
   424  		Name: "checksum",
   425  		Path: filepath.Join(tmpdir, "checksum"),
   426  		Type: artifact.Checksum,
   427  	})
   428  	ctx.Artifacts.Add(&artifact.Artifact{
   429  		Name: "checksum2",
   430  		Path: filepath.Join(tmpdir, "checksum2"),
   431  		Type: artifact.Checksum,
   432  	})
   433  	ctx.Artifacts.Add(&artifact.Artifact{
   434  		Name: "artifact4_1.0.0_linux_amd64",
   435  		Path: filepath.Join(tmpdir, "linux_amd64", "artifact4"),
   436  		Type: artifact.UploadableBinary,
   437  		Extra: map[string]interface{}{
   438  			"ID": "foo3",
   439  		},
   440  	})
   441  	ctx.Artifacts.Add(&artifact.Artifact{
   442  		Name: "artifact5.tar.gz",
   443  		Path: filepath.Join(tmpdir, "artifact5.tar.gz"),
   444  		Type: artifact.UploadableSourceArchive,
   445  	})
   446  
   447  	// configure the pipeline
   448  	// make sure we are using the test keyring
   449  	require.NoError(t, Pipe{}.Default(ctx))
   450  	for i := range ctx.Config.Signs {
   451  		ctx.Config.Signs[i].Args = append(
   452  			[]string{"--homedir", keyring},
   453  			ctx.Config.Signs[i].Args...,
   454  		)
   455  	}
   456  
   457  	// run the pipeline
   458  	if expectedErrMsg != "" {
   459  		require.EqualError(t, Pipe{}.Run(ctx), expectedErrMsg)
   460  		return
   461  	}
   462  
   463  	require.NoError(t, Pipe{}.Run(ctx))
   464  
   465  	// ensure all artifacts have an ID
   466  	for _, arti := range ctx.Artifacts.Filter(artifact.ByType(artifact.Signature)).List() {
   467  		require.NotEmptyf(t, arti.ExtraOr("ID", ""), ".Extra.ID on %s", arti.Path)
   468  	}
   469  
   470  	// verify that only the artifacts and the signatures are in the dist dir
   471  	gotFiles := []string{}
   472  
   473  	require.NoError(t, filepath.Walk(tmpdir,
   474  		func(path string, info os.FileInfo, err error) error {
   475  			if err != nil {
   476  				return err
   477  			}
   478  			if info.IsDir() {
   479  				return nil
   480  			}
   481  			relPath, err := filepath.Rel(tmpdir, path)
   482  			if err != nil {
   483  				return err
   484  			}
   485  			gotFiles = append(gotFiles, relPath)
   486  			return nil
   487  		}),
   488  	)
   489  
   490  	wantFiles := append(artifacts, signaturePaths...)
   491  	sort.Strings(wantFiles)
   492  	require.ElementsMatch(t, wantFiles, gotFiles)
   493  
   494  	// verify the signatures
   495  	for _, sig := range signaturePaths {
   496  		verifySignature(t, ctx, sig, user)
   497  	}
   498  
   499  	var signArtifacts []string
   500  	for _, sig := range ctx.Artifacts.Filter(artifact.ByType(artifact.Signature)).List() {
   501  		signArtifacts = append(signArtifacts, sig.Name)
   502  	}
   503  	// check signature is an artifact
   504  	require.ElementsMatch(t, signArtifacts, signatureNames)
   505  }
   506  
   507  func verifySignature(t testing.TB, ctx *context.Context, sig string, user string) {
   508  	artifact := strings.Replace(sig, filepath.Ext(sig), "", 1)
   509  
   510  	// verify signature was made with key for usesr 'nopass'
   511  	cmd := exec.Command("gpg", "--homedir", keyring, "--verify", filepath.Join(ctx.Config.Dist, sig), filepath.Join(ctx.Config.Dist, artifact))
   512  	out, err := cmd.CombinedOutput()
   513  	require.NoError(t, err)
   514  
   515  	// check if the signature matches the user we expect to do this properly we
   516  	// might need to have either separate keyrings or export the key from the
   517  	// keyring before we do the verification. For now we punt and look in the
   518  	// output.
   519  	if !bytes.Contains(out, []byte(user)) {
   520  		t.Fatalf("%s: signature is not from %s: %s", sig, user, string(out))
   521  	}
   522  }
   523  
   524  func TestSeveralSignsWithTheSameID(t *testing.T) {
   525  	var ctx = &context.Context{
   526  		Config: config.Project{
   527  			Signs: []config.Sign{
   528  				{
   529  					ID: "a",
   530  				},
   531  				{
   532  					ID: "a",
   533  				},
   534  			},
   535  		},
   536  	}
   537  	require.EqualError(t, Pipe{}.Default(ctx), "found 2 signs with the ID 'a', please fix your config")
   538  }