Passkey smart accountの仕組み|WebAuthn・P-256を検証
Passkey smart accountをWebAuthn assertion、challenge、origin、RP ID、P-256、EIP-7951、ERC-1271へ分け、正常・改変caseとgasを検証します。

Passkey smart accountは、顔や指紋をblockchainへ送るaccountではありません。端末側のauthenticatorが利用者確認を行い、そのcredentialの秘密鍵で作った公開鍵署名をapplicationとaccountが検証する仕組みです。
この記事では、credential登録とauthenticationを分け、clientDataJSON、authenticatorData、DER署名、P-256 verifier、ERC-1271までを同じ固定vectorで追います。正常系、origin不一致、challenge不一致、RP ID不一致、署名改変、high-s、複数credentialのtestもlocalで実行します。
仕様statusは2026年8月30日時点で、WebAuthn Level 3がW3C Recommendation、RIP-7212・EIP-7951・ERC-1271がFinalです。
個人Passkey、biometric prompt、実在wallet、public RPC、deployed account、transaction、実資産は使いません。公開検証vectorの秘密値は誰でも知っているtest用scalarなので、accountやcredentialへ流用できません。
Passkey・WebAuthn・P-256・smart accountを分ける
最初に4つの役割を分けます。
| 用語 | この記事での役割 | それだけでは決まらないもの |
|---|---|---|
| Passkey | passwordを公開鍵credentialへ置き換える利用体験・credential | account contract、chain、recovery policy |
| WebAuthn | browser、RP、authenticator間の登録・認証ceremonyとdata形式 | EVMでの検証cost、account interface |
| P-256 / secp256r1 | WebAuthnのES256で使える署名curve | origin、challenge、RP IDが正しいか |
| smart account | 公開鍵・policy・実行権限を持つcontract account | 端末が本当に利用者確認したか、credential sync方法 |
端末で顔・指紋・PINを使う場合も、その結果は通常「user verificationを通過した」というflagと署名へ反映されます。顔画像や指紋templateそのものをRPやchainへ渡す設計ではありません。
ただし、すべてのPasskeyが同じhardware保護、sync、backup、recoveryを持つとは限りません。authenticator、OS、browser、credential policyを別に確認します。
credential作成時に保存するもの
WebAuthn Level 3では、登録と認証は別のceremonyです。登録ではRPが一度だけ使うchallengeを作り、navigator.credentials.create()を通してauthenticatorへcredential作成を依頼します。
applicationは少なくとも次をaccountまたは安全なregistryへ対応させます。
- credential ID
- COSE公開鍵とalgorithm。ES256ならP-256の
qx、qy - credentialを作ったRP IDと許可するorigin policy
- user verification、backup、attestationをどう扱うか
- 追加credential、削除、device紛失時のrecovery policy
秘密鍵は検証側へ保存しません。公開検証vectorでは再現のため公開test scalarから鍵を作りますが、production credentialのprivate keyをserver、contract、logへ出す手順ではありません。
attestationは「そのcredentialをどの種類のauthenticatorが作ったか」を評価する別の仕組みです。authentication assertionのP-256署名が正しいことと、attestationを信頼することは分けます。
authentication assertionは何へ署名するか
認証では、RPが新しいchallengeを渡し、navigator.credentials.get()が登録済みcredentialへassertionを依頼します。主な返り値は次の通りです。
| field | 内容 | 主な検査 |
|---|---|---|
rawId / credential ID |
どの公開鍵候補を使うか | 登録済み・有効なcredentialか |
clientDataJSON |
type、challenge、origin、cross-origin情報 | requestとbrowser contextへ一致するか |
authenticatorData |
RP ID hash、flags、signCount、extension data | RP・利用者確認・backup state・counter policy |
signature |
ES256ならDER形式のP-256署名 | 保存済み公開鍵で検証できるか |
userHandle |
discoverable credentialのuser対応 | 期待するaccountへ対応するか |
署名対象は、単なるchallengeでもtransaction hashでもありません。
clientDataHash = SHA-256(clientDataJSON)
signedBytes = authenticatorData || clientDataHash
messageHash = SHA-256(signedBytes)
signature = P-256-sign(messageHash)
次の図では、端末内の利用者確認と、外へ渡るassertionを分けています。
biometric情報ではなく、公開鍵で検証できるassertionがapplication側へ戻ります。そのため、P-256署名だけを確認しても不十分で、どのchallenge・origin・RP IDに対するassertionかを同じ入力として検査します。
challenge・origin・RP ID・flags・counterを読む
challengeは一回の認可対象へbindする
challengeはserverまたはaccount applicationが作る予測困難な値です。loginならsession、smart accountならuserOpHashやoperation digestなど、今回許可する対象へ対応させます。
古いchallengeを再利用したり、署名後にoperationを差し替えたりできないよう、期限、nonce、chain、account、callをどのdigestへ含めたかも固定します。WebAuthnがreplay policyを自動で設計してくれるわけではありません。
originとRP ID hashは別field
clientDataJSON.originはbrowserが見たoriginです。一方、authenticatorDataの先頭32 bytesはSHA-256(RP ID)です。
今回の公開検証vectorは次を別々に固定します。
origin = https://wallet.3mikan.test
RP ID = wallet.3mikan.test
RP ID hash = SHA-256("wallet.3mikan.test")
originの文字列だけ、またはRP ID hashだけを確認するのではなく、登録時policyとrequest contextへ両方を対応させます。crossOriginや関連originを許可する場合も、暗黙に全originを通さずpolicyを明示します。
flagsは一つの「生体認証済み」booleanではない
37 bytes以上ある通常のauthenticatorDataでは、32 bytesのRP ID hashの次がflags、続く4 bytesがbig-endianのsignCountです。
| bit | 名称 | 確認する意味 |
|---|---|---|
0x01 |
UP | user presenceが確認された |
0x04 |
UV | PINやbiometric等によるuser verificationが行われた |
0x08 |
BE | credentialがbackup対象になり得る |
0x10 |
BS | credentialが現在backupされている |
0x40 |
AT | attested credential dataを含む |
0x80 |
ED | extension dataを含む |
資産操作でUVを要求するかはaccount policyです。BS=1ならBE=1である関係も確認します。ただし、BE / BSは「このproviderなら安全」「別端末から必ず復旧できる」という保証ではありません。
counterは値が増える前提だけでhard rejectしない
RPは保存済みcounterと新しいsignCountを比べ、credential cloneのsignalにできます。ただし、counterが常に意味のある増分を返すとは限りません。0を返すauthenticatorや、multi-device credentialの挙動を想定し、risk signal、step-up、credential失効などapplication policyとして決めます。
ERC-1271のisValidSignatureはread-only判定なので、counter stateの更新には向きません。counterをon-chainで消費するなら、account execution validationとstate更新を同じtransaction境界で設計し、第三者が先にcounterだけ消費するDoSも避けます。
OpenZeppelin WebAuthnで省略される検査を補う
OpenZeppelin Contracts 5.6.1のWebAuthn.verifyは、次を検査します。
- typeが
webauthn.get - challengeが期待値と一致
- UP、必要ならUV
- BE / BSの組み合わせ
- P-256署名とlow-s policy
一方、同versionのdocumentとsourceは、origin、RP ID hash、counter、extension output、attestationを意図的に省略すると明記しています。これはWebAuthn RP全体で不要という意味ではありません。
公開例のERC-1271 wrapperは、保存済みRP ID hashをauthenticatorDataと比較し、固定したoriginを含むclientDataJSONへ一致させてからWebAuthn.verifyを呼びます。
if (!_matchesRpId(auth.authenticatorData)) return 0xffffffff;
if (
keccak256(bytes(auth.clientDataJSON)) !=
keccak256(bytes(expectedClientDataJSON(digest)))
) return 0xffffffff;
return WebAuthn.verify(
abi.encodePacked(digest),
auth,
credential.qx,
credential.qy
) ? IERC1271.isValidSignature.selector : bytes4(0xffffffff);
完全なJSON文字列比較は、field順や追加fieldを許さない教育用例の単純化です。productionではW3Cのsemantic verificationを実装し、clientとcontractのどちらが何を検査したかを監査できる形にします。
DER署名を32-byteのr,sへ変換する
WebAuthnのES256 signatureは通常、ASN.1 DERのSEQUENCE(INTEGER r, INTEGER s)です。EIP-7951のprecompileはDERを受け取らず、rとsを各32 bytesのbig-endian値として受け取ります。
変換では次を確認します。
- outer tagが
0x30で、長さが入力全体と一致する rとsが0x02のINTEGERである- 負数ではなく、不要な先頭0や末尾dataがない
- 符号bitを避けるための先頭
0x00だけを外す - 左を0で埋め、各32 bytesへする
- applicationがlow-sを要求するなら
N/2以下へ正規化する
公開検証vectorのDERは70 bytesです。
DER = 3044 0220 <32-byte r> 0220 <32-byte s>
r = 19c7406a...cff50248
s = 73f92dbe...2efe4e7d
EIP-7951は0 < r,s < Nを要求しますが、low-sを必須にしません。OpenZeppelin P256はmalleability対策として0 < s <= N/2を要求します。high-sの同値署名が来た場合は、採用libraryのpolicyに合わせてbrowser / server側でN - sへ正規化するか、明示的に拒否します。
RIP-7212・EIP-7951・Solidity fallbackの違い
RIP-7212とEIP-7951は、どちらも0x0000000000000000000000000000000000000100へ次の160 bytesを渡します。
32 bytes message hash
32 bytes r
32 bytes s
32 bytes public key qx
32 bytes public key qy
成功時は32-byteの1、失敗時は空bytesです。EIP-7951はRIP-7212とのinterface互換を維持しつつ、infinity pointとr比較の境界を修正し、gasを6,900としています。
2026年8月30日時点の確認matrix
| 対象 | active仕様 | address | activation / gas | 確認範囲 |
|---|---|---|---|---|
| Ethereum mainnet | EIP-7951 | 0x100 |
Fusaka 2025-12-03 / 6,900 | EIP-7607とEthereum Foundation announcement |
| Base mainnet | RIP-7212 → EIP-7951相当 | 0x100 |
Fjord 2024-07-10で3,450、Azul 2026-05-21から6,900 | Base公式upgrade docs |
| OP Stack一般 | Fjord specにRIP-7212 | 0x100 |
chainごとのactivationが必要 | OP Stack採用だけで各chain有効化を断定しない |
| local Foundry | Osaka / EIP-7951 | 0x100 |
precompile trace 6,900 | Foundry 1.8.0 deterministic test |
chain名の一覧をcopyして判定せず、対象chainのfork設定、公式upgrade記録、known-valid vectorへのreturn dataを確認します。空returnだけでは「precompileがない」のか「署名が無効」なのかを区別できません。
同じvectorで測ったgas
Foundry 1.8.0、Solidity 0.8.36、optimizer 200、Osaka EVMで、同じvalid vectorをwarm-up後に測りました。
| path | local gas | 読み方 |
|---|---|---|
| EIP-7951 precompile本体 | 6,900 | protocol仕様とtraceが一致 |
P256.verifyNative external envelope |
13,424 | public key / low-s checkと外部callを含むlocal wrapper |
P256.verify auto external envelope |
13,391 | nativeがあるためprecompileを選択 |
P256.verifySolidity external envelope |
259,167 | Solidity fallbackとModExp callsを含む |
33 gasのauto / native差に意味を持たせず、桁の違いを見ます。これはaccount全体、calldata、ERC-4337 validation、transaction intrinsic gasを含む総gasではありません。compiler、library、warm / cold state、forkが変われば再計測します。
native-onlyはbytecodeとgasを抑えられますが、未対応chainでは使えません。OpenZeppelin P256.verifyはnativeをknown-valid vectorで検出し、未対応ならSolidityへfallbackします。対象chainを限定できないaccountでは、fallbackを含むdeploy sizeとvalidation gas limitも見積もります。
ERC-1271 accountでassertionを検証する
ERC-1271では、contract accountが現在のstateを読んで、validなら0x1626ba7eを返します。公開例はsignatureへcredential IDとWebAuthn assertionをABI encodeし、登録済み公開鍵を選びます。
| case | 変更したもの | expected | local result |
|---|---|---|---|
| normal | 変更なし | ERC-1271 magic value | pass |
| origin mismatch | clientDataJSON.origin |
invalid | pass |
| challenge mismatch | expected digest | invalid | pass |
| RP ID mismatch | expected RP ID hash | invalid | pass |
| signature mutation | rの1 bit |
invalid | pass |
| high-s | s = N - s |
OpenZeppelin policyでinvalid | pass |
| malformed encoding | 4 bytesだけ | revertせずinvalid | pass |
| removed credential | recovery後に旧credential削除 | invalid | pass |
Foundryは11/11、browserのWebCrypto検証は5 caseを通しました。端末credentialを使わずにfieldとcurveの境界を確認できるlocal assertion demoも用意しています。
ERC-1271はsignature形式を標準化しないため、callerとaccountが同じABIを使う必要があります。contract signer全般のmagic value、revert、state依存はERC-1271でcontract account署名を検証する方法へつなげられます。
accountがまだdeployされていない段階で同じERC-1271 signatureを検証する場合は、ERC-6492のwrapper・rollback・factory policy検証を使います。ERC-6492は外側のaccount準備を担当し、WebAuthn challenge、origin、RP ID、flags、credential stateの確認は内側のaccount実装に残ります。
ERC-4337 accountへ組み込む場合も、UserOperation.signatureの中身はaccount実装が決めます。challengeへuserOpHashをbindし、signature mismatchをbundler受付、account validation、inclusion後のexecutionと分ける手順はERC-4337 UserOperationの失敗解析を参照してください。
EIP-7702でcodeを委任したEOA addressが同じvalidatorを提供する構成もあります。EOAのaddressと委任codeの責務はEIP-7702 delegationの確認方法で分けています。
複数credentialとdevice紛失を先に設計する
Passkeyを追加しただけでは、account recoveryは完成しません。登録時点で少なくとも次を決めます。
- 同じaccountへ複数credentialを登録できるか
- 追加・削除を誰が、何署名で、どのdelay後に実行できるか
- 最後のcredentialを削除できるか
- synced credentialとdevice-bound credentialを区別するか
- guardian、別端末、hardware key、social recoveryをどう組み合わせるか
- 盗難時にcredentialを失効し、古いassertionをreplayできないか
- recovery主体がaccountを奪えるcentralizationをどう制限するか
公開例は二つ目のcredentialを追加し、旧credentialを削除した後に同じassertionがinvalidになること、最後の一つは削除できないこと、admin以外は追加できないことをtestします。
ただし、このadminはtradeoffを見せる教育用です。productionで単一serverをrecovery adminにすると、そのserverがaccountを乗っ取れる権限になります。self-call、guardian threshold、timelock、利用者通知、cancel windowなど、脅威modelに合う経路へ置き換えます。
deviceを失った後に初めて二つ目のcredentialを追加する設計では、追加を承認する鍵が残っていない可能性があります。利用可能なcredentialと独立したrecovery経路を、資産を置く前に試すことが必要です。
browser versionは基本APIとPasskey機能を分ける
MDN browser-compat-dataの2026年8月30日確認値では、基本PublicKeyCredential interfaceは次のversionから記録されています。
| browser | basic PublicKeyCredential |
注意 |
|---|---|---|
| Chrome | 67 | conditional UIや各extensionのversionではない |
| Edge | 18 | 現行Chromium版の全Passkey挙動を表す表ではない |
| Firefox | 60 | dataには初期のUSB U2F制約注記がある |
| Safari | 13 | platform credential、sync、extensionは別に確認 |
この表は「そのversionなら同じPasskey UX、sync、biometric、recoveryが使える」という互換表ではありません。実装時は必要なmethod、extension、conditional mediation、OS / authenticator条件をfeature detectionと実機testで確認します。
この記事のbrowser demoもnavigator.credentials.create() / get()を呼ばず、公開vectorをcrypto.subtle.verify()で検算するだけです。実在browserのPasskey UI captureが必要な作業はIssue #76へ分離します。
実装前のチェックリスト
- registrationとauthenticationを別ceremonyとして設計した
- credential ID、P-256公開鍵、RP ID、origin、recovery policyを対応させた
- challengeをoperation、nonce、期限、chain、accountへbindした
-
clientDataJSONのtype、challenge、origin、cross-origin policyを検査した -
authenticatorDataのRP ID hash、UP、UV、BE / BS、counter policyを検査した - DERをstrictにparseし、採用libraryのlow-s policyへ合わせた
- 対象chainの
0x100activationとgasを公式情報・known-valid vectorで確認した - native-onlyとfallbackのdeploy size・validation gasを計測した
- ERC-1271 magic valueまたはERC-4337 account validationまでtestした
- 複数credential、最後のcredential、失効、device紛失、recovery権限をtestした
- biometric dataやPasskey syncをchain側の保証として説明していない
Passkeyで守る対象はP-256署名だけではありません。challenge、origin、RP ID、flags、credential state、recovery policyを同じaccount認可へ対応させることが実装の中心です。
なお、Passkey SDK、smart account infrastructure、auth service、security auditのsponsorを将来掲載する場合も、広告であることを明示し、仕様status、chain対応、failure test、recoveryのsecurity評価から分離します。採用serviceを安全証明として扱いません。
確認した一次情報
- W3C Web Authentication Level 3 Recommendation確認日: 2026/08/30
- EIP-7951 secp256r1 precompile確認日: 2026/08/30
- RIP-7212 secp256r1 precompile確認日: 2026/08/30
- EIP-7607 Fusaka hardfork meta確認日: 2026/08/30
- Base Fjord upgrade確認日: 2026/08/30
- Base Azul P-256 gas update確認日: 2026/08/30
- OpenZeppelin Contracts 5.x WebAuthn確認日: 2026/08/30
- OpenZeppelin Contracts 5.x P256確認日: 2026/08/30
- OpenZeppelin Contracts v5.6.1確認日: 2026/08/30
- ERC-1271 contract signature validation確認日: 2026/08/30
- MDN PublicKeyCredential browser compatibility data確認日: 2026/08/30
- Foundry v1.8.0確認日: 2026/08/30



