Proof-as-Auth: 鍵を一度も送らずにサインインする
従来のあらゆる認証フローには、避けられない瞬間があります。秘密がネットワークを渡る瞬間です。Bearerトークン、リフレッシュトークン、ハッシュ化されたパスワード――サーバーは何かを受け取り、保存し、守り続けなければなりません。Sealプルーフ認証はその前提を崩します。鍵はブラウザを出ない。サーバーが受け取るのは鍵の所持を証明するゼロ知識証明で、鍵そのものも鍵のハッシュもネットワークには出ません。
TL;DR
従来のあらゆる認証フローには、避けられない瞬間があります。秘密がネットワークを渡る瞬間です。Sealプルーフ認証はその前提を崩します。鍵はブラウザを出ません。サーバーが受け取るのは鍵の所持を証明するゼロ知識証明だけで、改ざんも転用もできず、鍵そのものもそのハッシュもネットワークには出ません。
鍵を受け取らずに本人性を検証する
認証とはサーバーが識別できる何かを送ることだ、と私たちは学んできました。パスワード、トークン、クッキー。サーバーは受け取ったものと保存済みのものを照合します。この直感は自然なものです。
Sealプルーフ認証は仕組みが違います。ブラウザが鍵に対してGroth16ゼロ知識回路を実行し、証明(proof) と短い nullifier を生成します。nullifierは「この鍵」と「このセッション」に固有の値ですが、鍵そのものも鍵のハッシュも何も明かしません。サーバーは証明の正当性を検証し、nullifierで身元を確認してサインイン完了とします。
証明とnullifierがサーバーに届きます。鍵は届かない。鍵のハッシュも届かない。
ブラウザの中で何が起きているか
Lemma DashboardでSealプルーフサインインを選ぶと、3つのことが順に起きます。
1. チャレンジ取得。 サーバーがランダムなnonceを発行し、有効期限5分の署名付きトークンに包みます。このnonceが証明を「この特定のサインイン試行」に結びつけ、nullifierをこのセッション限定のものにします。
2. 証明生成。 APIキーが512ビット(64 ASCIIバイト)に展開されます。WebAssemblyでブラウザ内で動くseal-identity-v1 Groth16回路が、そのビット列をプライベート入力として、nonceをパブリック入力として受け取ります。回路の内部では次のことが起きます。
keyHash = SHA-256(keyBits)を計算する――ただし中間シグナルとして扱い、公開出力には出さない- 256ビットのhashを2つの128ビットフィールド要素に分割:
keyHash_hiとkeyHash_lo nullifier = Poseidon(keyHash_hi, keyHash_lo, nonce)を計算する
証明が生成する公開シグナルはちょうど2値: nullifier と nonce だけです。鍵ビットもSHA-256ハッシュも、いかなる出力にも現れません。
ブラウザで動くコードはそのままこれです。
import { secretToBits } from "@lemmaoracle/seal";
import { create, prover } from "@lemmaoracle/sdk";
const witness = {
keyBits: secretToBits(key).map(Number),
nonce: challenge.nonce,
};
const client = create({});
const { proof, inputs } = await prover.prove(client, {
circuitId: "seal-identity-v1",
witness,
});証明が実際にサーバーに何を確信させるか。 生成された証明は3つの主張を同時に確立します。
知識の証明。 「
SHA-256(keyBits)が登録済みのhashになるような512bitのkeyBitsを知っている。」秘密入力は存在し、証明者が知っている。その実体は隠されています。計算の証明。 「
nullifier = Poseidon(SHA-256(keyBits)_hi, SHA-256(keyBits)_lo, nonce)を正しく実行した。」中間計算の連鎖――SHA-256、128bit分割、Poseidon――が正しく実行されたことを、公開出力だけで証明します。サーバーは中間値を一切見ずに計算の正当性を信頼できます。鮮度の証明。 「この証明はこのnonceに対して生成されたもので、転用できない。」
nonce * nonce制約がnonceを回路にbindします。以前のサインインでキャプチャした証明は別のチャレンジに対して提出できません。
まとめると、証明はこう述べています: 「私は登録済みidentityの秘密の保持者であり、このチャレンジに対して1回限りのnullifierを生成した。」 サーバーはどのidentityかを知ることはなく、それがレジストリに存在することだけを知ります。
3. 証明の送信。 証明と2つの公開シグナル(nullifier、nonce)をサーバーへ送ります。サーバーは証明の数学的な正当性を検証し、nullifierで身元を確認してセッションクッキーを発行します。
ネットワークログに残るのは、/api/auth/sealへのPOSTリクエスト1本。中身はproof、publicSignals(2値)、token。APIキーなし。keyHashなし。
アクセストークン、リフレッシュトークン、Passkeyとの比較
サーバーが保存するもの。 アクセストークン(Bearerトークンとも呼ばれる)方式ではサーバーが秘密そのもの(あるいは直接比較で検証できる形式)を保存します。リフレッシュトークン方式では長命なトークンをサーバー側で保持します。Passkey(WebAuthn)はクライアントデバイスに鍵を保存し、サーバーは公開鍵のみを登録します。Sealプルーフ認証もPasskeyと同様にクライアントに鍵を保持しますが、サーバーは公開鍵ではなく鍵のハッシュだけを登録します。いずれの方式でも、DB漏洩や鍵盗難のリスクはあります。ただしSealの場合、認証時にネットワークを渡るのはnullifierだけで、鍵そのものやハッシュさえも送信されません。
リプレイ耐性。 漏洩したアクセストークンは有効期限まで使えます。漏洩したSeal証明は無効です。nullifierはこのセッションのnonceにのみバインドされています。同じ鍵が異なる2つのセッションで証明を生成すると、まったく無関係な2つのnullifierが生まれます。相関を取る手がかりがありません。(Passkeyも同様に、チャレンジが異なるためリプレイ耐性があります。)
秘密の出処。 アクセストークン方式では秘密(トークン)をサーバーに送信します。PasskeyとSeal証明はどちらも秘密をデバイス/ブラウザから出しません。ただしPasskeyはハードウェア(セキュリティキーや生体認証)に依存するのに対し、Sealはソフトウェアキー(APIキー)に依存します。そのためSealはエージェント認証やM2M(システム間)認証にも適用できます。
より本質的な違い。 アクセストークン方式では信頼は秘密の所持によって確立されます。Passkeyは秘密鍵の所持です。Sealプルーフフローでは信頼はハッシュ原像の知識の証明によって確立されます――しかもそのハッシュ自体もPoseidonコミットメントの後ろに隠されています。秘密は証明回路の中に参加しますが出力には現れない。ハッシュはnullifier導出に参加しますが、こちらも出力には現れない。あなたの鍵もそのハッシュも見ることができないサーバーは、どちらも漏洩させることができません。
nullifierが保証すること
nullifierはこのセッションのnonceと鍵に固有の値です。同じ鍵でも、セッションが変わればnullifierは完全に別の値になります。サーバーはnullifierを検証するだけで身元を確認でき、鍵もそのハッシュも知る必要がありません。
サインイン1回あたり少し多く計算する代わりに、鍵のハッシュは回路の外には一切出なくなります。これがプライバシーと計算量のトレードオフです。
シンプルなユースケース
Lemma APIキーでユーザーが認証する開発者ツールを構築しているとします。バックエンドが生の鍵を受け取ったり保存したりせずにサインインをサポートしたい――さらに言えば、ハッシュさえも持ちたくない。ハッシュがあれば、セッションをまたいで同じユーザーを追跡できてしまうからです。
Sealプルーフ認証では、証明送信のステップまでサインインフローは完全にクライアントサイドで完結します。ユーザーがローカルで鍵を入力し、回路がセッションnonceにバインドされたnullifierを生成し、バックエンドは証明を検証してハッシュスキャンでアカウントを解決します。各セッションが異なるnullifierを生成するため、自社のログからでも同じ鍵によるサインインをまたいで相関づけることはできません。
AIエージェントがセッションに自らサインインする場合も同じ原則が使えます。エージェントが生の鍵を持ち、証明を生成する。オーケストレーター層はハッシュのみを保存し、nullifierのみを受け取ります。
Dashboardについて
Lemma Dashboardはdashboard.lemma.workers.devでプレビュー公開中です。Sealプルーフサインインはセカンダリパスとして実装されています。初回サインインはGitHub OAuthが必要で、アカウントの作成と最初のAPIキーの発行を行います。一度鍵を持てば、その後のサインインはSealプルーフで完結できます。上のスニペットはDashboardがブラウザで実行するコードのプレビューとして常時表示しているものと同一です。
サーバーが触れないもの
サーバーはproofと公開シグナル(nullifier、nonce)を受け取ります。証明の正当性を検証し、nullifierで身元を確認し、セッションを発行します。
この過程でサーバーがあなたのAPIキーもそのSHA-256ハッシュも参照、送受信、保存、照合することは一切ありません。鍵はブラウザ内で処理され、ハッシュは回路の中間値として扱われます。証明が身元を検証し、nullifierがセッションごとの一意性を担保します。
始め方
Proof-as-Auth は、より大きな Seal パターンのひとつの形です――秘密を一度も露出させずに、その所持を証明する。鍵レスのダッシュボードサインイン、エージェント/M2M 認証、セッションごとにリンク不能な識別は、いずれも同じ基盤の上に構築できます。
プラットフォームエンジニア、セキュリティ/ID 管理担当、エージェント基盤の開発者 の方に、3 つの出発点を用意しています。
- Dashboard で試す — Seal プルーフサインインは dashboard.lemma.workers.dev でプレビュー公開中です。
- 開発者ウェイトリストに登録 — tally.so/r/kd0bZR で SDK と回路への早期アクセス。
- ご相談 — プロダクトやエージェントの鍵レス認証について Discovery Call を予約ください。原則 1 営業日以内にご返信いたします。
Built for decisions that matter.
意思決定のために
つくられている。
Lemma を組織の信頼インフラに。