From 24a43c5ad7cfc549e8a4ec930521a97a30f26cc8 Mon Sep 17 00:00:00 2001 From: Andrew Lytvynov Date: Thu, 3 Aug 2023 05:36:30 -0700 Subject: [PATCH] feat: add support for remote signing keys (#695) * 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 https://github.com/tailscale/tailscale/issues/1882 * Update rpm/rpm_test.go Co-authored-by: Carlos Alexandro Becker --------- Co-authored-by: Carlos Alexandro Becker --- apk/apk.go | 12 +++++++++--- apk/apk_test.go | 27 +++++++++++++++++++++++++++ deb/deb.go | 17 ++++++++++++++--- deb/deb_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ nfpm.go | 7 +++++++ rpm/rpm.go | 5 +++++ rpm/rpm_test.go | 27 +++++++++++++++++++++++++++ 7 files changed, 129 insertions(+), 6 deletions(-) diff --git a/apk/apk.go b/apk/apk.go index 39c17987..a7c9c816 100644 --- a/apk/apk.go +++ b/apk/apk.go @@ -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) } @@ -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 } diff --git a/apk/apk_test.go b/apk/apk_test.go index 56043f16..f94bbe20 100644 --- a/apk/apk_test.go +++ b/apk/apk_test.go @@ -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 diff --git a/deb/deb.go b/deb/deb.go index 7ee344bd..625ad9f5 100644 --- a/deb/deb.go +++ b/deb/deb.go @@ -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 @@ -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} } @@ -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} } diff --git a/deb/deb_test.go b/deb/deb_test.go index cc6acda9..6769deb7 100644 --- a/deb/deb_test.go +++ b/deb/deb_test.go @@ -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" @@ -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 diff --git a/nfpm.go b/nfpm.go index caba9f2a..f99c6c92 100644 --- a/nfpm.go +++ b/nfpm.go @@ -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 { diff --git a/rpm/rpm.go b/rpm/rpm.go index 27570d60..97953bff 100644 --- a/rpm/rpm.go +++ b/rpm/rpm.go @@ -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 diff --git a/rpm/rpm_test.go b/rpm/rpm_test.go index 3c255aae..314da90c 100644 --- a/rpm/rpm_test.go +++ b/rpm/rpm_test.go @@ -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" ) @@ -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"