castrum 0.9.3 → 0.9.6
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.
- package/Cargo.lock +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +105 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +69 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +136 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +211 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +770 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +567 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +883 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +73 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +594 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/health.ts +1 -2
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +71 -27
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// rust/crypto/jwt/api.rs — napi entry points for JWT signing/verification.
|
|
2
|
+
//
|
|
3
|
+
// Thin napi boundary over the pure token core in `token.rs`: scalar sign/
|
|
4
|
+
// verify, the precompiled-key `JwtSigner` instance, the C-ABI `_core` helpers
|
|
5
|
+
// consumed by `rust/ffi.rs`, and the packed sign/verify batches.
|
|
6
|
+
|
|
7
|
+
use napi::bindgen_prelude::*;
|
|
8
|
+
use napi_derive::napi;
|
|
9
|
+
|
|
10
|
+
use crate::crypto::base64::base64url_encode_bytes;
|
|
11
|
+
|
|
12
|
+
use super::{
|
|
13
|
+
build_token, build_token_with_key, inject_and_payload_b64, inject_and_payload_b64_sonic,
|
|
14
|
+
inject_ttl_claims, jwt_header_b64, sign_eddsa, verify_token, verify_token_eddsa,
|
|
15
|
+
verify_token_with_key,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/// Sign a JWT (HS256). `claims` is any JSON value; when it is an object and a
|
|
19
|
+
/// positive `ttlSeconds` is given, `iat`/`exp` are injected (unless present).
|
|
20
|
+
/// `nowSeconds` is the caller-supplied current epoch seconds (keeps the core
|
|
21
|
+
/// time-free and testable). Returns the compact token bytes.
|
|
22
|
+
#[napi]
|
|
23
|
+
pub fn jwt_sign(
|
|
24
|
+
claims: serde_json::Value,
|
|
25
|
+
secret: Uint8Array,
|
|
26
|
+
ttl_seconds: Option<i64>,
|
|
27
|
+
now_seconds: i64,
|
|
28
|
+
) -> Result<Buffer> {
|
|
29
|
+
let mut claims = claims;
|
|
30
|
+
let payload_b64 = inject_and_payload_b64(&mut claims, ttl_seconds, now_seconds)?;
|
|
31
|
+
Ok(Buffer::from(build_token(
|
|
32
|
+
jwt_header_b64(),
|
|
33
|
+
&payload_b64,
|
|
34
|
+
secret.as_ref(),
|
|
35
|
+
)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Sign a JWT (HS256) from pre-serialized claim JSON bytes. Avoids the napi
|
|
39
|
+
/// `serde_json::Value` DOM marshal of `jwt_sign` for callers that already hold
|
|
40
|
+
/// the claim bytes (e.g. JSON.stringify'd on the JS side). The claims are
|
|
41
|
+
/// parsed straight into a `sonic_rs::Value` (compact, no per-key heap
|
|
42
|
+
/// `String`). Semantics are identical to `jwt_sign` (incl. `iat`/`exp`
|
|
43
|
+
/// injection).
|
|
44
|
+
#[napi]
|
|
45
|
+
pub fn jwt_sign_bytes(
|
|
46
|
+
claims_json: Uint8Array,
|
|
47
|
+
secret: Uint8Array,
|
|
48
|
+
ttl_seconds: Option<i64>,
|
|
49
|
+
now_seconds: i64,
|
|
50
|
+
) -> Result<Buffer> {
|
|
51
|
+
let mut claims: sonic_rs::Value =
|
|
52
|
+
sonic_rs::from_slice(claims_json.as_ref()).map_err(napi_err)?;
|
|
53
|
+
let payload_b64 = inject_and_payload_b64_sonic(&mut claims, ttl_seconds, now_seconds)?;
|
|
54
|
+
Ok(Buffer::from(build_token(
|
|
55
|
+
jwt_header_b64(),
|
|
56
|
+
&payload_b64,
|
|
57
|
+
secret.as_ref(),
|
|
58
|
+
)))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Verify a JWT (HS256 signature + time claims). Returns the decoded claims
|
|
62
|
+
/// object (marshaled from the compact sonic Value), or `null` on any failure
|
|
63
|
+
/// (malformed, bad signature, wrong `alg`, expired, not-yet-valid `nbf`/`iat`,
|
|
64
|
+
/// non-JSON payload).
|
|
65
|
+
#[napi]
|
|
66
|
+
pub fn jwt_verify(
|
|
67
|
+
env: Env,
|
|
68
|
+
token: Uint8Array,
|
|
69
|
+
secret: Uint8Array,
|
|
70
|
+
now_seconds: i64,
|
|
71
|
+
) -> Result<Unknown<'static>> {
|
|
72
|
+
let Some(payload) = verify_token(token.as_ref(), secret.as_ref(), now_seconds) else {
|
|
73
|
+
return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
|
|
74
|
+
};
|
|
75
|
+
match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
|
|
76
|
+
Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
|
|
77
|
+
// The payload already parsed for the time-claim check — unreachable.
|
|
78
|
+
Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Sign an EdDSA (Ed25519) JWT from pre-serialized claim JSON bytes. Semantics
|
|
83
|
+
/// identical to `jwt_sign_bytes` (incl. `iat`/`exp` injection); the signature
|
|
84
|
+
/// is Ed25519 (RFC 8032) under the `EdDSA` alg.
|
|
85
|
+
#[napi]
|
|
86
|
+
pub fn jwt_sign_eddsa(
|
|
87
|
+
claims_json: Uint8Array,
|
|
88
|
+
private_key: Uint8Array,
|
|
89
|
+
ttl_seconds: Option<i64>,
|
|
90
|
+
now_seconds: i64,
|
|
91
|
+
) -> Result<Buffer> {
|
|
92
|
+
match sign_eddsa(
|
|
93
|
+
claims_json.as_ref(),
|
|
94
|
+
private_key.as_ref(),
|
|
95
|
+
ttl_seconds,
|
|
96
|
+
now_seconds,
|
|
97
|
+
) {
|
|
98
|
+
Some(token) => Ok(Buffer::from(token)),
|
|
99
|
+
None => Err(Error::from_reason(
|
|
100
|
+
"EdDSA sign failed (invalid claims or private key)",
|
|
101
|
+
)),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Verify an EdDSA (Ed25519) JWT (signature + time claims). Returns the decoded
|
|
106
|
+
/// claims object, or `null` on any failure.
|
|
107
|
+
#[napi]
|
|
108
|
+
pub fn jwt_verify_eddsa(
|
|
109
|
+
env: Env,
|
|
110
|
+
token: Uint8Array,
|
|
111
|
+
public_key: Uint8Array,
|
|
112
|
+
now_seconds: i64,
|
|
113
|
+
) -> Result<Unknown<'static>> {
|
|
114
|
+
let Some(payload) = verify_token_eddsa(token.as_ref(), public_key.as_ref(), now_seconds) else {
|
|
115
|
+
return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
|
|
116
|
+
};
|
|
117
|
+
match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
|
|
118
|
+
Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
|
|
119
|
+
// The payload already parsed for the time-claim check — unreachable.
|
|
120
|
+
Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Higher-order instance: precompiles the HMAC-SHA256 key once from `secret`,
|
|
125
|
+
/// so `sign`/`verify` never re-derive the key on the per-call path. `ttl` is
|
|
126
|
+
/// fixed at construction (0/absent → no `iat`/`exp` injection).
|
|
127
|
+
#[napi]
|
|
128
|
+
pub struct JwtSigner {
|
|
129
|
+
pub(crate) key: aws_lc_rs::hmac::Key,
|
|
130
|
+
ttl_seconds: Option<i64>,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[napi]
|
|
134
|
+
impl JwtSigner {
|
|
135
|
+
#[napi(constructor)]
|
|
136
|
+
pub fn new(secret: Uint8Array, ttl_seconds: Option<i64>) -> Self {
|
|
137
|
+
Self {
|
|
138
|
+
key: aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref()),
|
|
139
|
+
ttl_seconds,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Opaque handle to the precompiled key + ttl, for the `bun:ffi` C-ABI
|
|
144
|
+
/// fast path (`castrum_jwt_signer_*` in rust/ffi.rs). Only valid while THIS
|
|
145
|
+
/// instance is alive; the JS wrapper holds the instance.
|
|
146
|
+
#[napi]
|
|
147
|
+
pub fn inner_ptr(&self) -> u64 {
|
|
148
|
+
self as *const JwtSigner as u64
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Sign a JWT with the precompiled key. When `ttl` was set at construction,
|
|
152
|
+
/// `iat`/`exp` are injected into object claims (unless already present).
|
|
153
|
+
#[napi]
|
|
154
|
+
pub fn sign(&self, claims: serde_json::Value, now_seconds: i64) -> Result<Buffer> {
|
|
155
|
+
let mut claims = claims;
|
|
156
|
+
let payload_b64 = inject_and_payload_b64(&mut claims, self.ttl_seconds, now_seconds)?;
|
|
157
|
+
Ok(Buffer::from(build_token_with_key(
|
|
158
|
+
jwt_header_b64(),
|
|
159
|
+
&payload_b64,
|
|
160
|
+
&self.key,
|
|
161
|
+
)))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Sign a JWT from pre-serialized claim JSON bytes with the precompiled
|
|
165
|
+
/// key — no napi `serde_json::Value` marshal, no per-call key derivation.
|
|
166
|
+
#[napi]
|
|
167
|
+
pub fn sign_bytes(&self, claims_json: Uint8Array, now_seconds: i64) -> Result<Buffer> {
|
|
168
|
+
let mut claims: sonic_rs::Value =
|
|
169
|
+
sonic_rs::from_slice(claims_json.as_ref()).map_err(napi_err)?;
|
|
170
|
+
let payload_b64 = inject_and_payload_b64_sonic(&mut claims, self.ttl_seconds, now_seconds)?;
|
|
171
|
+
Ok(Buffer::from(build_token_with_key(
|
|
172
|
+
jwt_header_b64(),
|
|
173
|
+
&payload_b64,
|
|
174
|
+
&self.key,
|
|
175
|
+
)))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Verify a JWT with the precompiled key (signature + `alg` + time claims).
|
|
179
|
+
/// Returns the decoded claims object, or `null` on any failure.
|
|
180
|
+
#[napi]
|
|
181
|
+
pub fn verify(
|
|
182
|
+
&self,
|
|
183
|
+
env: Env,
|
|
184
|
+
token: Uint8Array,
|
|
185
|
+
now_seconds: i64,
|
|
186
|
+
) -> Result<Unknown<'static>> {
|
|
187
|
+
let Some(payload) = verify_token_with_key(token.as_ref(), &self.key, now_seconds) else {
|
|
188
|
+
return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
|
|
189
|
+
};
|
|
190
|
+
match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
|
|
191
|
+
Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
|
|
192
|
+
// The payload already parsed for the time-claim check — unreachable.
|
|
193
|
+
Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// C-ABI support: sign pre-serialized claim JSON with the precompiled key.
|
|
199
|
+
/// `Err(())` on invalid claims JSON (napi parity: throws).
|
|
200
|
+
///
|
|
201
|
+
/// # Safety
|
|
202
|
+
/// `p` must be a valid `*const JwtSigner` from `inner_ptr`, alive for the call.
|
|
203
|
+
pub(crate) unsafe fn jwt_signer_sign_bytes_core(
|
|
204
|
+
p: *const JwtSigner,
|
|
205
|
+
claims_json: &[u8],
|
|
206
|
+
now_seconds: i64,
|
|
207
|
+
) -> std::result::Result<Vec<u8>, ()> {
|
|
208
|
+
let this = &*p;
|
|
209
|
+
let mut claims: sonic_rs::Value = sonic_rs::from_slice(claims_json).map_err(|_| ())?;
|
|
210
|
+
let payload_b64 =
|
|
211
|
+
inject_and_payload_b64_sonic(&mut claims, this.ttl_seconds, now_seconds).map_err(|_| ())?;
|
|
212
|
+
Ok(build_token_with_key(
|
|
213
|
+
jwt_header_b64(),
|
|
214
|
+
&payload_b64,
|
|
215
|
+
&this.key,
|
|
216
|
+
))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// C-ABI support: verify a JWT with the precompiled key → the decoded payload
|
|
220
|
+
/// BYTES (valid JSON claims), or None on invalid signature / expired /
|
|
221
|
+
/// malformed. No `serde_json::Value` DOM, no re-serialize.
|
|
222
|
+
///
|
|
223
|
+
/// # Safety
|
|
224
|
+
/// `p` must be a valid `*const JwtSigner` from `inner_ptr`, alive for the call.
|
|
225
|
+
pub(crate) unsafe fn jwt_signer_verify_core(
|
|
226
|
+
p: *const JwtSigner,
|
|
227
|
+
token: &[u8],
|
|
228
|
+
now_seconds: i64,
|
|
229
|
+
) -> Option<Vec<u8>> {
|
|
230
|
+
let this = &*p;
|
|
231
|
+
verify_token_with_key(token, &this.key, now_seconds)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn napi_err(e: impl std::fmt::Display) -> Error {
|
|
235
|
+
Error::from_reason(e.to_string())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Parallel sign batch: packed `[u32 count]{[u32 len][claims-json]}` in →
|
|
239
|
+
/// packed `[u32 count]{[u32 len][token]}` out (same secret/ttl/now for all).
|
|
240
|
+
#[napi]
|
|
241
|
+
pub fn jwt_sign_batch_packed(
|
|
242
|
+
data: Uint8Array,
|
|
243
|
+
secret: Uint8Array,
|
|
244
|
+
ttl_seconds: Option<i64>,
|
|
245
|
+
now_seconds: i64,
|
|
246
|
+
) -> Result<Buffer> {
|
|
247
|
+
// Derive the HMAC key ONCE for the whole batch (build_token derives a
|
|
248
|
+
// fresh key per call), then route through the shared packed batch helper
|
|
249
|
+
// (zero-alloc serial PackedIter path; rayon direct-write when justified).
|
|
250
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref());
|
|
251
|
+
|
|
252
|
+
let sign_one = |claims_json: &[u8]| -> Vec<u8> {
|
|
253
|
+
let Ok(mut claims) = sonic_rs::from_slice::<sonic_rs::Value>(claims_json) else {
|
|
254
|
+
return Vec::new();
|
|
255
|
+
};
|
|
256
|
+
inject_ttl_claims(&mut claims, ttl_seconds, now_seconds);
|
|
257
|
+
let header_b64 = jwt_header_b64();
|
|
258
|
+
let Ok(payload_bytes) = sonic_rs::to_vec(&claims) else {
|
|
259
|
+
return Vec::new();
|
|
260
|
+
};
|
|
261
|
+
let payload_b64 = base64url_encode_bytes(&payload_bytes);
|
|
262
|
+
build_token_with_key(header_b64, &payload_b64, &key)
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
crate::util::run_packed_batch(data.as_ref(), sign_one).map(Buffer::from)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Parallel verify batch: packed `[u32 count]{[u32 len][token]}` in → bitset
|
|
269
|
+
/// (`[u32 count][ceil(n/8) bytes]`) of valid tokens.
|
|
270
|
+
#[napi]
|
|
271
|
+
pub fn jwt_verify_batch_packed(
|
|
272
|
+
data: Uint8Array,
|
|
273
|
+
secret: Uint8Array,
|
|
274
|
+
now_seconds: i64,
|
|
275
|
+
) -> Result<Buffer> {
|
|
276
|
+
// Derive the HMAC key ONCE for the whole batch (verify_token derives a
|
|
277
|
+
// fresh key per call), then route through the shared bitset batch helper
|
|
278
|
+
// (zero-alloc serial PackedIter path; rayon direct-write when justified).
|
|
279
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref());
|
|
280
|
+
crate::util::run_bitset_batch(
|
|
281
|
+
data.as_ref(),
|
|
282
|
+
|token| verify_token_with_key(token, &key, now_seconds).is_some(),
|
|
283
|
+
4096,
|
|
284
|
+
)
|
|
285
|
+
.map(Buffer::from)
|
|
286
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//! HS256 / EdDSA JSON Web Tokens.
|
|
2
|
+
//!
|
|
3
|
+
//! Split into a small folder so the token machinery is navigable:
|
|
4
|
+
//! - `token.rs` — pure token core (headers, HMAC, split/build/verify, zero-DOM
|
|
5
|
+
//! time-claim checks, `iat`/`exp` injection). Reused by the napi boundary,
|
|
6
|
+
//! the C-ABI fast paths in `rust/ffi.rs`, and `crate::crypto::ed25519`.
|
|
7
|
+
//! - `api.rs` — napi entry points (scalar sign/verify, precompiled-key
|
|
8
|
+
//! `JwtSigner` instance, C-ABI `_core` helpers, packed sign/verify batches).
|
|
9
|
+
//! - `tests.rs` — unit tests.
|
|
10
|
+
//!
|
|
11
|
+
//! All items are re-exported at this module root (`pub use self::token::*` +
|
|
12
|
+
//! `pub use self::api::*`) so existing `crate::crypto::jwt::*` call sites keep
|
|
13
|
+
//! working unchanged.
|
|
14
|
+
|
|
15
|
+
mod api;
|
|
16
|
+
mod token;
|
|
17
|
+
|
|
18
|
+
#[cfg(test)]
|
|
19
|
+
mod tests;
|
|
20
|
+
|
|
21
|
+
pub use self::api::*;
|
|
22
|
+
pub use self::token::*;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// rust/crypto/jwt/tests.rs — unit tests for the JWT core + napi entry points.
|
|
2
|
+
//
|
|
3
|
+
// Exercises the token core in `token.rs` and the napi boundary in `napi.rs`.
|
|
4
|
+
// The old local base64url helpers were consolidated into
|
|
5
|
+
// `crate::crypto::base64`; the tests import them under their former `b64url_*`
|
|
6
|
+
// names so the assertions read the same.
|
|
7
|
+
|
|
8
|
+
use super::*;
|
|
9
|
+
use crate::crypto::base64::{
|
|
10
|
+
base64url_decode_bytes as b64url_decode, base64url_encode_bytes as b64url_encode,
|
|
11
|
+
};
|
|
12
|
+
use napi::bindgen_prelude::*;
|
|
13
|
+
use sonic_rs::JsonValueTrait;
|
|
14
|
+
|
|
15
|
+
const SECRET: &[u8] = b"test-secret-key";
|
|
16
|
+
|
|
17
|
+
fn token(claims: &serde_json::Value, secret: &[u8]) -> Vec<u8> {
|
|
18
|
+
token_with_header(
|
|
19
|
+
&serde_json::json!({ "alg": "HS256", "typ": "JWT" }),
|
|
20
|
+
claims,
|
|
21
|
+
secret,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn token_with_header(
|
|
26
|
+
header: &serde_json::Value,
|
|
27
|
+
claims: &serde_json::Value,
|
|
28
|
+
secret: &[u8],
|
|
29
|
+
) -> Vec<u8> {
|
|
30
|
+
let header_b64 = b64url_encode(&serde_json::to_vec(header).unwrap());
|
|
31
|
+
let payload_b64 = b64url_encode(&serde_json::to_vec(claims).unwrap());
|
|
32
|
+
build_token(&header_b64, &payload_b64, secret)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn verify_token_rejects_wrong_alg() {
|
|
37
|
+
// Regression: `alg` must be allowlisted. A token with a VALID HS256
|
|
38
|
+
// signature but header `alg: "none"` (or any non-HS256) must be
|
|
39
|
+
// rejected — otherwise alg-confusion attacks become possible later.
|
|
40
|
+
let claims = serde_json::json!({ "sub": "1" });
|
|
41
|
+
let none = token_with_header(
|
|
42
|
+
&serde_json::json!({ "alg": "none", "typ": "JWT" }),
|
|
43
|
+
&claims,
|
|
44
|
+
SECRET,
|
|
45
|
+
);
|
|
46
|
+
assert!(verify_token(&none, SECRET, 1_000_000).is_none());
|
|
47
|
+
|
|
48
|
+
let rs256 = token_with_header(
|
|
49
|
+
&serde_json::json!({ "alg": "RS256", "typ": "JWT" }),
|
|
50
|
+
&claims,
|
|
51
|
+
SECRET,
|
|
52
|
+
);
|
|
53
|
+
assert!(verify_token(&rs256, SECRET, 1_000_000).is_none());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[test]
|
|
57
|
+
fn verify_token_enforces_nbf() {
|
|
58
|
+
let claims = serde_json::json!({ "sub": "1", "nbf": 2_000_000 });
|
|
59
|
+
let t = token(&claims, SECRET);
|
|
60
|
+
// Before nbf -> rejected; at/after nbf -> accepted.
|
|
61
|
+
assert!(verify_token(&t, SECRET, 1_999_999).is_none());
|
|
62
|
+
assert!(verify_token(&t, SECRET, 2_000_000).is_some());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn verify_token_enforces_iat_leeway() {
|
|
67
|
+
let now = 1_000_000;
|
|
68
|
+
// iat in the far future -> rejected (beyond 60s skew leeway).
|
|
69
|
+
let far = token(
|
|
70
|
+
&serde_json::json!({ "sub": "1", "iat": now + 10_000 }),
|
|
71
|
+
SECRET,
|
|
72
|
+
);
|
|
73
|
+
assert!(verify_token(&far, SECRET, now).is_none());
|
|
74
|
+
// iat within leeway -> accepted.
|
|
75
|
+
let near = token(&serde_json::json!({ "sub": "1", "iat": now + 30 }), SECRET);
|
|
76
|
+
assert!(verify_token(&near, SECRET, now).is_some());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn verify_token_accepts_valid_claims() {
|
|
81
|
+
let claims = serde_json::json!({ "sub": "123", "role": "admin" });
|
|
82
|
+
let t = token(&claims, SECRET);
|
|
83
|
+
let payload = verify_token(&t, SECRET, 1_000_000).expect("valid token verifies");
|
|
84
|
+
let v: sonic_rs::Value = sonic_rs::from_slice(&payload).unwrap();
|
|
85
|
+
assert_eq!(v["sub"], "123");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn sign_then_verify_roundtrip() {
|
|
90
|
+
let claims = serde_json::json!({ "sub": "123", "name": "John", "role": "admin" });
|
|
91
|
+
let t = token(&claims, SECRET);
|
|
92
|
+
assert!(verify_signature(&t, SECRET));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn verify_rejects_wrong_secret() {
|
|
97
|
+
let claims = serde_json::json!({ "sub": "1" });
|
|
98
|
+
let t = token(&claims, SECRET);
|
|
99
|
+
assert!(!verify_signature(&t, b"wrong-secret"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[test]
|
|
103
|
+
fn verify_rejects_tampered_payload() {
|
|
104
|
+
let claims = serde_json::json!({ "sub": "1", "role": "user" });
|
|
105
|
+
let t = token(&claims, SECRET);
|
|
106
|
+
let first_dot = memchr::memchr(b'.', &t).unwrap();
|
|
107
|
+
let second_rel = memchr::memchr(b'.', &t[first_dot + 1..]).unwrap();
|
|
108
|
+
let payload_start = first_dot + 1;
|
|
109
|
+
let payload_end = payload_start + second_rel;
|
|
110
|
+
let mid = (payload_start + payload_end) / 2;
|
|
111
|
+
|
|
112
|
+
let mut tampered = t.clone();
|
|
113
|
+
tampered[mid] ^= 0x01;
|
|
114
|
+
assert!(!verify_signature(&tampered, SECRET));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn verify_rejects_malformed() {
|
|
119
|
+
assert!(!verify_signature(b"no-dots-here", SECRET));
|
|
120
|
+
assert!(!verify_signature(b"a.b.c.d", SECRET));
|
|
121
|
+
assert!(!verify_signature(b".", SECRET));
|
|
122
|
+
assert!(!verify_signature(b"", SECRET));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[test]
|
|
126
|
+
fn base64url_roundtrip() {
|
|
127
|
+
let data = b"hello world \x00\xff binary";
|
|
128
|
+
let enc = b64url_encode(data);
|
|
129
|
+
assert_eq!(b64url_decode(&enc).unwrap(), data);
|
|
130
|
+
assert!(!enc.contains(&b'='));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn base64url_uses_urlsafe_alphabet() {
|
|
135
|
+
// Bytes 0xfb 0xff encode to base64url "-_8" (no +/ or /).
|
|
136
|
+
assert_eq!(b64url_encode(b"\xfb\xff"), b"-_8");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn split_handles_three_segments() {
|
|
141
|
+
let t = b"aaa.bbb.ccc";
|
|
142
|
+
let parts = split_token(t).unwrap();
|
|
143
|
+
assert_eq!(parts.header_b64, b"aaa");
|
|
144
|
+
assert_eq!(parts.payload_b64, b"bbb");
|
|
145
|
+
assert_eq!(parts.sig_b64, b"ccc");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[test]
|
|
149
|
+
fn jwt_signer_instance_roundtrips_and_injects_claims() {
|
|
150
|
+
// Precompiled-key instance: sign+verify with no per-call key derivation.
|
|
151
|
+
let signer = JwtSigner::new(Uint8Array::new(SECRET.to_vec()), Some(3600));
|
|
152
|
+
let now = 1_000_000i64;
|
|
153
|
+
|
|
154
|
+
let claims = serde_json::json!({ "sub": "123" });
|
|
155
|
+
let token = signer.sign(claims.clone(), now).unwrap();
|
|
156
|
+
|
|
157
|
+
// ttl injected iat/exp at construction time.
|
|
158
|
+
let verified_payload = verify_token_with_key(&token, &signer.key, now).expect("token verifies");
|
|
159
|
+
let verified: sonic_rs::Value = sonic_rs::from_slice(&verified_payload).unwrap();
|
|
160
|
+
assert_eq!(verified["sub"], "123");
|
|
161
|
+
assert_eq!(verified["iat"], now);
|
|
162
|
+
assert_eq!(verified["exp"], now + 3600);
|
|
163
|
+
|
|
164
|
+
// Same instance verifies, wrong key instance rejects.
|
|
165
|
+
let other = JwtSigner::new(Uint8Array::new(b"other-secret".to_vec()), None);
|
|
166
|
+
assert!(verify_token_with_key(&token, &other.key, now).is_none());
|
|
167
|
+
|
|
168
|
+
// No ttl -> no iat/exp injected.
|
|
169
|
+
let no_ttl = JwtSigner::new(Uint8Array::new(SECRET.to_vec()), None);
|
|
170
|
+
let t2 = no_ttl.sign(claims.clone(), now).unwrap();
|
|
171
|
+
let v2_payload = verify_token_with_key(&t2, &no_ttl.key, now).unwrap();
|
|
172
|
+
let v2: sonic_rs::Value = sonic_rs::from_slice(&v2_payload).unwrap();
|
|
173
|
+
assert!(v2.get("iat").is_none());
|
|
174
|
+
assert!(v2.get("exp").is_none());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[test]
|
|
178
|
+
fn jwt_sign_bytes_matches_value_sign_and_verifies() {
|
|
179
|
+
let claims_json = br#"{"sub":"123","name":"John"}"#.to_vec();
|
|
180
|
+
let secret = Uint8Array::new(SECRET.to_vec());
|
|
181
|
+
|
|
182
|
+
// Byte-JSON sign (no napi Value marshal) matches the Value sign
|
|
183
|
+
// semantically. Key ORDER differs between the two serializers — sonic
|
|
184
|
+
// (`sort_keys` → BTreeMap) emits sorted keys, serde_json
|
|
185
|
+
// (`preserve_order` → IndexMap) emits insertion order — so compare the
|
|
186
|
+
// decoded claims, not the raw token bytes.
|
|
187
|
+
let from_bytes = jwt_sign_bytes(
|
|
188
|
+
Uint8Array::new(claims_json.clone()),
|
|
189
|
+
secret,
|
|
190
|
+
None,
|
|
191
|
+
1_000_000,
|
|
192
|
+
)
|
|
193
|
+
.unwrap();
|
|
194
|
+
let claims: serde_json::Value = serde_json::from_slice(&claims_json).unwrap();
|
|
195
|
+
let from_value = jwt_sign(claims, Uint8Array::new(SECRET.to_vec()), None, 1_000_000).unwrap();
|
|
196
|
+
let claims_from_bytes: sonic_rs::Value = sonic_rs::from_slice(
|
|
197
|
+
&b64url_decode(split_token(&from_bytes).unwrap().payload_b64).unwrap(),
|
|
198
|
+
)
|
|
199
|
+
.unwrap();
|
|
200
|
+
let claims_from_value: sonic_rs::Value = sonic_rs::from_slice(
|
|
201
|
+
&b64url_decode(split_token(&from_value).unwrap().payload_b64).unwrap(),
|
|
202
|
+
)
|
|
203
|
+
.unwrap();
|
|
204
|
+
assert_eq!(claims_from_bytes, claims_from_value);
|
|
205
|
+
|
|
206
|
+
// The bytes-signed token verifies and carries the claims.
|
|
207
|
+
let verified_payload = verify_token(from_bytes.as_ref(), SECRET, 1_000_000).unwrap();
|
|
208
|
+
let verified: sonic_rs::Value = sonic_rs::from_slice(&verified_payload).unwrap();
|
|
209
|
+
assert_eq!(verified["sub"], "123");
|
|
210
|
+
assert_eq!(verified["name"], "John");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn jwt_sign_bytes_injects_ttl_and_instance_sign_bytes_works() {
|
|
215
|
+
let claims_json = br#"{"sub":"123"}"#.to_vec();
|
|
216
|
+
let token = jwt_sign_bytes(
|
|
217
|
+
Uint8Array::new(claims_json.clone()),
|
|
218
|
+
Uint8Array::new(SECRET.to_vec()),
|
|
219
|
+
Some(3600),
|
|
220
|
+
1_000_000,
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
223
|
+
let payload = verify_token(&token, SECRET, 1_000_000).unwrap();
|
|
224
|
+
let v: sonic_rs::Value = sonic_rs::from_slice(&payload).unwrap();
|
|
225
|
+
assert_eq!(v["iat"], 1_000_000);
|
|
226
|
+
assert_eq!(v["exp"], 1_000_000 + 3600);
|
|
227
|
+
|
|
228
|
+
// Precompiled-key instance: sign_bytes injects its construction ttl.
|
|
229
|
+
let signer = JwtSigner::new(Uint8Array::new(SECRET.to_vec()), Some(3600));
|
|
230
|
+
let t2 = signer
|
|
231
|
+
.sign_bytes(Uint8Array::new(claims_json), 2_000_000)
|
|
232
|
+
.unwrap();
|
|
233
|
+
let v2_payload = verify_token_with_key(&t2, &signer.key, 2_000_000).unwrap();
|
|
234
|
+
let v2: sonic_rs::Value = sonic_rs::from_slice(&v2_payload).unwrap();
|
|
235
|
+
assert_eq!(v2["iat"], 2_000_000);
|
|
236
|
+
assert_eq!(v2["exp"], 2_000_000 + 3600);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn eddsa_sign_verify_round_trip() {
|
|
241
|
+
let (private_der, public_der) = crate::crypto::ed25519::generate_keypair().unwrap();
|
|
242
|
+
let claims_json = br#"{"sub":"user-1","roles":["admin"]}"#.to_vec();
|
|
243
|
+
|
|
244
|
+
let token = sign_eddsa(&claims_json, &private_der, None, 1_000_000).unwrap();
|
|
245
|
+
// Compact token: exactly two dots, EdDSA header.
|
|
246
|
+
let parts = split_token(&token).unwrap();
|
|
247
|
+
assert_eq!(
|
|
248
|
+
b64url_decode(parts.header_b64).unwrap(),
|
|
249
|
+
br#"{"alg":"EdDSA","typ":"JWT"}"#
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// Verifies with the matching public key.
|
|
253
|
+
let payload = verify_token_eddsa(&token, &public_der, 1_000_000).unwrap();
|
|
254
|
+
let v: sonic_rs::Value = sonic_rs::from_slice(&payload).unwrap();
|
|
255
|
+
assert_eq!(v["sub"], "user-1");
|
|
256
|
+
assert_eq!(v["roles"][0], "admin");
|
|
257
|
+
|
|
258
|
+
// Wrong public key (a different keypair) fails.
|
|
259
|
+
let (_, other_pub) = crate::crypto::ed25519::generate_keypair().unwrap();
|
|
260
|
+
assert!(verify_token_eddsa(&token, &other_pub, 1_000_000).is_none());
|
|
261
|
+
|
|
262
|
+
// Tampered payload fails (signature over header.payload is invalid).
|
|
263
|
+
let mut bad = token.clone();
|
|
264
|
+
let n = bad.len();
|
|
265
|
+
bad[n - 2] ^= 0x01;
|
|
266
|
+
assert!(verify_token_eddsa(&bad, &public_der, 1_000_000).is_none());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[test]
|
|
270
|
+
fn eddsa_injects_ttl_and_enforces_time_claims() {
|
|
271
|
+
let (private_der, public_der) = crate::crypto::ed25519::generate_keypair().unwrap();
|
|
272
|
+
let claims_json = br#"{"sub":"user-1"}"#.to_vec();
|
|
273
|
+
|
|
274
|
+
let token = sign_eddsa(&claims_json, &private_der, Some(3600), 1_000_000).unwrap();
|
|
275
|
+
let v: sonic_rs::Value =
|
|
276
|
+
sonic_rs::from_slice(&verify_token_eddsa(&token, &public_der, 1_000_000).unwrap()).unwrap();
|
|
277
|
+
assert_eq!(v["iat"], 1_000_000);
|
|
278
|
+
assert_eq!(v["exp"], 1_000_000 + 3600);
|
|
279
|
+
|
|
280
|
+
// Expired token rejected.
|
|
281
|
+
assert!(verify_token_eddsa(&token, &public_der, 1_000_000 + 3600).is_none());
|
|
282
|
+
// Not-yet-valid (future iat beyond leeway) rejected.
|
|
283
|
+
let future = sign_eddsa(&claims_json, &private_der, Some(3600), 1_000_000).unwrap();
|
|
284
|
+
assert!(verify_token_eddsa(&future, &public_der, 1_000_000 - 61).is_none());
|
|
285
|
+
assert!(verify_token_eddsa(&future, &public_der, 1_000_000).is_some());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[test]
|
|
289
|
+
fn eddsa_rejects_wrong_alg_and_malformed() {
|
|
290
|
+
let (private_der, public_der) = crate::crypto::ed25519::generate_keypair().unwrap();
|
|
291
|
+
let claims_json = br#"{"sub":"x"}"#.to_vec();
|
|
292
|
+
let token = sign_eddsa(&claims_json, &private_der, None, 1_000_000).unwrap();
|
|
293
|
+
|
|
294
|
+
// Swap the header alg to HS256 while keeping a valid Ed25519 signature
|
|
295
|
+
// over the new header.payload — the alg allowlist must reject it.
|
|
296
|
+
let header_b64 = b64url_encode(br#"{"alg":"HS256","typ":"JWT"}"#);
|
|
297
|
+
let parts = split_token(&token).unwrap();
|
|
298
|
+
let mut forged_input = Vec::new();
|
|
299
|
+
forged_input.extend_from_slice(&header_b64);
|
|
300
|
+
forged_input.push(b'.');
|
|
301
|
+
forged_input.extend_from_slice(parts.payload_b64);
|
|
302
|
+
let sig = crate::crypto::ed25519::sign(&private_der, &forged_input).unwrap();
|
|
303
|
+
let mut forged = forged_input;
|
|
304
|
+
forged.push(b'.');
|
|
305
|
+
forged.extend_from_slice(&b64url_encode(&sig));
|
|
306
|
+
assert!(verify_token_eddsa(&forged, &public_der, 1_000_000).is_none());
|
|
307
|
+
|
|
308
|
+
// Malformed tokens.
|
|
309
|
+
assert!(verify_token_eddsa(b"not-a-token", &public_der, 1_000_000).is_none());
|
|
310
|
+
assert!(verify_token_eddsa(b"a.b.c.d", &public_der, 1_000_000).is_none());
|
|
311
|
+
assert!(verify_token_eddsa(&token, &[], 1_000_000).is_none());
|
|
312
|
+
}
|