pq_crypto-jwt 0.1.1 → 0.2.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 +56 -0
- data/README.md +79 -17
- data/lib/pq_crypto/jwt/errors.rb +1 -0
- data/lib/pq_crypto/jwt/jwa/ml_dsa_44.rb +2 -0
- data/lib/pq_crypto/jwt/jwa/ml_dsa_87.rb +2 -0
- data/lib/pq_crypto/jwt/jwa/ml_dsa_streaming.rb +77 -59
- data/lib/pq_crypto/jwt/jwa.rb +24 -55
- data/lib/pq_crypto/jwt/jwk/akp.rb +39 -43
- data/lib/pq_crypto/jwt/jwk.rb +118 -62
- data/lib/pq_crypto/jwt/jwks.rb +24 -23
- data/lib/pq_crypto/jwt/keys.rb +42 -56
- data/lib/pq_crypto/jwt/version.rb +1 -1
- data/lib/pq_crypto/jwt.rb +10 -36
- metadata +13 -11
- data/lib/pq_crypto/jwt/algorithms/ml_dsa_44.rb +0 -3
- data/lib/pq_crypto/jwt/algorithms/ml_dsa_65.rb +0 -3
- data/lib/pq_crypto/jwt/algorithms/ml_dsa_87.rb +0 -3
- data/lib/pq_crypto/jwt/algorithms.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ded8bedea5417580ef9a3199f47543c037c1506dfe2e7a8db86bb6c1f2f94c7a
|
|
4
|
+
data.tar.gz: 716ed896e2f4960dcd3ca0afbd6ef94a43e45e63ea7f7284c1d277bfb2366418
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 371849705f4387e78f3c98d08982274ad072bf519f2699c6d1df7de5254fe70e01c1e3a8d2c3d54cd2fe1573666c9fc871431726089a5907c586f6dbdfe0a7c0
|
|
7
|
+
data.tar.gz: 8e408633d2862da41b4e546ad717153a8312e7983698058aeb53da5435523fff670ee1fe4ed2d9cb392b9aed7f655737046122aa9e2e09efe3455eea9f63d941
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Retargeted the adapter to the current parent line: `pq_crypto ~> 0.6.1`.
|
|
8
|
+
- Updated documentation and release language from draft-era ML-DSA JOSE wording to RFC 9964 terminology.
|
|
9
|
+
- Explicitly pass the RFC 9964 empty ML-DSA context in all one-shot and streaming sign/verify paths.
|
|
10
|
+
- Streaming detached JWS now applies to `ML-DSA-44`, `ML-DSA-65`, and `ML-DSA-87` instead of only `ML-DSA-65`.
|
|
11
|
+
- PEM/DER loading now delegates to `PQCrypto::Key.from_pem` / `PQCrypto::Key.from_der` for auto-dispatch instead of manually parsing ASN.1 OIDs in the JWT adapter.
|
|
12
|
+
- `signature_length_valid?` now fails closed when signature metadata cannot be resolved.
|
|
13
|
+
- `JWT::JWK.classes` is no longer mutated at require-time; AKP registration happens through `PQCrypto::JWT.register!`.
|
|
14
|
+
- CI matrix now covers Ruby `3.1`, `3.2`, `3.3`, `3.4`, `4.0` crossed with `jwt ~> 3.1.0` and `jwt ~> 3.2.0`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `PQCrypto::JWT::UnsupportedFeature` for release-scope or parent-API-gated functionality.
|
|
19
|
+
- Seed-based private AKP JWK import via `PQCrypto::JWT::JWK.secret_key_from_jwk`.
|
|
20
|
+
- Optional strict private JWK `pub`/`priv` consistency verification via `verify_public: true` for both import and explicit seed export; this is gated on future parent public seed-derivation APIs.
|
|
21
|
+
- Explicit seed-based private AKP JWK export via `PQCrypto::JWT::JWK.from_seed(..., public_key:)`, with optional `verify_public: true` consistency checking when parent support exists.
|
|
22
|
+
- Reserved `PQCrypto::JWT::JWK.from_secret_key` for future seed-exporting parent keys; current `pq_crypto 0.6.1` keys raise `UnsupportedFeature` instead of reading private parent state.
|
|
23
|
+
- Optional public JWK metadata on export: `use:` and `key_ops:`.
|
|
24
|
+
- DER helper methods:
|
|
25
|
+
- `PQCrypto::JWT::Keys.public_from_der`
|
|
26
|
+
- `PQCrypto::JWT::Keys.secret_from_der`
|
|
27
|
+
- JWKS lookup helpers for JWK thumbprint fallback and multi-match rotation windows:
|
|
28
|
+
- `PQCrypto::JWT::JWKS.find(..., thumbprint:)`
|
|
29
|
+
- `PQCrypto::JWT::JWKS.find_all`
|
|
30
|
+
- Negative tests for malformed streaming headers, extra compact segments, invalid private seed lengths, strict private JWK verification gating, dependency constraints, private API constraints, and read contract behavior.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Removed private parent-state reads from private AKP JWK export; the adapter no longer uses parent instance variables as compatibility API.
|
|
35
|
+
- `DetachedSigningInputIO#read(nil)` now follows the Ruby IO contract and reads to EOF.
|
|
36
|
+
- Streaming verification now rejects non-object decoded JOSE headers before consulting `alg`.
|
|
37
|
+
- Streaming verification now rejects compact tokens with extra segments.
|
|
38
|
+
- One-shot verification rescues only expected verification/input/encoding errors instead of masking all `StandardError`.
|
|
39
|
+
- `JWT::JWK::AKP` no longer advertises automatic dispatch for `SecretKey`/`Keypair` until parent seed-export APIs exist.
|
|
40
|
+
- Removed a redundant unreachable private-JWK parameter check from `JWT::JWK::AKP`.
|
|
41
|
+
- JWKS loader caching no longer relies on `||=`, so falsey JWKS values do not cause repeated reloads.
|
|
42
|
+
|
|
43
|
+
### Deferred
|
|
44
|
+
|
|
45
|
+
- Composite / hybrid JOSE signatures.
|
|
46
|
+
- ML-KEM / JWE support.
|
|
47
|
+
- RBS / Sorbet signatures.
|
|
48
|
+
- Macro refactor for the three ML-DSA modules.
|
|
49
|
+
|
|
50
|
+
## 0.1.2
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- Lowered the supported Ruby floor from `>= 3.4.0` to `>= 3.1.0`, matching the `pq_crypto` 0.5.x compatibility line.
|
|
55
|
+
- Updated the runtime dependency to `pq_crypto >= 0.5.3, < 0.6` so Ruby 3.1-3.3 use the compatibility path from the core gem while Ruby 3.4+ keeps the optimized path.
|
|
56
|
+
- Expanded CI coverage to Ruby 3.1, 3.2, 3.3, 3.4, and 4.0 on Linux and macOS.
|
|
57
|
+
- Added hard-constraint tests for the Ruby floor and `pq_crypto` dependency range.
|
|
58
|
+
|
|
3
59
|
## Initial public release
|
|
4
60
|
|
|
5
61
|
### Included
|
data/README.md
CHANGED
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
`pq_crypto-jwt` is a small adapter that connects [`pq_crypto`](https://rubygems.org/gems/pq_crypto) to the [`ruby-jwt`](https://rubygems.org/gems/jwt) ecosystem.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This release focuses on the RFC 9964 ML-DSA JWS surface:
|
|
6
6
|
|
|
7
7
|
- ML-DSA JWS signing and verification for `ruby-jwt`
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- ML-DSA
|
|
8
|
+
- AKP JWK/JWKS helpers for ML-DSA verification keys
|
|
9
|
+
- seed-based private AKP JWK import and explicit seed export paths
|
|
10
|
+
- PEM/DER import helpers for ML-DSA SPKI/PKCS#8 keys through `PQCrypto::Key`
|
|
11
|
+
- streaming detached JWS helpers for `ML-DSA-44`, `ML-DSA-65`, and `ML-DSA-87`
|
|
11
12
|
|
|
12
|
-
ML-KEM/JWE is **not included
|
|
13
|
+
ML-KEM/JWE is **not included**. Full JWE support needs a separate standards-compatible implementation and interoperability tests.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Ruby `>= 3.1.0`
|
|
18
|
+
- `pq_crypto` `~> 0.6.1`
|
|
19
|
+
- `jwt` `>= 3.1`, `< 4.0`
|
|
20
|
+
|
|
21
|
+
`pq_crypto-jwt` is Ruby-only and does not ship its own native extension. Native ML-DSA work, seed-aware keys, PKCS#8/SPKI parsing, and streaming signing/verification are delegated to `pq_crypto`.
|
|
13
22
|
|
|
14
23
|
## Install
|
|
15
24
|
|
|
16
25
|
```ruby
|
|
17
|
-
gem "pq_crypto-jwt", "~> 0.
|
|
26
|
+
gem "pq_crypto-jwt", "~> 0.2"
|
|
18
27
|
```
|
|
19
28
|
|
|
20
29
|
## Register the algorithms
|
|
@@ -47,9 +56,11 @@ token = JWT.encode({ "sub" => "alice" }, keypair.secret_key, "ML-DSA-65")
|
|
|
47
56
|
payload, header = JWT.decode(token, keypair.public_key, true, algorithm: "ML-DSA-65")
|
|
48
57
|
```
|
|
49
58
|
|
|
50
|
-
The adapter validates both the JOSE algorithm string and the concrete pq_crypto key type. A token signed with `ML-DSA-44`, for example, will not verify under `ML-DSA-65`.
|
|
59
|
+
The adapter validates both the JOSE algorithm string and the concrete `pq_crypto` key type. A token signed with `ML-DSA-44`, for example, will not verify under `ML-DSA-65`.
|
|
60
|
+
|
|
61
|
+
For RFC 9964 JOSE algorithms, the FIPS 204 `ctx` value is fixed to the empty string. The adapter passes `context: "".b` explicitly in both one-shot and streaming paths and does not expose a per-token context option for `ML-DSA-44`, `ML-DSA-65`, or `ML-DSA-87`.
|
|
51
62
|
|
|
52
|
-
## PEM import
|
|
63
|
+
## PEM and DER import
|
|
53
64
|
|
|
54
65
|
SPKI public keys and PKCS#8 secret keys can be imported through the helper API:
|
|
55
66
|
|
|
@@ -68,6 +79,13 @@ public_key = PQCrypto::JWT::Keys.public_from_pem(spki_pem, expect: :signature)
|
|
|
68
79
|
secret_key = PQCrypto::JWT::Keys.secret_from_pem(pkcs8_pem, expect: :signature)
|
|
69
80
|
```
|
|
70
81
|
|
|
82
|
+
DER helpers are also available:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
public_key = PQCrypto::JWT::Keys.public_from_der(spki_der)
|
|
86
|
+
secret_key = PQCrypto::JWT::Keys.secret_from_der(pkcs8_der, passphrase: passphrase)
|
|
87
|
+
```
|
|
88
|
+
|
|
71
89
|
## JWK and JWKS
|
|
72
90
|
|
|
73
91
|
Public AKP JWK round-trip:
|
|
@@ -78,6 +96,39 @@ jwk = PQCrypto::JWT::JWK.from_public_key(keypair.public_key, kid: "signing-key")
|
|
|
78
96
|
public_key = PQCrypto::JWT::JWK.public_key_from_jwk(jwk)
|
|
79
97
|
```
|
|
80
98
|
|
|
99
|
+
Optional public JWK metadata can be supplied directly:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
jwk = PQCrypto::JWT::JWK.from_public_key(
|
|
103
|
+
keypair.public_key,
|
|
104
|
+
kid: "signing-key",
|
|
105
|
+
use: "sig",
|
|
106
|
+
key_ops: ["verify"]
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Private AKP JWK uses RFC 9964 seed format: `priv` is the 32 raw-byte ML-DSA seed encoded as base64url. It is not the expanded ML-DSA secret key, and helper APIs expect raw seed bytes, not hex or text encodings.
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
secret_key = PQCrypto::JWT::JWK.secret_key_from_jwk(private_jwk)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
With `pq_crypto 0.6.1`, private import trusts the JWK `pub` field as metadata because the parent gem does not expose public seed derivation yet. Callers that need a strict `pub`/`priv` consistency check can request it explicitly; this will raise `UnsupportedFeature` until the parent exposes `Signature.public_key_from_seed` or `Signature.keypair_from_seed`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
secret_key = PQCrypto::JWT::JWK.secret_key_from_jwk(private_jwk, verify_public: true)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Export is intentionally explicit. Current `pq_crypto 0.6.1` can import seed-aware keys but does not expose a stable public seed accessor / public-key-from-seed API, so use `from_seed` and pass the matching public key:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
jwk = PQCrypto::JWT::JWK.from_seed(seed_32_bytes, alg: "ML-DSA-65", public_key: public_key)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`from_seed` also accepts `verify_public: true` for symmetry with private import. On current `pq_crypto 0.6.1` this raises `UnsupportedFeature`; once the parent exposes public seed derivation, it will reject a `public_key:` that does not match the supplied seed. Without `verify_public: true`, the explicitly supplied `public_key:` is trusted.
|
|
129
|
+
|
|
130
|
+
`from_secret_key` is reserved for a future parent release that exposes a public seed accessor. Expanded-only keys, and current `pq_crypto 0.6.1` keys, raise `UnsupportedFeature` instead of reading parent private state.
|
|
131
|
+
|
|
81
132
|
JWKS lookup with `ruby-jwt`:
|
|
82
133
|
|
|
83
134
|
```ruby
|
|
@@ -89,11 +140,11 @@ token = JWT.encode({ "sub" => "alice" }, keypair.secret_key, "ML-DSA-65", kid: "
|
|
|
89
140
|
payload, header = JWT.decode(token, nil, true, algorithms: ["ML-DSA-65"], jwks: jwks)
|
|
90
141
|
```
|
|
91
142
|
|
|
92
|
-
For rotation, pass `PQCrypto::JWT::JWKS.loader(callable_or_hash)` as the `jwks:` value.
|
|
143
|
+
For rotation, pass `PQCrypto::JWT::JWKS.loader(callable_or_hash)` as the `jwks:` value. The loader refreshes when `ruby-jwt` calls it with `invalidate: true`.
|
|
93
144
|
|
|
94
145
|
## Streaming detached JWS
|
|
95
146
|
|
|
96
|
-
|
|
147
|
+
All three ML-DSA JOSE algorithms support the streaming detached JWS helper when backed by `pq_crypto ~> 0.6.1`. The compact form is `header..signature`; callers must supply the same payload stream separately for verification.
|
|
97
148
|
|
|
98
149
|
```ruby
|
|
99
150
|
File.open("payload.bin", "rb") do |payload_io|
|
|
@@ -112,24 +163,35 @@ File.open("payload.bin", "rb") do |payload_io|
|
|
|
112
163
|
end
|
|
113
164
|
```
|
|
114
165
|
|
|
115
|
-
|
|
166
|
+
Equivalent helpers exist on:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
PQCrypto::JWT::JWA::MLDSA44
|
|
170
|
+
PQCrypto::JWT::JWA::MLDSA65
|
|
171
|
+
PQCrypto::JWT::JWA::MLDSA87
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`verify_io` returns `[payload_position, header]` on success. `payload_position` is `nil` for non-seekable streams that do not respond to `#pos`.
|
|
175
|
+
|
|
176
|
+
## Non-goals
|
|
116
177
|
|
|
117
|
-
|
|
178
|
+
This adapter deliberately does **not** expose:
|
|
118
179
|
|
|
119
180
|
- ML-KEM JWE key agreement
|
|
120
181
|
- JWE compact or JSON serialization
|
|
121
182
|
- JWE content encryption, AAD, IV, or authentication tag handling
|
|
122
|
-
-
|
|
183
|
+
- composite / hybrid JOSE signatures
|
|
184
|
+
- custom ML-DSA JOSE `ctx` values
|
|
123
185
|
- general-purpose JWT claims policy beyond what `ruby-jwt` already provides
|
|
124
186
|
|
|
125
|
-
This keeps the public API small and avoids publishing
|
|
187
|
+
This keeps the public API small and avoids publishing speculative JWE or composite-signature behavior before the relevant JOSE work is stable.
|
|
126
188
|
|
|
127
189
|
## Security status
|
|
128
190
|
|
|
129
191
|
```text
|
|
130
|
-
unaudited;
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
unaudited; implements the RFC 9964 ML-DSA JOSE algorithm identifiers and AKP
|
|
193
|
+
JWK seed encoding; backed by pq_crypto, which should also be reviewed before
|
|
194
|
+
production use.
|
|
133
195
|
```
|
|
134
196
|
|
|
135
197
|
Use in production only after your own security review and interoperability testing.
|
data/lib/pq_crypto/jwt/errors.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../jwa"
|
|
4
|
+
require_relative "ml_dsa_streaming"
|
|
4
5
|
|
|
5
6
|
module PQCrypto
|
|
6
7
|
module JWT
|
|
@@ -8,6 +9,7 @@ module PQCrypto
|
|
|
8
9
|
module MLDSA44
|
|
9
10
|
extend ::JWT::JWA::SigningAlgorithm
|
|
10
11
|
extend PQCrypto::JWT::JWA::MLDSA
|
|
12
|
+
extend PQCrypto::JWT::JWA::MLDSAStreaming
|
|
11
13
|
|
|
12
14
|
ALG = "ML-DSA-44".freeze
|
|
13
15
|
PQ_CRYPTO_ALGORITHM = :ml_dsa_44
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../jwa"
|
|
4
|
+
require_relative "ml_dsa_streaming"
|
|
4
5
|
|
|
5
6
|
module PQCrypto
|
|
6
7
|
module JWT
|
|
@@ -8,6 +9,7 @@ module PQCrypto
|
|
|
8
9
|
module MLDSA87
|
|
9
10
|
extend ::JWT::JWA::SigningAlgorithm
|
|
10
11
|
extend PQCrypto::JWT::JWA::MLDSA
|
|
12
|
+
extend PQCrypto::JWT::JWA::MLDSAStreaming
|
|
11
13
|
|
|
12
14
|
ALG = "ML-DSA-87".freeze
|
|
13
15
|
PQ_CRYPTO_ALGORITHM = :ml_dsa_87
|
|
@@ -8,24 +8,23 @@ module PQCrypto
|
|
|
8
8
|
module JWA
|
|
9
9
|
module MLDSAStreaming
|
|
10
10
|
DEFAULT_CHUNK_SIZE = 1 << 20
|
|
11
|
-
|
|
11
|
+
STREAMING_ERRORS = [JSON::ParserError, ArgumentError, PQCrypto::Error, PQCrypto::JWT::Error, EncodingError].freeze
|
|
12
12
|
|
|
13
13
|
def streaming_supported?
|
|
14
|
-
pq_crypto_algorithm
|
|
15
|
-
|
|
14
|
+
PQCrypto::Signature.supported.include?(pq_crypto_algorithm)
|
|
15
|
+
rescue PQCrypto::Error
|
|
16
|
+
false
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def sign_io(signing_key:, payload_io: nil, io: nil, header_fields: {}, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ensure_secret_key!(signing_key)
|
|
20
|
+
ensure_streaming!
|
|
21
|
+
ensure_key!(signing_key, PQCrypto::Signature::SecretKey, "signing")
|
|
22
22
|
source = payload_io || io
|
|
23
23
|
raise ArgumentError, "payload_io must respond to #read" unless source.respond_to?(:read)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
signature = signing_key.sign_io(signing_input, chunk_size: chunk_size, context: EMPTY_CONTEXT)
|
|
25
|
+
encoded_header = base64url(JSON.generate(header_fields.transform_keys(&:to_s).merge("alg" => alg)))
|
|
26
|
+
input = DetachedSigningInputIO.new(encoded_header, source, chunk_size: chunk_size)
|
|
27
|
+
signature = signing_key.sign_io(input, chunk_size: chunk_size, context: EMPTY_CONTEXT)
|
|
29
28
|
"#{encoded_header}..#{base64url(signature)}"
|
|
30
29
|
rescue PQCrypto::JWT::Error, ArgumentError
|
|
31
30
|
raise
|
|
@@ -34,46 +33,43 @@ module PQCrypto
|
|
|
34
33
|
end
|
|
35
34
|
|
|
36
35
|
def verify_io(verification_key:, token:, payload_io:, chunk_size: DEFAULT_CHUNK_SIZE)
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
ensure_streaming!
|
|
37
|
+
ensure_key!(verification_key, PQCrypto::Signature::PublicKey, "verification")
|
|
39
38
|
raise ArgumentError, "token must be a String" unless token.is_a?(String)
|
|
40
39
|
raise ArgumentError, "payload_io must respond to #read" unless payload_io.respond_to?(:read)
|
|
41
40
|
|
|
42
|
-
encoded_header, encoded_payload, encoded_signature = token.split(".", -1)
|
|
43
|
-
return false unless
|
|
41
|
+
encoded_header, encoded_payload, encoded_signature, extra = token.split(".", -1)
|
|
42
|
+
return false unless extra.nil? && encoded_payload == "" && encoded_signature
|
|
44
43
|
|
|
45
44
|
header = JSON.parse(Base64.urlsafe_decode64(encoded_header))
|
|
46
|
-
return false unless header["alg"] == alg
|
|
45
|
+
return false unless header.is_a?(Hash) && header["alg"] == alg
|
|
47
46
|
|
|
48
47
|
signature = Base64.urlsafe_decode64(encoded_signature)
|
|
49
|
-
|
|
50
|
-
verified = verification_key.verify_io(signing_input, signature, chunk_size: chunk_size, context: EMPTY_CONTEXT)
|
|
51
|
-
return false unless verified
|
|
48
|
+
return false unless signature_length_valid?(signature)
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
input = DetachedSigningInputIO.new(encoded_header, payload_io, chunk_size: chunk_size)
|
|
51
|
+
return false unless verification_key.verify_io(input, signature, chunk_size: chunk_size, context: EMPTY_CONTEXT)
|
|
52
|
+
|
|
53
|
+
[payload_io.respond_to?(:pos) ? payload_io.pos : nil, header]
|
|
54
|
+
rescue *STREAMING_ERRORS
|
|
55
55
|
false
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
def verify_io!(
|
|
59
|
-
|
|
60
|
-
raise ::JWT::VerificationError, "Streaming JWS verification failed" unless result
|
|
61
|
-
|
|
58
|
+
def verify_io!(**kwargs)
|
|
59
|
+
verify_io(**kwargs) || raise(::JWT::VerificationError, "Streaming JWS verification failed")
|
|
62
60
|
true
|
|
63
61
|
end
|
|
64
62
|
|
|
65
63
|
private
|
|
66
64
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
end
|
|
65
|
+
def ensure_streaming!
|
|
66
|
+
return if streaming_supported?
|
|
70
67
|
|
|
71
|
-
|
|
72
|
-
hash.each_with_object({}) { |(key, value), out| out[String(key)] = value }
|
|
68
|
+
raise PQCrypto::JWT::UnsupportedFeature, "#{alg} does not support streaming JWS"
|
|
73
69
|
end
|
|
74
70
|
|
|
75
|
-
def
|
|
76
|
-
|
|
71
|
+
def base64url(bytes)
|
|
72
|
+
Base64.urlsafe_encode64(String(bytes).b, padding: false)
|
|
77
73
|
end
|
|
78
74
|
end
|
|
79
75
|
|
|
@@ -89,45 +85,67 @@ module PQCrypto
|
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
def read(length = nil, outbuf = nil)
|
|
92
|
-
length
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
if length.nil?
|
|
89
|
+
drain_until_eof
|
|
90
|
+
return nil if @buffer.empty?
|
|
91
|
+
|
|
92
|
+
result = @buffer
|
|
93
|
+
@buffer = +""
|
|
94
|
+
else
|
|
95
|
+
raise ArgumentError, "negative length #{length} given" if length.negative?
|
|
96
|
+
return replace_outbuf(outbuf, +"") if length.zero?
|
|
97
|
+
|
|
98
|
+
fill(length)
|
|
99
|
+
return nil if @buffer.empty?
|
|
100
|
+
|
|
101
|
+
result = @buffer.byteslice(0, length)
|
|
102
|
+
@buffer = @buffer.byteslice(result.bytesize..-1) || +""
|
|
103
|
+
end
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
replace_outbuf(outbuf, result)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def replace_outbuf(outbuf, result)
|
|
98
111
|
outbuf&.replace(result)
|
|
99
112
|
outbuf || result
|
|
100
113
|
end
|
|
101
114
|
|
|
102
|
-
|
|
115
|
+
def drain_until_eof
|
|
116
|
+
consume_prefix
|
|
117
|
+
feed_one_chunk until @payload_done
|
|
118
|
+
end
|
|
103
119
|
|
|
104
120
|
def fill(length)
|
|
105
|
-
|
|
106
|
-
while @buffer.bytesize < length && !@payload_done
|
|
107
|
-
chunk = @payload_io.read(@chunk_size)
|
|
108
|
-
if chunk.nil? || chunk.empty?
|
|
109
|
-
@buffer << Base64.urlsafe_encode64(@carry, padding: false) unless @carry.empty?
|
|
110
|
-
@carry = +""
|
|
111
|
-
@payload_done = true
|
|
112
|
-
break
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
bytes = @carry + chunk.b
|
|
116
|
-
full_length = bytes.bytesize - (bytes.bytesize % 3)
|
|
117
|
-
if full_length.positive?
|
|
118
|
-
@buffer << Base64.urlsafe_encode64(bytes.byteslice(0, full_length), padding: false)
|
|
119
|
-
@carry = bytes.byteslice(full_length..-1) || +""
|
|
120
|
-
else
|
|
121
|
-
@carry = bytes
|
|
122
|
-
end
|
|
123
|
-
end
|
|
121
|
+
consume_prefix
|
|
122
|
+
feed_one_chunk while @buffer.bytesize < length && !@payload_done
|
|
124
123
|
end
|
|
125
124
|
|
|
126
|
-
def consume_prefix
|
|
127
|
-
return
|
|
125
|
+
def consume_prefix
|
|
126
|
+
return if @prefix_done
|
|
128
127
|
|
|
128
|
+
@buffer << @prefix
|
|
129
129
|
@prefix_done = true
|
|
130
|
-
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def feed_one_chunk
|
|
133
|
+
chunk = @payload_io.read(@chunk_size)
|
|
134
|
+
if chunk.nil? || chunk.empty?
|
|
135
|
+
@buffer << Base64.urlsafe_encode64(@carry, padding: false) unless @carry.empty?
|
|
136
|
+
@carry = +""
|
|
137
|
+
@payload_done = true
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
bytes = @carry + chunk.b
|
|
142
|
+
aligned = bytes.bytesize - (bytes.bytesize % 3)
|
|
143
|
+
if aligned.positive?
|
|
144
|
+
@buffer << Base64.urlsafe_encode64(bytes.byteslice(0, aligned), padding: false)
|
|
145
|
+
@carry = bytes.byteslice(aligned..-1) || +""
|
|
146
|
+
else
|
|
147
|
+
@carry = bytes
|
|
148
|
+
end
|
|
131
149
|
end
|
|
132
150
|
end
|
|
133
151
|
end
|
data/lib/pq_crypto/jwt/jwa.rb
CHANGED
|
@@ -6,40 +6,23 @@ require "pq_crypto"
|
|
|
6
6
|
module PQCrypto
|
|
7
7
|
module JWT
|
|
8
8
|
module JWA
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self::PQ_CRYPTO_ALGORITHM
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def streaming_supported?
|
|
19
|
-
false
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def sign_io(**)
|
|
23
|
-
raise PQCrypto::JWT::UnsupportedAlgorithm, "#{alg} does not support streaming JWS"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def verify_io(**)
|
|
27
|
-
raise PQCrypto::JWT::UnsupportedAlgorithm, "#{alg} does not support streaming JWS"
|
|
28
|
-
end
|
|
9
|
+
EMPTY_CONTEXT = "".b.freeze
|
|
10
|
+
VERIFY_ERRORS = [
|
|
11
|
+
PQCrypto::Error, PQCrypto::JWT::Error,
|
|
12
|
+
::JWT::DecodeError, ::JWT::VerificationError, ::JWT::JWKError,
|
|
13
|
+
ArgumentError, TypeError, EncodingError
|
|
14
|
+
].freeze
|
|
29
15
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def key_kind
|
|
35
|
-
:signature
|
|
36
|
-
end
|
|
16
|
+
module MLDSA
|
|
17
|
+
def valid_alg?(alg_to_validate) = alg_to_validate == alg
|
|
18
|
+
def pq_crypto_algorithm = self::PQ_CRYPTO_ALGORITHM
|
|
19
|
+
def key_kind = :signature
|
|
37
20
|
|
|
38
21
|
def sign(data:, signing_key:)
|
|
39
|
-
|
|
22
|
+
ensure_key!(signing_key, PQCrypto::Signature::SecretKey, "signing")
|
|
40
23
|
raise ArgumentError, "data must be a String" unless data.is_a?(String)
|
|
41
24
|
|
|
42
|
-
signing_key.sign(data.b)
|
|
25
|
+
signing_key.sign(data.b, context: EMPTY_CONTEXT)
|
|
43
26
|
rescue PQCrypto::JWT::KeyTypeError, ArgumentError
|
|
44
27
|
raise
|
|
45
28
|
rescue StandardError => e
|
|
@@ -47,47 +30,33 @@ module PQCrypto
|
|
|
47
30
|
end
|
|
48
31
|
|
|
49
32
|
def verify(data:, signature:, verification_key:)
|
|
50
|
-
return false unless
|
|
51
|
-
return false unless
|
|
52
|
-
return false unless signature.is_a?(String)
|
|
33
|
+
return false unless verification_key.is_a?(PQCrypto::Signature::PublicKey)
|
|
34
|
+
return false unless verification_key.algorithm == pq_crypto_algorithm
|
|
35
|
+
return false unless data.is_a?(String) && signature.is_a?(String)
|
|
53
36
|
return false unless signature_length_valid?(signature)
|
|
54
37
|
|
|
55
|
-
verification_key.verify(data.b, signature.b)
|
|
56
|
-
rescue
|
|
38
|
+
verification_key.verify(data.b, signature.b, context: EMPTY_CONTEXT)
|
|
39
|
+
rescue *VERIFY_ERRORS
|
|
57
40
|
false
|
|
58
41
|
end
|
|
59
42
|
|
|
60
43
|
private
|
|
61
44
|
|
|
62
|
-
def
|
|
63
|
-
unless key.is_a?(
|
|
64
|
-
raise PQCrypto::JWT::KeyTypeError,
|
|
65
|
-
"#{alg} signing requires PQCrypto::Signature::SecretKey"
|
|
45
|
+
def ensure_key!(key, klass, role)
|
|
46
|
+
unless key.is_a?(klass)
|
|
47
|
+
raise PQCrypto::JWT::KeyTypeError, "#{alg} #{role} requires #{klass}"
|
|
66
48
|
end
|
|
67
|
-
|
|
68
49
|
return if key.algorithm == pq_crypto_algorithm
|
|
69
50
|
|
|
70
51
|
raise PQCrypto::JWT::KeyTypeError,
|
|
71
|
-
"#{alg}
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def ensure_public_key!(key)
|
|
75
|
-
unless public_key_for_this_algorithm?(key)
|
|
76
|
-
raise PQCrypto::JWT::KeyTypeError,
|
|
77
|
-
"#{alg} verification requires PQCrypto::Signature::PublicKey for #{pq_crypto_algorithm.inspect}"
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def public_key_for_this_algorithm?(key)
|
|
82
|
-
key.is_a?(PQCrypto::Signature::PublicKey) && key.algorithm == pq_crypto_algorithm
|
|
52
|
+
"#{alg} #{role} requires #{pq_crypto_algorithm.inspect} key, got #{key.algorithm.inspect}"
|
|
83
53
|
end
|
|
84
54
|
|
|
85
55
|
def signature_length_valid?(signature)
|
|
86
|
-
|
|
87
|
-
expected = details[:signature_bytes] || details["signature_bytes"]
|
|
56
|
+
expected = PQCrypto::Signature.details(pq_crypto_algorithm)[:signature_bytes]
|
|
88
57
|
expected.nil? || signature.bytesize == expected
|
|
89
|
-
rescue
|
|
90
|
-
|
|
58
|
+
rescue *VERIFY_ERRORS
|
|
59
|
+
false
|
|
91
60
|
end
|
|
92
61
|
end
|
|
93
62
|
end
|