Skip to content

Commit bceacf4

Browse files
mtharpelliotpeele
andcommitted
feat: support RPMs with a payload digest but no SIG_MD5 (#28)
Co-authored-by: Elliot Peele <elliot@bentlogic.net>
1 parent 277b154 commit bceacf4

8 files changed

Lines changed: 260 additions & 92 deletions

File tree

header.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ package rpmutils
1818

1919
import (
2020
"bytes"
21-
"crypto/sha1"
21+
"crypto"
2222
"encoding/binary"
23+
"encoding/hex"
2324
"errors"
2425
"fmt"
2526
"io"
@@ -38,7 +39,7 @@ type entry struct {
3839
type rpmHeader struct {
3940
entries map[int]entry
4041
isSource bool
41-
origSize int
42+
orig []byte
4243
}
4344

4445
type headerIntro struct {
@@ -71,19 +72,24 @@ func readExact(f io.Reader, n int) ([]byte, error) {
7172
return buf, err
7273
}
7374

74-
func readHeader(f io.Reader, hash string, isSource bool, sigBlock bool) (*rpmHeader, error) {
75+
func readHeader(f io.Reader, hash string, hashType crypto.Hash, isSource bool, sigBlock bool) (*rpmHeader, error) {
76+
// save original header
77+
var origBuf bytes.Buffer
78+
f = io.TeeReader(f, &origBuf)
79+
// verify intro
7580
var intro headerIntro
7681
if err := binary.Read(f, binary.BigEndian, &intro); err != nil {
7782
return nil, fmt.Errorf("error reading RPM header: %s", err.Error())
7883
}
7984
if intro.Magic != introMagic {
8085
return nil, fmt.Errorf("bad magic for header")
8186
}
87+
// read entries
8288
entryTable, err := readExact(f, int(intro.Entries*16))
8389
if err != nil {
8490
return nil, fmt.Errorf("error reading RPM header table: %s", err.Error())
8591
}
86-
92+
// read data
8793
size := intro.Size
8894
if sigBlock {
8995
// signature block is padded to 8 byte alignment
@@ -93,24 +99,16 @@ func readHeader(f io.Reader, hash string, isSource bool, sigBlock bool) (*rpmHea
9399
if err != nil {
94100
return nil, fmt.Errorf("error reading RPM header data: %s", err.Error())
95101
}
96-
97-
// Check sha1 if it was specified
102+
// Check hash if it was specified
98103
if len(hash) > 1 {
99-
h := sha1.New()
100-
if err = binary.Write(h, binary.BigEndian, &intro); err != nil {
101-
return nil, err
102-
}
103-
if _, err = h.Write(entryTable); err != nil {
104-
return nil, err
105-
}
106-
if _, err = h.Write(data); err != nil {
107-
return nil, err
108-
}
109-
if fmt.Sprintf("%x", h.Sum(nil)) != hash {
110-
return nil, fmt.Errorf("bad header sha1")
104+
h := hashType.New()
105+
h.Write(origBuf.Bytes())
106+
calculated := hex.EncodeToString(h.Sum(nil))
107+
if calculated != hash {
108+
return nil, fmt.Errorf("%s mismatch in signature header", hashType)
111109
}
112110
}
113-
111+
// parse entries
114112
ents := make(map[int]entry)
115113
buf := bytes.NewReader(entryTable)
116114
for i := 0; i < int(intro.Entries); i++ {
@@ -143,7 +141,7 @@ func readHeader(f io.Reader, hash string, isSource bool, sigBlock bool) (*rpmHea
143141
return &rpmHeader{
144142
entries: ents,
145143
isSource: isSource,
146-
origSize: 16 + len(entryTable) + len(data),
144+
orig: origBuf.Bytes(),
147145
}, nil
148146
}
149147

rpmutils.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ func ReadHeader(f io.Reader) (*RpmHeader, error) {
100100
return nil, err
101101
}
102102

103-
genHeader, err := readHeader(f, getSha1(sigHeader), sigHeader.isSource, false)
103+
hash, hashType := getHashAndType(sigHeader)
104+
genHeader, err := readHeader(f, hash, hashType, sigHeader.isSource, false)
104105
if err != nil {
105106
return nil, err
106107
}
@@ -130,7 +131,7 @@ func readSignatureHeader(f io.Reader) ([]byte, *rpmHeader, error) {
130131
isSource := binary.BigEndian.Uint16(lead[6:8]) == 1
131132

132133
// Return signature header
133-
hdr, err := readHeader(f, "", isSource, true)
134+
hdr, err := readHeader(f, "", 0, isSource, true)
134135
return lead, hdr, err
135136
}
136137

@@ -144,8 +145,8 @@ type HeaderRange struct {
144145

145146
// GetRange returns the byte offsets that the RPM header spans within the original RPM file
146147
func (hdr *RpmHeader) GetRange() HeaderRange {
147-
start := 96 + hdr.sigHeader.origSize
148-
end := start + hdr.genHeader.origSize
148+
start := 96 + len(hdr.sigHeader.orig)
149+
end := start + len(hdr.genHeader.orig)
149150
return HeaderRange{
150151
Start: start,
151152
End: end,

signatures.go

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ package rpmutils
1919
import (
2020
"bytes"
2121
"crypto"
22-
"crypto/md5"
23-
"errors"
2422
"hash"
2523
"io"
26-
"io/ioutil"
2724
"os"
2825
"path"
2926
"time"
@@ -54,21 +51,16 @@ func (opts *SignatureOptions) creationTime() time.Time {
5451
return time.Now()
5552
}
5653

57-
func makeSignature(stream io.Reader, key *packet.PrivateKey, opts *SignatureOptions) ([]byte, error) {
58-
hash := opts.hash()
54+
func makeSignature(h hash.Hash, key *packet.PrivateKey, opts *SignatureOptions) ([]byte, error) {
55+
hashType := opts.hash()
5956
sig := &packet.Signature{
6057
SigType: packet.SigTypeBinary,
6158
CreationTime: opts.creationTime(),
6259
PubKeyAlgo: key.PublicKey.PubKeyAlgo,
63-
Hash: hash,
60+
Hash: hashType,
6461
IssuerKeyId: &key.KeyId,
6562
}
66-
h := hash.New()
67-
_, err := io.Copy(h, stream)
68-
if err != nil {
69-
return nil, err
70-
}
71-
err = sig.Sign(h, key, nil)
63+
err := sig.Sign(h, key, nil)
7264
if err != nil {
7365
return nil, err
7466
}
@@ -103,43 +95,59 @@ func getSha1(sigHeader *rpmHeader) string {
10395
return vals[0]
10496
}
10597

106-
func checkMd5(sigHeader *rpmHeader, h hash.Hash) bool {
107-
sigmd5, err := sigHeader.GetBytes(SIG_MD5 - _SIGHEADER_TAG_BASE)
98+
func getSha256(sigHeader *rpmHeader) string {
99+
vals, err := sigHeader.GetStrings(SIG_SHA256)
108100
if err != nil {
109-
return true
101+
return ""
110102
}
111-
return bytes.Equal(sigmd5, h.Sum(nil))
103+
return vals[0]
104+
}
105+
106+
func getHashAndType(sigHeader *rpmHeader) (string, crypto.Hash) {
107+
// RPM v4 introduced SHA256 header signatures, prefer them over the
108+
// previous SHA1
109+
if h := getSha256(sigHeader); h != "" {
110+
return h, crypto.SHA256
111+
}
112+
return getSha1(sigHeader), crypto.SHA1
113+
}
114+
115+
func digestForSigning(sigHeader, genHeader *rpmHeader, payloadReader io.Reader, opts *SignatureOptions) (genHash, combinedHash hash.Hash, err error) {
116+
genHash, combinedHash = opts.hash().New(), opts.hash().New()
117+
// write header
118+
genHash.Write(genHeader.orig)
119+
combinedHash.Write(genHeader.orig)
120+
// write and verify payload
121+
err = digestPayload(sigHeader, genHeader, payloadReader, []io.Writer{combinedHash})
122+
return genHash, combinedHash, err
112123
}
113124

114125
// SignRpmStream reads an RPM and signs it, returning the set of headers updated with the new signature.
115126
func SignRpmStream(stream io.Reader, key *packet.PrivateKey, opts *SignatureOptions) (header *RpmHeader, err error) {
116127
lead, sigHeader, err := readSignatureHeader(stream)
117128
if err != nil {
118-
return
129+
return nil, err
119130
}
120-
// parse the general header and also tee it into a buffer
121-
genHeaderBuf := new(bytes.Buffer)
122-
headerTee := io.TeeReader(stream, genHeaderBuf)
123-
genHeader, err := readHeader(headerTee, getSha1(sigHeader), sigHeader.isSource, false)
131+
// parse the general header
132+
headerDigestValue, headerDigestType := getHashAndType(sigHeader)
133+
genHeader, err := readHeader(stream, headerDigestValue, headerDigestType, sigHeader.isSource, false)
124134
if err != nil {
125-
return
135+
return nil, err
126136
}
127-
genHeaderBlob := genHeaderBuf.Bytes()
128-
// chain the buffered general header to the rest of the payload, and digest the whole lot of it
129-
genHeaderAndPayload := io.MultiReader(bytes.NewReader(genHeaderBlob), stream)
130-
payloadDigest := md5.New()
131-
payloadTee := io.TeeReader(genHeaderAndPayload, payloadDigest)
132-
sigPgp, err := makeSignature(payloadTee, key, opts)
137+
// hash and sign header
138+
genHash, combinedHash, err := digestForSigning(sigHeader, genHeader, stream, opts)
133139
if err != nil {
134-
return
135-
}
136-
if !checkMd5(sigHeader, payloadDigest) {
137-
return nil, errors.New("md5 digest mismatch")
140+
return nil, err
138141
}
139-
sigRsa, err := makeSignature(bytes.NewReader(genHeaderBlob), key, opts)
142+
// sign header and payload
143+
sigPgp, err := makeSignature(combinedHash, key, opts)
140144
if err != nil {
141145
return
142146
}
147+
sigRsa, err := makeSignature(genHash, key, opts)
148+
if err != nil {
149+
return nil, err
150+
}
143151
insertSignatures(sigHeader, sigPgp, sigRsa)
144152
return &RpmHeader{
145153
lead: lead,
@@ -149,6 +157,34 @@ func SignRpmStream(stream io.Reader, key *packet.PrivateKey, opts *SignatureOpti
149157
}, nil
150158
}
151159

160+
func getPayloadDigest(header *rpmHeader) (string, crypto.Hash) {
161+
digests, err := header.GetStrings(PAYLOADDIGEST)
162+
if err != nil || len(digests) == 0 {
163+
// no payload digest
164+
return "", 0
165+
}
166+
digest := digests[0]
167+
algos, err := header.GetUint32s(PAYLOADDIGESTALGO)
168+
if err != nil || len(algos) == 0 {
169+
return "", 0
170+
}
171+
switch algos[0] {
172+
case HASH_MD5:
173+
return digest, crypto.MD5
174+
case HASH_SHA1:
175+
return digest, crypto.SHA1
176+
case HASH_SHA256:
177+
return digest, crypto.SHA256
178+
case HASH_SHA384:
179+
return digest, crypto.SHA384
180+
case HASH_SHA512:
181+
return digest, crypto.SHA512
182+
case HASH_SHA224:
183+
return digest, crypto.SHA224
184+
}
185+
return "", 0
186+
}
187+
152188
func canOverwrite(ininfo, outinfo os.FileInfo) bool {
153189
if !outinfo.Mode().IsRegular() {
154190
return false
@@ -216,7 +252,7 @@ func rewriteRpm(infile *os.File, outpath string, header *RpmHeader) error {
216252
}
217253
if outstream == nil {
218254
// write-rename
219-
tempfile, err := ioutil.TempFile(path.Dir(outpath), path.Base(outpath))
255+
tempfile, err := os.CreateTemp(path.Dir(outpath), path.Base(outpath))
220256
if err != nil {
221257
return err
222258
}
@@ -275,7 +311,7 @@ func writeRpm(infile io.ReadSeeker, outstream io.Writer, sigHeader *rpmHeader) e
275311
if err = sigHeader.WriteTo(outstream, RPMTAG_HEADERSIGNATURES); err != nil {
276312
return err
277313
}
278-
if _, err := infile.Seek(int64(len(lead)+sigHeader.origSize), 0); err != nil {
314+
if _, err := infile.Seek(int64(len(lead)+len(sigHeader.orig)), 0); err != nil {
279315
return err
280316
}
281317
_, err = io.Copy(outstream, infile)

signatures_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"io"
2222
"os"
2323
"testing"
24+
"testing/iotest"
2425

2526
"golang.org/x/crypto/openpgp"
2627
)
@@ -104,3 +105,83 @@ bCw9mwgJ2r0mQLqjrXjEYBhaE49I8A==
104105
=+d52
105106
-----END PGP PRIVATE KEY BLOCK-----
106107
`
108+
109+
func TestSHA256HeaderAndPayload(t *testing.T) {
110+
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(testkey)))
111+
if err != nil {
112+
t.Fatal("failed to parse test key:", err)
113+
}
114+
entity := keyring[0]
115+
116+
f, err := os.Open("./testdata/nfpm/test-1.0.0.x86_64.rpm")
117+
if err != nil {
118+
t.Fatal(err)
119+
}
120+
defer f.Close()
121+
122+
h, err := SignRpmStream(f, entity.PrivateKey, nil)
123+
if err != nil {
124+
t.Fatal("error signing rpm:", err)
125+
}
126+
sigblob, err := h.DumpSignatureHeader(false)
127+
if err != nil {
128+
t.Fatal("error writing sig header:", err)
129+
}
130+
if len(sigblob)%8 != 0 {
131+
t.Fatalf("incorrect padding: got %d bytes, expected a multiple of 8", len(sigblob))
132+
}
133+
// verify by merging the new sig header with the original file
134+
if _, err = f.Seek(int64(h.OriginalSignatureHeaderSize()), io.SeekStart); err != nil {
135+
t.Fatal("error seeking:", err)
136+
}
137+
signed := io.MultiReader(bytes.NewReader(sigblob), f)
138+
_, sigs, err := Verify(signed, keyring)
139+
if err != nil {
140+
t.Fatal("error verifying signature:", err)
141+
}
142+
if len(sigs) != 2 || sigs[0].Signer != entity || sigs[1].Signer != entity {
143+
t.Fatalf("error verifying signature: incorrect signers. found: %#v", sigs)
144+
}
145+
// check padding for odd sized signature tags
146+
h.sigHeader.entries[1234] = entry{dataType: RPM_BIN_TYPE, count: 3, contents: []byte("foo")}
147+
sigblob, err = h.DumpSignatureHeader(false)
148+
if err != nil {
149+
t.Fatal("error writing sig header:", err)
150+
}
151+
if len(sigblob)%8 != 0 {
152+
t.Fatalf("incorrect padding: got %d bytes, expected a multiple of 8", len(sigblob))
153+
}
154+
155+
}
156+
157+
func TestReadSha256PayloadHeader(t *testing.T) {
158+
f, err := os.Open("./testdata/nfpm/test-1.0.0.x86_64.rpm")
159+
if err != nil {
160+
t.Fatal(err)
161+
}
162+
defer f.Close()
163+
164+
rpm, err := ReadRpm(iotest.HalfReader(f))
165+
if err != nil {
166+
t.Fatal(err)
167+
}
168+
169+
hashType, err := rpm.Header.GetInt(PAYLOADDIGESTALGO)
170+
if err != nil {
171+
t.Fatal(err)
172+
}
173+
174+
if hashType != HASH_SHA256 {
175+
t.Fatalf("expected hash type of %d, got %d", HASH_SHA256, hashType)
176+
}
177+
178+
digest, err := rpm.Header.GetString(PAYLOADDIGEST)
179+
if err != nil {
180+
t.Fatal(err)
181+
}
182+
expected := "0ba72025da7d469c62e03d2dcec072c4ea6b8fecbede01d01f4f620a162f8309"
183+
if digest != expected {
184+
t.Fatalf("expected digest of '%s', got %s", expected, digest)
185+
}
186+
187+
}

0 commit comments

Comments
 (0)