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 }