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,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ml_dsa_impl_template.h — Parametric amalgamation template.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for which PQClean .c files are included per
|
|
5
|
+
* parameter set. Each ml_dsa_NN_impl.c defines ML_DSA_PS to its
|
|
6
|
+
* parameter set code (44, 65, or 87) and includes this template.
|
|
7
|
+
*
|
|
8
|
+
* Including .c files causes GCC/Clang to resolve their quoted includes
|
|
9
|
+
* (e.g. #include "params.h") relative to the directory of the included
|
|
10
|
+
* file, so each variant picks up its own params.h automatically.
|
|
11
|
+
* fips202.h and randombytes.h are resolved via -I$(srcdir) (ext/ml_dsa/).
|
|
12
|
+
*
|
|
13
|
+
* To add a new PQClean source file, add it ONCE here — all three
|
|
14
|
+
* parameter sets pick it up automatically.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
#ifndef ML_DSA_PS
|
|
18
|
+
# error "ML_DSA_PS must be defined before including ml_dsa_impl_template.h"
|
|
19
|
+
#endif
|
|
20
|
+
|
|
21
|
+
/* Token-paste helpers to build paths like ml-dsa-44/clean/ntt.c */
|
|
22
|
+
#define ML_DSA_PASTE2(a, b) a ## b
|
|
23
|
+
#define ML_DSA_PASTE(a, b) ML_DSA_PASTE2(a, b)
|
|
24
|
+
#define ML_DSA_PATH(file) ML_DSA_STRINGIFY(ml-dsa-ML_DSA_PS/clean/file)
|
|
25
|
+
#define ML_DSA_STRINGIFY(x) ML_DSA_STRINGIFY2(x)
|
|
26
|
+
#define ML_DSA_STRINGIFY2(x) #x
|
|
27
|
+
|
|
28
|
+
#include ML_DSA_PATH(ntt.c)
|
|
29
|
+
#include ML_DSA_PATH(packing.c)
|
|
30
|
+
#include ML_DSA_PATH(poly.c)
|
|
31
|
+
#include ML_DSA_PATH(polyvec.c)
|
|
32
|
+
#include ML_DSA_PATH(reduce.c)
|
|
33
|
+
#include ML_DSA_PATH(rounding.c)
|
|
34
|
+
#include ML_DSA_PATH(symmetric-shake.c)
|
|
35
|
+
#include ML_DSA_PATH(sign.c)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ml_dsa_internal.h — Shared declarations for the ML-DSA C extension.
|
|
3
|
+
*
|
|
4
|
+
* This header provides the minimal interface consumed by ml_dsa_ext.c
|
|
5
|
+
* and the parametric impl template files (ml_dsa_NN_impl.c). It contains:
|
|
6
|
+
* - Standard and Ruby includes
|
|
7
|
+
* - C11 atomics compatibility macros
|
|
8
|
+
* - PQClean api.h includes (ifdef-guarded by parameter set)
|
|
9
|
+
* - Constants and named boundary-array positions
|
|
10
|
+
* - Dispatch table typedef
|
|
11
|
+
* - TypedData struct definitions for PublicKey and SecretKey
|
|
12
|
+
*
|
|
13
|
+
* Design decisions:
|
|
14
|
+
*
|
|
15
|
+
* Why no Signer/Verifier streaming classes?
|
|
16
|
+
* True incremental ML-DSA signing would require splitting PQClean's
|
|
17
|
+
* sign/verify at the SHAKE-256 mu-computation boundary — a fragile
|
|
18
|
+
* change that breaks on every PQClean update. The former Ruby
|
|
19
|
+
* Signer/Verifier classes buffered the full message anyway, providing
|
|
20
|
+
* an illusion of streaming without actual streaming. Removed in
|
|
21
|
+
* favor of direct sk.sign / pk.verify calls.
|
|
22
|
+
*
|
|
23
|
+
* Why batch-only C layer (no single-op sign/verify in C)?
|
|
24
|
+
* The batch sign/verify C paths already handle single items. Having
|
|
25
|
+
* separate single-op C functions duplicated ~190 lines of nogvl
|
|
26
|
+
* structs, callbacks, body/ensure pairs. Now sk.sign and pk.verify
|
|
27
|
+
* are Ruby methods that call sign_many/verify_many with a single-
|
|
28
|
+
* element array. The overhead (one Ruby Array + one SignRequest/
|
|
29
|
+
* VerifyRequest) is negligible vs. the ~50us crypto operation.
|
|
30
|
+
*
|
|
31
|
+
* Why ParameterSet in Ruby, not C?
|
|
32
|
+
* ParameterSet is a value object with Comparable, inspect, to_s —
|
|
33
|
+
* features that are trivial in Ruby but would require 100+ lines of
|
|
34
|
+
* boilerplate in C. The C layer passes integer codes (44/65/87)
|
|
35
|
+
* through the boundary. The build_param_data array-of-arrays bridges
|
|
36
|
+
* C constants into Ruby with no per-call overhead (runs once at
|
|
37
|
+
* require time).
|
|
38
|
+
*
|
|
39
|
+
* Why no code generator for the dispatch table?
|
|
40
|
+
* ML-DSA has exactly 3 parameter sets (44, 65, 87), standardized by
|
|
41
|
+
* NIST FIPS 204. This set will not change. A code generator would
|
|
42
|
+
* add build complexity for a table that has 3 entries.
|
|
43
|
+
*
|
|
44
|
+
* Why context is a plain string (not a Context class)?
|
|
45
|
+
* FIPS 204 defines context as 0..255 bytes — that's a string with a
|
|
46
|
+
* length check. A wrapper class adds API surface without adding
|
|
47
|
+
* invariants beyond what the primitive can express.
|
|
48
|
+
*
|
|
49
|
+
* rb_ensure pattern:
|
|
50
|
+
* Every GVL-released crypto operation follows this structure:
|
|
51
|
+
* 1. Allocate buffers in body (covered by ensure on OOM)
|
|
52
|
+
* 2. Pin Ruby strings with rb_str_new_frozen before GVL drop
|
|
53
|
+
* 3. Call rb_thread_call_without_gvl
|
|
54
|
+
* 4. Build result objects
|
|
55
|
+
* 5. Place RB_GC_GUARD at END of body (not in ensure!)
|
|
56
|
+
* 6. Ensure function: secure_zero + free all allocations
|
|
57
|
+
* CRITICAL: RB_GC_GUARD must live on the body's stack frame, not
|
|
58
|
+
* in ensure, because ensure may execute after body's locals are gone.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
#ifndef ML_DSA_INTERNAL_H
|
|
62
|
+
#define ML_DSA_INTERNAL_H
|
|
63
|
+
|
|
64
|
+
/* Must precede <string.h> so memset_s is declared when available. */
|
|
65
|
+
#define __STDC_WANT_LIB_EXT1__ 1
|
|
66
|
+
|
|
67
|
+
#include <ruby.h>
|
|
68
|
+
#include <ruby/thread.h>
|
|
69
|
+
#include <ruby/st.h>
|
|
70
|
+
#include <ruby/encoding.h>
|
|
71
|
+
#include <string.h>
|
|
72
|
+
#include <stdint.h>
|
|
73
|
+
#include <stdarg.h>
|
|
74
|
+
|
|
75
|
+
/* C11 atomics for thread-safe wipe flag. MSVC < 2022 lacks stdatomic.h,
|
|
76
|
+
* so we fall back to volatile int + compiler barriers on that platform. */
|
|
77
|
+
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_ATOMICS__)
|
|
78
|
+
# include <stdatomic.h>
|
|
79
|
+
# define ML_DSA_ATOMIC _Atomic
|
|
80
|
+
# define ML_DSA_ATOMIC_LOAD(p) atomic_load_explicit((p), memory_order_acquire)
|
|
81
|
+
# define ML_DSA_ATOMIC_STORE(p, v) atomic_store_explicit((p), (v), memory_order_release)
|
|
82
|
+
#else
|
|
83
|
+
# define ML_DSA_ATOMIC volatile
|
|
84
|
+
# define ML_DSA_ATOMIC_LOAD(p) (*(p))
|
|
85
|
+
# define ML_DSA_ATOMIC_STORE(p, v) (*(p) = (v))
|
|
86
|
+
#endif
|
|
87
|
+
|
|
88
|
+
#ifdef ML_DSA_ENABLE_44
|
|
89
|
+
# include "ml-dsa-44/clean/api.h"
|
|
90
|
+
#endif
|
|
91
|
+
#ifdef ML_DSA_ENABLE_65
|
|
92
|
+
# include "ml-dsa-65/clean/api.h"
|
|
93
|
+
#endif
|
|
94
|
+
#ifdef ML_DSA_ENABLE_87
|
|
95
|
+
# include "ml-dsa-87/clean/api.h"
|
|
96
|
+
#endif
|
|
97
|
+
|
|
98
|
+
#include "randombytes.h"
|
|
99
|
+
|
|
100
|
+
/* ------------------------------------------------------------------ */
|
|
101
|
+
/* Constants */
|
|
102
|
+
/* ------------------------------------------------------------------ */
|
|
103
|
+
|
|
104
|
+
#define ML_DSA_RNDBYTES 32
|
|
105
|
+
#define ML_DSA_SEED_BYTES 32
|
|
106
|
+
|
|
107
|
+
/* Named positions for the flat Ruby->C boundary arrays.
|
|
108
|
+
* sign_many: [sk, message, ctx_or_nil, deterministic_or_nil, rnd_or_nil]
|
|
109
|
+
* verify_many: [pk, message, signature, ctx_or_nil] */
|
|
110
|
+
#define SIGN_OP_SK 0
|
|
111
|
+
#define SIGN_OP_MSG 1
|
|
112
|
+
#define SIGN_OP_CTX 2
|
|
113
|
+
#define SIGN_OP_DET 3
|
|
114
|
+
#define SIGN_OP_RND 4 /* optional pre-generated rnd bytes from pluggable RNG */
|
|
115
|
+
|
|
116
|
+
#define VERIFY_OP_PK 0
|
|
117
|
+
#define VERIFY_OP_MSG 1
|
|
118
|
+
#define VERIFY_OP_SIG 2
|
|
119
|
+
#define VERIFY_OP_CTX 3
|
|
120
|
+
|
|
121
|
+
#ifdef ML_DSA_ENABLE_87
|
|
122
|
+
# define ML_DSA_MAX_PK_BYTES PQCLEAN_MLDSA87_CLEAN_CRYPTO_PUBLICKEYBYTES
|
|
123
|
+
# define ML_DSA_MAX_SK_BYTES PQCLEAN_MLDSA87_CLEAN_CRYPTO_SECRETKEYBYTES
|
|
124
|
+
# define ML_DSA_MAX_SIG_BYTES PQCLEAN_MLDSA87_CLEAN_CRYPTO_BYTES
|
|
125
|
+
#elif defined(ML_DSA_ENABLE_65)
|
|
126
|
+
# define ML_DSA_MAX_PK_BYTES PQCLEAN_MLDSA65_CLEAN_CRYPTO_PUBLICKEYBYTES
|
|
127
|
+
# define ML_DSA_MAX_SK_BYTES PQCLEAN_MLDSA65_CLEAN_CRYPTO_SECRETKEYBYTES
|
|
128
|
+
# define ML_DSA_MAX_SIG_BYTES PQCLEAN_MLDSA65_CLEAN_CRYPTO_BYTES
|
|
129
|
+
#else
|
|
130
|
+
# define ML_DSA_MAX_PK_BYTES PQCLEAN_MLDSA44_CLEAN_CRYPTO_PUBLICKEYBYTES
|
|
131
|
+
# define ML_DSA_MAX_SK_BYTES PQCLEAN_MLDSA44_CLEAN_CRYPTO_SECRETKEYBYTES
|
|
132
|
+
# define ML_DSA_MAX_SIG_BYTES PQCLEAN_MLDSA44_CLEAN_CRYPTO_BYTES
|
|
133
|
+
#endif
|
|
134
|
+
|
|
135
|
+
/* Convenience macro — wraps rb_ensure with the cast boilerplate. */
|
|
136
|
+
#define ML_DSA_ENSURE(body, ensure, state) \
|
|
137
|
+
rb_ensure((body), (VALUE)(state), (ensure), (VALUE)(state))
|
|
138
|
+
|
|
139
|
+
/* ------------------------------------------------------------------ */
|
|
140
|
+
/* Dispatch table */
|
|
141
|
+
/* ------------------------------------------------------------------ */
|
|
142
|
+
|
|
143
|
+
typedef int (*keygen_fn_t)(uint8_t *, uint8_t *, const uint8_t *);
|
|
144
|
+
typedef int (*sign_fn_t)(uint8_t *, size_t *,
|
|
145
|
+
const uint8_t *, size_t,
|
|
146
|
+
const uint8_t *, size_t,
|
|
147
|
+
const uint8_t *,
|
|
148
|
+
const uint8_t *); /* rnd_in */
|
|
149
|
+
typedef int (*verify_fn_t)(const uint8_t *, size_t,
|
|
150
|
+
const uint8_t *, size_t,
|
|
151
|
+
const uint8_t *, size_t,
|
|
152
|
+
const uint8_t *);
|
|
153
|
+
|
|
154
|
+
typedef struct {
|
|
155
|
+
int ps;
|
|
156
|
+
int security_level;
|
|
157
|
+
keygen_fn_t keygen_fn;
|
|
158
|
+
sign_fn_t sign_fn;
|
|
159
|
+
verify_fn_t verify_fn;
|
|
160
|
+
size_t pk_len;
|
|
161
|
+
size_t sk_len;
|
|
162
|
+
size_t sig_len;
|
|
163
|
+
} ml_dsa_impl_t;
|
|
164
|
+
|
|
165
|
+
extern const ml_dsa_impl_t ML_DSA_IMPLS[];
|
|
166
|
+
extern const size_t ML_DSA_IMPL_COUNT;
|
|
167
|
+
|
|
168
|
+
/* ------------------------------------------------------------------ */
|
|
169
|
+
/* TypedData structures */
|
|
170
|
+
/* ------------------------------------------------------------------ */
|
|
171
|
+
|
|
172
|
+
typedef struct {
|
|
173
|
+
size_t len;
|
|
174
|
+
int ps_code;
|
|
175
|
+
VALUE fingerprint; /* cached SHA-256 hex prefix, Qnil until computed */
|
|
176
|
+
uint8_t bytes[]; /* flexible array member — allocated inline */
|
|
177
|
+
} ml_dsa_pk_t;
|
|
178
|
+
|
|
179
|
+
typedef struct {
|
|
180
|
+
size_t len;
|
|
181
|
+
int ps_code;
|
|
182
|
+
ML_DSA_ATOMIC int wiped; /* non-zero after wipe! — atomic for thread safety */
|
|
183
|
+
int has_seed; /* non-zero if seed[] contains the keygen seed */
|
|
184
|
+
uint8_t seed[ML_DSA_SEED_BYTES]; /* keygen seed (zeroed if !has_seed) */
|
|
185
|
+
uint8_t bytes[]; /* flexible array member — allocated inline */
|
|
186
|
+
} ml_dsa_sk_t;
|
|
187
|
+
|
|
188
|
+
#endif /* ML_DSA_INTERNAL_H */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* randombytes.c — OS-backed CSPRNG.
|
|
3
|
+
*
|
|
4
|
+
* Replaces PQClean's common/randombytes.c. All randomness for keygen
|
|
5
|
+
* and signing is generated before the GVL drop and passed explicitly
|
|
6
|
+
* to the PQClean functions (seed_in for keygen, rnd_in for signing).
|
|
7
|
+
* This file is only used for OS CSPRNG calls made while holding the GVL.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include "randombytes.h"
|
|
11
|
+
|
|
12
|
+
#if defined(_WIN32)
|
|
13
|
+
# include <windows.h>
|
|
14
|
+
# include <bcrypt.h>
|
|
15
|
+
# pragma comment(lib, "bcrypt.lib")
|
|
16
|
+
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
|
17
|
+
# include <stdlib.h> /* arc4random_buf */
|
|
18
|
+
#else
|
|
19
|
+
# include <errno.h>
|
|
20
|
+
# include <sys/random.h> /* getrandom(2), Linux >= 3.17 */
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
void randombytes(uint8_t *x, size_t xlen)
|
|
24
|
+
{
|
|
25
|
+
#if defined(_WIN32)
|
|
26
|
+
/* BCryptGenRandom takes ULONG; guard against truncation on 64-bit. */
|
|
27
|
+
if (xlen > (size_t)ULONG_MAX) abort();
|
|
28
|
+
NTSTATUS status = BCryptGenRandom(NULL, x, (ULONG)xlen,
|
|
29
|
+
BCRYPT_USE_SYSTEM_PREFERRED_RNG);
|
|
30
|
+
if (!BCRYPT_SUCCESS(status)) abort();
|
|
31
|
+
|
|
32
|
+
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
|
33
|
+
arc4random_buf(x, xlen);
|
|
34
|
+
|
|
35
|
+
#else
|
|
36
|
+
/* Linux: loop on EINTR; getrandom never returns partial reads for <= 256 bytes */
|
|
37
|
+
while (xlen > 0) {
|
|
38
|
+
ssize_t ret = getrandom(x, xlen, 0);
|
|
39
|
+
if (ret < 0) {
|
|
40
|
+
if (errno == EINTR) continue;
|
|
41
|
+
/* getrandom failure is fatal — caller cannot continue safely */
|
|
42
|
+
__builtin_trap();
|
|
43
|
+
}
|
|
44
|
+
x += (size_t)ret;
|
|
45
|
+
xlen -= (size_t)ret;
|
|
46
|
+
}
|
|
47
|
+
#endif
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#ifndef ML_DSA_RANDOMBYTES_H
|
|
2
|
+
#define ML_DSA_RANDOMBYTES_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Generate xlen random bytes into x using the OS CSPRNG:
|
|
9
|
+
* - Linux: getrandom(2)
|
|
10
|
+
* - macOS/BSD: arc4random_buf(3)
|
|
11
|
+
* - Windows: BCryptGenRandom
|
|
12
|
+
*/
|
|
13
|
+
void randombytes(uint8_t *x, size_t xlen);
|
|
14
|
+
|
|
15
|
+
#endif /* ML_DSA_RANDOMBYTES_H */
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MlDsa
|
|
4
|
+
# Collects sign or verify operations for batch execution.
|
|
5
|
+
# Obtained via {MlDsa.batch}. Do not mix sign and verify in one batch.
|
|
6
|
+
class BatchBuilder
|
|
7
|
+
def initialize
|
|
8
|
+
@ops = []
|
|
9
|
+
@mode = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Add a sign operation to the batch.
|
|
13
|
+
# @param sk [SecretKey]
|
|
14
|
+
# @param message [String]
|
|
15
|
+
# @param context [String, nil] FIPS 204 context (0..255 bytes)
|
|
16
|
+
# @param deterministic [Boolean] use deterministic signing
|
|
17
|
+
# @return [self] for chaining
|
|
18
|
+
def sign(sk:, message:, context: nil, deterministic: false)
|
|
19
|
+
check_mode!(:sign)
|
|
20
|
+
@ops << SignRequest.new(sk: sk, message: message,
|
|
21
|
+
context: context, deterministic: deterministic)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Add a verify operation to the batch.
|
|
26
|
+
# @param pk [PublicKey]
|
|
27
|
+
# @param message [String]
|
|
28
|
+
# @param signature [String]
|
|
29
|
+
# @param context [String, nil] FIPS 204 context (0..255 bytes)
|
|
30
|
+
# @return [self] for chaining
|
|
31
|
+
def verify(pk:, message:, signature:, context: nil)
|
|
32
|
+
check_mode!(:verify)
|
|
33
|
+
@ops << VerifyRequest.new(pk: pk, message: message,
|
|
34
|
+
signature: signature, context: context)
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @api private
|
|
39
|
+
def execute(config: MlDsa.config, yield_every: Internal::DEFAULT_YIELD_EVERY)
|
|
40
|
+
return [] if @ops.empty?
|
|
41
|
+
if @mode == :sign
|
|
42
|
+
MlDsa.sign_many(@ops, config: config, yield_every: yield_every)
|
|
43
|
+
else
|
|
44
|
+
MlDsa.verify_many(@ops, config: config, yield_every: yield_every)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def check_mode!(mode)
|
|
51
|
+
if @mode && @mode != mode
|
|
52
|
+
raise ArgumentError, "cannot mix sign and verify in the same batch"
|
|
53
|
+
end
|
|
54
|
+
@mode = mode
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MlDsa
|
|
4
|
+
# Thread-safe configuration holder for instrumentation subscribers
|
|
5
|
+
# and pluggable RNG. Each Ractor or test context can have its own
|
|
6
|
+
# Config instance.
|
|
7
|
+
#
|
|
8
|
+
# @example Custom config for testing
|
|
9
|
+
# cfg = MlDsa::Config.new
|
|
10
|
+
# cfg.random_source = proc { |n| "\x42" * n }
|
|
11
|
+
# pk, sk = MlDsa.keygen(MlDsa::ML_DSA_65, config: cfg)
|
|
12
|
+
class Config
|
|
13
|
+
def initialize
|
|
14
|
+
@subscribers = []
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
@random_source = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Subscribe to instrumentation events. The block receives a frozen Hash
|
|
20
|
+
# with keys +:operation+, +:param_set+, +:count+, and +:duration_ns+.
|
|
21
|
+
#
|
|
22
|
+
# @yield [Hash] event payload
|
|
23
|
+
# @return [Proc] the subscriber (pass to {.unsubscribe} to remove)
|
|
24
|
+
def subscribe(&block)
|
|
25
|
+
raise ArgumentError, "subscribe requires a block" unless block
|
|
26
|
+
@mutex.synchronize { @subscribers << block }
|
|
27
|
+
block
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Remove a previously registered subscriber.
|
|
31
|
+
# @param subscriber [Proc] the block returned by {#subscribe}
|
|
32
|
+
# @return [Proc, nil] the removed subscriber, or nil if not found
|
|
33
|
+
def unsubscribe(subscriber)
|
|
34
|
+
@mutex.synchronize { @subscribers.delete(subscriber) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Proc, nil] the current random source, or nil for OS CSPRNG
|
|
38
|
+
attr_reader :random_source
|
|
39
|
+
|
|
40
|
+
# Set a custom random source. The proc must accept a single Integer
|
|
41
|
+
# argument (byte count) and return a binary String of that length.
|
|
42
|
+
#
|
|
43
|
+
# @param source [Proc, nil]
|
|
44
|
+
def random_source=(source)
|
|
45
|
+
if source && !source.respond_to?(:call)
|
|
46
|
+
raise TypeError, "random_source must respond to :call or be nil"
|
|
47
|
+
end
|
|
48
|
+
@random_source = source
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @api private — fire event to all subscribers
|
|
52
|
+
def notify(operation, param_set, count, duration_ns)
|
|
53
|
+
# In non-main Ractors, skip instrumentation to avoid isolation errors
|
|
54
|
+
if defined?(Ractor) && Ractor.respond_to?(:main?) && !Ractor.main?
|
|
55
|
+
return nil
|
|
56
|
+
end
|
|
57
|
+
subs = @mutex.synchronize { @subscribers.dup }
|
|
58
|
+
return if subs.empty?
|
|
59
|
+
event = {
|
|
60
|
+
operation: operation,
|
|
61
|
+
param_set: param_set,
|
|
62
|
+
count: count,
|
|
63
|
+
duration_ns: duration_ns
|
|
64
|
+
}.freeze
|
|
65
|
+
subs.each { |s| s.call(event) }
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MlDsa
|
|
4
|
+
# Nested module with private_constant so that MlDsa::Internal is
|
|
5
|
+
# accessible inside this gem but invisible to external callers.
|
|
6
|
+
module Internal
|
|
7
|
+
# Named positions for the flat Ruby→C boundary arrays.
|
|
8
|
+
# Must match SIGN_OP_* / VERIFY_OP_* macros in ml_dsa_internal.h.
|
|
9
|
+
SIGN_OP_SK = 0
|
|
10
|
+
SIGN_OP_MSG = 1
|
|
11
|
+
SIGN_OP_CTX = 2
|
|
12
|
+
SIGN_OP_DET = 3
|
|
13
|
+
SIGN_OP_RND = 4 # optional pre-generated rnd bytes (pluggable RNG)
|
|
14
|
+
|
|
15
|
+
VERIFY_OP_PK = 0
|
|
16
|
+
VERIFY_OP_MSG = 1
|
|
17
|
+
VERIFY_OP_SIG = 2
|
|
18
|
+
VERIFY_OP_CTX = 3
|
|
19
|
+
|
|
20
|
+
RND_BYTES = 32
|
|
21
|
+
|
|
22
|
+
# Default number of items between cooperative yields to the fiber
|
|
23
|
+
# scheduler during batch normalization loops. Zero means no yielding.
|
|
24
|
+
DEFAULT_YIELD_EVERY = 0
|
|
25
|
+
|
|
26
|
+
# Yield to the fiber scheduler if one is active and yield_every > 0.
|
|
27
|
+
# Called inside normalization loops to allow other fibers to run
|
|
28
|
+
# during large batch preparations.
|
|
29
|
+
def self.maybe_yield(i, yield_every)
|
|
30
|
+
return unless yield_every > 0 && ((i + 1) % yield_every).zero?
|
|
31
|
+
scheduler = Fiber.scheduler
|
|
32
|
+
return unless scheduler
|
|
33
|
+
scheduler.yield
|
|
34
|
+
rescue NoMethodError
|
|
35
|
+
# Fiber.scheduler or scheduler.yield not available (Ruby < 3.1)
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raise MlDsa::Error::Deserialization with structured metadata.
|
|
40
|
+
def self.raise_deser(format_str, position, reason, message)
|
|
41
|
+
err = MlDsa::Error::Deserialization.new(message)
|
|
42
|
+
err.instance_variable_set(:@format, format_str)
|
|
43
|
+
err.instance_variable_set(:@position, position)
|
|
44
|
+
err.instance_variable_set(:@reason, reason.to_sym)
|
|
45
|
+
raise err
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Look up the ParameterSet for a given code (44, 65, or 87).
|
|
49
|
+
def self.param_set_for_code(code)
|
|
50
|
+
MlDsa.const_get("ML_DSA_#{code}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Validate and return a ParameterSet.
|
|
54
|
+
def self.resolve_ps(ps)
|
|
55
|
+
case ps
|
|
56
|
+
when ParameterSet then ps
|
|
57
|
+
else
|
|
58
|
+
raise TypeError,
|
|
59
|
+
"param_set must be a MlDsa::ParameterSet " \
|
|
60
|
+
"(ML_DSA_44, ML_DSA_65, or ML_DSA_87), got #{ps.class}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Validate a hex string and decode to binary bytes.
|
|
65
|
+
def self.decode_hex(hex)
|
|
66
|
+
raise TypeError, "hex must be a String, got #{hex.class}" unless hex.is_a?(String)
|
|
67
|
+
raise ArgumentError, "hex string must not be empty" if hex.empty?
|
|
68
|
+
unless hex.match?(/\A[0-9a-fA-F]+\z/) && hex.length.even?
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"hex must contain only hexadecimal characters and have even length"
|
|
71
|
+
end
|
|
72
|
+
[hex].pack("H*")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
private_constant :Internal
|
|
76
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MlDsa
|
|
4
|
+
# Holds a matched public/secret key pair returned by {MlDsa.keygen}.
|
|
5
|
+
# Supports destructuring: +pk, sk = MlDsa.keygen(...)+.
|
|
6
|
+
class KeyPair
|
|
7
|
+
# @return [PublicKey]
|
|
8
|
+
attr_reader :public_key
|
|
9
|
+
# @return [SecretKey]
|
|
10
|
+
attr_reader :secret_key
|
|
11
|
+
|
|
12
|
+
def initialize(public_key, secret_key)
|
|
13
|
+
@public_key = public_key
|
|
14
|
+
@secret_key = secret_key
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Support destructuring: +pk, sk = keygen(...)+.
|
|
19
|
+
# @return [Array(PublicKey, SecretKey)]
|
|
20
|
+
def to_ary
|
|
21
|
+
[public_key, secret_key]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alias_method :to_a, :to_ary
|
|
25
|
+
alias_method :deconstruct, :to_ary
|
|
26
|
+
|
|
27
|
+
# @return [ParameterSet]
|
|
28
|
+
def param_set
|
|
29
|
+
public_key.param_set
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String]
|
|
33
|
+
def inspect
|
|
34
|
+
"#<MlDsa::KeyPair #{param_set.name}>"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias_method :to_s, :inspect
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MlDsa
|
|
4
|
+
# Encapsulates a concrete ML-DSA parameter set (44, 65, or 87).
|
|
5
|
+
#
|
|
6
|
+
# Obtain instances via the +ML_DSA_44+, +ML_DSA_65+, +ML_DSA_87+ constants;
|
|
7
|
+
# do not call +new+ directly. Supports +Comparable+ ordering by security
|
|
8
|
+
# level: +ML_DSA_44 < ML_DSA_65 < ML_DSA_87+.
|
|
9
|
+
class ParameterSet
|
|
10
|
+
include Comparable
|
|
11
|
+
|
|
12
|
+
# @return [String] human-readable name, e.g. +"ML-DSA-44"+
|
|
13
|
+
attr_reader :name
|
|
14
|
+
# @return [Integer] numeric code (44, 65, or 87)
|
|
15
|
+
attr_reader :code
|
|
16
|
+
# @return [Integer] NIST security level (2, 3, or 5)
|
|
17
|
+
attr_reader :security_level
|
|
18
|
+
# @return [Integer] public key size in bytes
|
|
19
|
+
attr_reader :public_key_bytes
|
|
20
|
+
# @return [Integer] secret key size in bytes
|
|
21
|
+
attr_reader :secret_key_bytes
|
|
22
|
+
# @return [Integer] signature size in bytes
|
|
23
|
+
attr_reader :signature_bytes
|
|
24
|
+
|
|
25
|
+
def initialize(name, code, security_level, pk, sk, sig)
|
|
26
|
+
@name = name.freeze
|
|
27
|
+
@code = code
|
|
28
|
+
@security_level = security_level
|
|
29
|
+
@public_key_bytes = pk
|
|
30
|
+
@secret_key_bytes = sk
|
|
31
|
+
@signature_bytes = sig
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def <=>(other)
|
|
36
|
+
return nil unless other.is_a?(ParameterSet)
|
|
37
|
+
@security_level <=> other.security_level
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
@name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def inspect
|
|
45
|
+
"#<MlDsa::ParameterSet #{@name}>"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method :new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Build ParameterSet constants from C extension data — no raw integer
|
|
52
|
+
# constants are exposed in the public API.
|
|
53
|
+
# Each row is [code, security_level, pk_bytes, sk_bytes, sig_bytes].
|
|
54
|
+
_param_data.each do |code, security_level, pk_bytes, sk_bytes, sig_bytes|
|
|
55
|
+
name = "ML-DSA-#{code}"
|
|
56
|
+
const_name = "ML_DSA_#{code}"
|
|
57
|
+
ps = ParameterSet.send(:new,
|
|
58
|
+
name, code, security_level, pk_bytes, sk_bytes, sig_bytes)
|
|
59
|
+
const_set(const_name, ps)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# OIDs assigned by NIST for ML-DSA parameter sets (FIPS 204).
|
|
63
|
+
ML_DSA_OIDS = {
|
|
64
|
+
44 => "2.16.840.1.101.3.4.3.17",
|
|
65
|
+
65 => "2.16.840.1.101.3.4.3.18",
|
|
66
|
+
87 => "2.16.840.1.101.3.4.3.19"
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
ML_DSA_OID_TO_CODE = ML_DSA_OIDS.invert.freeze
|
|
70
|
+
|
|
71
|
+
# O(1) lookup tables for param_set_for_signature_size / param_set_for_pk_size
|
|
72
|
+
PARAM_SET_BY_SIG_SIZE = constants.filter_map { |c|
|
|
73
|
+
ps = const_get(c)
|
|
74
|
+
[ps.signature_bytes, ps] if ps.is_a?(ParameterSet)
|
|
75
|
+
}.to_h.freeze
|
|
76
|
+
private_constant :PARAM_SET_BY_SIG_SIZE
|
|
77
|
+
|
|
78
|
+
PARAM_SET_BY_PK_SIZE = constants.filter_map { |c|
|
|
79
|
+
ps = const_get(c)
|
|
80
|
+
[ps.public_key_bytes, ps] if ps.is_a?(ParameterSet)
|
|
81
|
+
}.to_h.freeze
|
|
82
|
+
private_constant :PARAM_SET_BY_PK_SIZE
|
|
83
|
+
|
|
84
|
+
PARAM_SET_BY_SK_SIZE = constants.filter_map { |c|
|
|
85
|
+
ps = const_get(c)
|
|
86
|
+
[ps.secret_key_bytes, ps] if ps.is_a?(ParameterSet)
|
|
87
|
+
}.to_h.freeze
|
|
88
|
+
private_constant :PARAM_SET_BY_SK_SIZE
|
|
89
|
+
end
|