Class: PqcRails::HybridKem
- Inherits:
-
Object
- Object
- PqcRails::HybridKem
- Defined in:
- lib/pqc_rails/hybrid_kem.rb
Overview
古典ECDH(DhKem)とPQ KEM(liboqs経由のKem)を組み合わせたハイブリッドKEM。
量子コンピュータがPQ側を破ったとしても古典側が安全なら(あるいはその逆でも) 全体の共有鍵は安全、という「移行期間中の保険」を提供するのが目的。 NIST標準化されたML-KEMはまだ実運用歴が浅いため、実績のあるX25519と組み合わせることで 「PQアルゴリズム自体に未知の脆弱性が見つかった場合」のリスクを下げる。
コンバイナの方式: 古典側の共有鍵とPQ側の共有鍵を連結し、両者のciphertext (encapsulateの出力)をHKDFのinfoにバインドした上でHKDF-SHA256にかけて 32バイトの最終共有鍵を導出する。ciphertextをバインドするのは、 通常のTLSハンドシェイクのようなトランスクリプトハッシュによる結合が無い スタンドアロン用途のため、コンバイナ自体に結合の安全性を持たせる必要があるため。
この「連結してからHKDFにかける」方式は自己流ではなく、RFC 9954(TLS 1.3ハイブリッド鍵共有、 旧draft-ietf-tls-hybrid-design)が採用する"concatenation approach"と同じ構成であり、 NIST SP 800-56Cが承認済みアルゴリズムと非承認アルゴリズムのハイブリッド共有鍵生成における 承認済み手法として単純連結を挙げている。RFC 9954はこの構成が「dual-PRFコンバイナ」に対応し、 ハッシュ関数がdual-PRFであるという仮定の下で安全性が証明されているとしている([BINDEL]論文)。 なお、RFC 9370(IKEv2の複数鍵交換)はSKEYSEED(n) = prf(SK_d(n-1), SK(n) | Ni | Nr)という 逐次的なPRF連鎖(cascade)を採るため、本実装とは別のコンバイナ方式である点に注意 (2026-07-16、一次資料読解セッションでRFC本文を確認して検証済み)。
使い方:
PqcRails::HybridKem.open(:ml_kem_512) do |hybrid|
keypair = hybrid.generate_keypair
encap = hybrid.encapsulate(keypair.public_key)
shared = hybrid.decapsulate(encap.ciphertext, keypair.secret_key)
shared == encap.shared_secret # => true
end
Defined Under Namespace
Classes: Encapsulation, Keypair
Constant Summary collapse
- COMBINER_LABEL =
"pqc_rails-hybrid-kem-v1"- SHARED_SECRET_LENGTH =
32
Instance Attribute Summary collapse
-
#classical_curve ⇒ Object
readonly
Returns the value of attribute classical_curve.
-
#pq_alg_name ⇒ Object
readonly
Returns the value of attribute pq_alg_name.
Class Method Summary collapse
Instance Method Summary collapse
- #decapsulate(ciphertext, secret_key) ⇒ Object
- #encapsulate(public_key) ⇒ Object
- #free ⇒ Object
- #generate_keypair ⇒ Object
-
#initialize(pq_alg_name, classical_curve = "X25519") ⇒ HybridKem
constructor
A new instance of HybridKem.
Constructor Details
#initialize(pq_alg_name, classical_curve = "X25519") ⇒ HybridKem
Returns a new instance of HybridKem.
47 48 49 50 51 52 53 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 47 def initialize(pq_alg_name, classical_curve = "X25519") @pq_alg_name = pq_alg_name @classical_curve = classical_curve @classical = DhKem.new(classical_curve) @pq = Kem.new(pq_alg_name) @freed = false end |
Instance Attribute Details
#classical_curve ⇒ Object (readonly)
Returns the value of attribute classical_curve.
45 46 47 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 45 def classical_curve @classical_curve end |
#pq_alg_name ⇒ Object (readonly)
Returns the value of attribute pq_alg_name.
45 46 47 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 45 def pq_alg_name @pq_alg_name end |
Class Method Details
.open(pq_alg_name, classical_curve = "X25519") ⇒ Object
100 101 102 103 104 105 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 100 def self.open(pq_alg_name, classical_curve = "X25519") hybrid = new(pq_alg_name, classical_curve) yield hybrid ensure hybrid&.free end |
Instance Method Details
#decapsulate(ciphertext, secret_key) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 81 def decapsulate(ciphertext, secret_key) ensure_not_freed! classical_ciphertext, pq_ciphertext = BlobPacking.unpack(ciphertext) classical_secret_key, pq_secret_key = BlobPacking.unpack(secret_key) classical_shared = @classical.decapsulate(classical_ciphertext, classical_secret_key) pq_shared = @pq.decapsulate(pq_ciphertext, pq_secret_key) combine(classical_shared, pq_shared, classical_ciphertext, pq_ciphertext) end |
#encapsulate(public_key) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 67 def encapsulate(public_key) ensure_not_freed! classical_public_key, pq_public_key = BlobPacking.unpack(public_key) classical_encap = @classical.encapsulate(classical_public_key) pq_encap = @pq.encapsulate(pq_public_key) Encapsulation.new( BlobPacking.pack(classical_encap.ciphertext, pq_encap.ciphertext), combine(classical_encap.shared_secret, pq_encap.shared_secret, classical_encap.ciphertext, pq_encap.ciphertext) ) end |
#free ⇒ Object
93 94 95 96 97 98 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 93 def free return if @freed @pq.free @freed = true end |
#generate_keypair ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/pqc_rails/hybrid_kem.rb', line 55 def generate_keypair ensure_not_freed! classical_keypair = @classical.generate_keypair pq_keypair = @pq.generate_keypair Keypair.new( BlobPacking.pack(classical_keypair.public_key, pq_keypair.public_key), BlobPacking.pack(classical_keypair.secret_key, pq_keypair.secret_key) ) end |