Skip to content

Commit

Permalink
Fixed E2E test with WebAuthn signer
Browse files Browse the repository at this point in the history
  • Loading branch information
nlordell committed Jan 8, 2024
1 parent daf7591 commit 847c3aa
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 19 deletions.
1 change: 1 addition & 0 deletions 4337/contracts/test/WebAuthnSigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
96 changes: 77 additions & 19 deletions 4337/test/e2e/WebAuthnSigner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand Down Expand Up @@ -159,59 +161,115 @@ 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 <https://webauthn.guide/#registration>.
*/
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<number, unknown> = CBOR.decode(cosePublicKey);
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),
}
}

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:
* - <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]
}
})

0 comments on commit 847c3aa

Please sign in to comment.