Merge security/p2.2-edge-fail-closed
ober
50119b1fb6b21da484b3d26d6883e973e3424960
--- a/edge.ss +++ b/edge.ss @@ -58,6 +58,12 @@ (def *port* (or (and (getenv "EDGE_PORT") (string->number (getenv "EDGE_PORT"))) 8080)) (def *workers* (or (and (getenv "EDGE_WORKERS") (string->number (getenv "EDGE_WORKERS"))) 4)) (def *secret* (or (getenv "EDGE_SECRET") "")) +;; P2.2 fix: empty *secret* must NOT silently accept all requests. +;; To run without HMAC for local development, set EDGE_ALLOW_UNSIGNED=1 +;; explicitly. Any other unset/empty value fails closed. +(def *allow-unsigned?* + (let ([v (getenv "EDGE_ALLOW_UNSIGNED")]) + (and v (or (string=? v "1") (string=? v "true") (string=? v "yes"))))) (def *db-path* (getenv "EDGE_DB_PATH")) ;; SQLite file path, or #f for in-memory only (def *tls-cert* (getenv "EDGE_TLS_CERT")) ;; PEM cert path, or #f to disable TLS (def *tls-key* (getenv "EDGE_TLS_KEY")) ;; PEM key path, or #f to disable TLS @@ -280,15 +286,23 @@ (string-set! out (+ (* i 2) 1) (string-ref "0123456789abcdef" lo)))))) (def (verify-signature sig body) - (or (string=? *secret* "") ;; skip when no secret configured - (and sig - (let ([expected (bytes->hex - (native-hmac-sha256 - (string->utf8 *secret*) - (string->utf8 body)))]) - (native-crypto-memcmp - (string->utf8 expected) - (string->utf8 sig)))))) + ;; P2.2 fix: fail-closed when no secret is configured. Permit unsigned + ;; requests ONLY when an operator has explicitly set EDGE_ALLOW_UNSIGNED=1 + ;; (development mode). An accidentally-unset EDGE_SECRET no longer + ;; defeats signature verification. + (cond + [(string=? *secret* "") + ;; No secret configured. Only honor unsigned requests in explicit dev mode. + *allow-unsigned?*] + [(not sig) #f] ;; secret configured but no signature provided → reject + [else + (let ([expected (bytes->hex + (native-hmac-sha256 + (string->utf8 *secret*) + (string->utf8 body)))]) + (native-crypto-memcmp + (string->utf8 expected) + (string->utf8 sig)))])) ;; ═══════════════════════════════════════════════════════════════ ;; Sandboxed User Filters (Phase 2.1) @@ -1004,7 +1018,11 @@ (displayln " ──────────────────────────────────────────────") (printf " port: ~a~n" *port*) (printf " workers: ~a (supervised, one-for-one)~n" *workers*) - (printf " hmac: ~a~n" (if (string=? *secret* "") "disabled" "enabled")) + (printf " hmac: ~a~n" + (cond + [(not (string=? *secret* "")) "enabled"] + [*allow-unsigned?* "DISABLED (EDGE_ALLOW_UNSIGNED=1 — dev mode, unsigned requests accepted)"] + [else "DISABLED — fail-closed (set EDGE_SECRET=... to enable HMAC, or EDGE_ALLOW_UNSIGNED=1 for dev)"])) (printf " retries: ~a (1s/2s/4s backoff → dead-letter)~n" *max-retries*) (printf " sqlite: ~a~n" (if *db-path* *db-path* "disabled")) (printf " tls: ~a~n" (if *tls-cert* (format ":~a" *tls-port*) "disabled"))