jwt-pq 0.1.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 +4 -4
- data/CHANGELOG.md +62 -0
- data/Gemfile +3 -0
- data/README.md +25 -24
- data/Rakefile +8 -0
- data/bench/fixtures/ml_dsa_65_sk.pem +128 -0
- data/bench/sign_throughput.rb +26 -0
- data/bench/verify_throughput.rb +29 -0
- data/bin/smoke.rb +40 -0
- data/ext/jwt/pq/extconf.rb +193 -0
- data/jwt-pq.gemspec +7 -4
- data/lib/jwt/pq/algorithms/ml_dsa.rb +22 -5
- data/lib/jwt/pq/hybrid_key.rb +17 -0
- data/lib/jwt/pq/jwk.rb +10 -6
- data/lib/jwt/pq/key.rb +30 -5
- data/lib/jwt/pq/liboqs.rb +23 -6
- data/lib/jwt/pq/ml_dsa.rb +52 -14
- data/lib/jwt/pq/version.rb +1 -1
- metadata +15 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fc1d22df85af9d7c753800dbd77e08ce874d7c1bf177aa9afafab54a3ef6aa9
|
|
4
|
+
data.tar.gz: 3f8d5ef4114c28e79eeb609eac5bcff0b5e5bbe0b4869006b20e1e076cf7142f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 333d7706c667a66e7858813a4de3b813d46c097b283822ed228f3fa23bd5a58fe67dfd0b2cc3a0b7a3e71aceac70ff604c3d8084871828585ce633beba6f7199
|
|
7
|
+
data.tar.gz: d4ba329c764b33f0aea5dd37f10d0c9eb9623eb34d999bc003382096a18c58e2b914ad51fdd7e472b0e400a5504690acb21744a922944ded0b67cb5ee9bf9e70
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
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
|
+
|
|
30
|
+
## [0.2.0] - 2026-04-06
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Vendored liboqs build — `gem install jwt-pq` now compiles liboqs from source automatically
|
|
35
|
+
- `Key#destroy!` and `HybridKey#destroy!` for explicit zeroization of private key material
|
|
36
|
+
- `--use-system-libraries` escape hatch for users with pre-installed liboqs
|
|
37
|
+
- `JWT_PQ_LIBOQS_SOURCE` env var for air-gapped environments
|
|
38
|
+
- Path traversal protection in tarball extraction (defense-in-depth)
|
|
39
|
+
- Smoke test job in CI (builds gem, installs, runs end-to-end verification)
|
|
40
|
+
- Weekly CI schedule to catch dependency breakage
|
|
41
|
+
- Dependabot for automated dependency updates
|
|
42
|
+
- Secret scanning and push protection
|
|
43
|
+
- Code coverage with SimpleCov and Codecov
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- CMake and a C compiler (gcc/clang) are now required at install time
|
|
48
|
+
- `Key#inspect` and `HybridKey#inspect` no longer expose private key material
|
|
49
|
+
- `Key.resolve_algorithm` is now a private class method
|
|
50
|
+
- `JWK::ALGORITHMS` derived from `MlDsa::ALGORITHMS` (single source of truth)
|
|
51
|
+
- Pin CI actions to commit SHAs for security
|
|
52
|
+
- Use `Net::HTTP` instead of `URI.open` for tarball download
|
|
53
|
+
- Restrict CI workflow GITHUB_TOKEN permissions to `contents: read`
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- ML-DSA verify with invalid key type now raises `DecodeError` instead of `EncodeError`
|
|
58
|
+
- JWK import now validates missing `pub` field and malformed base64url input
|
|
59
|
+
- FFI memory holding secret keys is now zeroed after use
|
|
60
|
+
|
|
61
|
+
### Dependencies
|
|
62
|
+
|
|
63
|
+
- Bump codecov/codecov-action from 5.5.4 to 6.0.0
|
|
64
|
+
|
|
8
65
|
## [0.1.0] - 2026-04-04
|
|
9
66
|
|
|
10
67
|
### Added
|
|
@@ -18,3 +75,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
75
|
- Concatenated signature format: Ed25519 (64B) || ML-DSA
|
|
19
76
|
- Optional dependency on jwt-eddsa / ed25519
|
|
20
77
|
- Error classes: `LiboqsError`, `KeyError`, `SignatureError`, `MissingDependencyError`
|
|
78
|
+
|
|
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
|
|
81
|
+
[0.2.0]: https://github.com/marcelopazzo/jwt-pq/compare/v0.1.0...v0.2.0
|
|
82
|
+
[0.1.0]: https://github.com/marcelopazzo/jwt-pq/releases/tag/v0.1.0
|
data/Gemfile
CHANGED
|
@@ -5,6 +5,8 @@ source "https://rubygems.org"
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
7
|
group :development, :test do
|
|
8
|
+
gem "benchmark-ips", "~> 2.14"
|
|
9
|
+
gem "rake"
|
|
8
10
|
gem "rspec", "~> 3.13"
|
|
9
11
|
gem "rubocop", "~> 1.75"
|
|
10
12
|
gem "rubocop-rspec", "~> 3.0"
|
|
@@ -13,4 +15,5 @@ end
|
|
|
13
15
|
group :test do
|
|
14
16
|
gem "jwt-eddsa", "~> 0.9"
|
|
15
17
|
gem "simplecov", require: false
|
|
18
|
+
gem "simplecov-cobertura", require: false
|
|
16
19
|
end
|
data/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# jwt-pq
|
|
2
2
|
|
|
3
|
+
[](https://rubygems.org/gems/jwt-pq)
|
|
4
|
+
[](https://github.com/marcelopazzo/jwt-pq/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/marcelopazzo/jwt-pq)
|
|
6
|
+
|
|
3
7
|
Post-quantum JWT signatures for Ruby. Adds **ML-DSA** (FIPS 204) support to the [ruby-jwt](https://github.com/jwt/ruby-jwt) ecosystem, with an optional **hybrid EdDSA + ML-DSA** mode.
|
|
4
8
|
|
|
5
9
|
## Features
|
|
@@ -13,27 +17,7 @@ Post-quantum JWT signatures for Ruby. Adds **ML-DSA** (FIPS 204) support to the
|
|
|
13
17
|
## Requirements
|
|
14
18
|
|
|
15
19
|
- Ruby >= 3.2
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
### Installing liboqs
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
# macOS
|
|
22
|
-
brew install cmake ninja
|
|
23
|
-
git clone --depth 1 https://github.com/open-quantum-safe/liboqs
|
|
24
|
-
cd liboqs && mkdir build && cd build
|
|
25
|
-
cmake -GNinja -DBUILD_SHARED_LIBS=ON ..
|
|
26
|
-
ninja && sudo ninja install
|
|
27
|
-
|
|
28
|
-
# Ubuntu / Debian
|
|
29
|
-
sudo apt-get install cmake ninja-build
|
|
30
|
-
git clone --depth 1 https://github.com/open-quantum-safe/liboqs
|
|
31
|
-
cd liboqs && mkdir build && cd build
|
|
32
|
-
cmake -GNinja -DBUILD_SHARED_LIBS=ON ..
|
|
33
|
-
ninja && sudo ninja install && sudo ldconfig
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
You can also set the `OQS_LIB` environment variable to point to a custom `liboqs.so` / `liboqs.dylib` path.
|
|
20
|
+
- CMake >= 3.15 and a C compiler — gcc or clang (for building the bundled liboqs)
|
|
37
21
|
|
|
38
22
|
## Installation
|
|
39
23
|
|
|
@@ -45,6 +29,22 @@ gem "jwt-pq"
|
|
|
45
29
|
gem "jwt-eddsa"
|
|
46
30
|
```
|
|
47
31
|
|
|
32
|
+
liboqs is automatically compiled from source during gem installation (ML-DSA algorithms only, ~30 seconds).
|
|
33
|
+
|
|
34
|
+
### Using system liboqs
|
|
35
|
+
|
|
36
|
+
If you prefer to use a system-installed liboqs:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
gem install jwt-pq -- --use-system-libraries
|
|
40
|
+
# or
|
|
41
|
+
JWT_PQ_USE_SYSTEM_LIBRARIES=1 gem install jwt-pq
|
|
42
|
+
# or in Bundler
|
|
43
|
+
bundle config build.jwt-pq --use-system-libraries
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also point to a specific library with `OQS_LIB=/path/to/liboqs.dylib`.
|
|
47
|
+
|
|
48
48
|
## Usage
|
|
49
49
|
|
|
50
50
|
### Basic ML-DSA signing
|
|
@@ -133,9 +133,10 @@ The `alg` header values follow a `ClassicAlg+PQAlg` convention. The IETF draft `
|
|
|
133
133
|
## Development
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
|
-
bundle install
|
|
137
|
-
|
|
138
|
-
bundle exec rubocop
|
|
136
|
+
bundle install # compiles liboqs automatically
|
|
137
|
+
bundle exec rspec # run tests
|
|
138
|
+
bundle exec rubocop # lint
|
|
139
|
+
rake compile # recompile liboqs manually
|
|
139
140
|
```
|
|
140
141
|
|
|
141
142
|
## License
|
data/Rakefile
CHANGED
|
@@ -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)}"
|
data/bin/smoke.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "jwt/pq"
|
|
5
|
+
|
|
6
|
+
puts "jwt-pq v#{JWT::PQ::VERSION} smoke test"
|
|
7
|
+
|
|
8
|
+
# ML-DSA-65 sign/verify round-trip
|
|
9
|
+
key = JWT::PQ::Key.generate(:ml_dsa_65)
|
|
10
|
+
token = JWT.encode({ "sub" => "smoke-test" }, key, "ML-DSA-65")
|
|
11
|
+
decoded = JWT.decode(token, key, true, algorithms: ["ML-DSA-65"])
|
|
12
|
+
|
|
13
|
+
raise "Decode failed" unless decoded.first["sub"] == "smoke-test"
|
|
14
|
+
|
|
15
|
+
puts "ML-DSA-65 sign/verify: OK"
|
|
16
|
+
|
|
17
|
+
# PEM round-trip
|
|
18
|
+
pub_pem = key.to_pem
|
|
19
|
+
restored = JWT::PQ::Key.from_pem(pub_pem)
|
|
20
|
+
decoded = JWT.decode(token, restored, true, algorithms: ["ML-DSA-65"])
|
|
21
|
+
|
|
22
|
+
raise "PEM round-trip failed" unless decoded.first["sub"] == "smoke-test"
|
|
23
|
+
|
|
24
|
+
puts "PEM round-trip: OK"
|
|
25
|
+
|
|
26
|
+
# JWK round-trip
|
|
27
|
+
jwk = JWT::PQ::JWK.new(key)
|
|
28
|
+
imported = JWT::PQ::JWK.import(jwk.export)
|
|
29
|
+
decoded = JWT.decode(token, imported, true, algorithms: ["ML-DSA-65"])
|
|
30
|
+
|
|
31
|
+
raise "JWK round-trip failed" unless decoded.first["sub"] == "smoke-test"
|
|
32
|
+
|
|
33
|
+
puts "JWK round-trip: OK"
|
|
34
|
+
|
|
35
|
+
# Key destroy
|
|
36
|
+
key.destroy!
|
|
37
|
+
raise "destroy! failed" if key.private?
|
|
38
|
+
|
|
39
|
+
puts "Key#destroy!: OK"
|
|
40
|
+
puts "All smoke tests passed"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "digest"
|
|
8
|
+
require "rubygems/package"
|
|
9
|
+
require "zlib"
|
|
10
|
+
require "etc"
|
|
11
|
+
require "tmpdir"
|
|
12
|
+
|
|
13
|
+
LIBOQS_VERSION = "0.15.0"
|
|
14
|
+
LIBOQS_SHA256 = "3983f7cd1247f37fb76a040e6fd684894d44a84cecdcfbdb90559b3216684b5c"
|
|
15
|
+
LIBOQS_URL = "https://github.com/open-quantum-safe/liboqs/archive/refs/tags/#{LIBOQS_VERSION}.tar.gz"
|
|
16
|
+
|
|
17
|
+
PACKAGE_ROOT_DIR = File.expand_path("../../..", __dir__)
|
|
18
|
+
VENDOR_DIR = File.join(PACKAGE_ROOT_DIR, "lib", "jwt", "pq", "vendor")
|
|
19
|
+
|
|
20
|
+
def write_dummy_makefile
|
|
21
|
+
File.write("Makefile", <<~MAKEFILE)
|
|
22
|
+
all:
|
|
23
|
+
\t@echo "jwt-pq: nothing to compile"
|
|
24
|
+
install:
|
|
25
|
+
\t@echo "jwt-pq: nothing to install"
|
|
26
|
+
clean:
|
|
27
|
+
\t@echo "jwt-pq: nothing to clean"
|
|
28
|
+
MAKEFILE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def use_system_libraries?
|
|
32
|
+
ENV.key?("JWT_PQ_USE_SYSTEM_LIBRARIES") ||
|
|
33
|
+
enable_config("system-libraries", false)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check_cmake!
|
|
37
|
+
cmake = find_executable("cmake")
|
|
38
|
+
unless cmake
|
|
39
|
+
abort <<~MSG
|
|
40
|
+
ERROR: cmake is required to compile liboqs for jwt-pq.
|
|
41
|
+
|
|
42
|
+
Install it with:
|
|
43
|
+
macOS: brew install cmake
|
|
44
|
+
Ubuntu: sudo apt-get install cmake
|
|
45
|
+
|
|
46
|
+
Alternatively, install liboqs manually and use:
|
|
47
|
+
gem install jwt-pq -- --use-system-libraries
|
|
48
|
+
MSG
|
|
49
|
+
end
|
|
50
|
+
cmake
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def download_via_http(url, dest_path, redirect_limit = 5)
|
|
54
|
+
abort "ERROR: too many redirects downloading liboqs" if redirect_limit.zero?
|
|
55
|
+
|
|
56
|
+
uri = URI.parse(url)
|
|
57
|
+
response = Net::HTTP.get_response(uri)
|
|
58
|
+
|
|
59
|
+
case response
|
|
60
|
+
when Net::HTTPSuccess
|
|
61
|
+
File.binwrite(dest_path, response.body)
|
|
62
|
+
when Net::HTTPRedirection
|
|
63
|
+
download_via_http(response["location"], dest_path, redirect_limit - 1)
|
|
64
|
+
else
|
|
65
|
+
abort "ERROR: failed to download liboqs (HTTP #{response.code}): #{uri}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def download_source(dest_dir)
|
|
70
|
+
tarball_path = File.join(dest_dir, "liboqs-#{LIBOQS_VERSION}.tar.gz")
|
|
71
|
+
|
|
72
|
+
# Support local tarball via env var (for air-gapped environments)
|
|
73
|
+
source = ENV.fetch("JWT_PQ_LIBOQS_SOURCE", LIBOQS_URL)
|
|
74
|
+
|
|
75
|
+
$stdout.puts "jwt-pq: downloading liboqs #{LIBOQS_VERSION}..."
|
|
76
|
+
if source.start_with?("http")
|
|
77
|
+
download_via_http(source, tarball_path)
|
|
78
|
+
else
|
|
79
|
+
FileUtils.cp(source, tarball_path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Verify checksum
|
|
83
|
+
actual = Digest::SHA256.file(tarball_path).hexdigest
|
|
84
|
+
unless actual == LIBOQS_SHA256
|
|
85
|
+
abort "ERROR: SHA-256 mismatch for liboqs tarball.\n" \
|
|
86
|
+
" Expected: #{LIBOQS_SHA256}\n" \
|
|
87
|
+
" Got: #{actual}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
tarball_path
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def extract_tarball(tarball_path, dest_dir)
|
|
94
|
+
$stdout.puts "jwt-pq: extracting liboqs #{LIBOQS_VERSION}..."
|
|
95
|
+
File.open(tarball_path, "rb") do |file|
|
|
96
|
+
Zlib::GzipReader.wrap(file) do |gz|
|
|
97
|
+
Gem::Package::TarReader.new(gz) do |tar|
|
|
98
|
+
tar.each do |entry|
|
|
99
|
+
dest = File.expand_path(File.join(dest_dir, entry.full_name))
|
|
100
|
+
unless dest.start_with?("#{File.expand_path(dest_dir)}/")
|
|
101
|
+
abort "ERROR: path traversal detected in tarball: #{entry.full_name}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if entry.directory?
|
|
105
|
+
FileUtils.mkdir_p(dest)
|
|
106
|
+
elsif entry.file?
|
|
107
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
108
|
+
File.binwrite(dest, entry.read)
|
|
109
|
+
File.chmod(entry.header.mode, dest) if entry.header.mode && entry.header.mode > 0
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
File.join(dest_dir, "liboqs-#{LIBOQS_VERSION}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_liboqs(source_dir)
|
|
120
|
+
build_dir = File.join(source_dir, "build")
|
|
121
|
+
FileUtils.mkdir_p(build_dir)
|
|
122
|
+
FileUtils.mkdir_p(VENDOR_DIR)
|
|
123
|
+
|
|
124
|
+
nproc = begin
|
|
125
|
+
Etc.nprocessors
|
|
126
|
+
rescue StandardError
|
|
127
|
+
2
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Semicolons are cmake list separators (safe here — system() uses execvp, no shell)
|
|
131
|
+
cmake_args = %W[
|
|
132
|
+
-DBUILD_SHARED_LIBS=ON
|
|
133
|
+
-DOQS_MINIMAL_BUILD=SIG_ml_dsa_44;SIG_ml_dsa_65;SIG_ml_dsa_87
|
|
134
|
+
-DOQS_BUILD_ONLY_LIB=ON
|
|
135
|
+
-DOQS_USE_OPENSSL=OFF
|
|
136
|
+
-DCMAKE_BUILD_TYPE=Release
|
|
137
|
+
-DCMAKE_INSTALL_PREFIX=#{VENDOR_DIR}
|
|
138
|
+
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
# Use Ninja if available (faster), fall back to Make
|
|
142
|
+
if find_executable("ninja")
|
|
143
|
+
cmake_args << "-GNinja"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
$stdout.puts "jwt-pq: configuring liboqs #{LIBOQS_VERSION} (ML-DSA only)..."
|
|
147
|
+
unless system("cmake", "-S", source_dir, "-B", build_dir, *cmake_args)
|
|
148
|
+
abort "ERROR: cmake configure failed for liboqs"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
$stdout.puts "jwt-pq: compiling liboqs (using #{nproc} cores)..."
|
|
152
|
+
unless system("cmake", "--build", build_dir, "--parallel", nproc.to_s)
|
|
153
|
+
abort "ERROR: cmake build failed for liboqs"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
$stdout.puts "jwt-pq: installing liboqs to vendor directory..."
|
|
157
|
+
unless system("cmake", "--install", build_dir)
|
|
158
|
+
abort "ERROR: cmake install failed for liboqs"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def find_vendored_library
|
|
163
|
+
%w[dylib so].each do |ext|
|
|
164
|
+
path = File.join(VENDOR_DIR, "lib", "liboqs.#{ext}")
|
|
165
|
+
return path if File.exist?(path)
|
|
166
|
+
end
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# --- Main ---
|
|
171
|
+
|
|
172
|
+
if use_system_libraries?
|
|
173
|
+
$stdout.puts "jwt-pq: using system liboqs (--use-system-libraries)"
|
|
174
|
+
write_dummy_makefile
|
|
175
|
+
exit 0
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
check_cmake!
|
|
179
|
+
|
|
180
|
+
Dir.mktmpdir("jwt-pq-build") do |tmp_dir|
|
|
181
|
+
tarball = download_source(tmp_dir)
|
|
182
|
+
source_dir = extract_tarball(tarball, tmp_dir)
|
|
183
|
+
build_liboqs(source_dir)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
lib_path = find_vendored_library
|
|
187
|
+
if lib_path
|
|
188
|
+
$stdout.puts "jwt-pq: liboqs #{LIBOQS_VERSION} installed at #{lib_path}"
|
|
189
|
+
else
|
|
190
|
+
abort "ERROR: liboqs shared library not found after build"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
write_dummy_makefile
|
data/jwt-pq.gemspec
CHANGED
|
@@ -24,12 +24,14 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
"rubygems_mfa_required" => "true"
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
spec.requirements = ["
|
|
27
|
+
spec.requirements = ["cmake >= 3.15", "C compiler (gcc or clang)"]
|
|
28
28
|
|
|
29
29
|
spec.post_install_message = <<~MSG
|
|
30
|
-
jwt-pq
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
jwt-pq compiles liboqs from source during installation.
|
|
31
|
+
If the build failed, ensure cmake and a C compiler are installed.
|
|
32
|
+
To use a system-installed liboqs instead:
|
|
33
|
+
gem install jwt-pq -- --use-system-libraries
|
|
34
|
+
For hybrid EdDSA+ML-DSA mode, add 'jwt-eddsa' to your Gemfile.
|
|
33
35
|
MSG
|
|
34
36
|
|
|
35
37
|
spec.files = Dir.chdir(__dir__) do
|
|
@@ -39,6 +41,7 @@ Gem::Specification.new do |spec|
|
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
spec.extensions = ["ext/jwt/pq/extconf.rb"]
|
|
42
45
|
spec.require_paths = ["lib"]
|
|
43
46
|
|
|
44
47
|
spec.add_dependency "ffi", "~> 1.15"
|
|
@@ -15,23 +15,28 @@ module JWT
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def sign(data:, signing_key:)
|
|
18
|
-
key =
|
|
19
|
-
raise_sign_error!("Private key required for signing") unless key.private?
|
|
18
|
+
key = resolve_signing_key(signing_key)
|
|
20
19
|
key.sign(data)
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def verify(data:, signature:, verification_key:)
|
|
24
|
-
|
|
25
|
-
|
|
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)
|
|
26
30
|
rescue JWT::PQ::Error
|
|
27
31
|
false
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
private
|
|
31
35
|
|
|
32
|
-
def
|
|
36
|
+
def resolve_signing_key(key)
|
|
33
37
|
case key
|
|
34
38
|
when JWT::PQ::Key
|
|
39
|
+
raise_sign_error!("Private key required for signing") unless key.private?
|
|
35
40
|
key
|
|
36
41
|
else
|
|
37
42
|
raise_sign_error!(
|
|
@@ -41,6 +46,18 @@ module JWT
|
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
49
|
+
def resolve_verification_key(key)
|
|
50
|
+
case key
|
|
51
|
+
when JWT::PQ::Key
|
|
52
|
+
key
|
|
53
|
+
else
|
|
54
|
+
raise_verify_error!(
|
|
55
|
+
"Expected a JWT::PQ::Key, got #{key.class}. " \
|
|
56
|
+
"Use JWT::PQ::Key.generate(:#{alg_symbol}) to create a key."
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
44
61
|
def alg_symbol
|
|
45
62
|
alg.downcase.tr("-", "_")
|
|
46
63
|
end
|
data/lib/jwt/pq/hybrid_key.rb
CHANGED
|
@@ -51,6 +51,23 @@ module JWT
|
|
|
51
51
|
"EdDSA+#{@ml_dsa_key.algorithm}"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Zero and discard private key material from both key components.
|
|
55
|
+
# After calling this, the key can only be used for verification.
|
|
56
|
+
def destroy!
|
|
57
|
+
@ml_dsa_key.destroy!
|
|
58
|
+
if @ed25519_signing_key
|
|
59
|
+
seed = @ed25519_signing_key.to_bytes
|
|
60
|
+
seed.replace("\0" * seed.bytesize)
|
|
61
|
+
@ed25519_signing_key = nil
|
|
62
|
+
end
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def inspect
|
|
67
|
+
"#<#{self.class} algorithm=#{hybrid_algorithm} private=#{private?}>"
|
|
68
|
+
end
|
|
69
|
+
alias to_s inspect
|
|
70
|
+
|
|
54
71
|
def self.require_eddsa_dependency!
|
|
55
72
|
require "ed25519"
|
|
56
73
|
rescue LoadError
|
data/lib/jwt/pq/jwk.rb
CHANGED
|
@@ -13,7 +13,7 @@ module JWT
|
|
|
13
13
|
# pub: base64url-encoded public key
|
|
14
14
|
# priv: base64url-encoded private key (optional)
|
|
15
15
|
class JWK
|
|
16
|
-
ALGORITHMS =
|
|
16
|
+
ALGORITHMS = MlDsa::ALGORITHMS.keys.freeze
|
|
17
17
|
KTY = "AKP"
|
|
18
18
|
|
|
19
19
|
attr_reader :key
|
|
@@ -44,10 +44,12 @@ module JWT
|
|
|
44
44
|
|
|
45
45
|
validate_kty!(jwk)
|
|
46
46
|
alg = validate_alg!(jwk)
|
|
47
|
-
|
|
47
|
+
raise KeyError, "Missing 'pub' in JWK" unless jwk.key?("pub")
|
|
48
|
+
|
|
49
|
+
pub_bytes = decode_field(jwk, "pub")
|
|
48
50
|
|
|
49
51
|
if jwk.key?("priv")
|
|
50
|
-
priv_bytes =
|
|
52
|
+
priv_bytes = decode_field(jwk, "priv")
|
|
51
53
|
Key.new(algorithm: alg, public_key: pub_bytes, private_key: priv_bytes)
|
|
52
54
|
else
|
|
53
55
|
Key.new(algorithm: alg, public_key: pub_bytes)
|
|
@@ -83,10 +85,12 @@ module JWT
|
|
|
83
85
|
end
|
|
84
86
|
private_class_method :normalize_keys
|
|
85
87
|
|
|
86
|
-
def self.
|
|
87
|
-
::Base64.urlsafe_decode64(
|
|
88
|
+
def self.decode_field(jwk, field)
|
|
89
|
+
::Base64.urlsafe_decode64(jwk[field])
|
|
90
|
+
rescue ArgumentError => e
|
|
91
|
+
raise KeyError, "Invalid base64url in JWK '#{field}': #{e.message}"
|
|
88
92
|
end
|
|
89
|
-
private_class_method :
|
|
93
|
+
private_class_method :decode_field
|
|
90
94
|
|
|
91
95
|
private
|
|
92
96
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -63,6 +63,23 @@ module JWT
|
|
|
63
63
|
!@private_key.nil?
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Zero and discard private key material from Ruby memory.
|
|
67
|
+
# After calling this, the key can only be used for verification.
|
|
68
|
+
def destroy!
|
|
69
|
+
if @private_key
|
|
70
|
+
@private_key.replace("\0" * @private_key.bytesize)
|
|
71
|
+
@private_key = nil
|
|
72
|
+
end
|
|
73
|
+
@sk_buffer&.clear
|
|
74
|
+
@sk_buffer = nil
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def inspect
|
|
79
|
+
"#<#{self.class} algorithm=#{@algorithm} private=#{private?}>"
|
|
80
|
+
end
|
|
81
|
+
alias to_s inspect
|
|
82
|
+
|
|
66
83
|
# Import a Key from a PEM string (SPKI or PKCS#8).
|
|
67
84
|
def self.from_pem(pem_string)
|
|
68
85
|
info = PqcAsn1::DER.parse_pem(pem_string)
|
|
@@ -134,12 +151,20 @@ module JWT
|
|
|
134
151
|
new(algorithm: alg_name, public_key: info.public_key, private_key: sk_bytes)
|
|
135
152
|
end
|
|
136
153
|
|
|
137
|
-
private_class_method :extract_secure_bytes, :resolve_oid!, :build_from_pkcs8
|
|
154
|
+
private_class_method :resolve_algorithm, :extract_secure_bytes, :resolve_oid!, :build_from_pkcs8
|
|
138
155
|
|
|
139
156
|
private
|
|
140
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
|
+
|
|
141
166
|
def resolve_algorithm(algorithm)
|
|
142
|
-
self.class.resolve_algorithm
|
|
167
|
+
self.class.send(:resolve_algorithm, algorithm)
|
|
143
168
|
end
|
|
144
169
|
|
|
145
170
|
def validate!
|
data/lib/jwt/pq/liboqs.rb
CHANGED
|
@@ -6,29 +6,46 @@ module JWT
|
|
|
6
6
|
module PQ
|
|
7
7
|
# FFI bindings for liboqs signature operations.
|
|
8
8
|
#
|
|
9
|
-
#
|
|
9
|
+
# Library search order:
|
|
10
10
|
# 1. OQS_LIB environment variable (explicit path)
|
|
11
|
-
# 2.
|
|
11
|
+
# 2. Vendored liboqs (compiled during gem install)
|
|
12
|
+
# 3. System-installed liboqs (via standard library search)
|
|
12
13
|
module LibOQS
|
|
13
14
|
extend FFI::Library
|
|
14
15
|
|
|
15
16
|
OQS_SUCCESS = 0
|
|
16
17
|
OQS_ERROR = -1
|
|
17
18
|
|
|
18
|
-
# Determine library path
|
|
19
19
|
def self.lib_path
|
|
20
|
+
# 1. Explicit path from environment
|
|
20
21
|
return ENV["OQS_LIB"] if ENV["OQS_LIB"]
|
|
21
22
|
|
|
23
|
+
# 2. Vendored library (compiled during gem install)
|
|
24
|
+
vendored = vendored_lib_path
|
|
25
|
+
return vendored if vendored
|
|
26
|
+
|
|
27
|
+
# 3. System library
|
|
22
28
|
"oqs"
|
|
23
29
|
end
|
|
24
30
|
|
|
31
|
+
def self.vendored_lib_path
|
|
32
|
+
%w[dylib so].each do |ext|
|
|
33
|
+
path = File.join(__dir__, "vendor", "lib", "liboqs.#{ext}")
|
|
34
|
+
return path if File.exist?(path)
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
private_class_method :vendored_lib_path
|
|
39
|
+
|
|
25
40
|
begin
|
|
26
41
|
ffi_lib lib_path
|
|
27
42
|
rescue LoadError => e
|
|
28
43
|
raise JWT::PQ::LiboqsError,
|
|
29
|
-
"liboqs not found.
|
|
30
|
-
"
|
|
31
|
-
"
|
|
44
|
+
"liboqs not found. The vendored library may not have been compiled " \
|
|
45
|
+
"during gem install. Ensure cmake and a C compiler are installed, " \
|
|
46
|
+
"then reinstall: gem install jwt-pq. Alternatively, install liboqs " \
|
|
47
|
+
"manually and set OQS_LIB to the full path. " \
|
|
48
|
+
"Original error: #{e.message}"
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
# OQS_SIG *OQS_SIG_new(const char *method_name)
|
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)
|
|
@@ -39,6 +67,7 @@ module JWT
|
|
|
39
67
|
|
|
40
68
|
[pk.read_bytes(@sizes[:public_key]), sk.read_bytes(@sizes[:secret_key])]
|
|
41
69
|
ensure
|
|
70
|
+
sk&.clear
|
|
42
71
|
LibOQS.OQS_SIG_free(sig) if sig && !sig.null?
|
|
43
72
|
end
|
|
44
73
|
|
|
@@ -47,23 +76,28 @@ module JWT
|
|
|
47
76
|
def sign(message, secret_key)
|
|
48
77
|
validate_key_size!(secret_key, :secret_key)
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
52
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)
|
|
53
92
|
sig_buf = FFI::MemoryPointer.new(:uint8, @sizes[:signature])
|
|
54
93
|
sig_len = FFI::MemoryPointer.new(:size_t)
|
|
55
94
|
msg_buf = FFI::MemoryPointer.from_string(message)
|
|
56
|
-
sk_buf = FFI::MemoryPointer.new(:uint8, secret_key.bytesize)
|
|
57
|
-
sk_buf.put_bytes(0, secret_key)
|
|
58
95
|
|
|
59
96
|
status = LibOQS.OQS_SIG_sign(sig, sig_buf, sig_len,
|
|
60
97
|
msg_buf, message.bytesize, sk_buf)
|
|
61
98
|
raise SignatureError, "Signing failed for #{@algorithm}" unless status == LibOQS::OQS_SUCCESS
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
sig_buf.read_bytes(actual_len)
|
|
65
|
-
ensure
|
|
66
|
-
LibOQS.OQS_SIG_free(sig) if sig && !sig.null?
|
|
100
|
+
sig_buf.read_bytes(sig_len.read(:size_t))
|
|
67
101
|
end
|
|
68
102
|
|
|
69
103
|
# Verify a signature against a message and public key.
|
|
@@ -71,20 +105,24 @@ module JWT
|
|
|
71
105
|
def verify(message, signature, public_key)
|
|
72
106
|
validate_key_size!(public_key, :public_key)
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
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)
|
|
77
119
|
msg_buf = FFI::MemoryPointer.from_string(message)
|
|
78
120
|
sig_buf = FFI::MemoryPointer.new(:uint8, signature.bytesize)
|
|
79
121
|
sig_buf.put_bytes(0, signature)
|
|
80
|
-
pk_buf = FFI::MemoryPointer.new(:uint8, public_key.bytesize)
|
|
81
|
-
pk_buf.put_bytes(0, public_key)
|
|
82
122
|
|
|
83
123
|
status = LibOQS.OQS_SIG_verify(sig, msg_buf, message.bytesize,
|
|
84
124
|
sig_buf, signature.bytesize, pk_buf)
|
|
85
125
|
status == LibOQS::OQS_SUCCESS
|
|
86
|
-
ensure
|
|
87
|
-
LibOQS.OQS_SIG_free(sig) if sig && !sig.null?
|
|
88
126
|
end
|
|
89
127
|
|
|
90
128
|
# Key sizes for this algorithm
|
data/lib/jwt/pq/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marcelo Almeida
|
|
@@ -57,7 +57,8 @@ description: Adds ML-DSA-44, ML-DSA-65, and ML-DSA-87 post-quantum signature alg
|
|
|
57
57
|
email:
|
|
58
58
|
- contact@marcelopazzo.com
|
|
59
59
|
executables: []
|
|
60
|
-
extensions:
|
|
60
|
+
extensions:
|
|
61
|
+
- ext/jwt/pq/extconf.rb
|
|
61
62
|
extra_rdoc_files: []
|
|
62
63
|
files:
|
|
63
64
|
- CHANGELOG.md
|
|
@@ -65,6 +66,11 @@ files:
|
|
|
65
66
|
- LICENSE
|
|
66
67
|
- README.md
|
|
67
68
|
- Rakefile
|
|
69
|
+
- bench/fixtures/ml_dsa_65_sk.pem
|
|
70
|
+
- bench/sign_throughput.rb
|
|
71
|
+
- bench/verify_throughput.rb
|
|
72
|
+
- bin/smoke.rb
|
|
73
|
+
- ext/jwt/pq/extconf.rb
|
|
68
74
|
- jwt-pq.gemspec
|
|
69
75
|
- lib/jwt/pq.rb
|
|
70
76
|
- lib/jwt/pq/algorithms/hybrid_eddsa.rb
|
|
@@ -86,9 +92,11 @@ metadata:
|
|
|
86
92
|
documentation_uri: https://rubydoc.info/gems/jwt-pq
|
|
87
93
|
rubygems_mfa_required: 'true'
|
|
88
94
|
post_install_message: |
|
|
89
|
-
jwt-pq
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
jwt-pq compiles liboqs from source during installation.
|
|
96
|
+
If the build failed, ensure cmake and a C compiler are installed.
|
|
97
|
+
To use a system-installed liboqs instead:
|
|
98
|
+
gem install jwt-pq -- --use-system-libraries
|
|
99
|
+
For hybrid EdDSA+ML-DSA mode, add 'jwt-eddsa' to your Gemfile.
|
|
92
100
|
rdoc_options: []
|
|
93
101
|
require_paths:
|
|
94
102
|
- lib
|
|
@@ -103,7 +111,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
111
|
- !ruby/object:Gem::Version
|
|
104
112
|
version: '0'
|
|
105
113
|
requirements:
|
|
106
|
-
-
|
|
114
|
+
- cmake >= 3.15
|
|
115
|
+
- C compiler (gcc or clang)
|
|
107
116
|
rubygems_version: 3.6.9
|
|
108
117
|
specification_version: 4
|
|
109
118
|
summary: Post-quantum JWT signatures (ML-DSA / FIPS 204) for Ruby
|