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