llvmir: add the pure psk-hex kernels to the native LLVM build
ober
51f920cfe40f0ab5d7265bbb79dc71fd7358645e
--- a/Makefile +++ b/Makefile @@ -633,7 +633,8 @@ agent: rust native-runtime # language. See $(JERBOA)/docs/llvmir-backend.md. LLVMIR_DIR ?= build/llvmir LLVMIR_PURE := typed/analytics.ss typed/strbytes.ss typed/triage.ss \ - typed/lolbin.ss typed/obfuscate.ss typed/dga.ss + typed/lolbin.ss typed/obfuscate.ss typed/dga.ss \ + typed/psk-hex.ss # LLVM tool discovery: PATH first, then the Homebrew llvm keg. LLVM_BIN ?= $(shell if command -v llvm-as >/dev/null 2>&1; then dirname "$$(command -v llvm-as)"; \ elif [ -x /opt/homebrew/opt/llvm/bin/llvm-as ]; then echo /opt/homebrew/opt/llvm/bin; \ --- a/llvmir/harness.c +++ b/llvmir/harness.c @@ -32,6 +32,11 @@ extern uint64_t jt_llvm_jsecmon_dga__score_label(Buf); extern uint64_t jt_llvm_jsecmon_typed_obfuscate__obf_key_str(uint64_t); extern Buf jt_llvm_jsecmon_typed_obfuscate__obfuscate_string(Buf); extern Buf jt_llvm_jsecmon_typed_obfuscate__deobfuscate_string(Buf); +extern _Bool jt_llvm_jsecmon_typed_psk_hex__constant_time_eq_p(Buf, Buf); +extern Buf jt_llvm_jsecmon_typed_psk_hex__hex_encode(Buf); +extern Buf jt_llvm_jsecmon_typed_psk_hex__hex_decode(Buf); +extern _Bool jt_llvm_jsecmon_typed_psk_hex__hex_string_p(Buf); +extern _Bool jt_llvm_jsecmon_typed_psk_hex__psk_hex_32_p(Buf); static int failures = 0; @@ -103,6 +108,34 @@ int main(void) { check_bool("obfuscate/deobfuscate round-trips \"/etc/shadow\"", roundtrip && changed, 1); } + /* psk-hex — pure constant-time compare + lowercase hex codec (no crypto) */ + { + unsigned char raw[4] = { 0xde, 0xad, 0xbe, 0xef }; + Buf rawb = { raw, 4 }; + Buf enc = jt_llvm_jsecmon_typed_psk_hex__hex_encode(rawb); + int enc_ok = (enc.len == 8) && (memcmp(enc.ptr, "deadbeef", 8) == 0); + check_bool("hex_encode([de ad be ef]) == \"deadbeef\"", enc_ok, 1); + + Buf dec = jt_llvm_jsecmon_typed_psk_hex__hex_decode(S("deadbeef")); + int dec_ok = (dec.len == 4) && (memcmp(dec.ptr, raw, 4) == 0); + check_bool("hex_decode(\"deadbeef\") == [de ad be ef]", dec_ok, 1); + } + { + unsigned char x[3] = {1,2,3}, y[3] = {1,2,3}, z[3] = {1,2,4}, w[2] = {1,2}; + Buf bx = {x,3}, by = {y,3}, bz = {z,3}, bw = {w,2}; + check_bool("constant_time_eq?(equal)", jt_llvm_jsecmon_typed_psk_hex__constant_time_eq_p(bx,by), 1); + check_bool("constant_time_eq?(differ)", jt_llvm_jsecmon_typed_psk_hex__constant_time_eq_p(bx,bz), 0); + check_bool("constant_time_eq?(len mismatch)", jt_llvm_jsecmon_typed_psk_hex__constant_time_eq_p(bx,bw), 0); + } + check_bool("hex_string?(\"deadbeef\")", jt_llvm_jsecmon_typed_psk_hex__hex_string_p(S("deadbeef")), 1); + check_bool("hex_string?(\"deadbee\") [odd]", jt_llvm_jsecmon_typed_psk_hex__hex_string_p(S("deadbee")), 0); + check_bool("hex_string?(\"zz\") [non-hex]", jt_llvm_jsecmon_typed_psk_hex__hex_string_p(S("zz")), 0); + check_bool("psk_hex_32?(64 hex digits)", + jt_llvm_jsecmon_typed_psk_hex__psk_hex_32_p( + S("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")), 1); + check_bool("psk_hex_32?(\"dead\")", + jt_llvm_jsecmon_typed_psk_hex__psk_hex_32_p(S("dead")), 0); + if (failures == 0) { printf("\njsecmon-llvmir: PASS — all kernels match the Rust backend\n"); return 0; new file mode 100644 --- /dev/null +++ b/typed/psk-hex.ss @@ -0,0 +1,93 @@ +;;; jsecmon — PSK pure kernels (constant-time compare + lowercase hex codec). +;;; +;;; This is the PURE, crypto-free half of (jsecmon typed psk), carved into its +;;; own module so it compiles straight to native code through the Typed Jerboa +;;; -> LLVM IR backend (no Rust crate, no RustCrypto). `(jsecmon typed psk)` +;;; stays the source of truth for the Rust path and its callers; the bodies +;;; here mirror that module's pure functions byte-for-byte (the Typed Jerboa +;;; checker forbids re-exporting imported names, so the pure kernels are +;;; defined here rather than imported). The crypto half (HKDF/SHA256/AES-GCM) +;;; stays on the vetted RustCrypto path in psk.ss and is intentionally absent. +;;; +;;; Verified by jerboa-secmon's `make llvmir-bin`: every kernel below is +;;; asserted against the Rust backend's output. + +(typed-library (jsecmon typed psk-hex) + (export constant-time-eq? hex-encode hex-decode hex-string? psk-hex-32?) + + ;; --- 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))))))) + + ;; --- hex decoding + length validation (from_hex's hex::decode + check) --- + + ;; value of a hex-digit byte as 0..15; 16 (out of range) signals "not hex". + ;; Accepts 0-9 (48..57), a-f (97..102), A-F (65..70). + (def (hex-val (c : Nat)) : Nat + (if (and (>= c 48) (<= c 57)) (- c 48) + (if (and (>= c 97) (<= c 102)) (+ (- c 97) 10) + (if (and (>= c 65) (<= c 70)) (+ (- c 65) 10) + 16)))) + + (def (hex-digit? (c : Nat)) : Bool + (< (hex-val c) 16)) + + ;; #t iff every byte in [i,n) is a hex digit. + (def (all-hex-from (bs : Bytes) (i : Nat) (n : Nat)) : Bool + (if (>= i n) + #t + (and (hex-digit? (bytevector-u8-ref bs i)) + (all-hex-from bs (+ i 1) n)))) + + ;; str is decodable hex: an even number of digits, all hex. Empty string ok. + (def (hex-string? (s : String)) : Bool + (let ((bs (string->utf8 s))) + (let ((n (bytevector-length bs))) + (and (= n (* 2 (/ n 2))) + (all-hex-from bs 0 n))))) + + ;; secmon from_hex's full precondition for a PSK: exactly 64 hex digits + ;; (32 bytes). Collapses its two error arms (invalid hex / wrong byte count) + ;; into the single accept/reject the caller acts on. + (def (psk-hex-32? (s : String)) : Bool + (and (= (string-length s) 64) (hex-string? s))) + + ;; decode a hex string to bytes: output byte j is (digit 2j << 4) | digit + ;; 2j+1. Inverse of hex-encode. Assumes the caller checked hex-string?; a + ;; non-hex digit contributes its 16 sentinel, so only validated input + ;; round-trips faithfully. + (def (hex-decode (s : String)) : Bytes + (let ((bs (string->utf8 s))) + (bytes-build (/ (bytevector-length bs) 2) + (j (+ (* 16 (hex-val (bytevector-u8-ref bs (* 2 j)))) + (hex-val (bytevector-u8-ref bs (+ (* 2 j) 1)))))))))