Skip to content

Commit b6695e4

Browse files
znewman01cedricvanrompay-datadogtrishankatdatadog
authored
fix(verify): backport "Fix a vulnerability in the verification of threshold si… (#375)
fix(verify): Fix a vulnerability in the verification of threshold signatures (due to handling of keys with multiple IDs) (#369) * add test for several signatures same key diff ID * fix verifying threshold signatures * add some comments * rename variables and add comments Co-authored-by: Trishank Karthik Kuppusamy <[email protected]> Signed-off-by: Zachary Newman <[email protected]> Signed-off-by: Zachary Newman <[email protected]> Co-authored-by: Cédric Van Rompay <[email protected]> Co-authored-by: Trishank Karthik Kuppusamy <[email protected]>
1 parent 0d40b25 commit b6695e4

File tree

2 files changed

+154
-8
lines changed

2 files changed

+154
-8
lines changed

client/client_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package client
22

33
import (
44
"bytes"
5+
"crypto/sha256"
6+
"encoding/hex"
57
"encoding/json"
68
"errors"
79
"fmt"
@@ -1270,3 +1272,136 @@ func (s *ClientSuite) TestVerifyDigest(c *C) {
12701272

12711273
c.Assert(client.VerifyDigest(hash, "sha256", size, digest), IsNil)
12721274
}
1275+
1276+
type StateLessSuite struct{}
1277+
1278+
var _ = Suite(&StateLessSuite{})
1279+
1280+
func (s *StateLessSuite) TestRejectsMultiSignaturesSameKeyDifferentIDs(c *C) {
1281+
// In this test Alice and Bob want to create a TUF repo
1282+
// where a root key rotation would require both their signatures.
1283+
// Alice uses an old version of Go-TUF where each key gets assigned several IDs.
1284+
// Bob uses a modern version of Go-TUF that does not produce the same list of IDs for a same key.
1285+
// This test checks that the TUF client
1286+
// will not accept a root rotation
1287+
// signed twice with Alice's key with different key IDs each time.
1288+
// This test was failing with https://github.com/theupdateframework/go-tuf/tree/ac7b5d7bce18cca5a84a28b021bd6372f450b35b
1289+
// because the signature verification code was assuming that the key IDs used in the metadata
1290+
// were the same as the one the TUF library of the client would generate,
1291+
// breaking the security of threshold signatures.
1292+
1293+
// The attack works just the same if Alice is malicious from the beginning
1294+
// and convinces Bob to sign an initial "root.json"
1295+
// with additional key IDs for her only key,
1296+
// but this scenario show that the vulnerability can even impact situations
1297+
// where Alice is not malicious at all,
1298+
// she was simply using an old client and an attacker stole her key.
1299+
// The purpose of threshold signatures in TUF is precisely
1300+
// to make sure that an attacker cannot forge signatures
1301+
// if they did not steal a large enough number of keys.
1302+
1303+
alice, err := keys.GenerateEd25519Key()
1304+
if err != nil {
1305+
panic(err)
1306+
}
1307+
1308+
root := data.NewRoot()
1309+
root.Version = 1
1310+
root.Roles["root"] = &data.Role{
1311+
KeyIDs: []string{},
1312+
Threshold: 2, // Note the threshold
1313+
}
1314+
1315+
// reproduces how IDs were computed in
1316+
// https://github.com/theupdateframework/go-tuf/blob/8e84384bebe3/data/types.go#L50
1317+
oldTUFIDs := func(k *data.PublicKey) []string {
1318+
bytes, _ := cjson.EncodeCanonical(k)
1319+
digest := sha256.Sum256(bytes)
1320+
ids := []string{hex.EncodeToString(digest[:])}
1321+
1322+
if k.Scheme != "" || len(k.Algorithms) != 0 {
1323+
bytes, _ = cjson.EncodeCanonical(&data.PublicKey{
1324+
Type: k.Type,
1325+
Value: k.Value,
1326+
})
1327+
digest = sha256.Sum256(bytes)
1328+
ids = append(ids, hex.EncodeToString(digest[:]))
1329+
}
1330+
1331+
return ids
1332+
}
1333+
1334+
// Alice adds her key using an old version of go-tuf
1335+
// which will use several IDs
1336+
for _, keyID := range oldTUFIDs(alice.PublicData()) {
1337+
root.Keys[keyID] = alice.PublicData()
1338+
root.Roles["root"].KeyIDs = append(root.Roles["root"].KeyIDs, keyID)
1339+
}
1340+
1341+
bob, err := keys.GenerateEd25519Key()
1342+
if err != nil {
1343+
panic(err)
1344+
}
1345+
1346+
root.AddKey(bob.PublicData())
1347+
root.Roles["root"].KeyIDs = append(
1348+
root.Roles["root"].KeyIDs,
1349+
bob.PublicData().IDs()...,
1350+
)
1351+
1352+
// signer for the other roles, not important
1353+
delegatedSigner, _ := keys.GenerateEd25519Key()
1354+
root.AddKey(delegatedSigner.PublicData())
1355+
for _, role := range []string{"targets", "snapshot", "timestamp"} {
1356+
root.Roles[role] = &data.Role{
1357+
KeyIDs: delegatedSigner.PublicData().IDs(),
1358+
Threshold: 1,
1359+
}
1360+
}
1361+
1362+
signedRoot, err := sign.Marshal(root, alice, bob)
1363+
c.Assert(err, IsNil)
1364+
rootJSON, err := json.Marshal(signedRoot)
1365+
c.Assert(err, IsNil)
1366+
1367+
// producing evil root using only Alice's key
1368+
1369+
evilRoot := root
1370+
evilRoot.Version = 2
1371+
1372+
canonical, err := cjson.EncodeCanonical(evilRoot)
1373+
c.Assert(err, IsNil)
1374+
sig, err := alice.SignMessage(canonical)
1375+
c.Assert(err, IsNil)
1376+
signedEvilRoot := &data.Signed{
1377+
Signed: canonical,
1378+
Signatures: make([]data.Signature, 0),
1379+
}
1380+
for _, keyID := range oldTUFIDs(alice.PublicData()) {
1381+
signedEvilRoot.Signatures = append(signedEvilRoot.Signatures, data.Signature{
1382+
Signature: sig,
1383+
KeyID: keyID,
1384+
})
1385+
}
1386+
evilRootJSON, err := json.Marshal(signedEvilRoot)
1387+
c.Assert(err, IsNil)
1388+
1389+
// checking that client does not accept root rotation
1390+
// to evil root
1391+
1392+
localStore := MemoryLocalStore()
1393+
err = localStore.SetMeta("root.json", rootJSON)
1394+
c.Assert(err, IsNil)
1395+
1396+
remoteStore := newFakeRemoteStore()
1397+
remoteStore.meta["2.root.json"] = newFakeFile(evilRootJSON)
1398+
1399+
client := NewClient(localStore, remoteStore)
1400+
1401+
err = client.UpdateRoots()
1402+
if err != nil {
1403+
c.Assert(err, DeepEquals, verify.ErrRoleThreshold{Expected: 2, Actual: 1})
1404+
} else {
1405+
c.Fatalf("client returned no error when updating with evil root")
1406+
}
1407+
}

verify/verify.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ func (db *DB) VerifySignatures(s *data.Signed, role string) error {
9292
// Verify that a threshold of keys signed the data. Since keys can have
9393
// multiple key ids, we need to protect against multiple attached
9494
// signatures that just differ on the key id.
95-
seen := make(map[string]struct{})
96-
valid := 0
95+
verifiedKeyIDs := make(map[string]struct{})
96+
numVerifiedKeys := 0
9797
for _, sig := range s.Signatures {
9898
if !roleData.ValidKey(sig.KeyID) {
9999
continue
@@ -104,21 +104,32 @@ func (db *DB) VerifySignatures(s *data.Signed, role string) error {
104104
}
105105

106106
if err := verifier.Verify(msg, sig.Signature); err != nil {
107+
// FIXME: don't err out on the 1st bad signature.
107108
return ErrInvalid
108109
}
109110

110111
// Only consider this key valid if we haven't seen any of it's
111112
// key ids before.
112-
if _, ok := seen[sig.KeyID]; !ok {
113-
for _, id := range verifier.MarshalPublicKey().IDs() {
114-
seen[id] = struct{}{}
113+
// Careful: we must not rely on the key IDs _declared in the file_,
114+
// instead we get to decide what key IDs this key correspond to.
115+
// XXX dangerous; better stop supporting multiple key IDs altogether.
116+
keyIDs := verifier.MarshalPublicKey().IDs()
117+
wasKeySeen := false
118+
for _, keyID := range keyIDs {
119+
if _, present := verifiedKeyIDs[keyID]; present {
120+
wasKeySeen = true
121+
}
122+
}
123+
if !wasKeySeen {
124+
for _, id := range keyIDs {
125+
verifiedKeyIDs[id] = struct{}{}
115126
}
116127

117-
valid++
128+
numVerifiedKeys++
118129
}
119130
}
120-
if valid < roleData.Threshold {
121-
return ErrRoleThreshold{roleData.Threshold, valid}
131+
if numVerifiedKeys < roleData.Threshold {
132+
return ErrRoleThreshold{roleData.Threshold, numVerifiedKeys}
122133
}
123134

124135
return nil

0 commit comments

Comments
 (0)