pq_crypto 0.1.0

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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +37 -0
  3. data/CHANGELOG.md +29 -0
  4. data/GET_STARTED.md +65 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +135 -0
  7. data/SECURITY.md +57 -0
  8. data/ext/pqcrypto/extconf.rb +157 -0
  9. data/ext/pqcrypto/mldsa_api.h +51 -0
  10. data/ext/pqcrypto/mlkem_api.h +21 -0
  11. data/ext/pqcrypto/pqcrypto_ruby_secure.c +889 -0
  12. data/ext/pqcrypto/pqcrypto_secure.c +1178 -0
  13. data/ext/pqcrypto/pqcrypto_secure.h +135 -0
  14. data/ext/pqcrypto/vendor/.vendored +5 -0
  15. data/ext/pqcrypto/vendor/pqclean/common/aes.c +639 -0
  16. data/ext/pqcrypto/vendor/pqclean/common/aes.h +64 -0
  17. data/ext/pqcrypto/vendor/pqclean/common/compat.h +73 -0
  18. data/ext/pqcrypto/vendor/pqclean/common/crypto_declassify.h +7 -0
  19. data/ext/pqcrypto/vendor/pqclean/common/fips202.c +928 -0
  20. data/ext/pqcrypto/vendor/pqclean/common/fips202.h +166 -0
  21. data/ext/pqcrypto/vendor/pqclean/common/keccak2x/feat.S +168 -0
  22. data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.c +684 -0
  23. data/ext/pqcrypto/vendor/pqclean/common/keccak2x/fips202x2.h +60 -0
  24. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SIMD256.c +1028 -0
  25. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SnP.h +50 -0
  26. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-unrolling.macros +198 -0
  27. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile +8 -0
  28. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile.Microsoft_nmake +8 -0
  29. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/SIMD256-config.h +3 -0
  30. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/align.h +34 -0
  31. data/ext/pqcrypto/vendor/pqclean/common/keccak4x/brg_endian.h +142 -0
  32. data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.c +101 -0
  33. data/ext/pqcrypto/vendor/pqclean/common/nistseedexpander.h +39 -0
  34. data/ext/pqcrypto/vendor/pqclean/common/randombytes.c +355 -0
  35. data/ext/pqcrypto/vendor/pqclean/common/randombytes.h +27 -0
  36. data/ext/pqcrypto/vendor/pqclean/common/sha2.c +769 -0
  37. data/ext/pqcrypto/vendor/pqclean/common/sha2.h +173 -0
  38. data/ext/pqcrypto/vendor/pqclean/common/sp800-185.c +156 -0
  39. data/ext/pqcrypto/vendor/pqclean/common/sp800-185.h +27 -0
  40. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/LICENSE +5 -0
  41. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile +19 -0
  42. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile.Microsoft_nmake +23 -0
  43. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/api.h +18 -0
  44. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.c +83 -0
  45. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.h +11 -0
  46. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.c +327 -0
  47. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/indcpa.h +22 -0
  48. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.c +164 -0
  49. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/kem.h +23 -0
  50. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.c +146 -0
  51. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/ntt.h +14 -0
  52. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/params.h +36 -0
  53. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.c +299 -0
  54. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/poly.h +37 -0
  55. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.c +188 -0
  56. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/polyvec.h +26 -0
  57. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.c +41 -0
  58. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/reduce.h +13 -0
  59. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric-shake.c +71 -0
  60. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/symmetric.h +30 -0
  61. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.c +67 -0
  62. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.h +13 -0
  63. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/LICENSE +5 -0
  64. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile +19 -0
  65. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile.Microsoft_nmake +23 -0
  66. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/api.h +50 -0
  67. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.c +98 -0
  68. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.h +10 -0
  69. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.c +261 -0
  70. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.h +31 -0
  71. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/params.h +44 -0
  72. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.c +799 -0
  73. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.h +52 -0
  74. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.c +415 -0
  75. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.h +65 -0
  76. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.c +69 -0
  77. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/reduce.h +17 -0
  78. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.c +92 -0
  79. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/rounding.h +14 -0
  80. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.c +407 -0
  81. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/sign.h +47 -0
  82. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric-shake.c +26 -0
  83. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric.h +34 -0
  84. data/lib/pq_crypto/errors.rb +10 -0
  85. data/lib/pq_crypto/hybrid_kem.rb +106 -0
  86. data/lib/pq_crypto/kem.rb +199 -0
  87. data/lib/pq_crypto/serialization.rb +102 -0
  88. data/lib/pq_crypto/signature.rb +198 -0
  89. data/lib/pq_crypto/version.rb +5 -0
  90. data/lib/pq_crypto.rb +177 -0
  91. data/lib/pqcrypto.rb +3 -0
  92. data/script/vendor_libs.rb +199 -0
  93. metadata +195 -0
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module HybridKEM
5
+ CANONICAL_ALGORITHM = :ml_kem_768_x25519_hkdf_sha256
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 transcript-bound HKDF-SHA256.",
17
+ }.freeze,
18
+ }.freeze
19
+
20
+ class << self
21
+ def generate(algorithm = CANONICAL_ALGORITHM)
22
+ algorithm = resolve_algorithm!(algorithm)
23
+ public_key, secret_key = PQCrypto.__send__(:native_hybrid_kem_keypair)
24
+ Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
25
+ end
26
+
27
+ def public_key_from_bytes(algorithm, bytes)
28
+ PublicKey.new(resolve_algorithm!(algorithm), bytes)
29
+ end
30
+
31
+ def secret_key_from_bytes(algorithm, bytes)
32
+ SecretKey.new(resolve_algorithm!(algorithm), bytes)
33
+ end
34
+
35
+ def public_key_from_pqc_container_der(der, algorithm = nil)
36
+ resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
37
+ PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
38
+ end
39
+
40
+ def public_key_from_pqc_container_pem(pem, algorithm = nil)
41
+ resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_pem(algorithm, pem)
42
+ PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
43
+ end
44
+
45
+ def secret_key_from_pqc_container_der(der, algorithm = nil)
46
+ resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_der(algorithm, der)
47
+ SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
48
+ end
49
+
50
+ def secret_key_from_pqc_container_pem(pem, algorithm = nil)
51
+ resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_pem(algorithm, pem)
52
+ SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
53
+ end
54
+
55
+ def details(algorithm)
56
+ DETAILS.fetch(resolve_algorithm!(algorithm)).dup
57
+ end
58
+
59
+ def supported
60
+ DETAILS.keys.dup
61
+ end
62
+
63
+ private
64
+
65
+ def resolve_algorithm!(algorithm)
66
+ return algorithm if DETAILS.key?(algorithm)
67
+
68
+ raise UnsupportedAlgorithmError, "Unsupported hybrid KEM algorithm: #{algorithm.inspect}"
69
+ end
70
+ end
71
+
72
+ class Keypair < KEM::Keypair; end
73
+ class EncapsulationResult < KEM::EncapsulationResult; end
74
+
75
+ class PublicKey < KEM::PublicKey
76
+ def encapsulate
77
+ ciphertext, shared_secret = PQCrypto.__send__(:native_hybrid_kem_encapsulate, @bytes)
78
+ EncapsulationResult.new(ciphertext, shared_secret)
79
+ rescue ArgumentError => e
80
+ raise InvalidKeyError, e.message
81
+ end
82
+
83
+ private
84
+
85
+ def validate_length!
86
+ expected = HybridKEM.details(@algorithm).fetch(:public_key_bytes)
87
+ raise InvalidKeyError, "Invalid hybrid KEM public key length" unless @bytes.bytesize == expected
88
+ end
89
+ end
90
+
91
+ class SecretKey < KEM::SecretKey
92
+ def decapsulate(ciphertext)
93
+ PQCrypto.__send__(:native_hybrid_kem_decapsulate, String(ciphertext).b, @bytes)
94
+ rescue ArgumentError => e
95
+ raise InvalidCiphertextError, e.message
96
+ end
97
+
98
+ private
99
+
100
+ def validate_length!
101
+ expected = HybridKEM.details(@algorithm).fetch(:secret_key_bytes)
102
+ raise InvalidKeyError, "Invalid hybrid KEM secret key length" unless @bytes.bytesize == expected
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module KEM
5
+ CANONICAL_ALGORITHM = :ml_kem_768
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: ML_KEM_PUBLIC_KEY_BYTES,
13
+ secret_key_bytes: ML_KEM_SECRET_KEY_BYTES,
14
+ ciphertext_bytes: ML_KEM_CIPHERTEXT_BYTES,
15
+ shared_secret_bytes: ML_KEM_SHARED_SECRET_BYTES,
16
+ description: "Pure ML-KEM-768 primitive (FIPS 203).",
17
+ }.freeze,
18
+ }.freeze
19
+
20
+ class << self
21
+ def generate(algorithm = CANONICAL_ALGORITHM)
22
+ algorithm = resolve_algorithm!(algorithm)
23
+ public_key, secret_key = PQCrypto.__send__(:native_ml_kem_keypair)
24
+ Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
25
+ end
26
+
27
+ def public_key_from_bytes(algorithm, bytes)
28
+ PublicKey.new(resolve_algorithm!(algorithm), bytes)
29
+ end
30
+
31
+ def secret_key_from_bytes(algorithm, bytes)
32
+ SecretKey.new(resolve_algorithm!(algorithm), bytes)
33
+ end
34
+
35
+ def public_key_from_pqc_container_der(der, algorithm = nil)
36
+ resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
37
+ PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
38
+ end
39
+
40
+ def public_key_from_pqc_container_pem(pem, algorithm = nil)
41
+ resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_pem(algorithm, pem)
42
+ PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
43
+ end
44
+
45
+ def secret_key_from_pqc_container_der(der, algorithm = nil)
46
+ resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_der(algorithm, der)
47
+ SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
48
+ end
49
+
50
+ def secret_key_from_pqc_container_pem(pem, algorithm = nil)
51
+ resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_pem(algorithm, pem)
52
+ SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
53
+ end
54
+
55
+ def details(algorithm)
56
+ DETAILS.fetch(resolve_algorithm!(algorithm)).dup
57
+ end
58
+
59
+ def supported
60
+ DETAILS.keys.dup
61
+ end
62
+
63
+ private
64
+
65
+ def resolve_algorithm!(algorithm)
66
+ return algorithm if DETAILS.key?(algorithm)
67
+
68
+ raise UnsupportedAlgorithmError, "Unsupported KEM algorithm: #{algorithm.inspect}"
69
+ end
70
+ end
71
+
72
+ class Keypair
73
+ attr_reader :public_key, :secret_key
74
+
75
+ def initialize(public_key, secret_key)
76
+ @public_key = public_key
77
+ @secret_key = secret_key
78
+
79
+ unless @public_key.algorithm == @secret_key.algorithm
80
+ raise InvalidKeyError, "KEM keypair algorithms do not match"
81
+ end
82
+ end
83
+
84
+ def algorithm
85
+ @public_key.algorithm
86
+ end
87
+ end
88
+
89
+ class PublicKey
90
+ attr_reader :algorithm
91
+
92
+ def initialize(algorithm, bytes)
93
+ @algorithm = algorithm
94
+ @bytes = String(bytes).b
95
+ validate_length!
96
+ end
97
+
98
+ def to_bytes
99
+ @bytes.dup
100
+ end
101
+
102
+ def to_pqc_container_der
103
+ Serialization.public_key_to_pqc_container_der(@algorithm, @bytes)
104
+ end
105
+
106
+ def to_pqc_container_pem
107
+ Serialization.public_key_to_pqc_container_pem(@algorithm, @bytes)
108
+ end
109
+
110
+ def encapsulate
111
+ ciphertext, shared_secret = PQCrypto.__send__(:native_ml_kem_encapsulate, @bytes)
112
+ EncapsulationResult.new(ciphertext, shared_secret)
113
+ rescue ArgumentError => e
114
+ raise InvalidKeyError, e.message
115
+ end
116
+
117
+ def encapsulate_to_bytes
118
+ result = encapsulate
119
+ [result.ciphertext, result.shared_secret]
120
+ end
121
+
122
+ def ==(other)
123
+ other.is_a?(PublicKey) && other.algorithm == algorithm && other.to_bytes == @bytes
124
+ end
125
+
126
+ alias eql? ==
127
+
128
+ def hash
129
+ [self.class, algorithm, @bytes].hash
130
+ end
131
+
132
+ private
133
+
134
+ def validate_length!
135
+ expected = KEM.details(@algorithm).fetch(:public_key_bytes)
136
+ raise InvalidKeyError, "Invalid KEM public key length" unless @bytes.bytesize == expected
137
+ end
138
+ end
139
+
140
+ class SecretKey
141
+ attr_reader :algorithm
142
+
143
+ def initialize(algorithm, bytes)
144
+ @algorithm = algorithm
145
+ @bytes = String(bytes).b
146
+ validate_length!
147
+ end
148
+
149
+ def to_bytes
150
+ @bytes.dup
151
+ end
152
+
153
+ def to_pqc_container_der
154
+ Serialization.secret_key_to_pqc_container_der(@algorithm, @bytes)
155
+ end
156
+
157
+ def to_pqc_container_pem
158
+ Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
159
+ end
160
+
161
+ def decapsulate(ciphertext)
162
+ PQCrypto.__send__(:native_ml_kem_decapsulate, String(ciphertext).b, @bytes)
163
+ rescue ArgumentError => e
164
+ raise InvalidCiphertextError, e.message
165
+ end
166
+
167
+ def wipe!
168
+ PQCrypto.secure_wipe(@bytes)
169
+ self
170
+ end
171
+
172
+ def ==(other)
173
+ other.is_a?(SecretKey) && other.algorithm == algorithm && other.to_bytes == @bytes
174
+ end
175
+
176
+ alias eql? ==
177
+
178
+ def hash
179
+ [self.class, algorithm, @bytes].hash
180
+ end
181
+
182
+ private
183
+
184
+ def validate_length!
185
+ expected = KEM.details(@algorithm).fetch(:secret_key_bytes)
186
+ raise InvalidKeyError, "Invalid KEM secret key length" unless @bytes.bytesize == expected
187
+ end
188
+ end
189
+
190
+ class EncapsulationResult
191
+ attr_reader :ciphertext, :shared_secret
192
+
193
+ def initialize(ciphertext, shared_secret)
194
+ @ciphertext = String(ciphertext).b
195
+ @shared_secret = String(shared_secret).b
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module Serialization
5
+ ALGORITHM_METADATA = {
6
+ ml_kem_768: {
7
+ family: :ml_kem,
8
+ oid: "2.25.186599352125448088867056807454444238446",
9
+ }.freeze,
10
+ ml_kem_768_x25519_hkdf_sha256: {
11
+ family: :ml_kem_hybrid,
12
+ oid: "2.25.260242945110721168101139140490528778800",
13
+ }.freeze,
14
+ ml_dsa_65: {
15
+ family: :ml_dsa,
16
+ oid: "2.25.305232938483772195555080795650659207792",
17
+ }.freeze,
18
+ }.freeze
19
+
20
+ class << self
21
+ def algorithm_metadata(algorithm)
22
+ metadata = ALGORITHM_METADATA[algorithm]
23
+ raise SerializationError, "Unsupported serialization algorithm: #{algorithm.inspect}" unless metadata
24
+
25
+ metadata
26
+ end
27
+
28
+ def algorithm_to_oid(algorithm)
29
+ algorithm_metadata(algorithm).fetch(:oid)
30
+ end
31
+
32
+ def algorithm_to_family(algorithm)
33
+ algorithm_metadata(algorithm).fetch(:family)
34
+ end
35
+
36
+ def public_key_to_pqc_container_der(algorithm, bytes)
37
+ PQCrypto.__send__(:native_public_key_to_pqc_container_der, String(algorithm), String(bytes).b)
38
+ rescue ArgumentError, PQCrypto::Error => e
39
+ raise SerializationError, e.message
40
+ end
41
+
42
+ def public_key_to_pqc_container_pem(algorithm, bytes)
43
+ PQCrypto.__send__(:native_public_key_to_pqc_container_pem, String(algorithm), String(bytes).b)
44
+ rescue ArgumentError, PQCrypto::Error => e
45
+ raise SerializationError, e.message
46
+ end
47
+
48
+ def secret_key_to_pqc_container_der(algorithm, bytes)
49
+ PQCrypto.__send__(:native_secret_key_to_pqc_container_der, String(algorithm), String(bytes).b)
50
+ rescue ArgumentError, PQCrypto::Error => e
51
+ raise SerializationError, e.message
52
+ end
53
+
54
+ def secret_key_to_pqc_container_pem(algorithm, bytes)
55
+ PQCrypto.__send__(:native_secret_key_to_pqc_container_pem, String(algorithm), String(bytes).b)
56
+ rescue ArgumentError, PQCrypto::Error => e
57
+ raise SerializationError, e.message
58
+ end
59
+
60
+ def public_key_from_pqc_container_der(expected_algorithm, der)
61
+ algorithm, bytes = PQCrypto.__send__(:native_public_key_from_pqc_container_der, String(der).b)
62
+ validate_algorithm_expectation!(expected_algorithm, algorithm)
63
+ [algorithm, bytes]
64
+ rescue ArgumentError, PQCrypto::Error => e
65
+ raise SerializationError, e.message
66
+ end
67
+
68
+ def public_key_from_pqc_container_pem(expected_algorithm, pem)
69
+ algorithm, bytes = PQCrypto.__send__(:native_public_key_from_pqc_container_pem, String(pem).b)
70
+ validate_algorithm_expectation!(expected_algorithm, algorithm)
71
+ [algorithm, bytes]
72
+ rescue ArgumentError, PQCrypto::Error => e
73
+ raise SerializationError, e.message
74
+ end
75
+
76
+ def secret_key_from_pqc_container_der(expected_algorithm, der)
77
+ algorithm, bytes = PQCrypto.__send__(:native_secret_key_from_pqc_container_der, String(der).b)
78
+ validate_algorithm_expectation!(expected_algorithm, algorithm)
79
+ [algorithm, bytes]
80
+ rescue ArgumentError, PQCrypto::Error => e
81
+ raise SerializationError, e.message
82
+ end
83
+
84
+ def secret_key_from_pqc_container_pem(expected_algorithm, pem)
85
+ algorithm, bytes = PQCrypto.__send__(:native_secret_key_from_pqc_container_pem, String(pem).b)
86
+ validate_algorithm_expectation!(expected_algorithm, algorithm)
87
+ [algorithm, bytes]
88
+ rescue ArgumentError, PQCrypto::Error => e
89
+ raise SerializationError, e.message
90
+ end
91
+
92
+ private
93
+
94
+ def validate_algorithm_expectation!(expected_algorithm, actual_algorithm)
95
+ return if expected_algorithm.nil? || expected_algorithm == actual_algorithm
96
+
97
+ raise SerializationError,
98
+ "Expected #{expected_algorithm.inspect}, got #{actual_algorithm.inspect} (serialized key algorithm mismatch)"
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module Signature
5
+ CANONICAL_ALGORITHM = :ml_dsa_65
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: SIGN_PUBLIC_KEY_BYTES,
13
+ secret_key_bytes: SIGN_SECRET_KEY_BYTES,
14
+ signature_bytes: SIGN_BYTES,
15
+ description: "ML-DSA-65 signature primitive (FIPS 204).",
16
+ }.freeze,
17
+ }.freeze
18
+
19
+ class << self
20
+ def generate(algorithm = CANONICAL_ALGORITHM)
21
+ validate_algorithm!(algorithm)
22
+ public_key, secret_key = PQCrypto.__send__(:native_sign_keypair)
23
+ Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
24
+ end
25
+
26
+ def public_key_from_bytes(algorithm, bytes)
27
+ validate_algorithm!(algorithm)
28
+ PublicKey.new(algorithm, bytes)
29
+ end
30
+
31
+ def secret_key_from_bytes(algorithm, bytes)
32
+ validate_algorithm!(algorithm)
33
+ SecretKey.new(algorithm, bytes)
34
+ end
35
+
36
+ def public_key_from_pqc_container_der(der, algorithm = nil)
37
+ resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
38
+ validate_algorithm!(resolved_algorithm)
39
+ PublicKey.new(resolved_algorithm, bytes)
40
+ end
41
+
42
+ def public_key_from_pqc_container_pem(pem, algorithm = nil)
43
+ resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_pem(algorithm, pem)
44
+ validate_algorithm!(resolved_algorithm)
45
+ PublicKey.new(resolved_algorithm, bytes)
46
+ end
47
+
48
+ def secret_key_from_pqc_container_der(der, algorithm = nil)
49
+ resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_der(algorithm, der)
50
+ validate_algorithm!(resolved_algorithm)
51
+ SecretKey.new(resolved_algorithm, bytes)
52
+ end
53
+
54
+ def secret_key_from_pqc_container_pem(pem, algorithm = nil)
55
+ resolved_algorithm, bytes = Serialization.secret_key_from_pqc_container_pem(algorithm, pem)
56
+ validate_algorithm!(resolved_algorithm)
57
+ SecretKey.new(resolved_algorithm, bytes)
58
+ end
59
+
60
+ def details(algorithm)
61
+ DETAILS.fetch(validate_algorithm!(algorithm)).dup
62
+ end
63
+
64
+ def supported
65
+ DETAILS.keys.dup
66
+ end
67
+
68
+ private
69
+
70
+ def validate_algorithm!(algorithm)
71
+ return algorithm if DETAILS.key?(algorithm)
72
+
73
+ raise UnsupportedAlgorithmError, "Unsupported signature algorithm: #{algorithm.inspect}"
74
+ end
75
+ end
76
+
77
+ class Keypair
78
+ attr_reader :public_key, :secret_key
79
+
80
+ def initialize(public_key, secret_key)
81
+ @public_key = public_key
82
+ @secret_key = secret_key
83
+
84
+ unless @public_key.algorithm == @secret_key.algorithm
85
+ raise InvalidKeyError, "Signature keypair algorithms do not match"
86
+ end
87
+ end
88
+
89
+ def algorithm
90
+ @public_key.algorithm
91
+ end
92
+ end
93
+
94
+ class PublicKey
95
+ attr_reader :algorithm
96
+
97
+ def initialize(algorithm, bytes)
98
+ @algorithm = algorithm
99
+ @bytes = String(bytes).b
100
+ validate_length!
101
+ end
102
+
103
+ def to_bytes
104
+ @bytes.dup
105
+ end
106
+
107
+ def to_pqc_container_der
108
+ Serialization.public_key_to_pqc_container_der(@algorithm, @bytes)
109
+ end
110
+
111
+ def to_pqc_container_pem
112
+ Serialization.public_key_to_pqc_container_pem(@algorithm, @bytes)
113
+ end
114
+
115
+ def verify(message, signature)
116
+ PQCrypto.__send__(:native_verify, String(message).b, String(signature).b, @bytes)
117
+ rescue PQCrypto::VerificationError
118
+ false
119
+ rescue ArgumentError => e
120
+ raise InvalidKeyError, e.message
121
+ end
122
+
123
+ def verify!(message, signature)
124
+ ok = verify(message, signature)
125
+ raise PQCrypto::VerificationError, "Verification failed" unless ok
126
+
127
+ true
128
+ end
129
+
130
+ def ==(other)
131
+ other.is_a?(PublicKey) && other.algorithm == algorithm && other.to_bytes == @bytes
132
+ end
133
+
134
+ alias eql? ==
135
+
136
+ def hash
137
+ [self.class, algorithm, @bytes].hash
138
+ end
139
+
140
+ private
141
+
142
+ def validate_length!
143
+ expected = Signature.details(@algorithm).fetch(:public_key_bytes)
144
+ raise InvalidKeyError, "Invalid signature public key length" unless @bytes.bytesize == expected
145
+ end
146
+ end
147
+
148
+ class SecretKey
149
+ attr_reader :algorithm
150
+
151
+ def initialize(algorithm, bytes)
152
+ @algorithm = algorithm
153
+ @bytes = String(bytes).b
154
+ validate_length!
155
+ end
156
+
157
+ def to_bytes
158
+ @bytes.dup
159
+ end
160
+
161
+ def to_pqc_container_der
162
+ Serialization.secret_key_to_pqc_container_der(@algorithm, @bytes)
163
+ end
164
+
165
+ def to_pqc_container_pem
166
+ Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
167
+ end
168
+
169
+ def sign(message)
170
+ PQCrypto.__send__(:native_sign, String(message).b, @bytes)
171
+ rescue ArgumentError => e
172
+ raise InvalidKeyError, e.message
173
+ end
174
+
175
+ def wipe!
176
+ PQCrypto.secure_wipe(@bytes)
177
+ self
178
+ end
179
+
180
+ def ==(other)
181
+ other.is_a?(SecretKey) && other.algorithm == algorithm && other.to_bytes == @bytes
182
+ end
183
+
184
+ alias eql? ==
185
+
186
+ def hash
187
+ [self.class, algorithm, @bytes].hash
188
+ end
189
+
190
+ private
191
+
192
+ def validate_length!
193
+ expected = Signature.details(@algorithm).fetch(:secret_key_bytes)
194
+ raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ VERSION = "0.1.0"
5
+ end