ml_dsa 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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +104 -0
  3. data/LICENSE +14 -0
  4. data/LICENSE-APACHE +185 -0
  5. data/LICENSE-MIT +21 -0
  6. data/README.md +234 -0
  7. data/ext/ml_dsa/extconf.rb +47 -0
  8. data/ext/ml_dsa/fips202.c +933 -0
  9. data/ext/ml_dsa/fips202.h +166 -0
  10. data/ext/ml_dsa/ml-dsa-44/clean/api.h +52 -0
  11. data/ext/ml_dsa/ml-dsa-44/clean/ntt.c +98 -0
  12. data/ext/ml_dsa/ml-dsa-44/clean/ntt.h +10 -0
  13. data/ext/ml_dsa/ml-dsa-44/clean/packing.c +261 -0
  14. data/ext/ml_dsa/ml-dsa-44/clean/packing.h +31 -0
  15. data/ext/ml_dsa/ml-dsa-44/clean/params.h +44 -0
  16. data/ext/ml_dsa/ml-dsa-44/clean/poly.c +848 -0
  17. data/ext/ml_dsa/ml-dsa-44/clean/poly.h +52 -0
  18. data/ext/ml_dsa/ml-dsa-44/clean/polyvec.c +415 -0
  19. data/ext/ml_dsa/ml-dsa-44/clean/polyvec.h +65 -0
  20. data/ext/ml_dsa/ml-dsa-44/clean/reduce.c +69 -0
  21. data/ext/ml_dsa/ml-dsa-44/clean/reduce.h +17 -0
  22. data/ext/ml_dsa/ml-dsa-44/clean/rounding.c +98 -0
  23. data/ext/ml_dsa/ml-dsa-44/clean/rounding.h +14 -0
  24. data/ext/ml_dsa/ml-dsa-44/clean/sign.c +417 -0
  25. data/ext/ml_dsa/ml-dsa-44/clean/sign.h +49 -0
  26. data/ext/ml_dsa/ml-dsa-44/clean/symmetric-shake.c +26 -0
  27. data/ext/ml_dsa/ml-dsa-44/clean/symmetric.h +34 -0
  28. data/ext/ml_dsa/ml-dsa-65/clean/api.h +52 -0
  29. data/ext/ml_dsa/ml-dsa-65/clean/ntt.c +98 -0
  30. data/ext/ml_dsa/ml-dsa-65/clean/ntt.h +10 -0
  31. data/ext/ml_dsa/ml-dsa-65/clean/packing.c +261 -0
  32. data/ext/ml_dsa/ml-dsa-65/clean/packing.h +31 -0
  33. data/ext/ml_dsa/ml-dsa-65/clean/params.h +44 -0
  34. data/ext/ml_dsa/ml-dsa-65/clean/poly.c +799 -0
  35. data/ext/ml_dsa/ml-dsa-65/clean/poly.h +52 -0
  36. data/ext/ml_dsa/ml-dsa-65/clean/polyvec.c +415 -0
  37. data/ext/ml_dsa/ml-dsa-65/clean/polyvec.h +65 -0
  38. data/ext/ml_dsa/ml-dsa-65/clean/reduce.c +69 -0
  39. data/ext/ml_dsa/ml-dsa-65/clean/reduce.h +17 -0
  40. data/ext/ml_dsa/ml-dsa-65/clean/rounding.c +92 -0
  41. data/ext/ml_dsa/ml-dsa-65/clean/rounding.h +14 -0
  42. data/ext/ml_dsa/ml-dsa-65/clean/sign.c +415 -0
  43. data/ext/ml_dsa/ml-dsa-65/clean/sign.h +49 -0
  44. data/ext/ml_dsa/ml-dsa-65/clean/symmetric-shake.c +26 -0
  45. data/ext/ml_dsa/ml-dsa-65/clean/symmetric.h +34 -0
  46. data/ext/ml_dsa/ml-dsa-87/clean/api.h +52 -0
  47. data/ext/ml_dsa/ml-dsa-87/clean/ntt.c +98 -0
  48. data/ext/ml_dsa/ml-dsa-87/clean/ntt.h +10 -0
  49. data/ext/ml_dsa/ml-dsa-87/clean/packing.c +261 -0
  50. data/ext/ml_dsa/ml-dsa-87/clean/packing.h +31 -0
  51. data/ext/ml_dsa/ml-dsa-87/clean/params.h +44 -0
  52. data/ext/ml_dsa/ml-dsa-87/clean/poly.c +823 -0
  53. data/ext/ml_dsa/ml-dsa-87/clean/poly.h +52 -0
  54. data/ext/ml_dsa/ml-dsa-87/clean/polyvec.c +415 -0
  55. data/ext/ml_dsa/ml-dsa-87/clean/polyvec.h +65 -0
  56. data/ext/ml_dsa/ml-dsa-87/clean/reduce.c +69 -0
  57. data/ext/ml_dsa/ml-dsa-87/clean/reduce.h +17 -0
  58. data/ext/ml_dsa/ml-dsa-87/clean/rounding.c +92 -0
  59. data/ext/ml_dsa/ml-dsa-87/clean/rounding.h +14 -0
  60. data/ext/ml_dsa/ml-dsa-87/clean/sign.c +415 -0
  61. data/ext/ml_dsa/ml-dsa-87/clean/sign.h +49 -0
  62. data/ext/ml_dsa/ml-dsa-87/clean/symmetric-shake.c +26 -0
  63. data/ext/ml_dsa/ml-dsa-87/clean/symmetric.h +34 -0
  64. data/ext/ml_dsa/ml_dsa_44_impl.c +10 -0
  65. data/ext/ml_dsa/ml_dsa_65_impl.c +10 -0
  66. data/ext/ml_dsa/ml_dsa_87_impl.c +10 -0
  67. data/ext/ml_dsa/ml_dsa_ext.c +1360 -0
  68. data/ext/ml_dsa/ml_dsa_impl_template.h +35 -0
  69. data/ext/ml_dsa/ml_dsa_internal.h +188 -0
  70. data/ext/ml_dsa/randombytes.c +48 -0
  71. data/ext/ml_dsa/randombytes.h +15 -0
  72. data/lib/ml_dsa/batch_builder.rb +57 -0
  73. data/lib/ml_dsa/config.rb +69 -0
  74. data/lib/ml_dsa/internal.rb +76 -0
  75. data/lib/ml_dsa/key_pair.rb +39 -0
  76. data/lib/ml_dsa/parameter_set.rb +89 -0
  77. data/lib/ml_dsa/public_key.rb +180 -0
  78. data/lib/ml_dsa/requests.rb +96 -0
  79. data/lib/ml_dsa/secret_key.rb +221 -0
  80. data/lib/ml_dsa/version.rb +5 -0
  81. data/lib/ml_dsa.rb +277 -0
  82. data/patches/README.md +55 -0
  83. data/patches/pqclean-explicit-rnd.patch +64 -0
  84. data/sig/ml_dsa.rbs +178 -0
  85. data/test/fixtures/kat_vectors.yaml +16 -0
  86. metadata +194 -0
data/lib/ml_dsa.rb ADDED
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ml_dsa/version"
4
+ require "ml_dsa/ml_dsa_ext"
5
+ require "pqc_asn1"
6
+
7
+ require_relative "ml_dsa/config"
8
+ require_relative "ml_dsa/parameter_set"
9
+ require_relative "ml_dsa/internal"
10
+ require_relative "ml_dsa/requests"
11
+ require_relative "ml_dsa/key_pair"
12
+ require_relative "ml_dsa/public_key"
13
+ require_relative "ml_dsa/secret_key"
14
+ require_relative "ml_dsa/batch_builder"
15
+
16
+ module MlDsa
17
+ # Seed size (bytes) for deterministic key generation.
18
+ SEED_BYTES = 32
19
+
20
+ # The default global Config instance.
21
+ # @return [Config]
22
+ def self.config
23
+ @config
24
+ end
25
+
26
+ @config = Config.new
27
+
28
+ # -----------------------------------------------------------------------
29
+ # Convenience delegators — keep the existing top-level API working
30
+ # -----------------------------------------------------------------------
31
+
32
+ # Subscribe to instrumentation events on the default config.
33
+ # @see Config#subscribe
34
+ def self.subscribe(&block)
35
+ @config.subscribe(&block)
36
+ end
37
+
38
+ # Remove a subscriber from the default config.
39
+ # @see Config#unsubscribe
40
+ def self.unsubscribe(subscriber)
41
+ @config.unsubscribe(subscriber)
42
+ end
43
+
44
+ # @return [Proc, nil] the current random source on the default config
45
+ def self.random_source
46
+ @config.random_source
47
+ end
48
+
49
+ # Set the random source on the default config.
50
+ # @param source [Proc, nil]
51
+ def self.random_source=(source)
52
+ @config.random_source = source
53
+ end
54
+
55
+ # -----------------------------------------------------------------------
56
+ # Module-level API
57
+ # -----------------------------------------------------------------------
58
+
59
+ class << self
60
+ # Generate a key pair for the given parameter set.
61
+ #
62
+ # @param param_set [ParameterSet] ML_DSA_44, ML_DSA_65, or ML_DSA_87
63
+ # @param seed [String, nil] optional 32-byte seed for deterministic keygen
64
+ # @param config [Config] configuration (default: MlDsa.config)
65
+ # @return [KeyPair] frozen key pair (supports destructuring: +pk, sk = keygen(...)+)
66
+ # @raise [TypeError] if param_set is not a ParameterSet
67
+ # @raise [ArgumentError] if seed is not exactly 32 bytes
68
+ def keygen(param_set, seed: nil, config: nil)
69
+ cfg = config || @config
70
+ ps = Internal.resolve_ps(param_set)
71
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
72
+ if seed
73
+ raise TypeError, "seed must be a String" unless seed.is_a?(String)
74
+ seed_bin = seed.b
75
+ raise ArgumentError, "seed must be exactly 32 bytes, got #{seed_bin.bytesize}" unless seed_bin.bytesize == 32
76
+ pk, sk = _keygen_seed(ps.code, seed_bin)
77
+ elsif cfg.random_source
78
+ # Pluggable RNG: generate a seed and use deterministic keygen
79
+ rng_seed = cfg.random_source.call(SEED_BYTES)
80
+ unless rng_seed.is_a?(String) && rng_seed.bytesize == SEED_BYTES
81
+ raise ArgumentError,
82
+ "random_source must return #{SEED_BYTES} bytes, " \
83
+ "got #{rng_seed.is_a?(String) ? rng_seed.bytesize : rng_seed.class}"
84
+ end
85
+ pk, sk = _keygen_seed(ps.code, rng_seed.b)
86
+ else
87
+ pk, sk = _keygen(ps.code)
88
+ end
89
+ now = Time.now.freeze
90
+ pk.instance_variable_set(:@created_at, now)
91
+ sk.instance_variable_set(:@created_at, now)
92
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
93
+ cfg.notify(:keygen, ps, 1, duration)
94
+ KeyPair.new(pk, sk)
95
+ end
96
+
97
+ # Sign multiple messages in a single GVL drop.
98
+ #
99
+ # @param operations [Array<SignRequest>]
100
+ # @param config [Config] configuration (default: MlDsa.config)
101
+ # @param yield_every [Integer] yield to the fiber scheduler every N items
102
+ # during the normalization loop (0 = never, default)
103
+ # @return [Array<String>] frozen array of frozen binary signature strings
104
+ def sign_many(operations, config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY)
105
+ cfg = config || @config
106
+ unless operations.is_a?(Array)
107
+ raise TypeError, "operations must be an Array, got #{operations.class}"
108
+ end
109
+ return [].freeze if operations.empty?
110
+ ops = operations.each_with_index.map do |op, i|
111
+ Internal.maybe_yield(i, yield_every)
112
+ normalize_sign_op(op, i, cfg)
113
+ end
114
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
115
+ result = _sign_many(ops)
116
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
117
+ ps = operations.first&.sk&.param_set
118
+ cfg.notify(:sign, ps, operations.size, duration)
119
+ result
120
+ end
121
+
122
+ # Verify multiple signatures in a single GVL drop.
123
+ #
124
+ # Returns {Result} objects with per-item details: +.ok?+ indicates
125
+ # success, +.reason+ distinguishes wrong-size signatures from
126
+ # cryptographic verification failures.
127
+ #
128
+ # @param operations [Array<VerifyRequest>]
129
+ # @param config [Config] configuration (default: MlDsa.config)
130
+ # @param yield_every [Integer] yield to the fiber scheduler every N items
131
+ # during the normalization loop (0 = never, default)
132
+ # @return [Array<Result>] frozen array of Result objects
133
+ def verify_many(operations, config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY)
134
+ cfg = config || @config
135
+ unless operations.is_a?(Array)
136
+ raise TypeError, "operations must be an Array, got #{operations.class}"
137
+ end
138
+ return [].freeze if operations.empty?
139
+ # Pre-check signature sizes to distinguish size errors from crypto failures
140
+ size_ok = operations.map do |op|
141
+ Internal.resolve_ps(op.pk.param_set)
142
+ op.signature.bytesize == op.pk.param_set.signature_bytes
143
+ end
144
+ ops = operations.each_with_index.map do |op, i|
145
+ Internal.maybe_yield(i, yield_every)
146
+ normalize_verify_op(op, i)
147
+ end
148
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
149
+ bools = _verify_many(ops)
150
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
151
+ ps = operations.first&.pk&.param_set
152
+ cfg.notify(:verify, ps, operations.size, duration)
153
+ results = operations.each_with_index.map do |op, i|
154
+ if bools[i]
155
+ Result.new(value: true, ok: true, reason: nil)
156
+ elsif !size_ok[i]
157
+ expected = op.pk.param_set.signature_bytes
158
+ Result.new(value: false, ok: false,
159
+ reason: "wrong_signature_size: expected #{expected}, got #{op.signature.bytesize}")
160
+ else
161
+ Result.new(value: false, ok: false, reason: "verification_failed")
162
+ end
163
+ end
164
+ results.freeze
165
+ end
166
+
167
+ # Unified batch builder — collects sign/verify ops and executes them
168
+ # in a single GVL drop.
169
+ #
170
+ # @example Batch signing
171
+ # sigs = MlDsa.batch { |b| b.sign(sk: sk, message: msg) }
172
+ #
173
+ # @example Batch verification
174
+ # results = MlDsa.batch { |b| b.verify(pk: pk, message: msg, signature: sig) }
175
+ #
176
+ # @yield [BatchBuilder]
177
+ # @return [Array<String>] for sign batches, [Array<Result>] for verify batches
178
+ # @raise [ArgumentError] if the batch mixes sign and verify operations
179
+ def batch(config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY)
180
+ cfg = config || @config
181
+ builder = BatchBuilder.new
182
+ yield builder
183
+ builder.execute(config: cfg, yield_every: yield_every)
184
+ end
185
+
186
+ private
187
+
188
+ # Returns a flat 5-element array [sk, message, ctx, det, rnd_or_nil].
189
+ # Flat arrays avoid per-item Hash allocation in the Ruby→C boundary.
190
+ # Position constants are in Internal::SIGN_OP_*.
191
+ def normalize_sign_op(op, idx, cfg)
192
+ unless op.is_a?(SignRequest)
193
+ raise TypeError,
194
+ "sign_many: item #{idx} must be a SignRequest, got #{op.class}"
195
+ end
196
+ op.validate!
197
+ rnd = nil
198
+ rng = cfg.random_source
199
+ if !op.deterministic && rng
200
+ rnd = rng.call(Internal::RND_BYTES)
201
+ unless rnd.is_a?(String) && rnd.bytesize == Internal::RND_BYTES
202
+ raise ArgumentError,
203
+ "random_source must return #{Internal::RND_BYTES} bytes, " \
204
+ "got #{rnd.is_a?(String) ? rnd.bytesize : rnd.class}"
205
+ end
206
+ end
207
+ [op.sk, op.message, op.context, op.deterministic, rnd]
208
+ end
209
+
210
+ # Returns a flat 4-element array [pk, message, signature, ctx_or_nil].
211
+ # Position constants are in Internal::VERIFY_OP_*.
212
+ def normalize_verify_op(op, idx)
213
+ unless op.is_a?(VerifyRequest)
214
+ raise TypeError,
215
+ "verify_many: item #{idx} must be a VerifyRequest, got #{op.class}"
216
+ end
217
+ op.validate!
218
+ [op.pk, op.message, op.signature, op.context]
219
+ end
220
+
221
+ # Look up the ParameterSet whose signature size matches +n+ bytes.
222
+ # @param n [Integer] signature byte count
223
+ # @return [ParameterSet]
224
+ # @raise [ArgumentError] if no parameter set matches
225
+ def param_set_for_signature_size(n)
226
+ ps = PARAM_SET_BY_SIG_SIZE[n]
227
+ return ps if ps
228
+ raise ArgumentError, "no ML-DSA parameter set has #{n}-byte signatures"
229
+ end
230
+
231
+ # Look up the ParameterSet whose public key size matches +n+ bytes.
232
+ # @param n [Integer] public key byte count
233
+ # @return [ParameterSet]
234
+ # @raise [ArgumentError] if no parameter set matches
235
+ def param_set_for_pk_size(n)
236
+ ps = PARAM_SET_BY_PK_SIZE[n]
237
+ return ps if ps
238
+ raise ArgumentError, "no ML-DSA parameter set has #{n}-byte public keys"
239
+ end
240
+ end
241
+ end
242
+
243
+ # PQC umbrella namespace for post-quantum cryptographic algorithms.
244
+ #
245
+ # Currently only ML-DSA is implemented; future algorithms (ML-KEM,
246
+ # SLH-DSA, etc.) can register here for unified discovery.
247
+ #
248
+ # @example
249
+ # PQC.algorithms # => { ml_dsa: MlDsa }
250
+ # PQC.algorithm(:ml_dsa) # => MlDsa
251
+ # PQC::MlDsa == ::MlDsa # => true
252
+ module PQC
253
+ @registry = {}
254
+
255
+ # Register a PQC algorithm module under a symbolic name.
256
+ # @param name [Symbol]
257
+ # @param mod [Module]
258
+ def self.register(name, mod)
259
+ @registry[name.to_sym] = mod
260
+ end
261
+
262
+ # List all registered PQC algorithms.
263
+ # @return [Hash{Symbol => Module}]
264
+ def self.algorithms
265
+ @registry.dup
266
+ end
267
+
268
+ # Look up a registered algorithm by name.
269
+ # @param name [Symbol]
270
+ # @return [Module, nil]
271
+ def self.algorithm(name)
272
+ @registry[name.to_sym]
273
+ end
274
+
275
+ MlDsa = ::MlDsa
276
+ register(:ml_dsa, ::MlDsa)
277
+ end
data/patches/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # PQClean Patches
2
+
3
+ This directory tracks every modification made to vendored PQClean sources
4
+ under `ext/ml_dsa/ml-dsa-{44,65,87}/`.
5
+
6
+ ## Why we patch
7
+
8
+ FIPS 204 §3 allows both **hedged** signing (randomised `rnd`) and
9
+ **deterministic** signing (`rnd = 0^{32}`). The upstream PQClean
10
+ `crypto_sign_signature_ctx` function generates `rnd` internally via
11
+ `randombytes`. That couples the randomness source to the signing function and
12
+ makes deterministic testing impossible without mocking.
13
+
14
+ Our patch adds an explicit `const uint8_t *rnd_in` parameter so that:
15
+
16
+ * The **caller** (i.e. `ml_dsa_ext.c`) controls whether signing is hedged or
17
+ deterministic.
18
+ * The C implementation stays free of any Ruby-level state.
19
+ * Deterministic KAT vectors can be reproduced without patching `randombytes`.
20
+
21
+ ## Files modified
22
+
23
+ For each of `ml-dsa-44`, `ml-dsa-65`, `ml-dsa-87`:
24
+
25
+ | File | Change |
26
+ |------|--------|
27
+ | `clean/api.h` | Added `const uint8_t *rnd_in` as last argument to `crypto_sign_signature_ctx` |
28
+ | `clean/sign.h` | Same declaration change |
29
+ | `clean/sign.c` | Implementation: replaced internal `randombytes(rnd, RNDBYTES)` call with `memcpy(rnd, rnd_in, RNDBYTES)` |
30
+
31
+ ## Applying the patch
32
+
33
+ ```sh
34
+ patch -p1 < patches/pqclean-explicit-rnd.patch
35
+ ```
36
+
37
+ Run from the repository root. After applying, recompile with:
38
+
39
+ ```sh
40
+ bundle exec rake compile
41
+ ```
42
+
43
+ ## Verifying the patch matches vendored sources
44
+
45
+ ```sh
46
+ bundle exec rake pqclean:verify
47
+ ```
48
+
49
+ This task diffs the three `sign.c` / `sign.h` / `api.h` files against the
50
+ patch to confirm no untracked drift has occurred.
51
+
52
+ ## PQClean upstream commit
53
+
54
+ Vendored from PQClean commit `main` as of initial gem creation. No other
55
+ changes were made to the PQClean sources.
@@ -0,0 +1,64 @@
1
+ From: ml_dsa gem maintainers
2
+ Subject: [PATCH] Add explicit parameters to PQClean keygen and signing
3
+
4
+ Two patches applied to all three parameter set variants (44, 65, 87):
5
+
6
+ 1. crypto_sign_keypair: add `const uint8_t *seed_in` parameter.
7
+ Replaces the internal `randombytes(seedbuf, SEEDBYTES)` call with
8
+ `memcpy(seedbuf, seed_in, SEEDBYTES)`. The caller generates the
9
+ seed (from OS CSPRNG, pluggable RNG, or user-provided) and passes
10
+ it explicitly. This eliminates the need for thread-local seed
11
+ override hacks in randombytes.c.
12
+
13
+ 2. crypto_sign_signature_ctx: add `const uint8_t *rnd_in` parameter.
14
+ Replaces the internal `randombytes(rnd, RNDBYTES)` call with
15
+ `memcpy(rnd, rnd_in, RNDBYTES)`. The caller passes OS-random bytes
16
+ for hedged signing or zeroes for deterministic signing (FIPS 204 S3).
17
+
18
+ Applied to: ml-dsa-44/clean, ml-dsa-65/clean, ml-dsa-87/clean
19
+ Each variant receives identical structural changes; only the symbol
20
+ prefix (PQCLEAN_MLDSA44_CLEAN, etc.) differs.
21
+
22
+ ---
23
+ ext/ml_dsa/ml-dsa-{44,65,87}/clean/api.h | keygen + sign declarations
24
+ ext/ml_dsa/ml-dsa-{44,65,87}/clean/sign.h | keygen + sign declarations
25
+ ext/ml_dsa/ml-dsa-{44,65,87}/clean/sign.c | keygen + sign implementations
26
+
27
+ For each variant (shown for ml-dsa-44, identical for 65/87):
28
+
29
+ --- a/ext/ml_dsa/ml-dsa-44/clean/api.h
30
+ +++ b/ext/ml_dsa/ml-dsa-44/clean/api.h
31
+ @@ keygen declaration
32
+ -int PQCLEAN_MLDSA44_CLEAN_crypto_sign_keypair(uint8_t *pk, uint8_t *sk);
33
+ +int PQCLEAN_MLDSA44_CLEAN_crypto_sign_keypair(uint8_t *pk, uint8_t *sk,
34
+ + const uint8_t *seed_in);
35
+
36
+ @@ sign_signature_ctx declaration
37
+ int PQCLEAN_MLDSA44_CLEAN_crypto_sign_signature_ctx(uint8_t *sig, size_t *siglen,
38
+ const uint8_t *m, size_t mlen,
39
+ const uint8_t *ctx, size_t ctxlen,
40
+ - const uint8_t *sk);
41
+ + const uint8_t *sk,
42
+ + const uint8_t *rnd_in);
43
+
44
+ --- a/ext/ml_dsa/ml-dsa-44/clean/sign.c
45
+ +++ b/ext/ml_dsa/ml-dsa-44/clean/sign.c
46
+ @@ keygen implementation
47
+ -int PQCLEAN_MLDSA44_CLEAN_crypto_sign_keypair(uint8_t *pk, uint8_t *sk) {
48
+ +int PQCLEAN_MLDSA44_CLEAN_crypto_sign_keypair(uint8_t *pk, uint8_t *sk,
49
+ + const uint8_t *seed_in) {
50
+ /* ... */
51
+ - randombytes(seedbuf, SEEDBYTES);
52
+ + /* Caller provides the seed: OS-random for normal keygen,
53
+ + * or a user-supplied seed for deterministic keygen. */
54
+ + memcpy(seedbuf, seed_in, SEEDBYTES);
55
+
56
+ @@ sign_signature_ctx implementation
57
+ - const uint8_t *sk) {
58
+ + const uint8_t *sk,
59
+ + const uint8_t *rnd_in) {
60
+ /* ... */
61
+ - randombytes(rnd, RNDBYTES);
62
+ + /* rnd_in is provided by the caller: OS-random for hedged signing,
63
+ + * or 0^RNDBYTES for deterministic signing (FIPS 204 S3). */
64
+ + memcpy(rnd, rnd_in, RNDBYTES);
data/sig/ml_dsa.rbs ADDED
@@ -0,0 +1,178 @@
1
+ module MlDsa
2
+ VERSION: String
3
+ SEED_BYTES: Integer
4
+
5
+ ML_DSA_44: ParameterSet
6
+ ML_DSA_65: ParameterSet
7
+ ML_DSA_87: ParameterSet
8
+
9
+ ML_DSA_OIDS: Hash[Integer, String]
10
+ ML_DSA_OID_TO_CODE: Hash[String, Integer]
11
+
12
+ PARAM_SET_BY_PK_SIZE: Hash[Integer, ParameterSet]
13
+ PARAM_SET_BY_SK_SIZE: Hash[Integer, ParameterSet]
14
+ PARAM_SET_BY_SIG_SIZE: Hash[Integer, ParameterSet]
15
+
16
+ def self.config: () -> Config
17
+
18
+ def self.subscribe: () { (Hash[Symbol, untyped]) -> void } -> Proc
19
+ def self.unsubscribe: (Proc subscriber) -> Proc?
20
+ def self.random_source: () -> Proc?
21
+ def self.random_source=: (Proc? source) -> Proc?
22
+
23
+ def self.keygen: (ParameterSet param_set, ?seed: String?, ?config: Config?) -> KeyPair
24
+
25
+ def self.sign_many: (Array[SignRequest] operations, ?config: Config?, ?yield_every: Integer) -> Array[String]
26
+
27
+ def self.verify_many: (Array[VerifyRequest] operations, ?config: Config?, ?yield_every: Integer) -> Array[Result]
28
+
29
+ def self.batch: (?config: Config?, ?yield_every: Integer) { (BatchBuilder) -> void } -> (Array[String] | Array[Result])
30
+
31
+ class Config
32
+ def initialize: () -> void
33
+ def subscribe: () { (Hash[Symbol, untyped]) -> void } -> Proc
34
+ def unsubscribe: (Proc subscriber) -> Proc?
35
+ attr_reader random_source: Proc?
36
+ def random_source=: (Proc? source) -> void
37
+ def notify: (Symbol operation, ParameterSet? param_set, Integer count, Integer duration_ns) -> nil
38
+ end
39
+
40
+ class ParameterSet
41
+ include Comparable
42
+
43
+ attr_reader name: String
44
+ attr_reader code: Integer
45
+ attr_reader security_level: Integer
46
+ attr_reader public_key_bytes: Integer
47
+ attr_reader secret_key_bytes: Integer
48
+ attr_reader signature_bytes: Integer
49
+
50
+ def <=>: (ParameterSet other) -> Integer?
51
+ def to_s: () -> String
52
+ def inspect: () -> String
53
+ end
54
+
55
+ class KeyPair
56
+ attr_reader public_key: PublicKey
57
+ attr_reader secret_key: SecretKey
58
+
59
+ def to_ary: () -> [PublicKey, SecretKey]
60
+ def to_a: () -> [PublicKey, SecretKey]
61
+ def deconstruct: () -> [PublicKey, SecretKey]
62
+ def param_set: () -> ParameterSet
63
+ def inspect: () -> String
64
+ def to_s: () -> String
65
+ end
66
+
67
+ class PublicKey
68
+ def param_set: () -> ParameterSet
69
+ def bytesize: () -> Integer
70
+ def to_bytes: () -> String
71
+ def to_hex: () -> String
72
+ def fingerprint: () -> String
73
+ def to_der: () -> String
74
+ def to_pem: () -> String
75
+ def verify: (String message, String signature, ?context: String) -> bool
76
+ def to_s: () -> String
77
+ def inspect: () -> String
78
+ def ==: (untyped other) -> bool
79
+ def eql?: (untyped other) -> bool
80
+ def hash: () -> Integer
81
+
82
+ attr_reader created_at: Time?
83
+ attr_reader key_usage: Symbol?
84
+ def key_usage=: (Symbol? value) -> Symbol?
85
+
86
+ def self.from_bytes: (String bytes, ?ParameterSet? param_set) -> PublicKey
87
+ def self.from_hex: (String hex, ?ParameterSet? param_set) -> PublicKey
88
+ def self.from_der: (String der) -> PublicKey
89
+ def self.from_pem: (String pem) -> PublicKey
90
+ end
91
+
92
+ class SecretKey
93
+ def param_set: () -> ParameterSet
94
+ def public_key: () -> PublicKey?
95
+ def seed: () -> String?
96
+ def bytesize: () -> Integer
97
+ def with_bytes: [T] () { (String) -> T } -> T
98
+ def wipe!: () -> nil
99
+ def sign: (String message, ?deterministic: bool, ?context: String) -> String
100
+ def to_der: () -> String
101
+ def to_pem: () -> String
102
+ def to_s: () -> String
103
+ def inspect: () -> String
104
+ def ==: (untyped other) -> bool
105
+ def eql?: (untyped other) -> bool
106
+ def hash: () -> Integer
107
+
108
+ attr_reader created_at: Time?
109
+ attr_reader key_usage: Symbol?
110
+ def key_usage=: (Symbol? value) -> Symbol?
111
+
112
+ def self.from_bytes: (String bytes, ?ParameterSet? param_set) -> SecretKey
113
+ def self.from_hex: (String hex, ?ParameterSet? param_set) -> SecretKey
114
+ def self.from_der: (String der) -> SecretKey
115
+ def self.from_pem: (String pem) -> SecretKey
116
+ def self.from_seed: (String seed, ParameterSet param_set) -> SecretKey
117
+ end
118
+
119
+ class SignRequest
120
+ attr_accessor sk: SecretKey
121
+ attr_accessor message: String
122
+ attr_accessor context: String?
123
+ attr_accessor deterministic: bool?
124
+
125
+ def initialize: (?sk: SecretKey, ?message: String, ?context: String?, ?deterministic: bool?) -> void
126
+ def validate!: () -> self
127
+ end
128
+
129
+ class VerifyRequest
130
+ attr_accessor pk: PublicKey
131
+ attr_accessor message: String
132
+ attr_accessor signature: String
133
+ attr_accessor context: String?
134
+
135
+ def initialize: (?pk: PublicKey, ?message: String, ?signature: String, ?context: String?) -> void
136
+ def validate!: () -> self
137
+ end
138
+
139
+ class Result
140
+ attr_reader value: bool
141
+ attr_reader ok: bool
142
+ attr_reader reason: String?
143
+
144
+ def initialize: (?value: bool, ?ok: bool, ?reason: String?) -> void
145
+ def ok?: () -> bool
146
+ def inspect: () -> String
147
+ def to_s: () -> String
148
+ end
149
+
150
+ class BatchBuilder
151
+ def sign: (sk: SecretKey, message: String, ?context: String?, ?deterministic: bool) -> self
152
+ def verify: (pk: PublicKey, message: String, signature: String, ?context: String?) -> self
153
+ def execute: (?config: Config, ?yield_every: Integer) -> (Array[String] | Array[Result])
154
+ end
155
+
156
+ class Error < StandardError
157
+ def reason: () -> String?
158
+ end
159
+
160
+ class Error::KeyGeneration < Error
161
+ end
162
+
163
+ class Error::Signing < Error
164
+ end
165
+
166
+ class Error::Deserialization < Error
167
+ def format: () -> String?
168
+ def position: () -> Integer?
169
+ end
170
+ end
171
+
172
+ module PQC
173
+ MlDsa: Module
174
+
175
+ def self.register: (Symbol name, Module mod) -> void
176
+ def self.algorithms: () -> Hash[Symbol, Module]
177
+ def self.algorithm: (Symbol name) -> Module?
178
+ end