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}