cmd/dist,release/dist: sign release tarballs with an ECDSA key (#8759)

Pass an optional PEM-encoded ECDSA key to `cmd/dist` to sign all built
tarballs. The signature is stored next to the tarball with a `.sig`
extension.

Tested this with an `openssl`-generated key pair and verified the
resulting signature.

Updates #8760

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
pull/8765/head
Andrew Lytvynov 1 year ago committed by GitHub
parent ed46442cb1
commit eef15b4ffc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

5
cmd/dist/dist.go vendored

@ -6,6 +6,7 @@ package main
import ( import (
"context" "context"
"crypto"
"errors" "errors"
"flag" "flag"
"log" "log"
@ -19,10 +20,10 @@ import (
var synologyPackageCenter bool var synologyPackageCenter bool
func getTargets() ([]dist.Target, error) { func getTargets(tgzSigner crypto.Signer) ([]dist.Target, error) {
var ret []dist.Target 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 // Synology packages can be built either for sideloading, or for
// distribution by Synology in their package center. When // distribution by Synology in their package center. When
// distributed through the package center, apps can request // distributed through the package center, apps can request

@ -6,6 +6,9 @@ package cli
import ( import (
"context" "context"
"crypto"
"crypto/x509"
"encoding/pem"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
@ -23,7 +26,7 @@ import (
// getTargets is a function that gets run in the Exec function of commands that // 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 // 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. // 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{ return &ffcli.Command{
Name: "dist", Name: "dist",
ShortUsage: "dist [flags] <command> [command flags]", ShortUsage: "dist [flags] <command> [command flags]",
@ -33,7 +36,7 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
{ {
Name: "list", Name: "list",
Exec: func(ctx context.Context, args []string) error { Exec: func(ctx context.Context, args []string) error {
targets, err := getTargets() targets, err := getTargets(nil)
if err != nil { if err != nil {
return err return err
} }
@ -49,7 +52,11 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
{ {
Name: "build", Name: "build",
Exec: func(ctx context.Context, args []string) error { 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 { if err != nil {
return err return err
} }
@ -61,6 +68,7 @@ func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
fs := flag.NewFlagSet("build", flag.ExitOnError) fs := flag.NewFlagSet("build", flag.ExitOnError)
fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write") fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write")
fs.BoolVar(&buildArgs.verbose, "verbose", false, "verbose logging") 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 return fs
})(), })(),
LongHelp: strings.TrimSpace(` LongHelp: strings.TrimSpace(`
@ -88,8 +96,9 @@ func runList(ctx context.Context, filters []string, targets []dist.Target) error
} }
var buildArgs struct { var buildArgs struct {
manifest string manifest string
verbose bool verbose bool
tgzSigningKey string
} }
func runBuild(ctx context.Context, filters []string, targets []dist.Target) error { 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)) fmt.Println("Done! Took", time.Since(st))
return nil 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)
}

@ -7,6 +7,9 @@ package unixpkgs
import ( import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"crypto"
"crypto/rand"
"crypto/sha512"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -22,6 +25,7 @@ import (
type tgzTarget struct { type tgzTarget struct {
filenameArch string // arch to use in filename instead of deriving from goenv["GOARCH"] filenameArch string // arch to use in filename instead of deriving from goenv["GOARCH"]
goenv map[string]string goenv map[string]string
signer crypto.Signer
} }
func (t *tgzTarget) arch() string { func (t *tgzTarget) arch() string {
@ -65,7 +69,11 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) {
return nil, err return nil, err
} }
defer f.Close() 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() defer gw.Close()
tw := tar.NewWriter(gw) tw := tar.NewWriter(gw)
defer tw.Close() defer tw.Close()
@ -146,7 +154,21 @@ func (t *tgzTarget) Build(b *dist.Build) ([]string, error) {
return nil, err 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 { type debTarget struct {

@ -4,6 +4,7 @@
package unixpkgs package unixpkgs
import ( import (
"crypto"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
@ -14,7 +15,7 @@ import (
_ "github.com/goreleaser/nfpm/rpm" _ "github.com/goreleaser/nfpm/rpm"
) )
func Targets() []dist.Target { func Targets(signer crypto.Signer) []dist.Target {
var ret []dist.Target var ret []dist.Target
for goosgoarch := range tarballs { for goosgoarch := range tarballs {
goos, goarch := splitGoosGoarch(goosgoarch) goos, goarch := splitGoosGoarch(goosgoarch)
@ -23,6 +24,7 @@ func Targets() []dist.Target {
"GOOS": goos, "GOOS": goos,
"GOARCH": goarch, "GOARCH": goarch,
}, },
signer: signer,
}) })
} }
for goosgoarch := range debs { for goosgoarch := range debs {
@ -53,6 +55,7 @@ func Targets() []dist.Target {
"GOARCH": "386", "GOARCH": "386",
"GO386": "softfloat", "GO386": "softfloat",
}, },
signer: signer,
}) })
sort.Slice(ret, func(i, j int) bool { sort.Slice(ret, func(i, j int) bool {

Loading…
Cancel
Save