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.
Files changed (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,106 @@
1
+ // rust/crypto/csrf.rs — CSRF tokens (32-byte random hex + HMAC-SHA256 signature).
2
+ //
3
+ // Token format: `<random-hex>.<hex(HMAC-SHA256(secret, random-hex))>`. The
4
+ // `CsrfProtector` higher-order instance precomputes the HMAC key once.
5
+
6
+ use aws_lc_rs::hmac;
7
+ use napi::bindgen_prelude::*;
8
+ use napi_derive::napi;
9
+
10
+ use crate::util::bytes::{hex_decode_32, hex_encode};
11
+
12
+ fn csrf_token_with_key(key: &hmac::Key) -> Result<Buffer> {
13
+ let mut rnd = [0u8; 32];
14
+ getrandom::fill(&mut rnd).map_err(|e| Error::from_reason(e.to_string()))?;
15
+ let mut rnd_hex = [0u8; 64];
16
+ hex_encode(&rnd, &mut rnd_hex);
17
+ let tag = hmac::sign(key, &rnd_hex);
18
+ let mut sig_hex = [0u8; 64];
19
+ hex_encode(tag.as_ref(), &mut sig_hex);
20
+ let mut out = Vec::with_capacity(64 + 1 + 64);
21
+ out.extend_from_slice(&rnd_hex);
22
+ out.push(b'.');
23
+ out.extend_from_slice(&sig_hex);
24
+ Ok(Buffer::from(out))
25
+ }
26
+
27
+ /// Constant-time verify; returns true when the signature matches.
28
+ pub fn csrf_verify_with_key(token: &[u8], key: &hmac::Key) -> bool {
29
+ let Some(dot) = token.iter().position(|&b| b == b'.') else {
30
+ return false;
31
+ };
32
+ let (rnd_hex, sig_hex) = token.split_at(dot);
33
+ let sig_hex = &sig_hex[1..];
34
+ if rnd_hex.len() != 64 || sig_hex.len() != 64 {
35
+ return false;
36
+ }
37
+ let Some(sig) = hex_decode_32(sig_hex) else {
38
+ return false;
39
+ };
40
+ hmac::verify(key, rnd_hex, &sig).is_ok()
41
+ }
42
+
43
+ #[napi]
44
+ pub fn csrf_token(secret: Uint8Array) -> Result<Buffer> {
45
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref());
46
+ csrf_token_with_key(&key)
47
+ }
48
+
49
+ #[napi]
50
+ pub fn csrf_verify(token: Uint8Array, secret: Uint8Array) -> bool {
51
+ let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref());
52
+ csrf_verify_with_key(token.as_ref(), &key)
53
+ }
54
+
55
+ /// Higher-order instance: HMAC key compiled once, reused across create/verify.
56
+ #[napi]
57
+ pub struct CsrfProtector {
58
+ key: hmac::Key,
59
+ }
60
+
61
+ #[napi]
62
+ impl CsrfProtector {
63
+ #[napi(constructor)]
64
+ pub fn new(secret: Uint8Array) -> Self {
65
+ Self {
66
+ key: hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref()),
67
+ }
68
+ }
69
+
70
+ #[napi]
71
+ pub fn create(&self) -> Result<Buffer> {
72
+ csrf_token_with_key(&self.key)
73
+ }
74
+
75
+ #[napi]
76
+ pub fn verify(&self, token: Uint8Array) -> bool {
77
+ csrf_verify_with_key(token.as_ref(), &self.key)
78
+ }
79
+ }
80
+
81
+ #[cfg(test)]
82
+ mod tests {
83
+ use super::*;
84
+
85
+ fn key(secret: &[u8]) -> hmac::Key {
86
+ hmac::Key::new(hmac::HMAC_SHA256, secret)
87
+ }
88
+
89
+ #[test]
90
+ fn token_roundtrip() {
91
+ let k = key(b"csrf-secret");
92
+ let token = csrf_token_with_key(&k).unwrap();
93
+ let t = token.as_ref();
94
+ assert_eq!(t.len(), 64 + 1 + 64);
95
+ assert_eq!(t[64], b'.');
96
+ assert!(csrf_verify_with_key(t, &k));
97
+ }
98
+
99
+ #[test]
100
+ fn rejects_wrong_secret_and_malformed() {
101
+ let token = csrf_token_with_key(&key(b"secret-a")).unwrap();
102
+ assert!(!csrf_verify_with_key(token.as_ref(), &key(b"secret-b")));
103
+ assert!(!csrf_verify_with_key(b"tooshort", &key(b"s")));
104
+ assert!(!csrf_verify_with_key(b"x.bad", &key(b"s")));
105
+ }
106
+ }
@@ -0,0 +1,215 @@
1
+ // rust/crypto/ed25519.rs — Ed25519 keypair generation + sign/verify.
2
+ //
3
+ // Backend-framework feature: EdDSA (Ed25519) signing for JWT auth. Uses
4
+ // aws-lc-rs (already a dependency for HMAC/argon2/etc.) — no new crates.
5
+ //
6
+ // Pure-Rust core (no napi types in signatures) stays unit-testable; only the
7
+ // entry points use napi types — same split as the other crypto modules.
8
+ //
9
+ // Key formats (the `.env` contract consumed by @ignex/native):
10
+ // - private key: PKCS#8 v1 DER (RFC 5208/8410, 48 bytes) — byte-identical
11
+ // to Node `crypto.generateKeyPairSync("ed25519").privateKey.export({type:
12
+ // "pkcs8", format: "der"})`.
13
+ // - public key: SPKI DER (RFC 5280/8410, 44 bytes) — byte-identical to
14
+ // Node `publicKey.export({type: "spki", format: "der"})`.
15
+ // The raw 32-byte seed/public are recovered at the core boundary so the pure
16
+ // core only deals in raw bytes (aws-lc-rs `Ed25519KeyPair`/`UnparsedPublicKey`
17
+ // consume raw Ed25519 keys).
18
+
19
+ use aws_lc_rs::rand::SystemRandom;
20
+ use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _, UnparsedPublicKey, ED25519};
21
+ use napi::bindgen_prelude::*;
22
+ use napi_derive::napi;
23
+
24
+ pub const ED25519_SIGNATURE_LEN: usize = 64;
25
+ pub const ED25519_SEED_LEN: usize = 32;
26
+ pub const ED25519_PUBLIC_KEY_LEN: usize = 32;
27
+ /// PKCS#8 v1 DER length for an Ed25519 private key (RFC 8410: 48 bytes).
28
+ pub const ED25519_PKCS8_V1_LEN: usize = 48;
29
+ /// SPKI DER length for an Ed25519 public key (RFC 8410: 44 bytes).
30
+ pub const ED25519_SPKI_LEN: usize = 44;
31
+
32
+ /// The fixed SPKI DER header (AlgorithmIdentifier + BIT STRING wrapper) that
33
+ /// precedes the raw 32-byte Ed25519 public key — matches Node/OpenSSL.
34
+ const SPKI_HEADER: [u8; 12] = [
35
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
36
+ ];
37
+
38
+ /// Build the RFC 8410 SPKI DER encoding of a raw 32-byte Ed25519 public key.
39
+ pub fn spki_der(raw_public_key: &[u8]) -> Vec<u8> {
40
+ let mut out = Vec::with_capacity(ED25519_SPKI_LEN);
41
+ out.extend_from_slice(&SPKI_HEADER);
42
+ out.extend_from_slice(raw_public_key);
43
+ out
44
+ }
45
+
46
+ /// Recover the raw 32-byte public key from SPKI DER (44 bytes) — also accepts
47
+ /// the raw 32-byte form so callers can skip the DER round trip.
48
+ pub fn raw_public_key(public_der: &[u8]) -> Option<&[u8]> {
49
+ match public_der.len() {
50
+ ED25519_PUBLIC_KEY_LEN => Some(public_der),
51
+ ED25519_SPKI_LEN if public_der.starts_with(&SPKI_HEADER) => {
52
+ Some(&public_der[SPKI_HEADER.len()..])
53
+ }
54
+ _ => None,
55
+ }
56
+ }
57
+
58
+ /// Generate an Ed25519 keypair → `(pkcs8_v1_private_der, spki_public_der)`.
59
+ ///
60
+ /// `None` only if the OS CSPRNG fails (effectively never).
61
+ pub fn generate_keypair() -> Option<(Vec<u8>, Vec<u8>)> {
62
+ let rng = SystemRandom::new();
63
+ let document = Ed25519KeyPair::generate_pkcs8v1(&rng).ok()?;
64
+ let private_der = document.as_ref().to_vec();
65
+ let pair = Ed25519KeyPair::from_pkcs8(&private_der).ok()?;
66
+ let public_der = spki_der(pair.public_key().as_ref());
67
+ Some((private_der, public_der))
68
+ }
69
+
70
+ /// Sign `msg` with an Ed25519 private key (PKCS#8 v1/v2 DER) → 64-byte sig.
71
+ ///
72
+ /// `None` on an unparseable/invalid private key.
73
+ pub fn sign(private_der: &[u8], msg: &[u8]) -> Option<Vec<u8>> {
74
+ let pair = Ed25519KeyPair::from_pkcs8(private_der).ok()?;
75
+ let sig = pair.try_sign(msg).ok()?;
76
+ Some(sig.as_ref().to_vec())
77
+ }
78
+
79
+ /// Verify a 64-byte Ed25519 signature over `msg` with an SPKI DER (or raw
80
+ /// 32-byte) public key. Constant-time signature verification (aws-lc-rs).
81
+ pub fn verify(public_der: &[u8], msg: &[u8], sig: &[u8]) -> bool {
82
+ let Some(raw) = raw_public_key(public_der) else {
83
+ return false;
84
+ };
85
+ UnparsedPublicKey::new(&ED25519, raw)
86
+ .verify(msg, sig)
87
+ .is_ok()
88
+ }
89
+
90
+ // ── NAPI entry points ──────────────────────────────────────────
91
+
92
+ /// An Ed25519 keypair serialized for `.env` storage — PKCS#8 v1 DER (private)
93
+ /// + SPKI DER (public), base64url encoded (RFC 7515 §2, no padding). The
94
+ /// fields are camelCased by napi-rs → `{ privateKey, publicKey }` on the JS
95
+ /// side.
96
+ #[napi(object)]
97
+ pub struct Ed25519Keypair {
98
+ /// PKCS#8 v1 DER private key, base64url.
99
+ pub private_key: String,
100
+ /// SPKI DER public key, base64url.
101
+ pub public_key: String,
102
+ }
103
+
104
+ /// Generate an Ed25519 keypair. Returns `{ privateKey, publicKey }` as
105
+ /// base64url DER strings, ready to persist in `.env`.
106
+ #[napi]
107
+ pub fn generate_ed25519_keypair() -> Result<Ed25519Keypair> {
108
+ let (private_key, public_key) = generate_keypair()
109
+ .ok_or_else(|| Error::from_reason("ed25519 keypair generation failed (CSPRNG error)"))?;
110
+ Ok(Ed25519Keypair {
111
+ private_key: String::from_utf8(crate::crypto::base64::base64url_encode_bytes(&private_key))
112
+ .expect("base64url output is ASCII"),
113
+ public_key: String::from_utf8(crate::crypto::base64::base64url_encode_bytes(&public_key))
114
+ .expect("base64url output is ASCII"),
115
+ })
116
+ }
117
+
118
+ /// Sign `msg` with an Ed25519 private key (PKCS#8 DER) → 64-byte signature.
119
+ #[napi]
120
+ pub fn ed25519_sign(msg: Uint8Array, private_key: Uint8Array) -> Result<Buffer> {
121
+ sign(private_key.as_ref(), msg.as_ref())
122
+ .map(Buffer::from)
123
+ .ok_or_else(|| Error::from_reason("ed25519 sign failed (invalid private key)"))
124
+ }
125
+
126
+ /// Verify a 64-byte Ed25519 signature over `msg` with an SPKI DER (or raw
127
+ /// 32-byte) public key.
128
+ #[napi]
129
+ pub fn ed25519_verify(msg: Uint8Array, signature: Uint8Array, public_key: Uint8Array) -> bool {
130
+ verify(public_key.as_ref(), msg.as_ref(), signature.as_ref())
131
+ }
132
+
133
+ #[cfg(test)]
134
+ mod tests {
135
+ use super::*;
136
+
137
+ /// RFC 8032 §7.1 test vector 1.
138
+ const TEST_SECRET: [u8; 32] = [
139
+ 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c,
140
+ 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae,
141
+ 0x7f, 0x60,
142
+ ];
143
+ const TEST_PUBLIC: [u8; 32] = [
144
+ 0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07,
145
+ 0x3a, 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07,
146
+ 0x51, 0x1a,
147
+ ];
148
+ const TEST_SIGNATURE: [u8; 64] = [
149
+ 0xe5, 0x56, 0x43, 0x00, 0xc3, 0x60, 0xac, 0x72, 0x90, 0x86, 0xe2, 0xcc, 0x80, 0x6e, 0x82,
150
+ 0x8a, 0x84, 0x87, 0x7f, 0x1e, 0xb8, 0xe5, 0xd9, 0x74, 0xd8, 0x73, 0xe0, 0x65, 0x22, 0x49,
151
+ 0x01, 0x55, 0x5f, 0xb8, 0x82, 0x15, 0x90, 0xa3, 0x3b, 0xac, 0xc6, 0x1e, 0x39, 0x70, 0x1c,
152
+ 0xf9, 0xb4, 0x6b, 0xd2, 0x5b, 0xf5, 0xf0, 0x59, 0x5b, 0xbe, 0x24, 0x65, 0x51, 0x41, 0x43,
153
+ 0x8e, 0x7a, 0x10, 0x0b,
154
+ ];
155
+ const TEST_MESSAGE: &[u8] = b"";
156
+
157
+ fn pkcs8_v1_from_seed(seed: &[u8]) -> Vec<u8> {
158
+ // RFC 8410 PKCS#8 v1 for Ed25519: 48 bytes (16-byte prefix + seed).
159
+ let mut der = Vec::with_capacity(ED25519_PKCS8_V1_LEN);
160
+ der.extend_from_slice(&[
161
+ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22,
162
+ 0x04, 0x20,
163
+ ]);
164
+ der.extend_from_slice(seed);
165
+ der
166
+ }
167
+
168
+ #[test]
169
+ fn spki_der_round_trips() {
170
+ let der = spki_der(&TEST_PUBLIC);
171
+ assert_eq!(der.len(), ED25519_SPKI_LEN);
172
+ assert_eq!(raw_public_key(&der), Some(&TEST_PUBLIC[..]));
173
+ assert_eq!(raw_public_key(&TEST_PUBLIC), Some(&TEST_PUBLIC[..]));
174
+ assert_eq!(raw_public_key(&der[..4]), None);
175
+ assert_eq!(raw_public_key(&[]), None);
176
+ }
177
+
178
+ #[test]
179
+ fn verify_rfc8032_vector_1() {
180
+ let spki = spki_der(&TEST_PUBLIC);
181
+ assert!(verify(&spki, TEST_MESSAGE, &TEST_SIGNATURE));
182
+ // Signature is over the empty message — a non-empty message fails.
183
+ assert!(!verify(&spki, b"x", &TEST_SIGNATURE));
184
+ // Tampered signature fails.
185
+ let mut bad = TEST_SIGNATURE;
186
+ bad[0] ^= 0x01;
187
+ assert!(!verify(&spki, TEST_MESSAGE, &bad));
188
+ }
189
+
190
+ #[test]
191
+ fn sign_matches_rfc8032_vector_1() {
192
+ let pkcs8 = pkcs8_v1_from_seed(&TEST_SECRET);
193
+ let sig = sign(&pkcs8, TEST_MESSAGE).expect("sign should succeed");
194
+ assert_eq!(sig, TEST_SIGNATURE);
195
+ assert_eq!(sig.len(), ED25519_SIGNATURE_LEN);
196
+ }
197
+
198
+ #[test]
199
+ fn generate_sign_verify_round_trip() {
200
+ let (private_der, public_der) = generate_keypair().expect("keypair generation");
201
+ assert_eq!(private_der.len(), ED25519_PKCS8_V1_LEN);
202
+ assert_eq!(public_der.len(), ED25519_SPKI_LEN);
203
+
204
+ let msg = b"integration plan for ignex + ninox + rbac";
205
+ let sig = sign(&private_der, msg).expect("sign");
206
+ assert!(verify(&public_der, msg, &sig));
207
+ assert!(!verify(&public_der, b"tampered", &sig));
208
+ }
209
+
210
+ #[test]
211
+ fn invalid_key_rejected() {
212
+ assert!(sign(&[0u8; 10], b"msg").is_none());
213
+ assert!(!verify(&[0u8; 10], b"msg", &[0u8; 64]));
214
+ }
215
+ }
@@ -0,0 +1,140 @@
1
+ // rust/crypto/hashing.rs — FNV-1a / XXH3 checksums.
2
+
3
+ use napi::bindgen_prelude::*;
4
+ use napi_derive::napi;
5
+
6
+ use xxhash_rust::xxh3::xxh3_64;
7
+
8
+ pub const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
9
+ pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
10
+
11
+ // ── FNV-1a (byte-by-byte, correct per spec) ──────────────────────
12
+
13
+ /// FNV-1a in the "continue" style. FNV-1a is inherently byte-serial,
14
+ /// so we process each byte individually (this is the correct algorithm).
15
+ #[inline]
16
+ pub fn fnv1a64_continue(mut hash: u64, input: &[u8]) -> u64 {
17
+ for &b in input {
18
+ hash ^= u64::from(b);
19
+ hash = hash.wrapping_mul(FNV_PRIME);
20
+ }
21
+ hash
22
+ }
23
+
24
+ /// FNV-1a on a full byte slice, with fast path for small inputs.
25
+ #[inline]
26
+ pub fn fnv1a64_bytes(input: &[u8]) -> u64 {
27
+ fnv1a64_continue(FNV_OFFSET_BASIS, input)
28
+ }
29
+
30
+ // ── XXHash wrappers ────────────────────────────────────────────────
31
+
32
+ #[inline]
33
+ pub fn fast_hash_bytes(input: &[u8]) -> u64 {
34
+ xxh3_64(input)
35
+ }
36
+
37
+ // ── Napi exports ───────────────────────────────────────────────────
38
+
39
+ /// CRC32 over a raw byte slice (pure core, shared by the napi boundary and
40
+ /// the pure-core unit tests).
41
+ #[inline]
42
+ pub fn crc32_bytes(input: &[u8]) -> u32 {
43
+ crc32fast::hash(input)
44
+ }
45
+
46
+ #[napi]
47
+ pub fn crc32(input: Uint8Array) -> u32 {
48
+ crc32_bytes(input.as_ref())
49
+ }
50
+
51
+ #[napi(js_name = "fnv1a64")]
52
+ pub fn fnv1a64(input: Uint8Array) -> u64 {
53
+ fnv1a64_bytes(input.as_ref())
54
+ }
55
+
56
+ /// XXH3-64 over raw bytes. High-throughput non-cryptographic hash; the same
57
+ /// core the ingress IP-trust hasher uses. Exposed publicly so callers can
58
+ /// race it against `Bun.hash.xxHash3` — see docs/bun-builtins-decision-matrix.md.
59
+ #[napi]
60
+ pub fn xxh3(input: Uint8Array) -> u64 {
61
+ fast_hash_bytes(input.as_ref())
62
+ }
63
+
64
+ /// CRC32 batch — delegates to the shared packed-u32 writer (same wire format
65
+ /// as the `_into` variant; validation + parallelism live in
66
+ /// `util::packed::write_u32_batch_into`, single source of truth).
67
+ #[napi]
68
+ pub fn crc32_batch_packed(input: Uint8Array) -> Result<Buffer> {
69
+ let data = input.as_ref();
70
+ let count = crate::util::packed::PackedIter::new(data)?.len();
71
+ let mut out = vec![0u8; 4 + count.saturating_mul(4)];
72
+ let written = crate::util::write_u32_batch_into(data, &mut out, crc32_bytes)?;
73
+ out.truncate(written);
74
+ Ok(Buffer::from(out))
75
+ }
76
+
77
+ /// Reusable-output CRC32 batch: writes `[u32 count][u32…]` into `output` and
78
+ /// returns bytes written. Wire format is byte-identical to
79
+ /// [`crc32_batch_packed`]; the JS loader uses this with a pooled buffer.
80
+ #[napi]
81
+ pub fn crc32_batch_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
82
+ crate::util::run_packed_into(&input, &mut output, |data, out| {
83
+ crate::util::write_u32_batch_into(data, out, crc32_bytes)
84
+ })
85
+ }
86
+
87
+ #[cfg(test)]
88
+ mod tests {
89
+ use super::*;
90
+
91
+ #[test]
92
+ fn fnv1a_known_vectors() {
93
+ // Standard FNV-1a 64-bit test vectors.
94
+ assert_eq!(fnv1a64_bytes(b""), 0xcbf2_9ce4_8422_2325);
95
+ assert_eq!(fnv1a64_bytes(b"a"), 0xaf63_dc4c_8601_ec8c);
96
+ assert_eq!(fnv1a64_bytes(b"foobar"), 0x8594_4171_f739_67e8);
97
+ // Continue-style matches one-shot.
98
+ let h = fnv1a64_continue(fnv1a64_bytes(b"foo"), b"bar");
99
+ assert_eq!(h, fnv1a64_bytes(b"foobar"));
100
+ }
101
+
102
+ #[test]
103
+ fn crc32_known_vector() {
104
+ assert_eq!(crc32fast::hash(b""), 0);
105
+ // Standard CRC-32 check value.
106
+ assert_eq!(crc32fast::hash(b"123456789"), 0xcbf4_3926);
107
+ // The napi wrapper agrees.
108
+ assert_eq!(crc32(Uint8Array::new(b"123456789".to_vec())), 0xcbf4_3926);
109
+ }
110
+
111
+ #[test]
112
+ fn fnv1a_napi_agrees_with_core() {
113
+ assert_eq!(
114
+ fnv1a64(Uint8Array::new(b"foobar".to_vec())),
115
+ fnv1a64_bytes(b"foobar")
116
+ );
117
+ }
118
+
119
+ #[test]
120
+ fn xxh3_stable_and_distinct() {
121
+ let empty = fast_hash_bytes(b"");
122
+ assert_eq!(empty, fast_hash_bytes(b""));
123
+ assert_ne!(empty, fast_hash_bytes(b"a"));
124
+ assert_eq!(fast_hash_bytes(b"castrum"), fast_hash_bytes(b"castrum"));
125
+ }
126
+
127
+ #[test]
128
+ fn xxh3_napi_agrees_with_core() {
129
+ let input = b"castrum xxh3 known-vector check 0123456789";
130
+ assert_eq!(
131
+ xxh3(Uint8Array::new(input.to_vec())),
132
+ fast_hash_bytes(input)
133
+ );
134
+ // Deterministic across calls on the same bytes.
135
+ assert_eq!(
136
+ xxh3(Uint8Array::new(input.to_vec())),
137
+ xxh3(Uint8Array::new(input.to_vec()))
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,88 @@
1
+ // rust/crypto/hmac_sha256.rs — HMAC-SHA256 sign/verify (scalar + HmacSigner instance).
2
+
3
+ use crate::util::bytes::{hex_decode_32, hex_encode_32, trim_ascii_whitespace};
4
+ use aws_lc_rs::hmac;
5
+ use napi::bindgen_prelude::*;
6
+ use napi_derive::napi;
7
+
8
+ #[napi]
9
+ pub fn hmac_sha256(key: Uint8Array, data: Buffer) -> Buffer {
10
+ let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
11
+ let tag = hmac::sign(&key, data.as_ref());
12
+
13
+ let mut out = [0u8; 64];
14
+ hex_encode_32(tag.as_ref(), &mut out);
15
+
16
+ Buffer::from(out.to_vec())
17
+ }
18
+
19
+ #[napi]
20
+ pub fn hmac_sha256_verify(key: Uint8Array, data: Buffer, sig: Buffer) -> bool {
21
+ let sig = trim_ascii_whitespace(sig.as_ref());
22
+
23
+ let Some(sig_bytes) = hex_decode_32(sig) else {
24
+ return false;
25
+ };
26
+
27
+ let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
28
+ hmac::verify(&key, data.as_ref(), &sig_bytes).is_ok()
29
+ }
30
+
31
+ #[napi]
32
+ pub struct HmacSigner {
33
+ key: hmac::Key,
34
+ }
35
+
36
+ #[napi]
37
+ impl HmacSigner {
38
+ #[napi(constructor)]
39
+ pub fn new(key: Uint8Array) -> Self {
40
+ Self {
41
+ key: hmac::Key::new(hmac::HMAC_SHA256, key.as_ref()),
42
+ }
43
+ }
44
+
45
+ #[napi]
46
+ pub fn sign(&self, data: Buffer) -> Buffer {
47
+ let tag = hmac::sign(&self.key, data.as_ref());
48
+
49
+ let mut out = [0u8; 64];
50
+ hex_encode_32(tag.as_ref(), &mut out);
51
+
52
+ Buffer::from(out.to_vec())
53
+ }
54
+
55
+ #[napi]
56
+ pub fn verify(&self, data: Buffer, sig: Buffer) -> bool {
57
+ let sig = trim_ascii_whitespace(sig.as_ref());
58
+
59
+ let Some(sig_bytes) = hex_decode_32(sig) else {
60
+ return false;
61
+ };
62
+
63
+ hmac::verify(&self.key, data.as_ref(), &sig_bytes).is_ok()
64
+ }
65
+ }
66
+
67
+ #[cfg(test)]
68
+ mod tests {
69
+ #[test]
70
+ fn hmac_sign_then_verify_roundtrip() {
71
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, b"secret-key");
72
+ let data = b"hello world";
73
+ let tag = aws_lc_rs::hmac::sign(&key, data);
74
+
75
+ let mut hex = [0u8; 64];
76
+ crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
77
+ let decoded = crate::util::bytes::hex_decode_32(&hex).expect("valid hex decodes");
78
+
79
+ assert!(aws_lc_rs::hmac::verify(&key, data, &decoded).is_ok());
80
+ }
81
+
82
+ #[test]
83
+ fn hex_decode_32_rejects_bad_length_and_digits() {
84
+ assert!(crate::util::bytes::hex_decode_32(b"abc").is_none());
85
+ assert!(crate::util::bytes::hex_decode_32(&[b'0'; 64]).is_some());
86
+ assert!(crate::util::bytes::hex_decode_32(&[b'g'; 64]).is_none());
87
+ }
88
+ }