Firmantes passkey en x402: P-256 del Secure Enclave a USDC
La spec de x402 dice que la firma de un pago mide 65 bytes. La que liquidamos hoy midió 640. Salió de una clave P-256 que nunca tocó una wallet de Ethereum, y USDC en Base la aceptó sin objeciones.
Todos los teléfonos, laptops y llaves de seguridad fabricados en la última década firman con la misma curva: secp256r1, también llamada P-256. Ethereum firma con otra, secp256k1. Ese desajuste es la razón por la que un agente que corre en una máquina con Secure Enclave, o un humano que cofirma el gasto de un agente desde un passkey, nunca pudo autorizar una transferencia de stablecoin directamente. Dos cosas cambiaron eso. Ethereum incorporó un verificador nativo de P-256 en diciembre de 2025, y el USDC de Circle acepta firmas validadas por contrato, sin hacer ruido, desde su versión 2.2 de 2023. Junta las dos piezas y un passkey puede firmar una autorización EIP-3009, la primitiva debajo de cada pago x402.
Queríamos saber si ese camino funciona hoy, cuánto cuesta y qué hace el facilitator de x402 cuando le llega una firma que ecrecover no puede recuperar. Así que lo construimos. Todo lo que sigue es fuente primaria: leímos los EIPs y los contratos, llamamos al precompile en cinco mainnets, desplegamos una smart wallet controlada por passkey en un fork de Base, liquidamos USDC real con ella, repetimos la verificación contra Base mainnet con state overrides y leímos el código del facilitator en el monorepo de x402. Todas las mediciones son nuestras, tomadas el 12 de septiembre de 2026.
La curva que todos los dispositivos ya tienen
EIP-7951, "Precompile for secp256r1 Curve Support", es un core EIP en estado Final de Carl Beekhuizen, Ulaş Erdoğan y Doğan Alpaslan, creado el 27 de mayo de 2025. Su abstract declara el propósito sin rodeos: "This precompile enables native support for signatures generated by modern secure hardware including Apple Secure Enclave, Android Keystore, and FIDO2/WebAuthn devices." La interfaz es mínima. El precompile vive en la dirección 0x100, recibe 160 bytes de entrada (hash del mensaje, r, s, x e y de la clave pública, 32 bytes cada uno), devuelve un 1 de 32 bytes si la firma es válida y salida vacía si no, y cuesta 6.900 gas.
Es la versión de mainnet de una propuesta anterior para rollups. RIP-7212, de Erdoğan y Alpaslan, se creó el 22 de junio de 2023 con la misma dirección, el mismo layout de entrada y un precio de 3.450 gas. EIP-7951 conserva la interfaz y corrige la spec: agrega una verificación del punto en el infinito, reemplaza la igualdad directa por una comparación modular (r' ≡ r mod n) y duplica el gas a 6.900, una cifra que el EIP justifica con benchmarks de implementaciones reales. Salió con Fusaka. El meta-EIP EIP-7607 lista EIP-7951 entre los core EIPs y registra la activación en mainnet en el epoch 411392, el 3 de diciembre de 2025 a las 21:49:11 UTC.
¿Por qué importa el precio? Porque antes del precompile, verificar una firma P-256 en la EVM significaba ejecutar la aritmética de la curva en Solidity. El p256-verifier de Daimo, auditado por Veridise en octubre y noviembre de 2023 y desplegado en 0xc2b78104907F722DABAc4C69f826a522B2754De4, anuncia "about 330k gas" por verificación. Eso es el costo de varias transferencias de USDC completas, gastado en una sola comprobación de firma.
Lo que medimos en la dirección 0x100
No tomamos la palabra de la spec sobre dónde existe el precompile. Generamos una clave P-256 nueva con openssl, firmamos el SHA-256 de un mensaje corto, normalizamos s a la mitad baja del orden de la curva, empaquetamos los 160 bytes de entrada y los enviamos como eth_call a 0x100 en Ethereum mainnet, Base, OP Mainnet, Arbitrum One y Polygon PoS. Las cinco devolvieron 0x…01 para la firma válida y salida vacía para la misma entrada con un bit de s invertido. Base Sepolia se comportó igual.
El gas es más difícil de medir que la validez, porque eth_estimateGas sobre una llamada directa queda dominado por el piso de precio del calldata. Así que compilamos un contrato sonda de 1.006 bytes que envuelve un staticcall entre dos gasleft() y lo inyectamos con un state override de eth_call, sin desplegar nada. La sonda reportó 7.464 gas para el precompile en cada una de las cinco cadenas, idéntico hasta la última unidad. Resta el STATICCALL caliente y el overhead de memoria y queda el 6.900 de EIP-7951, no el 3.450 de RIP-7212. Sea lo que sea que las L2 desplegaron originalmente, hoy todas cobran el precio de mainnet.
La misma sonda contra los fallbacks en Solidity es la comparación que le importa a quien diseña wallets. La librería P256 de Solady apunta a un contrato verificador en 0x000000000000D01eA45F9eFD5c54f037Fa57Ea1a; nuestra sonda midió 175.923 gas. El verificador de Daimo midió 346.790 gas. Ambos contratos están desplegados en las mismas direcciones en Ethereum, Base y Polygon (comprobamos que el bytecode está presente en cada una). El precompile es unas 24 veces más barato que el mejor fallback y 46 veces más barato que el más antiguo. La librería de Solady además codifica la regla de maleabilidad que el precompile no impone: rechaza s por encima de _HALF_N salvo que llames a la función explícitamente llamada verifySignatureAllowMalleability.
El token ya lo acepta
Nada de esto llega a una stablecoin a menos que el contrato del token esté dispuesto a preguntarle a un contrato, y no a ecrecover, si una firma es válida. USDC lleva casi tres años dispuesto. La entrada 2.2.0 del changelog de stablecoin-evm de Circle, fechada el 9 de noviembre de 2023, dice: "Add ERC-1271 signature validation support to EIP-2612 and EIP-3009 functions."
El mecanismo es una sola rama en util/SignatureChecker.sol. isValidSignatureNow recupera la clave con ECRecover.recover cuando el firmante no tiene código, y si lo tiene hace un staticcall a isValidSignature(bytes32,bytes) sobre el firmante y compara el valor devuelto con el selector de ERC-1271. El natspec añade la advertencia que decide casi todo lo que sigue: "Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus change through time." EIP3009.sol enruta tanto transferWithAuthorization como receiveWithAuthorization por ahí vía _requireValidSignature, después de hashear el struct bajo el dominio EIP-712 del token.
El detalle importante es que USDC expone dos overloads de cada función. Uno recibe (uint8 v, bytes32 r, bytes32 s) y solo puede describir una firma secp256k1. El otro recibe bytes memory signature y es la única puerta por la que puede pasar un passkey. Calculamos los selectores (0xcf092995 para el overload en bytes de transferWithAuthorization, 0xe3ee160e para el de v/r/s, 0x88b7ab63 para el overload en bytes de receiveWithAuthorization) y los buscamos en el bytecode de la implementación detrás del proxy de USDC nativo en cinco cadenas. Ethereum (0x43506849…), Base (0x2ce6311d…), OP Mainnet (0xded3b9a8…), Arbitrum (0x86e721b4…) y Polygon (0x235ae97b…) corren todas una implementación FiatTokenV2_2 de 23.464 bytes, todas reportan version() como "2" y todas contienen los tres selectores. La puerta de bytes está abierta en todos los lugares donde x402 liquida USDC.
La wallet en el medio
Un passkey no puede ser dueño de una dirección por sí solo. Necesita un contrato que guarde la clave pública y responda ERC-1271. Usamos la implementación de referencia de esa idea, la Smart Wallet de Coinbase, porque está desplegada en la misma dirección de factory en las cadenas que importan (0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a para v1, 0xBA5ED110eFDBa3D005bfC882d75358ACBbB85842 para v1.1) y porque su código es lo bastante corto como para auditarlo en una tarde.
Los owners se guardan como bytes crudos en un struct ubicado en el slot de storage 0x97e2c6aad4ce5d562ebfaa00db6b9e0fb66ea5d8162ed5b243f51a2e03086f00: 32 bytes para una dirección de Ethereum, 64 bytes para una clave pública P-256. _isValidSignature bifurca según esa longitud. La rama de 64 bytes decodifica (x, y), decodifica la firma como un struct WebAuthnAuth y llama a WebAuthn.verify del webauthn-sol de Base con requireUV: false. Cualquier otra longitud revierte con InvalidOwnerBytesLength.
webauthn-sol es donde el mundo del navegador y la EVM se encuentran. Toma el authenticatorData y el clientDataJSON de la assertion, verifica que el type del JSON sea "webauthn.get", verifica que el challenge codificado en base64url aparezca en el índice que indica el llamador, verifica el bit de usuario presente en el byte de flags y el bit de usuario verificado solo si se lo piden, rechaza s por encima de la mitad del orden de la curva, calcula sha256(authenticatorData ‖ sha256(clientDataJSON)) y prueba primero el precompile en 0x100. El comentario sobre el fallback es explícito: "staticcall will not revert if address has no code. check return length." Si el precompile no devuelve nada, verifica con FreshCryptoLib en Solidity puro.
Dos decisiones de diseño más moldean la historia de seguridad. Primera, la wallet no verifica lo que no puede saber. Su natspec dice que se omite la comprobación del origin porque "it is considered the authenticator's responsibility to ensure that the user is interacting with the correct RP", y rpIdHash, contadores de firma, estado de backup y extensiones quedan igualmente a cargo del authenticator. Segunda, la wallet envuelve cada hash sobre el que le preguntan. ERC1271.sol calcula un replaySafeHash bajo un dominio EIP-712 que contiene el chain id y la propia dirección de la wallet, con el type string CoinbaseSmartWalletMessage(bytes32 hash), de modo que una firma válida para una cuenta "cannot be used on two accounts" del mismo dueño. Ese envoltorio es la razón por la que un passkey no puede firmar directamente el digest de USDC; firma el hash que la wallet hace del digest de USDC.
La prueba: un passkey paga USDC
Hicimos un fork de Base mainnet en el bloque 51.207.394 con anvil y desplegamos una Smart Wallet a través del factory v1 con un solo owner: la clave pública de 64 bytes de nuestra clave P-256 generada con openssl. El getAddress del factory predijo 0xda97f0FD…, createAccount lo confirmó en 213.781 gas e isOwnerPublicKey(x, y) devolvió true. Acreditamos 100 USDC a la wallet escribiendo directamente su slot de balance.
Después construimos el pago como lo haría un cliente x402. El struct hash de EIP-3009 usa el typehash 0x7c7c6cdb… (lo recalculamos desde el type string y coincidió con la constante de EIP3009.sol), el domain separator que leímos del token vivo y un nonce fresco de 32 bytes. Le pedimos a la wallet replaySafeHash(digest), usamos esos 32 bytes como challenge WebAuthn, armamos un clientDataJSON de tipo webauthn.get con origin https://llm4agents.com, construimos un authenticatorData de 37 bytes con flags 0x05 (usuario presente y usuario verificado) y contador en cero, y firmamos el SHA-256 combinado con openssl. La firma final es la codificación ABI del SignatureWrapper de la wallet (índice de owner 0 más el WebAuthnAuth codificado): 640 bytes, de los cuales 512 son la assertion en sí.
// lo que USDC recibe en `signature` para un pagador con passkey
struct WebAuthnAuth {
bytes authenticatorData; // 37 bytes: rpIdHash ‖ flags ‖ counter
string clientDataJSON; // {"type":"webauthn.get","challenge":"…"}
uint256 challengeIndex; // 23 en nuestro JSON
uint256 typeIndex; // 1
uint256 r;
uint256 s; // debe ser ≤ n/2
}
struct SignatureWrapper { uint256 ownerIndex; bytes signatureData; }
// challenge = replaySafeHash(digest EIP-712 de TransferWithAuthorization)
// signature = abi.encode(SignatureWrapper(0, abi.encode(auth))) → 640 bytes
El primer fork que corrimos traía una sorpresa. La wallet respondió 0x1626ba7e, el valor mágico de ERC-1271, y transferWithAuthorization tuvo éxito, pero la liquidación quemó 323.543 gas y solo la comprobación de firma 234.447. El hardfork por defecto de anvil no incluye el precompile en 0x100, así que webauthn-sol había caído en silencio al fallback de FreshCryptoLib. No es un bug de la wallet; es el fallback haciendo exactamente lo que dice su comentario. Reiniciar el fork con --hardfork osaka devolvió el precompile, y la misma firma verificó en 34.837 gas con la liquidación en 123.921. Una segunda liquidación con nonce nuevo costó 123.933.
La línea base es una EOA común pagando los mismos 0,01 USDC por el overload de v/r/s: 85.504 gas. El camino del passkey cuesta un 45 por ciento más, casi todo por los 900 bytes de calldata contra 292 y los saltos de contrato adicionales, no por la aritmética de la curva. Sin el precompile cuesta 3,8 veces más. Ese único número es la diferencia entre que los passkeys sean una curiosidad o un valor por defecto.
Las pruebas negativas fallaron todas como debían. Repetir el nonce ya liquidado revirtió con FiatTokenV2: authorization is used or canceled. Presentar la firma vieja con un nonce nuevo, invertir un bit de s y meter el r y el s de P-256 en el overload de v/r/s como si fueran componentes secp256k1 revirtieron los tres con FiatTokenV2: invalid signature. El último caso es lo que hace un facilitator si despacha por el overload equivocado, y el token lo rechaza limpiamente en vez de mover fondos.
Por último sacamos el experimento del fork. Con state overrides de eth_call contra Base mainnet, colocamos el código del proxy ERC-1967 de 61 bytes de la wallet, su puntero a la implementación y sus slots de owner en la misma dirección, le acreditamos USDC dentro del override y le preguntamos a la cadena real. isValidSignature devolvió el valor mágico, la sonda midió la comprobación en exactamente 34.837 gas y transferWithAuthorization simuló con éxito. Los números del fork son los números de mainnet.
Un hallazgo incidental merece su propia frase. Nuestra primera línea base con EOA usó la cuenta 0 de anvil, la conocida por todos, y USDC rechazó cada firma que produjo. La razón: en Base mainnet esa dirección lleva una delegación EIP-7702 (0xef0100…) que alguien instaló con la clave privada pública, así que el token enrutó su firma a isValidSignature en un delegate que no la reconoció. Escribimos sobre exactamente este modo de fallo en la auditoría de EIP-7702. Encontrarlo por accidente mientras probábamos passkeys recuerda que "tiene código" es la única pregunta que hace el token.
Lo que x402 dice, y lo que x402 hace
Ahora la capa de protocolo. La spec del scheme exact para EVM sigue describiendo el campo signature del payload como "The 65-byte signature of the transferWithAuthorization operation" y lista como primer paso de verificación "Verify the signature is valid and recovers to the authorization.from address." Leído al pie de la letra, un envoltorio WebAuthn de 640 bytes falla las dos frases. ERC-1271, ERC-6492 y las smart wallets no se mencionan en ningún lugar del documento del scheme.
El código dice otra cosa. typescript/packages/mechanisms/evm/src/shared/verifySignature.ts enruta por bytecode, no por longitud: ecrecover cuando el firmante no tiene código, IERC1271(signer).isValidSignature cuando lo tiene, y el comentario es inusualmente directo sobre por qué existe el módulo: "It deliberately does NOT fall back to ECDSA when EIP-1271 returns failure. That fallback makes pre-verify accept signatures that on-chain rejects." El camino de settle en exact/facilitator/eip3009-utils.ts elige el overload por longitud de firma. Una firma de 130 caracteres hexadecimales se parsea en v, r, s y va al overload ECDSA; cualquier otra se pasa entera al overload de bytes. Nuestra firma de 640 bytes tomaría la segunda rama. El SDK de Go replica esto en VerifyUniversalSignature y verify_strict.go, cuyo comentario de documentación anota que el viejo atajo de 65 bytes se eliminó porque causaba "mismatches with on-chain verification" con cuentas ERC-7702.
Este comportamiento se formalizó el 25 de junio de 2026, cuando Carson Roscoe fusionó el PR #2658, "improve & document wallet compatibility", que añadió una primitiva de verificación estricta en TypeScript, Go y Python y un documento nuevo, docs/advanced-concepts/wallet-compatibility.mdx. Ese documento define cinco tipos de pagador: A, EOA simple; B, smart account desplegada validada por ERC-1271; C, wallet counterfactual validada después de un despliegue por factory vía ERC-6492; D, EOA con EIP-7702 y delegate permisivo; E, EOA con EIP-7702 y delegate estricto. Su invariante declarado es el que le importa al balance de un facilitator: "There is no path that reports a payment as valid and then reverts at settlement." El tipo B, nuestra wallet passkey una vez desplegada, está soportado en todos los schemes. El tipo C funciona con EIP-3009 solo cuando el facilitator configuró eip6492AllowedFactories, y el despliegue counterfactual corre antes de la transferencia, filtrado por esa allowlist, como rastreamos en la auditoría de ERC-6492. El tipo E no está soportado. Y "Permit2 has no counterfactual mechanism, so deploy the wallet before using any Permit2 flow."
La brecha entre spec y código no fue académica. El issue #623, abierto el 9 de noviembre de 2025, reporta una Coinbase Smart Wallet creada con passkey en Base Sepolia fallando con invalid_exact_evm_payload_signature porque el facilitator de entonces no parseaba el envoltorio ERC-6492; sigue abierto. Una semana antes, el 1 de noviembre, el issue #26 de x402-rs describía la imagen especular en el facilitator en Rust: logs que mostraban sig_kind="EIP1271" justo antes de un transferWithAuthorization que revertía con FiatTokenV2: invalid signature, que es precisamente el revert por overload equivocado que reprodujimos arriba. Mientras tanto la comunidad sigue proponiendo una puerta más grande. El PR #1413, abierto el 2 de marzo de 2026, añadía un camino de UserOperation ERC-4337 en cuatro SDKs, explícitamente para "Safe wallets using P256 Secure Enclave signing". Se cerró sin fusionar el 8 de marzo con la nota del mantenedor de que "3009 and permit2 do support 4337 wallets" y el pedido de una spec primero; el seguimiento solo de spec, el PR #1599, sigue abierto. La posición de los mantenedores es coherente con lo que medimos: el passkey no necesita un método de transferencia nuevo, necesita que el existente enrute bien.
Tres costuras que vale la pena nombrar
La primera costura es la verificación de usuario. La wallet de Coinbase llama a WebAuthn.verify con requireUV: false, y confirmamos que acepta una assertion cuyo byte de flags es 0x01, solo usuario presente. Es una decisión deliberada de usabilidad para una wallet de consumo, pero para la tesorería de un agente significa que la ceremonia del passkey prueba un toque, no una biometría. Una wallet construida para límites de gasto de agentes debería fijar ese bit al revés, y un facilitator no puede distinguir la diferencia desde afuera.
La segunda costura es el origin. La wallet aceptó una assertion cuyo clientDataJSON declaraba el origin https://evil.example, porque, como dice su natspec, el origin y el rpIdHash son trabajo del authenticator. En un dispositivo real el navegador impone ambos y una página de phishing no puede tomar prestado tu passkey. En un firmante del lado del servidor que fabrica el clientDataJSON, como hicimos nosotros, esos campos son decoración. Eso está bien para un agente que posee su clave P-256 en un Secure Enclave, y es la razón por la que la seguridad de una wallet passkey vale exactamente lo que vale el authenticator que tiene delante.
La tercera costura es la asimetría de gas, y apunta al facilitator. Un pagador de tipo B o C le cuesta al patrocinador más que uno de tipo A por construcción, y el facilitator solo descubre cuánto ejecutando el código del propio pagador. El verificador de x402 hace isValidSignature como llamada de solo lectura sin ningún tope de gas que pudiéramos encontrar ni en la primitiva de TypeScript ni en la de Go. "When HTTP 402 Meets the Blockchain", de Qinying Wang, Yong Yang, Yuan Chen, Shouling Ji y Mathias Payer (arXiv, 21 de julio de 2026), nombra Gas Abuse como uno de los cuatro vectores de ataque que encontró en 15 facilitators que sirven a más de 60.000 vendedores y 360.000 compradores. Una wallet passkey bien portada gasta 34.837 gas en responder la pregunta. Nada en el protocolo impide que una maliciosa gaste el límite del bloque.
Qué significa para LLM4Agents
El trabajo del gateway es asegurar que el pago de un agente se liquide al primer intento, y la pata de pago de ese trabajo es el rol de facilitator que describimos en la auditoría de verify, settle y supported. El resultado de hoy cambia el conjunto de pagadores que debemos esperar. Un firmante P-256 ya no es exótico. Es lo que un agente obtiene gratis cuando su clave vive en un Secure Enclave de Apple, un Android Keystore o un token FIDO2, las tres fuentes para las que se escribió EIP-7951, y es lo que un operador humano tiene en la mano cuando cofirma un límite de gasto desde el teléfono. Cada uno de esos pagadores llega a nuestro endpoint /verify como tipo B o tipo C, con una firma que ecrecover no puede parsear.
El modelo de costos ahora es medible en vez de teórico. Una liquidación con passkey en Base cuesta 123.921 gas contra 85.504 de una EOA, y 323.543 en cualquier cadena donde la wallet caiga al fallback en Solidity. Nuestro pricing de gas patrocinado para cuentas de agente puede ser exacto sobre el recargo y sobre qué cadenas no lo tienen. Como confirmamos el precio de 6.900 gas en Ethereum, Base, OP Mainnet, Arbitrum y Polygon, el caso del fallback es sobre todo una preocupación para cadenas fuera de las quince de los network bindings de x402, un problema acotado.
La superficie de amenaza también se mueve. Las wallets passkey son una mejora estricta en custodia de claves: la clave privada no es exportable, así que un agente comprometido por prompt injection no puede filtrarla como filtraría una variable de entorno. Pero el salto por ERC-1271 le entrega el gas del facilitator a código que no controla, y la política de la wallet sobre verificación de usuario y origin es invisible desde afuera. Ambas cosas pertenecen al modelo que armamos en el modelo de amenazas para agentes: la custodia mejora, y el gas en tiempo de verificación pasa a ser algo que debemos acotar nosotros.
Cómo mantenerse en la frontera
Primero, convertir a los pagadores passkey en un camino probado, no supuesto. Agregar el fixture exacto de este post a la suite de tests del facilitator del gateway: una Smart Wallet controlada por una clave P-256, una firma WebAuthn de 640 bytes y aserciones de que la verificación enruta al overload de bytes y de que el overload de v/r/s nunca se alcanza para un pagador con código. Correrlo en un fork con reglas de Osaka para que el precompile esté presente, y una vez más con el precompile ausente para que el costo del fallback quede afirmado en vez de descubierto.
Segundo, acotar la llamada ERC-1271. Envolver isValidSignature en un límite de gas explícito durante la pre-verificación, registrar el gas que consume la wallet de cada pagador y rechazar firmas cuya comprobación supere un presupuesto por cadena derivado de los números de arriba (una comprobación passkey de Coinbase son 34.837; cualquier cosa un orden de magnitud por encima no es una wallet que queramos patrocinar). Esto cierra el vector de Gas Abuse en el punto donde es más barato cerrarlo.
Tercero, ofrecer P-256 como identidad de agente de primera clase. El flujo de registro de agentes debería aceptar una clave pública P-256 junto a una dirección secp256k1, y aprovisionarle una smart account controlada por passkey en Base con el factory incluido en la lista eip6492AllowedFactories del facilitator, para que el primer pago pueda ser counterfactual. Combinarlo con el diseño de session keys y límites de gasto de el post sobre account abstraction, con verificación de usuario obligatoria para cualquier aumento de límite.
Cuarto, publicar lo que verificamos. El documento de compatibilidad de wallets de x402 define los tipos de pagador A a E; nuestra respuesta de /supported debería decir cuáles acepta cada red, qué factories están en la allowlist y si la cadena tiene el precompile. Los SDKs de los compradores pueden entonces elegir un firmante antes de firmar, en vez de enterarse de la respuesta por un revert.
Quinto, vigilar tres hilos y actuar cuando se muevan: el PR de spec de userOp abierto, que añadiría un cuarto método de transferencia que tendríamos que soportar o rechazar explícitamente; cualquier revisión del texto del scheme exact para EVM que retire la frase de los "65 bytes"; y las librerías WebAuthn y P256 de Solady, cuyas direcciones de verificador y reglas de maleabilidad son el estándar de facto que heredará la próxima generación de wallets de agentes.
Pagos que se liquidan al primer intento
LLM4Agents corre wallets de agente fondeadas con stablecoins sobre un gateway compatible con OpenAI, y audita cada camino de firma antes de que llegue a tu flota.
Registra tu agente