Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions packages/wallet/core/src/signers/session/explicit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,7 @@ export class Explicit implements ExplicitSessionSigner {
}

// Sign it
const useDeprecatedHash =
Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) ||
Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions)
const callHash = SessionSignature.hashCallWithReplayProtection(payload, callIdx, chainId, useDeprecatedHash)
const callHash = SessionSignature.hashPayloadWithCallIdx(wallet, payload, callIdx, chainId, sessionManagerAddress)
const sessionSignature = await this._privateKey.signDigest(Bytes.fromHex(callHash))
return {
permissionIndex: BigInt(permissionIndex),
Expand Down
5 changes: 1 addition & 4 deletions packages/wallet/core/src/signers/session/implicit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,7 @@ export class Implicit implements ImplicitSessionSigner {
if (!isSupported) {
throw new Error('Unsupported call')
}
const useDeprecatedHash =
Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) ||
Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions)
const callHash = SessionSignature.hashCallWithReplayProtection(payload, callIdx, chainId, useDeprecatedHash)
const callHash = SessionSignature.hashPayloadWithCallIdx(wallet, payload, callIdx, chainId, sessionManagerAddress)
const sessionSignature = await this._privateKey.signDigest(Bytes.fromHex(callHash))
return {
attestation: this._attestation,
Expand Down
8 changes: 6 additions & 2 deletions packages/wallet/core/test/session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
USDC_ADDRESS,
} from './constants'
import { Extensions } from '@0xsequence/wallet-primitives'
import { ExplicitSessionConfig } from '../../wdk/src/sequence/types/sessions.js'
import { ExplicitSessionConfig } from '../src/utils/session/types.js'

const { PermissionBuilder, ERC20PermissionBuilder } = Utils

Expand All @@ -34,6 +34,10 @@ const ALL_EXTENSIONS = [
name: 'Rc3',
...Extensions.Rc3,
},
{
name: 'Rc4',
...Extensions.Rc4,
},
]

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

// Sign the transaction
expect(sessionManager.signSapient(wallet.address, chainId, payload, imageHash)).rejects.toThrow(
await expect(sessionManager.signSapient(wallet.address, chainId, payload, imageHash)).rejects.toThrow(
`Signer supporting call is expired: ${explicitSigner.address}`,
)
},
Expand Down
4 changes: 2 additions & 2 deletions packages/wallet/dapp-client/src/ChainSessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export class ChainSessionManager {
stateProvider: this.stateProvider,
})
this.sessionManager = new Signers.SessionManager(this.wallet, {
sessionManagerAddress: Extensions.Rc3.sessions,
sessionManagerAddress: Extensions.Rc4.sessions,
provider: this.provider!,
})
this.isInitialized = true
Expand Down Expand Up @@ -730,7 +730,7 @@ export class ChainSessionManager {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const tempManager = new Signers.SessionManager(this.wallet, {
sessionManagerAddress: Extensions.Rc3.sessions,
sessionManagerAddress: Extensions.Rc4.sessions,
provider: this.provider,
})
const topology = await tempManager.getTopology()
Expand Down
7 changes: 7 additions & 0 deletions packages/wallet/primitives/src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,12 @@ export const Rc3: Extensions = {
sessions: '0x0000000000CC58810c33F3a0D78aA1Ed80FaDcD8',
}

//FIXME This is a placeholder for the actual Rc4 extension
export const Rc4: Extensions = {
passkeys: Rc3.passkeys,
recovery: Rc3.recovery,
sessions: '0x6f1092241e82bD0786C5DA6b6919AD38966fff8E',
}

export * as Passkeys from './passkeys.js'
export * as Recovery from './recovery.js'
4 changes: 4 additions & 0 deletions packages/wallet/primitives/src/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ export function isCalls4337_07(payload: Payload): payload is Calls4337_07 {
return payload.type === 'call_4337_07'
}

export function isParented(payload: Payload): payload is Parented {
return 'parentWallets' in payload
}

export function toRecovery<T extends MayRecoveryPayload>(payload: T): Recovery<T> {
if (isRecovery(payload)) {
return payload
Expand Down
56 changes: 41 additions & 15 deletions packages/wallet/primitives/src/session-signature.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Address, Bytes, Hash, Hex } from 'ox'
import { Attestation, Extensions, Payload } from './index.js'
import { MAX_PERMISSIONS_COUNT } from './permission.js'
import {
decodeSessionsTopology,
Expand All @@ -10,7 +11,6 @@ import {
} from './session-config.js'
import { RSY } from './signature.js'
import { minBytesFor, packRSY, unpackRSY } from './utils.js'
import { Attestation, Payload } from './index.js'

export type ImplicitSessionCallSignature = {
attestation: Attestation.Attestation
Expand Down Expand Up @@ -273,22 +273,48 @@ export function decodeSessionSignature(encodedSignatures: Bytes.Bytes): {

// Call encoding

export function hashCallWithReplayProtection(
payload: Payload.Calls,
/**
* Hashes a call with replay protection parameters.
* @param payload The payload to hash.
* @param callIdx The index of the call to hash.
* @param chainId The chain ID. Use 0 when noChainId enabled.
* @param sessionManagerAddress The session manager address to compile the hash for. Only required to support deprecated hash encodings for Dev1, Dev2 and Rc3.
* @returns The hash of the call with replay protection parameters for sessions.
*/
export function hashPayloadWithCallIdx(
wallet: Address.Address,
payload: Payload.Calls & Payload.Parent,
callIdx: number,
chainId: number,
skipCallIdx: boolean = false, // Deprecated. Dev1 and Dev2 support
sessionManagerAddress?: Address.Address,
): Hex.Hex {
const call = payload.calls[callIdx]!
return Hex.fromBytes(
Hash.keccak256(
Bytes.concat(
Bytes.fromNumber(chainId, { size: 32 }),
Bytes.fromNumber(payload.space, { size: 32 }),
Bytes.fromNumber(payload.nonce, { size: 32 }),
skipCallIdx ? Bytes.from([]) : Bytes.fromNumber(callIdx, { size: 32 }),
Bytes.fromHex(Payload.hashCall(call)),
// Support deprecated hashes for Dev1, Dev2 and Rc3
const deprecatedHashing =
sessionManagerAddress &&
(Address.isEqual(sessionManagerAddress, Extensions.Dev1.sessions) ||
Address.isEqual(sessionManagerAddress, Extensions.Dev2.sessions) ||
Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions))
if (deprecatedHashing) {
const call = payload.calls[callIdx]!
const ignoreCallIdx = !Address.isEqual(sessionManagerAddress, Extensions.Rc3.sessions)
return Hex.fromBytes(
Hash.keccak256(
Bytes.concat(
Bytes.fromNumber(chainId, { size: 32 }),
Bytes.fromNumber(payload.space, { size: 32 }),
Bytes.fromNumber(payload.nonce, { size: 32 }),
ignoreCallIdx ? Bytes.from([]) : Bytes.fromNumber(callIdx, { size: 32 }),
Bytes.fromHex(Payload.hashCall(call)),
),
),
),
)
)
}
// Current hashing scheme uses entire payload hash and call index (without last parent)
const parentWallets = payload.parentWallets
if (payload.parentWallets && payload.parentWallets.length > 0) {
payload.parentWallets.pop()
}
const payloadHash = Payload.hash(wallet, chainId, payload)
payload.parentWallets = parentWallets
return Hex.fromBytes(Hash.keccak256(Bytes.concat(payloadHash, Bytes.fromNumber(callIdx, { size: 32 }))))
}
87 changes: 64 additions & 23 deletions packages/wallet/primitives/test/session-signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
encodeSessionCallSignatureForJson,
encodeSessionSignature,
ExplicitSessionCallSignature,
hashCallWithReplayProtection,
hashPayloadWithCallIdx,
ImplicitSessionCallSignature,
isExplicitSessionCallSignature,
isImplicitSessionCallSignature,
Expand All @@ -21,6 +21,7 @@ import {
sessionCallSignatureToJson,
} from '../src/session-signature.js'
import { RSY } from '../src/signature.js'
import { Extensions } from '../src/index.js'

describe('Session Signature', () => {
// Test data
Expand Down Expand Up @@ -444,24 +445,25 @@ describe('Session Signature', () => {
})

describe('Helper Functions', () => {
describe('hashCallWithReplayProtection', () => {
describe('hashPayloadWithCallIdx', () => {
it('should hash call with replay protection parameters', () => {
const result = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const result = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)

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

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

expect(hash1).not.toBe(hash2)
})

it('should produce different hashes for different spaces', () => {
const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const hash2 = hashCallWithReplayProtection(
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(
testAddress1,
{ ...samplePayload, space: samplePayload.space + 1n },
0,
testChainId,
Expand All @@ -471,8 +473,9 @@ describe('Session Signature', () => {
})

it('should produce different hashes for different nonces', () => {
const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const hash2 = hashCallWithReplayProtection(
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(
testAddress1,
{ ...samplePayload, nonce: samplePayload.nonce + 1n },
0,
testChainId,
Expand All @@ -488,17 +491,51 @@ describe('Session Signature', () => {
}
const payload2 = { ...samplePayload, calls: [call2] }

const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const hash2 = hashCallWithReplayProtection(payload2, 0, testChainId)
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(testAddress1, payload2, 0, testChainId)

expect(hash1).not.toBe(hash2)
})

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

const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId)

expect(hash1).not.toBe(hash2)
})

it('should NOT produce different hashes for different wallets when using deprecated hash encoding for Dev1 and Dev2', () => {
// This is ONLY for backward compatibility with Dev1 and Dev2
// This is exploitable and should not be used in practice
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }

const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId, Extensions.Dev2.sessions)

expect(hash1).toBe(hash2)
})

it('should produce different hashes for different wallets when using deprecated hash encoding for Dev1/2, Rc3 and latest', () => {
// This is ONLY for backward compatibility with Rc3
// This is exploitable and should not be used in practice
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }

const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
const hash2 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId, Extensions.Rc3.sessions)
const hash3 = hashPayloadWithCallIdx(testAddress2, payload, 0, testChainId)

expect(hash1).not.toBe(hash2)
expect(hash1).not.toBe(hash3)
expect(hash2).not.toBe(hash3)
})

it('should produce different hashes for same call at different index', () => {
const payload = { ...samplePayload, calls: [sampleCall, sampleCall] }

const hash1 = hashCallWithReplayProtection(payload, 0, testChainId)
const hash2 = hashCallWithReplayProtection(payload, 1, testChainId)
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 1, testChainId)

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

const hash1 = hashCallWithReplayProtection(payload, 0, testChainId, true)
const hash2 = hashCallWithReplayProtection(payload, 1, testChainId, true)
const hash1 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId, Extensions.Dev1.sessions)
const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 1, testChainId, Extensions.Dev1.sessions)

expect(hash1).toBe(hash2)
})

it('should be deterministic', () => {
const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const hash2 = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)

expect(hash1).toBe(hash2)
})
Expand All @@ -526,7 +563,8 @@ describe('Session Signature', () => {
const largeSpace = 2n ** 16n
const largeNonce = 2n ** 24n

const result = hashCallWithReplayProtection(
const result = hashPayloadWithCallIdx(
testAddress1,
{ ...samplePayload, space: largeSpace, nonce: largeNonce },
0,
largeChainId,
Expand All @@ -535,7 +573,7 @@ describe('Session Signature', () => {
})

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

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

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

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

const hash1 = hashCallWithReplayProtection(samplePayload, 0, testChainId)
const hash2 = hashCallWithReplayProtection(payload, 0, testChainId)
const hash1 = hashPayloadWithCallIdx(testAddress1, samplePayload, 0, testChainId)
const hash2 = hashPayloadWithCallIdx(testAddress1, payload, 0, testChainId)

expect(hash1).not.toBe(hash2)
})
Expand Down Expand Up @@ -735,12 +773,15 @@ describe('Session Signature', () => {
const calls: Payload.Call[] = [
sampleCall,
{ ...sampleCall, to: testAddress2 },
{ ...sampleCall, to: testAddress2 }, // Repeat call
{ ...sampleCall, value: 500000000000000000n },
]
const payload = { ...samplePayload, calls: calls }

// Generate hashes for each call
const hashes = calls.map((call) => hashCallWithReplayProtection(payload, calls.indexOf(call), testChainId))
const hashes = calls.map((call) =>
hashPayloadWithCallIdx(testAddress1, payload, calls.indexOf(call), testChainId),
)

// All hashes should be valid and different
for (let i = 0; i < hashes.length; i++) {
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/wdk/src/sequence/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export type ManagerOptions = {
export const ManagerOptionsDefaults = {
verbose: false,

extensions: Extensions.Rc3,
extensions: Extensions.Rc4,
context: Context.Rc3,
context4337: Context.Rc3_4337,
guest: Constants.DefaultGuestAddress,
Expand Down
Loading