updates
ober
5d3ca6b3ad8b5015b5fcaae57879ae9d18e5c048
--- a/jerboa-native-rs/Cargo.lock +++ b/jerboa-native-rs/Cargo.lock @@ -140,6 +140,32 @@ dependencies = [ ] [[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -178,6 +204,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -285,6 +317,15 @@ dependencies = [ ] [[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -327,6 +368,7 @@ name = "jerboa-native" version = "0.1.0" dependencies = [ "flate2", + "hkdf", "inotify", "libc", "postgres", @@ -338,8 +380,10 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "scrypt", + "sha2", "time", "webpki-roots 0.26.11", + "x25519-dalek", ] [[package]] @@ -751,6 +795,15 @@ dependencies = [ ] [[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] name = "rustls" version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -828,12 +881,19 @@ dependencies = [ ] [[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -1281,6 +1341,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] name = "yasna" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1314,3 +1386,17 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] --- a/jerboa-native-rs/Cargo.toml +++ b/jerboa-native-rs/Cargo.toml @@ -14,6 +14,9 @@ regex = "1" libc = "0.2" rusqlite = { version = "0.32", features = ["bundled"] } postgres = "0.19" +x25519-dalek = { version = "2", features = ["static_secrets"] } +hkdf = "0.12" +sha2 = "0.10" inotify = { version = "0.11", default-features = false } rcgen = { version = "0.13", features = ["ring", "pem"] } time = "0.3" --- a/jerboa-native-rs/src/lib.rs +++ b/jerboa-native-rs/src/lib.rs @@ -13,3 +13,5 @@ mod x509; mod antidebug; mod seccomp; mod integrity; +mod x25519; +mod process_ctl; new file mode 100644 --- /dev/null +++ b/jerboa-native-rs/src/process_ctl.rs @@ -0,0 +1,118 @@ +use crate::panic::{ffi_wrap, set_last_error}; + +/// Set the process name via prctl(PR_SET_NAME). +/// name: UTF-8 bytes, max 15 bytes (will be truncated). +#[no_mangle] +pub extern "C" fn jerboa_prctl_set_name(name: *const u8, name_len: usize) -> i32 { + ffi_wrap(|| { + if name.is_null() { + set_last_error("null name pointer".into()); + return -1; + } + #[cfg(target_os = "linux")] + { + let len = name_len.min(15); + let mut buf = [0u8; 16]; // 15 chars + null + unsafe { std::ptr::copy_nonoverlapping(name, buf.as_mut_ptr(), len) }; + buf[len] = 0; + let rc = unsafe { libc::prctl(libc::PR_SET_NAME, buf.as_ptr() as libc::c_ulong, 0, 0, 0) }; + if rc != 0 { + set_last_error("prctl PR_SET_NAME failed".into()); + return -1; + } + 0 + } + #[cfg(not(target_os = "linux"))] + { + let _ = name_len; + set_last_error("prctl not supported on this platform".into()); + -1 + } + }) +} + +/// Lock all current and future memory pages (prevent swapping). +#[no_mangle] +pub extern "C" fn jerboa_mlockall() -> i32 { + ffi_wrap(|| { + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + { + let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) }; + if rc != 0 { + set_last_error("mlockall failed (may need root)".into()); + return -1; + } + 0 + } + #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] + { + set_last_error("mlockall not supported".into()); + -1 + } + }) +} + +/// Probe whether a process exists using kill(pid, 0). +/// Returns: 1 = exists, 0 = does not exist, -1 = error. +#[no_mangle] +pub extern "C" fn jerboa_kill_probe(pid: u32) -> i32 { + ffi_wrap(|| { + let rc = unsafe { libc::kill(pid as libc::pid_t, 0) }; + if rc == 0 { + 1 // process exists + } else { + let err = unsafe { *libc::__errno_location() }; + if err == libc::ESRCH { + 0 // does not exist + } else if err == libc::EPERM { + 1 // exists but no permission (still alive) + } else { + set_last_error(format!("kill probe errno: {}", err)); + -1 + } + } + }) +} + +/// Read the path of /proc/self/exe. +/// output: buffer for the path +/// output_len: buffer size +/// actual_len: pointer to store actual path length +#[no_mangle] +pub extern "C" fn jerboa_proc_self_exe( + output: *mut u8, + output_len: usize, + actual_len: *mut usize, +) -> i32 { + ffi_wrap(|| { + #[cfg(target_os = "linux")] + { + if output.is_null() || actual_len.is_null() { + set_last_error("null pointer".into()); + return -1; + } + match std::fs::read_link("/proc/self/exe") { + Ok(path) => { + let bytes = path.to_string_lossy(); + let path_bytes = bytes.as_bytes(); + let copy_len = path_bytes.len().min(output_len); + unsafe { + std::ptr::copy_nonoverlapping(path_bytes.as_ptr(), output, copy_len); + *actual_len = path_bytes.len(); + } + 0 + } + Err(e) => { + set_last_error(format!("readlink failed: {}", e)); + -1 + } + } + } + #[cfg(not(target_os = "linux"))] + { + let _ = (output, output_len, actual_len); + set_last_error("proc_self_exe not supported".into()); + -1 + } + }) +} --- a/jerboa-native-rs/src/seccomp.rs +++ b/jerboa-native-rs/src/seccomp.rs @@ -22,6 +22,7 @@ const NR_PTRACE: u32 = 101; const NR_PROCESS_VM_READV: u32 = 310; const NR_PROCESS_VM_WRITEV: u32 = 311; const NR_PERSONALITY: u32 = 135; +const NR_MEMFD_CREATE: u32 = 319; // prevents code injection via memfd // seccomp_data field offsets (for BPF_ABS loads) const OFFSET_NR: u32 = 0; // offsetof(struct seccomp_data, nr) @@ -104,26 +105,29 @@ pub extern "C" fn jerboa_seccomp_lock() -> i32 { // 4: if nr == process_vm_readv, kill // 5: if nr == process_vm_writev, kill // 6: if nr == personality, kill - // 7: allow - // 8: kill + // 7: if nr == memfd_create, kill + // 8: allow + // 9: kill let filter = [ // 0: Load architecture bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_ARCH), - // 1: Verify x86_64 — if not, jump to kill (offset +6 -> instruction 8) - bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 6), + // 1: Verify x86_64 — if not, jump to kill (offset +7 -> instruction 9) + bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 7), // 2: Load syscall number bpf_stmt(BPF_LD | BPF_W | BPF_ABS, OFFSET_NR), - // 3: Check ptrace — if match, jump to kill (+4 -> instruction 8) - bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PTRACE, 4, 0), + // 3: Check ptrace — if match, jump to kill (+5 -> instruction 9) + bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PTRACE, 5, 0), // 4: Check process_vm_readv - bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_READV, 3, 0), + bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_READV, 4, 0), // 5: Check process_vm_writev - bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_WRITEV, 2, 0), + bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PROCESS_VM_WRITEV, 3, 0), // 6: Check personality - bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PERSONALITY, 1, 0), - // 7: Allow + bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_PERSONALITY, 2, 0), + // 7: Check memfd_create — prevents code injection via memfd + bpf_jump(BPF_JMP | BPF_JEQ | BPF_K, NR_MEMFD_CREATE, 1, 0), + // 8: Allow bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), - // 8: Kill process + // 9: Kill process bpf_stmt(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS), ]; new file mode 100644 --- /dev/null +++ b/jerboa-native-rs/src/x25519.rs @@ -0,0 +1,143 @@ +use crate::panic::{ffi_wrap, set_last_error}; +use hkdf::Hkdf; +use sha2::Sha256; +use x25519_dalek::{PublicKey, StaticSecret}; + +/// Generate an X25519 keypair using ring's CSPRNG. +/// private_out: 32-byte buffer for the private key +/// public_out: 32-byte buffer for the public key +#[no_mangle] +pub extern "C" fn jerboa_x25519_generate_keypair( + private_out: *mut u8, + public_out: *mut u8, +) -> i32 { + ffi_wrap(|| { + if private_out.is_null() || public_out.is_null() { + set_last_error("null output pointer".into()); + return -1; + } + // Use ring's CSPRNG to generate 32 random bytes for the private key + let rng = ring::rand::SystemRandom::new(); + let mut key_bytes = [0u8; 32]; + ring::rand::SecureRandom::fill(&rng, &mut key_bytes) + .map_err(|_| "CSPRNG failed") + .unwrap(); + let secret = StaticSecret::from(key_bytes); + let public = PublicKey::from(&secret); + unsafe { + std::ptr::copy_nonoverlapping(secret.as_bytes().as_ptr(), private_out, 32); + std::ptr::copy_nonoverlapping(public.as_bytes().as_ptr(), public_out, 32); + } + 0 + }) +} + +/// Compute public key from a private key. +/// private_key: 32-byte private key +/// public_out: 32-byte buffer for the public key +#[no_mangle] +pub extern "C" fn jerboa_x25519_public_from_private( + private_key: *const u8, + private_len: usize, + public_out: *mut u8, +) -> i32 { + ffi_wrap(|| { + if private_key.is_null() || public_out.is_null() || private_len != 32 { + set_last_error("invalid arguments".into()); + return -1; + } + let mut arr = [0u8; 32]; + unsafe { std::ptr::copy_nonoverlapping(private_key, arr.as_mut_ptr(), 32) }; + let secret = StaticSecret::from(arr); + let public = PublicKey::from(&secret); + unsafe { + std::ptr::copy_nonoverlapping(public.as_bytes().as_ptr(), public_out, 32); + } + 0 + }) +} + +/// Perform X25519 Diffie-Hellman key agreement. +/// our_private: 32-byte private key +/// their_public: 32-byte public key +/// shared_out: 32-byte buffer for the shared secret +#[no_mangle] +pub extern "C" fn jerboa_x25519_diffie_hellman( + our_private: *const u8, + priv_len: usize, + their_public: *const u8, + pub_len: usize, + shared_out: *mut u8, + shared_len: usize, +) -> i32 { + ffi_wrap(|| { + if our_private.is_null() || their_public.is_null() || shared_out.is_null() { + set_last_error("null pointer".into()); + return -1; + } + if priv_len != 32 || pub_len != 32 || shared_len < 32 { + set_last_error("invalid key sizes".into()); + return -1; + } + let mut priv_arr = [0u8; 32]; + let mut pub_arr = [0u8; 32]; + unsafe { + std::ptr::copy_nonoverlapping(our_private, priv_arr.as_mut_ptr(), 32); + std::ptr::copy_nonoverlapping(their_public, pub_arr.as_mut_ptr(), 32); + } + let secret = StaticSecret::from(priv_arr); + let public = PublicKey::from(pub_arr); + let shared = secret.diffie_hellman(&public); + unsafe { + std::ptr::copy_nonoverlapping(shared.as_bytes().as_ptr(), shared_out, 32); + } + 0 + }) +} + +/// HKDF-SHA256: extract + expand in one call. +/// ikm: input keying material +/// salt: salt bytes (can be NULL with salt_len=0 for no salt) +/// info: context/application info +/// output: buffer for derived key material +#[no_mangle] +pub extern "C" fn jerboa_hkdf_sha256( + ikm: *const u8, + ikm_len: usize, + salt: *const u8, + salt_len: usize, + info: *const u8, + info_len: usize, + output: *mut u8, + output_len: usize, +) -> i32 { + ffi_wrap(|| { + if ikm.is_null() || output.is_null() { + set_last_error("null pointer".into()); + return -1; + } + let ikm_slice = unsafe { std::slice::from_raw_parts(ikm, ikm_len) }; + let salt_opt = if salt.is_null() || salt_len == 0 { + None + } else { + Some(unsafe { std::slice::from_raw_parts(salt, salt_len) }) + }; + let info_slice = if info.is_null() || info_len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(info, info_len) } + }; + + let hkdf = Hkdf::<Sha256>::new(salt_opt, ikm_slice); + let mut out = vec![0u8; output_len]; + if hkdf.expand(info_slice, &mut out).is_err() { + set_last_error("HKDF expand failed".into()); + return -1; + } + unsafe { + std::ptr::copy_nonoverlapping(out.as_ptr(), output, output_len); + } + 0 + }) +} + --- a/lib/std/net/websocket.sls +++ b/lib/std/net/websocket.sls @@ -23,7 +23,9 @@ ;; Limits *ws-max-payload-size*) - (import (chezscheme)) + (import (chezscheme) + (std crypto native-rust) + (std text base64)) ;;; ========== Opcode constants ========== (define ws-opcode-continuation #x0) @@ -224,20 +226,16 @@ (define ws-rfc-test-key "dGhlIHNhbXBsZSBub25jZQ==") (define ws-rfc-test-accept "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") - ;; Compute the Sec-WebSocket-Accept from a key. - ;; This is a simplified/stub implementation that handles the RFC test vector - ;; exactly; for other keys it returns a placeholder. + ;; Compute the Sec-WebSocket-Accept from a client key. + ;; Per RFC 6455: SHA1(key + GUID) then base64-encode. (define (ws-handshake-accept key) - (if (string=? key ws-rfc-test-key) - ws-rfc-test-accept - ;; For real implementations, compute SHA1(key + ws-guid) then base64-encode. - ;; Here we return a placeholder for non-test keys. - (string-append "ACCEPT:" key))) - - ;; Generate a random-ish 16-byte WebSocket key (base64 of 16 bytes). - ;; For testing, returns a fixed key. + (let* ([combined (string->utf8 (string-append key ws-guid))] + [hash-bv (rust-sha1 combined)]) + (u8vector->base64-string hash-bv))) + + ;; Generate a random 16-byte WebSocket key (base64-encoded). (define (ws-handshake-key) - ws-rfc-test-key) + (u8vector->base64-string (rust-random-bytes 16))) ;; Validate that the server's accept matches the expected value. (define (ws-handshake-valid? key accept)