From c0778c030a5ed1de3f8e920ce58dfa674e54abf6 Mon Sep 17 00:00:00 2001 From: hjornigur Date: Mon, 17 Jun 2024 16:47:14 +0200 Subject: [PATCH 1/5] Check that message challenges returned by the passkey server are valid --- plugins/passkey/toPasskeyValidator.ts | 13 ++++++ plugins/passkey/utils.ts | 57 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/plugins/passkey/toPasskeyValidator.ts b/plugins/passkey/toPasskeyValidator.ts index a420bb55..bc77c19e 100644 --- a/plugins/passkey/toPasskeyValidator.ts +++ b/plugins/passkey/toPasskeyValidator.ts @@ -29,8 +29,10 @@ import { getValidatorAddress } from "./index.js" import type { WebAuthnKey } from "./toWebAuthnKey.js" import { b64ToBytes, + base64FromArrayBuffer, deserializePasskeyValidatorData, findQuoteIndices, + hexStringToUint8Array, isRIP7212SupportedNetwork, parseAndNormalizeSig, serializePasskeyValidatorData, @@ -78,6 +80,17 @@ const signMessageUsingWebAuthn = async ( ) const signInitiateResult = await signInitiateResponse.json() + const expectedChallenge = base64FromArrayBuffer( + hexStringToUint8Array(formattedMessage), + true + ) + + if (signInitiateResult.challenge !== expectedChallenge) { + throw new Error( + `Server has returned invalid challenge. Expected: ${expectedChallenge}, returned: ${signInitiateResult.challenge}` + ) + } + // prepare assertion options const assertionOptions: PublicKeyCredentialRequestOptionsJSON = { challenge: signInitiateResult.challenge, diff --git a/plugins/passkey/utils.ts b/plugins/passkey/utils.ts index 1d40a3a5..c8773a2d 100644 --- a/plugins/passkey/utils.ts +++ b/plugins/passkey/utils.ts @@ -12,6 +12,20 @@ export const uint8ArrayToHexString = (array: Uint8Array): `0x${string}` => { ).join("")}` as `0x${string}` } +export const hexStringToUint8Array = (hexString: string): Uint8Array => { + const formattedHexString = hexString.startsWith("0x") + ? hexString.slice(2) + : hexString + const byteArray = new Uint8Array(formattedHexString.length / 2) + for (let i = 0; i < formattedHexString.length; i += 2) { + byteArray[i / 2] = Number.parseInt( + formattedHexString.substring(i, i + 2), + 16 + ) + } + return byteArray +} + export const b64ToBytes = (base64: string): Uint8Array => { const paddedBase64 = base64 .replace(/-/g, "+") @@ -95,3 +109,46 @@ function bytesToBase64(bytes: Uint8Array) { const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join("") return btoa(binString) } + +/** + * Convenience function for creating a base64 encoded string from an ArrayBuffer instance + * Copied from @hexagon/base64 package (base64.fromArrayBuffer) + * @public + * + * @param {ArrayBuffer} arrBuf - ArrayBuffer to be encoded + * @param {boolean} [urlMode] - If set to true, URL mode string will be returned + * @returns {string} - Base64 representation of data + */ +export const base64FromArrayBuffer = ( + arrBuf: ArrayBuffer, + urlMode: boolean +): string => { + const // Regular base64 characters + chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + const // Base64url characters + charsUrl = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + + const bytes = new Uint8Array(arrBuf) + let result = "" + + const len = bytes.length + const target = urlMode ? charsUrl : chars + + for (let i = 0; i < len; i += 3) { + result += target[bytes[i] >> 2] + result += target[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)] + result += target[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)] + result += target[bytes[i + 2] & 63] + } + + const remainder = len % 3 + if (remainder === 2) { + result = result.substring(0, result.length - 1) + (urlMode ? "" : "=") + } else if (remainder === 1) { + result = result.substring(0, result.length - 2) + (urlMode ? "" : "==") + } + + return result +} From f0c83fb6ad915da2eb8b54ca93c094c913583d9e Mon Sep 17 00:00:00 2001 From: hjornigur Date: Tue, 18 Jun 2024 20:00:47 +0200 Subject: [PATCH 2/5] Remove redundant calls to passkey-server during message signing --- plugins/passkey/toPasskeyValidator.ts | 69 +++++++++------------------ plugins/passkey/toWebAuthnKey.ts | 23 +++++---- plugins/passkey/utils.ts | 1 + 3 files changed, 38 insertions(+), 55 deletions(-) diff --git a/plugins/passkey/toPasskeyValidator.ts b/plugins/passkey/toPasskeyValidator.ts index bc77c19e..5ac072b6 100644 --- a/plugins/passkey/toPasskeyValidator.ts +++ b/plugins/passkey/toPasskeyValidator.ts @@ -41,8 +41,9 @@ import { const signMessageUsingWebAuthn = async ( message: SignableMessage, - passkeyServerUrl: string, - chainId: number + passkeyServerUrl: string, // Won't be needed here + chainId: number, + allowCredentials?: PublicKeyCredentialRequestOptionsJSON["allowCredentials"] ) => { let messageContent: string if (typeof message === "string") { @@ -63,38 +64,15 @@ const signMessageUsingWebAuthn = async ( ? messageContent.slice(2) : messageContent - if (window.sessionStorage === undefined) { - throw new Error("sessionStorage is not available") - } - const userId = sessionStorage.getItem("userId") - - // initiate signing - const signInitiateResponse = await fetch( - `${passkeyServerUrl}/sign-initiate`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ data: formattedMessage, userId }), - credentials: "include" - } - ) - const signInitiateResult = await signInitiateResponse.json() - - const expectedChallenge = base64FromArrayBuffer( + const challenge = base64FromArrayBuffer( hexStringToUint8Array(formattedMessage), true ) - if (signInitiateResult.challenge !== expectedChallenge) { - throw new Error( - `Server has returned invalid challenge. Expected: ${expectedChallenge}, returned: ${signInitiateResult.challenge}` - ) - } - // prepare assertion options const assertionOptions: PublicKeyCredentialRequestOptionsJSON = { - challenge: signInitiateResult.challenge, - allowCredentials: signInitiateResult.allowCredentials, + challenge, + allowCredentials, userVerification: "required" } @@ -103,22 +81,8 @@ const signMessageUsingWebAuthn = async ( const { startAuthentication } = await import("@simplewebauthn/browser") const cred = await startAuthentication(assertionOptions) - // verify signature from server - const verifyResponse = await fetch(`${passkeyServerUrl}/sign-verify`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ cred, userId }), - credentials: "include" - }) - - const verifyResult = await verifyResponse.json() - - if (!verifyResult.success) { - throw new Error("Signature not verified") - } - // get authenticator data - const authenticatorData = verifyResult.authenticatorData + const { authenticatorData } = cred.response const authenticatorDataHex = uint8ArrayToHexString( b64ToBytes(authenticatorData) ) @@ -130,7 +94,7 @@ const signMessageUsingWebAuthn = async ( const { beforeType } = findQuoteIndices(clientDataJSON) // get signature r,s - const signature = verifyResult.signature + const { signature } = cred.response const signatureHex = uint8ArrayToHexString(b64ToBytes(signature)) const { r, s } = parseAndNormalizeSig(signatureHex) @@ -189,7 +153,12 @@ export async function toPasskeyValidator< // note that this address will be overwritten by actual address address: "0x0000000000000000000000000000000000000000", async signMessage({ message }) { - return signMessageUsingWebAuthn(message, passkeyServerUrl, chainId) + return signMessageUsingWebAuthn( + message, + passkeyServerUrl, + chainId, + [{ id: webAuthnKey.authenticatorId, type: "public-key" }] + ) }, async signTransaction(_, __) { throw new SignTransactionNotSupportedBySmartAccount() @@ -312,6 +281,7 @@ export async function toPasskeyValidator< validatorAddress ?? getValidatorAddress(entryPointAddress), pubKeyX: webAuthnKey.pubX, pubKeyY: webAuthnKey.pubY, + authenticatorId: webAuthnKey.authenticatorId, authenticatorIdHash: webAuthnKey.authenticatorIdHash }) } @@ -343,6 +313,7 @@ export async function deserializePasskeyValidator< validatorAddress, pubKeyX, pubKeyY, + authenticatorId, authenticatorIdHash } = deserializePasskeyValidatorData(serializedData) @@ -354,7 +325,12 @@ export async function deserializePasskeyValidator< // note that this address will be overwritten by actual address address: "0x0000000000000000000000000000000000000000", async signMessage({ message }) { - return signMessageUsingWebAuthn(message, passkeyServerUrl, chainId) + return signMessageUsingWebAuthn( + message, + passkeyServerUrl, + chainId, + [{ id: authenticatorId, type: "public-key" }] + ) }, async signTransaction(_, __) { throw new SignTransactionNotSupportedBySmartAccount() @@ -471,6 +447,7 @@ export async function deserializePasskeyValidator< validatorAddress, pubKeyX, pubKeyY, + authenticatorId, authenticatorIdHash }) } diff --git a/plugins/passkey/toWebAuthnKey.ts b/plugins/passkey/toWebAuthnKey.ts index af9907a4..394f3371 100644 --- a/plugins/passkey/toWebAuthnKey.ts +++ b/plugins/passkey/toWebAuthnKey.ts @@ -9,6 +9,7 @@ export enum WebAuthnMode { export type WebAuthnKey = { pubX: bigint pubY: bigint + authenticatorId: string authenticatorIdHash: Hex } @@ -29,7 +30,7 @@ export const toWebAuthnKey = async ({ return webAuthnKey } let pubKey: string | undefined - let authenticatorIdHash: Hex + let authenticatorId: string | undefined if (mode === WebAuthnMode.Login) { // Get login options const loginOptionsResponse = await fetch( @@ -46,10 +47,7 @@ export const toWebAuthnKey = async ({ const { startAuthentication } = await import("@simplewebauthn/browser") const loginCred = await startAuthentication(loginOptions) - // get authenticatorIdHash - authenticatorIdHash = keccak256( - uint8ArrayToHexString(b64ToBytes(loginCred.id)) - ) + authenticatorId = loginCred.id // Verify authentication const loginVerifyResponse = await fetch( @@ -99,10 +97,7 @@ export const toWebAuthnKey = async ({ const { startRegistration } = await import("@simplewebauthn/browser") const registerCred = await startRegistration(registerOptions.options) - // get authenticatorIdHash - authenticatorIdHash = keccak256( - uint8ArrayToHexString(b64ToBytes(registerCred.id)) - ) + authenticatorId = registerCred.id // Verify registration const registerVerifyResponse = await fetch( @@ -133,6 +128,15 @@ export const toWebAuthnKey = async ({ if (!pubKey) { throw new Error("No public key returned from registration credential") } + if (!authenticatorId) { + throw new Error( + "No authenticator id returned from registration credential" + ) + } + + const authenticatorIdHash = keccak256( + uint8ArrayToHexString(b64ToBytes(authenticatorId)) + ) const spkiDer = Buffer.from(pubKey, "base64") const key = await crypto.subtle.importKey( "spki", @@ -156,6 +160,7 @@ export const toWebAuthnKey = async ({ return { pubX: BigInt(`0x${pubKeyX}`), pubY: BigInt(`0x${pubKeyY}`), + authenticatorId, authenticatorIdHash } } diff --git a/plugins/passkey/utils.ts b/plugins/passkey/utils.ts index c8773a2d..ddefefa7 100644 --- a/plugins/passkey/utils.ts +++ b/plugins/passkey/utils.ts @@ -74,6 +74,7 @@ type PasskeyValidatorSerializedData = { validatorAddress: Hex pubKeyX: bigint pubKeyY: bigint + authenticatorId: string authenticatorIdHash: Hex } From a66a1527c2942929f7a7e8ab2ac66f1fe8538991 Mon Sep 17 00:00:00 2001 From: adnpark Date: Fri, 21 Jun 2024 10:56:33 +0900 Subject: [PATCH 3/5] chore: remove unnecessary passkey server url and use array buffer explicitly --- plugins/passkey/toPasskeyValidator.ts | 34 ++++++++++----------------- plugins/passkey/utils.ts | 1 - 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/plugins/passkey/toPasskeyValidator.ts b/plugins/passkey/toPasskeyValidator.ts index 5b9a5bf8..44e9aae3 100644 --- a/plugins/passkey/toPasskeyValidator.ts +++ b/plugins/passkey/toPasskeyValidator.ts @@ -41,7 +41,6 @@ import { const signMessageUsingWebAuthn = async ( message: SignableMessage, - passkeyServerUrl: string, // Won't be needed here chainId: number, allowCredentials?: PublicKeyCredentialRequestOptionsJSON["allowCredentials"] ) => { @@ -64,10 +63,12 @@ const signMessageUsingWebAuthn = async ( ? messageContent.slice(2) : messageContent - const challenge = base64FromArrayBuffer( - hexStringToUint8Array(formattedMessage), - true - ) + const uint8Array = hexStringToUint8Array(formattedMessage) + const arrayBuffer = new ArrayBuffer(uint8Array.byteLength) + const view = new Uint8Array(arrayBuffer) + view.set(uint8Array) + + const challenge = base64FromArrayBuffer(arrayBuffer, true) // prepare assertion options const assertionOptions: PublicKeyCredentialRequestOptionsJSON = { @@ -128,14 +129,12 @@ export async function toPasskeyValidator< client: Client, { webAuthnKey, - passkeyServerUrl, entryPoint: entryPointAddress, kernelVersion, validatorAddress: _validatorAddress, credentials = "include" }: { webAuthnKey: WebAuthnKey - passkeyServerUrl: string entryPoint: entryPoint kernelVersion: GetKernelVersion validatorAddress?: Address @@ -158,12 +157,9 @@ export async function toPasskeyValidator< // note that this address will be overwritten by actual address address: "0x0000000000000000000000000000000000000000", async signMessage({ message }) { - return signMessageUsingWebAuthn( - message, - passkeyServerUrl, - chainId, - [{ id: webAuthnKey.authenticatorId, type: "public-key" }] - ) + return signMessageUsingWebAuthn(message, chainId, [ + { id: webAuthnKey.authenticatorId, type: "public-key" } + ]) }, async signTransaction(_, __) { throw new SignTransactionNotSupportedBySmartAccount() @@ -284,7 +280,6 @@ export async function toPasskeyValidator< } const userId = sessionStorage.getItem("userId") return serializePasskeyValidatorData({ - passkeyServerUrl, credentials, entryPoint: entryPointAddress, validatorAddress, @@ -319,7 +314,6 @@ export async function deserializePasskeyValidator< } > { const { - passkeyServerUrl, credentials, entryPoint, validatorAddress, @@ -337,12 +331,9 @@ export async function deserializePasskeyValidator< // note that this address will be overwritten by actual address address: "0x0000000000000000000000000000000000000000", async signMessage({ message }) { - return signMessageUsingWebAuthn( - message, - passkeyServerUrl, - chainId, - [{ id: authenticatorId, type: "public-key" }] - ) + return signMessageUsingWebAuthn(message, chainId, [ + { id: authenticatorId, type: "public-key" } + ]) }, async signTransaction(_, __) { throw new SignTransactionNotSupportedBySmartAccount() @@ -458,7 +449,6 @@ export async function deserializePasskeyValidator< } const userId = sessionStorage.getItem("userId") return serializePasskeyValidatorData({ - passkeyServerUrl, credentials, entryPoint, validatorAddress, diff --git a/plugins/passkey/utils.ts b/plugins/passkey/utils.ts index b7ad27ba..a1421f03 100644 --- a/plugins/passkey/utils.ts +++ b/plugins/passkey/utils.ts @@ -68,7 +68,6 @@ export function parseAndNormalizeSig(derSig: Hex): { r: bigint; s: bigint } { } type PasskeyValidatorSerializedData = { - passkeyServerUrl: string credentials: string entryPoint: Hex validatorAddress: Hex From 08d94f830af51fa13bd4f57bc410d88f013c082b Mon Sep 17 00:00:00 2001 From: adnpark Date: Sat, 22 Jun 2024 10:25:06 +0900 Subject: [PATCH 4/5] chore: simplify challenge encoding --- plugins/passkey/toPasskeyValidator.ts | 12 +++++------- plugins/passkey/utils.ts | 17 ++++++++--------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/plugins/passkey/toPasskeyValidator.ts b/plugins/passkey/toPasskeyValidator.ts index 44e9aae3..cac8ea0a 100644 --- a/plugins/passkey/toPasskeyValidator.ts +++ b/plugins/passkey/toPasskeyValidator.ts @@ -29,7 +29,7 @@ import { getValidatorAddress } from "./index.js" import type { WebAuthnKey } from "./toWebAuthnKey.js" import { b64ToBytes, - base64FromArrayBuffer, + base64FromUint8Array, deserializePasskeyValidatorData, findQuoteIndices, hexStringToUint8Array, @@ -63,12 +63,10 @@ const signMessageUsingWebAuthn = async ( ? messageContent.slice(2) : messageContent - const uint8Array = hexStringToUint8Array(formattedMessage) - const arrayBuffer = new ArrayBuffer(uint8Array.byteLength) - const view = new Uint8Array(arrayBuffer) - view.set(uint8Array) - - const challenge = base64FromArrayBuffer(arrayBuffer, true) + const challenge = base64FromUint8Array( + hexStringToUint8Array(formattedMessage), + true + ) // prepare assertion options const assertionOptions: PublicKeyCredentialRequestOptionsJSON = { diff --git a/plugins/passkey/utils.ts b/plugins/passkey/utils.ts index a1421f03..c92092b5 100644 --- a/plugins/passkey/utils.ts +++ b/plugins/passkey/utils.ts @@ -121,12 +121,12 @@ function bytesToBase64(bytes: Uint8Array) { * Copied from @hexagon/base64 package (base64.fromArrayBuffer) * @public * - * @param {ArrayBuffer} arrBuf - ArrayBuffer to be encoded + * @param {Uint8Array} uint8Arr - Uint8Array to be encoded * @param {boolean} [urlMode] - If set to true, URL mode string will be returned * @returns {string} - Base64 representation of data */ -export const base64FromArrayBuffer = ( - arrBuf: ArrayBuffer, +export const base64FromUint8Array = ( + uint8Arr: Uint8Array, urlMode: boolean ): string => { const // Regular base64 characters @@ -136,17 +136,16 @@ export const base64FromArrayBuffer = ( charsUrl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" - const bytes = new Uint8Array(arrBuf) let result = "" - const len = bytes.length + const len = uint8Arr.length const target = urlMode ? charsUrl : chars for (let i = 0; i < len; i += 3) { - result += target[bytes[i] >> 2] - result += target[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)] - result += target[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)] - result += target[bytes[i + 2] & 63] + result += target[uint8Arr[i] >> 2] + result += target[((uint8Arr[i] & 3) << 4) | (uint8Arr[i + 1] >> 4)] + result += target[((uint8Arr[i + 1] & 15) << 2) | (uint8Arr[i + 2] >> 6)] + result += target[uint8Arr[i + 2] & 63] } const remainder = len % 3 From 6c9854317609ff3c42b908d132ce1cac85361377 Mon Sep 17 00:00:00 2001 From: adnpark Date: Sat, 22 Jun 2024 10:50:53 +0900 Subject: [PATCH 5/5] chore: remove unused userId --- plugins/passkey/toPasskeyValidator.ts | 14 ++------------ plugins/passkey/toWebAuthnKey.ts | 11 ----------- plugins/passkey/utils.ts | 5 ----- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/plugins/passkey/toPasskeyValidator.ts b/plugins/passkey/toPasskeyValidator.ts index cac8ea0a..6fb4ccce 100644 --- a/plugins/passkey/toPasskeyValidator.ts +++ b/plugins/passkey/toPasskeyValidator.ts @@ -273,10 +273,6 @@ export async function toPasskeyValidator< }, getSerializedData() { - if (window.sessionStorage === undefined) { - throw new Error("sessionStorage is not available") - } - const userId = sessionStorage.getItem("userId") return serializePasskeyValidatorData({ credentials, entryPoint: entryPointAddress, @@ -284,8 +280,7 @@ export async function toPasskeyValidator< pubKeyX: webAuthnKey.pubX, pubKeyY: webAuthnKey.pubY, authenticatorId: webAuthnKey.authenticatorId, - authenticatorIdHash: webAuthnKey.authenticatorIdHash, - userId: userId ?? "" + authenticatorIdHash: webAuthnKey.authenticatorIdHash }) } } @@ -442,10 +437,6 @@ export async function deserializePasskeyValidator< return false }, getSerializedData() { - if (window.sessionStorage === undefined) { - throw new Error("sessionStorage is not available") - } - const userId = sessionStorage.getItem("userId") return serializePasskeyValidatorData({ credentials, entryPoint, @@ -453,8 +444,7 @@ export async function deserializePasskeyValidator< pubKeyX, pubKeyY, authenticatorId, - authenticatorIdHash, - userId: userId ?? "" + authenticatorIdHash }) } } diff --git a/plugins/passkey/toWebAuthnKey.ts b/plugins/passkey/toWebAuthnKey.ts index 394f3371..b0c1a1e2 100644 --- a/plugins/passkey/toWebAuthnKey.ts +++ b/plugins/passkey/toWebAuthnKey.ts @@ -62,11 +62,6 @@ export const toWebAuthnKey = async ({ const loginVerifyResult = await loginVerifyResponse.json() - if (window.sessionStorage === undefined) { - throw new Error("sessionStorage is not available") - } - sessionStorage.setItem("userId", loginVerifyResult.userId) - if (!loginVerifyResult.verification.verified) { throw new Error("Login not verified") } @@ -87,12 +82,6 @@ export const toWebAuthnKey = async ({ ) const registerOptions = await registerOptionsResponse.json() - // save userId to sessionStorage - if (window.sessionStorage === undefined) { - throw new Error("sessionStorage is not available") - } - sessionStorage.setItem("userId", registerOptions.userId) - // Start registration const { startRegistration } = await import("@simplewebauthn/browser") const registerCred = await startRegistration(registerOptions.options) diff --git a/plugins/passkey/utils.ts b/plugins/passkey/utils.ts index c92092b5..4d195b31 100644 --- a/plugins/passkey/utils.ts +++ b/plugins/passkey/utils.ts @@ -75,7 +75,6 @@ type PasskeyValidatorSerializedData = { pubKeyY: bigint authenticatorId: string authenticatorIdHash: Hex - userId: string } export const serializePasskeyValidatorData = ( @@ -99,10 +98,6 @@ export const deserializePasskeyValidatorData = (params: string) => { const uint8Array = base64ToBytes(params) const jsonString = new TextDecoder().decode(uint8Array) const parsed = JSON.parse(jsonString) as PasskeyValidatorSerializedData - if (window.sessionStorage === undefined) { - throw new Error("sessionStorage is not available") - } - sessionStorage.setItem("userId", parsed.userId) return parsed }