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 }