diff --git a/cmd/dist/dist.go b/cmd/dist/dist.go index c184e6700..82f474193 100644 --- a/cmd/dist/dist.go +++ b/cmd/dist/dist.go @@ -6,6 +6,7 @@ package main import ( "context" + "crypto" "errors" "flag" "log" @@ -19,10 +20,10 @@ import ( var synologyPackageCenter bool -func getTargets() ([]dist.Target, error) { +func getTargets(tgzSigner crypto.Signer) ([]dist.Target, error) { var ret []dist.Target - ret = append(ret, unixpkgs.Targets()...) + ret = append(ret, unixpkgs.Targets(tgzSigner)...) // Synology packages can be built either for sideloading, or for // distribution by Synology in their package center. When // distributed through the package center, apps can request diff --git a/release/dist/cli/cli.go b/release/dist/cli/cli.go index bd1ecf856..b1d4ff9f5 100644 --- a/release/dist/cli/cli.go +++ b/release/dist/cli/cli.go @@ -6,6 +6,9 @@ package cli import ( "context" + "crypto" + "crypto/x509" + "encoding/pem" "errors" "flag" "fmt" @@ -23,7 +26,7 @@ import ( // getTargets is a function that gets run in the Exec function of commands that // need to know the target list. Its execution is deferred in this way to allow // customization of command FlagSets with flags that influence the target list. -func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command { +func CLI(getTargets func(tgzSigner crypto.Signer) ([]dist.Target, error)) *ffcli.Command { return &ffcli.Command{ Name: "dist", ShortUsage: "dist [flags] [command flags]", @@ -33,7 +36,7 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command { { Name: "list", Exec: func(ctx context.Context, args []string) error { - targets, err := getTargets() + targets, err := getTargets(nil) if err != nil { return err } @@ -49,7 +52,11 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command { { Name: "build", Exec: func(ctx context.Context, args []string) error { - targets, err := getTargets() + tgzSigner, err := parseSigningKey(buildArgs.tgzSigningKey) + if err != nil { + return err + } + targets, err := getTargets(tgzSigner) if err != nil { return err } @@ -61,6 +68,7 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command { fs := flag.NewFlagSet("build", flag.ExitOnError) fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write") fs.BoolVar(&buildArgs.verbose, "verbose", false, "verbose logging") + fs.StringVar(&buildArgs.tgzSigningKey, "tgz-signing-key", "", "path to private signing key for release tarballs") return fs })(), LongHelp: strings.TrimSpace(` @@ -88,8 +96,9 @@ func runList(ctx context.Context, filters []string, targets []dist.Target) error } var buildArgs struct { - manifest string - verbose bool + manifest string + verbose bool + tgzSigningKey string } func runBuild(ctx context.Context, filters []string, targets []dist.Target) error { @@ -142,3 +151,21 @@ func runBuild(ctx context.Context, filters []string, targets []dist.Target) erro fmt.Println("Done! Took", time.Since(st)) return nil } + +func parseSigningKey(path string) (crypto.Signer, error) { + if path == "" { + return nil, nil + } + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + b, rest := pem.Decode(raw) + if b == nil { + return nil, fmt.Errorf("failed to decode PEM data in %q", path) + } + if len(rest) > 0 { + return nil, fmt.Errorf("trailing data in %q, please check that the key file was not corrupted", path) + } + return x509.ParseECPrivateKey(b.Bytes) +} diff --git a/release/dist/unixpkgs/pkgs.go b/release/dist/unixpkgs/pkgs.go index f8a32fc3d..2ca8e575a 100644 --- a/release/dist/unixpkgs/pkgs.go +++ b/release/dist/unixpkgs/pkgs.go @@ -7,6 +7,9 @@ package unixpkgs import ( "archive/tar" "compress/gzip" + "crypto" + "crypto/rand" + "crypto/sha512" "errors" "fmt" "io" @@ -22,6 +25,7 @@ import ( type tgzTarget struct { filenameArch string // arch to use in filename instead of deriving from goenv["GOARCH"] goenv map[string]string + signer crypto.Signer } func (t *tgzTarget) arch() string { @@ -65,7 +69,11 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { return nil, err } defer f.Close() - gw := gzip.NewWriter(f) + // Hash the final output we're writing to the file, after tar and gzip + // writers did their thing. + h := sha512.New() + hw := io.MultiWriter(f, h) + gw := gzip.NewWriter(hw) defer gw.Close() tw := tar.NewWriter(gw) defer tw.Close() @@ -146,7 +154,21 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) { return nil, err } - return []string{filename}, nil + files := []string{filename} + + if t.signer != nil { + sig, err := t.signer.Sign(rand.Reader, h.Sum(nil), crypto.SHA512) + if err != nil { + return nil, err + } + sigFilename := out + ".sig" + if err := os.WriteFile(sigFilename, sig, 0644); err != nil { + return nil, err + } + files = append(files, filename+".sig") + } + + return files, nil } type debTarget struct { diff --git a/release/dist/unixpkgs/targets.go b/release/dist/unixpkgs/targets.go index aad79d75e..868db3777 100644 --- a/release/dist/unixpkgs/targets.go +++ b/release/dist/unixpkgs/targets.go @@ -4,6 +4,7 @@ package unixpkgs import ( + "crypto" "fmt" "sort" "strings" @@ -14,7 +15,7 @@ import ( _ "github.com/goreleaser/nfpm/rpm" ) -func Targets() []dist.Target { +func Targets(signer crypto.Signer) []dist.Target { var ret []dist.Target for goosgoarch := range tarballs { goos, goarch := splitGoosGoarch(goosgoarch) @@ -23,6 +24,7 @@ func Targets() []dist.Target { "GOOS": goos, "GOARCH": goarch, }, + signer: signer, }) } for goosgoarch := range debs { @@ -53,6 +55,7 @@ func Targets() []dist.Target { "GOARCH": "386", "GO386": "softfloat", }, + signer: signer, }) sort.Slice(ret, func(i, j int) bool {