From 468387625bc79caf2548bde73b29aa42ee6fb933 Mon Sep 17 00:00:00 2001 From: Yoan Blanc Date: Fri, 2 Oct 2020 21:26:37 +0200 Subject: [PATCH] feat: sign: extra option allowing stdin for commands (#1770) * feat: extra option allowing stdin for the signing command Signed-off-by: Yoan Blanc * feat: allow stdin to be an empty string Signed-off-by: Yoan Blanc * fix: increase code coverage Signed-off-by: Yoan Blanc --- internal/pipe/sign/sign.go | 18 ++++ internal/pipe/sign/sign_test.go | 99 ++++++++++++++++-- ...57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev | 32 ++++++ internal/pipe/sign/testdata/gnupg/password | 1 + ...FB585B45AFE4EB075EC88212972B3C25FCBFF5.key | Bin 0 -> 1158 bytes ...3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key | Bin 0 -> 1158 bytes internal/pipe/sign/testdata/gnupg/pubring.kbx | Bin 1430 -> 2824 bytes internal/pipe/sign/testdata/gnupg/trustdb.gpg | Bin 1280 -> 1360 bytes pkg/config/config.go | 2 + www/docs/customization/sign.md | 8 ++ 10 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 internal/pipe/sign/testdata/gnupg/openpgp-revocs.d/FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev create mode 100644 internal/pipe/sign/testdata/gnupg/password create mode 100644 internal/pipe/sign/testdata/gnupg/private-keys-v1.d/CAFB585B45AFE4EB075EC88212972B3C25FCBFF5.key create mode 100644 internal/pipe/sign/testdata/gnupg/private-keys-v1.d/FC3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key 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 0000000000000000000000000000000000000000..13135f44f021da2eefe67e7bce4cd68f8e787344 GIT binary patch literal 1158 zcmV;11bO=?GBG-Ea&L5HV{~O?EpT#ac42g7Eo)_YC^I^8b73ekI&LyGH#z{D*`F!r ze@~=R-Hxgo7X0wz-WKO&2LeU)xZ-<*{a@|Xi~Pt+8`QIhZ14rr2F3dcMpM{@Mg;|y5`6zYY)Uwn<`)C4%EtA+wTRk{P>v>p@Wd0fhT=;6HlH0q{e~Se33%OY{w(Gl z>6KRy4p$8F7ibk$V^`8LvJ6gyEbrFVLO3DBQ)0eH* z-r!#b2LrnOy_8bS?n-OISx(e9&fXZM2;rlppK1{qx061<5i7`%#VY%+;*x?VYPJz(O+-;waeGk(|IYPKT3n_a9^x13v95jH8~}JWV4U;B(7YiqmY(3XKMwx%!<(?EUSjqE-CSWn*+@WG!KI YF*Q0eFfuSOI5IR;FgP(aF)%49DfR^jz`xd%MM4WQAU5UjYyQdHPKF0Op zk<2k=Rk*j9;L%Dh#t#)SLsB?L`3qS{#X)_l@LTnW{$(@9e4sW}rVLOl1X?WiEdPTi z#t@ii&84Xa-{Ynq03t`0ab`r?n>s7t`rit7OP6wb8F#>H1}4~NyIxWBRV3HQYJ8C!IAo(Qqtv;BU?d87LwweVOv%GP=8;pKp9_ zY$kN4e%N;@#F7YZ;J)XUb`8^W;9d+nL2^8DX)cvCn4{Om9kKu~z#j`KC^0%^GdckP z0VyasI&gAtbY)|7Wn?lnI&W}gZg6LCEpsw!Gc9vyVKFUXWpgcKVq+*MG&*x=VKF#5 z%r}OViPEXlI65;lH#aajG&CtOHabkK?%k3C7*m)$kW*{1^XDlyGB7%0mw~HkKmy>7JHUXqmQTxFYii?sciStl5ai2x za7H}liio_qF-HY;=a~3)$VhVM>H}PO)|BF|7;MPxAjohPc?}}in|!L=Axon@3o7!^ zA;I92aC(5b$iz{1oo^C~6q>`L(yT#H14Ji1&M0<`_Y?6W<}q)6eCb3L9HUn1k&dRz zGgIQwr0YSSKxFLeLXL~231^#~!t1@7lLL;+5MXF^H~AhqR^Ec_Ibrj0Ovp+(N4&S` z3Jnp8BZ*qko`&j1V$%~h& znHV<>0;SQMD)2D|@CB$>CQK|VbjK{OOcS0Pk+!$Ri}^MJ(kc1iRw5H0rYFwj)f^}3 zskYCsbNa#GPn_Q3GtI!7miY$KJ;kpIK|XprNi;DcB3?7Q1P+eua2AvEt;L<=60F@x zTlf)#M)9eXQ~DX5r|H%OY|jnLe~B=xarzapLe)P&-J9bhh6CBl5YZIbUxx1>?P2{)SF zCbHF?=pqtAK-B>_x;Y}8mhiHq$NTa$SWn*+@WG!KI YF*Q0eFfuSOI5IR;FgP(aF)=AADLJ+#vj6}9 literal 0 HcmV?d00001 diff --git a/internal/pipe/sign/testdata/gnupg/pubring.kbx b/internal/pipe/sign/testdata/gnupg/pubring.kbx index 8b3726bed4254517d3caf97c73132de1fb96edf0..6a330595242473a24fae6f297895c9b7c58bc514 100644 GIT binary patch delta 1388 zcmbQn-66JN8fz2-18WfzBLfK3F)*+^U|?d9`5nF?A~NIE9oa=)y3P)dyUu=I^d2a$ z08&(Zd?$}bsGg+mr8z7&Z)wK#Y^pyC6t)4$0RbZe50ID#WN}QalBth(F6Rdc&t&9f ziFYph%gDhn`{sPj=k@+eg6{UMk{0{(;qg7O=c(*Wu3vXNs&4)p|MqJ4pA%lvS2nlj zd|>K*8Jg! z@Yc3RUVqHy%R2sTlQrb5xSRV==b7xwso?^A;XI$jGepCZ!!H?a;_++Id4KJilZE1u z;G~mI8`ITxT3L%MeR7JcZtMArNwp8{Zu`%@#aOvcVT~l8*uqU8;^aDJo11%E)K}<# zar(biCu-@vKlV-E6gI5M*Rv+yD%x^n!Fq$TMu=C`c?WF3&GYfr>MMLXCl; zlhKbw7#L_4Vu~!-Vuh6n6skwe19IburvpDL%Svtgd{schL%+?w%P`-^Q0-fld3I`~}0JSGcO)7FFB zVaKM1JQ7t>shX8wm^*8??Nf~{`~r7=ta@=uV4K2&=l~;~1fQ1AiYATEU!0rS=koMb zVUoklz;A4mbI;5F-p9Uw!8Qx81okCz0;Wd)mp;)8Zu+OWY*H-KQ$u%bpYhuK~Wjp~oEKxe&bpAK19}}37es<|f_WO^Q$ulUq zPb*Awxi#BL`@ygKT;<-=i>f5c4`i{a-ALaZ7i6^T-IG4S#Z|hWM84Mgec#L)aB%*b zdiw)kULN@oZFD12r^w4P6?)mfcYw~i`>Pwc_-6+>Q zGKnMa!M^9y%6KmqKZxV8b}X_f%G8@`GGp=eW3rnV^bg2$gVSUuqZx|?FtsTVNNqgG ziOS~~`#aX2^Q&d#8iLc22b}-6<3}C2QwpX(59iL=IrWOhJL+Cs{G1%Xq?5>4Jv-AbMdQyF)#(Qw>a;##*~#wuVuj1eKPOql z{nC5wH1rqs-@CWW@{*_$OW?r(Gqr1%9_(4x9Cl0GV@k-iMIFsw=Y`*@dZuwT<=;cj u3mNm+FO delta 7 OcmeAWo5sCi8Y=(_o&t*i diff --git a/internal/pipe/sign/testdata/gnupg/trustdb.gpg b/internal/pipe/sign/testdata/gnupg/trustdb.gpg index 264aa755707127d3077aac8592bfa047be1dd8f9..95dbe3b2ac9d9096ffd89aadaa20653f4a6bb927 100644 GIT binary patch delta 136 zcmZqRy1*sCm|l?1%*@Ej$i%=9?_4f4(NKJ&dK?R*!bCr<$s1Vgk{B=m55w>94H1zU zukOe$>e6*~c-(dN^P=}`5OsxN delta 58 zcmcb>)xagfm|l?1%*@Ej$i%=9Wqe6B=kLNP69t7gs>iWR^wplcfyGXUjR7410H=!! AegFUf 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