Advanced
Passkey Authentication
Complete guide to passkey-based wallets with WebAuthn integration, registration, and transaction signing.
Passkey wallets provide passwordless, biometric-based authentication without managing private keys. This guide covers complete passkey integration with the MSA API.
Overview
MSA supports three passkey-enabled custody types:
- PASSKEY_VALIDATOR (Type 3): Pure passkey-based
- ECDSA_PASSKEY_VALIDATOR (Type 2): Hybrid ECDSA + passkey
- MULTISIG_PASSKEY_VALIDATOR (Type 5): Multisig with passkey
Passkey Registration
Registration Requirements
Critical: Passkeys must be registered with these exact specifications:
const publicKeyOptions = {
pubKeyCredParams: [
{
type: "public-key" as const,
alg: -7, // ES256 (P-256 curve) - REQUIRED
},
],
attestation: "direct" as AttestationConveyancePreference,
authenticatorSelection: {
authenticatorAttachment: "platform" as AuthenticatorAttachment, // Platform authenticators
residentKey: "required" as ResidentKeyRequirement,
requireResidentKey: true,
userVerification: "required" as UserVerificationRequirement, // UV flag - REQUIRED
}
};Registration Code
async function registerPasskey(userId: string, userName: string) {
// Generate challenge
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = {
challenge,
rp: {
name: "Your App Name",
id: window.location.hostname,
},
user: {
id: new TextEncoder().encode(userId),
name: userName,
displayName: userName,
},
pubKeyCredParams: [
{
type: "public-key",
alg: -7, // ES256
},
],
attestation: "direct",
authenticatorSelection: {
authenticatorAttachment: "platform",
residentKey: "required",
requireResidentKey: true,
userVerification: "required",
},
};
// Create credential
const credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreationOptions
}) as PublicKeyCredential;
if (!credential) {
throw new Error('Failed to create passkey');
}
const response = credential.response as AuthenticatorAttestationResponse;
// Extract public key (CRITICAL: Keep in base64URL DER format)
const publicKey = extractPublicKey(response);
// Store credential ID and public key
const credentialId = arrayBufferToBase64URL(credential.rawId);
return {
credentialId,
publicKey, // base64URL encoded, DER format - DO NOT CONVERT
};
}Creating Passkey Wallets
Pure Passkey Wallet (Type 3)
// After registering passkey
const { credentialId, publicKey } = await registerPasskey(userId, userName);
// Create wallet with passkey
const wallet = await client.createWallet({
walletCustody: CustodyType.PASSKEY_VALIDATOR,
salt: `user-${userId}`,
passkeyPubKey: [publicKey] // Keep in base64URL DER format!
});Hybrid Passkey Wallet (Type 2)
const wallet = await client.createWallet({
walletCustody: CustodyType.ECDSA_PASSKEY_VALIDATOR,
salt: `user-${userId}`,
passkeyPubKey: [publicKey] // Passkey as additional auth method
});Signing Transactions
Complete Passkey Signing Flow
async function signTransactionWithPasskey(
userOpHash: string,
credentialId: string
): Promise<PasskeySignature> {
// 1. Convert hash to Uint8Array
const challengeBytes = hexToUint8Array(userOpHash);
// 2. Create authentication options
const publicKeyOptions: PublicKeyCredentialRequestOptions = {
challenge: challengeBytes,
allowCredentials: [
{
id: base64UrlToBuffer(credentialId),
type: "public-key",
transports: ["internal", "usb", "nfc", "ble", "hybrid"],
},
],
userVerification: "required" as UserVerificationRequirement,
timeout: 60000,
};
// 3. Get credentials and sign
const credential = await navigator.credentials.get({
publicKey: publicKeyOptions
}) as PublicKeyCredential;
if (!credential || !credential.response) {
throw new Error('Failed to sign with passkey');
}
const response = credential.response as AuthenticatorAssertionResponse;
// 4. Extract signature components
const authenticatorData = arrayBufferToHex(response.authenticatorData);
let clientDataJSON = new TextDecoder().decode(response.clientDataJSON);
// 5. Clean clientDataJSON
const clientData = JSON.parse(clientDataJSON);
delete clientData.other_keys_can_be_added_here;
clientDataJSON = JSON.stringify(clientData);
// 6. Parse DER signature
const signatureArray = new Uint8Array(response.signature);
const { r, s } = parseDERSignature(signatureArray);
return {
authenticatorData,
clientDataJSON,
r: r.toString(),
s: s.toString()
};
}Helper Functions
function hexToUint8Array(hex: string): Uint8Array {
return new Uint8Array(
hex.slice(2).match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))
);
}
function arrayBufferToHex(buffer: ArrayBuffer): string {
return '0x' + Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
function base64UrlToBuffer(base64url: string): ArrayBuffer {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const bin = atob(base64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) {
bytes[i] = bin.charCodeAt(i);
}
return bytes.buffer;
}
function parseDERSignature(derSignature: Uint8Array): { r: bigint; s: bigint } {
let offset = 4;
const rLength = derSignature[offset - 1];
let r = derSignature.slice(offset, offset + rLength);
if (r[0] === 0) r = r.slice(1);
offset += rLength + 2;
const sLength = derSignature[offset - 1];
let s = derSignature.slice(offset, offset + sLength);
if (s[0] === 0) s = s.slice(1);
return {
r: BigInt('0x' + Array.from(r).map(b => b.toString(16).padStart(2, '0')).join('')),
s: BigInt('0x' + Array.from(s).map(b => b.toString(16).padStart(2, '0')).join(''))
};
}Encoding Passkey Signatures
Before sending to the API, encode signatures:
import { encodeAbiParameters } from 'viem';
function encodePasskeySignature(sig: PasskeySignature): `0x${string}` {
return encodeAbiParameters(
[{
type: 'tuple', components: [
{ type: 'bytes' }, // authenticatorData
{ type: 'string' }, // clientDataJSON
{ type: 'uint256' }, // challenge (23)
{ type: 'uint256' }, // type (1)
{ type: 'uint256' }, // r
{ type: 'uint256' } // s
]
}],
[[
sig.authenticatorData,
sig.clientDataJSON,
BigInt(23),
BigInt(1),
BigInt(sig.r),
BigInt(sig.s)
]]
) as `0x${string}`;
}Complete Transaction Flow
async function executeWithPasskey(
salt: string,
credentialId: string,
operation: Operation
) {
// 1. Build UserOperations
const result = await smartWallet.buildUserOperations([{
account: {
walletCustody: CustodyType.PASSKEY_VALIDATOR,
salt,
publicKeys: [publicKey]
},
operations: [operation],
settings: {}
}], [], []);
// 2. Sign each UserOp hash with passkey
const passkeySignatures = await Promise.all(
result.userOps.map(userOp =>
signTransactionWithPasskey(userOp.userOpHash, credentialId)
)
);
// 3. Encode signatures
const encodedSignatures = passkeySignatures.map(encodePasskeySignature);
// 4. Execute
const response = await smartWallet.provideSignaturesAndRequestSendUserOperations(
result.userOps,
result.accessList,
encodedSignatures
);
return response;
}Multiple Passkeys
Support multiple passkeys for backup:
// Register multiple passkeys
const passkey1 = await registerPasskey(userId, 'Device 1');
const passkey2 = await registerPasskey(userId, 'Device 2');
const passkey3 = await registerPasskey(userId, 'Device 3');
// Create wallet with all passkeys
const wallet = await client.createWallet({
walletCustody: CustodyType.PASSKEY_VALIDATOR,
salt: `user-${userId}`,
passkeyPubKey: [
passkey1.publicKey,
passkey2.publicKey,
passkey3.publicKey
]
});
// Use any registered passkey to sign
const signature = await signTransactionWithPasskey(
userOpHash,
passkey1.credentialId // or passkey2, passkey3
);Best Practices
- Always Enable UV Flag: Required for security
- Use P-256 Curve: ES256 algorithm required
- Keep DER Format: Don't convert public key format
- Multiple Passkeys: Register backup passkeys
- Store Credential IDs: Keep credentialId for each passkey
- Handle Errors: Network issues can interrupt signing
- User Feedback: Show loading during signing
Common Issues
Public Key Format
Error: "Invalid public key format"
Solution: Keep public key in base64URL DER format. Do NOT convert:
// ❌ Wrong
const publicKey = JSON.parse(publicKeyString);
// ✅ Correct
const publicKey = extractedPublicKey; // Keep as-is from credentialSignature Parsing
Error: "Invalid signature"
Solution: Ensure proper DER parsing:
// ✅ Correct
const { r, s } = parseDERSignature(response.signature);
// Remove leading zeros if presentNext Steps
- Passkey Execution - Detailed execution guide
- Multisig Wallets - Combine with multisig
- Troubleshooting - Common issues
🔐 Passwordless Future: Passkey wallets provide the best user experience with biometric security.