mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-03-17 20:47:50 +02:00
feat: sign: extra option allowing stdin for commands (#1770)
* feat: extra option allowing stdin for the signing command Signed-off-by: Yoan Blanc <yoan@dosimple.ch> * feat: allow stdin to be an empty string Signed-off-by: Yoan Blanc <yoan@dosimple.ch> * fix: increase code coverage Signed-off-by: Yoan Blanc <yoan@dosimple.ch>
This commit is contained in:
parent
e98aa8bc87
commit
468387625b
@ -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)
|
||||
|
@ -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'
|
||||
|
32
internal/pipe/sign/testdata/gnupg/openpgp-revocs.d/FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev
vendored
Normal file
32
internal/pipe/sign/testdata/gnupg/openpgp-revocs.d/FB57B0585968EADC1DA28A2D4340E38ACDF3A2EF.rev
vendored
Normal file
@ -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-----
|
1
internal/pipe/sign/testdata/gnupg/password
vendored
Normal file
1
internal/pipe/sign/testdata/gnupg/password
vendored
Normal file
@ -0,0 +1 @@
|
||||
password
|
BIN
internal/pipe/sign/testdata/gnupg/private-keys-v1.d/CAFB585B45AFE4EB075EC88212972B3C25FCBFF5.key
vendored
Normal file
BIN
internal/pipe/sign/testdata/gnupg/private-keys-v1.d/CAFB585B45AFE4EB075EC88212972B3C25FCBFF5.key
vendored
Normal file
Binary file not shown.
BIN
internal/pipe/sign/testdata/gnupg/private-keys-v1.d/FC3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key
vendored
Normal file
BIN
internal/pipe/sign/testdata/gnupg/private-keys-v1.d/FC3A9AF0226DC94FBEEE5B3E6A4387FB1BFB4CC6.key
vendored
Normal file
Binary file not shown.
BIN
internal/pipe/sign/testdata/gnupg/pubring.kbx
vendored
BIN
internal/pipe/sign/testdata/gnupg/pubring.kbx
vendored
Binary file not shown.
BIN
internal/pipe/sign/testdata/gnupg/trustdb.gpg
vendored
BIN
internal/pipe/sign/testdata/gnupg/trustdb.gpg
vendored
Binary file not shown.
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user