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 <trishank.kuppusamy@datadoghq.com>
Signed-off-by: Zachary Newman <z@znewman.net>

Signed-off-by: Zachary Newman <z@znewman.net>
Co-authored-by: Cédric Van Rompay <97546950+cedricvanrompay-datadog@users.noreply.github.com>
Co-authored-by: Trishank Karthik Kuppusamy <trishank.kuppusamy@datadoghq.com>
diff --git a/client/client_test.go b/client/client_test.go
index 2085dcd..b476440 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -2,6 +2,8 @@
 
 import (
 	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -1270,3 +1272,136 @@
 
 	c.Assert(client.VerifyDigest(hash, "sha256", size, digest), IsNil)
 }
+
+type StateLessSuite struct{}
+
+var _ = Suite(&StateLessSuite{})
+
+func (s *StateLessSuite) TestRejectsMultiSignaturesSameKeyDifferentIDs(c *C) {
+	// In this test Alice and Bob want to create a TUF repo
+	// where a root key rotation would require both their signatures.
+	// Alice uses an old version of Go-TUF where each key gets assigned several IDs.
+	// Bob uses a modern version of Go-TUF that does not produce the same list of IDs for a same key.
+	// This test checks that the TUF client
+	// will not accept a root rotation
+	// signed twice with Alice's key with different key IDs each time.
+	// This test was failing with https://github.com/theupdateframework/go-tuf/tree/ac7b5d7bce18cca5a84a28b021bd6372f450b35b
+	// because the signature verification code was assuming that the key IDs used in the metadata
+	// were the same as the one the TUF library of the client would generate,
+	// breaking the security of threshold signatures.
+
+	// The attack works just the same if Alice is malicious from the beginning
+	// and convinces Bob to sign an initial "root.json"
+	// with additional key IDs for her only key,
+	// but this scenario show that the vulnerability can even impact situations
+	// where Alice is not malicious at all,
+	// she was simply using an old client and an attacker stole her key.
+	// The purpose of threshold signatures in TUF is precisely
+	// to make sure that an attacker cannot forge signatures
+	// if they did not steal a large enough number of keys.
+
+	alice, err := keys.GenerateEd25519Key()
+	if err != nil {
+		panic(err)
+	}
+
+	root := data.NewRoot()
+	root.Version = 1
+	root.Roles["root"] = &data.Role{
+		KeyIDs:    []string{},
+		Threshold: 2, // Note the threshold
+	}
+
+	// reproduces how IDs were computed in
+	// https://github.com/theupdateframework/go-tuf/blob/8e84384bebe3/data/types.go#L50
+	oldTUFIDs := func(k *data.PublicKey) []string {
+		bytes, _ := cjson.EncodeCanonical(k)
+		digest := sha256.Sum256(bytes)
+		ids := []string{hex.EncodeToString(digest[:])}
+
+		if k.Scheme != "" || len(k.Algorithms) != 0 {
+			bytes, _ = cjson.EncodeCanonical(&data.PublicKey{
+				Type:  k.Type,
+				Value: k.Value,
+			})
+			digest = sha256.Sum256(bytes)
+			ids = append(ids, hex.EncodeToString(digest[:]))
+		}
+
+		return ids
+	}
+
+	// Alice adds her key using an old version of go-tuf
+	// which will use several IDs
+	for _, keyID := range oldTUFIDs(alice.PublicData()) {
+		root.Keys[keyID] = alice.PublicData()
+		root.Roles["root"].KeyIDs = append(root.Roles["root"].KeyIDs, keyID)
+	}
+
+	bob, err := keys.GenerateEd25519Key()
+	if err != nil {
+		panic(err)
+	}
+
+	root.AddKey(bob.PublicData())
+	root.Roles["root"].KeyIDs = append(
+		root.Roles["root"].KeyIDs,
+		bob.PublicData().IDs()...,
+	)
+
+	// signer for the other roles, not important
+	delegatedSigner, _ := keys.GenerateEd25519Key()
+	root.AddKey(delegatedSigner.PublicData())
+	for _, role := range []string{"targets", "snapshot", "timestamp"} {
+		root.Roles[role] = &data.Role{
+			KeyIDs:    delegatedSigner.PublicData().IDs(),
+			Threshold: 1,
+		}
+	}
+
+	signedRoot, err := sign.Marshal(root, alice, bob)
+	c.Assert(err, IsNil)
+	rootJSON, err := json.Marshal(signedRoot)
+	c.Assert(err, IsNil)
+
+	// producing evil root using only Alice's key
+
+	evilRoot := root
+	evilRoot.Version = 2
+
+	canonical, err := cjson.EncodeCanonical(evilRoot)
+	c.Assert(err, IsNil)
+	sig, err := alice.SignMessage(canonical)
+	c.Assert(err, IsNil)
+	signedEvilRoot := &data.Signed{
+		Signed:     canonical,
+		Signatures: make([]data.Signature, 0),
+	}
+	for _, keyID := range oldTUFIDs(alice.PublicData()) {
+		signedEvilRoot.Signatures = append(signedEvilRoot.Signatures, data.Signature{
+			Signature: sig,
+			KeyID:     keyID,
+		})
+	}
+	evilRootJSON, err := json.Marshal(signedEvilRoot)
+	c.Assert(err, IsNil)
+
+	// checking that client does not accept root rotation
+	// to evil root
+
+	localStore := MemoryLocalStore()
+	err = localStore.SetMeta("root.json", rootJSON)
+	c.Assert(err, IsNil)
+
+	remoteStore := newFakeRemoteStore()
+	remoteStore.meta["2.root.json"] = newFakeFile(evilRootJSON)
+
+	client := NewClient(localStore, remoteStore)
+
+	err = client.UpdateRoots()
+	if err != nil {
+		c.Assert(err, DeepEquals, verify.ErrRoleThreshold{Expected: 2, Actual: 1})
+	} else {
+		c.Fatalf("client returned no error when updating with evil root")
+	}
+}
diff --git a/verify/verify.go b/verify/verify.go
index c12aaaf..f5675a2 100644
--- a/verify/verify.go
+++ b/verify/verify.go
@@ -92,8 +92,8 @@
 	// Verify that a threshold of keys signed the data. Since keys can have
 	// multiple key ids, we need to protect against multiple attached
 	// signatures that just differ on the key id.
-	seen := make(map[string]struct{})
-	valid := 0
+	verifiedKeyIDs := make(map[string]struct{})
+	numVerifiedKeys := 0
 	for _, sig := range s.Signatures {
 		if !roleData.ValidKey(sig.KeyID) {
 			continue
@@ -104,21 +104,32 @@
 		}
 
 		if err := verifier.Verify(msg, sig.Signature); err != nil {
+			// FIXME: don't err out on the 1st bad signature.
 			return ErrInvalid
 		}
 
 		// Only consider this key valid if we haven't seen any of it's
 		// key ids before.
-		if _, ok := seen[sig.KeyID]; !ok {
-			for _, id := range verifier.MarshalPublicKey().IDs() {
-				seen[id] = struct{}{}
+		// Careful: we must not rely on the key IDs _declared in the file_,
+		// instead we get to decide what key IDs this key correspond to.
+		// XXX dangerous; better stop supporting multiple key IDs altogether.
+		keyIDs := verifier.MarshalPublicKey().IDs()
+		wasKeySeen := false
+		for _, keyID := range keyIDs {
+			if _, present := verifiedKeyIDs[keyID]; present {
+				wasKeySeen = true
+			}
+		}
+		if !wasKeySeen {
+			for _, id := range keyIDs {
+				verifiedKeyIDs[id] = struct{}{}
 			}
 
-			valid++
+			numVerifiedKeys++
 		}
 	}
-	if valid < roleData.Threshold {
-		return ErrRoleThreshold{roleData.Threshold, valid}
+	if numVerifiedKeys < roleData.Threshold {
+		return ErrRoleThreshold{roleData.Threshold, numVerifiedKeys}
 	}
 
 	return nil