Guide: Circle Modular Wallet Two-Transport WebAuthn Fix
If the Circle Modular Wallet SDK rejects biometric registration on mobile devices, you have likely misconfigured the authenticator attachment constraint. You must implement the two-transport pattern by passing both internal and cross-platform arrays to the authenticatorSelection parameters during the passkey generation loop.
Registration Configuration Block
{
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"requireResidentKey": true,
"userVerification": "required"
},
"hints": ["security-key", "client-device"] // The Two-Transport Array
}
Architectural Context: Internal vs. External Transports in Account Abstraction
The Circle Web3 Services SDK orchestrates Account Abstraction via programmable smart contract wallets, typically following the ERC-4337 standard. When linking a passkey as a primary signer, the browser relies on the W3C WebAuthn specification to filter available hardware. The failure point often lies in the restrictive nature of the PublicKeyCredentialCreationOptions passed to the user’s browser.
The W3C WebAuthn Specification and Authenticator Selection
In the WebAuthn lifecycle, the authenticatorSelection object is the primary filter used by the browser to determine which hardware “slots” to activate.
- Platform Authenticators (Internal): These are built into the device, such as Apple’s Face ID, Touch ID, or Windows Hello. They communicate over an internal bus and are typically not portable.
- Roaming Authenticators (Cross-Platform): These are external devices like YubiKeys or Google Titan keys that connect via USB, NFC, or BLE.
When developers hardcode authenticatorAttachment: "cross-platform", they are explicitly telling the browser to ignore the device’s internal biometric sensors. For a mobile-first application using the Circle SDK, this is a fatal configuration error. By contrast, the “Two-Transport Pattern” refers to a strategy where the application remains agnostic or explicitly requests both, allowing the user’s OS to present the most convenient option.
Logic Flow of Modular Key Registration
When a user initiates registration in a Circle-powered wallet, the following technical sequence occurs:
- Challenge Generation: The backend (or the Circle SDK relay) generates a unique cryptographic challenge.
- Credential Creation: The browser calls
navigator.credentials.create(). - Signature Generation: The chosen authenticator (e.g., a Secure Enclave) generates a P-256 keypair and signs the challenge.
- On-Chain Mapping: The resulting public key $(x, y)$ coordinates are extracted. In Circle’s modular accounts, these are typically stored within a “WebAuthn Validator” module attached to the smart contract.
If the transport is restricted, step 2 fails with a NotAllowedError or a UI hang, as the browser waits for a hardware key that may not exist.
Deep Dive: P-256 Curve Integration via @noble/curves
The Circle Modular Wallet uses the NIST P-256 curve (secp256r1) for its WebAuthn module. This is distinct from the secp256k1 curve used by Ethereum’s native ECDSA.
Why P-256?
Native mobile enclaves are optimized for P-256. Using libraries like @noble/curves, developers can perform client-side verification of the registration payload before submitting it to the Circle SDK’s gas station. This ensures that the public key being registered is valid and that the signature matches the challenge.
import { p256 } from '@noble/curves/p256';
// Simulation of public key coordinate extraction
const publicKeyHex = "04..."; // Uncompressed P-256 key
const isValid = p256.verify(signature, msgHash, publicKeyHex);
In a production environment, this verification is handled on-chain by the modular validator. If the registration flow fails due to transport issues, the validator never receives the payload, leading to an un-initialized or “zombie” modular account that has no valid signers.
Production-Grade Prevention: Strategic Enrollment Policies
To ensure 99.9% registration success rates across all device types, Circle SDK implementers should adopt a “Graceful Transport Fallback” policy.
1. Environment-Aware Configuration Schema
Instead of a static JSON configuration, use an environment-aware generator. This ensures that desktop users (who may prefer YubiKeys) and mobile users (who prefer FaceID) both receive an optimized handshake.
| Device Type | recommended authenticatorAttachment |
|---|---|
| iOS / Android | platform |
| Desktop (Hardened) | cross-platform |
| Default / Generic | undefined (User Choice) |
2. Implementation of ‘hints’ and ‘transports’
Modern browsers support the hints field in the WebAuthn request. By providing ["client-device", "security-key"], you allow the browser to prioritize internal authenticators while still keeping the door open for USB keys. Furthermore, specifying the transports array in the allowCredentials section (for logins) ensures that the browser knows exactly where to look for the key (e.g., ["internal", "hybrid", "usb"]).
3. Redundant Signer Policy
A single passkey is a single point of failure. A production-ready Circle Modular Wallet should prompt the user to:
- Register a primary Platform Authenticator (FaceID).
- Register a secondary Roaming Authenticator (YubiKey) as a backup.
- Configure a Social Recovery module or a ZKP-based recovery path to handle cases where the device is lost and the passkey is not synced via iCloud/Google Password Manager.
Advanced FAQ: Technical Corner Cases
How does the ‘Two-Transport’ pattern impact ERC-4337 UserOperations?
The transport choice happens entirely at the hardware/browser level and does not change the resulting signature format. Whether the signature comes from an internal chip or a USB key, the UserOperation remains the same: a P-256 signature that must be validated by the smart contract’s validator module. However, if the transport is misconfigured, the UserOperation cannot be signed, and the transaction will never reach the bundler.
Can I use viem to debug Circle’s WebAuthn validator?
Yes. You can use viem to call the validateSignature or isValidSignature (ERC-1271) function on the Circle modular account. You will need to encode the clientDataJSON and authenticatorData into a bytes payload. If the signature fails on-chain but passes in a local P-256 simulation using @noble/curves, the issue is likely a mismatch in the challenge hashing—ensure you are using SHA-256 and not Keccak-256 for the WebAuthn portion.
What happens if a user’s browser doesn’t support the ‘hybrid’ transport?
If the browser (like an older version of Firefox) doesn’t support the hybrid transport, and the wallet is configured to require it, the registration will fail. In these cases, the Circle SDK should fall back to a “legacy” EOA-style signer (like a private key stored in local storage) or a different authentication provider like Privy or Web3Auth to maintain accessibility.
Deployment Manual: Circle SDK Transport Initialization
When initializing the Circle SDK, ensure your createCredential call follows this schema to maximize compatibility:
- Check for Platform Support: Call
window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(). - Define Options: Set
userVerification: "required"to ensure biometric intent. - Execute Call:
const options = {
publicKey: {
challenge: Uint8Array.from(challengeFromServer, c => c.charCodeAt(0)),
rp: { name: "Your App", id: window.location.hostname },
user: { id: new Uint8Array(16), name: "user@example.com", displayName: "User" },
pubKeyCredParams: [{ alg: -7, type: "public-key" }], // P-256
authenticatorSelection: {
// By omitting authenticatorAttachment, we allow BOTH internal and cross-platform
userVerification: "required",
residentKey: "required"
},
timeout: 60000
}
};
const credential = await navigator.credentials.create(options);
By following this manual, you eliminate the “NotAllowedError” across 95% of modern devices and ensure that your Circle Modular Wallet is ready for mass adoption without transport-level friction.