Skip to content

Commit

Permalink
Adding Offchain Passkey Verification (#337)
Browse files Browse the repository at this point in the history
This PR verifies the user's ability to authenticate a user’s passkey
credential off-chain to provide access control for sensitive
information.

Also slightly update the other test to use the `signerFactory`.

Closes #281
  • Loading branch information
remedcu authored Apr 3, 2024
1 parent 78ae3b8 commit 9682c39
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 10 deletions.
16 changes: 11 additions & 5 deletions modules/passkey/test/4337/WebAuthn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ import { WebAuthnCredentials, decodePublicKey, encodeWebAuthnSignature } from '.

describe('Safe4337Module - WebAuthn Owner', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
const { SafeModuleSetup, SafeL2, SafeProxyFactory, FCLP256Verifier, Safe4337Module, SafeECDSASignerLaunchpad, EntryPoint } =
await deployments.fixture()
const {
SafeModuleSetup,
SafeL2,
SafeProxyFactory,
FCLP256Verifier,
Safe4337Module,
SafeECDSASignerLaunchpad,
EntryPoint,
WebAuthnSignerFactory,
} = await deployments.fixture()

const [user] = await ethers.getSigners()
const entryPoint = await ethers.getContractAt('IEntryPoint', EntryPoint.address)
Expand All @@ -24,9 +32,7 @@ describe('Safe4337Module - WebAuthn Owner', () => {
const signerLaunchpad = await ethers.getContractAt('SafeECDSASignerLaunchpad', SafeECDSASignerLaunchpad.address)
const singleton = await ethers.getContractAt(SafeL2.abi, SafeL2.address)
const verifier = await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address)

const WebAuthnSignerFactory = await ethers.getContractFactory('WebAuthnSignerFactory')
const signerFactory = await WebAuthnSignerFactory.deploy()
const signerFactory = await ethers.getContractAt('WebAuthnSignerFactory', WebAuthnSignerFactory.address)

const navigator = {
credentials: new WebAuthnCredentials(),
Expand Down
16 changes: 11 additions & 5 deletions modules/passkey/test/4337/WebAuthnSigner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@ describe('WebAuthn Signers [@4337]', () => {
})

const setupTests = deployments.createFixture(async ({ deployments }) => {
const { EntryPoint, Safe4337Module, SafeECDSASignerLaunchpad, SafeProxyFactory, SafeModuleSetup, SafeL2, FCLP256Verifier } =
await deployments.run()
const {
EntryPoint,
Safe4337Module,
SafeECDSASignerLaunchpad,
SafeProxyFactory,
SafeModuleSetup,
SafeL2,
FCLP256Verifier,
WebAuthnSignerFactory,
} = await deployments.run()
const [user] = await prepareAccounts()
const bundler = bundlerRpc()

Expand All @@ -24,9 +32,7 @@ describe('WebAuthn Signers [@4337]', () => {
const signerLaunchpad = await ethers.getContractAt('SafeECDSASignerLaunchpad', SafeECDSASignerLaunchpad.address)
const singleton = await ethers.getContractAt(SafeL2.abi, SafeL2.address)
const verifier = await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address)

const WebAuthnSignerFactory = await ethers.getContractFactory('WebAuthnSignerFactory')
const signerFactory = await WebAuthnSignerFactory.deploy()
const signerFactory = await ethers.getContractAt('WebAuthnSignerFactory', WebAuthnSignerFactory.address)

const navigator = {
credentials: new WebAuthnCredentials(),
Expand Down
114 changes: 114 additions & 0 deletions modules/passkey/test/userstories/OffchainPasskeyVerification.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { expect } from 'chai'
import { deployments, ethers } from 'hardhat'
import { WebAuthnCredentials, decodePublicKey, encodeWebAuthnSignature } from '../utils/webauthn'
import { buildSignatureBytes } from '@safe-global/safe-4337/src/utils/execution'

/**
* User story: Off-chain Passkey Signature Verification
* The user story here creates a passkey signer, deploys a Safe with that signer and verifies the signature.
*
* The flow can be summarized as follows:
* Step 1: Setup the contracts.
* Step 2: Create the `SafeMessage`, hash it and finally create the signature for the hash by the passkey signer.
* Step 3: Verify that the signature returns the EIP_1271_MAGIC_VALUE.
*/
describe('Offchain Passkey Signature Verification [@userstory]', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
const { SafeProxyFactory, SafeL2, FCLP256Verifier, WebAuthnSignerFactory, CompatibilityFallbackHandler } = await deployments.run()

const proxyFactory = await ethers.getContractAt(SafeProxyFactory.abi, SafeProxyFactory.address)
const singleton = await ethers.getContractAt(SafeL2.abi, SafeL2.address)
const fallbackHandler = await ethers.getContractAt(CompatibilityFallbackHandler.abi, CompatibilityFallbackHandler.address)
const verifier = await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address)
const signerFactory = await ethers.getContractAt('WebAuthnSignerFactory', WebAuthnSignerFactory.address)

const navigator = {
credentials: new WebAuthnCredentials(),
}

// Create the credentials for Passkey.
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 verifierAddress = await verifier.getAddress()

// Get the publicKey from the credential and create the signer.
const publicKey = decodePublicKey(credential.response)
await signerFactory.createSigner(publicKey.x, publicKey.y, verifierAddress)
const signerAddress = await signerFactory.getSigner(publicKey.x, publicKey.y, verifierAddress)
const signer = await ethers.getContractAt('WebAuthnSigner', signerAddress)

// Deploy Safe with the WebAuthn signer as a single owner.
const singletonAddress = await singleton.getAddress()
const setupData = singleton.interface.encodeFunctionData('setup', [
[signerAddress],
1,
ethers.ZeroAddress,
'0x',
await fallbackHandler.getAddress(),
ethers.ZeroAddress,
0,
ethers.ZeroAddress,
])
const safeAddress = await proxyFactory.createProxyWithNonce.staticCall(singletonAddress, setupData, 0)
await proxyFactory.createProxyWithNonce(singletonAddress, setupData, 0)
const safe = await ethers.getContractAt([...SafeL2.abi, ...CompatibilityFallbackHandler.abi], safeAddress)

return {
safe,
signer,
navigator,
credential,
}
})

it('should be possible to verify offchain passkey signature', async () => {
const { safe, signer, navigator, credential } = await setupTests()

// Define message to be signed. The message should be a 32 byte hash of some data as shown below.
const message = ethers.id('Signature verification with passkeys is cool!')

// Compute the `SafeMessage` hash which gets specified as the challenge and ultimately signed by the private key.
const { chainId } = await ethers.provider.getNetwork()
const safeMsgData = ethers.TypedDataEncoder.encode(
{ verifyingContract: await safe.getAddress(), chainId },
{ SafeMessage: [{ name: 'message', type: 'bytes' }] },
{ message },
)
const safeMsgHash = ethers.keccak256(safeMsgData)

// Creating the signature for the `safeMsgHash`.
const assertion = navigator.credentials.get({
publicKey: {
challenge: ethers.getBytes(safeMsgHash),
rpId: 'safe.global',
allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }],
userVerification: 'required',
},
})

// Encode the passkey signature for the safe.
const signature = buildSignatureBytes([
{
signer: await signer.getAddress(),
data: encodeWebAuthnSignature(assertion.response),
dynamic: true,
},
])

expect(await safe['isValidSignature(bytes32,bytes)'](message, signature)).to.eq('0x1626ba7e')
})
})
8 changes: 8 additions & 0 deletions packages/4337-local-bundler/src/deploy/safe.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import MultiSend from '@safe-global/safe-contracts/build/artifacts/contracts/libraries/MultiSend.sol/MultiSend.json'
import SafeProxyFactory from '@safe-global/safe-contracts/build/artifacts/contracts/proxies/SafeProxyFactory.sol/SafeProxyFactory.json'
import SafeL2 from '@safe-global/safe-contracts/build/artifacts/contracts/SafeL2.sol/SafeL2.json'
import CompatibilityFallbackHandler from '@safe-global/safe-contracts/build/artifacts/contracts/handler/CompatibilityFallbackHandler.sol/CompatibilityFallbackHandler.json'
import { DeployFunction } from 'hardhat-deploy/types'

const deploy: DeployFunction = async ({ deployments, getNamedAccounts, network }) => {
Expand Down Expand Up @@ -32,6 +33,13 @@ const deploy: DeployFunction = async ({ deployments, getNamedAccounts, network }
log: true,
deterministicDeployment: true,
})
await deploy("CompatibilityFallbackHandler", {

Check failure on line 36 in packages/4337-local-bundler/src/deploy/safe.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `"CompatibilityFallbackHandler"` with `'CompatibilityFallbackHandler'`
contract: CompatibilityFallbackHandler,
from: deployer,
args: [],
log: true,
deterministicDeployment: true,
});

Check failure on line 42 in packages/4337-local-bundler/src/deploy/safe.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `;`
}

deploy.tags = ['safe']
Expand Down

0 comments on commit 9682c39

Please sign in to comment.