code wiki / (root) / websocket_handshake.nx

websocket_handshake.nx source

↩ module page · 89 lines · 3425 B

1// websocket_handshake.nx -- compute Sec-WebSocket-Accept. 2// 3// RFC 6455 §4.2.2: the server's response to a WebSocket upgrade 4// request includes a Sec-WebSocket-Accept header whose value is 5// 6// base64( sha1( client_key || GUID ) ) 7// 8// where GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" is a 9// literal defined in the RFC (solely to make accidental 10// handshake-value forgery harder -- it doesn't add security). 11// client_key is the value of the Sec-WebSocket-Key request 12// header (a 16-byte random value, base64-encoded by the client, 13// so 24 ASCII chars including "=" padding). 14// 15// Composes sha1.nx + base64.nx. The concatenation buffer is at 16// most 24 + 36 = 60 bytes. 17// 18// Invariants: 19// WH1 Caller passes the raw client key bytes (typically 24 20// ASCII characters); we don't decode them -- the spec 21// says concatenate the client-sent base64 string AS IS 22// with the GUID string and hash that. 23// WH2 Output is 28 ASCII characters (base64 of 20 bytes). 24 25import "syscalls.nx" 26// 2026-08-01 -- REPOINTED FROM sha1.nx TO nx_sha1.nx (sev-8 remedy). 27// RFC 6455 sec 4.2.2: Sec-WebSocket-Accept = base64(SHA1(client-key || GUID)). sha1.nx computes WRONG 28// digests (0/9 vs RFC 2202, RED vs RFC 3174), so every handshake this module produced sent an accept 29// value the client would reject -- WebSocket connections could not have completed. 30// * I FOUND THIS FILE ONLY BY RE-ENUMERATING THE DEFECTIVE FILE'S IMPORTERS AFTER "FINISHING" THE FIX. 31// It appeared in NEITHER of my earlier ripgrep sweeps for sha1.nx importers -- the second confirmed 32// miss by that tool on this tree. A FIX IS NOT COMPLETE UNTIL A RE-ENUMERATION RETURNS ONLY DELIBERATE 33// IMPORTERS, and for blast-radius work a direct file scan beats the search tool: a missed import is a 34// shipped defect. 35import "nx_sha1.nx" 36import "base64.nx" 37 38// RFC 6455 GUID -- 36 bytes. 39func ws_guid() -> *u8 { 40 return "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 41} 42 43const WS_GUID_LEN: i64 = 36 44const WS_SHA1_BYTES: i64 = 20 45const WS_ACCEPT_LEN: i64 = 28 46 47// Compute the Sec-WebSocket-Accept value. `key` is the value of 48// the client's Sec-WebSocket-Key header (raw ASCII bytes). Out 49// must have room for 28 base64 chars. 50func ws_accept(key: *u8, key_len: i64, out: *u8) -> i64 { 51 // Build key || GUID in a scratch buffer. 52 let scratch: *u8 = sys_mmap(128) 53 var i: i64 = 0 54 while i < key_len { 55 scratch[i] = key[i] 56 i = i + 1 57 } 58 let guid: *u8 = ws_guid() 59 var j: i64 = 0 60 while j < WS_GUID_LEN { 61 scratch[key_len + j] = guid[j] 62 j = j + 1 63 } 64 65 // SHA-1 of the concatenation -> 20 bytes. 66 let hash: *u8 = sys_mmap(32) 67 sha1(scratch, key_len + WS_GUID_LEN, hash) 68 69 // Base64-encode the hash -> 28 chars. 70 b64_encode(hash, WS_SHA1_BYTES, out) 71 return WS_ACCEPT_LEN 72} 73 74// Compile-only smoke. RFC 6455 §1.3 worked example: 75// client key: "dGhlIHNhbXBsZSBub25jZQ==" 76// accept: "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" 77func main() -> i64 { 78 let out: *u8 = sys_mmap(64) 79 let n: i64 = ws_accept("dGhlIHNhbXBsZSBub25jZQ==", 24, out) 80 if n != WS_ACCEPT_LEN { return 1 } 81 82 let expected: *u8 = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" 83 var i: i64 = 0 84 while i < WS_ACCEPT_LEN { 85 if out[i] != expected[i] { return 10 + i } 86 i = i + 1 87 } 88 return 0 89}