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