mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-03-21 21:07:19 +02:00
The presence of an artifact field in the `signature` or `certificate` template field caused a silent failure in the template when re-applied after the external signing process was called. This was due to the artifact being presence in the template context before the signing process, but not after. An error here was also ignored. The fix supplies the artifact to the template context, and also allows a template failure to fail the overall process. As far as I can tell, this change aligns behaviour to match existing documentation. Fixes #5147
309 lines
8.1 KiB
Go
309 lines
8.1 KiB
Go
package sign
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/caarlos0/log"
|
|
"github.com/goreleaser/goreleaser/v2/internal/artifact"
|
|
"github.com/goreleaser/goreleaser/v2/internal/gio"
|
|
"github.com/goreleaser/goreleaser/v2/internal/git"
|
|
"github.com/goreleaser/goreleaser/v2/internal/ids"
|
|
"github.com/goreleaser/goreleaser/v2/internal/logext"
|
|
"github.com/goreleaser/goreleaser/v2/internal/pipe"
|
|
"github.com/goreleaser/goreleaser/v2/internal/semerrgroup"
|
|
"github.com/goreleaser/goreleaser/v2/internal/skips"
|
|
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
|
"github.com/goreleaser/goreleaser/v2/pkg/context"
|
|
)
|
|
|
|
// Pipe that signs common artifacts.
|
|
type Pipe struct{}
|
|
|
|
func (Pipe) String() string { return "signing artifacts" }
|
|
|
|
func (Pipe) Skip(ctx *context.Context) bool {
|
|
return skips.Any(ctx, skips.Sign) || len(ctx.Config.Signs) == 0
|
|
}
|
|
|
|
func (Pipe) Dependencies(ctx *context.Context) []string {
|
|
var cmds []string
|
|
for _, s := range ctx.Config.Signs {
|
|
cmds = append(cmds, s.Cmd)
|
|
}
|
|
return cmds
|
|
}
|
|
|
|
const defaultGpg = "gpg"
|
|
|
|
// Default sets the Pipes defaults.
|
|
func (Pipe) Default(ctx *context.Context) error {
|
|
gpgPath, _ := git.Clean(git.Run(ctx, "config", "gpg.program"))
|
|
if gpgPath == "" {
|
|
gpgPath = defaultGpg
|
|
}
|
|
|
|
ids := ids.New("signs")
|
|
for i := range ctx.Config.Signs {
|
|
cfg := &ctx.Config.Signs[i]
|
|
if cfg.Cmd == "" {
|
|
// gpgPath is either "gpg" (default) or the user's git config gpg.program value
|
|
cfg.Cmd = gpgPath
|
|
}
|
|
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"
|
|
}
|
|
if cfg.ID == "" {
|
|
cfg.ID = "default"
|
|
}
|
|
ids.Inc(cfg.ID)
|
|
}
|
|
return ids.Validate()
|
|
}
|
|
|
|
// Run executes the Pipe.
|
|
func (Pipe) Run(ctx *context.Context) error {
|
|
g := semerrgroup.New(ctx.Parallelism)
|
|
for i := range ctx.Config.Signs {
|
|
cfg := ctx.Config.Signs[i]
|
|
g.Go(func() error {
|
|
var filters []artifact.Filter
|
|
switch cfg.Artifacts {
|
|
case "checksum":
|
|
filters = append(filters, artifact.ByType(artifact.Checksum))
|
|
if len(cfg.IDs) > 0 {
|
|
log.Warn("when artifacts is `checksum`, `ids` has no effect. ignoring")
|
|
}
|
|
case "source":
|
|
filters = append(filters, artifact.ByType(artifact.UploadableSourceArchive))
|
|
if len(cfg.IDs) > 0 {
|
|
log.Warn("when artifacts is `source`, `ids` has no effect. ignoring")
|
|
}
|
|
case "all":
|
|
filters = append(filters, artifact.Or(
|
|
artifact.ByType(artifact.UploadableArchive),
|
|
artifact.ByType(artifact.UploadableBinary),
|
|
artifact.ByType(artifact.UploadableSourceArchive),
|
|
artifact.ByType(artifact.Checksum),
|
|
artifact.ByType(artifact.LinuxPackage),
|
|
artifact.ByType(artifact.SBOM),
|
|
))
|
|
case "archive":
|
|
filters = append(filters, artifact.ByType(artifact.UploadableArchive))
|
|
case "binary":
|
|
filters = append(filters, artifact.ByType(artifact.UploadableBinary))
|
|
case "sbom":
|
|
filters = append(filters, artifact.ByType(artifact.SBOM))
|
|
case "package":
|
|
filters = append(filters, artifact.ByType(artifact.LinuxPackage))
|
|
case "none": // TODO(caarlos0): this is not very useful, lets remove it.
|
|
return pipe.ErrSkipSignEnabled
|
|
default:
|
|
return fmt.Errorf("invalid list of artifacts to sign: %s", cfg.Artifacts)
|
|
}
|
|
|
|
if len(cfg.IDs) > 0 {
|
|
filters = append(filters, artifact.ByIDs(cfg.IDs...))
|
|
}
|
|
return sign(ctx, cfg, ctx.Artifacts.Filter(artifact.And(filters...)).List())
|
|
})
|
|
}
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.Artifacts.Refresh()
|
|
}
|
|
|
|
func sign(ctx *context.Context, cfg config.Sign, artifacts []*artifact.Artifact) error {
|
|
if len(artifacts) == 0 {
|
|
log.Warn("no artifacts matching the given filters found")
|
|
return nil
|
|
}
|
|
for _, a := range artifacts {
|
|
if err := a.Refresh(); err != nil {
|
|
return err
|
|
}
|
|
artifacts, err := signone(ctx, cfg, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, artifact := range artifacts {
|
|
ctx.Artifacts.Add(artifact)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func relativeToDist(dist, f string) (string, error) {
|
|
af, err := filepath.Abs(f)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
df, err := filepath.Abs(dist)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.HasPrefix(af, df) {
|
|
return f, nil
|
|
}
|
|
return filepath.Join(dist, f), nil
|
|
}
|
|
|
|
func tmplPath(ctx *context.Context, env map[string]string, a *artifact.Artifact, s string) (string, error) {
|
|
result, err := tmpl.New(ctx).WithArtifact(a).WithEnv(env).Apply(expand(s, env))
|
|
if err != nil || result == "" {
|
|
return "", err
|
|
}
|
|
return relativeToDist(ctx.Config.Dist, result)
|
|
}
|
|
|
|
func signone(ctx *context.Context, cfg config.Sign, art *artifact.Artifact) ([]*artifact.Artifact, error) {
|
|
env := ctx.Env.Copy()
|
|
env["artifactName"] = art.Name // shouldn't be used
|
|
env["artifact"] = art.Path
|
|
env["artifactID"] = art.ID()
|
|
env["digest"] = artifact.ExtraOr(*art, artifact.ExtraDigest, "")
|
|
|
|
tmplEnv, err := templateEnvS(ctx, cfg.Env)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
|
|
}
|
|
|
|
for k, v := range context.ToEnv(tmplEnv) {
|
|
env[k] = v
|
|
}
|
|
|
|
name, err := tmplPath(ctx, env, art, cfg.Signature)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
|
|
}
|
|
env["signature"] = name
|
|
|
|
cert, err := tmplPath(ctx, env, art, cfg.Certificate)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
|
|
}
|
|
env["certificate"] = cert
|
|
|
|
//nolint:prealloc
|
|
var args []string
|
|
for _, a := range cfg.Args {
|
|
arg, err := tmpl.New(ctx).WithEnv(env).Apply(expand(a, env))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
|
|
}
|
|
args = append(args, arg)
|
|
}
|
|
|
|
var stdin io.Reader
|
|
if cfg.Stdin != nil {
|
|
s, err := tmpl.New(ctx).WithEnv(env).Apply(expand(*cfg.Stdin, env))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stdin = strings.NewReader(s)
|
|
} else if cfg.StdinFile != "" {
|
|
f, err := os.Open(cfg.StdinFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: cannot open file %s: %w", cfg.StdinFile, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
stdin = f
|
|
}
|
|
|
|
log := log.WithField("cmd", cfg.Cmd).WithField("artifact", art.Name)
|
|
if name != "" {
|
|
log = log.WithField("signature", name)
|
|
}
|
|
if cert != "" {
|
|
log = log.WithField("certificate", cert)
|
|
}
|
|
|
|
// The GoASTScanner flags this as a security risk.
|
|
// However, this works as intended. The nosec annotation
|
|
// tells the scanner to ignore this.
|
|
// #nosec
|
|
cmd := exec.CommandContext(ctx, cfg.Cmd, args...)
|
|
var b bytes.Buffer
|
|
w := gio.Safe(&b)
|
|
cmd.Stderr = io.MultiWriter(logext.NewConditionalWriter(cfg.Output), w)
|
|
cmd.Stdout = io.MultiWriter(logext.NewConditionalWriter(cfg.Output), w)
|
|
if stdin != nil {
|
|
cmd.Stdin = stdin
|
|
}
|
|
cmd.Env = env.Strings()
|
|
log.Info("signing")
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, fmt.Errorf("sign: %s failed: %w: %s", cfg.Cmd, err, b.String())
|
|
}
|
|
|
|
var result []*artifact.Artifact
|
|
|
|
// re-execute template results, using artifact desc as artifact so they eval to the actual needed file desc.
|
|
env["artifact"] = art.Name
|
|
name, err = tmpl.New(ctx).WithArtifact(art).WithEnv(env).Apply(expand(cfg.Signature, env))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
|
|
}
|
|
cert, err = tmpl.New(ctx).WithArtifact(art).WithEnv(env).Apply(expand(cfg.Certificate, env))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
|
|
}
|
|
|
|
if cfg.Signature != "" {
|
|
result = append(result, &artifact.Artifact{
|
|
Type: artifact.Signature,
|
|
Name: name,
|
|
Path: env["signature"],
|
|
Extra: map[string]interface{}{
|
|
artifact.ExtraID: cfg.ID,
|
|
},
|
|
})
|
|
}
|
|
|
|
if cert != "" {
|
|
result = append(result, &artifact.Artifact{
|
|
Type: artifact.Certificate,
|
|
Name: cert,
|
|
Path: env["certificate"],
|
|
Extra: map[string]interface{}{
|
|
artifact.ExtraID: cfg.ID,
|
|
},
|
|
})
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func expand(s string, env map[string]string) string {
|
|
return os.Expand(s, func(key string) string {
|
|
return env[key]
|
|
})
|
|
}
|
|
|
|
func templateEnvS(ctx *context.Context, s []string) ([]string, error) {
|
|
var out []string
|
|
for _, s := range s {
|
|
ts, err := tmpl.New(ctx).WithEnvS(out).Apply(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, ts)
|
|
}
|
|
return out, nil
|
|
}
|