すべての記事

ファジーエクストラクターを用いた分散型アイデンティティのための生体情報による鍵導出

·
2025年10月21日
·
11 分で読めます
·
••• 回閲覧
·
••• いいね

英語の原文から翻訳した記事です。 英語で読む

ファジーエクストラクターを用いた DID のための生体情報バインド型の鍵導出

今日のアイデンティティシステムは、データを安全に保管してもらうという点で、企業に非常に大きな信頼を委ねています。複数の KYC プロバイダーがあなたのデータを無期限に保持しています。アプリは、あなたが 18 歳以上であることさえわかればいい場合でも、あなたの詳細な情報をすべて受け取ります。これは共有するには多すぎる情報で、しかもあちこちにバラバラに散らばったままになります。

自己主権型アイデンティティ SSI は、クレデンシャルの管理権をあなた自身に与えることでこれを解決します。必要なものだけを共有し、データを明かすことなく主張を証明できるのです。基本的に、検証可能なクレデンシャル VC を使えば、正確な生年月日を見せずに 18 歳以上であることを証明できます。しかし SSI には課題があります。アイデンティティを管理する秘密鍵を、いったいどこに保管するのでしょうか?

これにはさまざまな根本的な解決策があり、そのひとつが biometric crypto identities です。あまりメジャーな方法ではありませんが、問いはこうです。もしあなたの biometric、たとえば face が private-key だとしたら?失くす seed phrases も、忘れる passwords も、保管する keys もありません。あなたの biometric から wallet が生成され、そのウォレットが検証可能なクレデンシャルを保持し、あなたは ZKP を使ってそれを証明します。

これは fuzzy extractors + zero-knowledge proofs によって実現できます。その仕組みを見ていきましょう。

これが解決しうる本当の問題

従来の暗号システムは、再現可能なビット列を前提に設計されていました。しかし生体情報は、こうしたシステムが依存している 2 つの重要なルールを破ってしまいます:

鍵は完全に再現可能でなければならない - こうしたシステムは、ビット単位で完全に同じ鍵を再現することを求めます。トランザクションに署名するための秘密鍵は、毎回まったく同じでなければなりません。しかし生体情報はノイズが多く、考慮すべき変数がたくさんあります。今日の指紋スキャンは、センサーのばらつき、指の置き方、湿度、温度などによって、昨日のスキャンとビット単位で一致することはありません。挙げればきりがありません。顔スキャンや虹彩パターン、その他あらゆる生体計測も同じです。

鍵は一様ランダムでなければならない - 暗号の安全性は、鍵が鍵空間全体から一様ランダムに選ばれていることを前提としています。しかし生体データには構造やパターンがあります。虹彩は一様ランダムではなく、生物学的な制約や集団レベルでの類似性があり、エントロピーは特定の特徴に集中しています。

これはおなじみの「鶏が先か卵が先か」の問題を生み出します。生体情報をそのまま鍵として使うことはできませんし ノイズが多すぎる、安全に保管することもできませんし PII であり不変、ご存じのとおり単純にハッシュ化することもできません ノイズがハッシュ関数を壊す。では、どうすればいいのでしょうか?

さあ、ここからがおいしいところです!

ファジーエクストラクターとは?

ファジーエクストラクターは、ノイズの多いデータのための誤り訂正付き暗号だと考えてください。これは Gen 生成(generate) と Rep 再現(reproduce) という 2 つのアルゴリズムのペアで、とてもワクワクすることをやってのけます:

Gen(w)→(R,P)Gen(w) \rightarrow (R, P) - ノイズを含む生体入力 w を受け取り、一様ランダムな文字列 R 実際の暗号鍵 と、公開のヘルパー文字列 P を出力します。さらにもうひとつ面白い事実があります。P は安全性を損なうことなく公開できるのです。生体情報について意味のある情報が明かされることは一切ありません。

Rep(w′,P)→RRep(w', P) \rightarrow R - 異なる生体読み取り値 w' と同じ公開ヘルパー P を受け取り、w' が元の w に 十分近い 限り、まったく同じ R を再現します。ちょっと怖く聞こえるのはわかりますが、ここでいう 十分近い とは、ある距離空間における距離で測られます バイナリ文字列ならハミング距離、シーケンスなら編集距離、特徴ベクトルなら集合の差。

安全性の保証は情報理論的なものです。たとえ P が与えられても、生体情報が ≥m\geq m の最小エントロピーを持つ限り、抽出された鍵 R は一様分布に対して ε\varepsilon-close です。計算量的な困難性の仮定は一切必要ありません。無限の計算能力を持つ攻撃者であっても、ε\varepsilon より大きいアドバンテージで R を真にランダムな鍵と区別することはできません。

数学的にも美しい仕組みです。パラメータ (m,ℓ,t,ε)(m, \ell, t, \varepsilon) を持つファジーエクストラクターでは:

実際にどう動くのか

この構成では、2 つのプリミティブが連携して動きます:

セキュアスケッチ (SS,Rec)(SS, Rec)

セキュアスケッチは、ファジーなコミットメントのようなものです。SS(w)→sSS(w) \rightarrow s はスケッチ s を出力します。このスケッチは誤りを訂正するのにちょうど足りるだけの情報を明かしますが、w そのものを再構築できるほどの情報はまったく明かしません。そして Rec(w′,s)→wRec(w', s) \rightarrow w は、スケッチを使ってノイズを含む w' から元の w を復元します。

ハミング距離を使うバイナリ文字列の場合、シンドローム構成(Syndrome Construction)が最適です:

class SyndromeSketch:
    def __init__(self, n, k, t):
        self.n, self.k, self.t = n, k, t
        self.syn_matrix = generate_bch_syndrome_matrix(n, k)
 
    def sketch(self, w):
        return np.dot(self.syn_matrix, w) % 2
 
    def recover(self, w_prime, s):
        s_prime = np.dot(self.syn_matrix, w_prime) % 2
        error_syn = (s_prime - s) % 2
        e = bch_decode(error_syn, self.t)
        return (w_prime - e) % 2

ここで何が起きているのか:

さて、スケッチからエクストラクターへ

誤り訂正の後、復元した生体情報をハッシュ化して一様な鍵を抽出します。たとえば:

pub fn fuzzy_gen(biometric: &[u8]) -> (Vec<u8>, Vec<u8>) {
    let sketch = secure_sketch(biometric);
    let seed: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
    let key = extract_key(biometric, &seed);
    let helper = [sketch, seed].concat();
    (key, helper)
}
 
pub fn fuzzy_rep(noisy_biometric: &[u8], helper: &[u8]) -> Vec<u8> {
    let (sketch, seed) = split_helper(helper);
    let recovered = sketch_recover(noisy_biometric, sketch);
    extract_key(&recovered, &seed)
}

fuzzy_gen() 関数は:

fuzzy_rep() 関数は:

代表的な生体情報の場合:

注:生の生体表現には冗長性や相関が含まれています。虹彩コードでは、生のビット数(2048)と情報理論的なエントロピー(約 249 ビット)の間に約 8:1 の比率があります。鍵の抽出は、表現のサイズではなく、実際のエントロピーに従わなければなりません。

アイデンティティへの応用

SSI のトラストモデルには 3 つのアクターがいます:

全体の流れ: 生体情報 → ウォレット → クレデンシャル → ZK 証明 → 検証済みアイデンティティ

ステップごとに見ていきましょう:

生体情報からウォレットへ

async function biometricToWallet(rawBiometric) {
    const binary = await preprocessBiometric(rawBiometric);
    const { key, helper } = await fuzzyExtractor.gen(binary);
 
    const expandedKey = await hkdf(
        key,
        128,
        'bip39-seed',
        await crypto.getRandomValues(new Uint8Array(32))
    );
 
    const mnemonic = bip39.entropyToMnemonic(expandedKey);
    const seed = await bip39.mnemonicToSeed(mnemonic);
    const master = bip32.fromSeed(seed);
 
    return {
        btc: deriveBitcoin(master),
        eth: deriveEthereum(master),
        sol: deriveSolana(seed),
        helper,
        salt
    };
}

処理の流れ:

Solana の場合は特に、異なる曲線 を使います:

import { derivePath } from 'ed25519-hd-key';
 
const solSeed = derivePath("m/44'/501'/0'/0'", seed.toString('hex')).key;
const solKeypair = Keypair.fromSeed(solSeed);
const solAddress = solKeypair.publicKey.toBase58();

注:Solana は secp256k1 ではなく Ed25519 を使います。そのため実装では、SLIP-0010 に準拠するために ed25519-hd-key を使ってください Phantom、Solflare と一致します。

検証可能なクレデンシャルを取得する

これでウォレットは DID を持つようになりましたが、何かを証明するには検証可能なクレデンシャル VC が必要です。ここからトラストトライアングルが始まります。 Issuer はあなたの書類をオフチェーンで検証し、暗号学的に署名された証明を発行します:

async function requestCredential(wallet, issuerEndpoint, claimData) {
    const connectionRequest = await issuerEndpoint.createConnection(wallet.did);
    
    const credentialOffer = await issuerEndpoint.offerCredential({
        type: 'KYCCredential',
        schema: 'https://schema.org/kyc-v1',
        claims: {
            country: claimData.country,
            birthYear: claimData.birthYear,
            verified: true
        }
    });
    
    const signedVC = await wallet.acceptCredential(credentialOffer);
    await wallet.storeCredential(signedVC);
    
    return signedVC;
}

何が起きたのか:

クレデンシャルのスキーマは、VC に何が含まれるかを定義します:

{
  "@context": "https://www.w3.org/2018/credentials/v1",
  "type": ["VerifiableCredential", "KYCCredential"],
  "issuer": "did:polygonid:polygon:amoy:2qH7...",
  "issuanceDate": "2025-01-15T00:00:00Z",
  "credentialSubject": {
    "id": "did:polygonid:polygon:amoy:2qPz...",
    "birthYear": 1995,
    "country": "US",
    "kycVerified": true
  },
  "proof": {
    "type": "BJJSignature2021",
    "issuerData": {...},
    "signature": "0x..."
  }
}

これで、Verifier から 18 歳以上であることを証明して と求められたら、このクレデンシャルから ZKP を生成できます。

ゼロ知識証明レイヤー

ここでは、年齢、市民権、学位、住所、生年月日など の個人情報を、それらの機密データを明かすことなく証明できます。

async function proveAge(wallet, minAge) {
    const credential = await wallet.getCredential('kyc');
    const inputs = {
        birthYear: credential.birthYear,
        currentYear: 2025,
        minAge: minAge,
        signature: credential.signature
    };
 
    const { proof, publicSignals } = await snarkjs.groth16.fullProve(
        inputs,
        "ageCheck.wasm",
        "ageCheck.zkey"
    );
 
    return { proof, publicSignals };
}

証明される内容:

Circom の回路:

template AgeCheck() {
    signal input birthYear;
    signal input currentYear;
    signal input minAge;

    signal age <== currentYear - birthYear;

    component check = GreaterEqThan(16);
    check.in[0] <== age;
    check.in[1] <== minAge;
    
    signal output isValid;
    isValid <== check.out;
}

オンチェーンでの検証 - Solidity:

contract IdentityGate {
    mapping(address => mapping(uint256 => bool)) public verified;
 
    function submitProof(
        uint256 requestId,
        uint[2] calldata pA,
        uint[2][2] calldata pB,
        uint[2] calldata pC,
        uint[3] calldata pubSignals
    ) external {
        require(zkpVerifier.verifyProof(pA, pB, pC, pubSignals), "Invalid proof");
        require(pubSignals[2] >= minAge, "Age requirement not met");
 
        verified[msg.sender][requestId] = true;
        _grantAccess(msg.sender);
    }
}

このコントラクトは:

この流れから、あなたの生体情報が次のものになったことがわかるでしょう:

そしてこれは、複数の別々のシステムではありません。たったひとつの統合されたフローなのです。ウォレットの生成は登録時に一度だけ行われます。ZKP のフローは、本人確認が必要なときにいつでも使えます。こう考えてみてください。生体ウォレットは、通常のトランザクションでは他の暗号資産ウォレットとまったく同じように使えます。しかし dApp が KYC や何らかの PII ベースの検証 Polygon ID のユースケースのようなもの を必要とするときには、ZKP レイヤーが動き出し、データを公開することなくクレデンシャルを証明します。

考えられるユースケース

  1. エージェント用パスポート:

すべての AI エージェントが、生体情報から導出された DID を持てます。独自のウォレットと検証済みのクレデンシャルを持つ AI トレーディングエージェントを想像してみてください:

class TradingAgent {
    constructor(agentFingerprint) {
        this.wallet = await biometricToWallet(agentFingerprint);
        this.did = await createDID(this.wallet);
    }
 
    async trade(dex, params) {
        const proof = await this.proveReputation();
        return await dex.trade(params, proof);
    }
}

このエージェントは:

SingularityNET はまさにこれを構築しています。AI Agent Trust Registry と呼ばれるもので、エージェントは検証可能なクレデンシャルを使って、真正性、安全性、監査ステータスを証明します。biometric-wallet-ZKP のフローによって、エージェントは説明責任を保ちながら自律的に動作できるようになります。

  1. dApp のアイデンティティレイヤー:

ある DeFi プロトコルは KYC を必要としていますが、ユーザーデータは保存したくありません 規制上の責任 + ハッカーにとってのハニーポット:

contract DeFiProtocol {
    modifier onlyKYCVerified() {
        require(identityGate.verified(msg.sender, KYC_ID), "KYC required");
        _;
    }
 
    function borrowUndercollateralized(uint256 amount) external onlyKYCVerified {
        _issueLoan(msg.sender, amount);
    }
}

プロトコルがあなたのパスポートや名前、住所を目にすることは一切ありません。コンプライアンスを満たしつつ、プライバシーも守られます。

同じパターンは次のような用途にも使えます:

  1. Web2 のパスワードレスログイン
async function login(website) {
    const { key } = await fuzzyExtractor.rep(biometric, helper);
    const siteKey = deriveKey(key, website.domain);
    const signature = sign(challenge, siteKey);
    await website.verify(signature);
}

サイトごとに固有の鍵が導出されます。パスワードは送信されず、フィッシングされるクレデンシャルもありません。

  1. 生体認証による支払い:

あなたの生体情報がトランザクションを承認します:

async function pay(merchant, amount) {
    const wallet = await biometricToWallet(scan);
 
    if (amount > 1000) {
        await merchant.verifyProof(generateKYCProof(wallet));
    }
 
    await wallet.sendTransaction({ to: merchant.address, value: amount });
}

BMONI + BKey は現在まさにこれを実装しています。あなたのウォレットはすべて生体情報「あなたの顔」から完全に導出され、特定の EVM チェーンやトークンにまたがってウォレットがプロビジョニングされます。すべて顔だけで!

  1. 詐欺防止データベース:
contract AML {
    bytes32 public scamMerkleRoot;
 
    function checkClean(address wallet, bytes32[] calldata proof) public view returns (bool) {
        bytes32 hash = keccak256(abi.encodePacked(wallet));
        return !MerkleProof.verify(proof, scamMerkleRoot, hash);
    }
}

dApp はアクセスを許可する前に問い合わせます。既知の詐欺師はネットワーク全体でブロックされ、不審な活動の履歴があるものも記録されます。一方で、インテグレーターは過去の不審な活動について通知を受け取ります。

開発者向けのインテグレーション

生体アイデンティティを使って構築するには、インフラが必要です。エコシステムは 3 つのコアコンポーネントを提供しています:

Issuer Node

組織はセルフホストの Issuer Node を運用して、クレデンシャルを発行します:

docker-compose up -d
make import-private-key-to-kms private_key=<eth-key>
 
# access the ui at localhost:8088
# access the api at localhost:3001

機能:

API の例:

const issuerAPI = new IssuerNode({
    url: 'https://issuer.example.com',
    auth: { user: 'admin', password: 'secure' }
});
 
await issuerAPI.createIdentity({
    blockchain: 'polygon',
    network: 'amoy',
    method: 'polygonid'
});
 
await issuerAPI.issueCredential({
    did: 'did:polygonid:...',
    schema: 'KYCCredential',
    claims: { ageOver: 18 }
});

ツールのエコシステム

技術的な課題と解決策

1: 生体情報のばらつき

指紋はスキャンごとに 10-15% 変動します。解決策:BCH 符号はビットレベルの誤り訂正に最適です。2048 ビットの虹彩コード 実際のエントロピーは約 249 ビット で、誤り率が 10%(約 205 ビットの誤り)の場合:

重要な違い:2048 ビットというのは、生の虹彩コード表現 Daugman のアルゴリズムの出力 です。実際の情報理論的なエントロピーは、生物学的な相関と冗長性 約 8:1 の比率 のために、わずか約 249 ビットしかありません。

2: テンプレートの経年変化

問題:顔は時間とともに変化します 加齢、体重、ひげなど

解決策:鍵の移行を伴う定期的な再登録:

async function migrate(oldBio, newBio) {
    const oldWallet = await recover(oldBio);
    const { newWallet, newHelper } = await enroll(newBio);
    await oldWallet.transferAll(newWallet.address);
    await storage.update(newHelper);
}

顔なら 2-3 年ごと、虹彩なら 5 年ごと、声なら毎年といった具合に再登録します。

3: ライブネス検知

問題:あなたの写真を持っている攻撃者が、あなたのウォレットにアクセスできてはなりません。

解決策:生体情報の取得時にライブネスチェックを行う:

const checks = await Promise.all([
    depthSensor.measure(),
    motionDetector.verify(['blink', 'nod']),
    thermalCamera.capture(),
    videoAnalyzer.checkTemporal()
]);

WorldCoin の Orb は、虹彩スキャンでこれを行っています。25 以上のライブネスチェック、3D イメージング、赤外線 + RGB カメラです。驚くほどうまく機能していて、他人受入率は <0.0001%< 0.0001\% です。

4: ヘルパー文字列のプライバシー

先ほど説明した P は生体情報を漏らすのでしょうか?いいえ。定理 Dodis ら によれば、攻撃者が知りうるのは最大でも n−kn-k ビット スケッチの長さ で、鍵 R は統計的にランダムなままです。私たちの虹彩の例では、408 ビットが知られ、392 ビットのエントロピーが残ります。

本番投入できるのか?

必要なピースはすでに揃っています:

足りないものは?インテグレーションです...

本番実装の例

本番環境で実際にこのシステムを使うと、こんな感じになります

async function enrollUser() {
    const bio = await captureLive();
    const { key, helper } = await fuzzyExtractor.gen(bio);
 
    const salt = await crypto.getRandomValues(new Uint8Array(32));
    const expandedKey = await hkdf('sha256', key, salt, 'bip39-seed', 128/8);
    const wallet = await deriveWallet(expandedKey);
 
    const did = await createDID(wallet.eth);
    const kycCred = await requestKYC(did, { passport, selfie });
 
    await storage.save({ helper, salt, did, credentials: [kycCred] });
    return { wallet, did };
}
 
async function accessDApp(dappURL) {
    const bio = await captureLive();
    const { helper, salt } = await storage.getHelperData();
    const key = await fuzzyExtractor.rep(bio, helper);
 
    const expandedKey = await hkdf('sha256', key, salt, 'bip39-seed', 128/8);
    const wallet = await deriveWallet(expandedKey);
 
    const proof = await generateProof(wallet, dappURL.request);
    await submitProof(wallet, proof, dappURL.verifier);
 
    await dappURL.grantAccess(wallet.address);
}
 
async function sendPayment(recipient, amount) {
    const bio = await captureLive();
    const { helper, salt } = await storage.getHelperData();
    const key = await fuzzyExtractor.rep(bio, helper);
 
    const expandedKey = await hkdf('sha256', key, salt, 'bip39-seed', 128/8);
    const wallet = await deriveWallet(expandedKey);
 
    await wallet.send({ to: recipient, value: amount });
}

通常のウォレットのフローとアイデンティティのフローは共存します。ZKP が登場するのは検証が必要なときだけで、KYC のためか、年齢、市民権、学位、住所、生年月日などの PII ベースの検証のためです。

あなたの生体情報があなたの鍵であり、あなたのウォレットはサポートされているすべてのチェーンにまたがるあなたのアイデンティティです。シードフレーズや秘密鍵を失くす心配をする必要は一切ありません。すべてはあなたの身体的特徴に結びついていて、BMoni の実装ではそれはあなたの顔なのです!

必読リンク

•••
この記事をシェア:

最新情報を受け取る

最新の記事をメールでお届けします。