jwt-pq 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc80be78087261982368d78df297e89720290aa2c1252ed7b3b1f44298dd4856
4
- data.tar.gz: f4c95127b8f0aab465029c1eedff1e0ef37c6e1342348f3286bc396e26ef6961
3
+ metadata.gz: 9fc1d22df85af9d7c753800dbd77e08ce874d7c1bf177aa9afafab54a3ef6aa9
4
+ data.tar.gz: 3f8d5ef4114c28e79eeb609eac5bcff0b5e5bbe0b4869006b20e1e076cf7142f
5
5
  SHA512:
6
- metadata.gz: 3498399528c54d624c93ca00f1c6a884cb6db6591dd861cc26409aa58502a8ecf1ae1bd6901c7fb5b8d1ebe44cd5462661757b299dc8f7127100b60d9153d5ed
7
- data.tar.gz: 22a25bf0c7f3562f226a88832dba44ee164964328dcf2c2841a0c6ea286a585bd7f85c5237d5356d6272fc1ff3ef1ca3cf63f5b4057e91ef8895a22a2d75ff3b
6
+ metadata.gz: 333d7706c667a66e7858813a4de3b813d46c097b283822ed228f3fa23bd5a58fe67dfd0b2cc3a0b7a3e71aceac70ff604c3d8084871828585ce633beba6f7199
7
+ data.tar.gz: d4ba329c764b33f0aea5dd37f10d0c9eb9623eb34d999bc003382096a18c58e2b914ad51fdd7e472b0e400a5504690acb21744a922944ded0b67cb5ee9bf9e70
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-04-19
11
+
12
+ ### Added
13
+
14
+ - Sign-throughput benchmark at `bench/sign_throughput.rb` with a fixed PEM key fixture (`bench/fixtures/ml_dsa_65_sk.pem`), driven by `benchmark-ips`
15
+ - Verify-throughput benchmark at `bench/verify_throughput.rb`
16
+ - NIST ACVP sigVer KAT tests at `spec/jwt/pq/kat_spec.rb` — external interface, pure ML-DSA, empty context; covers ML-DSA-44, ML-DSA-65, and ML-DSA-87 with both passing and known-bad signatures as a canonical correctness gate
17
+ - `JWT::PQ::MlDsa#sign_with_sk_buffer` and `#verify_with_pk_buffer` — fast paths that accept pre-populated FFI buffers. The existing bytes-in `#sign` / `#verify` APIs are unchanged
18
+
19
+ ### Changed
20
+
21
+ - **ML-DSA signing throughput: +2.6%** (from 6676 to 6849 sigs/s on Ruby 3.4.6 + liboqs 0.15.0 for ML-DSA-65). Class-level cache of the `OQS_SIG` handle per algorithm avoids `OQS_SIG_new`/`OQS_SIG_free` per call; per-`Key` memoization of the secret-key FFI buffer avoids a 4032-byte allocation + copy per sign
22
+ - **ML-DSA verification throughput: +19.4%** (from 7995 to 9548 verifies/s on the same setup for ML-DSA-65). Class-level cache of the `OQS_SIG` handle for verify; per-`Key` memoization of the public-key FFI buffer; inlined type-check in the JWA verify entry point. `Key#verify` now reaches 93% of the raw `OQS_SIG_verify` ceiling; remaining overhead lives inside `ruby-jwt`
23
+ - `Key#destroy!` now also zeroes the cached secret-key FFI buffer (`@sk_buffer`) in addition to `@private_key`, preserving the secure-erase contract after the buffer memoization
24
+
25
+ ### Dependencies
26
+
27
+ - Add `benchmark-ips ~> 2.14` as a development/test dependency (powers the bench harnesses)
28
+ - Bump `ruby/setup-ruby` from 1.299.0 to 1.301.0 (#2)
29
+
10
30
  ## [0.2.0] - 2026-04-06
11
31
 
12
32
  ### Added
@@ -56,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
56
76
  - Optional dependency on jwt-eddsa / ed25519
57
77
  - Error classes: `LiboqsError`, `KeyError`, `SignatureError`, `MissingDependencyError`
58
78
 
59
- [Unreleased]: https://github.com/marcelopazzo/jwt-pq/compare/v0.2.0...HEAD
79
+ [Unreleased]: https://github.com/marcelopazzo/jwt-pq/compare/v0.3.0...HEAD
80
+ [0.3.0]: https://github.com/marcelopazzo/jwt-pq/compare/v0.2.0...v0.3.0
60
81
  [0.2.0]: https://github.com/marcelopazzo/jwt-pq/compare/v0.1.0...v0.2.0
61
82
  [0.1.0]: https://github.com/marcelopazzo/jwt-pq/releases/tag/v0.1.0
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  group :development, :test do
8
+ gem "benchmark-ips", "~> 2.14"
8
9
  gem "rake"
9
10
  gem "rspec", "~> 3.13"
10
11
  gem "rubocop", "~> 1.75"
@@ -0,0 +1,128 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ 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
127
+ aEqLBCQtoXub0y7W
128
+ -----END PRIVATE KEY-----
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+ require "jwt"
5
+ require "jwt/pq"
6
+
7
+ ALG = "ML-DSA-65"
8
+ PAYLOAD = { sub: "user-123", iat: 1_700_000_000, exp: 1_700_003_600 }.freeze
9
+ FIXTURE = File.expand_path("fixtures/ml_dsa_65_sk.pem", __dir__)
10
+
11
+ abort "Missing bench fixture: #{FIXTURE}" unless File.exist?(FIXTURE)
12
+ key = JWT::PQ::Key.from_pem(File.read(FIXTURE))
13
+
14
+ 100.times { JWT.encode(PAYLOAD, key, ALG) }
15
+
16
+ report = Benchmark.ips(quiet: true) do |x|
17
+ x.config(time: 5, warmup: 2)
18
+ x.report("sign") { JWT.encode(PAYLOAD, key, ALG) }
19
+ end
20
+
21
+ entry = report.entries.first
22
+ ips = entry.stats.central_tendency
23
+ us_per_op = 1_000_000.0 / ips
24
+
25
+ puts "METRIC sigs_per_sec=#{ips.round(2)}"
26
+ puts "METRIC us_per_sig=#{us_per_op.round(2)}"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+ require "jwt"
5
+ require "jwt/pq"
6
+
7
+ ALG = "ML-DSA-65"
8
+ PAYLOAD = { sub: "user-123", iat: 1_700_000_000, exp: 1_700_003_600 }.freeze
9
+ FIXTURE = File.expand_path("fixtures/ml_dsa_65_sk.pem", __dir__)
10
+
11
+ abort "Missing bench fixture: #{FIXTURE}" unless File.exist?(FIXTURE)
12
+ key = JWT::PQ::Key.from_pem(File.read(FIXTURE))
13
+ pub_key = JWT::PQ::Key.from_public_key(ALG, key.public_key)
14
+
15
+ TOKEN = JWT.encode(PAYLOAD, key, ALG)
16
+
17
+ 100.times { JWT.decode(TOKEN, pub_key, true, algorithms: [ALG], verify_expiration: false) }
18
+
19
+ report = Benchmark.ips(quiet: true) do |x|
20
+ x.config(time: 5, warmup: 2)
21
+ x.report("verify") { JWT.decode(TOKEN, pub_key, true, algorithms: [ALG], verify_expiration: false) }
22
+ end
23
+
24
+ entry = report.entries.first
25
+ ips = entry.stats.central_tendency
26
+ us_per_op = 1_000_000.0 / ips
27
+
28
+ puts "METRIC verifies_per_sec=#{ips.round(2)}"
29
+ puts "METRIC us_per_verify=#{us_per_op.round(2)}"
@@ -20,8 +20,13 @@ module JWT
20
20
  end
21
21
 
22
22
  def verify(data:, signature:, verification_key:)
23
- key = resolve_verification_key(verification_key)
24
- key.verify(data, signature)
23
+ unless verification_key.is_a?(JWT::PQ::Key)
24
+ raise_verify_error!(
25
+ "Expected a JWT::PQ::Key, got #{verification_key.class}. " \
26
+ "Use JWT::PQ::Key.generate(:#{alg_symbol}) to create a key."
27
+ )
28
+ end
29
+ verification_key.verify(data, signature)
25
30
  rescue JWT::PQ::Error
26
31
  false
27
32
  end
data/lib/jwt/pq/key.rb CHANGED
@@ -6,7 +6,7 @@ module JWT
6
6
  module PQ
7
7
  # Represents an ML-DSA keypair (public + optional private key).
8
8
  # Used as the signing/verification key for JWT operations.
9
- class Key
9
+ class Key # rubocop:disable Metrics/ClassLength
10
10
  ALGORITHM_ALIASES = {
11
11
  ml_dsa_44: "ML-DSA-44",
12
12
  ml_dsa_65: "ML-DSA-65",
@@ -50,12 +50,12 @@ module JWT
50
50
  def sign(data)
51
51
  raise KeyError, "Private key not available — cannot sign" unless @private_key
52
52
 
53
- @ml_dsa.sign(data, @private_key)
53
+ @ml_dsa.sign_with_sk_buffer(data, sk_buffer)
54
54
  end
55
55
 
56
56
  # Verify a signature using the public key.
57
57
  def verify(data, signature)
58
- @ml_dsa.verify(data, signature, @public_key)
58
+ @ml_dsa.verify_with_pk_buffer(data, signature, pk_buffer)
59
59
  end
60
60
 
61
61
  # Whether this key can be used for signing.
@@ -70,6 +70,8 @@ module JWT
70
70
  @private_key.replace("\0" * @private_key.bytesize)
71
71
  @private_key = nil
72
72
  end
73
+ @sk_buffer&.clear
74
+ @sk_buffer = nil
73
75
  true
74
76
  end
75
77
 
@@ -153,6 +155,14 @@ module JWT
153
155
 
154
156
  private
155
157
 
158
+ def sk_buffer
159
+ @sk_buffer ||= FFI::MemoryPointer.new(:uint8, @private_key.bytesize).put_bytes(0, @private_key)
160
+ end
161
+
162
+ def pk_buffer
163
+ @pk_buffer ||= FFI::MemoryPointer.new(:uint8, @public_key.bytesize).put_bytes(0, @public_key)
164
+ end
165
+
156
166
  def resolve_algorithm(algorithm)
157
167
  self.class.send(:resolve_algorithm, algorithm)
158
168
  end
data/lib/jwt/pq/ml_dsa.rb CHANGED
@@ -11,6 +11,34 @@ module JWT
11
11
  "ML-DSA-87" => { public_key: 2592, secret_key: 4896, signature: 4627, nist_level: 5 }
12
12
  }.freeze
13
13
 
14
+ @sign_handles = {}
15
+ @sign_handles_mutex = Mutex.new
16
+
17
+ def self.sign_handle(algorithm)
18
+ @sign_handles[algorithm] || @sign_handles_mutex.synchronize do
19
+ @sign_handles[algorithm] ||= begin
20
+ h = LibOQS.OQS_SIG_new(algorithm)
21
+ raise LiboqsError, "Failed to initialize #{algorithm}" if h.null?
22
+
23
+ h
24
+ end
25
+ end
26
+ end
27
+
28
+ @verify_handles = {}
29
+ @verify_handles_mutex = Mutex.new
30
+
31
+ def self.verify_handle(algorithm)
32
+ @verify_handles[algorithm] || @verify_handles_mutex.synchronize do
33
+ @verify_handles[algorithm] ||= begin
34
+ h = LibOQS.OQS_SIG_new(algorithm)
35
+ raise LiboqsError, "Failed to initialize #{algorithm}" if h.null?
36
+
37
+ h
38
+ end
39
+ end
40
+ end
41
+
14
42
  attr_reader :algorithm
15
43
 
16
44
  def initialize(algorithm)
@@ -48,24 +76,28 @@ module JWT
48
76
  def sign(message, secret_key)
49
77
  validate_key_size!(secret_key, :secret_key)
50
78
 
51
- sig = LibOQS.OQS_SIG_new(@algorithm)
52
- raise LiboqsError, "Failed to initialize #{@algorithm}" if sig.null?
79
+ sk_buf = FFI::MemoryPointer.new(:uint8, secret_key.bytesize)
80
+ sk_buf.put_bytes(0, secret_key)
81
+ sign_with_sk_buffer(message, sk_buf)
82
+ ensure
83
+ sk_buf&.clear
84
+ end
53
85
 
86
+ # Faster sign path: takes a pre-populated FFI::MemoryPointer holding the
87
+ # secret key. Caller is responsible for buffer lifecycle (allocation,
88
+ # zeroing). Used by JWT::PQ::Key to avoid re-allocating+copying the
89
+ # secret key on every sign call.
90
+ def sign_with_sk_buffer(message, sk_buf)
91
+ sig = self.class.sign_handle(@algorithm)
54
92
  sig_buf = FFI::MemoryPointer.new(:uint8, @sizes[:signature])
55
93
  sig_len = FFI::MemoryPointer.new(:size_t)
56
94
  msg_buf = FFI::MemoryPointer.from_string(message)
57
- sk_buf = FFI::MemoryPointer.new(:uint8, secret_key.bytesize)
58
- sk_buf.put_bytes(0, secret_key)
59
95
 
60
96
  status = LibOQS.OQS_SIG_sign(sig, sig_buf, sig_len,
61
97
  msg_buf, message.bytesize, sk_buf)
62
98
  raise SignatureError, "Signing failed for #{@algorithm}" unless status == LibOQS::OQS_SUCCESS
63
99
 
64
- actual_len = sig_len.read(:size_t)
65
- sig_buf.read_bytes(actual_len)
66
- ensure
67
- sk_buf&.clear
68
- LibOQS.OQS_SIG_free(sig) if sig && !sig.null?
100
+ sig_buf.read_bytes(sig_len.read(:size_t))
69
101
  end
70
102
 
71
103
  # Verify a signature against a message and public key.
@@ -73,20 +105,24 @@ module JWT
73
105
  def verify(message, signature, public_key)
74
106
  validate_key_size!(public_key, :public_key)
75
107
 
76
- sig = LibOQS.OQS_SIG_new(@algorithm)
77
- raise LiboqsError, "Failed to initialize #{@algorithm}" if sig.null?
108
+ pk_buf = FFI::MemoryPointer.new(:uint8, public_key.bytesize)
109
+ pk_buf.put_bytes(0, public_key)
110
+ verify_with_pk_buffer(message, signature, pk_buf)
111
+ end
78
112
 
113
+ # Faster verify path: takes a pre-populated FFI::MemoryPointer holding
114
+ # the public key. Caller is responsible for buffer lifecycle. Used by
115
+ # JWT::PQ::Key to avoid re-allocating+copying the public key on every
116
+ # verify call.
117
+ def verify_with_pk_buffer(message, signature, pk_buf)
118
+ sig = self.class.verify_handle(@algorithm)
79
119
  msg_buf = FFI::MemoryPointer.from_string(message)
80
120
  sig_buf = FFI::MemoryPointer.new(:uint8, signature.bytesize)
81
121
  sig_buf.put_bytes(0, signature)
82
- pk_buf = FFI::MemoryPointer.new(:uint8, public_key.bytesize)
83
- pk_buf.put_bytes(0, public_key)
84
122
 
85
123
  status = LibOQS.OQS_SIG_verify(sig, msg_buf, message.bytesize,
86
124
  sig_buf, signature.bytesize, pk_buf)
87
125
  status == LibOQS::OQS_SUCCESS
88
- ensure
89
- LibOQS.OQS_SIG_free(sig) if sig && !sig.null?
90
126
  end
91
127
 
92
128
  # Key sizes for this algorithm
@@ -2,6 +2,6 @@
2
2
 
3
3
  module JWT
4
4
  module PQ
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt-pq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcelo Almeida
@@ -66,6 +66,9 @@ files:
66
66
  - LICENSE
67
67
  - README.md
68
68
  - Rakefile
69
+ - bench/fixtures/ml_dsa_65_sk.pem
70
+ - bench/sign_throughput.rb
71
+ - bench/verify_throughput.rb
69
72
  - bin/smoke.rb
70
73
  - ext/jwt/pq/extconf.rb
71
74
  - jwt-pq.gemspec