Skip to content

Commit 2e56197

Browse files
cedricvanrompay-datadogtrishankatdatadog
authored andcommitted
fix(verify): Fix a vulnerability in the verification of threshold signatures (due to handling of keys with multiple IDs) (theupdateframework#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 <trishank.kuppusamy@datadoghq.com>
1 parent 198f913 commit 2e56197

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"
@@ -1240,3 +1242,136 @@ func (s *ClientSuite) TestVerifyDigest(c *C) {
12401242

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

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)