code wiki / (root) / nx_csrf_token.nx

nx_csrf_token.nx source

↩ module page · 179 lines · 5827 B

1// csrf_token.nx -- Cross-Site Request Forgery token generation. 2// 3// Standard defence against forged cross-origin form submits: 4// server issues a token tied to the user's session, embeds it in 5// forms, rejects submissions whose token doesn't HMAC-match the 6// session. Since cross-origin JS can't read the session cookie 7// or the issued token, attacker can't forge a valid request. 8// 9// OWASP recommended pattern: \"Signed Double-Submit Cookie\" -- 10// token = random + HMAC(session_id, random). Verification 11// splits, re-derives HMAC, constant-time compares. 12// 13// Composes rand.nx + hmac.nx + base64.nx + ct.nx. 14// 15// Token format (URL-safe, no padding): 16// base64url(random_32_bytes) . base64url(hmac[0..16]) 17// 18// 32 bytes random = 256 bits unpredictability; 128-bit truncated 19// HMAC = collision-resistant auth tag for this use. Total URI- 20// safe length ~65 chars. 21// 22// Invariants: 23// CT1 Token split at '.'; left = random, right = MAC. 24// CT2 Verification re-derives MAC from (session_id, random) 25// and ct_memcmp against provided MAC. 26// CT3 Session ID opaque to this module -- any byte-stable 27// session identifier works. 28 29// nx_safety_envelope: 30// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 31// sil_target: SIL1 32// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 33// verdict: NOT_YET_EVALUATED 34 35import "nx_syscalls.nx" 36import "nx_rand.nx" 37import "nx_hmac.nx" 38import "nx_base64.nx" 39import "nx_ct.nx" 40 41const CSRF_ERR_FORMAT: i64 = -1 42const CSRF_ERR_MAC: i64 = -2 43const CSRF_RAND_BYTES: i64 = 32 44const CSRF_MAC_BYTES: i64 = 16 45 46// Encode raw bytes as base64url without padding into out; returns 47// bytes written. 48func csrf_b64url_encode(data: *u8, n: i64, out: *u8) -> i64 { 49 let scratch: *u8 = sys_mmap(n * 2 + 16) 50 let b_len: i64 = b64_encode(data, n, scratch) 51 var stripped: i64 = b_len 52 while stripped > 0 { 53 if scratch[stripped - 1] != 0x3D { break } 54 stripped = stripped - 1 55 } 56 var i: i64 = 0 57 while i < stripped { 58 var c: i64 = scratch[i] 59 if c == 0x2B { c = 0x2D } 60 if c == 0x2F { c = 0x5F } 61 out[i] = c 62 i = i + 1 63 } 64 return stripped 65} 66 67// Decode base64url (padding-tolerant). 68func csrf_b64url_decode(chars: *u8, n: i64, out: *u8) -> i64 { 69 let scratch: *u8 = sys_mmap(n + 16) 70 var i: i64 = 0 71 while i < n { 72 var c: i64 = chars[i] 73 if c == 0x2D { c = 0x2B } 74 if c == 0x5F { c = 0x2F } 75 scratch[i] = c 76 i = i + 1 77 } 78 var padded: i64 = n 79 while padded % 4 != 0 { 80 scratch[padded] = 0x3D 81 padded = padded + 1 82 } 83 return b64_decode(scratch, padded, out) 84} 85 86// Generate a CSRF token bound to the given session ID. Writes 87// to out and returns bytes written. 88func csrf_token_new(session_id: *u8, session_id_len: i64, 89 out: *u8, cap: i64) -> i64 { 90 let randb: *u8 = sys_mmap(64) 91 rand_bytes(randb, CSRF_RAND_BYTES) 92 93 // MAC over the random bytes. 94 let mac: *u8 = sys_mmap(64) 95 hmac_sha256(session_id, session_id_len, 96 randb, CSRF_RAND_BYTES, mac) 97 98 // Encode both halves URL-safe. 99 let r_enc_len: i64 = csrf_b64url_encode(randb, CSRF_RAND_BYTES, out) 100 if r_enc_len <= 0 { return CSRF_ERR_FORMAT } 101 if r_enc_len + 1 >= cap { return CSRF_ERR_FORMAT } 102 out[r_enc_len] = 0x2E // '.' 103 let m_enc_len: i64 = csrf_b64url_encode(mac, CSRF_MAC_BYTES, 104 out + r_enc_len + 1) 105 if m_enc_len <= 0 { return CSRF_ERR_FORMAT } 106 return r_enc_len + 1 + m_enc_len 107} 108 109// Verify a token against a session ID. Returns 0 on success, 110// negative on format error or MAC mismatch. 111func csrf_token_verify(token: *u8, token_len: i64, 112 session_id: *u8, session_id_len: i64) -> i64 { 113 // Find separator '.'. 114 var dot: i64 = -1 115 var i: i64 = 0 116 while i < token_len { 117 if token[i] == 0x2E { dot = i; break } 118 i = i + 1 119 } 120 if dot < 0 { return CSRF_ERR_FORMAT } 121 122 let r_enc_len: i64 = dot 123 let m_enc_off: i64 = dot + 1 124 let m_enc_len: i64 = token_len - m_enc_off 125 126 // Decode random half. 127 let randb: *u8 = sys_mmap(64) 128 let r_bytes: i64 = csrf_b64url_decode(token, r_enc_len, randb) 129 if r_bytes != CSRF_RAND_BYTES { return CSRF_ERR_FORMAT } 130 131 // Decode provided MAC. 132 let prov_mac: *u8 = sys_mmap(64) 133 let p_bytes: i64 = csrf_b64url_decode(token + m_enc_off, m_enc_len, 134 prov_mac) 135 if p_bytes != CSRF_MAC_BYTES { return CSRF_ERR_FORMAT } 136 137 // Recompute expected MAC. 138 let exp_mac: *u8 = sys_mmap(64) 139 hmac_sha256(session_id, session_id_len, 140 randb, CSRF_RAND_BYTES, exp_mac) 141 // Only compare first CSRF_MAC_BYTES of the 32-byte HMAC (the 142 // token truncation matches). 143 if ct_memcmp(prov_mac, exp_mac, CSRF_MAC_BYTES) != 0 { 144 return CSRF_ERR_MAC 145 } 146 return 0 147} 148 149// Compile-only smoke. 150func main() -> i64 { 151 let session_id: *u8 = "session_for_elder_0123456789" 152 let tok: *u8 = sys_mmap(256) 153 let n: i64 = csrf_token_new(session_id, 28, tok, 256) 154 if n <= 0 { return 1 } 155 156 // Contains exactly one '.' 157 var dots: i64 = 0 158 var i: i64 = 0 159 while i < n { 160 if tok[i] == 0x2E { dots = dots + 1 } 161 i = i + 1 162 } 163 if dots != 1 { return 2 } 164 165 // Verify round-trip. 166 if csrf_token_verify(tok, n, session_id, 28) != 0 { return 3 } 167 168 // Wrong session -> MAC mismatch. 169 if csrf_token_verify(tok, n, "other_session_0000000000000", 27) 170 != CSRF_ERR_MAC { 171 return 4 172 } 173 174 // Tamper last byte -> fail. 175 tok[n - 1] = tok[n - 1] ^ 1 176 let rc: i64 = csrf_token_verify(tok, n, session_id, 28) 177 if rc == 0 { return 5 } 178 return 0 179}