feat: implement dpkg-sig Package signing (#508)

* implement dpkg-sig Package signing

* Fix dpkgsig template syntax

* Fix dpkgsig template syntax

* Correctly handle template errors when reading dpkg-sig templates

* Fix dkpgsig signature templateing
This commit is contained in:
Sas Swart
2022-05-30 15:34:45 +02:00
committed by GitHub
parent 86d9a4f1e0
commit 1eb3837edc
4 changed files with 203 additions and 14 deletions
+100 -13
View File
@@ -5,6 +5,7 @@ import (
"archive/tar"
"bytes"
"crypto/md5" // nolint:gas
"crypto/sha1"
"errors"
"fmt"
"io"
@@ -128,23 +129,49 @@ func (d *Deb) Package(info *nfpm.Info, deb io.Writer) (err error) { // nolint: f
}
// TODO: refactor this
method := "debsign"
if info.Deb.Signature.Method != "" {
method = info.Deb.Signature.Method
}
if info.Deb.Signature.KeyFile != "" {
data := io.MultiReader(bytes.NewReader(debianBinary), bytes.NewReader(controlTarGz),
bytes.NewReader(dataTarball))
var data io.Reader
var sigType string
var sig []byte
sig, err := sign.PGPArmoredDetachSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
if err != nil {
return &nfpm.ErrSigningFailure{Err: err}
}
if method == "debsign" {
data = readDebsignData(debianBinary, controlTarGz, dataTarball)
sigType := "origin"
if info.Deb.Signature.Type != "" {
sigType = info.Deb.Signature.Type
}
sigType = "origin"
if info.Deb.Signature.Type != "" {
sigType = info.Deb.Signature.Type
}
if sigType != "origin" && sigType != "maint" && sigType != "archive" {
return &nfpm.ErrSigningFailure{
Err: ErrInvalidSignatureType,
if sigType != "origin" && sigType != "maint" && sigType != "archive" {
return &nfpm.ErrSigningFailure{
Err: ErrInvalidSignatureType,
}
}
sig, err = sign.PGPArmoredDetachSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
if err != nil {
return &nfpm.ErrSigningFailure{Err: err}
}
} else if method == "dpkg-sig" {
data, err := readDpkgSigData(info, debianBinary, controlTarGz, dataTarball)
if err != nil {
return &nfpm.ErrSigningFailure{Err: err}
}
sigType = "builder"
if info.Deb.Signature.Type != "" {
sigType = info.Deb.Signature.Type
}
sig, err = sign.PGPClearSignWithKeyID(data, info.Deb.Signature.KeyFile, info.Deb.Signature.KeyPassphrase, info.Deb.Signature.KeyID)
if err != nil {
return &nfpm.ErrSigningFailure{Err: err}
}
}
@@ -158,6 +185,66 @@ func (d *Deb) Package(info *nfpm.Info, deb io.Writer) (err error) { // nolint: f
return nil
}
func readDebsignData(debianBinary, controlTarGz, dataTarball []byte) io.Reader {
return io.MultiReader(bytes.NewReader(debianBinary), bytes.NewReader(controlTarGz),
bytes.NewReader(dataTarball))
}
// reference: https://manpages.debian.org/jessie/dpkg-sig/dpkg-sig.1.en.html
const dpkgSigTemplate = `
Hash: SHA1
Version: 4
Signer: {{ .Signer }}
Date: {{ .Date }}
Role: {{ .Role }}
Files:
{{range .Files}}{{ .Md5Sum }} {{ .Sha1Sum }} {{ .Size }} {{ .Name }}{{end}}
`
type dpkgSigData struct {
Signer string
Date time.Time
Role string
Files []dpkgSigFileLine
Info *nfpm.Info
}
type dpkgSigFileLine struct {
Md5Sum [16]byte
Sha1Sum [20]byte
Size int
Name string
}
func newDpkgSigFileLine(name string, fileContent []byte) dpkgSigFileLine {
return dpkgSigFileLine{
Name: name,
Md5Sum: md5.Sum(fileContent),
Sha1Sum: sha1.Sum(fileContent),
Size: len(fileContent),
}
}
func readDpkgSigData(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []byte) (io.Reader, error) {
data := dpkgSigData{
Signer: info.Deb.Signature.Signer,
Date: time.Now(),
Role: info.Deb.Signature.Type,
Files: []dpkgSigFileLine{
newDpkgSigFileLine("debian-binary", debianBinary),
newDpkgSigFileLine("control.tar.gz", controlTarGz),
newDpkgSigFileLine("data.tar.gz", dataTarball),
},
}
temp, _ := template.New("dpkg-sig").Parse(dpkgSigTemplate)
buf := &bytes.Buffer{}
err := temp.Execute(buf, data)
if err != nil {
return nil, fmt.Errorf("dpkg-sig template error: %w", err)
}
return buf, nil
}
func (*Deb) SetPackagerDefaults(info *nfpm.Info) {
// Priority should be set on all packages per:
// https://www.debian.org/doc/debian-policy/ch-archive.html#priorities
+30
View File
@@ -946,6 +946,36 @@ func TestDebsigsSignatureError(t *testing.T) {
require.True(t, errors.As(err, &expectedError))
}
func TestDpkgSigSignature(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.KeyFile = "../internal/sign/testdata/privkey.asc"
info.Deb.Signature.KeyPassphrase = "hunter2"
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 TestDpkgSigSignatureError(t *testing.T) {
info := exampleInfo()
info.Deb.Signature.KeyFile = "/does/not/exist"
info.Deb.Signature.Method = "dpkg-sig"
var deb bytes.Buffer
err := Default.Package(info, &deb)
require.Error(t, err)
var expectedError *nfpm.ErrSigningFailure
require.True(t, errors.As(err, &expectedError))
}
func TestDisableGlobbing(t *testing.T) {
info := exampleInfo()
info.DisableGlobbing = true
+69
View File
@@ -11,6 +11,7 @@ import (
"unicode"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/goreleaser/nfpm/v2"
)
@@ -79,6 +80,48 @@ func PGPArmoredDetachSignWithKeyID(message io.Reader, keyFile, passphrase string
return signature.Bytes(), nil
}
func PGPClearSignWithKeyID(message io.Reader, keyFile, passphrase string, hexKeyId *string) ([]byte, error) {
keyId, err := parseKeyID(hexKeyId)
if err != nil {
return nil, fmt.Errorf("%v is not a valid key id: %w", hexKeyId, err)
}
key, err := readSigningKey(keyFile, passphrase)
if err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
var signature bytes.Buffer
writeCloser, err := clearsign.Encode(&signature, key.PrivateKey, &packet.Config{
SigningKeyId: keyId,
DefaultHash: crypto.SHA256,
})
if err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
messageBytes, err := io.ReadAll(message)
if err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
writtenLength, err := writeCloser.Write(messageBytes)
if err != nil {
return nil, fmt.Errorf("clear sign: %w", err)
}
if writtenLength != len(messageBytes) {
return nil, fmt.Errorf("partial signature written")
}
writeCloser.Close()
if err != nil {
return nil, fmt.Errorf("armored detach sign: %w", err)
}
return signature.Bytes(), nil
}
// PGPVerify is exported for use in tests and verifies an ASCII-armored or non-ASCII-armored
// signature using an ASCII-armored or non-ASCII-armored public key file. The signer
// identity is not explicitly checked, other that the obvious fact that the signer's key must
@@ -112,6 +155,32 @@ func PGPVerify(message io.Reader, signature []byte, armoredPubKeyFile string) er
return err
}
func PGPReadMessage(message []byte, armoredPubKeyFile string) error {
keyFileContent, err := ioutil.ReadFile(armoredPubKeyFile)
if err != nil {
return fmt.Errorf("reading armored public key file: %w", err)
}
var keyring openpgp.EntityList
if isASCII(keyFileContent) {
keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return fmt.Errorf("decoding armored public key file: %w", err)
}
} else {
keyring, err = openpgp.ReadKeyRing(bytes.NewReader(keyFileContent))
if err != nil {
return fmt.Errorf("decoding public key file: %w", err)
}
}
block, _ := clearsign.Decode(message)
_, err = block.VerifySignature(keyring, nil)
return err
}
func parseKeyID(hexKeyId *string) (uint64, error) {
if hexKeyId == nil || *hexKeyId == "" {
return 0, nil
+4 -1
View File
@@ -332,8 +332,11 @@ type Deb struct {
type DebSignature struct {
PackageSignature `yaml:",inline"`
// debsign, or dpkg-sig (defaults to debsign)
Method string `yaml:"method,omitempty" jsonschema:"title=method role,enum=debsign,enum=dpkg-sig,default=debsign"`
// origin, maint or archive (defaults to origin)
Type string `yaml:"type,omitempty" jsonschema:"title=signer role,enum=origin,enum=maint,enum=archive,default=origin"`
Type string `yaml:"type,omitempty" jsonschema:"title=signer role,enum=origin,enum=maint,enum=archive,default=origin"`
Signer string `yaml:"signer,omitempty" jsonschema:"title=signer"`
}
// DebTriggers contains triggers only available for deb packages.