【入門編】 オンチェーンKYC/AMLの実装とプライバシー保護のトレードオフ – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!Web3やIoTのセキュリティの世界へようこそ。
ブロックチェーンって、「すべての取引が世界中に公開されていて、誰が何をしたか丸見えなんでしょ?」というイメージを持っていませんか?

実はその通り。パブリックブロックチェーンは、ウォレットアドレスの履歴がすべてオープンになっています。ここで、現実社会の法律やルール、つまり「KYC(本人確認)」や「AML(マネーロンダリング防止)」とどう向き合うかが、今のWeb3開発における最大にして最高に面白いパズルなんです。

今回は、「ゼロ知識証明(ZKP)」という魔法のような技術を使って、「私は怪しい人間ではありませんよ」と証明しつつ、あなたのプライバシーもしっかり守る最先端の仕組みについて、身近な防犯にたとえて一歩ずつ優しく紐解いていきましょう!

—

1. 家の鍵と「秘密の合言葉」で例えるプライバシーのジレンマ

想像してみてください。あなたは今、会員制のとてもラグジュアリーなクラブに入ろうとしています。

クラブの入り口には、いかついガードマンが立っていてこう言いました。
「お客さん、入会するには『18歳以上であること』と『犯罪歴がないこと』を証明してください。パスポートと住民票、そして銀行の残高証明をここに置いていってくださいね」

…ちょっと待ってください!これでは、あなたのプライベートな情報がガードマンや、そこに集まる野次馬に丸見えになってしまいますよね。これが、従来のオンチェーンKYCが抱えている「プライバシーの丸裸問題」です。

では、次のような方法だったらどうでしょう?
あなたがポケットから小さな紙を取り出し、ガードマンに見せます。その紙には、こう書かれています。

> 「この証明書の持ち主は、年齢と素性の審査をクリアした実在の人間であり、犯罪歴はありません(署名:警察庁)」

ガードマンは、その紙に書かれた暗号のスタンプ(デジタル署名)を確認して、「よし、通って良し!」と言いました。ガードマンは、あなたの「年齢」も「本名」も「住所」も一切知りません。知っているのは、「この人はルールをクリアしている」という事実だけです。

この「中身を見せずに、正しい事実だけを証明する技術」こそが、今回お話しするゼロ知識証明(Zero-Knowledge Proof: ZKP)なんです。

—

2. 攻撃者はどこを狙う?(リアルな脅威)

「じゃあ、ゼロ知識証明を使えば完璧だね!」と安心するのはまだ早いです。攻撃者(泥棒たち)は、この新しい仕組みの「スキ」を虎視眈々と狙っています。

現実の防犯でも、いくら頑丈なスマートロックを玄関につけても、窓の鍵が空いていたり、合言葉をメモした付箋をポストに貼っていたら意味がありませんよね。
オンチェーンのKYCシステムでも、以下のような脆弱性が狙われます。

  • 証明書の使い回し(リプレイ攻撃):Aさんが取得した「私は適格者です」という証明のデータを、悪意あるBさんが盗み見して、自分のものとして偽って送信してしまう。
  • メタデータの漏洩:証明の「中身」は隠れていても、「いつ、どのウォレットからその証明を検証したか」というタイミングのデータ(メタデータ)から、個人の足取りが特定されてしまう。

これらを防ぐために、私たちはスマートコントラクト側でしっかりと「罠」を仕掛け、セキュリティを高める必要があります。

—

3. 実装してみよう:SolidityでのZKP検証コントラクト

それでは、実際に開発現場で使われているスマートコントラクトのコードを見ていきましょう。
今回は、Circomなどで生成されたゼロ知識証明の「証拠(Proof)」を、Ethereumなどのブロックチェーン上で検証するシンプルなSolidityのコード例です。

初心者の方でも迷わないように、日本語でたっぷりコメントを入れていますよ。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title プライバシーを守るオンチェーンKYC検証コントラクト
 * @notice ユーザーの個人情報を明かさずに、適格者であることだけを検証します
 */
interface IVerifier {
    // ゼロ知識証明の数学的な正当性をチェックする外部コントラクトの関数
    function verifyProof(
        uint[2] calldata a,
        uint[2][2] calldata b,
        uint[2] calldata c,
        uint[1] calldata input
    ) external view returns (bool);
}

contract PrivateKYCGateway {
    
    // 外部のZKP検証コントラクト(警察のスタンプを押す機関のようなもの)へのアドレス
    IVerifier public immutable zkpVerifier;

    // すでに利用された証明のハッシュを保存するマップ(リプレイ攻撃を防ぐための「使用済みスタンプ置き場」)
    mapping(bytes32 => bool) public usedNullifiers;

    // KYCをクリアしたウォレットアドレスを記録するマップ
    mapping(address => bool) public isVerifiedUser;

    // イベント定義(フロントエンドに結果を伝えるため)
    event UserVerified(address indexed user);

    /**
     * @notice デプロイ時にZKP検証コントラクトのアドレスをセットします
     * @param _verifierAddress 信頼できる検証コントラクトのアドレス
     */
    constructor(address _verifierAddress) {
        zkpVerifier = IVerifier(_verifierAddress);
    }

    /**
     * @notice ゼロ知識証明を使ってKYCを完了させる関数
     * @param a ZKPプルーフのパラメータA
     * @param b ZKPプルーフのパラメータB
     * @param c ZKPプルーフのパラメータC
     * @param input パブリック入力(今回は「この証明が一度きりのものであるか」を示す識別子を含みます)
     */
    function verifyAndRegister(
        uint[2] calldata a,
        uint[2][2] calldata b,
        uint[2] calldata c,
        uint[1] calldata input
    ) external {
        
        // 1. リプレイ攻撃対策:パブリック入力から一意の識別子(Nullifier)をハッシュ化して特定する
        bytes32 proofHash = keccak256(abi.encodePacked(input));
        
        // この証明がすでに使われていないかチェック(二重使用の防止)
        require(!usedNullifiers[proofHash], "Error: この証明書はすでに使用されています(リプレイ攻撃の検知)");

        // 2. ゼロ知識証明の数学的検証を実行
        bool isValid = zkpVerifier.verifyProof(a, b, c, input);
        require(isValid, "Error: ゼロ知識証明の検証に失敗しました。正しい証明ではありません。");

        // 3. 証明が本物と分かったので、使用済みとしてマークする
        usedNullifiers[proofHash] = true;

        // 4. ユーザーのプライベートな実名は一切保存せず、「適格である」というフラグだけを立てる!
        isVerifiedUser[msg.sender] = true;

        // 検証成功のイベントを発行
        emit UserVerified(msg.sender);
    }

    /**
     * @notice 特定のユーザーがKYCを通過しているか確認する関数
     */
    function checkAccess(address _user) external view returns (bool) {
        return isVerifiedUser[_user];
    }
}

コードのポイント解説

  • usedNullifiers によるリプレイ攻撃対策:家の鍵のコピーを作られて何度も侵入されないように、「1回使った鍵は二度と使えない仕組み(使い捨てチケット)」を実装しています。
  • プライバシーの死守:isVerifiedUser[msg.sender] = true; の部分を見てもらうと分かりますが、データベースに保存されているのは「このアドレスはOK」というフラグだけです。氏名や生年月日はブロックチェーンのどこにも書き込まれません。

—

4. フロントエンド(JavaScript)からの安全な呼び出し方

スマートコントラクトの準備ができたら、次はWebアプリ側からこの仕組みを呼び出してみましょう。ここでは、Ethers.jsを使った実装の雰囲気を少しだけ覗いてみます。

import { ethers } from "ethers";

/**
 * @notice ユーザーがZKPを使ってオンチェーンKYCを完了させる関数
 * @param {Object} zkProof ゼロ知識証明のデータ(zk-SNARKs等の出力結果)
 * @param {string} contractAddress デプロイされたコントラクトのアドレス
 * @param {Object} signer 接続中のウォレット(Metamaskなど)のsigner
 */
async function executeOnChainKYC(zkProof, contractAddress, signer) {
    try {
        // コントラクトのインスタンスを作成
        const abi = [
            "function verifyAndRegister(uint[2] a, uint[2][2] b, uint[2] c, uint[1] input) external"
        ];
        const kycContract = new ethers.Contract(contractAddress, abi, signer);

        console.log("ゼロ知識証明の検証をブロックチェーンに送信中...");

        // コントラクトの関数を呼び出し(ここでトランザクションが発生します)
        const tx = await kycContract.verifyAndRegister(
            zkProof.a,
            zkProof.b,
            zkProof.c,
            zkProof.input
        );

        // トランザクションがブロックに組み込まれるのを待つ
        console.log("トランザクション送信完了。承認を待っています...", tx.hash);
        const receipt = await tx.wait();

        console.log("おめでとうございます!プライバシーを守りながらKYCが完了しました!", receipt);
        
    } catch (error) {
        console.error("KYCの検証中にエラーが発生しました:", error.reason || error.message);
        // 実務では、エラーメッセージをユーザーに分かりやすく翻訳して表示してあげ親切にしましょう!
    }
}

—

5. まとめと次のステップ

今回は、オンチェーンKYC/AMLとプライバシー保護のトレードオフについて、ゼロ知識証明(ZKP)を用いたアプローチを解説しました。

  • プライバシーと規制のバランス:生データを明かさずに「ルールをクリアしている事実」だけを証明する。
  • 攻撃への備え:リプレイ攻撃(証明の使い回し)を防ぐために、Nullifier(使い捨て識別子)の仕組みを必ず導入する。

セキュリティやWeb3の開発は、最初は難しく感じるかもしれませんが、一つひとつの技術を「身の回りの防犯」に置き換えて考えると、本質がすっきりと見えてきます。

一歩ずつ、焦らずに確実な実装スキルを身につけていきましょう!
それでは、また次回のセキュリティ解説でお会いしましょう!

コメント

タイトルとURLをコピーしました