feat: rpm.signature.format

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker
2023-06-25 13:34:01 +00:00
parent ce5dbfc893
commit 9c46a58fff
9 changed files with 173 additions and 136 deletions
+12 -1
View File
@@ -261,8 +261,14 @@ func TestDebSpecific(t *testing.T) {
func TestRPMSign(t *testing.T) {
t.Parallel()
for _, os := range []string{"centos9", "centos8", "fedora34", "fedora36"} {
for os, sigformat := range map[string]string{
"centos9": "legacy",
"centos8": "legacy",
"fedora34": "legacy",
"fedora36": "modern",
} {
os := os
sigformat := sigformat
t.Run(fmt.Sprintf("rpm/amd64/sign/%s", os), func(t *testing.T) {
t.Parallel()
target := "signed"
@@ -270,6 +276,7 @@ func TestRPMSign(t *testing.T) {
Name: fmt.Sprintf("sign_%s_amd64", os),
Conf: "core.signed.yaml",
Format: "rpm",
Env: map[string]string{"TEST_RPM_SIGN_FORMAT": sigformat},
Docker: dockerParams{
File: fmt.Sprintf("rpm_%s.dockerfile", os),
Target: target,
@@ -315,6 +322,7 @@ type acceptParms struct {
Conf string
Format string
Docker dockerParams
Env map[string]string
}
type dockerParams struct {
@@ -342,6 +350,9 @@ func accept(t *testing.T, params acceptParms) {
case "SEMVER":
return "v1.0.0-0.1.b1+git.abcdefgh"
default:
if v, ok := params.Env[s]; ok {
return v
}
return os.Getenv(s)
}
}
+100
View File
@@ -0,0 +1,100 @@
package sign
import (
"bytes"
"crypto"
"fmt"
"os"
"github.com/goreleaser/nfpm/v2"
gopenpgp "golang.org/x/crypto/openpgp"
gopacket "golang.org/x/crypto/openpgp/packet"
)
// LegacyPGPSigner returns a PGP signer that creates a detached non-ASCII-armored
// signature and is compatible with rpmpack's signature API.
func LegacyPGPSigner(keyFile, passphrase string) func([]byte) ([]byte, error) {
return func(data []byte) ([]byte, error) {
key, err := goReadSigningKey(keyFile, passphrase)
if err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
var signature bytes.Buffer
if err := gopenpgp.DetachSign(
&signature,
key,
bytes.NewReader(data),
&gopacket.Config{
DefaultHash: crypto.SHA256,
},
); err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
return signature.Bytes(), nil
}
}
func goReadSigningKey(keyFile, passphrase string) (*gopenpgp.Entity, error) {
fileContent, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("reading PGP key file: %w", err)
}
var entityList gopenpgp.EntityList
if isASCII(fileContent) {
entityList, err = gopenpgp.ReadArmoredKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, fmt.Errorf("decoding armored PGP keyring: %w", err)
}
} else {
entityList, err = gopenpgp.ReadKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, fmt.Errorf("decoding PGP keyring: %w", err)
}
}
var key *gopenpgp.Entity
for _, candidate := range entityList {
if candidate.PrivateKey == nil {
continue
}
if !candidate.PrivateKey.CanSign() {
continue
}
if key != nil {
return nil, errMoreThanOneKey
}
key = candidate
}
if key == nil {
return nil, errNoKeys
}
if key.PrivateKey.Encrypted {
if passphrase == "" {
return nil, errNoPassword
}
pw := []byte(passphrase)
err = key.PrivateKey.Decrypt(pw)
if err != nil {
return nil, fmt.Errorf("decrypt secret signing key: %w", err)
}
for _, sub := range key.Subkeys {
if sub.PrivateKey != nil {
if err := sub.PrivateKey.Decrypt(pw); err != nil {
return nil, fmt.Errorf("gopenpgp: error in unlocking sub key: %w", err)
}
}
}
}
return key, nil
}
+1 -91
View File
@@ -14,39 +14,11 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/goreleaser/nfpm/v2"
gopenpgp "golang.org/x/crypto/openpgp"
gopacket "golang.org/x/crypto/openpgp/packet"
)
// PGPSigner returns a PGP signer that creates a detached non-ASCII-armored
// signature and is compatible with rpmpack's signature API.
func PGPSigner(keyFile, passphrase string) func([]byte) ([]byte, error) {
return func(data []byte) ([]byte, error) {
key, err := goReadSigningKey(keyFile, passphrase)
if err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
var signature bytes.Buffer
if err := gopenpgp.DetachSign(
&signature,
key,
bytes.NewReader(data),
&gopacket.Config{
DefaultHash: crypto.SHA256,
},
); err != nil {
return nil, &nfpm.ErrSigningFailure{Err: err}
}
return signature.Bytes(), nil
}
}
// PGPSignerWithKeyID returns a PGP signer that creates a detached non-ASCII-armored
// signature and is compatible with rpmpack's signature API.
func PGPSignerWithKeyID(keyFile, passphrase string, hexKeyID *string) func([]byte) ([]byte, error) {
func PGPSigner(keyFile, passphrase string, hexKeyID *string) func([]byte) ([]byte, error) {
return func(data []byte) ([]byte, error) {
keyID, err := parseKeyID(hexKeyID)
if err != nil {
@@ -215,68 +187,6 @@ var (
errNoPassword = errors.New("key is encrypted but no passphrase was provided")
)
func goReadSigningKey(keyFile, passphrase string) (*gopenpgp.Entity, error) {
fileContent, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("reading PGP key file: %w", err)
}
var entityList gopenpgp.EntityList
if isASCII(fileContent) {
entityList, err = gopenpgp.ReadArmoredKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, fmt.Errorf("decoding armored PGP keyring: %w", err)
}
} else {
entityList, err = gopenpgp.ReadKeyRing(bytes.NewReader(fileContent))
if err != nil {
return nil, fmt.Errorf("decoding PGP keyring: %w", err)
}
}
var key *gopenpgp.Entity
for _, candidate := range entityList {
if candidate.PrivateKey == nil {
continue
}
if !candidate.PrivateKey.CanSign() {
continue
}
if key != nil {
return nil, errMoreThanOneKey
}
key = candidate
}
if key == nil {
return nil, errNoKeys
}
if key.PrivateKey.Encrypted {
if passphrase == "" {
return nil, errNoPassword
}
pw := []byte(passphrase)
err = key.PrivateKey.Decrypt(pw)
if err != nil {
return nil, fmt.Errorf("decrypt secret signing key: %w", err)
}
for _, sub := range key.Subkeys {
if sub.PrivateKey != nil {
if err := sub.PrivateKey.Decrypt(pw); err != nil {
return nil, fmt.Errorf("gopenpgp: error in unlocking sub key: %w", err)
}
}
}
}
return key, nil
}
func readSigningKey(keyFile, passphrase string) (*openpgp.Entity, error) {
fileContent, err := os.ReadFile(keyFile)
if err != nil {
+10 -2
View File
@@ -41,7 +41,7 @@ func TestPGPSignerAndVerify(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
armoredPublicKey := fmt.Sprintf("%s.asc", testCase.pubKeyFile)
gpgPublicKey := fmt.Sprintf("%s.gpg", testCase.pubKeyFile)
sig, err := PGPSignerWithKeyID(testCase.privKeyFile, testCase.pass, testCase.keyID)(data)
sig, err := PGPSigner(testCase.privKeyFile, testCase.pass, testCase.keyID)(data)
require.NoError(t, err)
err = PGPVerify(bytes.NewReader(data), sig, armoredPublicKey)
@@ -102,7 +102,15 @@ func TestArmoredDetachSignAndVerify(t *testing.T) {
}
func TestPGPSignerError(t *testing.T) {
_, err := PGPSigner("/does/not/exist", "")([]byte("data"))
_, err := PGPSigner("/does/not/exist", "", nil)([]byte("data"))
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
require.True(t, errors.As(err, &expectedError))
}
func TestLegacyPGPSignerError(t *testing.T) {
_, err := LegacyPGPSigner("/does/not/exist", "")([]byte("data"))
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
+34 -35
View File
@@ -173,11 +173,11 @@ func (c *Config) expandEnvVarsStringSlice(items []string) []string {
func (c *Config) expandEnvVars() {
// Version related fields
c.Info.Release = os.Expand(c.Info.Release, c.envMappingFunc)
c.Info.Version = os.Expand(c.Info.Version, c.envMappingFunc)
c.Info.Prerelease = os.Expand(c.Info.Prerelease, c.envMappingFunc)
c.Info.Platform = os.Expand(c.Info.Platform, c.envMappingFunc)
c.Info.Arch = os.Expand(c.Info.Arch, c.envMappingFunc)
c.Release = os.Expand(c.Release, c.envMappingFunc)
c.Version = os.Expand(c.Version, c.envMappingFunc)
c.Prerelease = os.Expand(c.Prerelease, c.envMappingFunc)
c.Platform = os.Expand(c.Platform, c.envMappingFunc)
c.Arch = os.Expand(c.Arch, c.envMappingFunc)
for or := range c.Overrides {
c.Overrides[or].Conflicts = c.expandEnvVarsStringSlice(c.Overrides[or].Conflicts)
c.Overrides[or].Depends = c.expandEnvVarsStringSlice(c.Overrides[or].Depends)
@@ -186,53 +186,51 @@ func (c *Config) expandEnvVars() {
c.Overrides[or].Provides = c.expandEnvVarsStringSlice(c.Overrides[or].Provides)
c.Overrides[or].Suggests = c.expandEnvVarsStringSlice(c.Overrides[or].Suggests)
}
c.Info.Conflicts = c.expandEnvVarsStringSlice(c.Info.Conflicts)
c.Info.Depends = c.expandEnvVarsStringSlice(c.Info.Depends)
c.Info.Replaces = c.expandEnvVarsStringSlice(c.Info.Replaces)
c.Info.Recommends = c.expandEnvVarsStringSlice(c.Info.Recommends)
c.Info.Provides = c.expandEnvVarsStringSlice(c.Info.Provides)
c.Info.Suggests = c.expandEnvVarsStringSlice(c.Info.Suggests)
c.Conflicts = c.expandEnvVarsStringSlice(c.Conflicts)
c.Depends = c.expandEnvVarsStringSlice(c.Depends)
c.Replaces = c.expandEnvVarsStringSlice(c.Replaces)
c.Recommends = c.expandEnvVarsStringSlice(c.Recommends)
c.Provides = c.expandEnvVarsStringSlice(c.Provides)
c.Suggests = c.expandEnvVarsStringSlice(c.Suggests)
// Maintainer and vendor fields
c.Info.Name = os.Expand(c.Info.Name, c.envMappingFunc)
c.Info.Maintainer = os.Expand(c.Info.Maintainer, c.envMappingFunc)
c.Info.Vendor = os.Expand(c.Info.Vendor, c.envMappingFunc)
c.Name = os.Expand(c.Name, c.envMappingFunc)
c.Maintainer = os.Expand(c.Maintainer, c.envMappingFunc)
c.Vendor = os.Expand(c.Vendor, c.envMappingFunc)
// Package signing related fields
c.Info.Deb.Signature.KeyFile = os.Expand(c.Deb.Signature.KeyFile, c.envMappingFunc)
c.Info.RPM.Signature.KeyFile = os.Expand(c.RPM.Signature.KeyFile, c.envMappingFunc)
c.Info.APK.Signature.KeyFile = os.Expand(c.APK.Signature.KeyFile, c.envMappingFunc)
c.Info.Deb.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.Deb.Signature.KeyID), c.envMappingFunc))
c.Info.RPM.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.RPM.Signature.KeyID), c.envMappingFunc))
c.Info.APK.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.APK.Signature.KeyID), c.envMappingFunc))
c.Deb.Signature.KeyFile = os.Expand(c.Deb.Signature.KeyFile, c.envMappingFunc)
c.RPM.Signature.KeyFile = os.Expand(c.RPM.Signature.KeyFile, c.envMappingFunc)
c.APK.Signature.KeyFile = os.Expand(c.APK.Signature.KeyFile, c.envMappingFunc)
c.Deb.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.Deb.Signature.KeyID), c.envMappingFunc))
c.RPM.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.RPM.Signature.KeyID), c.envMappingFunc))
c.APK.Signature.KeyID = pointer.ToString(os.Expand(pointer.GetString(c.APK.Signature.KeyID), c.envMappingFunc))
// Package signing passphrase
generalPassphrase := os.Expand("$NFPM_PASSPHRASE", c.envMappingFunc)
c.Info.Deb.Signature.KeyPassphrase = generalPassphrase
c.Info.RPM.Signature.KeyPassphrase = generalPassphrase
c.Info.APK.Signature.KeyPassphrase = generalPassphrase
c.Deb.Signature.KeyPassphrase = generalPassphrase
c.RPM.Signature.KeyPassphrase = generalPassphrase
c.APK.Signature.KeyPassphrase = generalPassphrase
debPassphrase := os.Expand("$NFPM_DEB_PASSPHRASE", c.envMappingFunc)
if debPassphrase != "" {
c.Info.Deb.Signature.KeyPassphrase = debPassphrase
if pwd := os.Expand("$NFPM_DEB_PASSPHRASE", c.envMappingFunc); pwd != "" {
c.Deb.Signature.KeyPassphrase = pwd
}
rpmPassphrase := os.Expand("$NFPM_RPM_PASSPHRASE", c.envMappingFunc)
if rpmPassphrase != "" {
c.Info.RPM.Signature.KeyPassphrase = rpmPassphrase
if pwd := os.Expand("$NFPM_RPM_PASSPHRASE", c.envMappingFunc); pwd != "" {
c.RPM.Signature.KeyPassphrase = pwd
}
apkPassphrase := os.Expand("$NFPM_APK_PASSPHRASE", c.envMappingFunc)
if apkPassphrase != "" {
c.Info.APK.Signature.KeyPassphrase = apkPassphrase
if pwd := os.Expand("$NFPM_APK_PASSPHRASE", c.envMappingFunc); pwd != "" {
c.APK.Signature.KeyPassphrase = pwd
}
// RPM specific
c.Info.RPM.Packager = os.Expand(c.RPM.Packager, c.envMappingFunc)
c.RPM.Packager = os.Expand(c.RPM.Packager, c.envMappingFunc)
c.RPM.Signature.Format = os.Expand(c.RPM.Signature.Format, c.envMappingFunc)
// Deb specific
for k, v := range c.Info.Deb.Fields {
c.Info.Deb.Fields[k] = os.Expand(v, c.envMappingFunc)
for k, v := range c.Deb.Fields {
c.Deb.Fields[k] = os.Expand(v, c.envMappingFunc)
}
}
@@ -349,6 +347,7 @@ 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
Format string `yaml:"format,omitempty" json:"format,omitempty" jsonschema:"enum=legacy,enum=modern"`
}
type RPMSignature struct {
+5 -4
View File
@@ -121,13 +121,14 @@ func (*RPM) Package(info *nfpm.Info, w io.Writer) (err error) {
}
if info.RPM.Signature.KeyFile != "" {
if info.RPM.Signature.KeyID == nil || strings.TrimSpace(*info.RPM.Signature.KeyID) == "" {
rpm.SetPGPSigner(sign.PGPSigner(
switch info.RPM.Signature.Format {
case "legacy":
rpm.SetPGPSigner(sign.LegacyPGPSigner(
info.RPM.Signature.KeyFile,
info.RPM.Signature.KeyPassphrase,
))
} else {
rpm.SetPGPSigner(sign.PGPSignerWithKeyID(
default:
rpm.SetPGPSigner(sign.PGPSigner(
info.RPM.Signature.KeyFile,
info.RPM.Signature.KeyPassphrase,
info.RPM.Signature.KeyID,
+2 -1
View File
@@ -15,7 +15,8 @@ deb:
rpm:
signature:
key_file: ./internal/sign/testdata/privkey_unprotected.asc
# key_id: bc8acdd415bd80b3
key_id: bc8acdd415bd80b3
format: ${TEST_RPM_SIGN_FORMAT}
apk:
signature:
key_file: ./internal/sign/testdata/rsa_unprotected.priv
+2 -2
View File
@@ -79,10 +79,10 @@ FROM test_base AS signed
COPY keys/pubkey.asc /tmp/pubkey.asc
RUN rpm --import /tmp/pubkey.asc
RUN rpm -q gpg-pubkey --qf '%{NAME}-%{VERSION}-%{RELEASE}\t%{SUMMARY}\n'
RUN rpm -K /tmp/foo.rpm
RUN rpm -K /tmp/foo.rpm | grep -E "(?:pgp|digests signatures) OK"
RUN rpm -vK /tmp/foo.rpm
RUN rpm -vK /tmp/foo.rpm | grep "RSA/SHA256 Signature, key ID 15bd80b3: OK"
RUN rpm -K /tmp/foo.rpm
RUN rpm -K /tmp/foo.rpm | grep -E "(?:pgp|digests signatures) OK"
# Test with a repo
RUN yum install -y createrepo yum-utils
+7
View File
@@ -340,6 +340,13 @@ rpm:
# This will expand any env var you set in the field, e.g. key_id: ${RPM_SIGNING_KEY_ID}
key_id: bc8acdd415bd80b3
# Format of the PGP signature.
# Valid options:
# - legacy: works until RPM 4.16 (centos stream 9, fedora 34).
# Ignores key_id.
# - modern: works on RPM 4.17+ (fedora 35 and above)
format: legacy
# Custom configuration applied only to the Deb packager.
deb:
# deb specific architecture name that overrides "arch" without performing any replacements.