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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +104 -0
- data/LICENSE +14 -0
- data/LICENSE-APACHE +185 -0
- data/LICENSE-MIT +21 -0
- data/README.md +234 -0
- data/ext/ml_dsa/extconf.rb +47 -0
- data/ext/ml_dsa/fips202.c +933 -0
- data/ext/ml_dsa/fips202.h +166 -0
- data/ext/ml_dsa/ml-dsa-44/clean/api.h +52 -0
- data/ext/ml_dsa/ml-dsa-44/clean/ntt.c +98 -0
- data/ext/ml_dsa/ml-dsa-44/clean/ntt.h +10 -0
- data/ext/ml_dsa/ml-dsa-44/clean/packing.c +261 -0
- data/ext/ml_dsa/ml-dsa-44/clean/packing.h +31 -0
- data/ext/ml_dsa/ml-dsa-44/clean/params.h +44 -0
- data/ext/ml_dsa/ml-dsa-44/clean/poly.c +848 -0
- data/ext/ml_dsa/ml-dsa-44/clean/poly.h +52 -0
- data/ext/ml_dsa/ml-dsa-44/clean/polyvec.c +415 -0
- data/ext/ml_dsa/ml-dsa-44/clean/polyvec.h +65 -0
- data/ext/ml_dsa/ml-dsa-44/clean/reduce.c +69 -0
- data/ext/ml_dsa/ml-dsa-44/clean/reduce.h +17 -0
- data/ext/ml_dsa/ml-dsa-44/clean/rounding.c +98 -0
- data/ext/ml_dsa/ml-dsa-44/clean/rounding.h +14 -0
- data/ext/ml_dsa/ml-dsa-44/clean/sign.c +417 -0
- data/ext/ml_dsa/ml-dsa-44/clean/sign.h +49 -0
- data/ext/ml_dsa/ml-dsa-44/clean/symmetric-shake.c +26 -0
- data/ext/ml_dsa/ml-dsa-44/clean/symmetric.h +34 -0
- data/ext/ml_dsa/ml-dsa-65/clean/api.h +52 -0
- data/ext/ml_dsa/ml-dsa-65/clean/ntt.c +98 -0
- data/ext/ml_dsa/ml-dsa-65/clean/ntt.h +10 -0
- data/ext/ml_dsa/ml-dsa-65/clean/packing.c +261 -0
- data/ext/ml_dsa/ml-dsa-65/clean/packing.h +31 -0
- data/ext/ml_dsa/ml-dsa-65/clean/params.h +44 -0
- data/ext/ml_dsa/ml-dsa-65/clean/poly.c +799 -0
- data/ext/ml_dsa/ml-dsa-65/clean/poly.h +52 -0
- data/ext/ml_dsa/ml-dsa-65/clean/polyvec.c +415 -0
- data/ext/ml_dsa/ml-dsa-65/clean/polyvec.h +65 -0
- data/ext/ml_dsa/ml-dsa-65/clean/reduce.c +69 -0
- data/ext/ml_dsa/ml-dsa-65/clean/reduce.h +17 -0
- data/ext/ml_dsa/ml-dsa-65/clean/rounding.c +92 -0
- data/ext/ml_dsa/ml-dsa-65/clean/rounding.h +14 -0
- data/ext/ml_dsa/ml-dsa-65/clean/sign.c +415 -0
- data/ext/ml_dsa/ml-dsa-65/clean/sign.h +49 -0
- data/ext/ml_dsa/ml-dsa-65/clean/symmetric-shake.c +26 -0
- data/ext/ml_dsa/ml-dsa-65/clean/symmetric.h +34 -0
- data/ext/ml_dsa/ml-dsa-87/clean/api.h +52 -0
- data/ext/ml_dsa/ml-dsa-87/clean/ntt.c +98 -0
- data/ext/ml_dsa/ml-dsa-87/clean/ntt.h +10 -0
- data/ext/ml_dsa/ml-dsa-87/clean/packing.c +261 -0
- data/ext/ml_dsa/ml-dsa-87/clean/packing.h +31 -0
- data/ext/ml_dsa/ml-dsa-87/clean/params.h +44 -0
- data/ext/ml_dsa/ml-dsa-87/clean/poly.c +823 -0
- data/ext/ml_dsa/ml-dsa-87/clean/poly.h +52 -0
- data/ext/ml_dsa/ml-dsa-87/clean/polyvec.c +415 -0
- data/ext/ml_dsa/ml-dsa-87/clean/polyvec.h +65 -0
- data/ext/ml_dsa/ml-dsa-87/clean/reduce.c +69 -0
- data/ext/ml_dsa/ml-dsa-87/clean/reduce.h +17 -0
- data/ext/ml_dsa/ml-dsa-87/clean/rounding.c +92 -0
- data/ext/ml_dsa/ml-dsa-87/clean/rounding.h +14 -0
- data/ext/ml_dsa/ml-dsa-87/clean/sign.c +415 -0
- data/ext/ml_dsa/ml-dsa-87/clean/sign.h +49 -0
- data/ext/ml_dsa/ml-dsa-87/clean/symmetric-shake.c +26 -0
- data/ext/ml_dsa/ml-dsa-87/clean/symmetric.h +34 -0
- data/ext/ml_dsa/ml_dsa_44_impl.c +10 -0
- data/ext/ml_dsa/ml_dsa_65_impl.c +10 -0
- data/ext/ml_dsa/ml_dsa_87_impl.c +10 -0
- data/ext/ml_dsa/ml_dsa_ext.c +1360 -0
- data/ext/ml_dsa/ml_dsa_impl_template.h +35 -0
- data/ext/ml_dsa/ml_dsa_internal.h +188 -0
- data/ext/ml_dsa/randombytes.c +48 -0
- data/ext/ml_dsa/randombytes.h +15 -0
- data/lib/ml_dsa/batch_builder.rb +57 -0
- data/lib/ml_dsa/config.rb +69 -0
- data/lib/ml_dsa/internal.rb +76 -0
- data/lib/ml_dsa/key_pair.rb +39 -0
- data/lib/ml_dsa/parameter_set.rb +89 -0
- data/lib/ml_dsa/public_key.rb +180 -0
- data/lib/ml_dsa/requests.rb +96 -0
- data/lib/ml_dsa/secret_key.rb +221 -0
- data/lib/ml_dsa/version.rb +5 -0
- data/lib/ml_dsa.rb +277 -0
- data/patches/README.md +55 -0
- data/patches/pqclean-explicit-rnd.patch +64 -0
- data/sig/ml_dsa.rbs +178 -0
- data/test/fixtures/kat_vectors.yaml +16 -0
- 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
|