From 7b95e1e342e58fe7181706f9e56a44919f16325f Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Wed, 13 Dec 2017 17:14:01 +0100 Subject: [PATCH] feat: add artifact sign pipeline This patch adds a generic artifact signing pipeline. Fixes #166 --- config/config.go | 9 +++ context/context.go | 11 ++++ docs/075-sign.md | 51 ++++++++++++++++ goreleaserlib/goreleaser.go | 2 + pipeline/checksums/checksums.go | 1 + pipeline/defaults/defaults.go | 2 + pipeline/sign/sign.go | 103 ++++++++++++++++++++++++++++++++ pipeline/sign/sign_test.go | 18 ++++++ 8 files changed, 197 insertions(+) create mode 100644 docs/075-sign.md create mode 100644 pipeline/sign/sign.go create mode 100644 pipeline/sign/sign_test.go diff --git a/config/config.go b/config/config.go index 30be2c180..11348abd1 100644 --- a/config/config.go +++ b/config/config.go @@ -144,6 +144,14 @@ type FPM struct { XXX map[string]interface{} `yaml:",inline"` } +// Sign config +type Sign struct { + Cmd string `yaml:"cmd,omitempty"` + Args []string `yaml:"args,omitempty"` + Signature string `yaml:"signature,omitempty"` + Artifacts string `yaml:"artifacts,omitempty"` +} + // SnapcraftAppMetadata for the binaries that will be in the snap package type SnapcraftAppMetadata struct { Plugs []string @@ -238,6 +246,7 @@ type Project struct { Artifactories []Artifactory `yaml:",omitempty"` Changelog Changelog `yaml:",omitempty"` Dist string `yaml:",omitempty"` + Sign Sign `yaml:",omitempty"` // this is a hack ¯\_(ツ)_/¯ SingleBuild Build `yaml:"build,omitempty"` diff --git a/context/context.go b/context/context.go index b37ced395..594def0e4 100644 --- a/context/context.go +++ b/context/context.go @@ -37,6 +37,7 @@ type Context struct { Git GitInfo Binaries map[string]map[string][]Binary Artifacts []string + Checksums []string Dockers []string ReleaseNotes string Version string @@ -50,6 +51,7 @@ type Context struct { var ( artifactsLock sync.Mutex + checksumsLock sync.Mutex dockersLock sync.Mutex binariesLock sync.Mutex ) @@ -63,6 +65,15 @@ func (ctx *Context) AddArtifact(file string) { log.WithField("artifact", file).Info("new release artifact") } +// AddChecksum adds a checksum file. +func (ctx *Context) AddChecksum(file string) { + checksumsLock.Lock() + defer checksumsLock.Unlock() + file = strings.TrimPrefix(file, ctx.Config.Dist+string(filepath.Separator)) + ctx.Checksums = append(ctx.Checksums, file) + log.WithField("checksum", file).Info("new checksum file") +} + // AddDocker adds a docker image to the docker images list func (ctx *Context) AddDocker(image string) { dockersLock.Lock() diff --git a/docs/075-sign.md b/docs/075-sign.md new file mode 100644 index 000000000..0095c8f1a --- /dev/null +++ b/docs/075-sign.md @@ -0,0 +1,51 @@ +--- +title: Signing +--- + +GoReleaser can sign some or all of the generated artifacts. Signing ensures +that the artifacts have been generated by yourself and your users can verify +that by comparing the generated signature with your public signing key. + +Signing works in combination with checksum files and it is generally sufficient +to sign the checksum files only. + +The default is configured to create a detached signature for the checksum files +with [GunPG](https://www.gnupg.org/) and your default key. To enable signing +just add + +```yaml +# goreleaser.yml +sign: + artifacts: checksum +``` + +To customize the signing pipeline you can use the following options: + +```yml +# .goreleaser.yml +sign: + # name of the signature file. + # '${in}' is the path to the artifact that should be signed. + # + # signature: "${artifact}.sig" + + # path to the signature command + # + # cmd: gpg + + # command line arguments for the command + # + # to sign with a specific key use + # args: ["-u", "", "--output", "${signature}", "--detach-sign", "${artifact}"] + # + # args: ["--output", "${signature}", "--detach-sign", "${artifact}"] + + + # which artifacts to sign + # + # checksum: only checksum file(s) + # all: all artifacts + # none: no signing + # + # artifacts: none +``` diff --git a/goreleaserlib/goreleaser.go b/goreleaserlib/goreleaser.go index 75191671c..0eb52223e 100644 --- a/goreleaserlib/goreleaser.go +++ b/goreleaserlib/goreleaser.go @@ -25,6 +25,7 @@ import ( "github.com/goreleaser/goreleaser/pipeline/fpm" "github.com/goreleaser/goreleaser/pipeline/git" "github.com/goreleaser/goreleaser/pipeline/release" + "github.com/goreleaser/goreleaser/pipeline/sign" "github.com/goreleaser/goreleaser/pipeline/snapcraft" yaml "gopkg.in/yaml.v2" ) @@ -50,6 +51,7 @@ var pipes = []pipeline.Piper{ fpm.Pipe{}, // archive via fpm (deb, rpm, etc) snapcraft.Pipe{}, // archive via snapcraft (snap) checksums.Pipe{}, // checksums of the files + sign.Pipe{}, // sign artifacts docker.Pipe{}, // create and push docker images artifactory.Pipe{}, // push to artifactory release.Pipe{}, // release to github diff --git a/pipeline/checksums/checksums.go b/pipeline/checksums/checksums.go index 5051c66be..8a61fc87b 100644 --- a/pipeline/checksums/checksums.go +++ b/pipeline/checksums/checksums.go @@ -39,6 +39,7 @@ func (Pipe) Run(ctx *context.Context) (err error) { log.WithError(err).Errorf("failed to close %s", file.Name()) } ctx.AddArtifact(file.Name()) + ctx.AddChecksum(file.Name()) }() var g errgroup.Group for _, artifact := range ctx.Artifacts { diff --git a/pipeline/defaults/defaults.go b/pipeline/defaults/defaults.go index 842288d6e..51968d29e 100644 --- a/pipeline/defaults/defaults.go +++ b/pipeline/defaults/defaults.go @@ -14,6 +14,7 @@ import ( "github.com/goreleaser/goreleaser/pipeline/docker" "github.com/goreleaser/goreleaser/pipeline/fpm" "github.com/goreleaser/goreleaser/pipeline/release" + "github.com/goreleaser/goreleaser/pipeline/sign" "github.com/goreleaser/goreleaser/pipeline/snapshot" ) @@ -31,6 +32,7 @@ var defaulters = []pipeline.Defaulter{ build.Pipe{}, fpm.Pipe{}, checksums.Pipe{}, + sign.Pipe{}, docker.Pipe{}, artifactory.Pipe{}, brew.Pipe{}, diff --git a/pipeline/sign/sign.go b/pipeline/sign/sign.go new file mode 100644 index 000000000..4e80a959a --- /dev/null +++ b/pipeline/sign/sign.go @@ -0,0 +1,103 @@ +package sign + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/goreleaser/goreleaser/context" + "github.com/goreleaser/goreleaser/pipeline" +) + +type Pipe struct{} + +func (Pipe) String() string { + return "signing artifacts" +} + +func (Pipe) Default(ctx *context.Context) error { + cfg := &ctx.Config.Sign + if cfg.Cmd == "" { + cfg.Cmd = "gpg" + } + if cfg.Signature == "" { + cfg.Signature = "${artifact}.sig" + } + if len(cfg.Args) == 0 { + cfg.Args = []string{"--output", "$signature", "--detach-sig", "$artifact"} + } + if cfg.Artifacts == "" { + cfg.Artifacts = "none" + } + return nil +} + +func (Pipe) Run(ctx *context.Context) error { + switch ctx.Config.Sign.Artifacts { + case "checksum": + return sign(ctx, ctx.Checksums) + case "all": + return sign(ctx, ctx.Artifacts) + case "none": + return pipeline.Skip("artifact signing disabled") + default: + return fmt.Errorf("invalid list of artifacts to sign: %s", ctx.Config.Sign.Artifacts) + } +} + +func sign(ctx *context.Context, artifacts []string) error { + var sigs []string + for _, a := range artifacts { + sig, err := signone(ctx, a) + if err != nil { + return err + } + sigs = append(sigs, sig) + } + for _, sig := range sigs { + ctx.AddArtifact(sig) + } + return nil +} + +func signone(ctx *context.Context, artifact string) (string, error) { + cfg := ctx.Config.Sign + + artifact = filepath.Join(ctx.Config.Dist, artifact) + env := map[string]string{ + "artifact": artifact, + } + + sig := expand(cfg.Signature, env) + if sig == "" { + return "", fmt.Errorf("sign: signature file cannot be empty") + } + if sig == artifact { + return "", fmt.Errorf("sign: artifact and signature cannot be the same") + } + env["signature"] = sig + + // todo(fs): check if $out already exists + + var args []string + for _, a := range cfg.Args { + args = append(args, expand(a, env)) + } + + cmd := exec.Command(cfg.Cmd, args...) + output, err := cmd.CombinedOutput() + if len(output) > 200 { + output = output[:200] + } + if err != nil { + return "", fmt.Errorf("sign: %s failed with %q", cfg.Cmd, string(output)) + } + return sig, nil +} + +func expand(s string, env map[string]string) string { + return os.Expand(s, func(key string) string { + return env[key] + }) +} diff --git a/pipeline/sign/sign_test.go b/pipeline/sign/sign_test.go new file mode 100644 index 000000000..755288bca --- /dev/null +++ b/pipeline/sign/sign_test.go @@ -0,0 +1,18 @@ +package sign + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + gpgPlainKeyID = "0279C27FC1602A0E" + gpgEncryptedKeyID = "2AB4ABE1A4A47546" + gpgPassword = "secret" + gpgHome = "./testdata/gnupg" +) + +func TestDescription(t *testing.T) { + assert.NotEmpty(t, Pipe{}.String()) +}