jsecmon: PSK challenge/response handshake runnable end-to-end

ober

f01a329358d7dbf43166cb86a17895317264087f

diff --git a/examples/crypto_psk_check.ss b/examples/crypto_psk_check.ss
index ea34005..3121219 100644
--- a/examples/crypto_psk_check.ss
+++ b/examples/crypto_psk_check.ss
@@ -59,6 +59,37 @@
 (def wrong-tk (derive-transport-key (make-bytevector 32 67)))   ;; 0x43 * 32
 (check "wrong key -> #f" (transport-decrypt wrong-tk sealed1) #f)
 
+(displayln "psk challenge proof (deterministic kernel, i64 timestamp):")
+(def auth    (derive-auth-key psk))
+;; challenge nonce = bytes 0..31, the reference vector tests/psk_vectors.rs pins;
+;; if the i64 timestamp marshalled wrong, this digest would diverge.
+(def nonce32 (hex-decode "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"))
+(check "compute-proof vector"
+       (hex-encode (compute-proof auth nonce32 1700000000))
+       "8f356b4ac6d808b5734493865260fba1d525aac1a8234b9198437e5fe7165108")
+(check "verify-proof good"
+       (verify-proof auth nonce32 1700000000 (compute-proof auth nonce32 1700000000)) #t)
+;; a different timestamp recomputes a different proof -> rejected
+(check "verify-proof wrong ts"
+       (verify-proof auth nonce32 1700000001 (compute-proof auth nonce32 1700000000)) #f)
+
+(displayln "psk challenge/response (orchestration):")
+(def ch   (generate-challenge))
+(def resp (respond-to-challenge auth ch))
+(def ts   (psk-challenge-timestamp ch))
+;; a proof from the right key verifies within the freshness window...
+(check "verify fresh"        (verify-response auth ch resp 30 ts)        #t)
+(check "verify within window" (verify-response auth ch resp 30 (+ ts 30)) #t)
+;; ...but a challenge older/newer than max-age (either direction) is stale
+(check "verify stale future" (verify-response auth ch resp 30 (+ ts 31)) #f)
+(check "verify stale past"   (verify-response auth ch resp 30 (- ts 31)) #f)
+;; the wrong auth key (different PSK) cannot satisfy the proof
+(def wrong-auth (derive-auth-key (make-bytevector 32 67)))      ;; 0x43 * 32
+(check "verify wrong key"    (verify-response wrong-auth ch resp 30 ts) #f)
+;; each challenge carries a fresh random nonce
+(check "challenge nonce randomized"
+       (equal? (psk-challenge-nonce ch) (psk-challenge-nonce (generate-challenge))) #f)
+
 (newline)
 (if (= fails 0)
     (displayln "OK: psk transport round-trips, randomizes, and rejects tampering.")
diff --git a/jsecmon/crypto-psk.ss b/jsecmon/crypto-psk.ss
index 155c7d2..3826a40 100644
--- a/jsecmon/crypto-psk.ss
+++ b/jsecmon/crypto-psk.ss
@@ -13,7 +13,13 @@
 ;;; a 96-bit random nonce per message satisfies that with overwhelming margin.
 
 (library (jsecmon crypto-psk)
-  (export transport-encrypt transport-decrypt)
+  (export transport-encrypt transport-decrypt
+          make-psk-challenge psk-challenge?
+          psk-challenge-nonce psk-challenge-timestamp
+          make-psk-response psk-response?
+          psk-response-proof psk-response-counter-nonce
+          generate-challenge respond-to-challenge verify-response
+          current-epoch-seconds)
   (import (except (chezscheme)
                   make-hash-table hash-table?
                   sort sort!
@@ -36,4 +42,43 @@
   ;; decrypt_transport: recover the plaintext from a sealed frame, or #f if the
   ;; tag fails to authenticate (tampering, or the wrong transport key).
   (define (transport-decrypt transport-key sealed)
-    (psk-transport-open transport-key sealed)))
+    (psk-transport-open transport-key sealed))
+
+  ;; --- challenge/response handshake (psk.rs generate/respond/verify) ---
+  ;;
+  ;; secmon's PskChallenge { nonce: [u8;32], timestamp: i64 } and
+  ;; PskResponse { proof: [u8;32], counter_nonce: [u8;32] }. The proof itself
+  ;; (compute_proof) and its constant-time check (verify_proof) are the verified
+  ;; typed kernels; here we supply the two effects they leave out — a random
+  ;; 32-byte nonce and the wall clock — and the freshness magnitude check.
+
+  (defstruct psk-challenge (nonce timestamp))
+  (defstruct psk-response (proof counter-nonce))
+
+  ;; epoch seconds, matching secmon's chrono::Utc::now().timestamp() (i64).
+  (define (current-epoch-seconds) (time-second (current-time)))
+
+  ;; generate_challenge: a fresh random 32-byte nonce stamped with the clock.
+  (define (generate-challenge)
+    (make-psk-challenge (random-bytes 32) (current-epoch-seconds)))
+
+  ;; respond_to_challenge: prove knowledge of the auth key over the challenge,
+  ;; and include our own counter-nonce so the peer can authenticate us in turn
+  ;; (mutual auth — the reverse leg is the symmetric generate/respond/verify).
+  (define (respond-to-challenge auth-key challenge)
+    (make-psk-response
+      (compute-proof auth-key
+                     (psk-challenge-nonce challenge)
+                     (psk-challenge-timestamp challenge))
+      (random-bytes 32)))
+
+  ;; verify_response: reject a stale challenge (|now - ts| > max-age), else
+  ;; constant-time-check the proof. `now` is passed in (epoch seconds) so the
+  ;; magnitude test stays pure and testable; the live caller hands it
+  ;; (current-epoch-seconds), exactly the clock read secmon does internally.
+  (define (verify-response auth-key challenge response max-age-secs now)
+    (and (<= (abs (- now (psk-challenge-timestamp challenge))) max-age-secs)
+         (verify-proof auth-key
+                       (psk-challenge-nonce challenge)
+                       (psk-challenge-timestamp challenge)
+                       (psk-response-proof response)))))
diff --git a/jsecmon/kernels.ss b/jsecmon/kernels.ss
index d2880c2..1d702c6 100644
--- a/jsecmon/kernels.ss
+++ b/jsecmon/kernels.ss
@@ -18,6 +18,7 @@
           ;; psk crypto primitives
           constant-time-eq? hex-encode hex-decode hex-string? psk-hex-32?
           derive-auth-key derive-transport-key
+          compute-proof verify-proof
           psk-transport-seal psk-transport-open
           ;; analytics
           host-risk-score
@@ -163,6 +164,28 @@
   (define (derive-transport-key psk)
     (call->bytes (lambda (pp pl) (%derive-transport psk (bytevector-length psk) pp pl))))
 
+  ;; compute_proof: SHA256(auth_key ‖ nonce ‖ timestamp.to_le_bytes() ‖
+  ;; b"secmon-challenge-proof"). The i64 epoch-seconds timestamp marshals as
+  ;; integer-64; the kernel encodes it little-endian internally.
+  (define %compute-proof
+    (fp "jt_jsecmon_typed_psk_compute_proof"
+        (u8* size_t u8* size_t integer-64 void* void*) unsigned-8))
+  (define (compute-proof auth-key nonce timestamp)
+    (call->bytes (lambda (pp pl)
+      (%compute-proof auth-key (bytevector-length auth-key)
+                      nonce (bytevector-length nonce)
+                      timestamp pp pl))))
+
+  ;; verify_response's security core: recompute the expected proof and compare
+  ;; it to the candidate in constant time (#t iff they match).
+  (define %verify-proof
+    (fp "jt_jsecmon_typed_psk_verify_proof"
+        (u8* size_t u8* size_t integer-64 u8* size_t) unsigned-8))
+  (define (verify-proof auth-key nonce timestamp candidate)
+    (truthy (%verify-proof auth-key (bytevector-length auth-key)
+                           nonce (bytevector-length nonce)
+                           timestamp candidate (bytevector-length candidate))))
+
   ;; encrypt_transport: AES-256-GCM under the transport key with the 12-byte
   ;; nonce prepended to the frame (nonce ‖ ciphertext‖tag).
   (define %transport-seal