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}