Skip to content

Commit ec48e30

Browse files
authored
fix multi key support in APKINDEX verification (#1490)
The current logic uses the last .SIGN. file content as signature and verify that against all known keys. This PR fixes the logic to use matching signature to the right verification key. --------- Signed-off-by: Nghia Tran <[email protected]> Signed-off-by: Nghia Tran <[email protected]>
1 parent 2221938 commit ec48e30

6 files changed

+136
-32
lines changed

pkg/apk/apk/apkindex_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package apk
33
import (
44
"archive/tar"
55
"compress/gzip"
6+
"context"
67
"fmt"
78
"io"
89
"os"
@@ -238,3 +239,52 @@ k:9001
238239
require.Len(t, pkg.Provides, 0, "Expected no provides")
239240
require.Len(t, pkg.Dependencies, 0, "Expected no dependencies")
240241
}
242+
243+
func TestMultipleKeys(t *testing.T) {
244+
assert := assert.New(t)
245+
// read all the keys from testdata/signing/keys
246+
folder := "testdata/signing/keys"
247+
// get all the files in the folder
248+
files, _ := os.ReadDir(folder)
249+
keys := make(map[string][]byte)
250+
for _, file := range files {
251+
if file.IsDir() {
252+
continue
253+
}
254+
// read the file
255+
keyFile, err := os.Open(fmt.Sprintf("%s/%s", folder, file.Name()))
256+
require.Nil(t, err)
257+
// parse the key
258+
key, err := os.ReadFile(keyFile.Name())
259+
require.Nil(t, err)
260+
keys[file.Name()] = key
261+
}
262+
// read the index file into []byte
263+
indexBytes, err := os.ReadFile("testdata/signing/APKINDEX.tar.gz")
264+
require.Nil(t, err)
265+
266+
ctx := context.Background()
267+
// There are 2^N-1 combinations of keys, where N is the number of keys
268+
// We will test all of them
269+
for comb := 1; comb < (1 << len(keys)); comb++ {
270+
// get the keys to use
271+
usedKeys := make(map[string][]byte)
272+
for i := 0; i < len(keys); i++ {
273+
if (comb & (1 << i)) != 0 {
274+
usedKeys[files[i].Name()] = keys[files[i].Name()]
275+
}
276+
}
277+
// parse the index
278+
apkIndex, err := parseRepositoryIndex(ctx, "testdata/signing/APKINDEX.tar.gz",
279+
usedKeys, "aarch64", indexBytes, &indexOpts{})
280+
require.Nil(t, err)
281+
assert.Greater(len(apkIndex.Signature), 0, "Signature missing")
282+
}
283+
// Now, test the case where we have no matching key
284+
_, err = parseRepositoryIndex(ctx, "testdata/signing/APKINDEX.tar.gz",
285+
map[string][]byte{
286+
"unused-key": []byte("unused-key-data"),
287+
},
288+
"aarch64", indexBytes, &indexOpts{})
289+
require.NotNil(t, err)
290+
}

pkg/apk/apk/index.go

+44-32
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ import (
4444

4545
var signatureFileRegex = regexp.MustCompile(`^\.SIGN\.(DSA|RSA|RSA256|RSA512)\.(.*\.rsa\.pub)$`)
4646

47+
type Signature struct {
48+
KeyID string
49+
Signature []byte
50+
DigestAlgorithm crypto.Hash
51+
}
52+
4753
// This is terrible but simpler than plumbing around a cache for now.
4854
// We just hold the parsed index in memory rather than re-parsing it every time,
4955
// which requires gunzipping, which is (somewhat) expensive.
@@ -332,9 +338,11 @@ func fetchRepositoryIndex(ctx context.Context, u string, etag string, opts *inde
332338
func parseRepositoryIndex(ctx context.Context, u string, keys map[string][]byte, arch string, b []byte, opts *indexOpts) (*APKIndex, error) { //nolint:gocyclo
333339
_, span := otel.Tracer("go-apk").Start(ctx, "parseRepositoryIndex")
334340
defer span.End()
335-
336341
// validate the signature
337342
if shouldCheckSignatureForIndex(u, arch, opts) {
343+
if len(keys) == 0 {
344+
return nil, fmt.Errorf("no keys provided to verify signature")
345+
}
338346
buf := bytes.NewReader(b)
339347
gzipReader, err := gzip.NewReader(buf)
340348
if err != nil {
@@ -348,14 +356,12 @@ func parseRepositoryIndex(ctx context.Context, u string, keys map[string][]byte,
348356

349357
tarReader := tar.NewReader(gzipReader)
350358

351-
var keyfile string
352-
var signature []byte
353-
var indexDigestType crypto.Hash
359+
sigs := make([]Signature, 0, len(keys))
354360
for {
355361
// read the signature(s)
356362
signatureFile, err := tarReader.Next()
357363
// found everything, end of stream
358-
if len(keyfile) > 0 && errors.Is(err, io.EOF) {
364+
if errors.Is(err, io.EOF) {
359365
break
360366
}
361367
// oops something went wrong
@@ -366,61 +372,67 @@ func parseRepositoryIndex(ctx context.Context, u string, keys map[string][]byte,
366372
if len(matches) != 3 {
367373
return nil, fmt.Errorf("failed to find key name in signature file name: %s", signatureFile.Name)
368374
}
369-
// It is lucky that golang only iterates over sorted file names, and that
370-
// lexically latest is the strongest hash
375+
keyfile := matches[2]
376+
if _, ok := keys[keyfile]; !ok {
377+
// Ignore this signature if we don't have the key
378+
continue
379+
}
380+
var digestAlgorithm crypto.Hash
371381
switch signatureType := matches[1]; signatureType {
372382
case "DSA":
373383
// Obsolete
374384
continue
375385
case "RSA":
376386
// Current legacy compat
377-
indexDigestType = crypto.SHA1
387+
digestAlgorithm = crypto.SHA1
378388
case "RSA256":
379389
// Current best practice
380-
indexDigestType = crypto.SHA256
390+
digestAlgorithm = crypto.SHA256
381391
case "RSA512":
382392
// Too big, too slow, not compiled in
383393
continue
384394
default:
385395
return nil, fmt.Errorf("unknown signature format: %s", signatureType)
386396
}
387-
keyfile = matches[2]
388-
signature, err = io.ReadAll(tarReader)
397+
signature, err := io.ReadAll(tarReader)
389398
if err != nil {
390399
return nil, fmt.Errorf("failed to read signature from repository index: %w", err)
391400
}
401+
sigs = append(sigs, Signature{
402+
KeyID: keyfile,
403+
Signature: signature,
404+
DigestAlgorithm: digestAlgorithm,
405+
})
406+
}
407+
if len(sigs) == 0 {
408+
return nil, fmt.Errorf("no signature with known key found in repository index")
392409
}
393410
// we now have the signature bytes and name, get the contents of the rest;
394411
// this should be everything else in the raw gzip file as is.
395412
allBytes := len(b)
396413
unreadBytes := buf.Len()
397414
readBytes := allBytes - unreadBytes
398415
indexData := b[readBytes:]
399-
indexDigest, err := sign.HashData(indexData, indexDigestType)
400-
if err != nil {
401-
return nil, err
402-
}
403-
// now we can check the signature
404-
if keys == nil {
405-
return nil, fmt.Errorf("no keys provided to verify signature")
406-
}
407-
var verified bool
408-
keyData, ok := keys[keyfile]
409-
if ok {
410-
if err := sign.RSAVerifyDigest(indexDigest, indexDigestType, signature, keyData); err != nil {
411-
verified = false
412-
}
413-
}
414-
if !verified {
415-
for _, keyData := range keys {
416-
if err := sign.RSAVerifyDigest(indexDigest, indexDigestType, signature, keyData); err == nil {
417-
verified = true
418-
break
416+
indexDigest := make(map[crypto.Hash][]byte, len(keys))
417+
verified := false
418+
for _, sig := range sigs {
419+
// compute the digest if not already done
420+
if _, hasDigest := indexDigest[sig.DigestAlgorithm]; !hasDigest {
421+
digest, err := sign.HashData(indexData, sig.DigestAlgorithm)
422+
if err != nil {
423+
return nil, fmt.Errorf("failed to compute digest: %w", err)
419424
}
425+
indexDigest[sig.DigestAlgorithm] = digest
426+
}
427+
if err := sign.RSAVerifyDigest(indexDigest[sig.DigestAlgorithm], sig.DigestAlgorithm, sig.Signature, keys[sig.KeyID]); err == nil {
428+
verified = true
429+
break
430+
} else {
431+
clog.FromContext(ctx).Warnf("failed to verify signature for keyfile %s: %v", sig.KeyID, err)
420432
}
421433
}
422434
if !verified {
423-
return nil, fmt.Errorf("no key found to verify signature for keyfile %s; tried all other keys as well", keyfile)
435+
return nil, errors.New("signature verification failed for repository index, for all provided keys")
424436
}
425437
}
426438
// with a valid signature, convert it to an ApkIndex
3.88 KB
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2TxPZPC1Cdb647P0kvi5
3+
wtwcoaFCOGHspL3RmmD2idG2FNZc/rpwyIp2bnprFvOCWm7bKAEl/9JDfAGm7DAP
4+
q/EC7UeJ9yx03jY1I7+Oj1/1UUIyuvfDwyiMxEwcABaivAn9WhYZ5YeZKK8pUYoX
5+
hOPNqgI+N3BY1H+hgg3xgTrzWmfRSsvTEhSqoLHdl4qbsM5EE/T5W7u+96y9J2vn
6+
M89rUP557K3n1QKGzTIGjlesFQd8y+uH0TdAe6AFmxFpftdXRu4KInjdrDOArfBO
7+
cKLB0gHOERkd08JVMWYHd6Ne7BfWpwxmge371GgTj8XYOJrkAj3cg8ked/51Maw0
8+
FLt4aErQR6y02QIluGZ4gwbIZ8y+iBWvoh8egiab3Whr3SwGuX6X8WJbOgKLCyGK
9+
CmrxgPO/ahS4XUWtuTp3woIZBnnHGOBdHjRARkl9PdBgN2HhZPS9vj8rrIqbTp3c
10+
dmqhcsjsgmss6Sb3rOcOL3N3V8+dMiD7VZXeIgVocOpRtLJGQFCupjAfmz5ub4t5
11+
lBI2EgbR9LdSg9mrFoYbG11sU6MLa0iPMA8gU3nG4dzLDpsF9PbXy6sfBXXc8W25
12+
tfwZnV4wHouWEhiQ9Hfc4NOGcs+01Zwg9CZRS2SBKC9iqUPsz3RaTAKWsS/QNTND
13+
Zy9znhL5yGrbHaq2BOlnVqMCAwEAAQ==
14+
-----END PUBLIC KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAizNP/QKfB5NKlVBba6dX
3+
Jzz0/icPE++eM3aAtCARwlAKyRm3ivWM5sAz88lwHFjMAQV0QPD3hDgswcAExTXa
4+
EPN8M0RroxxnWuJLGhz99ONaGOLo66GDkcdBaaZIinXoWmhqg1zcequ6dLDOGArm
5+
ZR99egaBFKyQHvBYHIl5dQ1yT8us/m1Yfb7DlERcU2lf3lFhLDrJsBgVw5QYa5h+
6+
aSneA/B2ZB1BwfyE3F5+UUn0qWRy3Yg526oaWpjjwCaPVq4yaJfg4XuMS82wdVm9
7+
G6awP1PE0EWwgkcc8z8w7BmxFhdCRt0VNjmept14tJwiU0d81j1HPHsIkqWr2TBA
8+
f7Wwp34+ByfA7iHbKXFPgK1MKmNzyANoyPaPRo+x+MU8D2AqUBslKOKrhckyd4tQ
9+
ch3SyaMKMa7hc0PWnFrCCtDTmNZQaquNrUWnbbMtdkjCA/is//W+Lqn+O6BjmR9U
10+
yztR26l2OwGMu6uf3CMUADX/ra4k2BuxSnaPTgm7M19AAzXSnICa+aupgFsIfx1w
11+
UyGlq8dvCz8bnipXW4EbTWTSPdFrxQl/LQUB5w82kMorML1TQEpSLjoSmnhNg+rh
12+
rqIsjp7fwEnX/qTbiwUhgcN1fyLkGGv/UV/H62WcclStKiRHp+ND0ePMABefA+cv
13+
f9tYynbOV78qct+dQKkQ17kCAwEAAQ==
14+
-----END PUBLIC KEY-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwlV2kG9xGtkVWc/dS9nC
3+
Qqef/hx0AysYfrMFetY+mZ0MPvELqOaDJgtjlbMM6w/ujqDtScxbnNNf5/vY/wBh
4+
rHTeKU+oDlkRTsh8ZxKINFfGGSDxR2pKkvPZeVzxdhTNEPK9ZggbFHe5RhXUT9iK
5+
CNgF+Xa3p5A1Yi8zR6zEFJ5EDBMnTMagV2ueLfJrpFMm/4hBn6zWtjyDhDPVZyTD
6+
DyQc2/FIDXM1tJONWDT8wO2xuHf7xKGr6lO/CZca5Pnd/QS5jODoNbTo9VvG+oQt
7+
mRsWsdVFst0vS54s3mchtuAB2NXnWAZZuux9uYPgvO8eqI0RLqCKX7VLJ8tGOTmL
8+
3dblTzKbKbVcy4uHsyH2mZPvPUTO+ck7c58iSeUrOFV50B6zqynAXgCrDo3XMjpI
9+
mLC26LRMqF/Q/RWc9R0K+ZgiLQ7ootyOJILPSR69RhgdQJVt7/gz55dM89yoq/B9
10+
U2V+1mv0rBQmmNgCM1U3QdgUv5WeIm4Ed2avKZBwJLBVR5AxlY9xlwJ7sEU4Ms2t
11+
cEblWhqDze/sSsjCYOAmD4/M/90OnBelg+/BU2jOD4nqQdEQDOZdo+EUVeCIuPwD
12+
kz8kTv8pPtLjKI0jRhYLn2dhg/vTIDJf19iXexGQXOyK/rNwi41i+9snypVb8YSe
13+
cWTnq+m2CFOiXFsDWQh7RsUCAwEAAQ==
14+
-----END PUBLIC KEY-----

0 commit comments

Comments
 (0)