Skip to content

Commit

Permalink
feat: add support for remote signing keys (#695)
Browse files Browse the repository at this point in the history
* feat: Add support for remote signing keys

When used as a library, `nfpm.PackageSignature.SignFn` can be set as an
alternative to `KeyFile`. This allows arbitrary signing key
implementations, like a remote signing server.

Updates tailscale/tailscale#1882

* Update rpm/rpm_test.go

Co-authored-by: Carlos Alexandro Becker <[email protected]>

---------

Co-authored-by: Carlos Alexandro Becker <[email protected]>
  • Loading branch information
awly and caarlos0 authored Aug 3, 2023
1 parent 577ae45 commit 24a43c5
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 6 deletions.
12 changes: 9 additions & 3 deletions apk/apk.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (*Apk) Package(info *nfpm.Info, apk io.Writer) (err error) {
return err
}

if info.APK.Signature.KeyFile == "" {
if info.APK.Signature.KeyFile == "" && info.APK.Signature.SignFn == nil {
return combineToApk(apk, &bufControl, &bufData)
}

Expand Down Expand Up @@ -267,8 +267,14 @@ var errNoKeyAddress = errors.New("key name not set and maintainer mail address e

func createSignatureBuilder(digest []byte, info *nfpm.Info) func(*tar.Writer) error {
return func(tw *tar.Writer) error {
signature, err := sign.RSASignSHA1Digest(digest,
info.APK.Signature.KeyFile, info.APK.Signature.KeyPassphrase)
var signature []byte
var err error
if signFn := info.APK.Signature.SignFn; signFn != nil {
signature, err = signFn(bytes.NewReader(digest))
} else {
signature, err = sign.RSASignSHA1Digest(digest,
info.APK.Signature.KeyFile, info.APK.Signature.KeyPassphrase)
}
if err != nil {
return err
}
Expand Down
27 changes: 27 additions & 0 deletions apk/apk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,33 @@ func TestSignatureError(t *testing.T) {
require.True(t, errors.As(err, &expectedError))
}

func TestSignatureCallback(t *testing.T) {
info := exampleInfo()
info.APK.Signature.SignFn = func(r io.Reader) ([]byte, error) {
digest, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return sign.RSASignSHA1Digest(digest, "../internal/sign/testdata/rsa.priv", "hunter2")
}
info.APK.Signature.KeyName = "testkey.rsa.pub"
err := nfpm.PrepareForPackager(info, "apk")
require.NoError(t, err)

digest := sha1.New().Sum(nil) // nolint:gosec

var signatureTarGz bytes.Buffer
tw := tar.NewWriter(&signatureTarGz)
require.NoError(t, createSignatureBuilder(digest, info)(tw))

signature := extractFromTar(t, signatureTarGz.Bytes(), ".SIGN.RSA.testkey.rsa.pub")
err = sign.RSAVerifySHA1Digest(digest, signature, "../internal/sign/testdata/rsa.pub")
require.NoError(t, err)

err = Default.Package(info, io.Discard)
require.NoError(t, err)
}

func TestDisableGlobbing(t *testing.T) {
info := exampleInfo()
info.DisableGlobbing = true
Expand Down
17 changes: 14 additions & 3 deletions deb/deb.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (d *Deb) Package(info *nfpm.Info, deb io.Writer) (err error) { // nolint: f
return fmt.Errorf("cannot add data.tar.gz to deb: %w", err)
}

if info.Deb.Signature.KeyFile != "" {
if info.Deb.Signature.KeyFile != "" || info.Deb.Signature.SignFn != nil {
sig, sigType, err := doSign(info, debianBinary, controlTarGz, dataTarball)
if err != nil {
return err
Expand Down Expand Up @@ -172,7 +172,12 @@ func dpkgSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) (
return nil, sigType, &nfpm.ErrSigningFailure{Err: err}
}

sig, err := sign.PGPClearSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
var sig []byte
if signFn := info.Deb.Signature.SignFn; signFn != nil {
sig, err = signFn(data)
} else {
sig, err = sign.PGPClearSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
}
if err != nil {
return nil, sigType, &nfpm.ErrSigningFailure{Err: err}
}
Expand All @@ -193,7 +198,13 @@ func debSign(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) ([
}
}

sig, err := sign.PGPArmoredDetachSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
var sig []byte
var err error
if signFn := info.Deb.Signature.SignFn; signFn != nil {
sig, err = signFn(data)
} else {
sig, err = sign.PGPArmoredDetachSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
}
if err != nil {
return nil, sigType, &nfpm.ErrSigningFailure{Err: err}
}
Expand Down
40 changes: 40 additions & 0 deletions deb/deb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,28 @@ func TestDebsigsSignatureError(t *testing.T) {
require.True(t, errors.As(err, &expectedError))
}

func TestDebsigsSignatureCallback(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.SignFn = func(r io.Reader) ([]byte, error) {
return sign.PGPArmoredDetachSignWithKeyID(r, "../internal/sign/testdata/privkey.asc", "hunter2", nil)
}

var deb bytes.Buffer
err := Default.Package(info, &deb)
require.NoError(t, err)

debBinary := extractFileFromAr(t, deb.Bytes(), "debian-binary")
controlTarGz := extractFileFromAr(t, deb.Bytes(), "control.tar.gz")
dataTarball := extractFileFromAr(t, deb.Bytes(), findDataTarball(t, deb.Bytes()))
signature := extractFileFromAr(t, deb.Bytes(), "_gpgorigin")

message := io.MultiReader(bytes.NewReader(debBinary),
bytes.NewReader(controlTarGz), bytes.NewReader(dataTarball))

err = sign.PGPVerify(message, signature, "../internal/sign/testdata/pubkey.asc")
require.NoError(t, err)
}

func TestDpkgSigSignature(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.KeyFile = "../internal/sign/testdata/privkey.asc"
Expand Down Expand Up @@ -990,6 +1012,24 @@ func TestDpkgSigSignatureError(t *testing.T) {
require.True(t, errors.As(err, &expectedError))
}

func TestDpkgSigSignatureCallback(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.SignFn = func(r io.Reader) ([]byte, error) {
return sign.PGPClearSignWithKeyID(r, "../internal/sign/testdata/privkey.asc", "hunter2", nil)
}
info.Deb.Signature.Method = "dpkg-sig"
info.Deb.Signature.Signer = "bob McRobert"

var deb bytes.Buffer
err := Default.Package(info, &deb)
require.NoError(t, err)

signature := extractFileFromAr(t, deb.Bytes(), "_gpgbuilder")

err = sign.PGPReadMessage(signature, "../internal/sign/testdata/pubkey.asc")
require.NoError(t, err)
}

func TestDisableGlobbing(t *testing.T) {
info := exampleInfo()
info.DisableGlobbing = true
Expand Down
7 changes: 7 additions & 0 deletions nfpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,13 @@ type PackageSignature struct {
KeyFile string `yaml:"key_file,omitempty" json:"key_file,omitempty" jsonschema:"title=key file,example=key.gpg"`
KeyID *string `yaml:"key_id,omitempty" json:"key_id,omitempty" jsonschema:"title=key id,example=bc8acdd415bd80b3"`
KeyPassphrase string `yaml:"-" json:"-"` // populated from environment variable
// SignFn, if set, will be called with the package-specific data to sign.
// For deb and rpm packages, data is the full package content.
// For apk packages, data is the SHA1 digest of control tgz.
//
// This allows for signing implementations other than using a local file
// (for example using a remote signer like KMS).
SignFn func(data io.Reader) ([]byte, error) `yaml:"-" json:"-"` // populated when used as a library
}

type RPMSignature struct {
Expand Down
5 changes: 5 additions & 0 deletions rpm/rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ func (*RPM) Package(info *nfpm.Info, w io.Writer) (err error) {
info.RPM.Signature.KeyID,
))
}
if signFn := info.RPM.Signature.SignFn; signFn != nil {
rpm.SetPGPSigner(func(data []byte) ([]byte, error) {
return signFn(bytes.NewReader(data))
})
}

if err = createFilesInsideRPM(info, rpm); err != nil {
return err
Expand Down
27 changes: 27 additions & 0 deletions rpm/rpm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/goreleaser/chglog"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"github.com/goreleaser/nfpm/v2/internal/sign"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -743,6 +744,32 @@ func TestRPMSignatureError(t *testing.T) {
require.True(t, errors.As(err, &expectedError))
}

func TestRPMSignatureCallback(t *testing.T) {
info := exampleInfo()
info.RPM.Signature.SignFn = func(r io.Reader) ([]byte, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return sign.PGPSignerWithKeyID("../internal/sign/testdata/privkey.asc", "hunter2", nil)(data)
}

pubkeyFileContent, err := os.ReadFile("../internal/sign/testdata/pubkey.gpg")
require.NoError(t, err)

keyring, err := openpgp.ReadKeyRing(bytes.NewReader(pubkeyFileContent))
require.NoError(t, err)
require.NotNil(t, keyring, "cannot verify sigs with an empty keyring")

var rpmBuffer bytes.Buffer
err = Default.Package(info, &rpmBuffer)
require.NoError(t, err)

_, sigs, err := rpmutils.Verify(bytes.NewReader(rpmBuffer.Bytes()), keyring)
require.NoError(t, err)
require.Len(t, sigs, 2)
}

func TestRPMGhostFiles(t *testing.T) {
filename := "/usr/lib/casper.a"

Expand Down

0 comments on commit 24a43c5

Please sign in to comment.