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
|
@@ -0,0 +1,1360 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ml_dsa_ext.c — ML-DSA Ruby C extension (single translation unit).
|
|
3
|
+
*
|
|
4
|
+
* Contains:
|
|
5
|
+
* - Global VALUE/ID definitions
|
|
6
|
+
* - Platform helpers (secure zeroing, mlock, constant-time compare, hex)
|
|
7
|
+
* - Structured error helpers
|
|
8
|
+
* - Dispatch table for ML-DSA parameter sets
|
|
9
|
+
* - TypedData definitions for PublicKey and SecretKey
|
|
10
|
+
* - All key instance methods
|
|
11
|
+
* - Key generation (random + seeded) with GVL release
|
|
12
|
+
* - Batch sign/verify with GVL release
|
|
13
|
+
* - Init_ml_dsa_ext entry point
|
|
14
|
+
*
|
|
15
|
+
* Single-op sign/verify are NOT here — they are thin Ruby wrappers in
|
|
16
|
+
* lib/ml_dsa.rb that call sign_many/verify_many with a single-element
|
|
17
|
+
* array. See ml_dsa_internal.h for design rationale.
|
|
18
|
+
*
|
|
19
|
+
* DER/PEM serialization is implemented in Ruby (lib/ml_dsa.rb) using
|
|
20
|
+
* the pqc_asn1 gem, which provides algorithm-agnostic SPKI/PKCS#8
|
|
21
|
+
* DER/PEM encoding with SecureBuffer for secret key intermediates.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
#include "ml_dsa_internal.h"
|
|
25
|
+
#include <limits.h> /* LONG_MAX */
|
|
26
|
+
#include <stdio.h> /* snprintf */
|
|
27
|
+
|
|
28
|
+
/* ------------------------------------------------------------------ */
|
|
29
|
+
/* Global VALUE/ID definitions */
|
|
30
|
+
/* ------------------------------------------------------------------ */
|
|
31
|
+
|
|
32
|
+
static VALUE rb_mMlDsa;
|
|
33
|
+
static VALUE rb_eMlDsaError;
|
|
34
|
+
static VALUE rb_eKeyGenError;
|
|
35
|
+
static VALUE rb_eSigningError;
|
|
36
|
+
static VALUE rb_eDeserializationError;
|
|
37
|
+
static VALUE rb_cPublicKey;
|
|
38
|
+
static VALUE rb_cSecretKey;
|
|
39
|
+
|
|
40
|
+
static VALUE ml_dsa_param_data_cache = Qundef;
|
|
41
|
+
|
|
42
|
+
static ID id_name;
|
|
43
|
+
static ID id_at_format;
|
|
44
|
+
static ID id_at_position;
|
|
45
|
+
static ID id_at_reason;
|
|
46
|
+
static ID id_public_key;
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* Platform-appropriate secure memory zeroing */
|
|
50
|
+
/* */
|
|
51
|
+
/* The compiler may elide a plain memset() on a buffer that is freed */
|
|
52
|
+
/* immediately after (dead-store elimination). Each platform provides */
|
|
53
|
+
/* a zeroing primitive the compiler is contractually forbidden to */
|
|
54
|
+
/* optimise away. The volatile fallback adds a compiler fence to */
|
|
55
|
+
/* prevent reordering. */
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
|
|
58
|
+
#if defined(_WIN32)
|
|
59
|
+
# include <windows.h>
|
|
60
|
+
static void secure_zero(void *p, size_t n)
|
|
61
|
+
{
|
|
62
|
+
SecureZeroMemory(p, n);
|
|
63
|
+
}
|
|
64
|
+
#elif defined(HAVE_EXPLICIT_BZERO)
|
|
65
|
+
/* Forward-declare to avoid -std=c11 suppressing BSD/POSIX extensions */
|
|
66
|
+
extern void explicit_bzero(void *, size_t);
|
|
67
|
+
static void secure_zero(void *p, size_t n)
|
|
68
|
+
{
|
|
69
|
+
explicit_bzero(p, n);
|
|
70
|
+
}
|
|
71
|
+
#elif defined(HAVE_MEMSET_S) || defined(__STDC_LIB_EXT1__)
|
|
72
|
+
static void secure_zero(void *p, size_t n)
|
|
73
|
+
{
|
|
74
|
+
memset_s(p, n, 0, n);
|
|
75
|
+
}
|
|
76
|
+
#else
|
|
77
|
+
/* Fallback: volatile write + compiler fence to suppress dead-store elim. */
|
|
78
|
+
static void secure_zero(void *p, size_t n)
|
|
79
|
+
{
|
|
80
|
+
volatile unsigned char *vp = (volatile unsigned char *)p;
|
|
81
|
+
size_t i;
|
|
82
|
+
for (i = 0; i < n; i++) vp[i] = 0;
|
|
83
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
84
|
+
__asm__ volatile ("" : : "r"(vp) : "memory");
|
|
85
|
+
#endif
|
|
86
|
+
}
|
|
87
|
+
#endif
|
|
88
|
+
|
|
89
|
+
/* ------------------------------------------------------------------ */
|
|
90
|
+
/* mlock/munlock — prevent secret key pages from being swapped */
|
|
91
|
+
/* ------------------------------------------------------------------ */
|
|
92
|
+
|
|
93
|
+
#if defined(_WIN32)
|
|
94
|
+
static void sk_mlock(void *ptr, size_t len) { VirtualLock(ptr, len); }
|
|
95
|
+
static void sk_munlock(void *ptr, size_t len) { VirtualUnlock(ptr, len); }
|
|
96
|
+
#elif defined(HAVE_MLOCK)
|
|
97
|
+
#include <sys/mman.h>
|
|
98
|
+
static void sk_mlock(void *ptr, size_t len) { mlock(ptr, len); }
|
|
99
|
+
static void sk_munlock(void *ptr, size_t len) { munlock(ptr, len); }
|
|
100
|
+
#else
|
|
101
|
+
static void sk_mlock(void *ptr, size_t len) { (void)ptr; (void)len; }
|
|
102
|
+
static void sk_munlock(void *ptr, size_t len) { (void)ptr; (void)len; }
|
|
103
|
+
#endif
|
|
104
|
+
|
|
105
|
+
/* ------------------------------------------------------------------ */
|
|
106
|
+
/* Constant-time byte comparison with compiler fence */
|
|
107
|
+
/* */
|
|
108
|
+
/* Used for secret key equality — timing must not reveal which bytes */
|
|
109
|
+
/* differ. Public keys use plain memcmp (they are not secret). */
|
|
110
|
+
/* ------------------------------------------------------------------ */
|
|
111
|
+
|
|
112
|
+
static int ct_memeq(const uint8_t *a, const uint8_t *b, size_t n)
|
|
113
|
+
{
|
|
114
|
+
volatile uint8_t diff = 0;
|
|
115
|
+
size_t i;
|
|
116
|
+
for (i = 0; i < n; i++)
|
|
117
|
+
diff |= a[i] ^ b[i];
|
|
118
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
119
|
+
__asm__ volatile ("" : "+r"(diff) : : "memory");
|
|
120
|
+
#endif
|
|
121
|
+
return diff == 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ------------------------------------------------------------------ */
|
|
125
|
+
/* Shared helpers */
|
|
126
|
+
/* ------------------------------------------------------------------ */
|
|
127
|
+
|
|
128
|
+
static VALUE bytes_to_hex_value(const uint8_t *bytes, size_t len)
|
|
129
|
+
{
|
|
130
|
+
static const char hc[] = "0123456789abcdef";
|
|
131
|
+
if (len > LONG_MAX / 2)
|
|
132
|
+
rb_raise(rb_eArgError, "key too large to hex-encode");
|
|
133
|
+
VALUE hex = rb_str_new(NULL, (long)(len * 2));
|
|
134
|
+
char *p = RSTRING_PTR(hex);
|
|
135
|
+
size_t i;
|
|
136
|
+
for (i = 0; i < len; i++) {
|
|
137
|
+
*p++ = hc[(bytes[i] >> 4) & 0xf];
|
|
138
|
+
*p++ = hc[bytes[i] & 0xf];
|
|
139
|
+
}
|
|
140
|
+
OBJ_FREEZE(hex);
|
|
141
|
+
return hex;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static VALUE hash_key_bytes(int ps_code, const uint8_t *bytes, size_t len)
|
|
145
|
+
{
|
|
146
|
+
st_index_t h = rb_hash_start((st_index_t)ps_code);
|
|
147
|
+
h = rb_hash_uint(h, rb_memhash(bytes, (long)len));
|
|
148
|
+
h = rb_hash_end(h);
|
|
149
|
+
return LONG2NUM((long)h);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* Encode a context string to ASCII-8BIT if not already.
|
|
154
|
+
* Context is a plain string (0..255 bytes) per FIPS 204 — no wrapper
|
|
155
|
+
* class needed. Length validation is at the C boundary in batch ops.
|
|
156
|
+
*/
|
|
157
|
+
static VALUE encode_context_binary(VALUE rb_ctx)
|
|
158
|
+
{
|
|
159
|
+
if (rb_enc_get_index(rb_ctx) == rb_ascii8bit_encindex())
|
|
160
|
+
return rb_ctx;
|
|
161
|
+
return rb_str_encode(rb_ctx,
|
|
162
|
+
rb_enc_from_encoding(rb_ascii8bit_encoding()),
|
|
163
|
+
0, Qnil);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ------------------------------------------------------------------ */
|
|
167
|
+
/* Structured error helpers */
|
|
168
|
+
/* */
|
|
169
|
+
/* All MlDsa::Error subclasses carry a machine-readable @reason symbol */
|
|
170
|
+
/* so callers can programmatically distinguish failure types without */
|
|
171
|
+
/* parsing human-readable messages. */
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
|
|
174
|
+
static VALUE error_format(VALUE self) { return rb_ivar_get(self, id_at_format); }
|
|
175
|
+
static VALUE error_position(VALUE self) { return rb_ivar_get(self, id_at_position); }
|
|
176
|
+
static VALUE error_reason(VALUE self) { return rb_ivar_get(self, id_at_reason); }
|
|
177
|
+
|
|
178
|
+
/*
|
|
179
|
+
* Raise an MlDsa::Error subclass with a @reason symbol.
|
|
180
|
+
* Used by keygen, sign, and wipe error paths.
|
|
181
|
+
*/
|
|
182
|
+
NORETURN(static void raise_with_reason(VALUE klass, const char *reason,
|
|
183
|
+
const char *fmt, ...));
|
|
184
|
+
/* Definition repeats NORETURN via compiler attribute so both declaration
|
|
185
|
+
* and definition carry the noreturn contract. */
|
|
186
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
187
|
+
__attribute__((noreturn))
|
|
188
|
+
#elif defined(_MSC_VER)
|
|
189
|
+
__declspec(noreturn)
|
|
190
|
+
#endif
|
|
191
|
+
static void raise_with_reason(VALUE klass, const char *reason,
|
|
192
|
+
const char *fmt, ...)
|
|
193
|
+
{
|
|
194
|
+
va_list ap;
|
|
195
|
+
va_start(ap, fmt);
|
|
196
|
+
VALUE msg = rb_vsprintf(fmt, ap);
|
|
197
|
+
va_end(ap);
|
|
198
|
+
|
|
199
|
+
VALUE exc = rb_exc_new_str(klass, msg);
|
|
200
|
+
rb_ivar_set(exc, id_at_reason, ID2SYM(rb_intern(reason)));
|
|
201
|
+
rb_exc_raise(exc);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Convenience macro — raises MlDsa::Error if key has been wiped.
|
|
205
|
+
* Uses atomic load so a concurrent wipe! on another thread is visible. */
|
|
206
|
+
#define SK_CHECK_WIPED(d) \
|
|
207
|
+
do { if (ML_DSA_ATOMIC_LOAD(&(d)->wiped)) raise_with_reason(rb_eMlDsaError, "key_wiped", \
|
|
208
|
+
"SecretKey has been wiped"); } while (0)
|
|
209
|
+
|
|
210
|
+
/* ------------------------------------------------------------------ */
|
|
211
|
+
/* Dispatch table */
|
|
212
|
+
/* */
|
|
213
|
+
/* Single source of truth for all parameter-set-specific data. */
|
|
214
|
+
/* build_param_data derives Ruby ParameterSet constants from this. */
|
|
215
|
+
/* */
|
|
216
|
+
/* ML-DSA has exactly 3 parameter sets (44, 65, 87) standardized by */
|
|
217
|
+
/* NIST FIPS 204. This set is fixed — a code generator would add */
|
|
218
|
+
/* build complexity for a 3-entry table. The #ifdef guards are */
|
|
219
|
+
/* mechanical but stable. */
|
|
220
|
+
/* ------------------------------------------------------------------ */
|
|
221
|
+
|
|
222
|
+
const ml_dsa_impl_t ML_DSA_IMPLS[] = {
|
|
223
|
+
#ifdef ML_DSA_ENABLE_44
|
|
224
|
+
{ 44, 2,
|
|
225
|
+
PQCLEAN_MLDSA44_CLEAN_crypto_sign_keypair,
|
|
226
|
+
PQCLEAN_MLDSA44_CLEAN_crypto_sign_signature_ctx,
|
|
227
|
+
PQCLEAN_MLDSA44_CLEAN_crypto_sign_verify_ctx,
|
|
228
|
+
PQCLEAN_MLDSA44_CLEAN_CRYPTO_PUBLICKEYBYTES,
|
|
229
|
+
PQCLEAN_MLDSA44_CLEAN_CRYPTO_SECRETKEYBYTES,
|
|
230
|
+
PQCLEAN_MLDSA44_CLEAN_CRYPTO_BYTES },
|
|
231
|
+
#endif
|
|
232
|
+
#ifdef ML_DSA_ENABLE_65
|
|
233
|
+
{ 65, 3,
|
|
234
|
+
PQCLEAN_MLDSA65_CLEAN_crypto_sign_keypair,
|
|
235
|
+
PQCLEAN_MLDSA65_CLEAN_crypto_sign_signature_ctx,
|
|
236
|
+
PQCLEAN_MLDSA65_CLEAN_crypto_sign_verify_ctx,
|
|
237
|
+
PQCLEAN_MLDSA65_CLEAN_CRYPTO_PUBLICKEYBYTES,
|
|
238
|
+
PQCLEAN_MLDSA65_CLEAN_CRYPTO_SECRETKEYBYTES,
|
|
239
|
+
PQCLEAN_MLDSA65_CLEAN_CRYPTO_BYTES },
|
|
240
|
+
#endif
|
|
241
|
+
#ifdef ML_DSA_ENABLE_87
|
|
242
|
+
{ 87, 5,
|
|
243
|
+
PQCLEAN_MLDSA87_CLEAN_crypto_sign_keypair,
|
|
244
|
+
PQCLEAN_MLDSA87_CLEAN_crypto_sign_signature_ctx,
|
|
245
|
+
PQCLEAN_MLDSA87_CLEAN_crypto_sign_verify_ctx,
|
|
246
|
+
PQCLEAN_MLDSA87_CLEAN_CRYPTO_PUBLICKEYBYTES,
|
|
247
|
+
PQCLEAN_MLDSA87_CLEAN_CRYPTO_SECRETKEYBYTES,
|
|
248
|
+
PQCLEAN_MLDSA87_CLEAN_CRYPTO_BYTES },
|
|
249
|
+
#endif
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const size_t ML_DSA_IMPL_COUNT = sizeof(ML_DSA_IMPLS) / sizeof(ML_DSA_IMPLS[0]);
|
|
253
|
+
|
|
254
|
+
static const ml_dsa_impl_t *find_impl(int ps)
|
|
255
|
+
{
|
|
256
|
+
size_t i;
|
|
257
|
+
for (i = 0; i < ML_DSA_IMPL_COUNT; i++)
|
|
258
|
+
if (ML_DSA_IMPLS[i].ps == ps) return &ML_DSA_IMPLS[i];
|
|
259
|
+
rb_raise(rb_eArgError, "invalid or disabled parameter set code: %d", ps);
|
|
260
|
+
UNREACHABLE_RETURN(NULL);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ------------------------------------------------------------------ */
|
|
264
|
+
/* param_set lookup — derives Ruby ParameterSet from int ps_code */
|
|
265
|
+
/* ------------------------------------------------------------------ */
|
|
266
|
+
|
|
267
|
+
static VALUE lookup_param_set(int ps_code)
|
|
268
|
+
{
|
|
269
|
+
char name[16];
|
|
270
|
+
snprintf(name, sizeof(name), "ML_DSA_%d", ps_code);
|
|
271
|
+
return rb_const_get(rb_mMlDsa, rb_intern(name));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ================================================================== */
|
|
275
|
+
/* PublicKey TypedData */
|
|
276
|
+
/* ================================================================== */
|
|
277
|
+
|
|
278
|
+
static void pk_free(void *ptr)
|
|
279
|
+
{
|
|
280
|
+
ruby_xfree(ptr);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static void pk_mark(void *ptr)
|
|
284
|
+
{
|
|
285
|
+
ml_dsa_pk_t *d = (ml_dsa_pk_t *)ptr;
|
|
286
|
+
rb_gc_mark_movable(d->fingerprint);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
static void pk_compact(void *ptr)
|
|
290
|
+
{
|
|
291
|
+
ml_dsa_pk_t *d = (ml_dsa_pk_t *)ptr;
|
|
292
|
+
d->fingerprint = rb_gc_location(d->fingerprint);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
static size_t pk_memsize(const void *ptr)
|
|
296
|
+
{
|
|
297
|
+
const ml_dsa_pk_t *d = (const ml_dsa_pk_t *)ptr;
|
|
298
|
+
return sizeof(ml_dsa_pk_t) + d->len;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
static const rb_data_type_t ml_dsa_pk_type = {
|
|
302
|
+
"MlDsa::PublicKey",
|
|
303
|
+
{ pk_mark, pk_free, pk_memsize, pk_compact, {0} },
|
|
304
|
+
0, 0,
|
|
305
|
+
RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
static VALUE pk_alloc(VALUE klass)
|
|
309
|
+
{
|
|
310
|
+
ml_dsa_pk_t *d = (ml_dsa_pk_t *)ruby_xmalloc(sizeof(ml_dsa_pk_t));
|
|
311
|
+
d->len = 0;
|
|
312
|
+
d->ps_code = 0;
|
|
313
|
+
d->fingerprint = Qnil;
|
|
314
|
+
return TypedData_Wrap_Struct(klass, &ml_dsa_pk_type, d);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static VALUE pk_new_from_buf(VALUE klass, const uint8_t *raw_buf, size_t raw_len,
|
|
318
|
+
int ps_code)
|
|
319
|
+
{
|
|
320
|
+
ml_dsa_pk_t *d = (ml_dsa_pk_t *)ruby_xmalloc(sizeof(ml_dsa_pk_t) + raw_len);
|
|
321
|
+
d->len = raw_len;
|
|
322
|
+
d->ps_code = ps_code;
|
|
323
|
+
d->fingerprint = Qnil;
|
|
324
|
+
memcpy(d->bytes, raw_buf, raw_len);
|
|
325
|
+
return TypedData_Wrap_Struct(klass, &ml_dsa_pk_type, d);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ================================================================== */
|
|
329
|
+
/* SecretKey TypedData */
|
|
330
|
+
/* ================================================================== */
|
|
331
|
+
|
|
332
|
+
static void sk_free(void *ptr)
|
|
333
|
+
{
|
|
334
|
+
ml_dsa_sk_t *d = (ml_dsa_sk_t *)ptr;
|
|
335
|
+
if (!ML_DSA_ATOMIC_LOAD(&d->wiped) && d->len > 0) {
|
|
336
|
+
secure_zero(d->bytes, d->len);
|
|
337
|
+
sk_munlock(d->bytes, d->len);
|
|
338
|
+
}
|
|
339
|
+
if (d->has_seed)
|
|
340
|
+
secure_zero(d->seed, ML_DSA_SEED_BYTES);
|
|
341
|
+
ruby_xfree(d);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
static size_t sk_memsize(const void *ptr)
|
|
345
|
+
{
|
|
346
|
+
const ml_dsa_sk_t *d = (const ml_dsa_sk_t *)ptr;
|
|
347
|
+
return sizeof(ml_dsa_sk_t) + d->len;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
static const rb_data_type_t ml_dsa_sk_type = {
|
|
351
|
+
"MlDsa::SecretKey",
|
|
352
|
+
{ NULL, sk_free, sk_memsize, NULL, {0} },
|
|
353
|
+
0, 0,
|
|
354
|
+
RUBY_TYPED_FREE_IMMEDIATELY
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
static VALUE sk_alloc(VALUE klass)
|
|
358
|
+
{
|
|
359
|
+
ml_dsa_sk_t *d = (ml_dsa_sk_t *)ruby_xmalloc(sizeof(ml_dsa_sk_t));
|
|
360
|
+
d->len = 0;
|
|
361
|
+
d->ps_code = 0;
|
|
362
|
+
d->wiped = 0;
|
|
363
|
+
d->has_seed = 0;
|
|
364
|
+
memset(d->seed, 0, ML_DSA_SEED_BYTES);
|
|
365
|
+
return TypedData_Wrap_Struct(klass, &ml_dsa_sk_type, d);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
static VALUE sk_new_from_buf(VALUE klass, const uint8_t *raw_buf, size_t raw_len,
|
|
369
|
+
int ps_code)
|
|
370
|
+
{
|
|
371
|
+
ml_dsa_sk_t *d = (ml_dsa_sk_t *)ruby_xmalloc(sizeof(ml_dsa_sk_t) + raw_len);
|
|
372
|
+
d->len = raw_len;
|
|
373
|
+
d->ps_code = ps_code;
|
|
374
|
+
d->wiped = 0;
|
|
375
|
+
d->has_seed = 0;
|
|
376
|
+
memset(d->seed, 0, ML_DSA_SEED_BYTES);
|
|
377
|
+
sk_mlock(d->bytes, raw_len);
|
|
378
|
+
memcpy(d->bytes, raw_buf, raw_len);
|
|
379
|
+
return TypedData_Wrap_Struct(klass, &ml_dsa_sk_type, d);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ================================================================== */
|
|
383
|
+
/* PublicKey instance methods */
|
|
384
|
+
/* ================================================================== */
|
|
385
|
+
|
|
386
|
+
static VALUE pk_param_set(VALUE self)
|
|
387
|
+
{
|
|
388
|
+
ml_dsa_pk_t *d;
|
|
389
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
390
|
+
return lookup_param_set(d->ps_code);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
static VALUE pk_bytesize(VALUE self)
|
|
394
|
+
{
|
|
395
|
+
ml_dsa_pk_t *d;
|
|
396
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
397
|
+
return SIZET2NUM(d->len);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
static VALUE pk_to_bytes(VALUE self)
|
|
401
|
+
{
|
|
402
|
+
ml_dsa_pk_t *d;
|
|
403
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
404
|
+
VALUE s = rb_str_new((const char *)d->bytes, (long)d->len);
|
|
405
|
+
rb_enc_associate(s, rb_ascii8bit_encoding());
|
|
406
|
+
OBJ_FREEZE(s);
|
|
407
|
+
return s;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
static VALUE pk_to_hex(VALUE self)
|
|
411
|
+
{
|
|
412
|
+
ml_dsa_pk_t *d;
|
|
413
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
414
|
+
return bytes_to_hex_value(d->bytes, d->len);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/* Lazy-computed fingerprint: first 16 bytes (32 hex chars) of SHA-256
|
|
418
|
+
* of the raw public key bytes. Cached in the C struct. */
|
|
419
|
+
static VALUE pk_fingerprint(VALUE self)
|
|
420
|
+
{
|
|
421
|
+
ml_dsa_pk_t *d;
|
|
422
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
423
|
+
if (!NIL_P(d->fingerprint)) return d->fingerprint;
|
|
424
|
+
|
|
425
|
+
/* rb_require is idempotent; calling it every time is safe but we
|
|
426
|
+
* only get here once per PK object. */
|
|
427
|
+
rb_require("digest/sha2");
|
|
428
|
+
VALUE rb_digest = rb_path2class("Digest::SHA256");
|
|
429
|
+
VALUE raw = rb_str_new((const char *)d->bytes, (long)d->len);
|
|
430
|
+
VALUE hex = rb_funcall(rb_digest, rb_intern("hexdigest"), 1, raw);
|
|
431
|
+
VALUE prefix = rb_str_substr(hex, 0, 32);
|
|
432
|
+
OBJ_FREEZE(prefix);
|
|
433
|
+
RB_OBJ_WRITE(self, &d->fingerprint, prefix);
|
|
434
|
+
return d->fingerprint;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
static VALUE pk_inspect(VALUE self)
|
|
438
|
+
{
|
|
439
|
+
ml_dsa_pk_t *d;
|
|
440
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
441
|
+
if (d->len == 0)
|
|
442
|
+
return rb_str_new_cstr("#<MlDsa::PublicKey [uninitialized]>");
|
|
443
|
+
size_t show = d->len < 8 ? d->len : 8;
|
|
444
|
+
VALUE prefix_hex = bytes_to_hex_value(d->bytes, show);
|
|
445
|
+
VALUE ps = lookup_param_set(d->ps_code);
|
|
446
|
+
VALUE ps_name = rb_funcall(ps, id_name, 0);
|
|
447
|
+
VALUE result = rb_sprintf("#<MlDsa::PublicKey %"PRIsVALUE" %"PRIsVALUE"\xe2\x80\xa6>",
|
|
448
|
+
ps_name, prefix_hex);
|
|
449
|
+
RB_GC_GUARD(prefix_hex);
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static VALUE pk_to_s(VALUE self)
|
|
454
|
+
{
|
|
455
|
+
return pk_inspect(self);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
static VALUE pk_equal(VALUE self, VALUE other)
|
|
459
|
+
{
|
|
460
|
+
if (!rb_obj_is_kind_of(other, rb_cPublicKey)) return Qfalse;
|
|
461
|
+
ml_dsa_pk_t *d1, *d2;
|
|
462
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d1);
|
|
463
|
+
TypedData_Get_Struct(other, ml_dsa_pk_t, &ml_dsa_pk_type, d2);
|
|
464
|
+
if (d1->len != d2->len || d1->ps_code != d2->ps_code) return Qfalse;
|
|
465
|
+
return (memcmp(d1->bytes, d2->bytes, d1->len) == 0) ? Qtrue : Qfalse;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
static VALUE pk_eql(VALUE self, VALUE other)
|
|
469
|
+
{
|
|
470
|
+
return pk_equal(self, other);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
static VALUE pk_hash(VALUE self)
|
|
474
|
+
{
|
|
475
|
+
ml_dsa_pk_t *d;
|
|
476
|
+
TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
|
|
477
|
+
return hash_key_bytes(d->ps_code, d->bytes, d->len);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
static VALUE pk_from_bytes_raw(VALUE klass, VALUE rb_raw, VALUE rb_ps_code)
|
|
481
|
+
{
|
|
482
|
+
Check_Type(rb_raw, T_STRING);
|
|
483
|
+
int ps_code = NUM2INT(rb_ps_code);
|
|
484
|
+
const ml_dsa_impl_t *impl = find_impl(ps_code);
|
|
485
|
+
|
|
486
|
+
if ((size_t)RSTRING_LEN(rb_raw) != impl->pk_len)
|
|
487
|
+
rb_raise(rb_eArgError,
|
|
488
|
+
"expected %lu bytes for ML-DSA-%d, got %ld",
|
|
489
|
+
(unsigned long)impl->pk_len, ps_code, RSTRING_LEN(rb_raw));
|
|
490
|
+
|
|
491
|
+
return pk_new_from_buf(klass,
|
|
492
|
+
(const uint8_t *)RSTRING_PTR(rb_raw),
|
|
493
|
+
(size_t)RSTRING_LEN(rb_raw),
|
|
494
|
+
ps_code);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* dup/clone prevention — alloc creates NULL-bytes objects which silently break */
|
|
498
|
+
static VALUE pk_initialize_copy(VALUE self, VALUE orig)
|
|
499
|
+
{
|
|
500
|
+
(void)self; (void)orig;
|
|
501
|
+
rb_raise(rb_eTypeError,
|
|
502
|
+
"MlDsa::PublicKey cannot be duplicated; "
|
|
503
|
+
"use from_bytes or from_der to create a copy");
|
|
504
|
+
return Qnil;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* Marshal prevention — TypedData has no default marshal support */
|
|
508
|
+
static VALUE pk_dump_data(VALUE self)
|
|
509
|
+
{
|
|
510
|
+
(void)self;
|
|
511
|
+
rb_raise(rb_eTypeError,
|
|
512
|
+
"MlDsa::PublicKey cannot be marshalled; "
|
|
513
|
+
"use to_der/from_der or to_bytes/from_bytes for serialization");
|
|
514
|
+
return Qnil;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* ================================================================== */
|
|
518
|
+
/* SecretKey instance methods */
|
|
519
|
+
/* ================================================================== */
|
|
520
|
+
|
|
521
|
+
static VALUE sk_param_set(VALUE self)
|
|
522
|
+
{
|
|
523
|
+
ml_dsa_sk_t *d;
|
|
524
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
525
|
+
return lookup_param_set(d->ps_code);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/* Returns the associated PublicKey, or nil if the key was deserialized
|
|
529
|
+
* without one (e.g. from_bytes, from_der). */
|
|
530
|
+
static VALUE sk_public_key(VALUE self)
|
|
531
|
+
{
|
|
532
|
+
return rb_ivar_get(self, id_public_key);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/* Internal: set the associated public key after keygen.
|
|
536
|
+
* Called from keygen_body below. */
|
|
537
|
+
static void sk_set_public_key(VALUE sk_obj, VALUE pk_obj)
|
|
538
|
+
{
|
|
539
|
+
rb_ivar_set(sk_obj, id_public_key, pk_obj);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* Internal: store the keygen seed in the SK struct for later retrieval. */
|
|
543
|
+
static void sk_set_seed(VALUE sk_obj, const uint8_t *seed, size_t seed_len)
|
|
544
|
+
{
|
|
545
|
+
ml_dsa_sk_t *d;
|
|
546
|
+
TypedData_Get_Struct(sk_obj, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
547
|
+
if (seed && seed_len == ML_DSA_SEED_BYTES) {
|
|
548
|
+
memcpy(d->seed, seed, ML_DSA_SEED_BYTES);
|
|
549
|
+
d->has_seed = 1;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* Returns the 32-byte keygen seed as a frozen binary String, or nil
|
|
554
|
+
* if the key was not created from a seed (random keygen, from_bytes, etc.). */
|
|
555
|
+
static VALUE sk_seed(VALUE self)
|
|
556
|
+
{
|
|
557
|
+
ml_dsa_sk_t *d;
|
|
558
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
559
|
+
SK_CHECK_WIPED(d);
|
|
560
|
+
if (!d->has_seed) return Qnil;
|
|
561
|
+
VALUE s = rb_str_new((const char *)d->seed, ML_DSA_SEED_BYTES);
|
|
562
|
+
rb_enc_associate(s, rb_ascii8bit_encoding());
|
|
563
|
+
OBJ_FREEZE(s);
|
|
564
|
+
return s;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
static VALUE sk_bytesize(VALUE self)
|
|
568
|
+
{
|
|
569
|
+
ml_dsa_sk_t *d;
|
|
570
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
571
|
+
SK_CHECK_WIPED(d);
|
|
572
|
+
return SIZET2NUM(d->len);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/*
|
|
576
|
+
* with_bytes { |binary_string| ... } -> block_return_value
|
|
577
|
+
*
|
|
578
|
+
* Yields a temporary binary String. The buffer is secure_zero'd before
|
|
579
|
+
* with_bytes returns — guaranteed by rb_ensure even if the block raises.
|
|
580
|
+
*/
|
|
581
|
+
static VALUE sk_with_bytes_yield(VALUE buf)
|
|
582
|
+
{
|
|
583
|
+
return rb_yield(buf);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
static VALUE sk_with_bytes_ensure(VALUE buf)
|
|
587
|
+
{
|
|
588
|
+
/* rb_str_modify ensures exclusive ownership of the byte buffer.
|
|
589
|
+
* Without this, if the block called dup (which uses copy-on-write),
|
|
590
|
+
* secure_zero would write through the shared CoW buffer and corrupt
|
|
591
|
+
* the user's copy. After rb_str_modify, we zero only our private
|
|
592
|
+
* copy; the user's dup retains the original key bytes. */
|
|
593
|
+
rb_str_modify(buf);
|
|
594
|
+
char *ptr = RSTRING_PTR(buf);
|
|
595
|
+
long len = RSTRING_LEN(buf);
|
|
596
|
+
if (ptr && len > 0) secure_zero(ptr, (size_t)len);
|
|
597
|
+
rb_str_resize(buf, 0);
|
|
598
|
+
/* Freeze the emptied buffer so any stored reference fails loudly
|
|
599
|
+
* on mutation attempts rather than silently operating on empty data. */
|
|
600
|
+
OBJ_FREEZE(buf);
|
|
601
|
+
return Qnil;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
static VALUE sk_with_bytes(VALUE self)
|
|
605
|
+
{
|
|
606
|
+
if (!rb_block_given_p())
|
|
607
|
+
rb_raise(rb_eArgError, "with_bytes requires a block");
|
|
608
|
+
ml_dsa_sk_t *d;
|
|
609
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
610
|
+
SK_CHECK_WIPED(d);
|
|
611
|
+
VALUE buf = rb_str_new((const char *)d->bytes, (long)d->len);
|
|
612
|
+
return ML_DSA_ENSURE(sk_with_bytes_yield, sk_with_bytes_ensure, buf);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/*
|
|
616
|
+
* wipe! -> nil
|
|
617
|
+
*
|
|
618
|
+
* Explicitly zeroes and frees the key bytes. Callable even on a frozen
|
|
619
|
+
* object because the bytes live in C-managed memory, not Ruby ivars.
|
|
620
|
+
* After wipe! the key cannot be used for signing; inspect and param_set
|
|
621
|
+
* still work and show [wiped] status.
|
|
622
|
+
*
|
|
623
|
+
* Thread safety: the wiped flag is atomic, so a concurrent wipe! from
|
|
624
|
+
* another thread is visible to SK_CHECK_WIPED immediately. However,
|
|
625
|
+
* if one thread calls wipe! while another is between SK_CHECK_WIPED
|
|
626
|
+
* and the GVL drop in sign, the signing thread has already copied the
|
|
627
|
+
* sk_bytes pointer — secure_zero will zero the bytes under it. This
|
|
628
|
+
* is safe (PQClean reads from the buffer, which is now zeros, and will
|
|
629
|
+
* produce a garbage signature) but callers should still ensure wipe!
|
|
630
|
+
* is called only after all signing threads have finished.
|
|
631
|
+
*/
|
|
632
|
+
static VALUE sk_wipe(VALUE self)
|
|
633
|
+
{
|
|
634
|
+
ml_dsa_sk_t *d;
|
|
635
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
636
|
+
if (!ML_DSA_ATOMIC_LOAD(&d->wiped) && d->len > 0) {
|
|
637
|
+
secure_zero(d->bytes, d->len);
|
|
638
|
+
sk_munlock(d->bytes, d->len);
|
|
639
|
+
if (d->has_seed) {
|
|
640
|
+
secure_zero(d->seed, ML_DSA_SEED_BYTES);
|
|
641
|
+
d->has_seed = 0;
|
|
642
|
+
}
|
|
643
|
+
ML_DSA_ATOMIC_STORE(&d->wiped, 1);
|
|
644
|
+
}
|
|
645
|
+
return Qnil;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
static VALUE sk_inspect(VALUE self)
|
|
649
|
+
{
|
|
650
|
+
ml_dsa_sk_t *d;
|
|
651
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
652
|
+
VALUE ps = lookup_param_set(d->ps_code);
|
|
653
|
+
VALUE ps_name = rb_funcall(ps, id_name, 0);
|
|
654
|
+
if (ML_DSA_ATOMIC_LOAD(&d->wiped))
|
|
655
|
+
return rb_sprintf("#<MlDsa::SecretKey %"PRIsVALUE" [wiped]>", ps_name);
|
|
656
|
+
return rb_sprintf("#<MlDsa::SecretKey %"PRIsVALUE">", ps_name);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
static VALUE sk_to_s(VALUE self)
|
|
660
|
+
{
|
|
661
|
+
return sk_inspect(self);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/* == uses ct_memeq — secret key material must not leak timing */
|
|
665
|
+
static VALUE sk_equal(VALUE self, VALUE other)
|
|
666
|
+
{
|
|
667
|
+
if (!rb_obj_is_kind_of(other, rb_cSecretKey)) return Qfalse;
|
|
668
|
+
ml_dsa_sk_t *d1, *d2;
|
|
669
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d1);
|
|
670
|
+
TypedData_Get_Struct(other, ml_dsa_sk_t, &ml_dsa_sk_type, d2);
|
|
671
|
+
SK_CHECK_WIPED(d1);
|
|
672
|
+
SK_CHECK_WIPED(d2);
|
|
673
|
+
if (d1->len != d2->len || d1->ps_code != d2->ps_code) return Qfalse;
|
|
674
|
+
return ct_memeq(d1->bytes, d2->bytes, d1->len) ? Qtrue : Qfalse;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
static VALUE sk_eql(VALUE self, VALUE other)
|
|
678
|
+
{
|
|
679
|
+
return sk_equal(self, other);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
static VALUE sk_hash(VALUE self)
|
|
683
|
+
{
|
|
684
|
+
ml_dsa_sk_t *d;
|
|
685
|
+
TypedData_Get_Struct(self, ml_dsa_sk_t, &ml_dsa_sk_type, d);
|
|
686
|
+
SK_CHECK_WIPED(d);
|
|
687
|
+
return hash_key_bytes(d->ps_code, d->bytes, d->len);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
static VALUE sk_from_bytes_raw(VALUE klass, VALUE rb_raw, VALUE rb_ps_code)
|
|
691
|
+
{
|
|
692
|
+
Check_Type(rb_raw, T_STRING);
|
|
693
|
+
int ps_code = NUM2INT(rb_ps_code);
|
|
694
|
+
const ml_dsa_impl_t *impl = find_impl(ps_code);
|
|
695
|
+
|
|
696
|
+
if ((size_t)RSTRING_LEN(rb_raw) != impl->sk_len)
|
|
697
|
+
rb_raise(rb_eArgError,
|
|
698
|
+
"expected %lu bytes for ML-DSA-%d, got %ld",
|
|
699
|
+
(unsigned long)impl->sk_len, ps_code, RSTRING_LEN(rb_raw));
|
|
700
|
+
|
|
701
|
+
return sk_new_from_buf(klass,
|
|
702
|
+
(const uint8_t *)RSTRING_PTR(rb_raw),
|
|
703
|
+
(size_t)RSTRING_LEN(rb_raw),
|
|
704
|
+
ps_code);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* dup/clone prevention — would create a NULL-bytes object */
|
|
708
|
+
static VALUE sk_initialize_copy(VALUE self, VALUE orig)
|
|
709
|
+
{
|
|
710
|
+
(void)self; (void)orig;
|
|
711
|
+
rb_raise(rb_eTypeError,
|
|
712
|
+
"MlDsa::SecretKey cannot be duplicated; "
|
|
713
|
+
"use from_bytes or from_der to create a copy");
|
|
714
|
+
return Qnil;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* Marshal prevention — key material must not be silently serialised */
|
|
718
|
+
static VALUE sk_dump_data(VALUE self)
|
|
719
|
+
{
|
|
720
|
+
(void)self;
|
|
721
|
+
rb_raise(rb_eTypeError,
|
|
722
|
+
"MlDsa::SecretKey cannot be marshalled; "
|
|
723
|
+
"use to_der/from_der or with_bytes for serialization");
|
|
724
|
+
return Qnil;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* ================================================================== */
|
|
728
|
+
/* Key generation */
|
|
729
|
+
/* ================================================================== */
|
|
730
|
+
|
|
731
|
+
/* ------------------------------------------------------------------ */
|
|
732
|
+
/* keygen — nogvl callback */
|
|
733
|
+
/* */
|
|
734
|
+
/* Unified path: seed is always passed explicitly to PQClean keygen. */
|
|
735
|
+
/* The caller (Ruby side or C body) generates the seed from OS CSPRNG, */
|
|
736
|
+
/* pluggable RNG, or user-provided bytes before calling this. */
|
|
737
|
+
/* ------------------------------------------------------------------ */
|
|
738
|
+
|
|
739
|
+
struct keygen_nogvl_args {
|
|
740
|
+
keygen_fn_t keygen_fn;
|
|
741
|
+
uint8_t *pk;
|
|
742
|
+
uint8_t *sk;
|
|
743
|
+
const uint8_t *seed;
|
|
744
|
+
int result;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
static void *ml_dsa_keygen_nogvl(void *ptr)
|
|
748
|
+
{
|
|
749
|
+
struct keygen_nogvl_args *a = (struct keygen_nogvl_args *)ptr;
|
|
750
|
+
a->result = a->keygen_fn(a->pk, a->sk, a->seed);
|
|
751
|
+
return NULL;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/* ------------------------------------------------------------------ */
|
|
755
|
+
/* keygen with heap buffers + rb_ensure */
|
|
756
|
+
/* */
|
|
757
|
+
/* Unified keygen: seed is always passed explicitly. For random */
|
|
758
|
+
/* keygen, the seed is generated from OS CSPRNG before the GVL drop. */
|
|
759
|
+
/* For deterministic keygen, the caller provides it. */
|
|
760
|
+
/* */
|
|
761
|
+
/* Both allocations happen INSIDE keygen_body so keygen_ensure covers */
|
|
762
|
+
/* both — no partial-free leak if the second ruby_xmalloc raises OOM. */
|
|
763
|
+
/* ------------------------------------------------------------------ */
|
|
764
|
+
|
|
765
|
+
struct keygen_state {
|
|
766
|
+
const ml_dsa_impl_t *impl;
|
|
767
|
+
int ps;
|
|
768
|
+
int has_seed; /* caller provided seed -> store in SK */
|
|
769
|
+
uint8_t seed[ML_DSA_SEED_BYTES]; /* always filled before nogvl */
|
|
770
|
+
uint8_t *pk_buf; /* NULL until allocated in keygen_body */
|
|
771
|
+
uint8_t *sk_buf; /* NULL until allocated in keygen_body */
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
static VALUE keygen_body(VALUE arg)
|
|
775
|
+
{
|
|
776
|
+
struct keygen_state *s = (struct keygen_state *)arg;
|
|
777
|
+
|
|
778
|
+
/* Allocate inside body so ensure always runs on OOM from either malloc */
|
|
779
|
+
s->pk_buf = (uint8_t *)ruby_xmalloc(s->impl->pk_len);
|
|
780
|
+
s->sk_buf = (uint8_t *)ruby_xmalloc(s->impl->sk_len);
|
|
781
|
+
|
|
782
|
+
struct keygen_nogvl_args nargs;
|
|
783
|
+
nargs.keygen_fn = s->impl->keygen_fn;
|
|
784
|
+
nargs.pk = s->pk_buf;
|
|
785
|
+
nargs.sk = s->sk_buf;
|
|
786
|
+
nargs.seed = s->seed;
|
|
787
|
+
nargs.result = 0;
|
|
788
|
+
|
|
789
|
+
rb_thread_call_without_gvl(ml_dsa_keygen_nogvl, &nargs, RUBY_UBF_IO, NULL);
|
|
790
|
+
|
|
791
|
+
if (nargs.result != 0)
|
|
792
|
+
raise_with_reason(rb_eKeyGenError, "internal_failure",
|
|
793
|
+
"ML-DSA key generation failed");
|
|
794
|
+
|
|
795
|
+
VALUE pk_obj = pk_new_from_buf(rb_cPublicKey, s->pk_buf, s->impl->pk_len,
|
|
796
|
+
s->ps);
|
|
797
|
+
VALUE sk_obj = sk_new_from_buf(rb_cSecretKey, s->sk_buf, s->impl->sk_len,
|
|
798
|
+
s->ps);
|
|
799
|
+
sk_set_public_key(sk_obj, pk_obj);
|
|
800
|
+
if (s->has_seed)
|
|
801
|
+
sk_set_seed(sk_obj, s->seed, ML_DSA_SEED_BYTES);
|
|
802
|
+
VALUE pair = rb_ary_new2(2);
|
|
803
|
+
rb_ary_push(pair, pk_obj);
|
|
804
|
+
rb_ary_push(pair, sk_obj);
|
|
805
|
+
OBJ_FREEZE(pair);
|
|
806
|
+
return pair;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
static VALUE keygen_ensure(VALUE arg)
|
|
810
|
+
{
|
|
811
|
+
struct keygen_state *s = (struct keygen_state *)arg;
|
|
812
|
+
secure_zero(s->seed, ML_DSA_SEED_BYTES);
|
|
813
|
+
if (s->sk_buf) {
|
|
814
|
+
secure_zero(s->sk_buf, s->impl->sk_len);
|
|
815
|
+
ruby_xfree(s->sk_buf);
|
|
816
|
+
s->sk_buf = NULL;
|
|
817
|
+
}
|
|
818
|
+
if (s->pk_buf) {
|
|
819
|
+
ruby_xfree(s->pk_buf); /* public key — no secure zeroing needed */
|
|
820
|
+
s->pk_buf = NULL;
|
|
821
|
+
}
|
|
822
|
+
return Qnil;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
static VALUE rb_ml_dsa_keygen(VALUE self, VALUE rb_ps)
|
|
826
|
+
{
|
|
827
|
+
(void)self;
|
|
828
|
+
int ps = NUM2INT(rb_ps);
|
|
829
|
+
const ml_dsa_impl_t *impl = find_impl(ps);
|
|
830
|
+
|
|
831
|
+
struct keygen_state s;
|
|
832
|
+
s.impl = impl;
|
|
833
|
+
s.ps = ps;
|
|
834
|
+
s.has_seed = 0;
|
|
835
|
+
s.pk_buf = NULL;
|
|
836
|
+
s.sk_buf = NULL;
|
|
837
|
+
|
|
838
|
+
/* Generate seed from OS CSPRNG before GVL drop */
|
|
839
|
+
randombytes(s.seed, ML_DSA_SEED_BYTES);
|
|
840
|
+
|
|
841
|
+
return ML_DSA_ENSURE(keygen_body, keygen_ensure, &s);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/* ------------------------------------------------------------------ */
|
|
845
|
+
/* keygen_from_seed — deterministic keygen with caller-provided seed */
|
|
846
|
+
/* ------------------------------------------------------------------ */
|
|
847
|
+
|
|
848
|
+
static VALUE rb_ml_dsa_keygen_seed(VALUE self, VALUE rb_ps, VALUE rb_seed)
|
|
849
|
+
{
|
|
850
|
+
(void)self;
|
|
851
|
+
Check_Type(rb_seed, T_STRING);
|
|
852
|
+
if (RSTRING_LEN(rb_seed) != ML_DSA_SEED_BYTES)
|
|
853
|
+
rb_raise(rb_eArgError, "seed must be exactly %d bytes, got %ld",
|
|
854
|
+
ML_DSA_SEED_BYTES, RSTRING_LEN(rb_seed));
|
|
855
|
+
|
|
856
|
+
int ps = NUM2INT(rb_ps);
|
|
857
|
+
const ml_dsa_impl_t *impl = find_impl(ps);
|
|
858
|
+
|
|
859
|
+
struct keygen_state s;
|
|
860
|
+
s.impl = impl;
|
|
861
|
+
s.ps = ps;
|
|
862
|
+
s.has_seed = 1;
|
|
863
|
+
s.pk_buf = NULL;
|
|
864
|
+
s.sk_buf = NULL;
|
|
865
|
+
memcpy(s.seed, RSTRING_PTR(rb_seed), ML_DSA_SEED_BYTES);
|
|
866
|
+
|
|
867
|
+
return ML_DSA_ENSURE(keygen_body, keygen_ensure, &s);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* ================================================================== */
|
|
871
|
+
/* Batch signing */
|
|
872
|
+
/* */
|
|
873
|
+
/* MlDsa._sign_many(array) -> Array of frozen binary Strings */
|
|
874
|
+
/* */
|
|
875
|
+
/* Each element of `array` is a flat 5-element Array: */
|
|
876
|
+
/* [sk, message, ctx_or_nil, deterministic_or_nil, rnd_or_nil] */
|
|
877
|
+
/* */
|
|
878
|
+
/* This is the ONLY C codepath for signing. Single-op sk.sign is a */
|
|
879
|
+
/* Ruby wrapper that calls sign_many with a 1-element array. */
|
|
880
|
+
/* ================================================================== */
|
|
881
|
+
|
|
882
|
+
struct sign_batch_item {
|
|
883
|
+
sign_fn_t sign_fn;
|
|
884
|
+
const uint8_t *sk_bytes;
|
|
885
|
+
const uint8_t *m;
|
|
886
|
+
size_t mlen;
|
|
887
|
+
const uint8_t *ctx;
|
|
888
|
+
size_t ctxlen;
|
|
889
|
+
VALUE msg_pinned;
|
|
890
|
+
VALUE ctx_pinned;
|
|
891
|
+
uint8_t rnd[ML_DSA_RNDBYTES];
|
|
892
|
+
uint8_t *sig_buf;
|
|
893
|
+
size_t sig_max;
|
|
894
|
+
size_t siglen;
|
|
895
|
+
int result;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
struct sign_batch_state {
|
|
899
|
+
VALUE rb_ops;
|
|
900
|
+
struct sign_batch_item *items;
|
|
901
|
+
size_t count;
|
|
902
|
+
size_t items_initialized; /* items with rnd set */
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
struct sign_batch_nogvl_args {
|
|
906
|
+
struct sign_batch_item *items;
|
|
907
|
+
size_t count;
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
static void *ml_dsa_sign_batch_nogvl(void *ptr)
|
|
911
|
+
{
|
|
912
|
+
struct sign_batch_nogvl_args *a = (struct sign_batch_nogvl_args *)ptr;
|
|
913
|
+
size_t i;
|
|
914
|
+
for (i = 0; i < a->count; i++) {
|
|
915
|
+
struct sign_batch_item *it = &a->items[i];
|
|
916
|
+
it->result = it->sign_fn(it->sig_buf, &it->siglen,
|
|
917
|
+
it->m, it->mlen,
|
|
918
|
+
it->ctx, it->ctxlen,
|
|
919
|
+
it->sk_bytes, it->rnd);
|
|
920
|
+
}
|
|
921
|
+
return NULL;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
static VALUE sign_many_body(VALUE arg)
|
|
925
|
+
{
|
|
926
|
+
struct sign_batch_state *s = (struct sign_batch_state *)arg;
|
|
927
|
+
size_t i;
|
|
928
|
+
|
|
929
|
+
s->items = (struct sign_batch_item *)ruby_xcalloc(
|
|
930
|
+
s->count, sizeof(struct sign_batch_item));
|
|
931
|
+
|
|
932
|
+
for (i = 0; i < s->count; i++) {
|
|
933
|
+
/* Each op is a flat array: [sk, message, ctx, det, rnd_or_nil] */
|
|
934
|
+
VALUE op = RARRAY_AREF(s->rb_ops, (long)i);
|
|
935
|
+
Check_Type(op, T_ARRAY);
|
|
936
|
+
|
|
937
|
+
VALUE rb_sk = RARRAY_AREF(op, SIGN_OP_SK);
|
|
938
|
+
VALUE rb_msg = RARRAY_AREF(op, SIGN_OP_MSG);
|
|
939
|
+
VALUE rb_ctx_raw = RARRAY_AREF(op, SIGN_OP_CTX); /* may be nil */
|
|
940
|
+
VALUE rb_det = RARRAY_AREF(op, SIGN_OP_DET); /* may be nil */
|
|
941
|
+
/* Optional pre-generated rnd bytes (from pluggable RNG) */
|
|
942
|
+
VALUE rb_rnd = (RARRAY_LEN(op) > SIGN_OP_RND)
|
|
943
|
+
? RARRAY_AREF(op, SIGN_OP_RND) : Qnil;
|
|
944
|
+
|
|
945
|
+
if (!rb_obj_is_kind_of(rb_sk, rb_cSecretKey))
|
|
946
|
+
rb_raise(rb_eTypeError,
|
|
947
|
+
"_sign_many: item %lu sk must be a MlDsa::SecretKey", (unsigned long)i);
|
|
948
|
+
ml_dsa_sk_t *sk_d;
|
|
949
|
+
TypedData_Get_Struct(rb_sk, ml_dsa_sk_t, &ml_dsa_sk_type, sk_d);
|
|
950
|
+
SK_CHECK_WIPED(sk_d);
|
|
951
|
+
|
|
952
|
+
Check_Type(rb_msg, T_STRING);
|
|
953
|
+
|
|
954
|
+
VALUE rb_ctx = NIL_P(rb_ctx_raw) ? rb_str_new("", 0) : rb_ctx_raw;
|
|
955
|
+
Check_Type(rb_ctx, T_STRING);
|
|
956
|
+
if (RSTRING_LEN(rb_ctx) > 255)
|
|
957
|
+
rb_raise(rb_eArgError,
|
|
958
|
+
"_sign_many: item %lu context must not exceed 255 bytes", (unsigned long)i);
|
|
959
|
+
|
|
960
|
+
const ml_dsa_impl_t *impl = find_impl(sk_d->ps_code);
|
|
961
|
+
VALUE ctx_b = encode_context_binary(rb_ctx);
|
|
962
|
+
|
|
963
|
+
struct sign_batch_item *it = &s->items[i];
|
|
964
|
+
it->sign_fn = impl->sign_fn;
|
|
965
|
+
it->sk_bytes = sk_d->bytes;
|
|
966
|
+
it->msg_pinned = rb_str_new_frozen(rb_msg);
|
|
967
|
+
it->ctx_pinned = rb_str_new_frozen(ctx_b);
|
|
968
|
+
it->m = (const uint8_t *)RSTRING_PTR(it->msg_pinned);
|
|
969
|
+
it->mlen = (size_t)RSTRING_LEN(it->msg_pinned);
|
|
970
|
+
it->ctx = (const uint8_t *)RSTRING_PTR(it->ctx_pinned);
|
|
971
|
+
it->ctxlen = (size_t)RSTRING_LEN(it->ctx_pinned);
|
|
972
|
+
it->sig_buf = (uint8_t *)ruby_xmalloc(impl->sig_len);
|
|
973
|
+
it->sig_max = impl->sig_len;
|
|
974
|
+
it->siglen = impl->sig_len;
|
|
975
|
+
|
|
976
|
+
if (RTEST(rb_det)) {
|
|
977
|
+
memset(it->rnd, 0, ML_DSA_RNDBYTES);
|
|
978
|
+
} else if (!NIL_P(rb_rnd) && RB_TYPE_P(rb_rnd, T_STRING)
|
|
979
|
+
&& RSTRING_LEN(rb_rnd) == ML_DSA_RNDBYTES) {
|
|
980
|
+
/* Use pre-generated rnd bytes from the pluggable RNG */
|
|
981
|
+
memcpy(it->rnd, RSTRING_PTR(rb_rnd), ML_DSA_RNDBYTES);
|
|
982
|
+
} else {
|
|
983
|
+
randombytes(it->rnd, ML_DSA_RNDBYTES);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
s->items_initialized = i + 1; /* rnd is now set for item i */
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
struct sign_batch_nogvl_args nargs;
|
|
990
|
+
nargs.items = s->items;
|
|
991
|
+
nargs.count = s->count;
|
|
992
|
+
rb_thread_call_without_gvl(ml_dsa_sign_batch_nogvl, &nargs,
|
|
993
|
+
RUBY_UBF_IO, NULL);
|
|
994
|
+
|
|
995
|
+
VALUE out = rb_ary_new2((long)s->count);
|
|
996
|
+
for (i = 0; i < s->count; i++) {
|
|
997
|
+
struct sign_batch_item *it = &s->items[i];
|
|
998
|
+
if (it->result != 0)
|
|
999
|
+
raise_with_reason(rb_eSigningError, "internal_failure",
|
|
1000
|
+
"ML-DSA signing failed for batch item %lu",
|
|
1001
|
+
(unsigned long)i);
|
|
1002
|
+
VALUE sig = rb_str_new((const char *)it->sig_buf, (long)it->siglen);
|
|
1003
|
+
rb_enc_associate(sig, rb_ascii8bit_encoding());
|
|
1004
|
+
OBJ_FREEZE(sig);
|
|
1005
|
+
rb_ary_push(out, sig);
|
|
1006
|
+
}
|
|
1007
|
+
OBJ_FREEZE(out);
|
|
1008
|
+
|
|
1009
|
+
/* GC guards on the body's stack frame — where the pinned VALUEs are live */
|
|
1010
|
+
for (i = 0; i < s->count; i++) {
|
|
1011
|
+
RB_GC_GUARD(s->items[i].msg_pinned);
|
|
1012
|
+
RB_GC_GUARD(s->items[i].ctx_pinned);
|
|
1013
|
+
}
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
static VALUE sign_many_ensure(VALUE arg)
|
|
1018
|
+
{
|
|
1019
|
+
struct sign_batch_state *s = (struct sign_batch_state *)arg;
|
|
1020
|
+
size_t i;
|
|
1021
|
+
/* Only wipe rnd and free sig_buf for items that were actually initialized */
|
|
1022
|
+
for (i = 0; i < s->items_initialized; i++) {
|
|
1023
|
+
secure_zero(s->items[i].rnd, ML_DSA_RNDBYTES);
|
|
1024
|
+
if (s->items[i].sig_buf) {
|
|
1025
|
+
secure_zero(s->items[i].sig_buf, s->items[i].sig_max);
|
|
1026
|
+
ruby_xfree(s->items[i].sig_buf);
|
|
1027
|
+
s->items[i].sig_buf = NULL;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (s->items) {
|
|
1031
|
+
ruby_xfree(s->items);
|
|
1032
|
+
s->items = NULL;
|
|
1033
|
+
}
|
|
1034
|
+
return Qnil;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
static VALUE rb_ml_dsa_sign_many(VALUE self, VALUE rb_ops)
|
|
1038
|
+
{
|
|
1039
|
+
(void)self;
|
|
1040
|
+
Check_Type(rb_ops, T_ARRAY);
|
|
1041
|
+
long count = RARRAY_LEN(rb_ops);
|
|
1042
|
+
if (count == 0) {
|
|
1043
|
+
VALUE empty = rb_ary_new();
|
|
1044
|
+
OBJ_FREEZE(empty);
|
|
1045
|
+
return empty;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
struct sign_batch_state s;
|
|
1049
|
+
s.rb_ops = rb_ops;
|
|
1050
|
+
s.count = (size_t)count;
|
|
1051
|
+
s.items_initialized = 0;
|
|
1052
|
+
s.items = NULL;
|
|
1053
|
+
|
|
1054
|
+
return ML_DSA_ENSURE(sign_many_body, sign_many_ensure, &s);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/* ================================================================== */
|
|
1058
|
+
/* Batch verification */
|
|
1059
|
+
/* */
|
|
1060
|
+
/* MlDsa._verify_many(array) -> Array of true|false */
|
|
1061
|
+
/* */
|
|
1062
|
+
/* Each element is a flat 4-element Array: */
|
|
1063
|
+
/* [pk, message, signature, ctx_or_nil] */
|
|
1064
|
+
/* */
|
|
1065
|
+
/* This is the ONLY C codepath for verification. Single-op pk.verify */
|
|
1066
|
+
/* is a Ruby wrapper that calls verify_many with a 1-element array. */
|
|
1067
|
+
/* ================================================================== */
|
|
1068
|
+
|
|
1069
|
+
struct verify_batch_item {
|
|
1070
|
+
verify_fn_t verify_fn;
|
|
1071
|
+
const uint8_t *pk_bytes;
|
|
1072
|
+
const uint8_t *sig;
|
|
1073
|
+
size_t siglen;
|
|
1074
|
+
const uint8_t *m;
|
|
1075
|
+
size_t mlen;
|
|
1076
|
+
const uint8_t *ctx;
|
|
1077
|
+
size_t ctxlen;
|
|
1078
|
+
VALUE msg_pinned;
|
|
1079
|
+
VALUE sig_pinned;
|
|
1080
|
+
VALUE ctx_pinned;
|
|
1081
|
+
int result;
|
|
1082
|
+
int size_ok; /* 0 = bad size, skip nogvl */
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
struct verify_batch_state {
|
|
1086
|
+
VALUE rb_ops;
|
|
1087
|
+
struct verify_batch_item *items;
|
|
1088
|
+
size_t count;
|
|
1089
|
+
size_t items_initialized;
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
struct verify_batch_nogvl_args {
|
|
1093
|
+
struct verify_batch_item *items;
|
|
1094
|
+
size_t count;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
static void *ml_dsa_verify_batch_nogvl(void *ptr)
|
|
1098
|
+
{
|
|
1099
|
+
struct verify_batch_nogvl_args *a = (struct verify_batch_nogvl_args *)ptr;
|
|
1100
|
+
size_t i;
|
|
1101
|
+
for (i = 0; i < a->count; i++) {
|
|
1102
|
+
struct verify_batch_item *it = &a->items[i];
|
|
1103
|
+
if (!it->size_ok) {
|
|
1104
|
+
it->result = -1; /* will map to false */
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
it->result = it->verify_fn(it->sig, it->siglen,
|
|
1108
|
+
it->m, it->mlen,
|
|
1109
|
+
it->ctx, it->ctxlen,
|
|
1110
|
+
it->pk_bytes);
|
|
1111
|
+
}
|
|
1112
|
+
return NULL;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
static VALUE verify_many_body(VALUE arg)
|
|
1116
|
+
{
|
|
1117
|
+
struct verify_batch_state *s = (struct verify_batch_state *)arg;
|
|
1118
|
+
size_t i;
|
|
1119
|
+
|
|
1120
|
+
s->items = (struct verify_batch_item *)ruby_xcalloc(
|
|
1121
|
+
s->count, sizeof(struct verify_batch_item));
|
|
1122
|
+
|
|
1123
|
+
for (i = 0; i < s->count; i++) {
|
|
1124
|
+
/* Each op is a flat 4-element array: [pk, message, signature, ctx] */
|
|
1125
|
+
VALUE op = RARRAY_AREF(s->rb_ops, (long)i);
|
|
1126
|
+
Check_Type(op, T_ARRAY);
|
|
1127
|
+
|
|
1128
|
+
VALUE rb_pk = RARRAY_AREF(op, VERIFY_OP_PK);
|
|
1129
|
+
VALUE rb_msg = RARRAY_AREF(op, VERIFY_OP_MSG);
|
|
1130
|
+
VALUE rb_sig = RARRAY_AREF(op, VERIFY_OP_SIG);
|
|
1131
|
+
VALUE rb_ctx_raw = RARRAY_AREF(op, VERIFY_OP_CTX); /* may be nil */
|
|
1132
|
+
|
|
1133
|
+
if (!rb_obj_is_kind_of(rb_pk, rb_cPublicKey))
|
|
1134
|
+
rb_raise(rb_eTypeError,
|
|
1135
|
+
"_verify_many: item %lu pk must be a MlDsa::PublicKey", (unsigned long)i);
|
|
1136
|
+
ml_dsa_pk_t *pk_d;
|
|
1137
|
+
TypedData_Get_Struct(rb_pk, ml_dsa_pk_t, &ml_dsa_pk_type, pk_d);
|
|
1138
|
+
|
|
1139
|
+
if (!RB_TYPE_P(rb_msg, T_STRING))
|
|
1140
|
+
rb_raise(rb_eTypeError,
|
|
1141
|
+
"_verify_many: item %lu message must be a String", (unsigned long)i);
|
|
1142
|
+
if (!RB_TYPE_P(rb_sig, T_STRING))
|
|
1143
|
+
rb_raise(rb_eTypeError,
|
|
1144
|
+
"_verify_many: item %lu signature must be a String", (unsigned long)i);
|
|
1145
|
+
|
|
1146
|
+
VALUE rb_ctx = NIL_P(rb_ctx_raw) ? rb_str_new("", 0) : rb_ctx_raw;
|
|
1147
|
+
if (!RB_TYPE_P(rb_ctx, T_STRING) || RSTRING_LEN(rb_ctx) > 255)
|
|
1148
|
+
rb_raise(rb_eArgError,
|
|
1149
|
+
"_verify_many: item %lu context must be a String <= 255 bytes", (unsigned long)i);
|
|
1150
|
+
|
|
1151
|
+
const ml_dsa_impl_t *impl = find_impl(pk_d->ps_code);
|
|
1152
|
+
VALUE ctx_b = encode_context_binary(rb_ctx);
|
|
1153
|
+
|
|
1154
|
+
struct verify_batch_item *it = &s->items[i];
|
|
1155
|
+
it->verify_fn = impl->verify_fn;
|
|
1156
|
+
it->pk_bytes = pk_d->bytes;
|
|
1157
|
+
it->msg_pinned = rb_str_new_frozen(rb_msg);
|
|
1158
|
+
it->sig_pinned = rb_str_new_frozen(rb_sig);
|
|
1159
|
+
it->ctx_pinned = rb_str_new_frozen(ctx_b);
|
|
1160
|
+
it->m = (const uint8_t *)RSTRING_PTR(it->msg_pinned);
|
|
1161
|
+
it->mlen = (size_t)RSTRING_LEN(it->msg_pinned);
|
|
1162
|
+
it->sig = (const uint8_t *)RSTRING_PTR(it->sig_pinned);
|
|
1163
|
+
it->siglen = (size_t)RSTRING_LEN(it->sig_pinned);
|
|
1164
|
+
it->ctx = (const uint8_t *)RSTRING_PTR(it->ctx_pinned);
|
|
1165
|
+
it->ctxlen = (size_t)RSTRING_LEN(it->ctx_pinned);
|
|
1166
|
+
/* Early-reject wrong-size signatures before GVL drop */
|
|
1167
|
+
it->size_ok = (it->siglen == impl->sig_len) ? 1 : 0;
|
|
1168
|
+
if (!it->size_ok) it->result = -1; /* explicit reject before nogvl */
|
|
1169
|
+
|
|
1170
|
+
s->items_initialized = i + 1;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
struct verify_batch_nogvl_args nargs;
|
|
1174
|
+
nargs.items = s->items;
|
|
1175
|
+
nargs.count = s->count;
|
|
1176
|
+
rb_thread_call_without_gvl(ml_dsa_verify_batch_nogvl, &nargs,
|
|
1177
|
+
RUBY_UBF_IO, NULL);
|
|
1178
|
+
|
|
1179
|
+
VALUE out = rb_ary_new2((long)s->count);
|
|
1180
|
+
for (i = 0; i < s->count; i++) {
|
|
1181
|
+
rb_ary_push(out, s->items[i].result == 0 ? Qtrue : Qfalse);
|
|
1182
|
+
}
|
|
1183
|
+
OBJ_FREEZE(out);
|
|
1184
|
+
|
|
1185
|
+
/* GC guards on the body's stack frame */
|
|
1186
|
+
for (i = 0; i < s->count; i++) {
|
|
1187
|
+
RB_GC_GUARD(s->items[i].msg_pinned);
|
|
1188
|
+
RB_GC_GUARD(s->items[i].sig_pinned);
|
|
1189
|
+
RB_GC_GUARD(s->items[i].ctx_pinned);
|
|
1190
|
+
}
|
|
1191
|
+
return out;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
static VALUE verify_many_ensure(VALUE arg)
|
|
1195
|
+
{
|
|
1196
|
+
struct verify_batch_state *s = (struct verify_batch_state *)arg;
|
|
1197
|
+
if (s->items) {
|
|
1198
|
+
ruby_xfree(s->items);
|
|
1199
|
+
s->items = NULL;
|
|
1200
|
+
}
|
|
1201
|
+
return Qnil;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
static VALUE rb_ml_dsa_verify_many(VALUE self, VALUE rb_ops)
|
|
1205
|
+
{
|
|
1206
|
+
(void)self;
|
|
1207
|
+
Check_Type(rb_ops, T_ARRAY);
|
|
1208
|
+
long count = RARRAY_LEN(rb_ops);
|
|
1209
|
+
if (count == 0) {
|
|
1210
|
+
VALUE empty = rb_ary_new();
|
|
1211
|
+
OBJ_FREEZE(empty);
|
|
1212
|
+
return empty;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
struct verify_batch_state s;
|
|
1216
|
+
s.rb_ops = rb_ops;
|
|
1217
|
+
s.count = (size_t)count;
|
|
1218
|
+
s.items_initialized = 0;
|
|
1219
|
+
s.items = NULL;
|
|
1220
|
+
|
|
1221
|
+
return ML_DSA_ENSURE(verify_many_body, verify_many_ensure, &s);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/* ------------------------------------------------------------------ */
|
|
1225
|
+
/* _param_data — derived from ML_DSA_IMPLS */
|
|
1226
|
+
/* ------------------------------------------------------------------ */
|
|
1227
|
+
|
|
1228
|
+
static VALUE rb_ml_dsa_param_data(VALUE self)
|
|
1229
|
+
{
|
|
1230
|
+
(void)self;
|
|
1231
|
+
return ml_dsa_param_data_cache;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/* Build param data as array-of-arrays from ML_DSA_IMPLS — single source
|
|
1235
|
+
* of truth. Each inner array is [code, security_level, pk_len, sk_len,
|
|
1236
|
+
* sig_len] — positional access avoids per-item Hash/Symbol allocation. */
|
|
1237
|
+
static VALUE build_param_data(void)
|
|
1238
|
+
{
|
|
1239
|
+
VALUE arr = rb_ary_new_capa(ML_DSA_IMPL_COUNT);
|
|
1240
|
+
size_t i;
|
|
1241
|
+
for (i = 0; i < ML_DSA_IMPL_COUNT; i++) {
|
|
1242
|
+
const ml_dsa_impl_t *m = &ML_DSA_IMPLS[i];
|
|
1243
|
+
VALUE row = rb_ary_new_capa(5);
|
|
1244
|
+
rb_ary_push(row, INT2FIX(m->ps));
|
|
1245
|
+
rb_ary_push(row, INT2FIX(m->security_level));
|
|
1246
|
+
rb_ary_push(row, SIZET2NUM(m->pk_len));
|
|
1247
|
+
rb_ary_push(row, SIZET2NUM(m->sk_len));
|
|
1248
|
+
rb_ary_push(row, SIZET2NUM(m->sig_len));
|
|
1249
|
+
OBJ_FREEZE(row);
|
|
1250
|
+
rb_ary_push(arr, row);
|
|
1251
|
+
}
|
|
1252
|
+
OBJ_FREEZE(arr);
|
|
1253
|
+
return arr;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/* ------------------------------------------------------------------ */
|
|
1257
|
+
/* Init_ml_dsa_ext */
|
|
1258
|
+
/* ------------------------------------------------------------------ */
|
|
1259
|
+
|
|
1260
|
+
RUBY_FUNC_EXPORTED void Init_ml_dsa_ext(void)
|
|
1261
|
+
{
|
|
1262
|
+
/* Cache all IDs once at load time */
|
|
1263
|
+
id_name = rb_intern("name");
|
|
1264
|
+
id_at_format = rb_intern("@format");
|
|
1265
|
+
id_at_position = rb_intern("@position");
|
|
1266
|
+
id_at_reason = rb_intern("@reason");
|
|
1267
|
+
id_public_key = rb_intern("@public_key");
|
|
1268
|
+
|
|
1269
|
+
rb_mMlDsa = rb_define_module("MlDsa");
|
|
1270
|
+
rb_eMlDsaError = rb_define_class_under(rb_mMlDsa, "Error", rb_eStandardError);
|
|
1271
|
+
|
|
1272
|
+
/* Error subclasses nested under MlDsa::Error */
|
|
1273
|
+
rb_eKeyGenError = rb_define_class_under(rb_eMlDsaError, "KeyGeneration",
|
|
1274
|
+
rb_eMlDsaError);
|
|
1275
|
+
rb_eSigningError = rb_define_class_under(rb_eMlDsaError, "Signing",
|
|
1276
|
+
rb_eMlDsaError);
|
|
1277
|
+
rb_eDeserializationError = rb_define_class_under(rb_eMlDsaError, "Deserialization",
|
|
1278
|
+
rb_eMlDsaError);
|
|
1279
|
+
/* reason accessor on base Error — all subclasses inherit it */
|
|
1280
|
+
rb_define_method(rb_eMlDsaError, "reason", error_reason, 0);
|
|
1281
|
+
/* Structured error metadata accessors on DeserializationError */
|
|
1282
|
+
rb_define_method(rb_eDeserializationError, "format", error_format, 0);
|
|
1283
|
+
rb_define_method(rb_eDeserializationError, "position", error_position, 0);
|
|
1284
|
+
|
|
1285
|
+
ml_dsa_param_data_cache = build_param_data();
|
|
1286
|
+
rb_gc_register_mark_object(ml_dsa_param_data_cache);
|
|
1287
|
+
|
|
1288
|
+
/* Define the key classes */
|
|
1289
|
+
rb_cPublicKey = rb_define_class_under(rb_mMlDsa, "PublicKey", rb_cObject);
|
|
1290
|
+
rb_cSecretKey = rb_define_class_under(rb_mMlDsa, "SecretKey", rb_cObject);
|
|
1291
|
+
|
|
1292
|
+
/* ---- PublicKey: TypedData, WB-protected ---- */
|
|
1293
|
+
rb_define_alloc_func(rb_cPublicKey, pk_alloc);
|
|
1294
|
+
rb_undef_method(CLASS_OF(rb_cPublicKey), "new");
|
|
1295
|
+
|
|
1296
|
+
rb_define_method(rb_cPublicKey, "param_set", pk_param_set, 0);
|
|
1297
|
+
rb_define_method(rb_cPublicKey, "bytesize", pk_bytesize, 0);
|
|
1298
|
+
rb_define_method(rb_cPublicKey, "to_bytes", pk_to_bytes, 0);
|
|
1299
|
+
rb_define_method(rb_cPublicKey, "to_hex", pk_to_hex, 0);
|
|
1300
|
+
rb_define_method(rb_cPublicKey, "fingerprint", pk_fingerprint, 0);
|
|
1301
|
+
rb_define_method(rb_cPublicKey, "to_s", pk_to_s, 0);
|
|
1302
|
+
rb_define_method(rb_cPublicKey, "inspect", pk_inspect, 0);
|
|
1303
|
+
rb_define_method(rb_cPublicKey, "==", pk_equal, 1);
|
|
1304
|
+
rb_define_method(rb_cPublicKey, "eql?", pk_eql, 1);
|
|
1305
|
+
rb_define_method(rb_cPublicKey, "hash", pk_hash, 0);
|
|
1306
|
+
rb_define_method(rb_cPublicKey, "initialize_copy", pk_initialize_copy, 1);
|
|
1307
|
+
rb_define_method(rb_cPublicKey, "_dump_data", pk_dump_data, 0);
|
|
1308
|
+
|
|
1309
|
+
rb_define_singleton_method(rb_cPublicKey, "_from_bytes_raw",
|
|
1310
|
+
pk_from_bytes_raw, 2);
|
|
1311
|
+
rb_funcall(rb_cPublicKey, rb_intern("private_class_method"), 1,
|
|
1312
|
+
ID2SYM(rb_intern("_from_bytes_raw")));
|
|
1313
|
+
|
|
1314
|
+
/* ---- SecretKey: TypedData, secure_zero on GC ---- */
|
|
1315
|
+
rb_define_alloc_func(rb_cSecretKey, sk_alloc);
|
|
1316
|
+
rb_undef_method(CLASS_OF(rb_cSecretKey), "new");
|
|
1317
|
+
|
|
1318
|
+
rb_define_method(rb_cSecretKey, "param_set", sk_param_set, 0);
|
|
1319
|
+
rb_define_method(rb_cSecretKey, "public_key", sk_public_key, 0);
|
|
1320
|
+
rb_define_method(rb_cSecretKey, "seed", sk_seed, 0);
|
|
1321
|
+
rb_define_method(rb_cSecretKey, "bytesize", sk_bytesize, 0);
|
|
1322
|
+
rb_define_method(rb_cSecretKey, "with_bytes", sk_with_bytes, 0);
|
|
1323
|
+
rb_define_method(rb_cSecretKey, "wipe!", sk_wipe, 0);
|
|
1324
|
+
rb_define_method(rb_cSecretKey, "inspect", sk_inspect, 0);
|
|
1325
|
+
rb_define_method(rb_cSecretKey, "to_s", sk_to_s, 0);
|
|
1326
|
+
rb_define_method(rb_cSecretKey, "==", sk_equal, 1);
|
|
1327
|
+
rb_define_method(rb_cSecretKey, "eql?", sk_eql, 1);
|
|
1328
|
+
rb_define_method(rb_cSecretKey, "hash", sk_hash, 0);
|
|
1329
|
+
rb_define_method(rb_cSecretKey, "initialize_copy", sk_initialize_copy, 1);
|
|
1330
|
+
rb_define_method(rb_cSecretKey, "_dump_data", sk_dump_data, 0);
|
|
1331
|
+
|
|
1332
|
+
rb_define_singleton_method(rb_cSecretKey, "_from_bytes_raw",
|
|
1333
|
+
sk_from_bytes_raw, 2);
|
|
1334
|
+
rb_funcall(rb_cSecretKey, rb_intern("private_class_method"), 1,
|
|
1335
|
+
ID2SYM(rb_intern("_from_bytes_raw")));
|
|
1336
|
+
|
|
1337
|
+
/* ---- Keygen + batch ops — module singleton methods ---- */
|
|
1338
|
+
rb_define_singleton_method(rb_mMlDsa, "_keygen", rb_ml_dsa_keygen, 1);
|
|
1339
|
+
rb_define_singleton_method(rb_mMlDsa, "_keygen_seed", rb_ml_dsa_keygen_seed, 2);
|
|
1340
|
+
rb_define_singleton_method(rb_mMlDsa, "_sign_many", rb_ml_dsa_sign_many, 1);
|
|
1341
|
+
rb_define_singleton_method(rb_mMlDsa, "_verify_many", rb_ml_dsa_verify_many, 1);
|
|
1342
|
+
rb_define_singleton_method(rb_mMlDsa, "_param_data", rb_ml_dsa_param_data, 0);
|
|
1343
|
+
|
|
1344
|
+
/* Make all underscore-prefixed module methods private */
|
|
1345
|
+
{
|
|
1346
|
+
const char *private_methods[] = {
|
|
1347
|
+
"_keygen", "_keygen_seed", "_sign_many", "_verify_many", "_param_data"
|
|
1348
|
+
};
|
|
1349
|
+
size_t i;
|
|
1350
|
+
ID pcm = rb_intern("private_class_method");
|
|
1351
|
+
for (i = 0; i < sizeof(private_methods) / sizeof(private_methods[0]); i++)
|
|
1352
|
+
rb_funcall(rb_mMlDsa, pcm, 1, ID2SYM(rb_intern(private_methods[i])));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/* Declare Ractor safety — no mutable global state beyond
|
|
1356
|
+
* ml_dsa_param_data_cache which is deeply frozen. */
|
|
1357
|
+
#ifdef HAVE_RB_EXT_RACTOR_SAFE
|
|
1358
|
+
rb_ext_ractor_safe(true);
|
|
1359
|
+
#endif
|
|
1360
|
+
}
|