未来へメッセージを送る暗号 — tlock + drand を Rust で使いこなす
「指定した時刻になるまでは、誰にも(自分自身にも)復号できないメッセージ」を作れる、と聞いたら少し SF めいた話に聞こえるかもしれません。
しかし、これは Timelock Encryption(タイムロック暗号) という技術として実用段階に入っており、drand という分散ランダムネスネットワークと、その上で動く tlock というスキームの組み合わせによって実現されています。
この記事では、Rust 学習者向けに、tlock + drand の仕組みを理解しつつ、tlock-rs / drand_core といった Rust クレートを使って実際に動かすところまで解説します。
tlock + drand がやりたいこと
タイムロック暗号の用途は、思いのほか広範です。
代表的なものを挙げると、責任ある脆弱性開示(指定日まで詳細を読めなくする)、封印入札オークション、MEV(Maximal Extractable Value)対策、ターン制ゲームでのコミット、投票、相続的なシークレット引き渡しなどがあります。
従来このような「未来にしか開けない箱」を作る方法は主に二つありました。
一つは 信頼できる第三者 に鍵を預ける方法。
これは単一障害点・単一信頼点を生みます。
もう一つは Proof of Work のように「復号に大量計算を要求する」方式ですが、これはハードウェア進化で時刻精度が崩れますし、エネルギーの無駄でもあります。
tlock はこのどちらでもなく、閾値ネットワークによる BLS 署名 と ID ベース暗号(IBE) を組み合わせることで、エネルギーを消費せず、単一の第三者にも依存せず、しかも復号自体は完全にオフラインでできる、という性質を実現しています。
drand とは何か
drand は「公開検証可能なランダムネスを定期的に生成する分散ネットワーク」です。
世界各地の独立組織で構成された League of Entropy が運営しており、Cloudflare、Protocol Labs、EPFL、Kudelski Security といった組織がノードを提供しています。
仕組みとしては、各ノードが BLS の鍵シェアを持ち、一定周期(quicknet では 3 秒)ごとに、そのラウンド番号 r に対する 部分署名 を生成して持ち寄ります。
閾値(例: 22 ノード中 t ノード)以上が集まれば、それらを集約することでネットワーク全体の秘密鍵に対する署名 σ_r が再構成されます。
この σ_r をハッシュ化したものが、そのラウンドの「ランダムネス」として公開されます。
ここで重要なのは次の三点です。
第一に、署名は決定論的です。
BLS 署名は同じメッセージ・同じ鍵に対して常に同じ値になるので、誰が計算しても結果が一致します。
第二に、検証は公開鍵だけでできます。
ネットワークの集約公開鍵を持っていれば、誰でも σ_r が正しいかを検証できます。
第三に、閾値未満では署名を作れない。
これがタイムロックの安全性の根っこになります。
つまり drand は「正確な時刻に、決まったメッセージ(ラウンド番号)に対する署名を公開する分散時計」として機能します。
tlock の核心: 「未来の署名」を復号鍵に使う
ここで天才的なアイデアが登場します。
drand のラウンド r の署名 σ_r は、時刻 T_r に達するまでネットワークからは出てこない。
しかし、署名されるメッセージ(ラウンド番号 r)と公開鍵は 今この瞬間にも分かっている。
これは ID ベース暗号(Boneh-Franklin IBE) が要求する設定と完全に一致します。
IBE では「任意の文字列を公開鍵として暗号化できる」「対応する秘密鍵は鍵生成局(KGC)が後から発行できる」という性質があります。tlock では:
- 公開鍵: drand ネットワークの集約公開鍵 + ラウンド番号 r(これが ID の役割)
- 秘密鍵: ラウンド r に対する BLS 署名 σ_r(これは時刻 T_r 以降に drand が公開する)
という対応関係が成立します。
BLS と IBE はどちらもペアリング写像(双線形写像)を使う方式なので、ペアリングを通じて「未来の署名 = 復号鍵」という構造が綺麗にハマるわけです。
実用上は、16 バイトの対称鍵を tlock で IBE 暗号化し、本文は age 暗号(ストリーム暗号)でその対称鍵を使って暗号化する、というハイブリッド構成が標準です。
これが tlock_age クレートの役割です。
Rust エコシステム
Rust でこの技術を扱うには、Cloudflare の Thibault Meunier 氏が公開している drand-rs / tlock-rs 一族のクレートが定番です。
役割分担は次のようになっています。
drand_core は drand ネットワークからビーコン(ランダムネス)を取得し、署名を検証するクライアントです。
tlock は raw な tlock 実装で、16 バイトまでのメッセージを直接暗号化できます。
tlock_age は age を組み合わせたハイブリッド暗号で、任意長のメッセージを扱えます。
さらに dee という CLI も同じリポジトリで提供されていて、これがそのままリファレンス実装になっています。
Go 実装(drand/tlock)、TypeScript 実装(tlock-js)とバイナリ互換があり、相互運用できる点も重要です。
動かしてみる
まず Cargo.toml の設定です。
Copy[package]
name = "tlock_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
drand_core = "0.5"
tlock_age = "0.2"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
chrono = "0.4"
タイムロック暗号化の最小例は次のような形になります。quicknet(3 秒周期、BLS-unchained-G1)のチェーンを使います。
Copyuse anyhow::Result;
use drand_core::HttpClient;
use std::io::Cursor;
#[tokio::main]
async fn main() -> Result<()> {
// 1. quicknet クライアントを構築
let client: HttpClient =
"https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
.try_into()?;
let info = client.chain_info()?;
// 2. 「30 秒後」のラウンド番号を計算
let now = chrono::Utc::now().timestamp() as u64;
let target_time = now + 30;
let round = (target_time - info.genesis_time) / info.period as u64;
println!("encrypting to round {}", round);
// 3. メッセージを tlock_age で暗号化
let message = b"Hello, future world!";
let mut ciphertext = Vec::new();
tlock_age::encrypt(
&mut ciphertext,
Cursor::new(message),
&info.hash(),
&info.public_key(),
round,
)?;
// 4. 復号を試みる(まだ時刻に達していなければ取得時にエラー)
let beacon = client.get(round)?; // 時刻が来るまでブロック/失敗
let mut plaintext = Vec::new();
tlock_age::decrypt(
&mut plaintext,
Cursor::new(&ciphertext),
&info.hash(),
&beacon.signature(),
)?;
println!("decrypted: {}", String::from_utf8_lossy(&plaintext));
Ok(())
}
Copy
ポイントは「暗号化側は drand から何ももらわなくてよい」点です。
チェーン情報(公開鍵 + ジェネシス時刻 + 周期)さえ手元にあれば、未来の任意のラウンドに向けて完全にオフラインで暗号化できます。
drand ネットワークと通信が必要になるのは、復号時に対応するラウンドの署名を取りに行く瞬間だけです。
実際、dee CLI を使えばこれを 1 行で確認できます。
Copyecho 'Hello dee!' | dee crypt -u quicknet -r 30s > data.dee
# 30 秒待ってから
dee crypt --decrypt data.dee
# => Hello dee!
Rust 学習者として注目すべきポイント
このプロジェクトは、Rust の暗号エコシステムを学ぶ題材としてかなり良く出来ています。
いくつか観点を挙げてみます。
ペアリング暗号は計算的に重く、サイドチャネルや実装ミスが致命傷になります。
tlock-rs は arkworks 系の BLS12-381 実装をベースに構築されており、Rust の型システムで G1/G2 の群を区別している点が読み応えあります。
型でドメインを分けることで、本来異なる群の要素を混ぜてしまうバグをコンパイル時に弾けるのです。
また、drand_core のチェーン情報やビーコンは、Go 実装・JS 実装とワイヤ互換である必要があるため、serde でのシリアライズ仕様や、bls-unchained-g1-rfc9380 のような細かいスキーム ID の取り回しがリアルな相互運用問題として現れます。
実務で「他言語実装と互換を取る」コードを書く練習として勉強になります。
tlock_age が age プロトコルを下敷きにしているのも興味深く、str4d/rage(age の Rust 実装)を読み解く入口にもなります。レイヤを分けた暗号設計の手本のような構成です。
最後に、ライブラリが wasm32 ターゲットでビルドできるよう作られている点も実用的です。
ブラウザでタイムロック復号 UI を作る、といった応用への道が開けています。
セキュリティ上の留意点
便利な技術ですが、いくつか弱点も理解しておく必要があります。
まず 閾値の仮定。
quicknet は 18 組織 22 ノードで運用されており、閾値以上が結託すれば任意の未来ラウンドの署名を先に作って暗号文を復号できます。
中央集権よりは遥かにマシですが、ゼロトラストではありません。
次に 量子耐性がない。
BLS もペアリングベース IBE も離散対数問題に依存するため、実用的な量子コンピュータが現れれば破られます。「1000 年後に開く」ような用途には向きません。
そして ネットワーク継続性。
League of Entropy が解散して鍵を削除すると、それ以降に作られた暗号文は永久に復号不能になります。
逆に言うと、ネットワークがある限り復号できるという意味で、「鍵管理」を分散ネットワークに外部化したアーキテクチャだと捉えるとよいでしょう。
thibmeu/drand-rs 自体は外部監査を受けていない実装です。
プロダクション利用する場合はその点も含めて検討が必要です。
まとめ
tlock + drand は、「分散時計としての閾値 BLS ネットワーク」と「ID ベース暗号」という二つのアイデアを組み合わせることで、信頼できる第三者にも、無駄なエネルギー消費にも頼らないタイムロック暗号を実現しました。
Rust では drand_core / tlock / tlock_age の 3 クレートを軸に、CLI の dee がリファレンスとして整備されており、入門にも実装にも取り組みやすい状況が整っています。
Rust の型システム、暗号ライブラリの設計、他言語実装との相互運用、WASM 対応といった現代的なテーマがひとつのドメインに凝縮されているので、学習素材として一度自分の手で動かしてみることを強くおすすめします。
封印入札の小さなプロトタイプを作るあたりから始めると、技術の面白さが体感できるはずです。
参考リンク:
- tlock 論文: https://eprint.iacr.org/2023/189
- drand 公式ドキュメント: https://docs.drand.love/
- thibmeu/drand-rs (dee CLI / drand_core): https://github.com/thibmeu/drand-rs
- thibmeu/tlock-rs: https://github.com/thibmeu/tlock-rs
- drand/tlock(Go リファレンス): https://github.com/drand/tlock