github.com/triarius/goreleaser@v1.12.5/internal/pipe/sign/sign.go (about)

     1  package sign
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/caarlos0/log"
    13  	"github.com/triarius/goreleaser/internal/artifact"
    14  	"github.com/triarius/goreleaser/internal/gio"
    15  	"github.com/triarius/goreleaser/internal/ids"
    16  	"github.com/triarius/goreleaser/internal/logext"
    17  	"github.com/triarius/goreleaser/internal/pipe"
    18  	"github.com/triarius/goreleaser/internal/semerrgroup"
    19  	"github.com/triarius/goreleaser/internal/tmpl"
    20  	"github.com/triarius/goreleaser/pkg/config"
    21  	"github.com/triarius/goreleaser/pkg/context"
    22  )
    23  
    24  // Pipe that signs common artifacts.
    25  type Pipe struct{}
    26  
    27  func (Pipe) String() string                 { return "signing artifacts" }
    28  func (Pipe) Skip(ctx *context.Context) bool { return ctx.SkipSign || len(ctx.Config.Signs) == 0 }
    29  
    30  // Default sets the Pipes defaults.
    31  func (Pipe) Default(ctx *context.Context) error {
    32  	ids := ids.New("signs")
    33  	for i := range ctx.Config.Signs {
    34  		cfg := &ctx.Config.Signs[i]
    35  		if cfg.Cmd == "" {
    36  			cfg.Cmd = "gpg"
    37  		}
    38  		if cfg.Signature == "" {
    39  			cfg.Signature = "${artifact}.sig"
    40  		}
    41  		if len(cfg.Args) == 0 {
    42  			cfg.Args = []string{"--output", "$signature", "--detach-sig", "$artifact"}
    43  		}
    44  		if cfg.Artifacts == "" {
    45  			cfg.Artifacts = "none"
    46  		}
    47  		if cfg.ID == "" {
    48  			cfg.ID = "default"
    49  		}
    50  		ids.Inc(cfg.ID)
    51  	}
    52  	return ids.Validate()
    53  }
    54  
    55  // Run executes the Pipe.
    56  func (Pipe) Run(ctx *context.Context) error {
    57  	g := semerrgroup.New(ctx.Parallelism)
    58  	for i := range ctx.Config.Signs {
    59  		cfg := ctx.Config.Signs[i]
    60  		g.Go(func() error {
    61  			var filters []artifact.Filter
    62  			switch cfg.Artifacts {
    63  			case "checksum":
    64  				filters = append(filters, artifact.ByType(artifact.Checksum))
    65  				if len(cfg.IDs) > 0 {
    66  					log.Warn("when artifacts is `checksum`, `ids` has no effect. ignoring")
    67  				}
    68  			case "source":
    69  				filters = append(filters, artifact.ByType(artifact.UploadableSourceArchive))
    70  				if len(cfg.IDs) > 0 {
    71  					log.Warn("when artifacts is `source`, `ids` has no effect. ignoring")
    72  				}
    73  			case "all":
    74  				filters = append(filters, artifact.Or(
    75  					artifact.ByType(artifact.UploadableArchive),
    76  					artifact.ByType(artifact.UploadableBinary),
    77  					artifact.ByType(artifact.UploadableSourceArchive),
    78  					artifact.ByType(artifact.Checksum),
    79  					artifact.ByType(artifact.LinuxPackage),
    80  					artifact.ByType(artifact.SBOM),
    81  				))
    82  			case "archive":
    83  				filters = append(filters, artifact.ByType(artifact.UploadableArchive))
    84  			case "binary":
    85  				filters = append(filters, artifact.ByType(artifact.UploadableBinary))
    86  			case "sbom":
    87  				filters = append(filters, artifact.ByType(artifact.SBOM))
    88  			case "package":
    89  				filters = append(filters, artifact.ByType(artifact.LinuxPackage))
    90  			case "none": // TODO(caarlos0): this is not very useful, lets remove it.
    91  				return pipe.ErrSkipSignEnabled
    92  			default:
    93  				return fmt.Errorf("invalid list of artifacts to sign: %s", cfg.Artifacts)
    94  			}
    95  
    96  			if len(cfg.IDs) > 0 {
    97  				filters = append(filters, artifact.ByIDs(cfg.IDs...))
    98  			}
    99  			return sign(ctx, cfg, ctx.Artifacts.Filter(artifact.And(filters...)).List())
   100  		})
   101  	}
   102  	if err := g.Wait(); err != nil {
   103  		return err
   104  	}
   105  
   106  	return ctx.Artifacts.
   107  		Filter(artifact.ByType(artifact.Checksum)).
   108  		Visit(func(a *artifact.Artifact) error {
   109  			return a.Refresh()
   110  		})
   111  }
   112  
   113  func sign(ctx *context.Context, cfg config.Sign, artifacts []*artifact.Artifact) error {
   114  	for _, a := range artifacts {
   115  		if err := a.Refresh(); err != nil {
   116  			return err
   117  		}
   118  		artifacts, err := signone(ctx, cfg, a)
   119  		if err != nil {
   120  			return err
   121  		}
   122  		for _, artifact := range artifacts {
   123  			ctx.Artifacts.Add(artifact)
   124  		}
   125  	}
   126  	return nil
   127  }
   128  
   129  func relativeToDist(dist, f string) (string, error) {
   130  	af, err := filepath.Abs(f)
   131  	if err != nil {
   132  		return "", err
   133  	}
   134  	df, err := filepath.Abs(dist)
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  	if strings.HasPrefix(af, df) {
   139  		return f, nil
   140  	}
   141  	return filepath.Join(dist, f), nil
   142  }
   143  
   144  func tmplPath(ctx *context.Context, env map[string]string, s string) (string, error) {
   145  	result, err := tmpl.New(ctx).WithEnv(env).Apply(expand(s, env))
   146  	if err != nil || result == "" {
   147  		return "", err
   148  	}
   149  	return relativeToDist(ctx.Config.Dist, result)
   150  }
   151  
   152  func signone(ctx *context.Context, cfg config.Sign, art *artifact.Artifact) ([]*artifact.Artifact, error) {
   153  	env := ctx.Env.Copy()
   154  	env["artifactName"] = art.Name // shouldn't be used
   155  	env["artifact"] = art.Path
   156  	env["artifactID"] = art.ID()
   157  
   158  	tmplEnv, err := templateEnvS(ctx, cfg.Env)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   161  	}
   162  
   163  	for k, v := range context.ToEnv(tmplEnv) {
   164  		env[k] = v
   165  	}
   166  
   167  	name, err := tmplPath(ctx, env, cfg.Signature)
   168  	if err != nil {
   169  		return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   170  	}
   171  	env["signature"] = name
   172  
   173  	cert, err := tmplPath(ctx, env, cfg.Certificate)
   174  	if err != nil {
   175  		return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   176  	}
   177  	env["certificate"] = cert
   178  
   179  	// nolint:prealloc
   180  	var args []string
   181  	for _, a := range cfg.Args {
   182  		arg, err := tmpl.New(ctx).WithEnv(env).Apply(expand(a, env))
   183  		if err != nil {
   184  			return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   185  		}
   186  		args = append(args, arg)
   187  	}
   188  
   189  	var stdin io.Reader
   190  	if cfg.Stdin != nil {
   191  		s, err := tmpl.New(ctx).WithEnv(env).Apply(expand(*cfg.Stdin, env))
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  		stdin = strings.NewReader(s)
   196  	} else if cfg.StdinFile != "" {
   197  		f, err := os.Open(cfg.StdinFile)
   198  		if err != nil {
   199  			return nil, fmt.Errorf("sign failed: cannot open file %s: %w", cfg.StdinFile, err)
   200  		}
   201  		defer f.Close()
   202  
   203  		stdin = f
   204  	}
   205  
   206  	fields := log.Fields{"cmd": cfg.Cmd, "artifact": art.Name}
   207  	if name != "" {
   208  		fields["signature"] = name
   209  	}
   210  	if cert != "" {
   211  		fields["certificate"] = cert
   212  	}
   213  
   214  	// The GoASTScanner flags this as a security risk.
   215  	// However, this works as intended. The nosec annotation
   216  	// tells the scanner to ignore this.
   217  	// #nosec
   218  	cmd := exec.CommandContext(ctx, cfg.Cmd, args...)
   219  	var b bytes.Buffer
   220  	w := gio.Safe(&b)
   221  	cmd.Stderr = io.MultiWriter(logext.NewConditionalWriter(fields, logext.Error, cfg.Output), w)
   222  	cmd.Stdout = io.MultiWriter(logext.NewConditionalWriter(fields, logext.Info, cfg.Output), w)
   223  	if stdin != nil {
   224  		cmd.Stdin = stdin
   225  	}
   226  	cmd.Env = env.Strings()
   227  	log.WithFields(fields).Info("signing")
   228  	if err := cmd.Run(); err != nil {
   229  		return nil, fmt.Errorf("sign: %s failed: %w: %s", cfg.Cmd, err, b.String())
   230  	}
   231  
   232  	var result []*artifact.Artifact
   233  
   234  	// re-execute template results, using artifact desc as artifact so they eval to the actual needed file desc.
   235  	env["artifact"] = art.Name
   236  	name, _ = tmpl.New(ctx).WithEnv(env).Apply(expand(cfg.Signature, env))   // could never error as it passed the previous check
   237  	cert, _ = tmpl.New(ctx).WithEnv(env).Apply(expand(cfg.Certificate, env)) // could never error as it passed the previous check
   238  
   239  	if cfg.Signature != "" {
   240  		result = append(result, &artifact.Artifact{
   241  			Type: artifact.Signature,
   242  			Name: name,
   243  			Path: env["signature"],
   244  			Extra: map[string]interface{}{
   245  				artifact.ExtraID: cfg.ID,
   246  			},
   247  		})
   248  	}
   249  
   250  	if cert != "" {
   251  		result = append(result, &artifact.Artifact{
   252  			Type: artifact.Certificate,
   253  			Name: cert,
   254  			Path: env["certificate"],
   255  			Extra: map[string]interface{}{
   256  				artifact.ExtraID: cfg.ID,
   257  			},
   258  		})
   259  	}
   260  
   261  	return result, nil
   262  }
   263  
   264  func expand(s string, env map[string]string) string {
   265  	return os.Expand(s, func(key string) string {
   266  		return env[key]
   267  	})
   268  }
   269  
   270  func templateEnvS(ctx *context.Context, s []string) ([]string, error) {
   271  	var out []string
   272  	for _, s := range s {
   273  		ts, err := tmpl.New(ctx).WithEnvS(out).Apply(s)
   274  		if err != nil {
   275  			return nil, err
   276  		}
   277  		out = append(out, ts)
   278  	}
   279  	return out, nil
   280  }