Module: MlDsa

Defined in:
lib/ml_dsa.rb,
lib/ml_dsa/config.rb,
lib/ml_dsa/version.rb,
lib/ml_dsa/internal.rb,
lib/ml_dsa/key_pair.rb,
lib/ml_dsa/requests.rb,
lib/ml_dsa/public_key.rb,
lib/ml_dsa/secret_key.rb,
lib/ml_dsa/batch_builder.rb,
lib/ml_dsa/parameter_set.rb,
ext/ml_dsa/ml_dsa_ext.c

Defined Under Namespace

Classes: BatchBuilder, Config, Error, KeyPair, ParameterSet, PublicKey, Result, SecretKey, SignRequest, VerifyRequest

Constant Summary collapse

SEED_BYTES =

Seed size (bytes) for deterministic key generation.

32
VERSION =
"0.1.0"
ML_DSA_OIDS =

OIDs assigned by NIST for ML-DSA parameter sets (FIPS 204).

{
  44 => "2.16.840.1.101.3.4.3.17",
  65 => "2.16.840.1.101.3.4.3.18",
  87 => "2.16.840.1.101.3.4.3.19"
}.freeze
ML_DSA_OID_TO_CODE =
ML_DSA_OIDS.invert.freeze

Class Method Summary collapse

Class Method Details

._keygen(rb_ps) ⇒ Object

---- Keygen + batch ops — module singleton methods ----



825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
# File 'ext/ml_dsa/ml_dsa_ext.c', line 825

static VALUE rb_ml_dsa_keygen(VALUE self, VALUE rb_ps)
{
    (void)self;
    int ps = NUM2INT(rb_ps);
    const ml_dsa_impl_t *impl = find_impl(ps);

    struct keygen_state s;
    s.impl         = impl;
    s.ps           = ps;
    s.has_seed     = 0;
    s.pk_buf       = NULL;
    s.sk_buf       = NULL;

    /* Generate seed from OS CSPRNG before GVL drop */
    randombytes(s.seed, ML_DSA_SEED_BYTES);

    return ML_DSA_ENSURE(keygen_body, keygen_ensure, &s);
}

._keygen_seed(rb_ps, rb_seed) ⇒ Object




848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
# File 'ext/ml_dsa/ml_dsa_ext.c', line 848

static VALUE rb_ml_dsa_keygen_seed(VALUE self, VALUE rb_ps, VALUE rb_seed)
{
    (void)self;
    Check_Type(rb_seed, T_STRING);
    if (RSTRING_LEN(rb_seed) != ML_DSA_SEED_BYTES)
        rb_raise(rb_eArgError, "seed must be exactly %d bytes, got %ld",
                 ML_DSA_SEED_BYTES, RSTRING_LEN(rb_seed));

    int ps = NUM2INT(rb_ps);
    const ml_dsa_impl_t *impl = find_impl(ps);

    struct keygen_state s;
    s.impl         = impl;
    s.ps           = ps;
    s.has_seed     = 1;
    s.pk_buf       = NULL;
    s.sk_buf       = NULL;
    memcpy(s.seed, RSTRING_PTR(rb_seed), ML_DSA_SEED_BYTES);

    return ML_DSA_ENSURE(keygen_body, keygen_ensure, &s);
}

._param_data ⇒ Object




1228
1229
1230
1231
1232
# File 'ext/ml_dsa/ml_dsa_ext.c', line 1228

static VALUE rb_ml_dsa_param_data(VALUE self)
{
    (void)self;
    return ml_dsa_param_data_cache;
}

._sign_many(rb_ops) ⇒ Object



1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
# File 'ext/ml_dsa/ml_dsa_ext.c', line 1037

static VALUE rb_ml_dsa_sign_many(VALUE self, VALUE rb_ops)
{
    (void)self;
    Check_Type(rb_ops, T_ARRAY);
    long count = RARRAY_LEN(rb_ops);
    if (count == 0) {
        VALUE empty = rb_ary_new();
        OBJ_FREEZE(empty);
        return empty;
    }

    struct sign_batch_state s;
    s.rb_ops            = rb_ops;
    s.count             = (size_t)count;
    s.items_initialized = 0;
    s.items             = NULL;

    return ML_DSA_ENSURE(sign_many_body, sign_many_ensure, &s);
}

._verify_many(rb_ops) ⇒ Object



1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
# File 'ext/ml_dsa/ml_dsa_ext.c', line 1204

static VALUE rb_ml_dsa_verify_many(VALUE self, VALUE rb_ops)
{
    (void)self;
    Check_Type(rb_ops, T_ARRAY);
    long count = RARRAY_LEN(rb_ops);
    if (count == 0) {
        VALUE empty = rb_ary_new();
        OBJ_FREEZE(empty);
        return empty;
    }

    struct verify_batch_state s;
    s.rb_ops            = rb_ops;
    s.count             = (size_t)count;
    s.items_initialized = 0;
    s.items             = NULL;

    return ML_DSA_ENSURE(verify_many_body, verify_many_ensure, &s);
}

.batch(config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY) {|BatchBuilder| ... } ⇒ Array<String>

Unified batch builder — collects sign/verify ops and executes them in a single GVL drop.

Examples:

Batch signing

sigs = MlDsa.batch { |b| b.sign(sk: sk, message: msg) }

Batch verification

results = MlDsa.batch { |b| b.verify(pk: pk, message: msg, signature: sig) }

Yields:

Returns:

  • (Array<String>) —

    for sign batches, [Array] for verify batches

Raises:

  • (ArgumentError) —

    if the batch mixes sign and verify operations



179
180
181
182
183
184
# File 'lib/ml_dsa.rb', line 179

def batch(config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY)
  cfg = config || @config
  builder = BatchBuilder.new
  yield builder
  builder.execute(config: cfg, yield_every: yield_every)
end

.config ⇒ Config

The default global Config instance.

Returns:



22
23
24
# File 'lib/ml_dsa.rb', line 22

def self.config
  @config
end

.keygen(param_set, seed: nil, config: nil) ⇒ KeyPair

Generate a key pair for the given parameter set.

Parameters:

  • param_set (ParameterSet) —

    ML_DSA_44, ML_DSA_65, or ML_DSA_87

  • seed (String, nil) (defaults to: nil) —

    optional 32-byte seed for deterministic keygen

  • config (Config) (defaults to: nil) —

    configuration (default: MlDsa.config)

Returns:

  • (KeyPair) —

    frozen key pair (supports destructuring: pk, sk = keygen(...))

Raises:

  • (TypeError) —

    if param_set is not a ParameterSet

  • (ArgumentError) —

    if seed is not exactly 32 bytes



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ml_dsa.rb', line 68

def keygen(param_set, seed: nil, config: nil)
  cfg = config || @config
  ps = Internal.resolve_ps(param_set)
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
  if seed
    raise TypeError, "seed must be a String" unless seed.is_a?(String)
    seed_bin = seed.b
    raise ArgumentError, "seed must be exactly 32 bytes, got #{seed_bin.bytesize}" unless seed_bin.bytesize == 32
    pk, sk = _keygen_seed(ps.code, seed_bin)
  elsif cfg.random_source
    # Pluggable RNG: generate a seed and use deterministic keygen
    rng_seed = cfg.random_source.call(SEED_BYTES)
    unless rng_seed.is_a?(String) && rng_seed.bytesize == SEED_BYTES
      raise ArgumentError,
        "random_source must return #{SEED_BYTES} bytes, " \
        "got #{rng_seed.is_a?(String) ? rng_seed.bytesize : rng_seed.class}"
    end
    pk, sk = _keygen_seed(ps.code, rng_seed.b)
  else
    pk, sk = _keygen(ps.code)
  end
  now = Time.now.freeze
  pk.instance_variable_set(:@created_at, now)
  sk.instance_variable_set(:@created_at, now)
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
  cfg.notify(:keygen, ps, 1, duration)
  KeyPair.new(pk, sk)
end

.random_source ⇒ Proc?

Returns the current random source on the default config.

Returns:

  • (Proc, nil) —

    the current random source on the default config



45
46
47
# File 'lib/ml_dsa.rb', line 45

def self.random_source
  @config.random_source
end

.random_source=(source) ⇒ Object

Set the random source on the default config.

Parameters:

  • source (Proc, nil)


51
52
53
# File 'lib/ml_dsa.rb', line 51

def self.random_source=(source)
  @config.random_source = source
end

.sign_many(operations, config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY) ⇒ Array<String>

Sign multiple messages in a single GVL drop.

Parameters:

  • operations (Array<SignRequest>)
  • config (Config) (defaults to: nil) —

    configuration (default: MlDsa.config)

  • yield_every (Integer) (defaults to: Internal::DEFAULT_YIELD_EVERY) —

    yield to the fiber scheduler every N items during the normalization loop (0 = never, default)

Returns:

  • (Array<String>) —

    frozen array of frozen binary signature strings



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ml_dsa.rb', line 104

def sign_many(operations, config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY)
  cfg = config || @config
  unless operations.is_a?(Array)
    raise TypeError, "operations must be an Array, got #{operations.class}"
  end
  return [].freeze if operations.empty?
  ops = operations.each_with_index.map do |op, i|
    Internal.maybe_yield(i, yield_every)
    normalize_sign_op(op, i, cfg)
  end
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
  result = _sign_many(ops)
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
  ps = operations.first&.sk&.param_set
  cfg.notify(:sign, ps, operations.size, duration)
  result
end

.subscribe(&block) ⇒ Object

Subscribe to instrumentation events on the default config.



34
35
36
# File 'lib/ml_dsa.rb', line 34

def self.subscribe(&block)
  @config.subscribe(&block)
end

.unsubscribe(subscriber) ⇒ Object

Remove a subscriber from the default config.



40
41
42
# File 'lib/ml_dsa.rb', line 40

def self.unsubscribe(subscriber)
  @config.unsubscribe(subscriber)
end

.verify_many(operations, config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY) ⇒ Array<Result>

Verify multiple signatures in a single GVL drop.

Returns Result objects with per-item details: .ok? indicates success, .reason distinguishes wrong-size signatures from cryptographic verification failures.

Parameters:

  • operations (Array<VerifyRequest>)
  • config (Config) (defaults to: nil) —

    configuration (default: MlDsa.config)

  • yield_every (Integer) (defaults to: Internal::DEFAULT_YIELD_EVERY) —

    yield to the fiber scheduler every N items during the normalization loop (0 = never, default)

Returns:

  • (Array<Result>) —

    frozen array of Result objects



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/ml_dsa.rb', line 133

def verify_many(operations, config: nil, yield_every: Internal::DEFAULT_YIELD_EVERY)
  cfg = config || @config
  unless operations.is_a?(Array)
    raise TypeError, "operations must be an Array, got #{operations.class}"
  end
  return [].freeze if operations.empty?
  # Pre-check signature sizes to distinguish size errors from crypto failures
  size_ok = operations.map do |op|
    Internal.resolve_ps(op.pk.param_set)
    op.signature.bytesize == op.pk.param_set.signature_bytes
  end
  ops = operations.each_with_index.map do |op, i|
    Internal.maybe_yield(i, yield_every)
    normalize_verify_op(op, i)
  end
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
  bools = _verify_many(ops)
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
  ps = operations.first&.pk&.param_set
  cfg.notify(:verify, ps, operations.size, duration)
  results = operations.each_with_index.map do |op, i|
    if bools[i]
      Result.new(value: true, ok: true, reason: nil)
    elsif !size_ok[i]
      expected = op.pk.param_set.signature_bytes
      Result.new(value: false, ok: false,
        reason: "wrong_signature_size: expected #{expected}, got #{op.signature.bytesize}")
    else
      Result.new(value: false, ok: false, reason: "verification_failed")
    end
  end
  results.freeze
end