Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ tasks:
- go run ./cmd/nfpm/... pkg -f {{.SRC}} -p rpm -t ./dist/foo.rpm
- go run ./cmd/nfpm/... pkg -f {{.SRC}} -p apk -t ./dist/foo.apk
- go run ./cmd/nfpm/... pkg -f {{.SRC}} -p archlinux -t ./dist/foo.pkg.tar.zst
- go run ./cmd/nfpm/... pkg -f {{.SRC}} -p xbps -t ./dist/foo.xbps
- go run ./cmd/nfpm/... pkg -f {{.MSIX_SRC}} -p msix -t ./dist/foo.msix

acceptance:windows:install:
Expand Down
107 changes: 107 additions & 0 deletions acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
_ "github.com/goreleaser/nfpm/v2/ipk"
_ "github.com/goreleaser/nfpm/v2/msix"
_ "github.com/goreleaser/nfpm/v2/rpm"
_ "github.com/goreleaser/nfpm/v2/xbps"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -346,6 +347,112 @@ func TestDebSign(t *testing.T) {
}
}

func TestXBPSSpecific(t *testing.T) {
t.Parallel()
format := "xbps"
arch := "amd64"

for _, testCase := range []struct {
name string
conf string
pkgfile string
buildArgs []string
}{
{
name: "lifecycle",
conf: "xbps.lifecycle.yaml",
pkgfile: "foo-1.2.3_1.x86_64.xbps",
buildArgs: []string{"scenario=lifecycle"},
},
{
name: "metadata",
conf: "xbps.metadata.yaml",
pkgfile: "foo-1.2.3_1.x86_64.xbps",
buildArgs: []string{"scenario=metadata"},
},
{
name: "noarch",
conf: "xbps.noarch.yaml",
pkgfile: "foo-1.2.3_1.noarch.xbps",
buildArgs: []string{"scenario=noarch"},
},
} {
testCase := testCase
t.Run(fmt.Sprintf("%s/%s/%s", format, arch, testCase.name), func(t *testing.T) {
t.Parallel()
generatedPackage := fmt.Sprintf("tmp/xbps_%s_%s.%s", testCase.name, arch, format)
buildArgs := append([]string{}, testCase.buildArgs...)
buildArgs = append(buildArgs,
fmt.Sprintf("pkgfile=%s", testCase.pkgfile),
fmt.Sprintf("oldpackage=%s", generatedPackage),
fmt.Sprintf("oldpkgfile=%s", testCase.pkgfile),
)
accept(t, acceptParms{
Name: fmt.Sprintf("xbps_%s_%s", testCase.name, arch),
Conf: testCase.conf,
Format: format,
Docker: dockerParams{
File: "xbps.dockerfile",
Target: "scenario",
Arch: arch,
BuildArgs: buildArgs,
},
})
})
}

t.Run(fmt.Sprintf("%s/%s/upgrade", format, arch), func(t *testing.T) {
t.Parallel()
testArch := arch
oldpkg := fmt.Sprintf("tmp/xbps_upgrade_%s.v1.%s", testArch, format)
target := fmt.Sprintf("./testdata/acceptance/%s", oldpkg)
repoTmp := "./testdata/acceptance/tmp"
require.NoError(t, os.MkdirAll(repoTmp, 0o700))

config, err := nfpm.ParseFileWithEnvMapping("./testdata/acceptance/xbps.upgrade.v1.yaml", func(s string) string {
switch s {
case "BUILD_ARCH":
return testArch
default:
return os.Getenv(s)
}
})
require.NoError(t, err)

info, err := config.Get(format)
require.NoError(t, err)
require.NoError(t, nfpm.Validate(info))

pkg, err := nfpm.Get(format)
require.NoError(t, err)

f, err := os.Create(target)
require.NoError(t, err)
info.Target = target
packageErr := pkg.Package(nfpm.WithDefaults(info), f)
closeErr := f.Close()
require.NoError(t, packageErr)
require.NoError(t, closeErr)

accept(t, acceptParms{
Name: fmt.Sprintf("xbps_upgrade_%s.v2", testArch),
Conf: "xbps.upgrade.v2.yaml",
Format: format,
Docker: dockerParams{
File: "xbps.dockerfile",
Target: "scenario",
Arch: testArch,
BuildArgs: []string{
"scenario=upgrade",
fmt.Sprintf("oldpackage=%s", oldpkg),
"oldpkgfile=foo-1.2.3_1.x86_64.xbps",
"pkgfile=foo-1.2.4_1.x86_64.xbps",
},
},
})
})
}

type acceptParms struct {
Name string
Conf string
Expand Down
2 changes: 1 addition & 1 deletion cmd/nfpm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func main() {

func buildVersion(version, commit, date, builtBy, treeState string) goversion.Info {
return goversion.GetVersionInfo(
goversion.WithAppDetails("nfpm", "a simple and 0-dependencies apk, arch linux, deb, ipk, msix, and rpm packager written in Go", website),
goversion.WithAppDetails("nfpm", "a simple and 0-dependencies apk, arch linux, deb, ipk, msix, rpm, and xbps packager written in Go", website),
goversion.WithASCIIName(asciiArt),
func(i *goversion.Info) {
if commit != "" {
Expand Down
5 changes: 3 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
_ "github.com/goreleaser/nfpm/v2/ipk" // ipk packager
_ "github.com/goreleaser/nfpm/v2/msix" // msix packager
_ "github.com/goreleaser/nfpm/v2/rpm" // rpm packager
_ "github.com/goreleaser/nfpm/v2/xbps" // xbps packager
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -45,8 +46,8 @@ func newRootCmd(version goversion.Info, exit func(int)) *rootCmd {
}
cmd := &cobra.Command{
Use: "nfpm",
Short: "Packages apps on RPM, Deb, APK, Arch Linux, ipk, and MSIX formats based on a YAML configuration file",
Long: `nFPM is a simple and 0-dependencies apk, arch, deb, ipk, msix, and rpm packager written in Go.`,
Short: "Packages apps on RPM, Deb, APK, Arch Linux, ipk, MSIX, and XBPS formats based on a YAML configuration file",
Long: `nFPM is a simple and 0-dependencies apk, arch, deb, ipk, msix, rpm, and xbps packager written in Go.`,
Version: version.String(),
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
Expand Down
110 changes: 68 additions & 42 deletions internal/sign/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1" // nolint:gosec
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"errors"
Expand All @@ -14,10 +15,11 @@ import (
)

var (
errNoPemBlock = errors.New("no PEM block found")
errDigestNotSH1 = errors.New("digest is not a SHA1 hash")
errNoPassphrase = errors.New("key is encrypted but no passphrase was provided")
errNoRSAKey = errors.New("key is not an RSA key")
errNoPemBlock = errors.New("no PEM block found")
errDigestNotSH1 = errors.New("digest is not a SHA1 hash")
errDigestNotSHA256 = errors.New("digest is not a SHA256 hash")
errNoPassphrase = errors.New("key is encrypted but no passphrase was provided")
errNoRSAKey = errors.New("key is not an RSA key")
)

const (
Expand All @@ -28,15 +30,38 @@ const (
// RSASignSHA1Digest signs the provided SHA1 message digest. The key file
// must be in the PEM format and can either be encrypted or not.
func RSASignSHA1Digest(sha1Digest []byte, keyFile, passphrase string) ([]byte, error) {
if len(sha1Digest) != sha1.Size {
return nil, errDigestNotSH1
return rsaSignDigest(sha1Digest, keyFile, passphrase, crypto.SHA1, sha1.Size, errDigestNotSH1)
}

// RSASignSHA256Digest signs the provided SHA256 message digest. The key file
// must be in the PEM format and can either be encrypted or not.
func RSASignSHA256Digest(sha256Digest []byte, keyFile, passphrase string) ([]byte, error) {
return rsaSignDigest(sha256Digest, keyFile, passphrase, crypto.SHA256, sha256.Size, errDigestNotSHA256)
}

func rsaSignDigest(digest []byte, keyFile, passphrase string, hash crypto.Hash, digestSize int, digestErr error) ([]byte, error) {
if len(digest) != digestSize {
return nil, digestErr
}

priv, err := loadPrivateKey(keyFile, passphrase)
if err != nil {
return nil, err
}

signature, err := priv.Sign(rand.Reader, digest, hash)
if err != nil {
return nil, fmt.Errorf("signing: %w", err)
}

return signature, nil
}

func loadPrivateKey(keyFile, passphrase string) (crypto.Signer, error) {
keyFileContent, err := os.ReadFile(keyFile)
if err != nil {
return nil, fmt.Errorf("reading key file: %w", err)
}

block, _ := pem.Decode(keyFileContent)
if block == nil {
return nil, errNoPemBlock
Expand All @@ -47,45 +72,29 @@ func RSASignSHA1Digest(sha1Digest []byte, keyFile, passphrase string) ([]byte, e
if passphrase == "" {
return nil, errNoPassphrase
}

var decryptedBlockData []byte

decryptedBlockData, err = x509.DecryptPEMBlock(block, []byte(passphrase)) //nolint:staticcheck
decryptedBlockData, err := x509.DecryptPEMBlock(block, []byte(passphrase)) //nolint:staticcheck
if err != nil {
return nil, fmt.Errorf("decrypt private key PEM block: %w", err)
}

blockData = decryptedBlockData
}

var priv crypto.Signer

switch block.Type {
case PKCS1PrivkeyPreamble:
priv, err = x509.ParsePKCS1PrivateKey(blockData)
if err != nil {
return nil, fmt.Errorf("parse PKCS#1 private key: %w", err)
}
return x509.ParsePKCS1PrivateKey(blockData)
case PKCS8PrivkeyPreamble:
privAny, err := x509.ParsePKCS8PrivateKey(blockData)
if err != nil {
return nil, fmt.Errorf("parse PKCS#8 private key: %w", err)
}
privTmp, ok := privAny.(crypto.Signer)
priv, ok := privAny.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("cannot sign with given private key")
return nil, errNoRSAKey
}
priv = privTmp
return priv, nil
Comment thread
actx4gh marked this conversation as resolved.
default:
return nil, fmt.Errorf(`key type "%v" is not supported`, block.Type)
}

signature, err := priv.Sign(rand.Reader, sha1Digest, crypto.SHA1)
if err != nil {
return nil, fmt.Errorf("signing: %w", err)
}

return signature, nil
}

func rsaSign(message io.Reader, keyFile, passphrase string) ([]byte, error) {
Expand All @@ -101,36 +110,53 @@ func rsaSign(message io.Reader, keyFile, passphrase string) ([]byte, error) {
// RSAVerifySHA1Digest is exported for use in tests and verifies a signature over the
// provided SHA1 hash of a message. The key file must be in the PEM format.
func RSAVerifySHA1Digest(sha1Digest, signature []byte, publicKeyFile string) error {
if len(sha1Digest) != sha1.Size {
return errDigestNotSH1
return rsaVerifyDigest(sha1Digest, signature, publicKeyFile, crypto.SHA1, sha1.Size, errDigestNotSH1)
}

// RSAVerifySHA256Digest is exported for use in tests and verifies a signature over the
// provided SHA256 hash of a message. The key file must be in the PEM format.
func RSAVerifySHA256Digest(sha256Digest, signature []byte, publicKeyFile string) error {
return rsaVerifyDigest(sha256Digest, signature, publicKeyFile, crypto.SHA256, sha256.Size, errDigestNotSHA256)
}

func rsaVerifyDigest(digest, signature []byte, publicKeyFile string, hash crypto.Hash, digestSize int, digestErr error) error {
if len(digest) != digestSize {
return digestErr
}

keyFileContent, err := os.ReadFile(publicKeyFile)
rsaPub, err := loadRSAPublicKey(publicKeyFile)
if err != nil {
return fmt.Errorf("reading key file: %w", err)
return err
}

err = rsa.VerifyPKCS1v15(rsaPub, hash, digest, signature)
if err != nil {
return fmt.Errorf("verify PKCS1v15 signature: %w", err)
}

return nil
}

func loadRSAPublicKey(publicKeyFile string) (*rsa.PublicKey, error) {
keyFileContent, err := os.ReadFile(publicKeyFile)
if err != nil {
return nil, fmt.Errorf("reading key file: %w", err)
}
block, _ := pem.Decode(keyFileContent)
if block == nil {
return errNoPemBlock
return nil, errNoPemBlock
}

pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return fmt.Errorf("parse PKIX public key: %w", err)
return nil, fmt.Errorf("parse PKIX public key: %w", err)
}

rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return errNoRSAKey
}

err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA1, sha1Digest, signature)
if err != nil {
return fmt.Errorf("verify PKCS1v15 signature: %w", err)
return nil, errNoRSAKey
}

return nil
return rsaPub, nil
}

func rsaVerify(message io.Reader, signature []byte, publicKeyFile string) error {
Expand Down
Loading