mirror of
https://github.com/goreleaser/nfpm.git
synced 2026-06-19 08:05:04 +00:00
feat: rpm.signature.format
Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
+12
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Vendored
+2
-1
@@ -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
|
||||
|
||||
Vendored
+2
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user