From 847c3aa972e2b7a0cd3620d79c46d3f45fdb9261 Mon Sep 17 00:00:00 2001 From: Nicholas Rodrigues Lordello Date: Mon, 8 Jan 2024 16:08:57 +0100 Subject: [PATCH] Fixed E2E test with WebAuthn signer --- 4337/contracts/test/WebAuthnSigner.sol | 1 + 4337/test/e2e/WebAuthnSigner.spec.ts | 96 +++++++++++++++++++++----- 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/4337/contracts/test/WebAuthnSigner.sol b/4337/contracts/test/WebAuthnSigner.sol index 34269a391..45604fce3 100644 --- a/4337/contracts/test/WebAuthnSigner.sol +++ b/4337/contracts/test/WebAuthnSigner.sol @@ -14,6 +14,7 @@ struct SignatureData { function checkSignature(bytes memory data, bytes calldata signature, uint256 x, uint256 y) view returns (bytes4 magicValue) { SignatureData calldata signaturePointer; + // solhint-disable-next-line no-inline-assembly assembly { signaturePointer := signature.offset } diff --git a/4337/test/e2e/WebAuthnSigner.spec.ts b/4337/test/e2e/WebAuthnSigner.spec.ts index 36f19089a..a6b9b5306 100644 --- a/4337/test/e2e/WebAuthnSigner.spec.ts +++ b/4337/test/e2e/WebAuthnSigner.spec.ts @@ -3,9 +3,9 @@ import CBOR from 'cbor' import { deployments, ethers, network } from 'hardhat' import { bundlerRpc, prepareAccounts, waitForUserOp } from '../utils/e2e' import { chainId } from '../utils/encoding' -import { AuthenticatorAttestationResponse, PublicKeyCredential, WebAuthnCredentials } from '../utils/webauthn' +import { AuthenticatorAttestationResponse, WebAuthnCredentials, base64UrlEncode } from '../utils/webauthn' -describe.only('E2E - WebAuthn Signers', () => { +describe('E2E - WebAuthn Signers', () => { before(function () { if (network.name !== 'localhost') { this.skip() @@ -48,7 +48,8 @@ describe.only('E2E - WebAuthn Signers', () => { }) it('should execute a user op and deploy a WebAuthn signer', async () => { - const { user, bundler, proxyFactory, addModulesLib, module, entryPoint, signerLaunchpad, singleton, signerFactory, navigator } = await setupTests() + const { user, bundler, proxyFactory, addModulesLib, module, entryPoint, signerLaunchpad, singleton, signerFactory, navigator } = + await setupTests() const credential = navigator.credentials.create({ publicKey: { @@ -65,8 +66,9 @@ describe.only('E2E - WebAuthn Signers', () => { pubKeyCredParams: [{ type: 'public-key', alg: -7 }], }, }) - const publicKey = extractPublicKey(credential.response); - const signerData = ethers.solidityPacked(["uint256", "uint256"], [publicKey.x, publicKey.y]) + 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, @@ -159,47 +161,56 @@ describe.only('E2E - WebAuthn Signers', () => { const assertion = navigator.credentials.get({ publicKey: { challenge: ethers.getBytes(safeInitOpHash), - rpId: "safe.global", + rpId: 'safe.global', allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], }, }) - const signature = ethers.AbiCoder.defaultAbiCoder().encode( - ['bytes', 'bytes', 'uint256', 'uint256[2]'], + const signature = ethers.solidityPacked( + ['uint48', 'uint48', 'bytes'], [ - new Uint8Array(assertion.response.authenticatorData), - new Uint8Array(assertion.response.clientDataJSON), - extractChallengeOffset(assertion.response), - extractSignature(assertion.response) + 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 bundler.sendUserOperation({ ...userOp, signature }, await entryPoint.getAddress()) await waitForUserOp(userOp) 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([signer]) + 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 } { + 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 key: Map = CBOR.decode(cosePublicKey) const bn = (bytes: Uint8Array) => BigInt(ethers.hexlify(bytes)) return { x: bn(key.get(-2) as Uint8Array), @@ -207,11 +218,58 @@ describe.only('E2E - WebAuthn Signers', () => { } } - function extractChallengeOffset(response: AuthenticatorAssertionResponse): { x: bigint, y: bigint } { - // TODO + /** + * 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 } - function extractSignature(response: AuthenticatorAssertionResponse): { x: bigint, y: bigint } { - // TODO + /** + * 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] } })