Class: PqcRails::HybridKem

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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_curveObject (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_nameObject (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

#freeObject



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_keypairObject



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