diff --git a/internal/pipe/sign/sign.go b/internal/pipe/sign/sign.go index 42266e3fa..2c5a88034 100644 --- a/internal/pipe/sign/sign.go +++ b/internal/pipe/sign/sign.go @@ -2,9 +2,11 @@ package sign import ( "fmt" + "io" "os" "os/exec" "path/filepath" + "strings" "github.com/apex/log" "github.com/goreleaser/goreleaser/internal/artifact" @@ -120,6 +122,19 @@ func signone(ctx *context.Context, cfg config.Sign, a *artifact.Artifact) (*arti args = append(args, arg) } + var stdin io.Reader + if cfg.Stdin != nil { + stdin = strings.NewReader(*cfg.Stdin) + } else if cfg.StdinFile != "" { + f, err := os.Open(cfg.StdinFile) + if err != nil { + return nil, errors.Wrapf(err, "sign failed: cannot open file %s", cfg.StdinFile) + } + defer f.Close() + + stdin = f + } + // The GoASTScanner flags this as a security risk. // However, this works as intended. The nosec annotation // tells the scanner to ignore this. @@ -127,6 +142,9 @@ func signone(ctx *context.Context, cfg config.Sign, a *artifact.Artifact) (*arti cmd := exec.CommandContext(ctx, cfg.Cmd, args...) cmd.Stderr = logext.NewWriter(log.WithField("cmd", cfg.Cmd)) cmd.Stdout = cmd.Stderr + if stdin != nil { + cmd.Stdin = stdin + } log.WithField("cmd", cmd.Args).Info("signing") if err := cmd.Run(); err != nil { return nil, fmt.Errorf("sign: %s failed", cfg.Cmd) diff --git a/internal/pipe/sign/sign_test.go b/internal/pipe/sign/sign_test.go index bdb42aa12..366ab683b 100644 --- a/internal/pipe/sign/sign_test.go +++ b/internal/pipe/sign/sign_test.go @@ -23,6 +23,9 @@ import ( var originKeyring = "testdata/gnupg" var keyring string +const user = "nopass" +const passwordUser = "password" + func TestMain(m *testing.M) { rand.Seed(time.Now().UnixNano()) keyring = fmt.Sprintf("/tmp/gorel_gpg_test.%d", rand.Int()) @@ -31,6 +34,7 @@ func TestMain(m *testing.M) { fmt.Printf("failed to copy %s to %s: %s", originKeyring, keyring, err) os.Exit(1) } + defer os.RemoveAll(keyring) os.Exit(m.Run()) } @@ -79,12 +83,14 @@ func TestSignInvalidArtifacts(t *testing.T) { } func TestSignArtifacts(t *testing.T) { + stdin := passwordUser tests := []struct { desc string ctx *context.Context signaturePaths []string signatureNames []string expectedErrMsg string + user string }{ { desc: "sign errors", @@ -283,18 +289,99 @@ func TestSignArtifacts(t *testing.T) { signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"}, 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"}, }, + { + desc: "sign single with password from stdin", + ctx: context.New( + config.Project{ + Signs: []config.Sign{ + { + Artifacts: "all", + Args: []string{ + "-u", + passwordUser, + "--batch", + "--pinentry-mode", + "loopback", + "--passphrase-fd", + "0", + "--output", + "${signature}", + "--detach-sign", + "${artifact}", + }, + Stdin: &stdin, + }, + }, + }, + ), + signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"}, + 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"}, + user: passwordUser, + }, + { + desc: "sign single with password from stdin_file", + ctx: context.New( + config.Project{ + Signs: []config.Sign{ + { + Artifacts: "all", + Args: []string{ + "-u", + passwordUser, + "--batch", + "--pinentry-mode", + "loopback", + "--passphrase-fd", + "0", + "--output", + "${signature}", + "--detach-sign", + "${artifact}", + }, + StdinFile: filepath.Join(keyring, passwordUser), + }, + }, + }, + ), + signaturePaths: []string{"artifact1.sig", "artifact2.sig", "artifact3.sig", "checksum.sig", "checksum2.sig", "linux_amd64/artifact4.sig", "artifact5.tar.gz.sig"}, + 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"}, + user: passwordUser, + }, + { + desc: "missing stdin_file", + ctx: context.New( + config.Project{ + Signs: []config.Sign{ + { + Artifacts: "all", + Args: []string{ + "--batch", + "--pinentry-mode", + "loopback", + "--passphrase-fd", + "0", + }, + StdinFile: "/tmp/non-existing-file", + }, + }, + }, + ), + expectedErrMsg: `sign failed: cannot open file /tmp/non-existing-file: open /tmp/non-existing-file: no such file or directory`, + }, } for _, test := range tests { + if test.user == "" { + test.user = user + } + t.Run(test.desc, func(tt *testing.T) { - testSign(tt, test.ctx, test.signaturePaths, test.signatureNames, test.expectedErrMsg) + testSign(tt, test.ctx, test.signaturePaths, test.signatureNames, test.user, test.expectedErrMsg) }) } } -const user = "nopass" - -func testSign(t *testing.T, ctx *context.Context, signaturePaths []string, signatureNames []string, expectedErrMsg string) { +func testSign(t *testing.T, ctx *context.Context, signaturePaths []string, signatureNames []string, user, expectedErrMsg string) { // create temp dir for file and signature tmpdir, err := ioutil.TempDir("", "goreleaser") assert.NoError(t, err) @@ -411,7 +498,7 @@ func testSign(t *testing.T, ctx *context.Context, signaturePaths []string, signa // verify the signatures for _, sig := range signaturePaths { - verifySignature(t, ctx, sig) + verifySignature(t, ctx, sig, user) } var signArtifacts []string @@ -422,7 +509,7 @@ func testSign(t *testing.T, ctx *context.Context, signaturePaths []string, signa assert.ElementsMatch(t, signArtifacts, signatureNames) } -func verifySignature(t *testing.T, ctx *context.Context, sig string) { +func verifySignature(t *testing.T, ctx *context.Context, sig string, user string) { artifact := strings.Replace(sig, filepath.Ext(sig), "", 1) // verify signature was made with key for usesr 'nopass' diff --git a/internal/pipe/sign/testdata/gnupg/openpgp-revocs.d/FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev b/internal/pipe/sign/testdata/gnupg/openpgp-revocs.d/FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev new file mode 100644 index 000000000..f1f71d475 --- /dev/null +++ b/internal/pipe/sign/testdata/gnupg/openpgp-revocs.d/FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev @@ -0,0 +1,32 @@ +Ceci est un certificat de révocation pour la clef OpenPGP : + +pub rsa2048 2020-08-24 [S] + FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF +uid password + +A revocation certificate is a kind of "kill switch" to publicly +declare that a key shall not anymore be used. It is not possible +to retract such a revocation certificate once it has been published. + +Use it to revoke this key in case of a compromise or loss of +the secret key. However, if the secret key is still accessible, +it is better to generate a new revocation certificate and give +a reason for the revocation. For details see the description of +of the gpg command "--generate-revocation" in the GnuPG manual. + +To avoid an accidental use of this file, a colon has been inserted +before the 5 dashes below. Remove this colon with a text editor +before importing and publishing this revocation certificate. + +:-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: This is a revocation certificate + +iQE2BCABCAAgFiEE+1ewWFlo6twdoootQ0Djis3zou8FAl9Ddw8CHQAACgkQQ0Dj +is3zou9JJAf8CizfKMs2FxUyLVrxl46zKUsvABFdan9FCY24kg+1sEmiGO7pSqJ9 +sja6hYaOU1qE3LhqJ+ULqDaBX33ACYPRlkvhKrWV+EVS4Ppx22zu7Y/Vn+xskeJO +lh26eRXiHAJoCjMjnnLNa1gZUWSKVghZ9JhLhhZ/pyHQefsxGmc/nqrrx8SiiWPZ +wknfZ5f2DhABKOOkO7dZ72W3+ApwUF0T8z19kzn6ZaY3JM9GQOo/OIKuRFrmxbEu +2owjHd1NRO2xJaMqv+GlwyUZ55zBR248tHBqpvS46wNjftJqYHLgFqrMGYju7nJe +it1y6FjfT2zLgPqPLcpBdaynd8+rJQo1QQ== +=cnWP +-----END PGP PUBLIC KEY BLOCK----- diff --git a/internal/pipe/sign/testdata/gnupg/password b/internal/pipe/sign/testdata/gnupg/password new file mode 100644 index 000000000..7aa311adf --- /dev/null +++ b/internal/pipe/sign/testdata/gnupg/password @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/internal/pipe/sign/testdata/gnupg/private-keys-v1.d/CAFB585B45AFE4EB075EC88212972B3C25FCBFF5.key b/internal/pipe/sign/testdata/gnupg/private-keys-v1.d/CAFB585B45AFE4EB075EC88212972B3C25FCBFF5.key new file mode 100644 index 000000000..13135f44f Binary files /dev/null and b/internal/pipe/sign/testdata/gnupg/private-keys-v1.d/CAFB585B45AFE4EB075EC88212972B3C25FCBFF5.key differ diff --git a/internal/pipe/sign/testdata/gnupg/private-keys-v1.d/FC3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key b/internal/pipe/sign/testdata/gnupg/private-keys-v1.d/FC3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key new file mode 100644 index 000000000..cd389f214 Binary files /dev/null and b/internal/pipe/sign/testdata/gnupg/private-keys-v1.d/FC3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key differ diff --git a/internal/pipe/sign/testdata/gnupg/pubring.kbx b/internal/pipe/sign/testdata/gnupg/pubring.kbx index 8b3726bed..6a3305952 100644 Binary files a/internal/pipe/sign/testdata/gnupg/pubring.kbx and b/internal/pipe/sign/testdata/gnupg/pubring.kbx differ diff --git a/internal/pipe/sign/testdata/gnupg/trustdb.gpg b/internal/pipe/sign/testdata/gnupg/trustdb.gpg index 264aa7557..95dbe3b2a 100644 Binary files a/internal/pipe/sign/testdata/gnupg/trustdb.gpg and b/internal/pipe/sign/testdata/gnupg/trustdb.gpg differ diff --git a/pkg/config/config.go b/pkg/config/config.go index 012729050..8ff3fff8b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -347,6 +347,8 @@ type Sign struct { Signature string `yaml:"signature,omitempty"` Artifacts string `yaml:"artifacts,omitempty"` IDs []string `yaml:"ids,omitempty"` + Stdin *string `yaml:"stdin,omitempty"` + StdinFile string `yaml:"stdin_file,omitempty"` } // SnapcraftAppMetadata for the binaries that will be in the snap package. diff --git a/www/docs/customization/sign.md b/www/docs/customization/sign.md index ead008c90..0523a9e97 100644 --- a/www/docs/customization/sign.md +++ b/www/docs/customization/sign.md @@ -69,6 +69,14 @@ signs: ids: - foo - bar + + # Stdin data to be given to the signature command as stdin. + # defaults to empty + stdin: password + + # StdinFile file to be given to the signature command as stdin. + # defaults to empty + stdin_file: ./.password ``` ### Limitations