Skip to content

Commit cc7d305

Browse files
committed
fix: bind setup participants for keygen, keyrefresh and quorumchange too (F-2026-18199)
1 parent e994237 commit cc7d305

3 files changed

Lines changed: 117 additions & 7 deletions

File tree

universalClient/tss/dkls/utils.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package dkls
22

33
import (
44
"crypto/sha256"
5+
"fmt"
6+
7+
session "go-wrapper/go-dkls/sessions"
58
)
69

710
// deriveKeyID derives a key ID bytes from a string key ID.
@@ -22,3 +25,28 @@ func encodeParticipantIDs(participants []string) []byte {
2225
}
2326
return ids
2427
}
28+
29+
// SetupParticipants returns the participant list embedded in a DKLS setup blob,
30+
// in index order. The setup is what actually drives the session, so callers must
31+
// confirm it matches the participants they validated. Otherwise a coordinator
32+
// can present one list for validation and run the session over another.
33+
//
34+
// Party names decode by index and come back empty past the end, which is how the
35+
// list terminates.
36+
func SetupParticipants(setupData []byte) ([]string, error) {
37+
if len(setupData) == 0 {
38+
return nil, fmt.Errorf("setupData is required")
39+
}
40+
var participants []string
41+
for i := 0; ; i++ {
42+
name, err := session.DklsDecodePartyName(setupData, i)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to decode party name at index %d: %w", i, err)
45+
}
46+
if len(name) == 0 {
47+
break
48+
}
49+
participants = append(participants, string(name))
50+
}
51+
return participants, nil
52+
}

universalClient/tss/sessionmanager/sessionmanager.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"fmt"
1010
"math/big"
11+
"slices"
1112
"sync"
1213
"time"
1314

@@ -212,6 +213,17 @@ func (sm *SessionManager) handleSetupMessage(ctx context.Context, senderPeerID s
212213
}
213214
}
214215

216+
// 6d. Same split for the participant list: we validate msg.Participants above
217+
// but the session runs on the list embedded in Payload, so an unbound setup
218+
// could run over a different set than the one we approved.
219+
if err := verifySetupBindsParticipants(msg.Payload, msg.Participants); err != nil {
220+
sm.logger.Error().Err(err).
221+
Str("event_id", msg.EventID).
222+
Str("coordinator", senderPeerID).
223+
Msg("setup message participants do not match the validated list - rejecting")
224+
return err
225+
}
226+
215227
// 7. Create session based on protocol type
216228
session, err := sm.createSession(ctx, event, msg)
217229
if err != nil {
@@ -1010,6 +1022,28 @@ func verifySetupBindsHash(setupData, verifiedHash []byte) error {
10101022
return nil
10111023
}
10121024

1025+
// verifySetupBindsParticipants requires the DKLS setup blob to embed exactly the
1026+
// participant list the caller already validated, in the same order. Index order
1027+
// is part of the protocol, so a reorder is as consequential as a substitution.
1028+
//
1029+
// Note this cannot cover the threshold: the setup embeds one, the wrapper exposes
1030+
// no decoder for it, and the threshold argument the session constructors take is
1031+
// unused. So the coordinator's embedded threshold is authoritative and unchecked.
1032+
func verifySetupBindsParticipants(setupData []byte, validated []string) error {
1033+
if len(validated) == 0 {
1034+
return fmt.Errorf("no validated participants to bind setup message to")
1035+
}
1036+
embedded, err := dkls.SetupParticipants(setupData)
1037+
if err != nil {
1038+
return fmt.Errorf("cannot decode setup message participants: %w", err)
1039+
}
1040+
if !slices.Equal(embedded, validated) {
1041+
return fmt.Errorf("setup message participants %v do not match validated participants %v",
1042+
embedded, validated)
1043+
}
1044+
return nil
1045+
}
1046+
10131047
// maxNonceGap bounds how far above the ceiling base a coordinator may assign.
10141048
// An honest coordinator starts at the pending nonce and increments at most
10151049
// coordinator.PerChainCap times per poll, so pending+PerChainCap is the true

universalClient/tss/sessionmanager/sessionmanager_test.go

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"math/big"
1111
"reflect"
12+
"strings"
1213
"testing"
1314
"time"
1415
"unsafe"
@@ -469,18 +470,17 @@ func TestSessionManager_Integration(t *testing.T) {
469470
Type: "setup",
470471
EventID: event.EventID,
471472
Participants: []string{"validator1", "validator2", "validator3"},
472-
Payload: []byte("invalid setup data"), // Will fail when creating session
473+
Payload: []byte("invalid setup data"), // rejected before a session is created
473474
}
474475

475-
// This will fail at session creation or GetLatestBlockNum, but validation should pass
476+
// Rejected at the setup-binding check (the payload is not a decodable DKLS
477+
// setup), or earlier at GetLatestBlockNum. Either way validation must not
478+
// let an unbound payload reach session creation.
476479
err := sm.HandleIncomingMessage(ctx, "peer1", &msg)
477-
// We expect an error because we can't create a real DKLS session with invalid data
478-
// or because GetLatestBlockNum fails
479480
assert.Error(t, err)
480-
// Error should be about session creation, DKLS library, or no endpoints
481481
assert.True(t,
482-
containsAny(err.Error(), []string{"failed to create session", "DKLS", "dkls", "session", "no endpoints"}),
483-
"error should be about session creation or endpoints, got: %s", err.Error())
482+
containsAny(err.Error(), []string{"failed to create session", "DKLS", "dkls", "session", "setup message", "no endpoints"}),
483+
"error should be about setup binding, session creation or endpoints, got: %s", err.Error())
484484
}
485485

486486
func TestVerifySigningRequest_OutboundDisabled(t *testing.T) {
@@ -1496,3 +1496,51 @@ func TestVerifySetupBindsHash(t *testing.T) {
14961496
assert.Contains(t, err.Error(), "no verified signing hash")
14971497
})
14981498
}
1499+
1500+
// Keygen, keyrefresh and quorumchange have the same split as the sign path: we
1501+
// validate msg.Participants, but the session runs on the list embedded in
1502+
// Payload. The threshold cannot be bound this way, see verifySetupBindsParticipants.
1503+
func TestVerifySetupBindsParticipants(t *testing.T) {
1504+
validated := []string{"validator1", "validator2", "validator3"}
1505+
encode := func(ids []string) []byte {
1506+
return []byte(strings.Join(ids, "\x00"))
1507+
}
1508+
1509+
legitSetup, err := session.DklsKeygenSetupMsgNew(2, nil, encode(validated))
1510+
require.NoError(t, err)
1511+
1512+
t.Run("accepts setup with the validated participants", func(t *testing.T) {
1513+
require.NoError(t, verifySetupBindsParticipants(legitSetup, validated))
1514+
})
1515+
1516+
t.Run("rejects setup with a substituted participant", func(t *testing.T) {
1517+
swapped, err := session.DklsKeygenSetupMsgNew(2, nil,
1518+
encode([]string{"validator1", "validator2", "attacker"}))
1519+
require.NoError(t, err)
1520+
err = verifySetupBindsParticipants(swapped, validated)
1521+
require.Error(t, err)
1522+
assert.Contains(t, err.Error(), "do not match validated participants")
1523+
})
1524+
1525+
t.Run("rejects setup with a dropped participant", func(t *testing.T) {
1526+
fewer, err := session.DklsKeygenSetupMsgNew(2, nil,
1527+
encode([]string{"validator1", "validator2"}))
1528+
require.NoError(t, err)
1529+
require.Error(t, verifySetupBindsParticipants(fewer, validated))
1530+
})
1531+
1532+
// Index order is part of the protocol, so a reorder is as consequential as
1533+
// a substitution.
1534+
t.Run("rejects reordered participants", func(t *testing.T) {
1535+
reordered, err := session.DklsKeygenSetupMsgNew(2, nil,
1536+
encode([]string{"validator3", "validator2", "validator1"}))
1537+
require.NoError(t, err)
1538+
require.Error(t, verifySetupBindsParticipants(reordered, validated))
1539+
})
1540+
1541+
t.Run("rejects undecodable setup and missing validated list", func(t *testing.T) {
1542+
require.Error(t, verifySetupBindsParticipants([]byte("not-a-setup"), validated))
1543+
require.Error(t, verifySetupBindsParticipants(nil, validated))
1544+
require.Error(t, verifySetupBindsParticipants(legitSetup, nil))
1545+
})
1546+
}

0 commit comments

Comments
 (0)