Skip to content

Commit 726c242

Browse files
committed
New sessions replay protection hashes payload
1 parent 7d99a1d commit 726c242

File tree

10 files changed

+583
-536
lines changed

10 files changed

+583
-536
lines changed

packages/wallet/core/src/signers/session/explicit.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,13 +263,7 @@ export class Explicit implements ExplicitSessionSigner {
263263
}
264264

265265
// Sign it
266-
const callHash = SessionSignature.hashCallWithReplayProtection(
267-
wallet,
268-
payload,
269-
callIdx,
270-
chainId,
271-
sessionManagerAddress,
272-
)
266+
const callHash = SessionSignature.hashPayloadWithCallIdx(wallet, payload, callIdx, chainId, sessionManagerAddress)
273267
const sessionSignature = await this._privateKey.signDigest(Bytes.fromHex(callHash))
274268
return {
275269
permissionIndex: BigInt(permissionIndex),

packages/wallet/core/src/signers/session/implicit.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,7 @@ export class Implicit implements ImplicitSessionSigner {
115115
if (!isSupported) {
116116
throw new Error('Unsupported call')
117117
}
118-
const callHash = SessionSignature.hashCallWithReplayProtection(
119-
wallet,
120-
payload,
121-
callIdx,
122-
chainId,
123-
sessionManagerAddress,
124-
)
118+
const callHash = SessionSignature.hashPayloadWithCallIdx(wallet, payload, callIdx, chainId, sessionManagerAddress)
125119
const sessionSignature = await this._privateKey.signDigest(Bytes.fromHex(callHash))
126120
return {
127121
attestation: this._attestation,

packages/wallet/core/test/session-manager.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
USDC_ADDRESS,
1414
} from './constants'
1515
import { Extensions } from '@0xsequence/wallet-primitives'
16-
import { ExplicitSessionConfig } from '../../wdk/src/sequence/types/sessions.js'
16+
import { ExplicitSessionConfig } from '../src/utils/session/types.js'
1717

1818
const { PermissionBuilder, ERC20PermissionBuilder } = Utils
1919

@@ -34,6 +34,10 @@ const ALL_EXTENSIONS = [
3434
name: 'Rc3',
3535
...Extensions.Rc3,
3636
},
37+
{
38+
name: 'Rc4',
39+
...Extensions.Rc4,
40+
},
3741
]
3842

3943
// Handle the increment call being first or last depending on the session manager version
@@ -561,7 +565,7 @@ for (const extension of ALL_EXTENSIONS) {
561565
}
562566

563567
// Sign the transaction
564-
expect(sessionManager.signSapient(wallet.address, chainId, payload, imageHash)).rejects.toThrow(
568+
await expect(sessionManager.signSapient(wallet.address, chainId, payload, imageHash)).rejects.toThrow(
565569
`Signer supporting call is expired: ${explicitSigner.address}`,
566570
)
567571
},

packages/wallet/dapp-client/src/ChainSessionManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export class ChainSessionManager {
221221
stateProvider: this.stateProvider,
222222
})
223223
this.sessionManager = new Signers.SessionManager(this.wallet, {
224-
sessionManagerAddress: Extensions.Rc3.sessions,
224+
sessionManagerAddress: Extensions.Rc4.sessions,
225225
provider: this.provider!,
226226
})
227227
this.isInitialized = true
@@ -730,7 +730,7 @@ export class ChainSessionManager {
730730
for (let attempt = 1; attempt <= maxRetries; attempt++) {
731731
try {
732732
const tempManager = new Signers.SessionManager(this.wallet, {
733-
sessionManagerAddress: Extensions.Rc3.sessions,
733+
sessionManagerAddress: Extensions.Rc4.sessions,
734734
provider: this.provider,
735735
})
736736
const topology = await tempManager.getTopology()

packages/wallet/primitives/src/extensions/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,12 @@ export const Rc3: Extensions = {
2424
sessions: '0x0000000000CC58810c33F3a0D78aA1Ed80FaDcD8',
2525
}
2626

27+
//FIXME This is a placeholder for the actual Rc4 extension
28+
export const Rc4: Extensions = {
29+
passkeys: Rc3.passkeys,
30+
recovery: Rc3.recovery,
31+
sessions: '0x6f1092241e82bD0786C5DA6b6919AD38966fff8E',
32+
}
33+
2734
export * as Passkeys from './passkeys.js'
2835
export * as Recovery from './recovery.js'

packages/wallet/primitives/src/payload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ export function isCalls4337_07(payload: Payload): payload is Calls4337_07 {
184184
return payload.type === 'call_4337_07'
185185
}
186186

187+
export function isParented(payload: Payload): payload is Parented {
188+
return 'parentWallets' in payload
189+
}
190+
187191
export function toRecovery<T extends MayRecoveryPayload>(payload: T): Recovery<T> {
188192
if (isRecovery(payload)) {
189193
return payload

packages/wallet/primitives/src/session-signature.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -281,31 +281,40 @@ export function decodeSessionSignature(encodedSignatures: Bytes.Bytes): {
281281
* @param sessionManagerAddress The session manager address to compile the hash for. Only required to support deprecated hash encodings for Dev1, Dev2 and Rc3.
282282
* @returns The hash of the call with replay protection parameters for sessions.
283283
*/
284-
export function hashCallWithReplayProtection(
284+
export function hashPayloadWithCallIdx(
285285
wallet: Address.Address,
286-
payload: Payload.Calls,
286+
payload: Payload.Calls & Payload.Parent,
287287
callIdx: number,
288288
chainId: number,
289289
sessionManagerAddress?: Address.Address,
290290
): Hex.Hex {
291-
const call = payload.calls[callIdx]!
292291
// Support deprecated hashes for Dev1, Dev2 and Rc3
293-
const ignoreCallIdx =
292+
const deprecatedHashing =
294293
sessionManagerAddress &&
295294
(Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) ||
296-
Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions))
297-
const ignoreWallet =
298-
ignoreCallIdx || (sessionManagerAddress && Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions))
299-
return Hex.fromBytes(
300-
Hash.keccak256(
301-
Bytes.concat(
302-
ignoreWallet ? Bytes.from([]) : Bytes.fromHex(wallet),
303-
Bytes.fromNumber(chainId, { size: 32 }),
304-
Bytes.fromNumber(payload.space, { size: 32 }),
305-
Bytes.fromNumber(payload.nonce, { size: 32 }),
306-
ignoreCallIdx ? Bytes.from([]) : Bytes.fromNumber(callIdx, { size: 32 }),
307-
Bytes.fromHex(Payload.hashCall(call)),
295+
Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions) ||
296+
Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions))
297+
if (deprecatedHashing) {
298+
const call = payload.calls[callIdx]!
299+
const ignoreCallIdx = !Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions)
300+
return Hex.fromBytes(
301+
Hash.keccak256(
302+
Bytes.concat(
303+
Bytes.fromNumber(chainId, { size: 32 }),
304+
Bytes.fromNumber(payload.space, { size: 32 }),
305+
Bytes.fromNumber(payload.nonce, { size: 32 }),
306+
ignoreCallIdx ? Bytes.from([]) : Bytes.fromNumber(callIdx, { size: 32 }),
307+
Bytes.fromHex(Payload.hashCall(call)),
308+
),
308309
),
309-
),
310-
)
310+
)
311+
}
312+
// Current hashing scheme uses entire payload hash and call index (without last parent)
313+
const parentWallets = payload.parentWallets
314+
if (payload.parentWallets && payload.parentWallets.length > 0) {
315+
payload.parentWallets.pop()
316+
}
317+
const payloadHash = Payload.hash(wallet, chainId, payload)
318+
payload.parentWallets = parentWallets
319+
return Hex.fromBytes(Hash.keccak256(Bytes.concat(payloadHash, Bytes.fromNumber(callIdx, { size: 32 }))))
311320
}

packages/wallet/primitives/test/session-signature.test.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
encodeSessionCallSignatureForJson,
1212
encodeSessionSignature,
1313
ExplicitSessionCallSignature,
14-
hashCallWithReplayProtection,
14+
hashPayloadWithCallIdx,
1515
ImplicitSessionCallSignature,
1616
isExplicitSessionCallSignature,
1717
isImplicitSessionCallSignature,
@@ -445,24 +445,24 @@ describe('Session Signature', () => {
445445
})
446446

447447
describe('Helper Functions', () => {
448-
describe('hashCallWithReplayProtection', () => {
448+
describe('hashPayloadWithCallIdx', () => {
449449
it('should hash call with replay protection parameters', () => {
450-
const result = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
450+
const result = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
451451

452452
expect(result).toMatch(/^0x[0-9a-f]{64}$/) // 32-byte hex string
453453
expect(Hex.size(result)).toBe(32)
454454
})
455455

456456
it('should produce different hashes for different chain IDs', () => {
457-
const hash1 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, ChainId.MAINNET)
458-
const hash2 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, ChainId.POLYGON)
457+
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, ChainId.MAINNET)
458+
const hash2 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, ChainId.POLYGON)
459459

460460
expect(hash1).not.toBe(hash2)
461461
})
462462

463463
it('should produce different hashes for different spaces', () => {
464-
const hash1 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
465-
const hash2 = hashCallWithReplayProtection(
464+
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
465+
const hash2 = hashPayloadWithCallIdx(
466466
testAddress1,
467467
{ ...samplePayload, space: samplePayload.space + 1n },
468468
0,
@@ -473,8 +473,8 @@ describe('Session Signature', () => {
473473
})
474474

475475
it('should produce different hashes for different nonces', () => {
476-
const hash1 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
477-
const hash2 = hashCallWithReplayProtection(
476+
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
477+
const hash2 = hashPayloadWithCallIdx(
478478
testAddress1,
479479
{ ...samplePayload, nonce: samplePayload.nonce + 1n },
480480
0,
@@ -491,17 +491,17 @@ describe('Session Signature', () => {
491491
}
492492
const payload2 = { ...samplePayload, calls: [call2] }
493493

494-
const hash1 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
495-
const hash2 = hashCallWithReplayProtection(testAddress1, payload2, 0, testChainId)
494+
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
495+
const hash2 = hashPayloadWithCallIdx(testAddress1, payload2, 0, testChainId)
496496

497497
expect(hash1).not.toBe(hash2)
498498
})
499499

500500
it('should produce different hashes for different wallets', () => {
501501
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }
502502

503-
const hash1 = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId)
504-
const hash2 = hashCallWithReplayProtection(testAddress2, payload, 0, testChainId)
503+
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)
504+
const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId)
505505

506506
expect(hash1).not.toBe(hash2)
507507
})
@@ -511,8 +511,8 @@ describe('Session Signature', () => {
511511
// This is exploitable and should not be used in practice
512512
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }
513513

514-
const hash1 = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
515-
const hash2 = hashCallWithReplayProtection(testAddress2, payload, 0, testChainId, Extensions.Dev2.sessions)
514+
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
515+
const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId, Extensions.Dev2.sessions)
516516

517517
expect(hash1).toBe(hash2)
518518
})
@@ -522,9 +522,9 @@ describe('Session Signature', () => {
522522
// This is exploitable and should not be used in practice
523523
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }
524524

525-
const hash1 = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
526-
const hash2 = hashCallWithReplayProtection(testAddress2, payload, 0, testChainId, Extensions.Rc3.sessions)
527-
const hash3 = hashCallWithReplayProtection(testAddress2, payload, 0, testChainId)
525+
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
526+
const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId, Extensions.Rc3.sessions)
527+
const hash3 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId)
528528

529529
expect(hash1).not.toBe(hash2)
530530
expect(hash1).not.toBe(hash3)
@@ -534,8 +534,8 @@ describe('Session Signature', () => {
534534
it('should produce different hashes for same call at different index', () => {
535535
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }
536536

537-
const hash1 = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId)
538-
const hash2 = hashCallWithReplayProtection(testAddress1, payload, 1, testChainId)
537+
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)
538+
const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 1, testChainId)
539539

540540
expect(hash1).not.toBe(hash2)
541541
})
@@ -545,15 +545,15 @@ describe('Session Signature', () => {
545545
// This is exploitable and should not be used in practice
546546
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }
547547

548-
const hash1 = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
549-
const hash2 = hashCallWithReplayProtection(testAddress1, payload, 1, testChainId, Extensions.Dev1.sessions)
548+
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
549+
const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 1, testChainId, Extensions.Dev1.sessions)
550550

551551
expect(hash1).toBe(hash2)
552552
})
553553

554554
it('should be deterministic', () => {
555-
const hash1 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
556-
const hash2 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
555+
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
556+
const hash2 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
557557

558558
expect(hash1).toBe(hash2)
559559
})
@@ -563,7 +563,7 @@ describe('Session Signature', () => {
563563
const largeSpace = 2n ** 16n
564564
const largeNonce = 2n ** 24n
565565

566-
const result = hashCallWithReplayProtection(
566+
const result = hashPayloadWithCallIdx(
567567
testAddress1,
568568
{ ...samplePayload, space: largeSpace, nonce: largeNonce },
569569
0,
@@ -573,7 +573,7 @@ describe('Session Signature', () => {
573573
})
574574

575575
it('should handle zero values', () => {
576-
const result = hashCallWithReplayProtection(testAddress1, { ...samplePayload, space: 0n, nonce: 0n }, 0, 0)
576+
const result = hashPayloadWithCallIdx(testAddress1, { ...samplePayload, space: 0n, nonce: 0n }, 0, 0)
577577
expect(result).toMatch(/^0x[0-9a-f]{64}$/)
578578
})
579579

@@ -584,7 +584,7 @@ describe('Session Signature', () => {
584584
}
585585
const payload = { ...samplePayload, calls: [callWithEmptyData] }
586586

587-
const result = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId)
587+
const result = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)
588588
expect(result).toMatch(/^0x[0-9a-f]{64}$/)
589589
})
590590

@@ -595,8 +595,8 @@ describe('Session Signature', () => {
595595
}
596596
const payload = { ...samplePayload, calls: [delegateCall] }
597597

598-
const hash1 = hashCallWithReplayProtection(testAddress1, samplePayload, 0, testChainId)
599-
const hash2 = hashCallWithReplayProtection(testAddress1, payload, 0, testChainId)
598+
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
599+
const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)
600600

601601
expect(hash1).not.toBe(hash2)
602602
})
@@ -780,7 +780,7 @@ describe('Session Signature', () => {
780780

781781
// Generate hashes for each call
782782
const hashes = calls.map((call) =>
783-
hashCallWithReplayProtection(testAddress1, payload, calls.indexOf(call), testChainId),
783+
hashPayloadWithCallIdx(testAddress1, payload, calls.indexOf(call), testChainId),
784784
)
785785

786786
// All hashes should be valid and different

packages/wallet/wdk/src/sequence/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export type ManagerOptions = {
8585
export const ManagerOptionsDefaults = {
8686
verbose: false,
8787

88-
extensions: Extensions.Rc3,
88+
extensions: Extensions.Rc4,
8989
context: Context.Rc3,
9090
context4337: Context.Rc3_4337,
9191
guest: Constants.DefaultGuestAddress,

0 commit comments

Comments
 (0)