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 }