diff --git a/pkg/identity/identity_test.go b/pkg/identity/identity_test.go index 99d4f68e..d7ed7c28 100644 --- a/pkg/identity/identity_test.go +++ b/pkg/identity/identity_test.go @@ -5,6 +5,8 @@ import ( "crypto/ed25519" "crypto/rand" "encoding/hex" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "strings" "testing" @@ -766,3 +768,32 @@ func TestAuthorizeInitiatorMessage_NilSignature(t *testing.T) { t.Error("AuthorizeInitiatorMessage() should fail with nil signature") } } + +func TestVerifyInitiatorMessage_DerivationPathBound(t *testing.T) { + pub, priv, _ := generateTestEd25519Key() + + store := &fileStore{ + initiatorKey: &InitiatorKey{ + Algorithm: types.EventInitiatorKeyTypeEd25519, + Ed25519: pub, + }, + } + + msg := &types.SignTxMessage{ + KeyType: types.KeyTypeSecp256k1, + WalletID: "w", + NetworkInternalCode: "ETH", + TxID: "tx", + Tx: []byte("d"), + DerivationPath: []uint32{44, 60, 0, 0, 0, 1}, + } + raw, err := msg.Raw() + require.NoError(t, err) + msg.Signature = ed25519.Sign(priv, raw) + + require.NoError(t, store.VerifyInitiatorMessage(msg), "should verify") + + // tamper path + msg.DerivationPath = []uint32{44, 60, 0, 0, 0, 999} + assert.Error(t, store.VerifyInitiatorMessage(msg), "should not verify") +} diff --git a/pkg/types/initiator_msg.go b/pkg/types/initiator_msg.go index 61004929..f781be1c 100644 --- a/pkg/types/initiator_msg.go +++ b/pkg/types/initiator_msg.go @@ -64,17 +64,19 @@ type ResharingMessage struct { func (m *SignTxMessage) Raw() ([]byte, error) { // omit the Signature field itself when computing the signed‐over data payload := struct { - KeyType KeyType `json:"key_type"` - WalletID string `json:"wallet_id"` - NetworkInternalCode string `json:"network_internal_code"` - TxID string `json:"tx_id"` - Tx []byte `json:"tx"` + KeyType KeyType `json:"key_type"` + WalletID string `json:"wallet_id"` + NetworkInternalCode string `json:"network_internal_code"` + TxID string `json:"tx_id"` + Tx []byte `json:"tx"` + DerivationPath []uint32 `json:"derivation_path,omitempty"` }{ KeyType: m.KeyType, WalletID: m.WalletID, NetworkInternalCode: m.NetworkInternalCode, TxID: m.TxID, Tx: m.Tx, + DerivationPath: m.DerivationPath, } return json.Marshal(payload) } diff --git a/pkg/types/initiator_msg_test.go b/pkg/types/initiator_msg_test.go index 741e577a..6b0e3d25 100644 --- a/pkg/types/initiator_msg_test.go +++ b/pkg/types/initiator_msg_test.go @@ -1,6 +1,8 @@ package types import ( + "crypto/ed25519" + "crypto/rand" "encoding/json" "testing" @@ -209,3 +211,49 @@ func TestGenerateKeyMessage_EmptyWallet(t *testing.T) { assert.Equal(t, []byte(""), raw) assert.Equal(t, "", msg.InitiatorID()) } + +func TestSignTxMessage_Raw_IncludesDerivationPath(t *testing.T) { + msg := &SignTxMessage{ + KeyType: KeyTypeSecp256k1, + WalletID: "wallet-123", + NetworkInternalCode: "BTC", + TxID: "tx-456", + Tx: []byte("transaction-data"), + Signature: []byte("signature-data"), + DerivationPath: []uint32{44, 60, 0, 0, 0, 1}, + } + raw, err := msg.Raw() + require.NoError(t, err) + assert.NotEmpty(t, raw) + assert.Contains(t, string(raw), `"derivation_path":[44,60,0,0,0,1]`) +} + +func TestSignTxMessage_DerivationPathIsSignatureBound(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + msg := &SignTxMessage{ + KeyType: KeyTypeSecp256k1, + WalletID: "w", + NetworkInternalCode: "ETH", + TxID: "tx", + Tx: []byte("d"), + DerivationPath: []uint32{44, 60, 0, 0, 1}, + } + + // initiator sign on Raw() + raw, err := msg.Raw() + require.NoError(t, err) + assert.NotEmpty(t, raw) + sig := ed25519.Sign(priv, raw) + + // verify pass + rawOK, _ := msg.Raw() + assert.True(t, ed25519.Verify(pub, rawOK, sig), "original raw should be valid") + + // Attacker: change derivation_path, same sig + msg.DerivationPath = []uint32{44, 60, 0, 0, 2} + rawTempered, err := msg.Raw() + require.NoError(t, err) + assert.False(t, ed25519.Verify(pub, rawTempered, sig), "changed derivation_path should invalidate sig") +}