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 }