Share
Embed
人命を救う技術としてのEnd-to-End暗号化とMessaging Layer Security
2026/1/9 @ BuriKaigi 2026
sylph01
January 08, 2026
Transcript
97 slides
-
人命を救う技術としての End-to-End 暗号化と Messaging Layer Security 梶原 龍 / sylph01
2026/1/9 @ BuriKaigi 2026 1 -
いろいろやります 音ゲー ( 主にDanceDanceRevolution) クラシック音楽 ( オーケストラでファゴットを吹きます) DJ ( 音ゲー楽曲を中心に)
鉄道乗りつぶし (JR 線99%+) 自作キーボード 気になることがあったら懇親会で話しましょう! 5 -
今回のトークに関係のあることもやり ます デジタルアイデンティティとセキュリティに軸を置いたフリーラン スのWeb エンジニアです 標準化活動に参加してきました/ います HTTPS in Local
Network CG / Web of Things WG @ W3C, OAuth / Messaging Layer Security WG @ IETF Internet Society Japan Chapter のOfficer をしていました (2020-23) 7 -
いつもの注意事項 暗号API は誤った使い方をしやすいです プロトコルが安全でも運用をミスることがあります 「私は暗号の専門家ではありません」 a.k.a. "I am not djb"
production で使う場合には専門家のレビューを受けてください あと私は該当分野で博士号はおろか修士号すら持ってないので… 8 -
End-to-End 暗号化 = サービス運営者がメッセージを読めない LINE, Facebook Messenger, Signal, WhatsApp, X
Chat, ... それぞれ(似てるけど)独自のE2EE のプロトコルを持っている 一部のアプリでは明示的に有効にしないとE2EE にならないことに注意 14 -
Messaging Layer Security より相互運用性のあるE2EE 鍵交換の方式を定めたもの Protocol (RFC 9420), Architecture (RFC
9750) RFC 9420 をRuby で実装した話をします 21 -
前提整理 Any% 共通鍵暗号: 暗号化と復号に使う鍵が同じ AES などがよく使われる。暗号利用モードの話は省略 公開鍵暗号: 暗号化と復号に用いられる鍵が違う 公開鍵は晒しても基本的に痛くない 「公開鍵を使って暗号化、秘密鍵を使って復号」
通常逆は想定しない RSA がよく知られるがMLS の文脈では楕円曲線暗号 鍵のカプセル化(Key Encapsulation) のために用いる 28 -
Hybrid Public Key Encryption MLS のビルディングブロックの一つ 「公開鍵で暗号化して秘密鍵で復号」をよりフォーマルにしたやつ 以下の組み合わせ Key Encapsulation
Mechanism (KEM) (≒ 公開鍵暗号) Key Derivation Function (KDF) (≒ ハッシュ) Authenticated Encryption with Associated Data (AEAD) (= 共通鍵 暗号) Ruby では hpke gem として提供しています だいたい2023-2024 にかけての主な活動。2025/12/31 にver 1.0.0 をリリースしました 29 -
2 人の場合は簡単 # 両者とも共通の秘密から始めて chain_key[0] = "some common secret" #
HMAC は「鍵付きハッシュ」 # n 周期目のメッセージの鍵はn 周期目のchain_key から作る message_key[n] = hmac_sha256(chain_key[n], 0x02) # n+1 周期目のchain key はn 周期目のchain key から # メッセージの鍵とは違うHMAC 鍵を使って作る chain_key[n+1] = hmac_sha256(chain_key[n], 0x01) Double Ratchet algorithm の "symmetric key ratchet" と呼ばれる部分。要するにハッシュをチェーンしてる。 33 -
配列で木を表現 完全平衡木は一次元配列で表現で きる 右図の木は以下の配列で表現可: [0, 1, 2, 3, 4, 5,
6, 7, 8, 9, nil, 11, nil, nil, nil] 36 -
配列で木を表現 葉ノードは常に偶数index 親ノードは常に奇数index これらの性質を元に left , right , parent ,
sibling な どの操作を定義していく 37 -
Secret Tree def self.populate_tree_impl(suite, tree, index, secret) tree.array[index] = {
... } unless Melos::Tree.leaf?(index) left_secret = Melos::Crypto.expand_with_label(suite, secret, "tree", "left", suite.kdf.n_h) right_secret = Melos::Crypto.expand_with_label(suite, secret, "tree", "right", suite.kdf.n_h) populate_tree_impl(suite, tree, Melos::Tree.left(index), left_secret) populate_tree_impl(suite, tree, Melos::Tree.right(index), right_secret) end end encryption_secret から再帰的に木の葉ノードまでsecret を作る file lib/melos/secret_tree.rb 43 -
Secret Tree ratcheting を行う部分は以下: def self.ratchet_application(suite, tree, leaf_index) node_index =
leaf_index * 2 generation = tree.array[node_index]['next_application_ratchet_secret_generation'] application_ratchet_secret = tree.array[node_index]['application_ratchet_secret'] application_nonce = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "nonce", generation, suite.hpke.n_n) application_key = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "key", generation, suite.hpke.n_k) next_application_ratchet_secret = Melos::Crypto.derive_tree_secret(suite, application_ratchet_secret, "secret", generation, suite.kdf.n_h) tree.array[node_index]['next_application_ratchet_secret_generation'] = generation + 1 tree.array[node_index]['application_ratchet_secret'] = next_application_ratchet_secret tree.array[node_index]['application_nonce'] = application_nonce tree.array[node_index]['application_key'] = application_key end file lib/melos/secret_tree.rb 44 -
Secret Tree だけど実質はこう: key = Crypto.derive_tree_secret(ratchet_secret[n], "key", n) nonce =
Crypto.derive_tree_secret(ratchet_secret[n], "nonce", n) ratchet_secret[n + 1] = Crypto.derive_tree_secret(ratchet_secret[n], "secret", n) これでメッセージごとの key と nonce が得られる。 2-party のratcheting と比較するとわかりやすい: chain_key[0] = "some common secret" message_key[n] = hmac_sha256(chain_key[n], 0x02) chain_key[n+1] = hmac_sha256(chain_key[n], 0x01) 45 -
Key Schedule 世代 n の epoch_secret と次の世代に進めるためのグループ共通の値 commit_secret が合意できていれば、次の世代の epoch_secret
も合 意できる。 see lib/melos/key_schedule.rb and test/test_key_schedule.rb 46 -
TreeKEM 各ユーザーは 葉ノード が割り当て られ、 それぞれ leaf key pair を持つ
各ノードに対応するkey pair は path_secret から導出できる (後述) 48 -
TreeKEM 自分自身の葉の位置で path_secret を作り 親ノードの path_secret は子の path_secret から計算 それをroot
まで計算 commit_secret はroot の path_secret から計算 50 -
TreeKEM path_secret[0] = (random) path_secret[1] = hash(path_secret[0]) path_secret[3] = hash(path_secret[1])
commit_secret = hash(path_secret[7]) ( 7 = root) 51 -
TreeKEM 0 が UpdatePath ( 黄) を作るとき UpdatePath 沿いの copath
nodes (green) を探す ParentNode が全部埋まっている とき、 path secret を copath node の鍵それぞれに対して暗号化する ParentNode の公開鍵は path_secret から導出できる 52 -
TreeKEM 最も単純な2-leaf tree で見てみる 0 がUpdatePath 作成、 1 は path_secret
を持つ path_secret は 2 の公開鍵で暗号 化される ノード 2 は自身の秘密鍵を知ってい るので復号可能 53 -
TreeKEM ツリーに空白がある場合はどうする? copath node の resolution を計算し、どのnode の鍵が利用可能かを 特定する その後、UpdatePath
の path_secret をresolution のそれぞれの鍵に 対して暗号化する 54 -
TreeKEM 右図の紫で示したノードのresolution は [3, 2] resolution は自身のindex + unmerged leaves
のリストをその順にくっつけた もの 55 -
TreeKEM 右図の場合は [3, 2, 9, 14] 自身が空白の場合、左のノードの resolution + 右のノードのresolution
を その順にくっつけたもの、として再帰 的に計算する 56 -
TreeKEM なので実際にはこうなる: UpdatePath 上の各ノードの path_secret を計算する その後、各 path_secret をそれ ぞれのcopath
node のresolution に 対して暗号化する 58 -
TreeKEM commit_secret を直接全員に対 して暗号化しても動作はする けどそれは O(n) (n = # of
members) commit_secret をUpdatePath 沿 いのcopath node に対して暗号化し ていくことで暗号化の数は O(log n) 59 -
まとめ Secret Tree によって epoch_secret から各メッセージ ごとの鍵を得られる Key Schedule で今の世代の
epoch_secret と commit_secret から次の世代の epoch_secret が得られる TreeKEM で commit_secret の合 意ができる 60 -
実際のメッセージの送信 アプリケーションメッセージは暗号化+ 署名された PrivateMessage に 包まれて送られる。 framed_content = Melos::Struct::FramedContent.create(...) authenticated_content
= Melos::Struct::AuthenticatedContent.create(...) authenticated_content.sign(...) app_message = Melos::Struct::PrivateMessage.protect( authenticated_content, secret_tree, ... ) ハンドシェイクなどは署名のみの PublicMessage の場合がある 61 -
グループの一生 グループは proposal と commit を通して更新される。 Proposal ユーザーの追加と削除 ユーザーのleaf key
の更新 Pre-Shared Keys の挿入 Commit はこれらを確定し次の世代(epoch) に進める UpdatePath はCommit の中で伝えられる 62 -
グループの一生 Group はユーザーの LeafNode を0-node group に追加して作られる ランダムな epoch_secret から開始する
def init_group(node) # single node, a leaf node containing an HPKE PK and credential for the creator @ratchet_tree = [node] @leaf_index = 0 @tree_hash = Melos::Struct::RatchetTree.root_tree_hash(@cipher_suite, @ratchet_tree) @confirmed_transcript_hash = '' @epoch_secret = SecureRandom.random_bytes(@cipher_suite.kdf.n_h) ... @group_initialized = true end lib/group.rb from a work-in-progress version 63 -
グループの一生 グループにjoin するユーザーは自 身の公開鍵を含むアイデンティ ティを KeyPackage の形で Directory に公開する グループの状態を含む
Welcome メッセージを受け取りjoin する Figure 2 in RFC 9420 Section 3.2. The Delivery Service is out of scope for this implementation 64 -
グループの一生 ユーザー A がグループ作成 B を追加するには A が B を追加する
Add proposal を送信して Commit A は B に対して Welcome を送信する Figure 3 in RFC 9420 Section 3.2. 65 -
グループの一生 Add で行われるツリーへの操作 KeyPackage 中の leaf_node を 一番左の空白のノードに追加 そこからroot までのノードに対
して、追加したノードのindex を unmerged leaves list に追加する 66 -
グループの一生 Update B は自身の鍵の更新を Update proposal で通知 他のユーザー A がそれを
Commit し次のepoch に含め る Figure 4 in RFC 9420 Section 3.2. 67 -
グループの一生 Update で行われるツリーへの操作 Update に含まれる送信者の leaf_node を使って葉ノードを 置き換える root までの経路上のノードをすべ
て空白化 鍵を繰り返し更新することによってpost-compromise security が得られる 68 -
グループの一生 Remove Z は B を削除する Remove と Commit を送信
B は Commit に含まれる UpdatePath の path_secret を復号化でき ず、次の epoch_secret を 知ることができない 69 -
グループの一生 空白のノードはいつ埋められる? Commit の処理時 Commit の処理時に UpdatePath がratchet tree に組み込まれ
る UpdatePath の中身を使って ParentNode が作られる 71 -
実装どんな感じよ? テストベクターは全部通る RubyKaigi → RubyConf Taiwan で以下をやった 諸々リファクタリング メッセージのparse を
StringIO 化 Group の状態を Group オブジェクトにまとめた まだやってない クライアント向けAPI 他の実装に対する相互運用性テスト ほんとはここでクライアント向けAPI 終わった!って話をしたかったんだけど、年末にインフルをやってしまってな… 75 -
クライアントインターフェース 鍵ペアの生成 グループにjoin する 0 人→1 人の場合は自身の鍵ペアをtree に登録してepoch 0 を初
期化 n 人→n+1 人 (n>0) のときは KeyPackage を受け取りそこから鍵 を取り出してグループに追加、 Welcome メッセージを作る group ブランチ https:/ /github.com/sylph01/melos/commits/group でやってる 76 -
クライアントインターフェース メッセージを作る FramedContent : application / proposal / commit を送信
者の情報と合わせてラッピングする AuthenticatedContent : FramedContentTBS + FramedContentAuthData を使って FramedContent を署名し たもの PublicMessage : AuthenticatedContent に membership_tag というMAC を付加したもの RFC 9420, Section 6 77 -
クライアントインターフェース 暗号化されたメッセージを作る FramedContent 、 AuthenticatedContent は同じ PrivateMessage : SenderData と
PrivateMessageContent が暗号化されたもの。 PrivateMessageContent に AuthenticatedContent が含まれる 78 -
クライアント向けAPI cipher_suite_id = 1 client = Melos::Client.new(cipher_suite_id) # generate key
pairs key_package = Melos::KeyPackage.new(client) # create KP to publish to DS leaf_node = key_package.create_leaf_node('credential_name') node = Melos::Struct::Node.new_leaf_node(leaf_node) group = Melos::Group.new(client, 'group_id') group.init_group(node) # create a 1-node group 79 -
クライアント向けAPI client2 = Melos::Client.new(cipher_suite_id) key_package2 = Melos::KeyPackage.new(client2) leaf_node2 = key_package2.create_leaf_node('credential_name2')
key_package_message2 = key_package2.create_message(leaf_node2) # User 1 creates add proposal prop = group.create_add_proposal( key_package_message2, client.signature_private_key) # something like this...? commit = create_commit(prop, ...) group.apply_commit(commit) group2.apply_welcome(welcome) 80 -
あったら嬉しい機能 鍵ペアの対応を調べる(Edwards curve とそうでないEC でAPI が違う) def self.signature_key_pair_corresponds?(suite, private_key, public_key)
private_pkey = suite.pkey.deserialize_private_signature_key(private_key) public_pkey = suite.pkey.deserialize_public_signature_key(public_key) if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448) # is an Edwards curve; check equality of the raw public key private_pkey.raw_public_key == public_pkey.raw_public_key else # is an EC; check equality of the public key Point private_pkey.public_key == public_pkey.public_key end end 83 -
あったら嬉しい機能 UncompressedPointRepresentation 形式の鍵を得る(のもEdwards curve かそうでないかで違う) def self.derive_key_pair(suite, secret) pkey =
suite.hpke.kem.derive_key_pair(secret) if suite.pkey.equal?(Melos::Crypto::CipherSuite::X25519) || suite.pkey.equal?(Melos::Crypto::CipherSuite::X448) # is an Edwards curve [pkey.raw_private_key, pkey.raw_public_key] else # is an EC [pkey.private_key.to_s(2), pkey.public_key.to_bn.to_s(2)] end end 84 -
あったら嬉しい機能 OpenSSL 3.5.0 には post-quantum cryptography( 耐量子暗号) のAPI がある ML-KEM,
ML-DSA, SLH-DSA MLS にML-KEM を導入するInternet-Draft がすでに出てる そういえば耐量子暗号のセッションありますね?暗号のセッション3 つもあるし2026 年は大暗号時代ですよ 85 -
Why do you need End-to-End Encryption in Ruby? Because... DanceDanceRevolution
のネタです。MAX.(period), Over the "Period" のニコニコ大百科が詳しい 88 -
そのうちハンズオンをやります 実装がある程度でき次第 日本でMLS やMIMI(More Instant Messaging Interoperability) やる人増 やしたい Ruby
実装があると irb で途中状態追いかけられて嬉しい Internet Society Japan Chapter のWorkshop などの形式を予定 95 -
Shoutouts The Messaging Layer Security Working Group @ IETF Ruby
でプロトコルを実装してる皆様(最近増えてる!) BuriKaigi のオーガナイザーの皆様 96