Class: JWT::PQ::MlDsa

Inherits:
Object
  • Object
show all
Defined in:
lib/jwt/pq/ml_dsa.rb

Overview

Ruby wrapper around liboqs ML-DSA operations. Handles memory allocation, FFI calls, and cleanup.

Constant Summary collapse

ALGORITHMS =
{
  "ML-DSA-44" => { public_key: 1312, secret_key: 2560, signature: 2420, nist_level: 2 },
  "ML-DSA-65" => { public_key: 1952, secret_key: 4032, signature: 3309, nist_level: 3 },
  "ML-DSA-87" => { public_key: 2592, secret_key: 4896, signature: 4627, nist_level: 5 }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(algorithm) ⇒ MlDsa

Returns a new instance of MlDsa.



44
45
46
47
48
49
50
51
52
53
54
# File 'lib/jwt/pq/ml_dsa.rb', line 44

def initialize(algorithm)
  algorithm = algorithm.to_s
  unless ALGORITHMS.key?(algorithm)
    raise UnsupportedAlgorithmError,
          "Unsupported algorithm: #{algorithm}. " \
          "Supported: #{ALGORITHMS.keys.join(", ")}"
  end

  @algorithm = algorithm
  @sizes = ALGORITHMS[algorithm]
end

Instance Attribute Details

#algorithmObject (readonly)

Returns the value of attribute algorithm.



42
43
44
# File 'lib/jwt/pq/ml_dsa.rb', line 42

def algorithm
  @algorithm
end

Class Method Details

.sign_handle(algorithm) ⇒ Object



17
18
19
20
21
22
23
24
25
26
# File 'lib/jwt/pq/ml_dsa.rb', line 17

def self.sign_handle(algorithm)
  @sign_handles[algorithm] || @sign_handles_mutex.synchronize do
    @sign_handles[algorithm] ||= begin
      h = LibOQS.OQS_SIG_new(algorithm)
      raise LiboqsError, "Failed to initialize #{algorithm}" if h.null?

      h
    end
  end
end

.verify_handle(algorithm) ⇒ Object



31
32
33
34
35
36
37
38
39
40
# File 'lib/jwt/pq/ml_dsa.rb', line 31

def self.verify_handle(algorithm)
  @verify_handles[algorithm] || @verify_handles_mutex.synchronize do
    @verify_handles[algorithm] ||= begin
      h = LibOQS.OQS_SIG_new(algorithm)
      raise LiboqsError, "Failed to initialize #{algorithm}" if h.null?

      h
    end
  end
end

Instance Method Details

#keypairObject

Generate a new keypair. Returns [public_key_bytes, secret_key_bytes]



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/jwt/pq/ml_dsa.rb', line 58

def keypair
  sig = LibOQS.OQS_SIG_new(@algorithm)
  raise LiboqsError, "Failed to initialize #{@algorithm}" if sig.null?

  pk = FFI::MemoryPointer.new(:uint8, @sizes[:public_key])
  sk = FFI::MemoryPointer.new(:uint8, @sizes[:secret_key])

  status = LibOQS.OQS_SIG_keypair(sig, pk, sk)
  raise LiboqsError, "Keypair generation failed for #{@algorithm}" unless status == LibOQS::OQS_SUCCESS

  [pk.read_bytes(@sizes[:public_key]), sk.read_bytes(@sizes[:secret_key])]
ensure
  sk&.clear
  LibOQS.OQS_SIG_free(sig) if sig && !sig.null?
end

#public_key_sizeObject

Key sizes for this algorithm



129
130
131
# File 'lib/jwt/pq/ml_dsa.rb', line 129

def public_key_size
  @sizes[:public_key]
end

#secret_key_sizeObject



133
134
135
# File 'lib/jwt/pq/ml_dsa.rb', line 133

def secret_key_size
  @sizes[:secret_key]
end

#sign(message, secret_key) ⇒ Object

Sign a message with a secret key. Returns the signature bytes.



76
77
78
79
80
81
82
83
84
# File 'lib/jwt/pq/ml_dsa.rb', line 76

def sign(message, secret_key)
  validate_key_size!(secret_key, :secret_key)

  sk_buf = FFI::MemoryPointer.new(:uint8, secret_key.bytesize)
  sk_buf.put_bytes(0, secret_key)
  sign_with_sk_buffer(message, sk_buf)
ensure
  sk_buf&.clear
end

#sign_with_sk_buffer(message, sk_buf) ⇒ Object

Faster sign path: takes a pre-populated FFI::MemoryPointer holding the secret key. Caller is responsible for buffer lifecycle (allocation, zeroing). Used by JWT::PQ::Key to avoid re-allocating+copying the secret key on every sign call.

Raises:



90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/jwt/pq/ml_dsa.rb', line 90

def sign_with_sk_buffer(message, sk_buf)
  sig = self.class.sign_handle(@algorithm)
  sig_buf = FFI::MemoryPointer.new(:uint8, @sizes[:signature])
  sig_len = FFI::MemoryPointer.new(:size_t)
  msg_buf = FFI::MemoryPointer.from_string(message)

  status = LibOQS.OQS_SIG_sign(sig, sig_buf, sig_len,
                               msg_buf, message.bytesize, sk_buf)
  raise SignatureError, "Signing failed for #{@algorithm}" unless status == LibOQS::OQS_SUCCESS

  sig_buf.read_bytes(sig_len.read(:size_t))
end

#signature_sizeObject



137
138
139
# File 'lib/jwt/pq/ml_dsa.rb', line 137

def signature_size
  @sizes[:signature]
end

#verify(message, signature, public_key) ⇒ Object

Verify a signature against a message and public key. Returns true if valid, false otherwise.



105
106
107
108
109
110
111
# File 'lib/jwt/pq/ml_dsa.rb', line 105

def verify(message, signature, public_key)
  validate_key_size!(public_key, :public_key)

  pk_buf = FFI::MemoryPointer.new(:uint8, public_key.bytesize)
  pk_buf.put_bytes(0, public_key)
  verify_with_pk_buffer(message, signature, pk_buf)
end

#verify_with_pk_buffer(message, signature, pk_buf) ⇒ Object

Faster verify path: takes a pre-populated FFI::MemoryPointer holding the public key. Caller is responsible for buffer lifecycle. Used by JWT::PQ::Key to avoid re-allocating+copying the public key on every verify call.



117
118
119
120
121
122
123
124
125
126
# File 'lib/jwt/pq/ml_dsa.rb', line 117

def verify_with_pk_buffer(message, signature, pk_buf)
  sig = self.class.verify_handle(@algorithm)
  msg_buf = FFI::MemoryPointer.from_string(message)
  sig_buf = FFI::MemoryPointer.new(:uint8, signature.bytesize)
  sig_buf.put_bytes(0, signature)

  status = LibOQS.OQS_SIG_verify(sig, msg_buf, message.bytesize,
                                 sig_buf, signature.bytesize, pk_buf)
  status == LibOQS::OQS_SUCCESS
end