diff --git a/4337/docker-compose.yaml b/4337/docker-compose.yaml index a6ad32012..0d189872f 100644 --- a/4337/docker-compose.yaml +++ b/4337/docker-compose.yaml @@ -2,7 +2,7 @@ version: '3.8' services: geth: - image: docker.io/ethereum/client-go:latest + image: docker.io/ethereum/client-go:stable restart: always environment: GETH_DEV: 'true' diff --git a/4337/docker/bundler/Dockerfile b/4337/docker/bundler/Dockerfile index 3fe362bc9..6dec50a66 100644 --- a/4337/docker/bundler/Dockerfile +++ b/4337/docker/bundler/Dockerfile @@ -2,8 +2,7 @@ FROM docker.io/library/node:18 RUN git clone https://github.com/eth-infinitism/bundler /src/bundler WORKDIR /src/bundler -# v0.6.1 -RUN git checkout 30dc20da10214415df60a5ee15a6bec0975c9af1 +RUN git checkout v0.6.1 RUN yarn && yarn preprocess ENTRYPOINT ["yarn", "bundler"] diff --git a/4337/src/utils/execution.ts b/4337/src/utils/execution.ts index 4dd101e24..6164e6042 100644 --- a/4337/src/utils/execution.ts +++ b/4337/src/utils/execution.ts @@ -41,11 +41,33 @@ export const signHash = async (signer: Signer, hash: string): Promise { +const sortSignatures = (signatures: SafeSignature[]) => { signatures.sort((left, right) => left.signer.toLowerCase().localeCompare(right.signer.toLowerCase())) +} + +export const buildSignatureBytes = (signatures: SafeSignature[]): string => { + sortSignatures(signatures) return ethers.concat(signatures.map((signature) => signature.data)) } +export const buildContractSignatureBytes = (signatures: SafeSignature[]): string => { + sortSignatures(signatures) + const start = 65 * signatures.length + const { segments } = signatures.reduce( + ({ segments, offset }, { signer, data }) => { + return { + segments: [...segments, ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [signer, start + offset, 0])], + offset: offset + 32 + ethers.dataLength(data), + } + }, + { segments: [] as string[], offset: 0 }, + ) + return ethers.concat([ + ...segments, + ...signatures.map(({ data }) => ethers.solidityPacked(['uint256', 'bytes'], [ethers.dataLength(data), data])), + ]) +} + export const logGas = async (message: string, tx: Promise, skip?: boolean): Promise => { return tx.then(async (result) => { const receipt = await result.wait() diff --git a/4337/test/e2e/SingletonSigners.spec.ts b/4337/test/e2e/SingletonSigners.spec.ts index 9d95966e0..d7729bfc8 100644 --- a/4337/test/e2e/SingletonSigners.spec.ts +++ b/4337/test/e2e/SingletonSigners.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' import { deployments, ethers, network } from 'hardhat' -import { buildSignatureBytes } from '../../src/utils/execution' +import { buildContractSignatureBytes } from '../../src/utils/execution' import { buildUserOperationFromSafeUserOperation, buildSafeUserOpTransaction } from '../../src/utils/userOp' import { bundlerRpc, encodeMultiSendTransactions, prepareAccounts, waitForUserOp } from '../utils/e2e' @@ -107,18 +107,15 @@ describe('E2E - Singleton Signers', () => { const opHash = await validator.getOperationHash( buildUserOperationFromSafeUserOperation({ safeOp, - signature: buildSignatureBytes([]), + signature: '0x', }), ) - const signature = ethers.concat([ - buildSignatureBytes( - customSigners.map(({ signer }, i) => ({ - signer: signer.target as string, - data: ethers.solidityPacked(['uint256', 'uint256', 'uint8'], [signer.target, 65 * customSigners.length + 64 * i, 0]), - })), - ), - ...customSigners.map(({ key }) => ethers.solidityPacked(['uint256', 'bytes'], [32, ethers.toBeHex(BigInt(opHash) ^ key)])), - ]) + const signature = buildContractSignatureBytes( + customSigners.map(({ signer, key }) => ({ + signer: signer.target as string, + data: ethers.toBeHex(BigInt(opHash) ^ key), + })), + ) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, diff --git a/4337/test/e2e/WebAuthnSigner.spec.ts b/4337/test/e2e/WebAuthnSigner.spec.ts index a6b9b5306..42b7cf8dd 100644 --- a/4337/test/e2e/WebAuthnSigner.spec.ts +++ b/4337/test/e2e/WebAuthnSigner.spec.ts @@ -1,9 +1,8 @@ import { expect } from 'chai' -import CBOR from 'cbor' import { deployments, ethers, network } from 'hardhat' import { bundlerRpc, prepareAccounts, waitForUserOp } from '../utils/e2e' import { chainId } from '../utils/encoding' -import { AuthenticatorAttestationResponse, WebAuthnCredentials, base64UrlEncode } from '../utils/webauthn' +import { WebAuthnCredentials, extractChallengeOffset, extractPublicKey, extractSignature } from '../utils/webauthn' describe('E2E - WebAuthn Signers', () => { before(function () { @@ -13,7 +12,7 @@ describe('E2E - WebAuthn Signers', () => { }) const setupTests = deployments.createFixture(async ({ deployments }) => { - const { EntryPoint, Safe4337Module, SafeSignerLaunchpad, SafeProxyFactory, AddModulesLib, SafeL2, MultiSend } = await deployments.run() + const { EntryPoint, Safe4337Module, SafeSignerLaunchpad, SafeProxyFactory, AddModulesLib, SafeL2 } = await deployments.run() const [user] = await prepareAccounts() const bundler = bundlerRpc() @@ -23,7 +22,6 @@ describe('E2E - WebAuthn Signers', () => { const addModulesLib = await ethers.getContractAt('AddModulesLib', AddModulesLib.address) const signerLaunchpad = await ethers.getContractAt('SafeSignerLaunchpad', SafeSignerLaunchpad.address) const singleton = await ethers.getContractAt('SafeL2', SafeL2.address) - const multiSend = await ethers.getContractAt('MultiSend', MultiSend.address) const WebAuthnSignerFactory = await ethers.getContractFactory('WebAuthnSignerFactory') const signerFactory = await WebAuthnSignerFactory.deploy() @@ -41,7 +39,6 @@ describe('E2E - WebAuthn Signers', () => { entryPoint, signerLaunchpad, singleton, - multiSend, signerFactory, navigator, } @@ -200,76 +197,4 @@ describe('E2E - WebAuthn Signers', () => { const safeInstance = await ethers.getContractAt('SafeL2', safe) expect(await safeInstance.getOwners()).to.deep.equal([signerAddress]) }) - - /** - * Extract the x and y coordinates of the public key from a created public key credential. - * Inspired from . - */ - function extractPublicKey(response: AuthenticatorAttestationResponse): { x: bigint; y: bigint } { - const attestationObject = CBOR.decode(response.attestationObject) - const authDataView = new DataView(attestationObject.authData.buffer) - const credentialIdLength = authDataView.getUint16(53) - const cosePublicKey = attestationObject.authData.slice(55 + credentialIdLength) - const key: Map = CBOR.decode(cosePublicKey) - const bn = (bytes: Uint8Array) => BigInt(ethers.hexlify(bytes)) - return { - x: bn(key.get(-2) as Uint8Array), - y: bn(key.get(-3) as Uint8Array), - } - } - - /** - * Compute the challenge offset in the client data JSON. This is the offset, in bytes, of the - * value associated with the `challenge` key in the JSON blob. - */ - function extractChallengeOffset(response: AuthenticatorAssertionResponse, challenge: string): number { - const clientDataJSON = new TextDecoder('utf-8').decode(response.clientDataJSON) - - const encodedChallenge = base64UrlEncode(challenge) - const offset = clientDataJSON.indexOf(encodedChallenge) - if (offset < 0) { - throw new Error('challenge not found in client data JSON') - } - - return offset - } - - /** - * Extracts the signature into R and S values from the authenticator response. - * - * See: - * - - * - - */ - function extractSignature(response: AuthenticatorAssertionResponse): [bigint, bigint] { - const check = (x: boolean) => { - if (!x) { - throw new Error('invalid signature encoding') - } - } - - // Decode the DER signature. Note that we assume that all lengths fit into 8-bit integers, - // which is true for the kinds of signatures we are decoding but generally false. I.e. this - // code should not be used in any serious application. - const view = new DataView(response.signature) - - // check that the sequence header is valid - check(view.getUint8(0) === 0x30) - check(view.getUint8(1) === view.byteLength - 2) - - // read r and s - const readInt = (offset: number) => { - check(view.getUint8(offset) === 0x02) - const len = view.getUint8(offset + 1) - const start = offset + 2 - const end = start + len - const n = BigInt(ethers.hexlify(new Uint8Array(view.buffer.slice(start, end)))) - check(n < ethers.MaxUint256) - return [n, end] as const - } - const [r, sOffset] = readInt(2) - const [s] = readInt(sOffset) - - return [r, s] - } }) diff --git a/4337/test/eip4337/EIP4337WebAuthn.spec.ts b/4337/test/eip4337/EIP4337WebAuthn.spec.ts new file mode 100644 index 000000000..d665cea1a --- /dev/null +++ b/4337/test/eip4337/EIP4337WebAuthn.spec.ts @@ -0,0 +1,273 @@ +import { expect } from 'chai' +import { deployments, ethers } from 'hardhat' +import { deployReferenceEntryPoint } from '../utils/setup' +import { buildContractSignatureBytes, logGas } from '../../src/utils/execution' +import { buildSafeUserOpTransaction, buildUserOperationFromSafeUserOperation, calculateSafeOperationHash } from '../../src/utils/userOp' +import { chainId } from '../utils/encoding' +import { WebAuthnCredentials, extractChallengeOffset, extractPublicKey, extractSignature } from '../utils/webauthn' +import { Safe4337 } from '../../src/utils/safe' + +describe('Safe4337Module - WebAuthn Owner', () => { + const setupTests = deployments.createFixture(async ({ deployments }) => { + const { AddModulesLib, SafeL2, SafeProxyFactory } = await deployments.fixture() + + const [deployer, relayer, user] = await ethers.getSigners() + const entryPoint = await deployReferenceEntryPoint(deployer, relayer) + const moduleFactory = await ethers.getContractFactory('Safe4337Module') + const module = await moduleFactory.deploy(entryPoint.target) + const proxyFactory = await ethers.getContractAt('SafeProxyFactory', SafeProxyFactory.address) + const addModulesLib = await ethers.getContractAt('AddModulesLib', AddModulesLib.address) + const signerLaunchpadFactory = await ethers.getContractFactory('SafeSignerLaunchpad') + const signerLaunchpad = await signerLaunchpadFactory.deploy(entryPoint.target) + const singleton = await ethers.getContractAt('SafeL2', SafeL2.address) + + const WebAuthnSignerFactory = await ethers.getContractFactory('WebAuthnSignerFactory') + const signerFactory = await WebAuthnSignerFactory.deploy() + + const navigator = { + credentials: new WebAuthnCredentials(), + } + + return { + user, + proxyFactory, + addModulesLib, + module, + entryPoint, + signerLaunchpad, + singleton, + signerFactory, + navigator, + } + }) + + describe('executeUserOp - new account', () => { + it('should execute user operation', async () => { + const { user, proxyFactory, addModulesLib, module, entryPoint, signerLaunchpad, singleton, signerFactory, navigator } = + await setupTests() + + const credential = navigator.credentials.create({ + publicKey: { + rp: { + name: 'Safe', + id: 'safe.global', + }, + user: { + id: ethers.getBytes(ethers.id('chucknorris')), + name: 'chucknorris', + displayName: 'Chuck Norris', + }, + challenge: ethers.toBeArray(Date.now()), + pubKeyCredParams: [{ type: 'public-key', alg: -7 }], + }, + }) + const publicKey = extractPublicKey(credential.response) + const signerData = ethers.solidityPacked(['uint256', 'uint256'], [publicKey.x, publicKey.y]) + const signerAddress = await signerFactory.getSigner(signerData) + + const safeInit = { + singleton: singleton.target, + signerFactory: signerFactory.target, + signerData, + setupTo: addModulesLib.target, + setupData: addModulesLib.interface.encodeFunctionData('enableModules', [[module.target]]), + fallbackHandler: module.target, + } + const safeInitHash = ethers.TypedDataEncoder.hash( + { verifyingContract: await signerLaunchpad.getAddress(), chainId: await chainId() }, + { + SafeInit: [ + { type: 'address', name: 'singleton' }, + { type: 'address', name: 'signerFactory' }, + { type: 'bytes', name: 'signerData' }, + { type: 'address', name: 'setupTo' }, + { type: 'bytes', name: 'setupData' }, + { type: 'address', name: 'fallbackHandler' }, + ], + }, + safeInit, + ) + + expect( + await signerLaunchpad.getInitHash( + safeInit.singleton, + safeInit.signerFactory, + safeInit.signerData, + safeInit.setupTo, + safeInit.setupData, + safeInit.fallbackHandler, + ), + ).to.equal(safeInitHash) + + const launchpadInitializer = signerLaunchpad.interface.encodeFunctionData('preValidationSetup', [ + safeInitHash, + ethers.ZeroAddress, + '0x', + ]) + const safeSalt = Date.now() + const safe = await proxyFactory.createProxyWithNonce.staticCall(signerLaunchpad.target, launchpadInitializer, safeSalt) + + const userOp = { + sender: safe, + nonce: ethers.toBeHex(await entryPoint.getNonce(safe, 0)), + initCode: ethers.solidityPacked( + ['address', 'bytes'], + [ + proxyFactory.target, + proxyFactory.interface.encodeFunctionData('createProxyWithNonce', [signerLaunchpad.target, launchpadInitializer, safeSalt]), + ], + ), + callData: signerLaunchpad.interface.encodeFunctionData('initializeThenUserOp', [ + safeInit.singleton, + safeInit.signerFactory, + safeInit.signerData, + safeInit.setupTo, + safeInit.setupData, + safeInit.fallbackHandler, + module.interface.encodeFunctionData('executeUserOp', [user.address, ethers.parseEther('0.5'), '0x', 0]), + ]), + callGasLimit: ethers.toBeHex(2500000), + verificationGasLimit: ethers.toBeHex(500000), + preVerificationGas: ethers.toBeHex(60000), + maxFeePerGas: ethers.toBeHex(10000000000), + maxPriorityFeePerGas: ethers.toBeHex(10000000000), + paymasterAndData: '0x', + } + + const safeInitOp = { + userOpHash: await entryPoint.getUserOpHash({ ...userOp, signature: '0x' }), + validAfter: 0, + validUntil: 0, + entryPoint: entryPoint.target, + } + const safeInitOpHash = ethers.TypedDataEncoder.hash( + { verifyingContract: await signerLaunchpad.getAddress(), chainId: await chainId() }, + { + SafeInitOp: [ + { type: 'bytes32', name: 'userOpHash' }, + { type: 'uint48', name: 'validAfter' }, + { type: 'uint48', name: 'validUntil' }, + { type: 'address', name: 'entryPoint' }, + ], + }, + safeInitOp, + ) + + const assertion = navigator.credentials.get({ + publicKey: { + challenge: ethers.getBytes(safeInitOpHash), + rpId: 'safe.global', + allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], + }, + }) + const signature = ethers.solidityPacked( + ['uint48', 'uint48', 'bytes'], + [ + safeInitOp.validAfter, + safeInitOp.validUntil, + ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes', 'bytes', 'uint256', 'uint256[2]'], + [ + new Uint8Array(assertion.response.authenticatorData), + new Uint8Array(assertion.response.clientDataJSON), + extractChallengeOffset(assertion.response, safeInitOpHash), + extractSignature(assertion.response), + ], + ), + ], + ) + + await user.sendTransaction({ to: safe, value: ethers.parseEther('1') }).then((tx) => tx.wait()) + expect(await ethers.provider.getBalance(safe)).to.equal(ethers.parseEther('1')) + expect(await ethers.provider.getCode(safe)).to.equal('0x') + expect(await ethers.provider.getCode(signerAddress)).to.equal('0x') + + await logGas('WebAuthn signer Safe deployment', entryPoint.handleOps([{ ...userOp, signature }], user.address)) + + expect(await ethers.provider.getBalance(safe)).to.be.lessThanOrEqual(ethers.parseEther('0.5')) + expect(await ethers.provider.getCode(safe)).to.not.equal('0x') + expect(await ethers.provider.getCode(signerAddress)).to.not.equal('0x') + + const [implementation] = ethers.AbiCoder.defaultAbiCoder().decode(['address'], await ethers.provider.getStorage(safe, 0)) + expect(implementation).to.equal(singleton.target) + + const safeInstance = await ethers.getContractAt('SafeL2', safe) + expect(await safeInstance.getOwners()).to.deep.equal([signerAddress]) + }) + }) + + describe('executeUserOp - existing account', () => { + it('should execute user operation', async () => { + const { user, proxyFactory, addModulesLib, module, entryPoint, singleton, signerFactory, navigator } = await setupTests() + const credential = navigator.credentials.create({ + publicKey: { + rp: { + name: 'Safe', + id: 'safe.global', + }, + user: { + id: ethers.getBytes(ethers.id('chucknorris')), + name: 'chucknorris', + displayName: 'Chuck Norris', + }, + challenge: ethers.toBeArray(Date.now()), + pubKeyCredParams: [{ type: 'public-key', alg: -7 }], + }, + }) + const publicKey = extractPublicKey(credential.response) + const signerData = ethers.solidityPacked(['uint256', 'uint256'], [publicKey.x, publicKey.y]) + await signerFactory.createSigner(signerData) + const signer = await ethers.getContractAt('WebAuthnSigner', await signerFactory.getSigner(signerData)) + + const safe = await Safe4337.withSigner(await signer.getAddress(), { + safeSingleton: await singleton.getAddress(), + entryPoint: await entryPoint.getAddress(), + erc4337module: await module.getAddress(), + proxyFactory: await proxyFactory.getAddress(), + addModulesLib: await addModulesLib.getAddress(), + proxyCreationCode: await proxyFactory.proxyCreationCode(), + chainId: Number(await chainId()), + }) + await safe.deploy(user) + + const safeOp = buildSafeUserOpTransaction( + safe.address, + user.address, + ethers.parseEther('0.5'), + '0x', + '0', + await entryPoint.getAddress(), + ) + const safeOpHash = calculateSafeOperationHash(await module.getAddress(), safeOp, await chainId()) + const assertion = navigator.credentials.get({ + publicKey: { + challenge: ethers.getBytes(safeOpHash), + rpId: 'safe.global', + allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], + }, + }) + const signature = buildContractSignatureBytes([ + { + signer: signer.target as string, + data: ethers.AbiCoder.defaultAbiCoder().encode( + ['bytes', 'bytes', 'uint256', 'uint256[2]'], + [ + new Uint8Array(assertion.response.authenticatorData), + new Uint8Array(assertion.response.clientDataJSON), + extractChallengeOffset(assertion.response, safeOpHash), + extractSignature(assertion.response), + ], + ), + }, + ]) + + await user.sendTransaction({ to: safe.address, value: ethers.parseEther('1') }).then((tx) => tx.wait()) + expect(await ethers.provider.getBalance(safe.address)).to.equal(ethers.parseEther('1')) + + const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature }) + await logGas('WebAuthn signer Safe operation', entryPoint.handleOps([userOp], user.address)) + + expect(await ethers.provider.getBalance(safe.address)).to.be.lessThanOrEqual(ethers.parseEther('0.5')) + }) + }) +}) diff --git a/4337/test/eip4337/ReferenceEntryPoint.spec.ts b/4337/test/eip4337/ReferenceEntryPoint.spec.ts index 93622fefc..ff746dc7c 100644 --- a/4337/test/eip4337/ReferenceEntryPoint.spec.ts +++ b/4337/test/eip4337/ReferenceEntryPoint.spec.ts @@ -1,10 +1,9 @@ import { expect } from 'chai' import { deployments, ethers } from 'hardhat' import { time } from '@nomicfoundation/hardhat-network-helpers' -import { EventLog, Log, Signer } from 'ethers' -import EntryPointArtifact from '@account-abstraction/contracts/artifacts/EntryPoint.json' -import { getFactory, getAddModulesLib } from '../utils/setup' -import { buildSignatureBytes, logGas } from '../../src/utils/execution' +import { EventLog, Log } from 'ethers' +import { deployReferenceEntryPoint, getFactory, getAddModulesLib } from '../utils/setup' +import { buildContractSignatureBytes, buildSignatureBytes, logGas } from '../../src/utils/execution' import { buildSafeUserOpTransaction, buildUserOperationFromSafeUserOperation, @@ -19,7 +18,7 @@ describe('Safe4337Module - Reference EntryPoint', () => { await deployments.fixture() const [deployer, user, relayer] = await ethers.getSigners() - const entryPoint = await deployEntryPoint(deployer, relayer) + const entryPoint = await deployReferenceEntryPoint(deployer, relayer) const moduleFactory = await ethers.getContractFactory('Safe4337Module') const module = await moduleFactory.deploy(await entryPoint.getAddress()) const proxyFactory = await getFactory() @@ -49,17 +48,6 @@ describe('Safe4337Module - Reference EntryPoint', () => { } } - const deployEntryPoint = async (deployer: Signer, relayer: Signer) => { - const { abi, bytecode } = EntryPointArtifact - const transaction = await deployer.sendTransaction({ data: bytecode }) - const receipt = await transaction.wait() - const contractAddress = receipt.contractAddress - if (contractAddress === null) { - throw new Error(`contract deployment transaction ${transaction.hash} missing address`) - } - return new ethers.Contract(contractAddress, abi, relayer) - } - it('should deploy a Safe and execute transactions', async () => { const { user, relayer, safe, validator, entryPoint } = await setupTests() @@ -180,42 +168,23 @@ describe('Safe4337Module - Reference EntryPoint', () => { }, ) const opData = calculateSafeOperationData(await validator.getAddress(), safeOp, await chainId()) - const signature = ethers.solidityPacked( - ['bytes', 'bytes'], - [ - buildSignatureBytes([ + const signature = buildContractSignatureBytes([ + { + signer: parentSafe.address, + data: await user.signTypedData( { - signer: parentSafe.address, - data: ethers.solidityPacked( - ['bytes32', 'bytes32', 'uint8'], - [ - ethers.toBeHex(parentSafe.address, 32), // `r` holds the contract signer - ethers.toBeHex(65, 32), // `s` holds the offset of the signature bytes - 0, // `v` of 0 indicates a contract signer - ], - ), + verifyingContract: parentSafe.address, + chainId: await chainId(), + }, + { + SafeMessage: [{ type: 'bytes', name: 'message' }], + }, + { + message: opData, }, - ]), - ethers.solidityPacked( - ['uint256', 'bytes'], - [ - 65, // signature length - await user.signTypedData( - { - verifyingContract: parentSafe.address, - chainId: await chainId(), - }, - { - SafeMessage: [{ type: 'bytes', name: 'message' }], - }, - { - message: opData, - }, - ), - ], ), - ], - ) + }, + ]) const userOp = buildUserOperationFromSafeUserOperation({ safeOp, signature, diff --git a/4337/test/utils/setup.ts b/4337/test/utils/setup.ts index 3c0f362d6..a6c7f13a7 100644 --- a/4337/test/utils/setup.ts +++ b/4337/test/utils/setup.ts @@ -1,8 +1,9 @@ +import EntryPointArtifact from '@account-abstraction/contracts/artifacts/EntryPoint.json' import { deployments, ethers } from 'hardhat' import { Contract, Signer } from 'ethers' import solc from 'solc' import { logGas } from '../../src/utils/execution' -import { Safe4337Mock, SafeMock } from '../../typechain-types' +import { Safe4337Mock, SafeMock, IEntryPoint } from '../../typechain-types' const getRandomInt = (min = 0, max: number = Number.MAX_SAFE_INTEGER): number => { return Math.floor(Math.random() * (max - min + 1)) + min @@ -112,3 +113,14 @@ export const deployContract = async (deployer: Signer, source: string): Promise< } return new Contract(contractAddress, output.interface, deployer) } + +export const deployReferenceEntryPoint = async (deployer: Signer, relayer?: Signer) => { + const { abi, bytecode } = EntryPointArtifact + const transaction = await deployer.sendTransaction({ data: bytecode }) + const receipt = await transaction.wait() + const contractAddress = receipt.contractAddress + if (contractAddress === null) { + throw new Error(`contract deployment transaction ${transaction.hash} missing address`) + } + return new ethers.Contract(contractAddress, abi, relayer) as unknown as IEntryPoint +} diff --git a/4337/test/utils/webauthn.ts b/4337/test/utils/webauthn.ts index 7ebe16985..f860acb80 100644 --- a/4337/test/utils/webauthn.ts +++ b/4337/test/utils/webauthn.ts @@ -247,3 +247,75 @@ export function base64UrlEncode(data: BytesLike | ArrayBufferLike): string { function b2ab(buf: Uint8Array): ArrayBuffer { return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) } + +/** + * Extract the x and y coordinates of the public key from a created public key credential. + * Inspired from . + */ +export function extractPublicKey(response: AuthenticatorAttestationResponse): { x: bigint; y: bigint } { + const attestationObject = CBOR.decode(response.attestationObject) + const authDataView = new DataView(attestationObject.authData.buffer) + const credentialIdLength = authDataView.getUint16(53) + const cosePublicKey = attestationObject.authData.slice(55 + credentialIdLength) + const key: Map = CBOR.decode(cosePublicKey) + const bn = (bytes: Uint8Array) => BigInt(ethers.hexlify(bytes)) + return { + x: bn(key.get(-2) as Uint8Array), + y: bn(key.get(-3) as Uint8Array), + } +} + +/** + * Compute the challenge offset in the client data JSON. This is the offset, in bytes, of the + * value associated with the `challenge` key in the JSON blob. + */ +export function extractChallengeOffset(response: AuthenticatorAssertionResponse, challenge: string): number { + const clientDataJSON = new TextDecoder('utf-8').decode(response.clientDataJSON) + + const encodedChallenge = base64UrlEncode(challenge) + const offset = clientDataJSON.indexOf(encodedChallenge) + if (offset < 0) { + throw new Error('challenge not found in client data JSON') + } + + return offset +} + +/** + * Extracts the signature into R and S values from the authenticator response. + * + * See: + * - + * - + */ +export function extractSignature(response: AuthenticatorAssertionResponse): [bigint, bigint] { + const check = (x: boolean) => { + if (!x) { + throw new Error('invalid signature encoding') + } + } + + // Decode the DER signature. Note that we assume that all lengths fit into 8-bit integers, + // which is true for the kinds of signatures we are decoding but generally false. I.e. this + // code should not be used in any serious application. + const view = new DataView(response.signature) + + // check that the sequence header is valid + check(view.getUint8(0) === 0x30) + check(view.getUint8(1) === view.byteLength - 2) + + // read r and s + const readInt = (offset: number) => { + check(view.getUint8(offset) === 0x02) + const len = view.getUint8(offset + 1) + const start = offset + 2 + const end = start + len + const n = BigInt(ethers.hexlify(new Uint8Array(view.buffer.slice(start, end)))) + check(n < ethers.MaxUint256) + return [n, end] as const + } + const [r, sOffset] = readInt(2) + const [s] = readInt(sOffset) + + return [r, s] +}