3MIKAN
仮想通貨直コン

Passkey smart accountの仕組み|WebAuthn・P-256を検証

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

3MIKANのブランドキャラクターが端末の鍵を複数の検査ゲート越しにsmart accountへ対応させる記事画像

Passkey smart accountは、顔や指紋をblockchainへ送るaccountではありません。端末側のauthenticatorが利用者確認を行い、そのcredentialの秘密鍵で作った公開鍵署名をapplicationとaccountが検証する仕組みです。

この記事では、credential登録とauthenticationを分け、clientDataJSONauthenticatorData、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のqxqy
  • 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を分けています。

browser、authenticator、assertion、P-256 verifier、smart accountを結ぶ検証経路

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も避けます。

challenge、origin、RP ID hash、flags、counter、P-256署名の検査担当を分けた図

OpenZeppelin WebAuthnで省略される検査を補う

OpenZeppelin Contracts 5.6.1WebAuthn.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を受け取らず、rsを各32 bytesのbig-endian値として受け取ります。

変換では次を確認します。

  1. outer tagが0x30で、長さが入力全体と一致する
  2. rs0x02のINTEGERである
  3. 負数ではなく、不要な先頭0や末尾dataがない
  4. 符号bitを避けるための先頭0x00だけを外す
  5. 左を0で埋め、各32 bytesへする
  6. 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の0x100 activationと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を安全証明として扱いません。

確認した一次情報