pq_crypto 0.3.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -0
  3. data/CHANGELOG.md +50 -0
  4. data/GET_STARTED.md +374 -30
  5. data/README.md +59 -195
  6. data/SECURITY.md +101 -82
  7. data/ext/pqcrypto/extconf.rb +85 -9
  8. data/ext/pqcrypto/mldsa_api.h +71 -1
  9. data/ext/pqcrypto/mlkem_api.h +24 -0
  10. data/ext/pqcrypto/pq_externalmu.c +310 -0
  11. data/ext/pqcrypto/pqcrypto_ruby_secure.c +784 -85
  12. data/ext/pqcrypto/pqcrypto_secure.c +179 -72
  13. data/ext/pqcrypto/pqcrypto_secure.h +103 -7
  14. data/ext/pqcrypto/pqcrypto_version.h +7 -0
  15. data/ext/pqcrypto/vendor/.vendored +1 -1
  16. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
  17. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/LICENSE +5 -0
  18. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile +19 -0
  19. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile.Microsoft_nmake +23 -0
  20. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/api.h +18 -0
  21. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.c +83 -0
  22. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.h +11 -0
  23. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.c +327 -0
  24. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.h +22 -0
  25. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.c +164 -0
  26. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.h +23 -0
  27. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.c +146 -0
  28. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.h +14 -0
  29. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/params.h +36 -0
  30. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.c +311 -0
  31. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.h +37 -0
  32. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.c +198 -0
  33. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.h +26 -0
  34. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.c +41 -0
  35. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.h +13 -0
  36. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric-shake.c +71 -0
  37. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric.h +30 -0
  38. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.c +67 -0
  39. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.h +13 -0
  40. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/LICENSE +5 -0
  41. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile +19 -0
  42. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile.Microsoft_nmake +23 -0
  43. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/api.h +18 -0
  44. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.c +108 -0
  45. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.h +11 -0
  46. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.c +327 -0
  47. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.h +22 -0
  48. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.c +164 -0
  49. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.h +23 -0
  50. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.c +146 -0
  51. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.h +14 -0
  52. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/params.h +36 -0
  53. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.c +299 -0
  54. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.h +37 -0
  55. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.c +188 -0
  56. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.h +26 -0
  57. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.c +41 -0
  58. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.h +13 -0
  59. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric-shake.c +71 -0
  60. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric.h +30 -0
  61. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.c +67 -0
  62. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.h +13 -0
  63. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile +19 -0
  64. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/LICENSE +5 -0
  65. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile +19 -0
  66. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile.Microsoft_nmake +23 -0
  67. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/api.h +50 -0
  68. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.c +98 -0
  69. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.h +10 -0
  70. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.c +261 -0
  71. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.h +31 -0
  72. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/params.h +44 -0
  73. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.c +848 -0
  74. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.h +52 -0
  75. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.c +415 -0
  76. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.h +65 -0
  77. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.c +69 -0
  78. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.h +17 -0
  79. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.c +98 -0
  80. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.h +14 -0
  81. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.c +407 -0
  82. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.h +47 -0
  83. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric-shake.c +26 -0
  84. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric.h +34 -0
  85. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile +19 -0
  86. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/LICENSE +5 -0
  87. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile +19 -0
  88. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile.Microsoft_nmake +23 -0
  89. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/api.h +50 -0
  90. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.c +98 -0
  91. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.h +10 -0
  92. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.c +261 -0
  93. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.h +31 -0
  94. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/params.h +44 -0
  95. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.c +823 -0
  96. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.h +52 -0
  97. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.c +415 -0
  98. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.h +65 -0
  99. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.c +69 -0
  100. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.h +17 -0
  101. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.c +92 -0
  102. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.h +14 -0
  103. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.c +407 -0
  104. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.h +47 -0
  105. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric-shake.c +26 -0
  106. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric.h +34 -0
  107. data/lib/pq_crypto/algorithm_registry.rb +200 -0
  108. data/lib/pq_crypto/hybrid_kem.rb +1 -12
  109. data/lib/pq_crypto/kem.rb +104 -13
  110. data/lib/pq_crypto/pkcs8.rb +387 -0
  111. data/lib/pq_crypto/serialization.rb +1 -14
  112. data/lib/pq_crypto/signature.rb +231 -13
  113. data/lib/pq_crypto/spki.rb +131 -0
  114. data/lib/pq_crypto/version.rb +1 -1
  115. data/lib/pq_crypto.rb +90 -19
  116. data/script/vendor_libs.rb +4 -0
  117. metadata +99 -3
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module AlgorithmRegistry
5
+ class << self
6
+ def entries
7
+ @entries ||= begin
8
+ {
9
+ ml_kem_512: {
10
+ family: :ml_kem,
11
+ legacy_oid: nil,
12
+ standard_oid: "2.16.840.1.101.3.4.4.1",
13
+ public_key_bytes: PQCrypto::ML_KEM_512_PUBLIC_KEY_BYTES,
14
+ secret_key_bytes: PQCrypto::ML_KEM_512_SECRET_KEY_BYTES,
15
+ ciphertext_bytes: PQCrypto::ML_KEM_512_CIPHERTEXT_BYTES,
16
+ shared_secret_bytes: PQCrypto::ML_KEM_512_SHARED_SECRET_BYTES,
17
+ signature_bytes: nil,
18
+ description: "Pure ML-KEM-512 primitive (FIPS 203).",
19
+ }.freeze,
20
+ ml_kem_768: {
21
+ family: :ml_kem,
22
+ legacy_oid: "2.25.186599352125448088867056807454444238446",
23
+ standard_oid: "2.16.840.1.101.3.4.4.2",
24
+ public_key_bytes: PQCrypto::ML_KEM_PUBLIC_KEY_BYTES,
25
+ secret_key_bytes: PQCrypto::ML_KEM_SECRET_KEY_BYTES,
26
+ ciphertext_bytes: PQCrypto::ML_KEM_CIPHERTEXT_BYTES,
27
+ shared_secret_bytes: PQCrypto::ML_KEM_SHARED_SECRET_BYTES,
28
+ signature_bytes: nil,
29
+ description: "Pure ML-KEM-768 primitive (FIPS 203).",
30
+ }.freeze,
31
+ ml_kem_1024: {
32
+ family: :ml_kem,
33
+ legacy_oid: nil,
34
+ standard_oid: "2.16.840.1.101.3.4.4.3",
35
+ public_key_bytes: PQCrypto::ML_KEM_1024_PUBLIC_KEY_BYTES,
36
+ secret_key_bytes: PQCrypto::ML_KEM_1024_SECRET_KEY_BYTES,
37
+ ciphertext_bytes: PQCrypto::ML_KEM_1024_CIPHERTEXT_BYTES,
38
+ shared_secret_bytes: PQCrypto::ML_KEM_1024_SHARED_SECRET_BYTES,
39
+ signature_bytes: nil,
40
+ description: "Pure ML-KEM-1024 primitive (FIPS 203).",
41
+ }.freeze,
42
+ ml_kem_768_x25519_xwing: {
43
+ family: :ml_kem_hybrid,
44
+ legacy_oid: "1.3.6.1.4.1.62253.25722",
45
+ standard_oid: nil,
46
+ public_key_bytes: PQCrypto::HYBRID_KEM_PUBLIC_KEY_BYTES,
47
+ secret_key_bytes: PQCrypto::HYBRID_KEM_SECRET_KEY_BYTES,
48
+ ciphertext_bytes: PQCrypto::HYBRID_KEM_CIPHERTEXT_BYTES,
49
+ shared_secret_bytes: PQCrypto::HYBRID_KEM_SHARED_SECRET_BYTES,
50
+ signature_bytes: nil,
51
+ description: "Hybrid KEM: ML-KEM-768 + X25519 combined via X-Wing SHA3-256 combiner (draft-connolly-cfrg-xwing-kem).",
52
+ }.freeze,
53
+ ml_dsa_44: {
54
+ family: :ml_dsa,
55
+ legacy_oid: nil,
56
+ standard_oid: "2.16.840.1.101.3.4.3.17",
57
+ public_key_bytes: PQCrypto::SIGN_44_PUBLIC_KEY_BYTES,
58
+ secret_key_bytes: PQCrypto::SIGN_44_SECRET_KEY_BYTES,
59
+ ciphertext_bytes: nil,
60
+ shared_secret_bytes: nil,
61
+ signature_bytes: PQCrypto::SIGN_44_BYTES,
62
+ description: "ML-DSA-44 signature primitive (FIPS 204).",
63
+ }.freeze,
64
+ ml_dsa_65: {
65
+ family: :ml_dsa,
66
+ legacy_oid: "2.25.305232938483772195555080795650659207792",
67
+ standard_oid: "2.16.840.1.101.3.4.3.18",
68
+ public_key_bytes: PQCrypto::SIGN_PUBLIC_KEY_BYTES,
69
+ secret_key_bytes: PQCrypto::SIGN_SECRET_KEY_BYTES,
70
+ ciphertext_bytes: nil,
71
+ shared_secret_bytes: nil,
72
+ signature_bytes: PQCrypto::SIGN_BYTES,
73
+ description: "ML-DSA-65 signature primitive (FIPS 204).",
74
+ }.freeze,
75
+ ml_dsa_87: {
76
+ family: :ml_dsa,
77
+ legacy_oid: nil,
78
+ standard_oid: "2.16.840.1.101.3.4.3.19",
79
+ public_key_bytes: PQCrypto::SIGN_87_PUBLIC_KEY_BYTES,
80
+ secret_key_bytes: PQCrypto::SIGN_87_SECRET_KEY_BYTES,
81
+ ciphertext_bytes: nil,
82
+ shared_secret_bytes: nil,
83
+ signature_bytes: PQCrypto::SIGN_87_BYTES,
84
+ description: "ML-DSA-87 signature primitive (FIPS 204).",
85
+ }.freeze,
86
+ }.freeze
87
+ end
88
+ end
89
+
90
+ def fetch(symbol)
91
+ entries.fetch(symbol) do
92
+ raise UnsupportedAlgorithmError, "Unsupported algorithm: #{symbol.inspect}"
93
+ end
94
+ end
95
+
96
+ def legacy_oid(symbol)
97
+ fetch(symbol).fetch(:legacy_oid)
98
+ end
99
+
100
+ def standard_oid(symbol)
101
+ oid = fetch(symbol).fetch(:standard_oid)
102
+ raise SerializationError, "No standard OID registered for #{symbol.inspect}" if oid.nil?
103
+
104
+ oid
105
+ end
106
+
107
+ def by_legacy_oid(oid)
108
+ legacy_oid_index.fetch(oid, nil)
109
+ end
110
+
111
+ def by_standard_oid(oid)
112
+ standard_oid_index.fetch(oid, nil)
113
+ end
114
+
115
+ def supported_kems
116
+ supported_by_family(:ml_kem)
117
+ end
118
+
119
+ def supported_hybrid_kems
120
+ supported_by_family(:ml_kem_hybrid)
121
+ end
122
+
123
+ def supported_signatures
124
+ supported_by_family(:ml_dsa)
125
+ end
126
+
127
+ def legacy_metadata_view
128
+ @legacy_metadata_view ||= entries.each_with_object({}) do |(algorithm, entry), view|
129
+ oid = entry.fetch(:legacy_oid)
130
+ next if oid.nil?
131
+
132
+ view[algorithm] = {
133
+ family: entry.fetch(:family),
134
+ oid: oid,
135
+ }.freeze
136
+ end.freeze
137
+ end
138
+
139
+ def details_for_family(family)
140
+ @details_for_family ||= {}
141
+ @details_for_family[family] ||= begin
142
+ entries.each_with_object({}) do |(algorithm, entry), details|
143
+ next unless entry.fetch(:family) == family
144
+
145
+ details[algorithm] = details_entry(algorithm, entry)
146
+ end.freeze
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ def supported_by_family(family)
153
+ supported_by_family_cache[family] ||= entries.each_with_object([]) do |(algorithm, entry), supported|
154
+ supported << algorithm if entry.fetch(:family) == family
155
+ end.freeze
156
+ end
157
+
158
+ def supported_by_family_cache
159
+ @supported_by_family_cache ||= {}
160
+ end
161
+
162
+ def legacy_oid_index
163
+ @legacy_oid_index ||= entries.each_with_object({}) do |(algorithm, entry), index|
164
+ oid = entry.fetch(:legacy_oid)
165
+ index[oid] = algorithm unless oid.nil?
166
+ end.freeze
167
+ end
168
+
169
+ def standard_oid_index
170
+ @standard_oid_index ||= entries.each_with_object({}) do |(algorithm, entry), index|
171
+ oid = entry.fetch(:standard_oid)
172
+ index[oid] = algorithm unless oid.nil?
173
+ end.freeze
174
+ end
175
+
176
+ def details_entry(algorithm, entry)
177
+ # :oid intentionally remains an alias for the legacy pqc_container_* OID.
178
+ # Use .standard_oid when an RFC 9935/9881 OID is required.
179
+ detail = {
180
+ name: algorithm,
181
+ family: entry.fetch(:family),
182
+ oid: entry.fetch(:legacy_oid),
183
+ public_key_bytes: entry.fetch(:public_key_bytes),
184
+ secret_key_bytes: entry.fetch(:secret_key_bytes),
185
+ }
186
+
187
+ if entry.fetch(:ciphertext_bytes)
188
+ detail[:ciphertext_bytes] = entry.fetch(:ciphertext_bytes)
189
+ detail[:shared_secret_bytes] = entry.fetch(:shared_secret_bytes)
190
+ end
191
+
192
+ detail[:signature_bytes] = entry.fetch(:signature_bytes) if entry.fetch(:signature_bytes)
193
+ detail[:description] = entry.fetch(:description)
194
+
195
+ detail.freeze
196
+ end
197
+
198
+ end
199
+ end
200
+ end
@@ -4,18 +4,7 @@ module PQCrypto
4
4
  module HybridKEM
5
5
  CANONICAL_ALGORITHM = :ml_kem_768_x25519_xwing
6
6
 
7
- DETAILS = {
8
- CANONICAL_ALGORITHM => {
9
- name: CANONICAL_ALGORITHM,
10
- family: Serialization.algorithm_to_family(CANONICAL_ALGORITHM),
11
- oid: Serialization.algorithm_to_oid(CANONICAL_ALGORITHM),
12
- public_key_bytes: HYBRID_KEM_PUBLIC_KEY_BYTES,
13
- secret_key_bytes: HYBRID_KEM_SECRET_KEY_BYTES,
14
- ciphertext_bytes: HYBRID_KEM_CIPHERTEXT_BYTES,
15
- shared_secret_bytes: HYBRID_KEM_SHARED_SECRET_BYTES,
16
- description: "Hybrid KEM: ML-KEM-768 + X25519 combined via X-Wing SHA3-256 combiner (draft-connolly-cfrg-xwing-kem).",
17
- }.freeze,
18
- }.freeze
7
+ DETAILS = AlgorithmRegistry.details_for_family(:ml_kem_hybrid).freeze
19
8
 
20
9
  class << self
21
10
  def generate(algorithm = CANONICAL_ALGORITHM)
data/lib/pq_crypto/kem.rb CHANGED
@@ -6,23 +6,33 @@ module PQCrypto
6
6
  module KEM
7
7
  CANONICAL_ALGORITHM = :ml_kem_768
8
8
 
9
- DETAILS = {
10
- CANONICAL_ALGORITHM => {
11
- name: CANONICAL_ALGORITHM,
12
- family: Serialization.algorithm_to_family(CANONICAL_ALGORITHM),
13
- oid: Serialization.algorithm_to_oid(CANONICAL_ALGORITHM),
14
- public_key_bytes: ML_KEM_PUBLIC_KEY_BYTES,
15
- secret_key_bytes: ML_KEM_SECRET_KEY_BYTES,
16
- ciphertext_bytes: ML_KEM_CIPHERTEXT_BYTES,
17
- shared_secret_bytes: ML_KEM_SHARED_SECRET_BYTES,
18
- description: "Pure ML-KEM-768 primitive (FIPS 203).",
9
+ DETAILS = AlgorithmRegistry.details_for_family(:ml_kem).freeze
10
+
11
+ NATIVE_DISPATCH = {
12
+ ml_kem_512: {
13
+ keypair: :native_ml_kem_512_keypair,
14
+ keypair_from_seed: :native_ml_kem_512_keypair_from_seed,
15
+ encapsulate: :native_ml_kem_512_encapsulate,
16
+ decapsulate: :native_ml_kem_512_decapsulate,
17
+ }.freeze,
18
+ ml_kem_768: {
19
+ keypair: :native_ml_kem_keypair,
20
+ keypair_from_seed: :native_ml_kem_keypair_from_seed,
21
+ encapsulate: :native_ml_kem_encapsulate,
22
+ decapsulate: :native_ml_kem_decapsulate,
23
+ }.freeze,
24
+ ml_kem_1024: {
25
+ keypair: :native_ml_kem_1024_keypair,
26
+ keypair_from_seed: :native_ml_kem_1024_keypair_from_seed,
27
+ encapsulate: :native_ml_kem_1024_encapsulate,
28
+ decapsulate: :native_ml_kem_1024_decapsulate,
19
29
  }.freeze,
20
30
  }.freeze
21
31
 
22
32
  class << self
23
33
  def generate(algorithm = CANONICAL_ALGORITHM)
24
34
  algorithm = resolve_algorithm!(algorithm)
25
- public_key, secret_key = PQCrypto.__send__(:native_ml_kem_keypair)
35
+ public_key, secret_key = PQCrypto.__send__(native_method_for(algorithm, :keypair))
26
36
  Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
27
37
  end
28
38
 
@@ -54,6 +64,26 @@ module PQCrypto
54
64
  SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
55
65
  end
56
66
 
67
+ def secret_key_from_pkcs8_der(der)
68
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der))
69
+ end
70
+
71
+ def secret_key_from_pkcs8_pem(pem)
72
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem))
73
+ end
74
+
75
+ def public_key_from_spki_der(der, algorithm: nil)
76
+ resolved_algorithm, bytes = SPKI.decode_der(der)
77
+ validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
78
+ PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
79
+ end
80
+
81
+ def public_key_from_spki_pem(pem, algorithm: nil)
82
+ resolved_algorithm, bytes = SPKI.decode_pem(pem)
83
+ validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
84
+ PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
85
+ end
86
+
57
87
  def details(algorithm)
58
88
  DETAILS.fetch(resolve_algorithm!(algorithm)).dup
59
89
  end
@@ -69,6 +99,37 @@ module PQCrypto
69
99
 
70
100
  raise UnsupportedAlgorithmError, "Unsupported KEM algorithm: #{algorithm.inspect}"
71
101
  end
102
+
103
+ def secret_key_from_decoded_pkcs8(algorithm, format, material)
104
+ secret_material = case format
105
+ when :seed
106
+ _public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
107
+ expanded
108
+ when :both
109
+ _seed, expanded = material
110
+ expanded
111
+ when :expanded
112
+ material
113
+ else
114
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
115
+ end
116
+
117
+ SecretKey.new(resolve_algorithm!(algorithm), secret_material)
118
+ end
119
+
120
+ def native_method_for(algorithm, operation)
121
+ NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
122
+ end
123
+
124
+ def validate_algorithm_match!(expected_algorithm, actual_algorithm)
125
+ expected = resolve_algorithm!(expected_algorithm)
126
+ return if expected == actual_algorithm
127
+
128
+ raise SerializationError,
129
+ "Expected #{expected.inspect}, got #{actual_algorithm.inspect} (SPKI key algorithm mismatch)"
130
+ rescue UnsupportedAlgorithmError => e
131
+ raise SerializationError, e.message
132
+ end
72
133
  end
73
134
 
74
135
  class Keypair
@@ -109,8 +170,16 @@ module PQCrypto
109
170
  Serialization.public_key_to_pqc_container_pem(@algorithm, @bytes)
110
171
  end
111
172
 
173
+ def to_spki_der
174
+ SPKI.encode_der(@algorithm, @bytes)
175
+ end
176
+
177
+ def to_spki_pem
178
+ SPKI.encode_pem(@algorithm, @bytes)
179
+ end
180
+
112
181
  def encapsulate
113
- ciphertext, shared_secret = PQCrypto.__send__(:native_ml_kem_encapsulate, @bytes)
182
+ ciphertext, shared_secret = PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :encapsulate), @bytes)
114
183
  EncapsulationResult.new(ciphertext, shared_secret)
115
184
  rescue ArgumentError => e
116
185
  raise InvalidKeyError, e.message
@@ -165,8 +234,30 @@ module PQCrypto
165
234
  Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
166
235
  end
167
236
 
237
+ def to_pkcs8_der(format: :expanded)
238
+ case format
239
+ when :expanded
240
+ PKCS8.encode_der(@algorithm, @bytes, format: :expanded)
241
+ when :seed, :both
242
+ raise SerializationError, "PKCS#8 #{format.inspect} export from KEM::SecretKey requires original seed material"
243
+ else
244
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
245
+ end
246
+ end
247
+
248
+ def to_pkcs8_pem(format: :expanded)
249
+ case format
250
+ when :expanded
251
+ PKCS8.encode_pem(@algorithm, @bytes, format: :expanded)
252
+ when :seed, :both
253
+ raise SerializationError, "PKCS#8 #{format.inspect} export from KEM::SecretKey requires original seed material"
254
+ else
255
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
256
+ end
257
+ end
258
+
168
259
  def decapsulate(ciphertext)
169
- PQCrypto.__send__(:native_ml_kem_decapsulate, String(ciphertext).b, @bytes)
260
+ PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :decapsulate), String(ciphertext).b, @bytes)
170
261
  rescue ArgumentError => e
171
262
  raise InvalidCiphertextError, e.message
172
263
  end