PR

PQCのSSL/TLS+αになるプロトコルに対応させたブラウザとサーバーゲートウェイとCAサーバーのPoCを作ってみた

Rintaが書いた
[自動生成]無理やり短縮リンク: 短縮URLを生成中...
スポンサーリンク

背景

こんにちは。
題名にある通りのことを書いていきます。

アドベントカレンダーに書くためにこののようなページを書いたのですが、あまりにも適当&何が伝えたいのかわからなかった&しょうもなな過ぎたので再度別の話題を作って書いてみたという感じです。

さて、まずPQCって知っていますでしょうか?
PQCとは、Post-Quantum Cryptographyの略で直訳すると耐量子暗号となります。

これは、もし量子コンピュータが実用化された場合RSAECCが死ぬと言われている中で、対量子コンピュータの暗号搭載したやつを考えて作ろうぜっていうやつでございます。

「量子コンピュータなんてまだ先の話でしょ?」と思われるかもしれません。 ですが、セキュリティ業界には「Harvest Now, Decrypt Later(今収集し、後で解読する)」というマインドも存在します。1学生として、なんか気にするだけでも0→1になるんでいい感じになるのではないでしょうか(?)。

その中で、ブラウザとか作ってみたいなーと思っていたのでついでに作ろうと思った感じです。
予定ではテスト終わりから(11/24~)「ブラウザ、サーバーソフト、CAサーバー、VPN、ブロックチェーンAlt coin」を作成する予定だったのですが、アドベントカレンダーの締切(12月中)に終わりそうにないのでとりあえずブラウザとサーバーゲートウェイとCAのPoCだけでもいいやって感じになって今日(12/5)に一段落にさせたって感じです。

ちなみに、このレベルのことはすでにやられているので全く新しい試みってわけではありませんが教養としての作成として行いました。

途中まで「PQC」をQVCに引っ張られて「QPC」と誤解してたので今も混ざるかもしれません。ご了承ください。

また、数学が苦手なため暗号の仕組み等はよく理解しておりません。

この記事を書くのに当たって、コードベースから例としてコードを抽出したりするのにAIを使用しました。

何を作ったか

AZPは、 Aether-Zero Protocol の略です。かっこいいですね。
固有名詞としてPhantom-X Ultraという名前をつけました。もっとかっこいいですね。

そして、前述の通りこれは3つのものから成り立っており、

  1. Aegis-Web (ブラウザ):Chromiumベースのブラウザ。
  2. Q-Gateway (サーバーの門番):rust製の逆プロキシサーバーで、通信の入り口ですべての暗号化処理を担います。
  3. Constellation PKI (信頼の星空): CA証明書を発行できます。ただ、完全に手動でつかわなくてはいけないのでCAサーバーみたいな大層なものでは一切ありません。

ちなみに、色々と「考える→geminiによる評価→Claudeによる評価→考える」を繰り返して9回目でひとまずできて、その後作成中にホールがあったら随時修正したって感じです。

ハイブリッド暗号と「幻影チケット」←かっこいい!

さて、ここから少し技術的なお話をしましょう。今回実装したバージョン9.0では、主に2つの大きな武器を搭載しました。

1. ハイブリッド暗号方式 (Kyber + X25519)

まず、暗号の心臓部には、NIST(米国国立標準技術研究所)が推奨する格子暗号「Kyber-1024」を採用しました。これが対量子コンピュータ用の盾です。 しかし、新しい暗号というのは未知の脆弱性が見つかるリスクもあります。そこで、従来の楕円曲線暗号である「X25519」も併用する「ハイブリッド方式」をとりました。 これなら、もし万が一Kyberが破られても、従来レベルの安全性は担保されるというわけです。

日本語訳

たとえば、妹を想像してみてください。
妹はまだ小学生で、最新のセキュリティボックス(たとえば、指紋認証と顔認識が組み合わさった新しい鍵付きの宝箱)が大好きなんです。これがKyber-1024の役割で、格子暗号(Lattice-based cryptography)という数学の迷宮みたいな仕組みを使って、量子コンピュータのスーパーパワー攻撃からも守ってくれるんです。
格子暗号とは、簡単に言うと、無限に広がる格子状の点々の中で、正しい道だけを探すパズルみたいなもの。
量子コンピュータがいくら速くても、道を探しきれないよう設計されているんですよ
でも、妹は時々「新しいガジェット、壊れちゃうかも」と心配して、古いけど頑丈な南京錠(これがX25519の楕円曲線暗号(ECDH))を併用します。
楕円曲線暗号は、曲線上の点の秘密の関係性を鍵に使った、従来のコンピュータでは解きにくい古典的な盾で、Curve25519という有名な曲線を基にしたX25519は、特に鍵交換(Key Exchange)に特化していて、速くて安全です。
※ただしECCは量子コンピューター耐性が低いとされています。因みにECDHはECCというカテゴリの暗号

ハイブリッド方式で両方を組み合わせるのは、妹が宝箱に新しい鍵と古い錠をダブルでかけるようなもの。新しいのが万一ハッキングされても、古い方がバックアップで守ってくれるんです。これで、妹の大事な食べ物(チョコミントアイス)は、どんな脅威からも安心なんですよ。

2. Phantom Ticket(ファントム・チケット)

一番苦労したのがここです。Kyberのような新しい暗号は、鍵サイズが大きく、通信が遅くなりがちです。そこで、「Phantom Ticket」という仕組みを考案しました。

これは、一度接続したサーバーとの間で「再接続用のチケット」を発行する仕組みなのですが、ただのチケットではありません。 クライアント側で、サーバーの「エポック鍵(Epoch Key)」を使ってカプセル化(Encapsulation)し、AES-GCMで暗号化して送りつけるのです。

これによって、0-RTT(ゼロ・ラウンドトリップタイム)、つまり「挨拶抜きでいきなりデータを送りつける」爆速通信を実現しつつ、その最初の1パケット目から耐量子セキュリティで守ることに成功しました。

日本語訳

妹をまたまた想像してみてください。
妹は学校の友達とオンラインゲームで遊ぶのが大好きなんですが、新しい暗号のKyberみたいに「鍵がデカくて重い」せいで、接続が遅くてイライラしちゃうんですよ。
それと同じで最新のセキュリティボックスはカッコいいけど、荷物みたいに重たくて、毎回持ち運ぶのが面倒くさいんです。

そこで、妹が考えたのが「Phantom Ticket」。一度友達の家(サーバー)に遊びに行って、帰りに「次はすぐ入ってね」って特別なチケットをもらうんです。再入場のブラックライトのスタンプみたいなやつですね。

でも、このチケットはただの紙切れじゃなくて、妹が友達のエポック鍵(短い時間だけ有効な合言葉。2FA的な)を使って、秘密の箱に入れて(カプセル化)、AES-GCMっていう超頑丈な金庫(対称鍵暗号の一種。暗号化しつつ改ざん検知もできる速くて安全なやつ)でガッチリ鍵をかけて持ち帰るんです。

これで、次に友達の家に行くと、妹は「挨拶(ハンドシェイク)なしでドカン!」って感じで、0-RTT(Zero Round-Trip Time、往復の手間なく一発でデータ飛ばせる爆速モード)でゲームのデータ送りつけちゃうんですよ。
普通の接続だと「こんにちは?」「あ、こんにちは!」ってやり取りで時間がかかるけど、これなら最初の1パケット目から、Kyberの耐量子セキュリティで守られてるから、量子コンピュータの攻撃も防げるんですよ。

雑談。

当初の設計では、サーバーの認証にDilithiumという耐量子署名を使う予定でした。
しかし、これには欠点がありました。署名データがデカすぎるのです(約数KBになることも)。 これではTCPのパケットが分割(フラグメンテーション)され、通信遅延が起きてしまいます。Webブラウジングにおいて遅延=罪です。

そこで、バージョン9では「Implicit Authentication(暗黙的認証)」という荒技を採用しました。 「サーバーがクライアントの送った暗号化チケットを復号できたなら、それは本物のサーバーである」という理屈です。これにより、署名データを丸ごと削ぎ落とし、ハンドシェイクを極限まで軽量化しました。

まあ、中間者攻撃に対してザコくなるのですがそれ以上思考するのは私には無理でした。
署名なんて数学の塊ですから。
そこでConstellation PKIだったり後記のAZP-Routeヘッダーの署名、キーの短期間での回転でなんとなくカバーしているつもりです。

実際に実装する(重複表現)

プロトコルが決まれば、次はサーバーの実装です。 言語はRustにしました。個人的に最近tauriにハマってるのと、安全性と速度、そして並行処理に強いからです。実装したサーバーの名前は「Q-Gateway」。Nginxなどの既存Webサーバーの前に立ち、AZP通信を処理するリバースプロキシとして動作します。

命を削る「Epoch Key Rotation」

サーバー実装で最もこだわったのが、「Epoch(エポック)」という概念です。 もしサーバーの秘密鍵が漏れたら、過去の通信も未来の通信もすべて解読されてしまいます。これを防ぐため、Q-Gatewayは15分ごとに鍵を捨てます。

src/azp/epoch.rsに実装されたEpochManagerは、常に以下の2つの鍵を持っています。

  • Current Epoch (N): 今使っているKyber-1024の鍵ペア。
  • Previous Epoch (N-1): 15分前に使っていた鍵ペア。

なぜ古い鍵を持つのか?
それは、通信中に「15分の境界」をまたいでしまったクライアントや、時計が少しズレているクライアントを救済するためです。この仕組みにより、ユーザーは鍵の切り替わりを意識することなく通信を続けられます。

リプレイ攻撃を弾く “Strike List”

0-RTT(ゼロ・ラウンドトリップタイム)通信、つまり「挨拶と同時にデータを送る」仕組みには、リプレイ攻撃(攻撃者が同じデータを再送信する攻撃)のリスクがあります。 これを防ぐために実装したのが、src/azp/replay.rsにあるStrikeListです。

ここにはRustの高性能並行ハッシュマップDashMapを使用しました。 一度使われたチケットのIDを記録し、30分間(チケットの有効期限)は絶対に再利用させません。数万件のアクセスがあってもロックフリーで高速に判定できる、鉄壁の守りです。

ブラウザ編 〜Chromiumという魔境への潜入〜

ここからが本当の地獄でした。Google ChromeのベースであるChromiumのソースコードを改造し、「Aegis-Web」を作り上げる作業です。
Flootとか言うブラウザが一時期はやった際に、firefoxベースなので使わなかったのですが、いつかブラウザ作ってみたいと思ってたので。

ネットワークスタックへの介入

Chromiumの通信処理は、net/というディレクトリに集約されています。
私は net/ssl/ssl_client_socket_impl.ccという、SSL通信の核心部分にメスを入れました。

通常のTLSハンドシェイクが始まるDoHandshake()という関数の直前に、自作のフックを仕込みます。 「おい、接続先はAZP対応か?」 もし対応していれば、通常のTLS手順をねじ曲げ、「Phantom Ticket」を生成して注入します。

幻影のチケット “Phantom Ticket”

これは通常のTLSセッションチケットとは異なり、このチケットはクライアント側で生成されます。

azp_key_manager.ccの中で行われている処理は以下の通りです。

  1. カプセル化 (Kyber-1024): まず、ブラウザはサーバーのEpoch公開鍵を使って、共通の秘密(Shared Secret)を生成し、それをカプセル化(Encapsulation)します。
  2. 暗号化 (AES-256-GCM): 生成した秘密を使って、チケットの中身(クライアントIDなど)をAES-GCMで暗号化します。
  3. 送信: カプセル化された鍵(Ciphertext)と、暗号化されたチケットをセットにして、ClientHelloメッセージのpre_shared_key拡張領域に詰め込んで送信します。

これにより、最初の1パケット目から耐量子セキュリティで保護されたデータが飛んでいくわけです。

信頼の基点 “Constellation PKI”

ブラウザがサーバーの鍵をどうやって知るのか?
そのために、「AZP-Route」というHTTPヘッダを設計しました。 サーバーはレスポンスにこのヘッダを含め、自分の身分証明書(署名付きバイナリblob)を送りつけます。

ブラウザ側のAzpRouteVerifierazp_route_verifier.cc)は、このヘッダを受け取ると、ソースコードにハードコードされた**「Genesis Root(創世記ルート鍵)」**(azp_genesis.h)まで遡って署名を検証します。 検証に成功して初めて、そのサーバーの鍵をローカルのSQLiteデータベース(gatewaysテーブル)に保存します。これが「Secure Learning(安全な学習)」です。

おまけ

おまけにサイドバーを追加しときました。vivaldiを使ってからサイドバーがないと使いづらいと感じる体質になったので。

具体例

Q-Gateway (Rust) 実装詳細

Q-Gatewayは、非同期ランタイム `tokio` 上で動作する高性能リバースプロキシです。

Epoch Key Rotation (鍵ローテーション)

15分ごとにローテーションされるEpoch Keyです。これには `Kyber-1024` が使用されます。

/// Epoch Key Pair (Kyber-1024)
#[derive(Clone)]
pub struct EpochKey {
    /// Kyber-1024 Public Key (クライアントがチケット鍵をカプセル化するために使用)
    pub public_key: [u8; KYBER_PUBLICKEYBYTES],
    /// Kyber-1024 Secret Key (サーバーがチケット鍵をデカプセル化するために使用)
    pub secret_key: [u8; KYBER_SECRETKEYBYTES],
    /// Epoch number (インクリメントされるカウンタ)
    pub epoch_number: u64,
    // ...
}

impl EpochKey {
    /// 新しいEpoch Keyを生成 (Kyber-1024)
    pub fn generate(epoch_number: u64) -> Self {
        let mut rng = rand::thread_rng();
        // pqc_kyberクレートを使用して鍵ペアを生成
        let keys = keypair(&mut rng).expect("Failed to generate Kyber keys");
        
        // ... (タイムスタンプ設定など)
        
        Self {
            public_key: keys.public,
            secret_key: keys.secret,
            epoch_number,
            // ...
        }
    }
}

ハンドシェイクステートマシン

TLS 1.3のハンドシェイクをフックし、AZP独自の処理(チケット復号とKEM)を行います。

    /// クライアントからのClientHelloを処理
    pub fn process_client_hello(&mut self, data: &[u8]) -> Result<bool, HandshakeError> {
        // ... (ClientHelloのパース) ...
        
        // 1. チケットの復号 (Decapsulation + Decryption)
        // EpochManagerを使用して、カプセル化された鍵を復元し、チケット本体を復号する
        let (plaintext, epoch_num) = self.epoch_manager
            .try_decrypt_ticket(
                encrypted_ticket.epoch_hint,
                &encrypted_ticket.ciphertext,
                &encrypted_ticket.nonce,
                &encrypted_ticket.encapsulated_key, // Kyber Ciphertext
            )
            .ok_or(HandshakeError::TicketDecryptionFailed)?;
        
        // 2. チケットの検証
        let ticket = PhantomTicket::from_bytes(&plaintext)?;
        if !ticket.is_valid() {
            return Ok(false); // フォールバック
        }

        // 3. PSK Binderの検証 (改ざん検知)
        if !PskBinder::verify(&hello.client_random, &ticket.temp_secret, ...) {
            return Err(HandshakeError::BinderMismatch);
        }
        
        // ...
        Ok(true)
    }

    /// KEMの実行とServerHelloの生成
    pub fn perform_kem(&mut self) -> Result<ServerHelloResponse, HandshakeError> {
        // ...
        
        // Kyber-1024 Encapsulation (サーバー -> クライアント)
        // クライアントの公開鍵に対して共有シークレットをカプセル化
        let (kyber_ct, kyber_ss) = encapsulate(&client_pk, &mut rng)?;
        
        // X25519 ECDH (前方秘匿性の強化)
        let x25519_ss = x25519_secret.diffie_hellman(&client_x25519_pub);
        
        // マスターシークレットの導出 (Hybrid)
        let master_secret = derive_master_secret(
            &hello.client_random,
            &ticket.temp_secret,
            &kyber_ss,
            &x25519_ss.as_bytes(),
        );
        
        // ...
    }

Aegis-Web (Chromium) 実装詳細

Phantom Ticket の生成と注入

TLS接続開始時に割り込み、AZP対応ドメインであればPhantom Ticketを生成して注入します。

std::optional<std::vector<uint8_t>> AzpSslIntegration::PrepareClientHelloExtension(
    const std::string& host,
    AzpHandshakeState* state) {
  
  // 1. ゲートウェイ情報のルックアップ (SQLite DB)
  auto gateway_info = key_manager_->LookupGateway(host);
  if (!gateway_info) return std::nullopt;
  
  // 2. Epoch有効期限のチェック
  // ...

  // 3. 一時的な鍵ペアの生成 (Kyber + X25519)
  auto kyber_kp = crypto::KyberGenerateKeyPair();
  auto x25519_kp = crypto::X25519GenerateKeyPair();
  
  // 4. Phantom Ticket の偽造 (Forge)
  // クライアント側でチケットを作成し、ゲートウェイのEpoch Keyで暗号化する
  auto ticket_opt = key_manager_->ForgeTicket(host);
  
  // ... (ClientHello拡張の構築) ...
}

チケット構造体とシリアライズ

// static
PhantomTicket PhantomTicket::Generate(const std::string& domain,
                                       uint8_t epoch_hint) {
  PhantomTicket ticket;
  
  // ランダムなClient IDとTemp Secretを生成
  ticket.client_id.resize(32);
  crypto::RandBytes(ticket.client_id.data(), 32);
  
  ticket.temp_secret.resize(32);
  crypto::RandBytes(ticket.temp_secret.data(), 32);
  
  // 有効期限の設定 (30分)
  ticket.issue_time = base::Time::Now();
  ticket.expiry_time = ticket.issue_time + base::Seconds(kTicketValiditySecs);
  
  return ticket;
}

クライアント (Rust Test Client) 実装詳細

ブラウザ実装の正当性を検証するためのスタンドアロンクライアントです。

impl EncryptedTicket {
    pub fn encrypt(ticket: &PhantomTicket, epoch_pk: &[u8]) -> Result<Self> {
        let mut rng = rand::thread_rng();
        
        // 1. 共有シークレットのカプセル化 (Kyber)
        // ゲートウェイのEpoch Public Keyを使用
        let (ct, ss) = pqc_kyber::encapsulate(&pk, &mut rng)?;

        // 2. チケット本体の暗号化 (AES-GCM)
        // カプセル化で得られた共有シークレット(ss)を鍵として使用
        let key = Key::<Aes256Gcm>::from_slice(&ss);
        let cipher = Aes256Gcm::new(key);
        
        let ciphertext = cipher.encrypt(nonce_ref, plaintext.as_ref())?;
        
        Ok(Self {
            epoch_hint: ticket.epoch_hint,
            nonce,
            encapsulated_key: ct.to_vec(), // これをサーバーに送ることで、サーバーはssを復元できる
            ciphertext,
        })
    }
}

まとめ

はい。前よりはちょっとマシになったのではないでしょうか。
基本的に文章を書くのが苦手なため画像やコードでゴリ押したいのですがアドベントカレンダーなのでできるだけ文章で書いてみました。

兎にも角にも、chromiumのビルド時間が長過ぎる
フルビルドだと一回に4時間ちょいほどかかって1日に作業できる量が少なくなってしまう。
あと容量をバカ食うのでコードが欲しい人がいればNASかポータブルSSDを用意して持ってきてください。

ちょうど開発に着手したときがGemini 3 proやClaude 4.5 opusがリリースされた時あたりだったため開発も楽でした。
そろそろChatGPTを使う意味がなくなってきた
もちろん自分だけで作れるわけがないので時代に感謝ですね。
PoCづくりが目的なのでとりあえず動けばいいですし()

またこれから、Wiresharkやこの通信のためのペンテストツールを作ってみてテストして同じような感じで色々作ってみたいと思いました。

ありがとうございました!



コメント

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