castrum 0.9.6 → 0.9.8
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 +4 -3
- package/Cargo.toml +1 -1
- 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 +501 -5
- package/dist/index.js +1658 -1314
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/BENCHMARKS.md +13 -0
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +1 -1
- package/docs/FFI_BUN_GUIDE.md +13 -1
- package/docs/HOW_TO_ADD_AN_OP.md +24 -0
- package/docs/INGRESS.md +4 -4
- package/docs/REPO_MAP.md +8 -6
- package/docs/superpowers/plans/2026-09-18-castrum-hardening-cleanup.md +617 -0
- package/index.ts +1 -0
- package/package.json +3 -2
- package/rust/crypto/aead.rs +44 -0
- package/rust/crypto/argon2.rs +32 -2
- package/rust/crypto/jwt/tests.rs +19 -0
- package/rust/crypto/jwt/token.rs +43 -28
- package/rust/crypto/session.rs +170 -7
- package/rust/ffi/crypto.rs +182 -61
- package/rust/ffi/hashing.rs +48 -33
- package/rust/ffi/http.rs +62 -36
- package/rust/ffi/metrics.rs +6 -2
- package/rust/ffi/payload.rs +47 -27
- package/rust/ffi/task.rs +51 -32
- package/rust/ffi/tests.rs +197 -0
- package/rust/ffi/util.rs +40 -0
- package/rust/http/headers.rs +13 -0
- package/rust/http/multipart.rs +139 -0
- package/rust/ingress/native_route.rs +21 -3
- package/rust/task/completion.rs +7 -0
- package/rust/task/mod.rs +1 -1
- package/rust/task/tests.rs +30 -0
- package/src/ingress/decode/baked-result.ts +13 -5
- package/src/ingress/decode/fast-result.ts +1 -1
- package/src/ingress/decode/result-base.ts +16 -2
- package/src/ingress/handlers.ts +103 -31
- package/src/ingress/headers/memoized-headers.ts +40 -0
- package/src/ingress/native-route.ts +25 -6
- package/src/ingress/options.ts +26 -2
- package/src/ingress/packing/gather-raw-headers.ts +24 -8
- package/src/ingress/response/baked-response.ts +118 -28
- package/src/ingress/router.ts +14 -2
- package/src/ingress/routes/common.ts +26 -10
- package/src/ingress/routes/echo.ts +50 -7
- package/src/ingress/routes/json-write.ts +21 -8
- package/src/ingress/routes/responder.ts +4 -4
- package/src/ingress/server-node.ts +33 -14
- package/src/ingress/server.ts +3 -4
- package/src/ingress/shared.ts +1 -1
- package/src/ingress/types.ts +15 -0
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs-selftest.ts +305 -0
- package/src/native/ffi/build/codecs.ts +0 -296
- package/src/native/ffi/build/instances-selftest.ts +264 -0
- package/src/native/ffi/build/instances.ts +39 -241
- package/src/native/ffi/build/metrics.ts +0 -7
- package/src/native/ffi/build/parse-selftest.ts +238 -0
- package/src/native/ffi/build/parse.ts +11 -234
- package/src/native/ffi/build.ts +5 -1
- package/src/native/ffi/selftest.ts +3 -3
- package/src/native/ffi/symbols.ts +460 -0
- package/src/native/ffi/types.ts +17 -0
- package/src/native/ffi.ts +2 -422
- package/src/native/ingress-binding.ts +39 -0
- package/src/runtime/codec.ts +24 -0
- package/src/runtime/types.ts +11 -0
- package/src/rust-ffi/batch/build.ts +18 -8
- package/src/rust-ffi/batch/types.ts +15 -0
- package/src/rust-ffi/options.ts +19 -0
- package/src/rust-ffi/scalar/crypto.ts +8 -0
- package/src/rust-ffi/scalar/factories.ts +47 -20
- package/src/rust-ffi/scalar/http.ts +60 -10
- package/src/rust-ffi/scalar/interface.ts +4 -1
- package/src/rust-ffi/scalar/payload.ts +27 -1
- package/src/shared/buffer-pool.ts +44 -9
- package/src/shared/codec.ts +26 -0
- package/src/shared/packed/parsers.ts +64 -7
- package/src/task/runtime.ts +29 -28
package/rust/ffi/crypto.rs
CHANGED
|
@@ -9,7 +9,7 @@ use pbkdf2::pbkdf2_hmac;
|
|
|
9
9
|
use sha2::Sha256;
|
|
10
10
|
use std::slice;
|
|
11
11
|
|
|
12
|
-
use super::util::{aead_alg, cstring_return,
|
|
12
|
+
use super::util::{aead_alg, cstring_return, panic_guard, with_hmac_key_cached};
|
|
13
13
|
|
|
14
14
|
/// HMAC-SHA256 verify → 1/0 (constant-time, hex sig compared after
|
|
15
15
|
/// whitespace-trim — mirrors the napi scalar path).
|
|
@@ -32,8 +32,9 @@ pub unsafe extern "C" fn castrum_hmac_sha256_verify(
|
|
|
32
32
|
let Some(sig_bytes) = crate::util::bytes::hex_decode_32(sig_t) else {
|
|
33
33
|
return 0;
|
|
34
34
|
};
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
with_hmac_key_cached(slice::from_raw_parts(key, klen), |k| {
|
|
36
|
+
u8::from(hmac::verify(k, slice::from_raw_parts(data, dlen), &sig_bytes).is_ok())
|
|
37
|
+
})
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/// CSRF constant-time verify → 1/0 (token format `<64-hex>.<64-hex>`).
|
|
@@ -50,11 +51,12 @@ pub unsafe extern "C" fn castrum_csrf_verify(
|
|
|
50
51
|
if token.is_null() || secret.is_null() {
|
|
51
52
|
return 0;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
|
|
55
|
+
u8::from(crate::crypto::csrf::csrf_verify_with_key(
|
|
56
|
+
slice::from_raw_parts(token, tlen),
|
|
57
|
+
k,
|
|
58
|
+
))
|
|
59
|
+
})
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/// Argon2id password verify → 1/0 (constant-time internally).
|
|
@@ -71,10 +73,17 @@ pub unsafe extern "C" fn castrum_password_verify(
|
|
|
71
73
|
if password.is_null() || phc.is_null() {
|
|
72
74
|
return 0;
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// The argon2 core allocates/derives — a panic must become 0, not unwind
|
|
77
|
+
// through `extern "C"`.
|
|
78
|
+
panic_guard(
|
|
79
|
+
|| {
|
|
80
|
+
u8::from(crate::crypto::argon2::verify_password(
|
|
81
|
+
slice::from_raw_parts(password, plen),
|
|
82
|
+
slice::from_raw_parts(phc, phclen),
|
|
83
|
+
))
|
|
84
|
+
},
|
|
85
|
+
0,
|
|
86
|
+
)
|
|
78
87
|
}
|
|
79
88
|
|
|
80
89
|
/// bcrypt password verify → 1/0 (PHC `$2b$` string).
|
|
@@ -95,10 +104,15 @@ pub unsafe extern "C" fn castrum_password_verify_bcrypt(
|
|
|
95
104
|
if password.is_null() || hash.is_null() {
|
|
96
105
|
return 0;
|
|
97
106
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
panic_guard(
|
|
108
|
+
|| {
|
|
109
|
+
let Ok(h) = std::ffi::CStr::from_ptr(hash).to_str() else {
|
|
110
|
+
return 0;
|
|
111
|
+
};
|
|
112
|
+
u8::from(bcrypt::verify(slice::from_raw_parts(password, plen), h).unwrap_or(false))
|
|
113
|
+
},
|
|
114
|
+
0,
|
|
115
|
+
)
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/// HMAC-SHA256 hex (64 chars) into `out`.
|
|
@@ -117,8 +131,9 @@ pub unsafe extern "C" fn castrum_hmac_sha256(
|
|
|
117
131
|
if key.is_null() || data.is_null() || out.is_null() || out_cap < 64 {
|
|
118
132
|
return 0;
|
|
119
133
|
}
|
|
120
|
-
let
|
|
121
|
-
|
|
134
|
+
let tag = with_hmac_key_cached(slice::from_raw_parts(key, klen), |k| {
|
|
135
|
+
hmac::sign(k, slice::from_raw_parts(data, dlen))
|
|
136
|
+
});
|
|
122
137
|
let mut hex = [0u8; 64];
|
|
123
138
|
crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
|
|
124
139
|
slice::from_raw_parts_mut(out, 64).copy_from_slice(&hex);
|
|
@@ -141,9 +156,10 @@ pub unsafe extern "C" fn castrum_sign_cookie(
|
|
|
141
156
|
return std::ptr::null();
|
|
142
157
|
}
|
|
143
158
|
let v = slice::from_raw_parts(value, vlen);
|
|
144
|
-
let key = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
145
159
|
cstring_return(vlen + 65, |out| {
|
|
146
|
-
|
|
160
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, slen), |key| {
|
|
161
|
+
crate::crypto::cookie_sign::sign_cookie_into(v, key, out)
|
|
162
|
+
})
|
|
147
163
|
})
|
|
148
164
|
}
|
|
149
165
|
|
|
@@ -173,9 +189,14 @@ pub unsafe extern "C" fn castrum_sign_cookie_into(
|
|
|
173
189
|
return needed;
|
|
174
190
|
}
|
|
175
191
|
let v = slice::from_raw_parts(value, vlen);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, slen), |key| {
|
|
193
|
+
crate::crypto::cookie_sign::sign_cookie_into(
|
|
194
|
+
v,
|
|
195
|
+
key,
|
|
196
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
.unwrap_or_default()
|
|
179
200
|
}
|
|
180
201
|
|
|
181
202
|
/// Verify a signed cookie → the value without its signature, returned as a
|
|
@@ -195,9 +216,10 @@ pub unsafe extern "C" fn castrum_verify_cookie(
|
|
|
195
216
|
return std::ptr::null();
|
|
196
217
|
}
|
|
197
218
|
let s = slice::from_raw_parts(signed, slen);
|
|
198
|
-
let key = hmac_key_cached(slice::from_raw_parts(secret, klen));
|
|
199
219
|
cstring_return(slen, |out| {
|
|
200
|
-
|
|
220
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, klen), |key| {
|
|
221
|
+
crate::crypto::cookie_sign::verify_cookie_into(s, key, out)
|
|
222
|
+
})
|
|
201
223
|
})
|
|
202
224
|
}
|
|
203
225
|
|
|
@@ -224,7 +246,6 @@ pub unsafe extern "C" fn castrum_verify_cookie_into(
|
|
|
224
246
|
return 0;
|
|
225
247
|
}
|
|
226
248
|
let s = slice::from_raw_parts(signed, slen);
|
|
227
|
-
let key = hmac_key_cached(slice::from_raw_parts(secret, klen));
|
|
228
249
|
// The value is a prefix of `signed` (everything before the last `.`), so
|
|
229
250
|
// `slen` is always a sufficient bound — needed-size only differs when the
|
|
230
251
|
// caller passed less.
|
|
@@ -237,8 +258,14 @@ pub unsafe extern "C" fn castrum_verify_cookie_into(
|
|
|
237
258
|
}
|
|
238
259
|
return slen;
|
|
239
260
|
}
|
|
240
|
-
|
|
241
|
-
|
|
261
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, klen), |key| {
|
|
262
|
+
crate::crypto::cookie_sign::verify_cookie_into(
|
|
263
|
+
s,
|
|
264
|
+
key,
|
|
265
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
.unwrap_or_default()
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
/// CSRF token (`<64-hex(rnd)>.<64-hex(sig)>`, 129 bytes) returned as a
|
|
@@ -254,7 +281,6 @@ pub unsafe extern "C" fn castrum_csrf_token(
|
|
|
254
281
|
if secret.is_null() {
|
|
255
282
|
return std::ptr::null();
|
|
256
283
|
}
|
|
257
|
-
let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
258
284
|
let mut rnd = [0u8; 32];
|
|
259
285
|
if getrandom::fill(&mut rnd).is_err() {
|
|
260
286
|
return std::ptr::null();
|
|
@@ -262,7 +288,10 @@ pub unsafe extern "C" fn castrum_csrf_token(
|
|
|
262
288
|
let mut rnd_hex = [0u8; 64];
|
|
263
289
|
crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
|
|
264
290
|
let mut sig_hex = [0u8; 64];
|
|
265
|
-
|
|
291
|
+
let signed = with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
|
|
292
|
+
hmac::sign(k, &rnd_hex)
|
|
293
|
+
});
|
|
294
|
+
crate::util::bytes::hex_encode(signed.as_ref(), &mut sig_hex);
|
|
266
295
|
cstring_return(129, |out| {
|
|
267
296
|
out[..64].copy_from_slice(&rnd_hex);
|
|
268
297
|
out[64] = b'.';
|
|
@@ -291,7 +320,6 @@ pub unsafe extern "C" fn castrum_csrf_token_into(
|
|
|
291
320
|
if out_cap < 129 {
|
|
292
321
|
return 129;
|
|
293
322
|
}
|
|
294
|
-
let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
295
323
|
let mut rnd = [0u8; 32];
|
|
296
324
|
if getrandom::fill(&mut rnd).is_err() {
|
|
297
325
|
return 0;
|
|
@@ -299,7 +327,10 @@ pub unsafe extern "C" fn castrum_csrf_token_into(
|
|
|
299
327
|
let mut rnd_hex = [0u8; 64];
|
|
300
328
|
crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
|
|
301
329
|
let mut sig_hex = [0u8; 64];
|
|
302
|
-
|
|
330
|
+
let signed = with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
|
|
331
|
+
hmac::sign(k, &rnd_hex)
|
|
332
|
+
});
|
|
333
|
+
crate::util::bytes::hex_encode(signed.as_ref(), &mut sig_hex);
|
|
303
334
|
let o = slice::from_raw_parts_mut(out, 129);
|
|
304
335
|
o[..64].copy_from_slice(&rnd_hex);
|
|
305
336
|
o[64] = b'.';
|
|
@@ -625,6 +656,58 @@ pub unsafe extern "C" fn castrum_random_token_into(
|
|
|
625
656
|
|
|
626
657
|
// ── Session envelope (fused JSON + HMAC) ────────────────────────────
|
|
627
658
|
|
|
659
|
+
/// Shared tail of both seal entry points: `None` (empty id/secret, or a panic)
|
|
660
|
+
/// → null pointer, otherwise clone the sealed token into the engine's
|
|
661
|
+
/// per-thread cstring buffer. Extracted so the cstring and byte forms cannot
|
|
662
|
+
/// diverge — the byte form exists only to change how the INPUTS cross.
|
|
663
|
+
fn sealed_to_cstr(sealed: Option<Vec<u8>>) -> *const std::os::raw::c_char {
|
|
664
|
+
let Some(tok) = sealed else {
|
|
665
|
+
return std::ptr::null();
|
|
666
|
+
};
|
|
667
|
+
super::util::cstring_return(tok.len(), move |buf| {
|
|
668
|
+
if buf.len() < tok.len() {
|
|
669
|
+
return None;
|
|
670
|
+
}
|
|
671
|
+
buf[..tok.len()].copy_from_slice(&tok);
|
|
672
|
+
Some(tok.len())
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/// Shared tail of both open entry points: pack the extracted envelope into the
|
|
677
|
+
/// caller's buffer as `[u8 ok=1][i64 exp][u32 idLen][id][u32 dataLen][dataJson]`,
|
|
678
|
+
/// or report the EXACT size required when `out_cap` is too small. `None` (bad
|
|
679
|
+
/// signature / malformed envelope) → `0`. Never returns `0` on a valid call.
|
|
680
|
+
///
|
|
681
|
+
/// # Safety
|
|
682
|
+
/// `out` must be valid for writes up to `out_cap`; the caller null-checks it.
|
|
683
|
+
unsafe fn pack_open_result(
|
|
684
|
+
opened: Option<(i64, Vec<u8>, Vec<u8>)>,
|
|
685
|
+
out: *mut u8,
|
|
686
|
+
out_cap: usize,
|
|
687
|
+
) -> usize {
|
|
688
|
+
let Some((exp, id, data)) = opened else {
|
|
689
|
+
return 0;
|
|
690
|
+
};
|
|
691
|
+
// Layout size: 1 + 8 + 4+id + 4+data
|
|
692
|
+
let need = 1 + 8 + 4 + id.len() + 4 + data.len();
|
|
693
|
+
if need > out_cap {
|
|
694
|
+
return need;
|
|
695
|
+
}
|
|
696
|
+
let o = slice::from_raw_parts_mut(out, need);
|
|
697
|
+
o[0] = 1;
|
|
698
|
+
o[1..9].copy_from_slice(&exp.to_le_bytes());
|
|
699
|
+
let id_len = id.len() as u32;
|
|
700
|
+
o[9..13].copy_from_slice(&id_len.to_le_bytes());
|
|
701
|
+
let mut p = 13usize;
|
|
702
|
+
o[p..p + id.len()].copy_from_slice(&id);
|
|
703
|
+
p += id.len();
|
|
704
|
+
let d_len = data.len() as u32;
|
|
705
|
+
o[p..p + 4].copy_from_slice(&d_len.to_le_bytes());
|
|
706
|
+
p += 4;
|
|
707
|
+
o[p..p + data.len()].copy_from_slice(&data);
|
|
708
|
+
need
|
|
709
|
+
}
|
|
710
|
+
|
|
628
711
|
/// Seal a session envelope NATIVELY: build
|
|
629
712
|
/// `{"id":"…","data":<data_json>,"exp":exp}` and HMAC-sign it into the
|
|
630
713
|
/// `payload.<64-hex>` cookie token — ONE crossing replaces the JS
|
|
@@ -650,16 +733,7 @@ pub unsafe extern "C" fn castrum_session_seal(
|
|
|
650
733
|
|| crate::crypto::session::seal_core(id_b, data_b, exp_secs, sec_b),
|
|
651
734
|
None,
|
|
652
735
|
);
|
|
653
|
-
|
|
654
|
-
return std::ptr::null();
|
|
655
|
-
};
|
|
656
|
-
super::util::cstring_return(tok.len(), move |buf| {
|
|
657
|
-
if buf.len() < tok.len() {
|
|
658
|
-
return None;
|
|
659
|
-
}
|
|
660
|
-
buf[..tok.len()].copy_from_slice(&tok);
|
|
661
|
-
Some(tok.len())
|
|
662
|
-
})
|
|
736
|
+
sealed_to_cstr(sealed)
|
|
663
737
|
}
|
|
664
738
|
|
|
665
739
|
/// Open a sealed session token: verify the HMAC and extract the envelope in
|
|
@@ -684,25 +758,72 @@ pub unsafe extern "C" fn castrum_session_open(
|
|
|
684
758
|
let tok = std::ffi::CStr::from_ptr(token).to_bytes();
|
|
685
759
|
let sec = std::ffi::CStr::from_ptr(secret).to_bytes();
|
|
686
760
|
let opened = panic_guard(|| crate::crypto::session::open_core(tok, sec), None);
|
|
687
|
-
|
|
761
|
+
pack_open_result(opened, out, out_cap)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/// Byte-arg sibling of `castrum_session_seal`: same core, same wire format, but
|
|
765
|
+
/// every input crosses as `(ptr, len)` instead of a `cstring` ARG.
|
|
766
|
+
///
|
|
767
|
+
/// Per `docs/FFI_BUN_GUIDE.md` §6.1 there are two reasons, and only the second
|
|
768
|
+
/// cannot be worked around:
|
|
769
|
+
/// 1. a `cstring` ARG costs the engine a transcode into a call-scoped
|
|
770
|
+
/// NUL-terminated buffer (~80ns per arg through the public surface vs ~37ns
|
|
771
|
+
/// for the byte path);
|
|
772
|
+
/// 2. an embedded `U+0000` SILENTLY TRUNCATES the value before Rust sees it.
|
|
773
|
+
/// `id` and `data_json` are user-derived (the id is escaped by
|
|
774
|
+
/// `seal_core`, which only makes sense if it may contain arbitrary bytes),
|
|
775
|
+
/// so a session id carrying a NUL would be sealed under its truncated form.
|
|
776
|
+
/// This form carries the exact length and cannot be truncated.
|
|
777
|
+
///
|
|
778
|
+
/// # Safety
|
|
779
|
+
/// `id`/`data_json`/`secret` must be valid for reads of their stated lengths;
|
|
780
|
+
/// null pointers are rejected (→ null).
|
|
781
|
+
#[no_mangle]
|
|
782
|
+
pub unsafe extern "C" fn castrum_session_seal_bytes(
|
|
783
|
+
id: *const u8,
|
|
784
|
+
id_len: usize,
|
|
785
|
+
data_json: *const u8,
|
|
786
|
+
data_len: usize,
|
|
787
|
+
exp_secs: i64,
|
|
788
|
+
secret: *const u8,
|
|
789
|
+
secret_len: usize,
|
|
790
|
+
) -> *const std::os::raw::c_char {
|
|
791
|
+
if id.is_null() || data_json.is_null() || secret.is_null() {
|
|
792
|
+
return std::ptr::null();
|
|
793
|
+
}
|
|
794
|
+
let id_b = slice::from_raw_parts(id, id_len);
|
|
795
|
+
let data_b = slice::from_raw_parts(data_json, data_len);
|
|
796
|
+
let sec_b = slice::from_raw_parts(secret, secret_len);
|
|
797
|
+
let sealed = super::util::panic_guard(
|
|
798
|
+
|| crate::crypto::session::seal_core(id_b, data_b, exp_secs, sec_b),
|
|
799
|
+
None,
|
|
800
|
+
);
|
|
801
|
+
sealed_to_cstr(sealed)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/// Byte-arg sibling of `castrum_session_open`: `token`/`secret` cross as
|
|
805
|
+
/// `(ptr, len)`; the output layout and the needed-size convention are identical
|
|
806
|
+
/// to `castrum_session_open` (`0` = bad signature / malformed). Saves the
|
|
807
|
+
/// per-arg transcode and, unlike the `cstring` form, cannot be truncated by an
|
|
808
|
+
/// embedded `U+0000` in the token.
|
|
809
|
+
///
|
|
810
|
+
/// # Safety
|
|
811
|
+
/// `token`/`secret` must be valid for reads of their stated lengths; `out` must
|
|
812
|
+
/// be valid for writes up to `out_cap`. Null pointers → 0.
|
|
813
|
+
#[no_mangle]
|
|
814
|
+
pub unsafe extern "C" fn castrum_session_open_bytes(
|
|
815
|
+
token: *const u8,
|
|
816
|
+
token_len: usize,
|
|
817
|
+
secret: *const u8,
|
|
818
|
+
secret_len: usize,
|
|
819
|
+
out: *mut u8,
|
|
820
|
+
out_cap: usize,
|
|
821
|
+
) -> usize {
|
|
822
|
+
if token.is_null() || secret.is_null() || out.is_null() {
|
|
688
823
|
return 0;
|
|
689
|
-
};
|
|
690
|
-
// Layout size: 1 + 8 + 4+id + 4+data
|
|
691
|
-
let need = 1 + 8 + 4 + id.len() + 4 + data.len();
|
|
692
|
-
if need > out_cap {
|
|
693
|
-
return need;
|
|
694
824
|
}
|
|
695
|
-
let
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
o[9..13].copy_from_slice(&id_len.to_le_bytes());
|
|
700
|
-
let mut p = 13usize;
|
|
701
|
-
o[p..p + id.len()].copy_from_slice(&id);
|
|
702
|
-
p += id.len();
|
|
703
|
-
let d_len = data.len() as u32;
|
|
704
|
-
o[p..p + 4].copy_from_slice(&d_len.to_le_bytes());
|
|
705
|
-
p += 4;
|
|
706
|
-
o[p..p + data.len()].copy_from_slice(&data);
|
|
707
|
-
need
|
|
825
|
+
let tok = slice::from_raw_parts(token, token_len);
|
|
826
|
+
let sec = slice::from_raw_parts(secret, secret_len);
|
|
827
|
+
let opened = panic_guard(|| crate::crypto::session::open_core(tok, sec), None);
|
|
828
|
+
pack_open_result(opened, out, out_cap)
|
|
708
829
|
}
|
package/rust/ffi/hashing.rs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
use std::ptr;
|
|
8
8
|
use std::slice;
|
|
9
9
|
|
|
10
|
+
use super::util::panic_guard;
|
|
11
|
+
|
|
10
12
|
/// CRC32 over `data[0..len]`. Returns the CRC-32 checksum.
|
|
11
13
|
///
|
|
12
14
|
/// # Safety
|
|
@@ -179,19 +181,22 @@ pub unsafe extern "C" fn castrum_json_sum_ids(
|
|
|
179
181
|
if data.is_null() || out.is_null() {
|
|
180
182
|
return 0;
|
|
181
183
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
184
|
+
panic_guard(
|
|
185
|
+
|| match crate::json::json_ops::json_sum_ids_bytes(slice::from_raw_parts(data, len)) {
|
|
186
|
+
Ok(sum) if out_len >= 9 => {
|
|
187
|
+
*out = 1;
|
|
188
|
+
ptr::copy_nonoverlapping(sum.to_le_bytes().as_ptr(), out.add(1), 8);
|
|
189
|
+
9
|
|
190
|
+
}
|
|
191
|
+
Ok(_) => 9, // too-small buffer → exact required size (growExact)
|
|
192
|
+
Err(_) if out_len >= 1 => {
|
|
193
|
+
*out = 0;
|
|
194
|
+
1
|
|
195
|
+
}
|
|
196
|
+
Err(_) => 1, // too-small buffer → exact required size
|
|
197
|
+
},
|
|
198
|
+
0,
|
|
199
|
+
)
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
/// Escape JS-RegExp metacharacters (`\ . * + ? ^ $ { } | ( ) [ ]`) in
|
|
@@ -212,13 +217,18 @@ pub unsafe extern "C" fn castrum_regex_escape(
|
|
|
212
217
|
if data.is_null() || out.is_null() {
|
|
213
218
|
return 0;
|
|
214
219
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
panic_guard(
|
|
221
|
+
|| {
|
|
222
|
+
let input = slice::from_raw_parts(data, len);
|
|
223
|
+
let needed = crate::util::text::regex_escape_len(input);
|
|
224
|
+
if needed > out_cap {
|
|
225
|
+
return needed;
|
|
226
|
+
}
|
|
227
|
+
crate::util::text::regex_escape_write(input, slice::from_raw_parts_mut(out, out_cap));
|
|
228
|
+
needed
|
|
229
|
+
},
|
|
230
|
+
0,
|
|
231
|
+
)
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
/// String-input / string-return sibling of [`castrum_regex_escape`]: the
|
|
@@ -269,17 +279,22 @@ pub unsafe extern "C" fn castrum_hex_validate_batch_str(
|
|
|
269
279
|
if ids.is_null() || out.is_null() {
|
|
270
280
|
return 0;
|
|
271
281
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
panic_guard(
|
|
283
|
+
|| {
|
|
284
|
+
let input = std::ffi::CStr::from_ptr(ids).to_bytes();
|
|
285
|
+
let needed = crate::util::validation::hex_batch_count(input);
|
|
286
|
+
if needed > out_cap {
|
|
287
|
+
return needed;
|
|
288
|
+
}
|
|
289
|
+
let mut buf = Vec::with_capacity(needed);
|
|
290
|
+
match crate::util::validation::hex_batch_valid_into(input, width as usize, &mut buf) {
|
|
291
|
+
Ok(()) if buf.len() <= out_cap => {
|
|
292
|
+
slice::from_raw_parts_mut(out, buf.len()).copy_from_slice(&buf);
|
|
293
|
+
buf.len()
|
|
294
|
+
}
|
|
295
|
+
_ => 0,
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
0,
|
|
299
|
+
)
|
|
285
300
|
}
|
package/rust/ffi/http.rs
CHANGED
|
@@ -446,7 +446,11 @@ pub unsafe extern "C" fn castrum_url_encode_query(
|
|
|
446
446
|
return None;
|
|
447
447
|
}
|
|
448
448
|
let count = u32::from_le_bytes([input[0], input[1], input[2], input[3]]) as usize;
|
|
449
|
-
|
|
449
|
+
// Borrow the keys/values as &str straight from the input slice
|
|
450
|
+
// instead of .to_string()-copying each into the map — the map is
|
|
451
|
+
// consumed inside this closure, so the borrows outlive it; the
|
|
452
|
+
// BTreeMap ordering (byte-lexicographic) is identical for &str.
|
|
453
|
+
let mut map = std::collections::BTreeMap::<&str, &str>::new();
|
|
450
454
|
let mut off = 4usize;
|
|
451
455
|
for _ in 0..count {
|
|
452
456
|
if off + 4 > input.len() {
|
|
@@ -462,9 +466,7 @@ pub unsafe extern "C" fn castrum_url_encode_query(
|
|
|
462
466
|
if off + klen > input.len() {
|
|
463
467
|
return None;
|
|
464
468
|
}
|
|
465
|
-
let key = std::str::from_utf8(&input[off..off + klen])
|
|
466
|
-
.ok()?
|
|
467
|
-
.to_string();
|
|
469
|
+
let key = std::str::from_utf8(&input[off..off + klen]).ok()?;
|
|
468
470
|
off += klen;
|
|
469
471
|
if off + 4 > input.len() {
|
|
470
472
|
return None;
|
|
@@ -479,9 +481,7 @@ pub unsafe extern "C" fn castrum_url_encode_query(
|
|
|
479
481
|
if off + vlen > input.len() {
|
|
480
482
|
return None;
|
|
481
483
|
}
|
|
482
|
-
let val = std::str::from_utf8(&input[off..off + vlen])
|
|
483
|
-
.ok()?
|
|
484
|
-
.to_string();
|
|
484
|
+
let val = std::str::from_utf8(&input[off..off + vlen]).ok()?;
|
|
485
485
|
off += vlen;
|
|
486
486
|
map.insert(key, val);
|
|
487
487
|
}
|
|
@@ -630,11 +630,18 @@ pub unsafe extern "C" fn castrum_http_parse_request_packed(
|
|
|
630
630
|
if data.is_null() || out.is_null() {
|
|
631
631
|
return 0;
|
|
632
632
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
633
|
+
// Attacker-controlled request bytes: a panic must become 0, not unwind
|
|
634
|
+
// through the C ABI.
|
|
635
|
+
panic_guard(
|
|
636
|
+
|| {
|
|
637
|
+
crate::http::http_parser::http_parse_request_packed_into_slice(
|
|
638
|
+
slice::from_raw_parts(data, len),
|
|
639
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
640
|
+
)
|
|
641
|
+
.unwrap_or(0)
|
|
642
|
+
},
|
|
643
|
+
0,
|
|
636
644
|
)
|
|
637
|
-
.unwrap_or(0)
|
|
638
645
|
}
|
|
639
646
|
|
|
640
647
|
/// Query string parse → packed output into `out`.
|
|
@@ -657,14 +664,19 @@ pub unsafe extern "C" fn castrum_query_parse_packed(
|
|
|
657
664
|
if data.is_null() || out.is_null() {
|
|
658
665
|
return 0;
|
|
659
666
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
667
|
+
panic_guard(
|
|
668
|
+
|| {
|
|
669
|
+
let input = slice::from_raw_parts(data, len);
|
|
670
|
+
let output = slice::from_raw_parts_mut(out, out_cap);
|
|
671
|
+
match crate::http::query_parser::query_parse_packed_into_slice(input, output) {
|
|
672
|
+
Ok(w) => w,
|
|
673
|
+
// Too-small OR malformed — the size pass disambiguates: it parses
|
|
674
|
+
// the same input, so Ok(needed) ⇒ too-small, Err ⇒ malformed.
|
|
675
|
+
Err(_) => crate::http::query_parser::query_parse_packed_size(input).unwrap_or(0),
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
0,
|
|
679
|
+
)
|
|
668
680
|
}
|
|
669
681
|
|
|
670
682
|
/// Cookie header parse → packed output into `out`.
|
|
@@ -683,12 +695,17 @@ pub unsafe extern "C" fn castrum_cookie_parse_packed(
|
|
|
683
695
|
if data.is_null() || out.is_null() {
|
|
684
696
|
return 0;
|
|
685
697
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
698
|
+
panic_guard(
|
|
699
|
+
|| {
|
|
700
|
+
let input = slice::from_raw_parts(data, len);
|
|
701
|
+
let output = slice::from_raw_parts_mut(out, out_cap);
|
|
702
|
+
match crate::http::cookie_parser::cookie_parse_packed_into_slice(input, output) {
|
|
703
|
+
Ok(w) => w,
|
|
704
|
+
Err(_) => crate::http::cookie_parser::cookie_parse_packed_size(input).unwrap_or(0),
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
0,
|
|
708
|
+
)
|
|
692
709
|
}
|
|
693
710
|
|
|
694
711
|
/// Parse an `application/x-www-form-urlencoded` body into packed pairs — the
|
|
@@ -730,23 +747,32 @@ pub unsafe extern "C" fn castrum_multipart_parse_packed(
|
|
|
730
747
|
}
|
|
731
748
|
// Wrap in panic_guard: the parser allocates internally — a panic must not
|
|
732
749
|
// unwind through the C ABI (process crash); it becomes 0 instead.
|
|
733
|
-
|
|
750
|
+
// Direct write: needed-size first (no partial output), then serialize
|
|
751
|
+
// straight into the caller's buffer — no intermediate Vec, no final copy.
|
|
752
|
+
// Two-pass (parse → len check → write) keeps the needed-size convention
|
|
753
|
+
// without re-parsing: parts are borrowed slices, writable twice cheaply.
|
|
754
|
+
let parts = panic_guard(
|
|
734
755
|
|| {
|
|
735
|
-
|
|
756
|
+
crate::http::multipart::parse_multipart_limited(
|
|
736
757
|
slice::from_raw_parts(body, blen),
|
|
737
758
|
slice::from_raw_parts(boundary, boundary_len),
|
|
738
759
|
&Default::default(),
|
|
739
|
-
)
|
|
740
|
-
let mut buf = Vec::new();
|
|
741
|
-
crate::http::multipart::parts_to_packed(&parts, &mut buf);
|
|
742
|
-
buf
|
|
760
|
+
)
|
|
743
761
|
},
|
|
744
762
|
Vec::new(),
|
|
745
763
|
);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return
|
|
764
|
+
let needed = panic_guard(|| crate::http::multipart::parts_packed_len(&parts), 0);
|
|
765
|
+
if needed == 0 || needed > out_cap {
|
|
766
|
+
return needed;
|
|
749
767
|
}
|
|
750
|
-
|
|
751
|
-
|
|
768
|
+
let written = panic_guard(
|
|
769
|
+
|| {
|
|
770
|
+
crate::http::multipart::parts_to_packed_into(
|
|
771
|
+
&parts,
|
|
772
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
773
|
+
)
|
|
774
|
+
},
|
|
775
|
+
None,
|
|
776
|
+
);
|
|
777
|
+
written.unwrap_or(0)
|
|
752
778
|
}
|
package/rust/ffi/metrics.rs
CHANGED
|
@@ -19,8 +19,12 @@ pub(crate) const METRICS_DECLARE_ERR: u32 = u32::MAX;
|
|
|
19
19
|
/// Create an empty metrics registry → opaque handle (`0` = allocation panic).
|
|
20
20
|
#[no_mangle]
|
|
21
21
|
pub extern "C" fn castrum_metrics_create() -> usize {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// An allocation panic must become the null-handle sentinel, not unwind
|
|
23
|
+
// through `extern "C"`.
|
|
24
|
+
panic_guard(
|
|
25
|
+
|| Box::into_raw(Box::new(crate::metrics::MetricsRegistry::new())) as usize,
|
|
26
|
+
0,
|
|
27
|
+
)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/// Declare a counter family → series id (`METRICS_DECLARE_ERR` on error).
|