diff --git a/go.mod b/go.mod index ada47b7..f8b1c3c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.3 require ( github.com/alecthomas/kong v1.4.0 + golang.org/x/crypto v0.9.0 golang.org/x/mod v0.22.0 pault.ag/go/debian v0.17.0 ) @@ -12,6 +13,5 @@ require ( github.com/kjk/lzma v0.0.0-20161016003348-3fd93898850d // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - golang.org/x/crypto v0.9.0 // indirect pault.ag/go/topsort v0.1.1 // indirect ) diff --git a/internal/publish/pgp.go b/internal/publish/pgp.go new file mode 100644 index 0000000..e01d0dc --- /dev/null +++ b/internal/publish/pgp.go @@ -0,0 +1,81 @@ +package publish + +import ( + "crypto" + "encoding/hex" + "fmt" + "io" + + _ "crypto/sha256" + + _ "golang.org/x/crypto/ripemd160" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" +) + +type signer struct { + keys []*packet.PrivateKey +} + +func newSigner(keychain io.Reader) (*signer, error) { + pr := packet.NewReader(keychain) + s := &signer{} + for { + pkt, err := pr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + switch key := pkt.(type) { + case *packet.PrivateKey: + if !key.IsSubkey && key.PublicKey.PublicKey != nil { + fmt.Println("private", hex.EncodeToString(key.PublicKey.Fingerprint[:]), key.IsSubkey) + s.keys = append(s.keys, key) + } + } + } + return s, nil +} + +type seekable interface { + io.Reader + io.Seeker +} + +func (s *signer) DetachSign(in seekable, out io.Writer) error { + if len(s.keys) == 0 { + return fmt.Errorf("no private keys found") + } + cfg := &packet.Config{ + DefaultHash: crypto.SHA256, + } + for _, key := range s.keys { + if _, err := in.Seek(0, io.SeekStart); err != nil { + return err + } + signer := &openpgp.Entity{PrivateKey: key} + if err := openpgp.DetachSign(out, signer, in, cfg); err != nil { + return err + } + } + return nil +} + +func (s *signer) ClearSign(in seekable, out io.Writer) error { + if len(s.keys) == 0 { + return fmt.Errorf("no private keys found") + } + + w, err := clearsign.EncodeMulti(out, s.keys, nil) + if err != nil { + return err + } + if _, err := io.Copy(w, in); err != nil { + return err + } + return w.Close() +} diff --git a/internal/publish/publish.go b/internal/publish/publish.go index fffff69..5b7d5f7 100644 --- a/internal/publish/publish.go +++ b/internal/publish/publish.go @@ -2,6 +2,7 @@ package publish import ( "fmt" + "os" "path/filepath" ) @@ -13,11 +14,6 @@ type CLI struct { } func (c *CLI) Run() error { - keyring, err := filepath.Abs(c.Keyring) - if err != nil { - return fmt.Errorf("publish: %w", err) - } - pkgs, err := scanPackages(c.Dists) if err != nil { return fmt.Errorf("publish: %w", err) @@ -33,11 +29,21 @@ func (c *CLI) Run() error { return fmt.Errorf("publish: globbing: %w", err) } + fd, err := os.Open(c.Keyring) + if err != nil { + return fmt.Errorf("publish: %w", err) + } + sign, err := newSigner(fd) + fd.Close() + if err != nil { + return fmt.Errorf("publish: %w", err) + } + for _, dist := range dists { if err := writeRelease(dist); err != nil { return fmt.Errorf("publish: %w", err) } - if err := signRelease(dist, keyring, c.SignUser); err != nil { + if err := signRelease(dist, sign); err != nil { return fmt.Errorf("publish: %w", err) } } diff --git a/internal/publish/release.go b/internal/publish/release.go index d23b3e6..1b53bd7 100644 --- a/internal/publish/release.go +++ b/internal/publish/release.go @@ -2,11 +2,9 @@ package publish import ( "compress/gzip" - "fmt" "io" "log/slog" "os" - "os/exec" "path/filepath" "slices" "strings" @@ -145,43 +143,44 @@ func writeRelease(dist string) error { return nil } -func signRelease(dist, keyring string, users []string) error { - opts := []string{ - "-o", filepath.Join(dist, "InRelease"), - "--yes", - "--no-default-keyring", - "--keyring", keyring, +func signRelease(dist string, s *signer) error { + in, err := os.Open(filepath.Join(dist, "Release")) + if err != nil { + return err } - for _, user := range users { - opts = append(opts, "-u", user) + defer in.Close() + + out, err := os.Create(filepath.Join(dist, "Release.gpg")) + if err != nil { + return err } - opts = append(opts, "--clear-sign", filepath.Join(dist, "Release")) - if bs, err := exec.Command("gpg", opts...).CombinedOutput(); err != nil { - return fmt.Errorf("Failed to sign release: %s", bs) + if err := s.DetachSign(in, out); err != nil { + return err } - if err := compress(filepath.Join(dist, "InRelease")); err != nil { + if err := out.Close(); err != nil { return err } - - opts = []string{ - "-o", filepath.Join(dist, "Release.gpg"), - "-a", - "--yes", - "--no-default-keyring", - "--keyring", keyring, + if err := compress(out.Name()); err != nil { + return err } - for _, user := range users { - opts = append(opts, "-u", user) + + if _, err := in.Seek(0, io.SeekStart); err != nil { + return err } - opts = append(opts, "--detach-sign", filepath.Join(dist, "Release")) - if bs, err := exec.Command("gpg", opts...).CombinedOutput(); err != nil { - return fmt.Errorf("Failed to sign release: %s", bs) + out, err = os.Create(filepath.Join(dist, "InRelease")) + if err != nil { + return err } - if err := compress(filepath.Join(dist, "Release.gpg")); err != nil { + if err := s.ClearSign(in, out); err != nil { + return err + } + if err := out.Close(); err != nil { + return err + } + if err := compress(out.Name()); err != nil { return err } - return nil }