jsecmon: port psk constant-time-eq + hex-encode crypto prims
Jaime Fournier
e45de92a6eefb09614a2549c681c785373672842
--- a/README.md +++ b/README.md @@ -37,5 +37,8 @@ then crypto orchestration, then I/O / async / FFI (monitors, server, storage). | `dga::shannon_entropy` | `typed/dga.ss` | ✅ ported, vectors pass | | `dga::score_domain` | `typed/dga.ss` | ⏳ needs string ops (lowercase, split, ends_with, char classes) | | `lolbin`, `sigma`, `triage` | — | ⏳ pure logic, queued | -| `crypto::{psk,ecies}` | — | ⏳ FFI-delegated; orchestration only | +| `psk::constant_time_eq` | `typed/psk.ss` | ✅ ported, vectors pass | +| `psk::from_hex` (hex codec) | `typed/psk.ss` | ✅ hex-encode ported; decode + length check queued | +| `psk` HKDF/SHA256/AES-GCM | — | ⏳ FFI-delegated to vetted crates (not reimplemented) | +| `crypto::ecies` | — | ⏳ FFI-delegated; orchestration only | | monitors / server / storage / ebpf / dtrace | — | ⏳ I/O+async+FFI, last | new file mode 100644 --- /dev/null +++ b/tests/psk_vectors.rs @@ -0,0 +1,48 @@ +//! Parity vectors for the PSK crypto primitives ported from +//! secmon/src/crypto/psk.rs to Typed Jerboa. +//! +//! `constant_time_eq_p` is the Jerboa lowering of `psk.rs::constant_time_eq` +//! (the `_p` suffix is the mangling of the trailing `?`). `hex_encode` is the +//! lowercase codec used to round-trip a PSK; its byte-for-byte parity with the +//! `hex` crate is proven in jerboa/tests/fixtures/typed/rust-bytes-build.ss, so +//! here we pin the literal expected encodings. + +use jerboa_typed_generated::jsecmon_typed_psk::{constant_time_eq_p, hex_encode}; + +fn hex(data: &[u8]) -> String { + String::from_utf8(hex_encode(data.to_vec())).unwrap() +} + +#[test] +fn constant_time_eq_matches_secmon() { + // equal buffers compare equal + assert!(constant_time_eq_p(vec![1, 2, 3], vec![1, 2, 3])); + assert!(constant_time_eq_p(vec![], vec![])); + + // any differing byte is unequal (first, middle, last) + assert!(!constant_time_eq_p(vec![9, 2, 3], vec![1, 2, 3])); + assert!(!constant_time_eq_p(vec![1, 9, 3], vec![1, 2, 3])); + assert!(!constant_time_eq_p(vec![1, 2, 9], vec![1, 2, 3])); + + // mismatched lengths are unequal (secmon's early `return false`) + assert!(!constant_time_eq_p(vec![1, 2, 3], vec![1, 2])); + assert!(!constant_time_eq_p(vec![], vec![0])); + + // psk.rs proof scenario: a correct 32-byte proof verifies, a proof that + // differs in a single byte (wrong PSK) does not. + let proof = vec![0x42u8; 32]; + let mut wrong = proof.clone(); + wrong[17] ^= 0x01; + assert!(constant_time_eq_p(proof.clone(), proof.clone())); + assert!(!constant_time_eq_p(proof, wrong)); +} + +#[test] +fn hex_encode_matches_hex_crate() { + assert_eq!(hex(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef"); + assert_eq!(hex(&[0x42; 4]), "42424242"); // the psk.rs test PSK byte + assert_eq!(hex(&[0x00, 0x0f, 0xa0, 0xff]), "000fa0ff"); + assert_eq!(hex(&[]), ""); + // a full 32-byte PSK round-trips to 64 lowercase hex chars + assert_eq!(hex(&[0x42u8; 32]).len(), 64); +} new file mode 100644 --- /dev/null +++ b/typed/psk.ss @@ -0,0 +1,48 @@ +;;; jsecmon — PSK auth crypto primitives. +;;; +;;; Portable kernels ported from secmon/src/crypto/psk.rs to Typed Jerboa, +;;; compiled to Rust by the Jerboa typed backend. +;;; +;;; Scope: the pure, timing-sensitive logic that benefits from being written +;;; once in a checked language — the constant-time comparison used by +;;; `verify_response`, and the lowercase hex codec used by `from_hex`. The +;;; actual primitive crypto (HKDF-SHA256 key derivation, the SHA256 proof +;;; HMAC, AES-256-GCM transport) stays as calls into vetted Rust crates; those +;;; are not things to reimplement in any language. + +(typed-library (jsecmon typed psk) + (export constant-time-eq? hex-encode) + + ;; --- constant-time comparison (psk.rs::constant_time_eq) --- + + ;; OR every byte-xor together; the running time depends only on the length + ;; n, never on where the first mismatch occurs. + (def (ct-fold (a : Bytes) (b : Bytes) (i : Nat) (n : Nat) (acc : Nat)) : Nat + (if (>= i n) + acc + (ct-fold a b (+ i 1) n + (bitwise-ior acc (bitwise-xor (bytevector-u8-ref a i) + (bytevector-u8-ref b i)))))) + + ;; Timing-safe equality. Mismatched lengths are unequal without folding, + ;; matching secmon's early `return false` (the length is not itself secret). + (def (constant-time-eq? (a : Bytes) (b : Bytes)) : Bool + (if (= (bytevector-length a) (bytevector-length b)) + (= (ct-fold a b 0 (bytevector-length a) 0) 0) + #f)) + + ;; --- lowercase hex encoding (inverse of from_hex's hex::decode) --- + + ;; map a 0..15 nibble to its lowercase-hex ASCII byte: 0-9 -> '0'..'9' (48), + ;; 10-15 -> 'a'..'f' (87 + n). + (def (nibble-hex (x : Nat)) : Nat + (if (< x 10) (+ 48 x) (+ 87 x))) + + ;; two output bytes per input byte; output index j maps to input byte j/2, + ;; even j the high nibble, odd j the low nibble. + (def (hex-encode (data : Bytes)) : Bytes + (bytes-build (* 2 (bytevector-length data)) + (j (let ((b (bytevector-u8-ref data (bitwise-arithmetic-shift-right j 1)))) + (if (= (bitwise-and j 1) 0) + (nibble-hex (bitwise-and (bitwise-arithmetic-shift-right b 4) 15)) + (nibble-hex (bitwise-and b 15))))))))