Class: MlDsa::PublicKey

Inherits:
Object
  • Object
show all
Defined in:
lib/ml_dsa/public_key.rb,
ext/ml_dsa/ml_dsa_ext.c

Overview

PublicKey — reopen the C TypedData class to add Ruby-level methods.

C methods: param_set, bytesize, to_bytes, to_hex, fingerprint, to_s, inspect, ==, eql?, hash, initialize_copy, _dump_data.

verify is defined HERE in Ruby — it delegates to verify_many (batch C API) with a single-element array. This eliminates a separate single-op verify C codepath that duplicated the batch logic.

Bytes live in C-managed memory with a stable pointer. dup/clone raise TypeError (C: initialize_copy). Marshal.dump raises TypeError (C: _dump_data).

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#created_at ⇒ Time (readonly)

Returns when this key was created (set by keygen/from_bytes/from_der/from_pem).

Returns:

  • (Time) —

    when this key was created (set by keygen/from_bytes/from_der/from_pem)



166
167
168
# File 'lib/ml_dsa/public_key.rb', line 166

def created_at
  @created_at
end

#key_usage ⇒ Symbol?

Returns application-defined usage label.

Returns:

  • (Symbol, nil) —

    application-defined usage label



169
170
171
# File 'lib/ml_dsa/public_key.rb', line 169

def key_usage
  @key_usage
end

Class Method Details

._from_bytes_raw(rb_raw, rb_ps_code) ⇒ Object



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'ext/ml_dsa/ml_dsa_ext.c', line 480

static VALUE pk_from_bytes_raw(VALUE klass, VALUE rb_raw, VALUE rb_ps_code)
{
    Check_Type(rb_raw, T_STRING);
    int ps_code = NUM2INT(rb_ps_code);
    const ml_dsa_impl_t *impl = find_impl(ps_code);

    if ((size_t)RSTRING_LEN(rb_raw) != impl->pk_len)
        rb_raise(rb_eArgError,
                 "expected %lu bytes for ML-DSA-%d, got %ld",
                 (unsigned long)impl->pk_len, ps_code, RSTRING_LEN(rb_raw));

    return pk_new_from_buf(klass,
                           (const uint8_t *)RSTRING_PTR(rb_raw),
                           (size_t)RSTRING_LEN(rb_raw),
                           ps_code);
}

.from_bytes(bytes, param_set = nil) ⇒ PublicKey

Deserialize a public key from raw binary bytes.

When param_set is omitted, the parameter set is auto-detected from the byte length (each ML-DSA parameter set has a unique PK size).

Parameters:

  • bytes (String)
  • param_set (ParameterSet, nil) (defaults to: nil) —

    auto-detected from size if omitted

Returns:

Raises:

  • (TypeError)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ml_dsa/public_key.rb', line 69

def self.from_bytes(bytes, param_set = nil)
  raise TypeError, "bytes must be a String, got #{bytes.class}" unless bytes.is_a?(String)
  if param_set
    ps = Internal.resolve_ps(param_set)
    unless bytes.bytesize == ps.public_key_bytes
      raise ArgumentError,
        "expected #{ps.public_key_bytes} bytes for #{ps.name}, " \
        "got #{bytes.bytesize}"
    end
  else
    ps = PARAM_SET_BY_PK_SIZE[bytes.bytesize]
    unless ps
      raise ArgumentError,
        "cannot auto-detect parameter set from #{bytes.bytesize}-byte public key " \
        "(expected #{PARAM_SET_BY_PK_SIZE.keys.sort.join(", ")})"
    end
  end
  pk = _from_bytes_raw(bytes.b, ps.code)
  pk.instance_variable_set(:@created_at, Time.now.freeze)
  pk
end

.from_der(der) ⇒ PublicKey

Deserialize a public key from SubjectPublicKeyInfo DER.

Parameters:

  • der (String) —

    DER-encoded SPKI

Returns:

Raises:



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/ml_dsa/public_key.rb', line 104

def self.from_der(der)
  raise TypeError, "der must be a String, got #{der.class}" unless der.is_a?(String)
  begin
    info = PqcAsn1::DER.parse_spki(der)
  rescue PqcAsn1::ParseError, PqcAsn1::Error => e
    Internal.raise_deser("DER", e.respond_to?(:offset) ? e.offset : nil,
      e.respond_to?(:code) ? e.code.to_s : "parse_error", e.message)
  end
  oid_code = ML_DSA_OID_TO_CODE[info.oid.dotted]
  unless oid_code
    Internal.raise_deser("DER", nil, "unknown_oid",
      "unknown ML-DSA OID: #{info.oid.dotted}")
  end
  ps = Internal.param_set_for_code(oid_code)
  unless info.key.bytesize == ps.public_key_bytes
    Internal.raise_deser("DER", nil, "wrong_key_size",
      "invalid DER: public key is #{info.key.bytesize} bytes, " \
      "expected #{ps.public_key_bytes} for #{ps.name}")
  end
  pk = from_bytes(info.key, ps)
  pk.instance_variable_set(:@created_at, Time.now.freeze)
  pk
end

.from_hex(hex, param_set = nil) ⇒ PublicKey

Deserialize a public key from a lowercase or uppercase hex string.

Parameters:

  • hex (String)
  • param_set (ParameterSet, nil) (defaults to: nil) —

    auto-detected from size if omitted

Returns:



96
97
98
# File 'lib/ml_dsa/public_key.rb', line 96

def self.from_hex(hex, param_set = nil)
  from_bytes(Internal.decode_hex(hex), param_set)
end

.from_pem(pem) ⇒ PublicKey

Deserialize a public key from PEM-encoded SubjectPublicKeyInfo.

Parameters:

  • pem (String) —

    PEM-encoded public key

Returns:

Raises:



132
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
# File 'lib/ml_dsa/public_key.rb', line 132

def self.from_pem(pem)
  raise TypeError, "pem must be a String, got #{pem.class}" unless pem.is_a?(String)
  begin
    result = PqcAsn1::PEM.decode_auto(pem)
  rescue PqcAsn1::ParseError, PqcAsn1::Error => e
    Internal.raise_deser("PEM", nil, "missing_armor", e.message)
  end
  unless result.label == "PUBLIC KEY"
    Internal.raise_deser("PEM", nil, "wrong_label",
      "invalid PEM: expected PUBLIC KEY, found #{result.label}")
  end
  begin
    info = PqcAsn1::DER.parse_spki(result.data)
  rescue PqcAsn1::ParseError, PqcAsn1::Error => e
    Internal.raise_deser("PEM", e.respond_to?(:offset) ? e.offset : nil,
      e.respond_to?(:code) ? e.code.to_s : "parse_error", e.message)
  end
  oid_code = ML_DSA_OID_TO_CODE[info.oid.dotted]
  unless oid_code
    Internal.raise_deser("PEM", nil, "unknown_oid",
      "unknown ML-DSA OID: #{info.oid.dotted}")
  end
  ps = Internal.param_set_for_code(oid_code)
  unless info.key.bytesize == ps.public_key_bytes
    Internal.raise_deser("PEM", nil, "wrong_key_size",
      "invalid PEM: public key is #{info.key.bytesize} bytes, " \
      "expected #{ps.public_key_bytes} for #{ps.name}")
  end
  pk = from_bytes(info.key, ps)
  pk.instance_variable_set(:@created_at, Time.now.freeze)
  pk
end

Instance Method Details

#==(other) ⇒ Object



458
459
460
461
462
463
464
465
466
# File 'ext/ml_dsa/ml_dsa_ext.c', line 458

static VALUE pk_equal(VALUE self, VALUE other)
{
    if (!rb_obj_is_kind_of(other, rb_cPublicKey)) return Qfalse;
    ml_dsa_pk_t *d1, *d2;
    TypedData_Get_Struct(self,  ml_dsa_pk_t, &ml_dsa_pk_type, d1);
    TypedData_Get_Struct(other, ml_dsa_pk_t, &ml_dsa_pk_type, d2);
    if (d1->len != d2->len || d1->ps_code != d2->ps_code) return Qfalse;
    return (memcmp(d1->bytes, d2->bytes, d1->len) == 0) ? Qtrue : Qfalse;
}

#_dump_data ⇒ Object

Marshal prevention — TypedData has no default marshal support



508
509
510
511
512
513
514
515
# File 'ext/ml_dsa/ml_dsa_ext.c', line 508

static VALUE pk_dump_data(VALUE self)
{
    (void)self;
    rb_raise(rb_eTypeError,
             "MlDsa::PublicKey cannot be marshalled; "
             "use to_der/from_der or to_bytes/from_bytes for serialization");
    return Qnil;
}

#bytesize ⇒ Object



393
394
395
396
397
398
# File 'ext/ml_dsa/ml_dsa_ext.c', line 393

static VALUE pk_bytesize(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    return SIZET2NUM(d->len);
}

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


468
469
470
471
# File 'ext/ml_dsa/ml_dsa_ext.c', line 468

static VALUE pk_eql(VALUE self, VALUE other)
{
    return pk_equal(self, other);
}

#fingerprint ⇒ Object

Lazy-computed fingerprint: first 16 bytes (32 hex chars) of SHA-256 of the raw public key bytes. Cached in the C struct.



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'ext/ml_dsa/ml_dsa_ext.c', line 419

static VALUE pk_fingerprint(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    if (!NIL_P(d->fingerprint)) return d->fingerprint;

    /* rb_require is idempotent; calling it every time is safe but we
     * only get here once per PK object. */
    rb_require("digest/sha2");
    VALUE rb_digest = rb_path2class("Digest::SHA256");
    VALUE raw = rb_str_new((const char *)d->bytes, (long)d->len);
    VALUE hex = rb_funcall(rb_digest, rb_intern("hexdigest"), 1, raw);
    VALUE prefix = rb_str_substr(hex, 0, 32);
    OBJ_FREEZE(prefix);
    RB_OBJ_WRITE(self, &d->fingerprint, prefix);
    return d->fingerprint;
}

#hash ⇒ Object



473
474
475
476
477
478
# File 'ext/ml_dsa/ml_dsa_ext.c', line 473

static VALUE pk_hash(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    return hash_key_bytes(d->ps_code, d->bytes, d->len);
}

#initialize_copy(orig) ⇒ Object

dup/clone prevention — alloc creates NULL-bytes objects which silently break



498
499
500
501
502
503
504
505
# File 'ext/ml_dsa/ml_dsa_ext.c', line 498

static VALUE pk_initialize_copy(VALUE self, VALUE orig)
{
    (void)self; (void)orig;
    rb_raise(rb_eTypeError,
             "MlDsa::PublicKey cannot be duplicated; "
             "use from_bytes or from_der to create a copy");
    return Qnil;
}

#inspect ⇒ Object



437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'ext/ml_dsa/ml_dsa_ext.c', line 437

static VALUE pk_inspect(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    if (d->len == 0)
        return rb_str_new_cstr("#<MlDsa::PublicKey [uninitialized]>");
    size_t show = d->len < 8 ? d->len : 8;
    VALUE prefix_hex = bytes_to_hex_value(d->bytes, show);
    VALUE ps = lookup_param_set(d->ps_code);
    VALUE ps_name = rb_funcall(ps, id_name, 0);
    VALUE result = rb_sprintf("#<MlDsa::PublicKey %"PRIsVALUE" %"PRIsVALUE"\xe2\x80\xa6>",
                              ps_name, prefix_hex);
    RB_GC_GUARD(prefix_hex);
    return result;
}

#param_set ⇒ Object

==================================================================



386
387
388
389
390
391
# File 'ext/ml_dsa/ml_dsa_ext.c', line 386

static VALUE pk_param_set(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    return lookup_param_set(d->ps_code);
}

#to_bytes ⇒ Object



400
401
402
403
404
405
406
407
408
# File 'ext/ml_dsa/ml_dsa_ext.c', line 400

static VALUE pk_to_bytes(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    VALUE s = rb_str_new((const char *)d->bytes, (long)d->len);
    rb_enc_associate(s, rb_ascii8bit_encoding());
    OBJ_FREEZE(s);
    return s;
}

#to_der ⇒ String

Build SubjectPublicKeyInfo DER using the pqc_asn1 gem.

Returns:

  • (String) —

    frozen binary DER (ASCII-8BIT)



47
48
49
50
# File 'lib/ml_dsa/public_key.rb', line 47

def to_der
  oid = PqcAsn1::OID[ML_DSA_OIDS[param_set.code]]
  PqcAsn1::DER.build_spki(oid, to_bytes, validate: false)
end

#to_hex ⇒ Object



410
411
412
413
414
415
# File 'ext/ml_dsa/ml_dsa_ext.c', line 410

static VALUE pk_to_hex(VALUE self)
{
    ml_dsa_pk_t *d;
    TypedData_Get_Struct(self, ml_dsa_pk_t, &ml_dsa_pk_type, d);
    return bytes_to_hex_value(d->bytes, d->len);
}

#to_pem ⇒ String

Build PEM-encoded SubjectPublicKeyInfo using the pqc_asn1 gem.

Returns:

  • (String) —

    frozen PEM string



54
55
56
# File 'lib/ml_dsa/public_key.rb', line 54

def to_pem
  PqcAsn1::PEM.encode(to_der, "PUBLIC KEY")
end

#to_s ⇒ Object



453
454
455
456
# File 'ext/ml_dsa/ml_dsa_ext.c', line 453

static VALUE pk_to_s(VALUE self)
{
    return pk_inspect(self);
}

#verify(message, signature, context: "") ⇒ Boolean

Verify a signature.

Delegates to the batch C API (verify_many) with a single element. This avoids maintaining a separate single-op C verify path.

Returns false (not raises) for: wrong-size signature, context >255 bytes, or cryptographic verification failure. Raises TypeError for non-String message/signature/context.

Parameters:

  • message (String)
  • signature (String)
  • context (String) (defaults to: "") —

    FIPS 204 context string (0..255 bytes)

Returns:

  • (Boolean)

Raises:

  • (TypeError)


31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/ml_dsa/public_key.rb', line 31

def verify(message, signature, context: "")
  raise TypeError, "message must be a String, got #{message.class}" unless message.is_a?(String)
  raise TypeError, "signature must be a String, got #{signature.class}" unless signature.is_a?(String)
  raise TypeError, "context must be a String, got #{context.class}" unless context.is_a?(String)
  # Context >255 bytes can never verify per FIPS 204 — return false
  # rather than raising, since verify is a predicate.
  return false unless context.bytesize <= 255
  # Early-reject wrong-size signatures without entering C
  return false unless signature.bytesize == param_set.signature_bytes
  req = VerifyRequest.new(pk: self, message: message,
    signature: signature, context: context)
  MlDsa.verify_many([req]).first.ok?
end