Class: MlDsa::PublicKey
- Inherits:
-
Object
- Object
- MlDsa::PublicKey
- 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
-
#created_at ⇒ Time
readonly
When this key was created (set by keygen/from_bytes/from_der/from_pem).
-
#key_usage ⇒ Symbol?
Application-defined usage label.
Class Method Summary collapse
- ._from_bytes_raw(rb_raw, rb_ps_code) ⇒ Object
-
.from_bytes(bytes, param_set = nil) ⇒ PublicKey
Deserialize a public key from raw binary bytes.
-
.from_der(der) ⇒ PublicKey
Deserialize a public key from SubjectPublicKeyInfo DER.
-
.from_hex(hex, param_set = nil) ⇒ PublicKey
Deserialize a public key from a lowercase or uppercase hex string.
-
.from_pem(pem) ⇒ PublicKey
Deserialize a public key from PEM-encoded SubjectPublicKeyInfo.
Instance Method Summary collapse
- #==(other) ⇒ Object
-
#_dump_data ⇒ Object
Marshal prevention — TypedData has no default marshal support.
- #bytesize ⇒ Object
- #eql?(other) ⇒ Boolean
-
#fingerprint ⇒ Object
Lazy-computed fingerprint: first 16 bytes (32 hex chars) of SHA-256 of the raw public key bytes.
- #hash ⇒ Object
-
#initialize_copy(orig) ⇒ Object
dup/clone prevention — alloc creates NULL-bytes objects which silently break.
- #inspect ⇒ Object
-
#param_set ⇒ Object
==================================================================.
- #to_bytes ⇒ Object
-
#to_der ⇒ String
Build SubjectPublicKeyInfo DER using the pqc_asn1 gem.
- #to_hex ⇒ Object
-
#to_pem ⇒ String
Build PEM-encoded SubjectPublicKeyInfo using the pqc_asn1 gem.
- #to_s ⇒ Object
-
#verify(message, signature, context: "") ⇒ Boolean
Verify a signature.
Instance Attribute Details
#created_at ⇒ Time (readonly)
Returns 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.
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).
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.
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.) 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.
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.
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.) 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.) 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
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.
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.
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.
31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/ml_dsa/public_key.rb', line 31 def verify(, signature, context: "") raise TypeError, "message must be a String, got #{.class}" unless .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: , signature: signature, context: context) MlDsa.verify_many([req]).first.ok? end |