code wiki / (root) / csrf_token.nx

csrf_token.nx source

↩ module page · 175 lines · 5850 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 29import "syscalls.nx" 30import "rand.nx" 31import "nx_hmac.nx" // was hmac.nx -- CODE-IDENTICAL twin (49/49 stmts) on the LEGACY syscalls.nx+sha256.nx family. 32// Two files defining hmac_sha256 + main, with the expander deduping BY PATH NOT BY SYMBOL, made 33// every legacy importer a duplicate-symbol landmine for the nx_ family (debt 1785524913). 34import "base64.nx" 35import "ct.nx" 36 37const CSRF_ERR_FORMAT: i64 = -1 38const CSRF_ERR_MAC: i64 = -2 39const CSRF_RAND_BYTES: i64 = 32 40const CSRF_MAC_BYTES: i64 = 16 41 42// Encode raw bytes as base64url without padding into out; returns 43// bytes written. 44func csrf_b64url_encode(data: *u8, n: i64, out: *u8) -> i64 { 45 let scratch: *u8 = sys_mmap(n * 2 + 16) 46 let b_len: i64 = b64_encode(data, n, scratch) 47 var stripped: i64 = b_len 48 while stripped > 0 { 49 if scratch[stripped - 1] != 0x3D { break } 50 stripped = stripped - 1 51 } 52 var i: i64 = 0 53 while i < stripped { 54 var c: i64 = scratch[i] 55 if c == 0x2B { c = 0x2D } 56 if c == 0x2F { c = 0x5F } 57 out[i] = c 58 i = i + 1 59 } 60 return stripped 61} 62 63// Decode base64url (padding-tolerant). 64func csrf_b64url_decode(chars: *u8, n: i64, out: *u8) -> i64 { 65 let scratch: *u8 = sys_mmap(n + 16) 66 var i: i64 = 0 67 while i < n { 68 var c: i64 = chars[i] 69 if c == 0x2D { c = 0x2B } 70 if c == 0x5F { c = 0x2F } 71 scratch[i] = c 72 i = i + 1 73 } 74 var padded: i64 = n 75 while padded % 4 != 0 { 76 scratch[padded] = 0x3D 77 padded = padded + 1 78 } 79 return b64_decode(scratch, padded, out) 80} 81 82// Generate a CSRF token bound to the given session ID. Writes 83// to out and returns bytes written. 84func csrf_token_new(session_id: *u8, session_id_len: i64, 85 out: *u8, cap: i64) -> i64 { 86 let randb: *u8 = sys_mmap(64) 87 rand_bytes(randb, CSRF_RAND_BYTES) 88 89 // MAC over the random bytes. 90 let mac: *u8 = sys_mmap(64) 91 hmac_sha256(session_id, session_id_len, 92 randb, CSRF_RAND_BYTES, mac) 93 94 // Encode both halves URL-safe. 95 let r_enc_len: i64 = csrf_b64url_encode(randb, CSRF_RAND_BYTES, out) 96 if r_enc_len <= 0 { return CSRF_ERR_FORMAT } 97 if r_enc_len + 1 >= cap { return CSRF_ERR_FORMAT } 98 out[r_enc_len] = 0x2E // '.' 99 let m_enc_len: i64 = csrf_b64url_encode(mac, CSRF_MAC_BYTES, 100 out + r_enc_len + 1) 101 if m_enc_len <= 0 { return CSRF_ERR_FORMAT } 102 return r_enc_len + 1 + m_enc_len 103} 104 105// Verify a token against a session ID. Returns 0 on success, 106// negative on format error or MAC mismatch. 107func csrf_token_verify(token: *u8, token_len: i64, 108 session_id: *u8, session_id_len: i64) -> i64 { 109 // Find separator '.'. 110 var dot: i64 = -1 111 var i: i64 = 0 112 while i < token_len { 113 if token[i] == 0x2E { dot = i; break } 114 i = i + 1 115 } 116 if dot < 0 { return CSRF_ERR_FORMAT } 117 118 let r_enc_len: i64 = dot 119 let m_enc_off: i64 = dot + 1 120 let m_enc_len: i64 = token_len - m_enc_off 121 122 // Decode random half. 123 let randb: *u8 = sys_mmap(64) 124 let r_bytes: i64 = csrf_b64url_decode(token, r_enc_len, randb) 125 if r_bytes != CSRF_RAND_BYTES { return CSRF_ERR_FORMAT } 126 127 // Decode provided MAC. 128 let prov_mac: *u8 = sys_mmap(64) 129 let p_bytes: i64 = csrf_b64url_decode(token + m_enc_off, m_enc_len, 130 prov_mac) 131 if p_bytes != CSRF_MAC_BYTES { return CSRF_ERR_FORMAT } 132 133 // Recompute expected MAC. 134 let exp_mac: *u8 = sys_mmap(64) 135 hmac_sha256(session_id, session_id_len, 136 randb, CSRF_RAND_BYTES, exp_mac) 137 // Only compare first CSRF_MAC_BYTES of the 32-byte HMAC (the 138 // token truncation matches). 139 if ct_memcmp(prov_mac, exp_mac, CSRF_MAC_BYTES) != 0 { 140 return CSRF_ERR_MAC 141 } 142 return 0 143} 144 145// Compile-only smoke. 146func main() -> i64 { 147 let session_id: *u8 = "session_for_elder_0123456789" 148 let tok: *u8 = sys_mmap(256) 149 let n: i64 = csrf_token_new(session_id, 28, tok, 256) 150 if n <= 0 { return 1 } 151 152 // Contains exactly one '.' 153 var dots: i64 = 0 154 var i: i64 = 0 155 while i < n { 156 if tok[i] == 0x2E { dots = dots + 1 } 157 i = i + 1 158 } 159 if dots != 1 { return 2 } 160 161 // Verify round-trip. 162 if csrf_token_verify(tok, n, session_id, 28) != 0 { return 3 } 163 164 // Wrong session -> MAC mismatch. 165 if csrf_token_verify(tok, n, "other_session_0000000000000", 27) 166 != CSRF_ERR_MAC { 167 return 4 168 } 169 170 // Tamper last byte -> fail. 171 tok[n - 1] = tok[n - 1] ^ 1 172 let rc: i64 = csrf_token_verify(tok, n, session_id, 28) 173 if rc == 0 { return 5 } 174 return 0 175}