Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Gas Metering Tests for WebAuthn Signers #201

Merged
merged 5 commits into from
Jan 12, 2024
Merged
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
2 changes: 1 addition & 1 deletion 4337/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 1 addition & 2 deletions 4337/docker/bundler/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
24 changes: 23 additions & 1 deletion 4337/src/utils/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,33 @@ export const signHash = async (signer: Signer, hash: string): Promise<SafeSignat
}
}

export const buildSignatureBytes = (signatures: SafeSignature[]): string => {
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<TransactionResponse>, skip?: boolean): Promise<TransactionResponse> => {
return tx.then(async (result) => {
const receipt = await result.wait()
Expand Down
19 changes: 8 additions & 11 deletions 4337/test/e2e/SingletonSigners.spec.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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,
Expand Down
79 changes: 2 additions & 77 deletions 4337/test/e2e/WebAuthnSigner.spec.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -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()

Expand All @@ -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()
Expand All @@ -41,7 +39,6 @@ describe('E2E - WebAuthn Signers', () => {
entryPoint,
signerLaunchpad,
singleton,
multiSend,
signerFactory,
navigator,
}
Expand Down Expand Up @@ -200,76 +197,4 @@ describe('E2E - WebAuthn Signers', () => {
const safeInstance = await ethers.getContractAt('SafeL2', safe)
expect(await safeInstance.getOwners()).to.deep.equal([signerAddress])
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to utils/webauthn.

/**
* Extract the x and y coordinates of the public key from a created public key credential.
* Inspired from <https://webauthn.guide/#registration>.
*/
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<number, unknown> = 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:
* - <https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3>
* - <https://en.wikipedia.org/wiki/X.690#BER_encoding>
*/
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]
}
})
Loading
Loading