github.com/goreleaser/goreleaser@v1.25.1/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/goreleaser/goreleaser/internal/artifact"
    14  	"github.com/goreleaser/goreleaser/internal/gio"
    15  	"github.com/goreleaser/goreleaser/internal/git"
    16  	"github.com/goreleaser/goreleaser/internal/ids"
    17  	"github.com/goreleaser/goreleaser/internal/logext"
    18  	"github.com/goreleaser/goreleaser/internal/pipe"
    19  	"github.com/goreleaser/goreleaser/internal/semerrgroup"
    20  	"github.com/goreleaser/goreleaser/internal/skips"
    21  	"github.com/goreleaser/goreleaser/internal/tmpl"
    22  	"github.com/goreleaser/goreleaser/pkg/config"
    23  	"github.com/goreleaser/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.Refresh()
   128  }
   129  
   130  func sign(ctx *context.Context, cfg config.Sign, artifacts []*artifact.Artifact) error {
   131  	if len(artifacts) == 0 {
   132  		log.Warn("no artifacts matching the given filters found")
   133  		return nil
   134  	}
   135  	for _, a := range artifacts {
   136  		if err := a.Refresh(); err != nil {
   137  			return err
   138  		}
   139  		artifacts, err := signone(ctx, cfg, a)
   140  		if err != nil {
   141  			return err
   142  		}
   143  		for _, artifact := range artifacts {
   144  			ctx.Artifacts.Add(artifact)
   145  		}
   146  	}
   147  	return nil
   148  }
   149  
   150  func relativeToDist(dist, f string) (string, error) {
   151  	af, err := filepath.Abs(f)
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	df, err := filepath.Abs(dist)
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  	if strings.HasPrefix(af, df) {
   160  		return f, nil
   161  	}
   162  	return filepath.Join(dist, f), nil
   163  }
   164  
   165  func tmplPath(ctx *context.Context, env map[string]string, s string) (string, error) {
   166  	result, err := tmpl.New(ctx).WithEnv(env).Apply(expand(s, env))
   167  	if err != nil || result == "" {
   168  		return "", err
   169  	}
   170  	return relativeToDist(ctx.Config.Dist, result)
   171  }
   172  
   173  func signone(ctx *context.Context, cfg config.Sign, art *artifact.Artifact) ([]*artifact.Artifact, error) {
   174  	env := ctx.Env.Copy()
   175  	env["artifactName"] = art.Name // shouldn't be used
   176  	env["artifact"] = art.Path
   177  	env["artifactID"] = art.ID()
   178  	env["digest"] = artifact.ExtraOr(*art, artifact.ExtraDigest, "")
   179  
   180  	tmplEnv, err := templateEnvS(ctx, cfg.Env)
   181  	if err != nil {
   182  		return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   183  	}
   184  
   185  	for k, v := range context.ToEnv(tmplEnv) {
   186  		env[k] = v
   187  	}
   188  
   189  	name, err := tmplPath(ctx, env, cfg.Signature)
   190  	if err != nil {
   191  		return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   192  	}
   193  	env["signature"] = name
   194  
   195  	cert, err := tmplPath(ctx, env, cfg.Certificate)
   196  	if err != nil {
   197  		return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   198  	}
   199  	env["certificate"] = cert
   200  
   201  	// nolint:prealloc
   202  	var args []string
   203  	for _, a := range cfg.Args {
   204  		arg, err := tmpl.New(ctx).WithEnv(env).Apply(expand(a, env))
   205  		if err != nil {
   206  			return nil, fmt.Errorf("sign failed: %s: %w", art.Name, err)
   207  		}
   208  		args = append(args, arg)
   209  	}
   210  
   211  	var stdin io.Reader
   212  	if cfg.Stdin != nil {
   213  		s, err := tmpl.New(ctx).WithEnv(env).Apply(expand(*cfg.Stdin, env))
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		stdin = strings.NewReader(s)
   218  	} else if cfg.StdinFile != "" {
   219  		f, err := os.Open(cfg.StdinFile)
   220  		if err != nil {
   221  			return nil, fmt.Errorf("sign failed: cannot open file %s: %w", cfg.StdinFile, err)
   222  		}
   223  		defer f.Close()
   224  
   225  		stdin = f
   226  	}
   227  
   228  	log := log.WithField("cmd", cfg.Cmd).WithField("artifact", art.Name)
   229  	if name != "" {
   230  		log = log.WithField("signature", name)
   231  	}
   232  	if cert != "" {
   233  		log = log.WithField("certificate", cert)
   234  	}
   235  
   236  	// The GoASTScanner flags this as a security risk.
   237  	// However, this works as intended. The nosec annotation
   238  	// tells the scanner to ignore this.
   239  	// #nosec
   240  	cmd := exec.CommandContext(ctx, cfg.Cmd, args...)
   241  	var b bytes.Buffer
   242  	w := gio.Safe(&b)
   243  	cmd.Stderr = io.MultiWriter(logext.NewConditionalWriter(cfg.Output), w)
   244  	cmd.Stdout = io.MultiWriter(logext.NewConditionalWriter(cfg.Output), w)
   245  	if stdin != nil {
   246  		cmd.Stdin = stdin
   247  	}
   248  	cmd.Env = env.Strings()
   249  	log.Info("signing")
   250  	if err := cmd.Run(); err != nil {
   251  		return nil, fmt.Errorf("sign: %s failed: %w: %s", cfg.Cmd, err, b.String())
   252  	}
   253  
   254  	var result []*artifact.Artifact
   255  
   256  	// re-execute template results, using artifact desc as artifact so they eval to the actual needed file desc.
   257  	env["artifact"] = art.Name
   258  	name, _ = tmpl.New(ctx).WithEnv(env).Apply(expand(cfg.Signature, env))   // could never error as it passed the previous check
   259  	cert, _ = tmpl.New(ctx).WithEnv(env).Apply(expand(cfg.Certificate, env)) // could never error as it passed the previous check
   260  
   261  	if cfg.Signature != "" {
   262  		result = append(result, &artifact.Artifact{
   263  			Type: artifact.Signature,
   264  			Name: name,
   265  			Path: env["signature"],
   266  			Extra: map[string]interface{}{
   267  				artifact.ExtraID: cfg.ID,
   268  			},
   269  		})
   270  	}
   271  
   272  	if cert != "" {
   273  		result = append(result, &artifact.Artifact{
   274  			Type: artifact.Certificate,
   275  			Name: cert,
   276  			Path: env["certificate"],
   277  			Extra: map[string]interface{}{
   278  				artifact.ExtraID: cfg.ID,
   279  			},
   280  		})
   281  	}
   282  
   283  	return result, nil
   284  }
   285  
   286  func expand(s string, env map[string]string) string {
   287  	return os.Expand(s, func(key string) string {
   288  		return env[key]
   289  	})
   290  }
   291  
   292  func templateEnvS(ctx *context.Context, s []string) ([]string, error) {
   293  	var out []string
   294  	for _, s := range s {
   295  		ts, err := tmpl.New(ctx).WithEnvS(out).Apply(s)
   296  		if err != nil {
   297  			return nil, err
   298  		}
   299  		out = append(out, ts)
   300  	}
   301  	return out, nil
   302  }