jsecmon: PSK challenge/response handshake runnable end-to-end
ober
f01a329358d7dbf43166cb86a17895317264087f
--- 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.") --- 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))))) --- 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