Repository: goreleaser/nfpm (go)

Issue:
Invalid dpkg-sig file
### What happened?

NFPM creates invalid dpkg-sig file, containing native byte slice string representations for hashes (instead of hex-encoded strings) and does not contain newlines between file lines. E.g.:
```
Hash: SHA1

Version: 4
Signer:
Date: 2024-07-09 07:18:57.751051895 +0000 UTC
Role: builder
Files:
[60 249 24 39 47 250 93 225 149 117 45 115 243 218 62 94] [121 89 201 105 224 146 242 165 168 96 78 34 135 128 122 197 177 179 132 173] 4 debian-binary[38 54 153 155 140 232 9 110 215 199 15 165 27 135 187 209] [94 8 122 215 206 198 28 95 108 189 249 33 180 125 163 156 241 131 41 92] 1311 control.tar.gz[86 193 58 25 7 246 222 53 69 246 157 91 0 159 228 155] [132 2 109 37 200 108 41 165 81 241 32 124 19 145 12 230 0 201 210 99] 74031244 data.tar.gz
```

Correct dpkg-sig file should look like this:

```
Hash: SHA512

Version: 4
Signer: Docker Release (CE deb) <docker@docker.com>
Date: Tue Jun 25 08:43:07 2024
Role: builder
Files: 
    3cf918272ffa5de195752d73f3da3e5e 7959c969e092f2a5a8604e2287807ac5b1b384ad 4 debian-binary
    81f84b4c6811bb42e8cd2fb442fb3ad0 e1b9fa01878f05c32e6a77df93c1b7a8cf2e2641 664 control.tar.xz
    0b02e196efc352a616d22ada33c31b98 173881a95119d07459cf21a17fff6b42b9e208d9 12496204 data.tar.xz
```


### How can we reproduce this?

Create a debian package using NFPM with enabled dpkg-sig signing:

```yaml
deb:
    signature:
      method: dpkg-sig
      key: key.gpg
      type: builder
```

### nfpm version

```bash
v2.38.0
```


### Search

- [X] I did search for other open and closed issues before opening this.

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

### Additional context

_No response_

Pull request:
fix(deb): dpkg-sig format
Closes #840

Applying hex-encoding to MD5 and SHA1 hashes in dpkg-sig signing method, adding newlines between single file lines.

Verifying digests in dpkg-sig unit testing

Patch:
diff --git a/deb/deb.go b/deb/deb.go
index be50cf7..e5218fe 100644
--- a/deb/deb.go
+++ b/deb/deb.go
@@ -8,6 +8,7 @@ import (
 	"compress/gzip"
 	"crypto/md5" // nolint:gas
 	"crypto/sha1"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"io"
@@ -230,7 +231,9 @@ Signer: {{ .Signer }}
 Date: {{ .Date }}
 Role: {{ .Role }}
 Files:
-{{range .Files}}{{ .Md5Sum }} {{ .Sha1Sum }} {{ .Size }} {{ .Name }}{{end}}
+{{range .Files -}}
+{{"\t"}}{{ hex .Md5Sum }} {{ hex .Sha1Sum }} {{ .Size }} {{ .Name }}
+{{end -}}
 `
 
 type dpkgSigData struct {
@@ -241,17 +244,18 @@ type dpkgSigData struct {
 	Info   *nfpm.Info
 }
 type dpkgSigFileLine struct {
-	Md5Sum  [16]byte
-	Sha1Sum [20]byte
+	Md5Sum  []byte
+	Sha1Sum []byte
 	Size    int
 	Name    string
 }
 
 func newDpkgSigFileLine(name string, fileContent []byte) dpkgSigFileLine {
+	md5Sum, sha1Sum := md5.Sum(fileContent), sha1.Sum(fileContent)
 	return dpkgSigFileLine{
 		Name:    name,
-		Md5Sum:  md5.Sum(fileContent),
-		Sha1Sum: sha1.Sum(fileContent),
+		Md5Sum:  md5Sum[:],
+		Sha1Sum: sha1Sum[:],
 		Size:    len(fileContent),
 	}
 }
@@ -267,7 +271,9 @@ func readDpkgSigData(info *nfpm.Info, debianBinary, controlTarGz, dataTarball []
 			newDpkgSigFileLine("data.tar.gz", dataTarball),
 		},
 	}
-	temp, _ := template.New("dpkg-sig").Parse(dpkgSigTemplate)
+	temp, _ := template.New("dpkg-sig").Funcs(template.FuncMap{
+		"hex": hex.EncodeToString,
+	}).Parse(dpkgSigTemplate)
 	buf := &bytes.Buffer{}
 	err := temp.Execute(buf, data)
 	if err != nil {
diff --git a/internal/sign/pgp.go b/internal/sign/pgp.go
index 24d6622..876d1b8 100644
--- a/internal/sign/pgp.go
+++ b/internal/sign/pgp.go
@@ -143,10 +143,10 @@ func PGPVerify(message io.Reader, signature []byte, armoredPubKeyFile string) er
 	return err
 }
 
-func PGPReadMessage(message []byte, armoredPubKeyFile string) error {
+func PGPReadMessage(message []byte, armoredPubKeyFile string) (plaintext []byte, err error) {
 	keyFileContent, err := os.ReadFile(armoredPubKeyFile)
 	if err != nil {
-		return fmt.Errorf("reading armored public key file: %w", err)
+		return nil, fmt.Errorf("reading armored public key file: %w", err)
 	}
 
 	var keyring openpgp.EntityList
@@ -154,19 +154,19 @@ func PGPReadMessage(message []byte, armoredPubKeyFile string) error {
 	if isASCII(keyFileContent) {
 		keyring, err = openpgp.ReadArmoredKeyRing(bytes.NewReader(keyFileContent))
 		if err != nil {
-			return fmt.Errorf("decoding armored public key file: %w", err)
+			return nil, 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)
+			return nil, fmt.Errorf("decoding public key file: %w", err)
 		}
 	}
 
 	block, _ := clearsign.Decode(message)
 	_, err = block.VerifySignature(keyring, nil)
 
-	return err
+	return block.Plaintext, err
 }
 
 func parseKeyID(hexKeyID *string) (uint64, error) {


Test patch:
diff --git a/deb/deb_test.go b/deb/deb_test.go
index 7ad20d5..86e65a5 100644
--- a/deb/deb_test.go
+++ b/deb/deb_test.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"compress/gzip"
 	"crypto/md5" // nolint: gosec
+	"crypto/sha1"
 	"encoding/hex"
 	"errors"
 	"flag"
@@ -13,6 +14,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
 	"strconv"
 	"strings"
 	"testing"
@@ -1001,8 +1003,10 @@ func TestDpkgSigSignature(t *testing.T) {
 
 	signature := extractFileFromAr(t, deb.Bytes(), "_gpgbuilder")
 
-	err = sign.PGPReadMessage(signature, "../internal/sign/testdata/pubkey.asc")
+	msg, err := sign.PGPReadMessage(signature, "../internal/sign/testdata/pubkey.asc")
 	require.NoError(t, err)
+
+	require.NoError(t, verifyDpkgSigFileHashes(extractAllFilesFromAr(t, deb.Bytes()), string(msg)))
 }
 
 func TestDpkgSigSignatureError(t *testing.T) {
@@ -1032,8 +1036,10 @@ func TestDpkgSigSignatureCallback(t *testing.T) {
 
 	signature := extractFileFromAr(t, deb.Bytes(), "_gpgbuilder")
 
-	err = sign.PGPReadMessage(signature, "../internal/sign/testdata/pubkey.asc")
+	msg, err := sign.PGPReadMessage(signature, "../internal/sign/testdata/pubkey.asc")
 	require.NoError(t, err)
+
+	require.NoError(t, verifyDpkgSigFileHashes(extractAllFilesFromAr(t, deb.Bytes()), string(msg)))
 }
 
 func TestDisableGlobbing(t *testing.T) {
@@ -1400,6 +1406,26 @@ func extractFileFromAr(tb testing.TB, arFile []byte, filename string) []byte {
 	return nil
 }
 
+func extractAllFilesFromAr(tb testing.TB, arFile []byte) map[string][]byte {
+	tb.Helper()
+
+	tr := ar.NewReader(bytes.NewReader(arFile))
+	files := make(map[string][]byte)
+	for {
+		hdr, err := tr.Next()
+		if errors.Is(err, io.EOF) {
+			break // End of archive
+		}
+		require.NoError(tb, err)
+
+		fileContents, err := io.ReadAll(tr)
+		require.NoError(tb, err)
+
+		files[hdr.Name] = fileContents
+	}
+	return files
+}
+
 func TestEmptyButRequiredDebFields(t *testing.T) {
 	item := nfpm.WithDefaults(&nfpm.Info{
 		Name:    "foo",
@@ -1506,3 +1532,45 @@ func (zrc *zstdReadCloser) Close() error {
 
 	return nil
 }
+
+func verifyDpkgSigFileHashes(arFiles map[string][]byte, msg string) error {
+	_, hashes, ok := strings.Cut(msg, "Files:")
+	if !ok {
+		return errors.New("expected Files section in dpkg-sig message")
+	}
+	lines := strings.Split(hashes, "\n")
+	for i := range lines {
+		lines[i] = strings.TrimSpace(lines[i])
+		if lines[i] == "" {
+			continue
+		}
+		var md5Hex, sha1Hex, size, name string
+		if n, err := fmt.Sscanln(lines[i], &md5Hex, &sha1Hex, &size, &name); err != nil {
+			return err
+		} else if n != 4 {
+			return fmt.Errorf("expected 4 elements in line %q, but got %d", lines[i], n)
+		}
+
+		md5Sum, err := hex.DecodeString(md5Hex)
+		if err != nil {
+			return err
+		}
+		sha1Sum, err := hex.DecodeString(sha1Hex)
+		if err != nil {
+			return err
+		}
+
+		content, ok := arFiles[name]
+		if !ok {
+			return fmt.Errorf("dpkg-sig message contains hash of file %q, but the package does not contain the file", name)
+		}
+		actualMD5Sum, actualSHA1Sum := md5.Sum(content), sha1.Sum(content)
+		if !slices.Equal(actualMD5Sum[:], md5Sum) {
+			return fmt.Errorf("file %q has invalid MD5 sum", name)
+		}
+		if !slices.Equal(actualSHA1Sum[:], sha1Sum) {
+			return fmt.Errorf("file %q has invalid SHA1 sum", name)
+		}
+	}
+	return nil
+}