castrum 0.9.3 → 0.9.4
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 +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- 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 +96 -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 +68 -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/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 +132 -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 +180 -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 +779 -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 +566 -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 +881 -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 +72 -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/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 +407 -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/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 +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- 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 +17 -11
- 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/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
package/rust/ffi/jwt.rs
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// rust/ffi/jwt.rs — JWT + Ed25519 C-ABI exports.
|
|
2
|
+
//
|
|
3
|
+
// HS256/EdDSA signing + verification and Ed25519 keypair/sign/verify for the
|
|
4
|
+
// RBAC auth path. Opaque-handle variants use the precompiled-key instances;
|
|
5
|
+
// the scalar variants derive the key per call.
|
|
6
|
+
|
|
7
|
+
use std::slice;
|
|
8
|
+
|
|
9
|
+
use super::util::{cstring_return, panic_guard};
|
|
10
|
+
|
|
11
|
+
/// JwtSigner: sign pre-serialized claim JSON with the PRECOMPILED key + ttl via
|
|
12
|
+
/// its opaque inner handle. Needed-size convention; 0 = invalid claims JSON /
|
|
13
|
+
/// null handle (real error).
|
|
14
|
+
///
|
|
15
|
+
/// # Safety
|
|
16
|
+
/// `inner` must be a valid `JwtSigner` pointer from `inner_ptr()`, alive for
|
|
17
|
+
/// the call; `out` for writes up to `out_cap`.
|
|
18
|
+
#[no_mangle]
|
|
19
|
+
pub unsafe extern "C" fn castrum_jwt_signer_sign(
|
|
20
|
+
inner: usize,
|
|
21
|
+
claims: *const u8,
|
|
22
|
+
claims_len: usize,
|
|
23
|
+
now_seconds: i64,
|
|
24
|
+
out: *mut u8,
|
|
25
|
+
out_cap: usize,
|
|
26
|
+
) -> usize {
|
|
27
|
+
if inner == 0 || claims.is_null() || out.is_null() {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let c = slice::from_raw_parts(claims, claims_len);
|
|
31
|
+
let Some(token) = panic_guard(
|
|
32
|
+
|| unsafe {
|
|
33
|
+
crate::crypto::jwt::jwt_signer_sign_bytes_core(
|
|
34
|
+
inner as *const crate::crypto::jwt::JwtSigner,
|
|
35
|
+
c,
|
|
36
|
+
now_seconds,
|
|
37
|
+
)
|
|
38
|
+
.ok()
|
|
39
|
+
},
|
|
40
|
+
None,
|
|
41
|
+
) else {
|
|
42
|
+
return 0;
|
|
43
|
+
};
|
|
44
|
+
if token.len() > out_cap {
|
|
45
|
+
return token.len();
|
|
46
|
+
}
|
|
47
|
+
slice::from_raw_parts_mut(out, token.len()).copy_from_slice(&token);
|
|
48
|
+
token.len()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// JwtSigner: verify a JWT with the PRECOMPILED key → claims JSON bytes.
|
|
52
|
+
/// Returns bytes written (0 = invalid signature / expired / malformed → null).
|
|
53
|
+
///
|
|
54
|
+
/// # Safety
|
|
55
|
+
/// `inner` must be a valid `JwtSigner` pointer from `inner_ptr()`, alive for
|
|
56
|
+
/// the call; `out` for writes up to `out_cap`.
|
|
57
|
+
#[no_mangle]
|
|
58
|
+
pub unsafe extern "C" fn castrum_jwt_signer_verify(
|
|
59
|
+
inner: usize,
|
|
60
|
+
token: *const u8,
|
|
61
|
+
token_len: usize,
|
|
62
|
+
now_seconds: i64,
|
|
63
|
+
out: *mut u8,
|
|
64
|
+
out_cap: usize,
|
|
65
|
+
) -> usize {
|
|
66
|
+
if inner == 0 || token.is_null() || out.is_null() {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
let t = slice::from_raw_parts(token, token_len);
|
|
70
|
+
let Some(claims) = panic_guard(
|
|
71
|
+
|| unsafe {
|
|
72
|
+
crate::crypto::jwt::jwt_signer_verify_core(
|
|
73
|
+
inner as *const crate::crypto::jwt::JwtSigner,
|
|
74
|
+
t,
|
|
75
|
+
now_seconds,
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
None,
|
|
79
|
+
) else {
|
|
80
|
+
return 0;
|
|
81
|
+
};
|
|
82
|
+
if claims.len() > out_cap {
|
|
83
|
+
return claims.len();
|
|
84
|
+
}
|
|
85
|
+
slice::from_raw_parts_mut(out, claims.len()).copy_from_slice(&claims);
|
|
86
|
+
claims.len()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Sign a JWT (HS256) from pre-serialized claim JSON bytes — the C-ABI sibling
|
|
90
|
+
/// of `jwt_sign_bytes`. `ttl <= 0` means no `iat`/`exp` injection (the napi
|
|
91
|
+
/// `Option<i64>` sentinel). Returns bytes written (0 on invalid claims JSON); a
|
|
92
|
+
/// result larger than `out_cap` reports the exact needed size (see
|
|
93
|
+
/// `compress_to_out!` for the convention).
|
|
94
|
+
///
|
|
95
|
+
/// # Safety
|
|
96
|
+
/// `claims`/`secret` must be valid for reads of their lengths; `out` for
|
|
97
|
+
/// writes up to `out_cap`.
|
|
98
|
+
#[no_mangle]
|
|
99
|
+
pub unsafe extern "C" fn castrum_jwt_sign_bytes(
|
|
100
|
+
claims: *const u8,
|
|
101
|
+
clen: usize,
|
|
102
|
+
secret: *const u8,
|
|
103
|
+
slen: usize,
|
|
104
|
+
ttl: i64,
|
|
105
|
+
now: i64,
|
|
106
|
+
) -> *const std::os::raw::c_char {
|
|
107
|
+
if claims.is_null() || secret.is_null() {
|
|
108
|
+
return std::ptr::null();
|
|
109
|
+
}
|
|
110
|
+
// Wrap in panic_guard: sonic parse + token build allocate — a panic must
|
|
111
|
+
// not unwind through the C ABI (process crash); it becomes null instead.
|
|
112
|
+
let token = panic_guard(
|
|
113
|
+
|| {
|
|
114
|
+
let Ok(mut value) =
|
|
115
|
+
sonic_rs::from_slice::<sonic_rs::Value>(slice::from_raw_parts(claims, clen))
|
|
116
|
+
else {
|
|
117
|
+
return None;
|
|
118
|
+
};
|
|
119
|
+
let Ok(payload_b64) = crate::crypto::jwt::inject_and_payload_b64_sonic(
|
|
120
|
+
&mut value,
|
|
121
|
+
if ttl > 0 { Some(ttl) } else { None },
|
|
122
|
+
now,
|
|
123
|
+
) else {
|
|
124
|
+
return None;
|
|
125
|
+
};
|
|
126
|
+
Some(crate::crypto::jwt::build_token(
|
|
127
|
+
crate::crypto::jwt::jwt_header_b64(),
|
|
128
|
+
&payload_b64,
|
|
129
|
+
slice::from_raw_parts(secret, slen),
|
|
130
|
+
))
|
|
131
|
+
},
|
|
132
|
+
None,
|
|
133
|
+
);
|
|
134
|
+
let Some(token) = token else {
|
|
135
|
+
return std::ptr::null();
|
|
136
|
+
};
|
|
137
|
+
cstring_return(token.len(), |out| {
|
|
138
|
+
out[..token.len()].copy_from_slice(&token);
|
|
139
|
+
Some(token.len())
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Sign a JWT (HS256) written directly into a caller buffer — the pooled
|
|
144
|
+
/// sibling of `castrum_jwt_sign_bytes` (no cstring round-trip; the caller
|
|
145
|
+
/// sizes from the claim JSON — same bound the cstring path uses). Returns
|
|
146
|
+
/// bytes written, the exact required size when `out_cap` is too small, or 0 on
|
|
147
|
+
/// invalid claims JSON.
|
|
148
|
+
///
|
|
149
|
+
/// # Safety
|
|
150
|
+
/// `claims`/`secret` must be valid for reads of their lengths; `out` for
|
|
151
|
+
/// writes up to `out_cap`.
|
|
152
|
+
#[no_mangle]
|
|
153
|
+
pub unsafe extern "C" fn castrum_jwt_sign_bytes_into(
|
|
154
|
+
claims: *const u8,
|
|
155
|
+
clen: usize,
|
|
156
|
+
secret: *const u8,
|
|
157
|
+
slen: usize,
|
|
158
|
+
ttl: i64,
|
|
159
|
+
now: i64,
|
|
160
|
+
out: *mut u8,
|
|
161
|
+
out_cap: usize,
|
|
162
|
+
) -> usize {
|
|
163
|
+
if claims.is_null() || secret.is_null() || out.is_null() {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
// Wrap in panic_guard: sonic parse + token build allocate — a panic must
|
|
167
|
+
// not unwind through the C ABI (process crash); it becomes 0 instead.
|
|
168
|
+
let token = panic_guard(
|
|
169
|
+
|| {
|
|
170
|
+
let Ok(mut value) =
|
|
171
|
+
sonic_rs::from_slice::<sonic_rs::Value>(slice::from_raw_parts(claims, clen))
|
|
172
|
+
else {
|
|
173
|
+
return None;
|
|
174
|
+
};
|
|
175
|
+
let Ok(payload_b64) = crate::crypto::jwt::inject_and_payload_b64_sonic(
|
|
176
|
+
&mut value,
|
|
177
|
+
if ttl > 0 { Some(ttl) } else { None },
|
|
178
|
+
now,
|
|
179
|
+
) else {
|
|
180
|
+
return None;
|
|
181
|
+
};
|
|
182
|
+
Some(crate::crypto::jwt::build_token(
|
|
183
|
+
crate::crypto::jwt::jwt_header_b64(),
|
|
184
|
+
&payload_b64,
|
|
185
|
+
slice::from_raw_parts(secret, slen),
|
|
186
|
+
))
|
|
187
|
+
},
|
|
188
|
+
None,
|
|
189
|
+
);
|
|
190
|
+
let Some(token) = token else {
|
|
191
|
+
return 0;
|
|
192
|
+
};
|
|
193
|
+
if out_cap < token.len() {
|
|
194
|
+
// Needed-size convention: exact retry, no re-run.
|
|
195
|
+
return token.len();
|
|
196
|
+
}
|
|
197
|
+
let o = slice::from_raw_parts_mut(out, token.len());
|
|
198
|
+
o.copy_from_slice(&token);
|
|
199
|
+
token.len()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Verify an HS256 compact JWT → its claims as a null-terminated JSON C string
|
|
203
|
+
/// into the per-thread reused buffer (`cstring` return). `null` on invalid
|
|
204
|
+
/// signature / expired / malformed. This replaces the NAPI `jwtVerify` object
|
|
205
|
+
/// marshal (which builds a `serde_json::Value` + napi value on the verify
|
|
206
|
+
/// path) with a plain string crossing for the claims.
|
|
207
|
+
///
|
|
208
|
+
/// # Safety
|
|
209
|
+
/// `token`/`secret` must be valid for reads of their lengths.
|
|
210
|
+
#[no_mangle]
|
|
211
|
+
pub unsafe extern "C" fn castrum_jwt_verify(
|
|
212
|
+
token: *const u8,
|
|
213
|
+
tlen: usize,
|
|
214
|
+
secret: *const u8,
|
|
215
|
+
slen: usize,
|
|
216
|
+
now: i64,
|
|
217
|
+
) -> *const std::os::raw::c_char {
|
|
218
|
+
if token.is_null() || secret.is_null() {
|
|
219
|
+
return std::ptr::null();
|
|
220
|
+
}
|
|
221
|
+
let claims = panic_guard(
|
|
222
|
+
|| {
|
|
223
|
+
crate::crypto::jwt::verify_token(
|
|
224
|
+
slice::from_raw_parts(token, tlen),
|
|
225
|
+
slice::from_raw_parts(secret, slen),
|
|
226
|
+
now,
|
|
227
|
+
)
|
|
228
|
+
},
|
|
229
|
+
None,
|
|
230
|
+
);
|
|
231
|
+
let Some(json) = claims else {
|
|
232
|
+
return std::ptr::null();
|
|
233
|
+
};
|
|
234
|
+
cstring_return(json.len(), |out| {
|
|
235
|
+
out[..json.len()].copy_from_slice(&json);
|
|
236
|
+
Some(json.len())
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Ed25519 / EdDSA JWT (RBAC auth) ────────────────────────────
|
|
241
|
+
// Ed25519 keypair generation + sign/verify for the EdDSA JWT path used by the
|
|
242
|
+
// auth module. Key formats: PKCS#8 v1 DER (private, 48 B) + SPKI DER (public,
|
|
243
|
+
// 44 B) — byte-identical to Node `crypto` exports, so the pure-TS fallback and
|
|
244
|
+
// the Rust addon produce the same `.env` keys.
|
|
245
|
+
|
|
246
|
+
/// Generate an Ed25519 keypair → packed `[u32 privLen][priv PKCS#8 v1 DER]
|
|
247
|
+
/// [u32 pubLen][pub SPKI DER]` (100 bytes total) written into `out`. Returns
|
|
248
|
+
/// bytes written, the exact required size when `out_cap` is too small, or 0
|
|
249
|
+
/// on CSPRNG failure.
|
|
250
|
+
///
|
|
251
|
+
/// # Safety
|
|
252
|
+
/// `out` must be valid for writes up to `out_cap`.
|
|
253
|
+
#[no_mangle]
|
|
254
|
+
pub unsafe extern "C" fn castrum_ed25519_generate_keypair(out: *mut u8, out_cap: usize) -> usize {
|
|
255
|
+
if out.is_null() {
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
let priv_len = crate::crypto::ed25519::ED25519_PKCS8_V1_LEN;
|
|
259
|
+
let pub_len = crate::crypto::ed25519::ED25519_SPKI_LEN;
|
|
260
|
+
let packed_len = 4 + priv_len + 4 + pub_len; // 100
|
|
261
|
+
if out_cap < packed_len {
|
|
262
|
+
// Needed-size convention: exact required size, no re-run.
|
|
263
|
+
return packed_len;
|
|
264
|
+
}
|
|
265
|
+
let result = panic_guard(crate::crypto::ed25519::generate_keypair, None);
|
|
266
|
+
let Some((private_der, public_der)) = result else {
|
|
267
|
+
return 0;
|
|
268
|
+
};
|
|
269
|
+
let o = slice::from_raw_parts_mut(out, packed_len);
|
|
270
|
+
o[0..4].copy_from_slice(&(private_der.len() as u32).to_le_bytes());
|
|
271
|
+
o[4..4 + private_der.len()].copy_from_slice(&private_der);
|
|
272
|
+
let pub_start = 4 + private_der.len();
|
|
273
|
+
o[pub_start..pub_start + 4].copy_from_slice(&(public_der.len() as u32).to_le_bytes());
|
|
274
|
+
o[pub_start + 4..packed_len].copy_from_slice(&public_der);
|
|
275
|
+
packed_len
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Sign a message with an Ed25519 private key (PKCS#8 DER) → the 64-byte
|
|
279
|
+
/// signature written into `out`. Returns bytes written (64), the exact
|
|
280
|
+
/// required size when `out_cap` is too small, or 0 on invalid private key.
|
|
281
|
+
///
|
|
282
|
+
/// # Safety
|
|
283
|
+
/// `key`/`msg` must be valid for reads of their lengths; `out` for writes up
|
|
284
|
+
/// to `out_cap`.
|
|
285
|
+
#[no_mangle]
|
|
286
|
+
pub unsafe extern "C" fn castrum_ed25519_sign(
|
|
287
|
+
key: *const u8,
|
|
288
|
+
klen: usize,
|
|
289
|
+
msg: *const u8,
|
|
290
|
+
mlen: usize,
|
|
291
|
+
out: *mut u8,
|
|
292
|
+
out_cap: usize,
|
|
293
|
+
) -> usize {
|
|
294
|
+
if key.is_null() || msg.is_null() || out.is_null() {
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
let sig_len = crate::crypto::ed25519::ED25519_SIGNATURE_LEN; // 64
|
|
298
|
+
if out_cap < sig_len {
|
|
299
|
+
return sig_len; // exact needed size
|
|
300
|
+
}
|
|
301
|
+
let sig = panic_guard(
|
|
302
|
+
|| {
|
|
303
|
+
crate::crypto::ed25519::sign(
|
|
304
|
+
slice::from_raw_parts(key, klen),
|
|
305
|
+
slice::from_raw_parts(msg, mlen),
|
|
306
|
+
)
|
|
307
|
+
},
|
|
308
|
+
None,
|
|
309
|
+
);
|
|
310
|
+
let Some(sig) = sig else {
|
|
311
|
+
return 0; // invalid private key — real error
|
|
312
|
+
};
|
|
313
|
+
slice::from_raw_parts_mut(out, sig_len).copy_from_slice(&sig);
|
|
314
|
+
sig_len
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Verify a 64-byte Ed25519 signature over `msg` with an SPKI DER (or raw
|
|
318
|
+
/// 32-byte) public key. Returns 1 (valid) or 0 (invalid).
|
|
319
|
+
///
|
|
320
|
+
/// # Safety
|
|
321
|
+
/// `key`/`msg`/`sig` must be valid for reads of their lengths.
|
|
322
|
+
#[no_mangle]
|
|
323
|
+
pub unsafe extern "C" fn castrum_ed25519_verify(
|
|
324
|
+
key: *const u8,
|
|
325
|
+
klen: usize,
|
|
326
|
+
msg: *const u8,
|
|
327
|
+
mlen: usize,
|
|
328
|
+
sig: *const u8,
|
|
329
|
+
slen: usize,
|
|
330
|
+
) -> u32 {
|
|
331
|
+
if key.is_null() || msg.is_null() || sig.is_null() {
|
|
332
|
+
return 0;
|
|
333
|
+
}
|
|
334
|
+
let valid = panic_guard(
|
|
335
|
+
|| {
|
|
336
|
+
crate::crypto::ed25519::verify(
|
|
337
|
+
slice::from_raw_parts(key, klen),
|
|
338
|
+
slice::from_raw_parts(msg, mlen),
|
|
339
|
+
slice::from_raw_parts(sig, slen),
|
|
340
|
+
)
|
|
341
|
+
},
|
|
342
|
+
false,
|
|
343
|
+
);
|
|
344
|
+
valid as u32
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/// Sign an EdDSA (Ed25519) JWT from pre-serialized claim JSON → compact token
|
|
348
|
+
/// as a null-terminated C string (cstring return — the engine clones it). `ttl
|
|
349
|
+
/// <= 0` means no `iat`/`exp` injection. `null` on invalid claims JSON /
|
|
350
|
+
/// invalid private key.
|
|
351
|
+
///
|
|
352
|
+
/// # Safety
|
|
353
|
+
/// `claims`/`key` must be valid for reads of their lengths.
|
|
354
|
+
#[no_mangle]
|
|
355
|
+
pub unsafe extern "C" fn castrum_jwt_eddsa_sign(
|
|
356
|
+
claims: *const u8,
|
|
357
|
+
clen: usize,
|
|
358
|
+
key: *const u8,
|
|
359
|
+
klen: usize,
|
|
360
|
+
ttl: i64,
|
|
361
|
+
now: i64,
|
|
362
|
+
) -> *const std::os::raw::c_char {
|
|
363
|
+
if claims.is_null() || key.is_null() {
|
|
364
|
+
return std::ptr::null();
|
|
365
|
+
}
|
|
366
|
+
let token = panic_guard(
|
|
367
|
+
|| {
|
|
368
|
+
crate::crypto::jwt::sign_eddsa(
|
|
369
|
+
slice::from_raw_parts(claims, clen),
|
|
370
|
+
slice::from_raw_parts(key, klen),
|
|
371
|
+
if ttl > 0 { Some(ttl) } else { None },
|
|
372
|
+
now,
|
|
373
|
+
)
|
|
374
|
+
},
|
|
375
|
+
None,
|
|
376
|
+
);
|
|
377
|
+
let Some(token) = token else {
|
|
378
|
+
return std::ptr::null();
|
|
379
|
+
};
|
|
380
|
+
cstring_return(token.len(), |out| {
|
|
381
|
+
out[..token.len()].copy_from_slice(&token);
|
|
382
|
+
Some(token.len())
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// Verify an EdDSA (Ed25519) JWT → its claims as a null-terminated JSON C
|
|
387
|
+
/// string (cstring return). `null` on invalid signature / expired / malformed.
|
|
388
|
+
///
|
|
389
|
+
/// # Safety
|
|
390
|
+
/// `token`/`key` must be valid for reads of their lengths.
|
|
391
|
+
#[no_mangle]
|
|
392
|
+
pub unsafe extern "C" fn castrum_jwt_eddsa_verify(
|
|
393
|
+
token: *const u8,
|
|
394
|
+
tlen: usize,
|
|
395
|
+
key: *const u8,
|
|
396
|
+
klen: usize,
|
|
397
|
+
now: i64,
|
|
398
|
+
) -> *const std::os::raw::c_char {
|
|
399
|
+
if token.is_null() || key.is_null() {
|
|
400
|
+
return std::ptr::null();
|
|
401
|
+
}
|
|
402
|
+
let claims = panic_guard(
|
|
403
|
+
|| {
|
|
404
|
+
crate::crypto::jwt::verify_token_eddsa(
|
|
405
|
+
slice::from_raw_parts(token, tlen),
|
|
406
|
+
slice::from_raw_parts(key, klen),
|
|
407
|
+
now,
|
|
408
|
+
)
|
|
409
|
+
},
|
|
410
|
+
None,
|
|
411
|
+
);
|
|
412
|
+
let Some(json) = claims else {
|
|
413
|
+
return std::ptr::null();
|
|
414
|
+
};
|
|
415
|
+
cstring_return(json.len(), |out| {
|
|
416
|
+
out[..json.len()].copy_from_slice(&json);
|
|
417
|
+
Some(json.len())
|
|
418
|
+
})
|
|
419
|
+
}
|