code wiki / (root) / nx_digest_auth.nx

nx_digest_auth.nx source

↩ module page · 179 lines · 6674 B

1// digest_auth.nx -- HTTP Digest authentication response builder. 2// 3// RFC 7616 / RFC 2617. Stronger than Basic auth (password not 4// transmitted) but still obsolete compared to bearer tokens + 5// TLS. Interop reality: many IP cameras, routers, printers, 6// legacy enterprise apps, and Microsoft IIS sites use Digest. 7// 8// Challenge format (server -> client, 401 response): 9// WWW-Authenticate: Digest realm=\"Protected\", nonce=\"abc123\", 10// qop=\"auth\", algorithm=MD5 11// 12// Response format (client -> server): 13// Authorization: Digest username=\"alice\", realm=\"Protected\", 14// nonce=\"abc123\", uri=\"/admin\", qop=auth, 15// nc=00000001, cnonce=\"xyz789\", 16// response=\"<hash>\", algorithm=MD5 17// 18// Response hash (RFC 7616 §3.4.1 for qop=auth): 19// HA1 = MD5(username : realm : password) 20// HA2 = MD5(method : uri) 21// response = MD5(HA1 : nonce : nc : cnonce : qop : HA2) 22// 23// We compute the `response` value + emit the full Authorization 24// header value. Challenge-parsing is a separate job; this module 25// assumes caller extracted realm/nonce/qop from the WWW- 26// Authenticate header already. 27// 28// Composes md5.nx + hex.nx. 29// 30// Invariants: 31// D1 HA1/HA2/response all emitted as 32 lowercase hex chars. 32// D2 Currently supports qop=auth + algorithm=MD5 only. SHA-256 33// variant (RFC 7616) is a future extension composing sha256. 34// D3 cnonce + nc counter are caller-supplied -- the module 35// doesn't generate them (entropy + state are caller's 36// concern). 37 38// nx_safety_envelope: 39// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 40// sil_target: SIL1 41// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 42// verdict: NOT_YET_EVALUATED 43 44import "nx_syscalls.nx" 45import "nx_md5_canonical.nx" 46import "nx_hex_codec.nx" 47 48const DA_ERR_SHORT: i64 = -1 49 50// Write a MD5 hash of concat(a, \":\", b) into 32 hex chars. 51func da_md5_two(a: *u8, a_len: i64, 52 b: *u8, b_len: i64, 53 out: *u8) -> i64 { 54 let scratch_len: i64 = a_len + 1 + b_len 55 let scratch: *u8 = sys_mmap(scratch_len + 16) 56 var i: i64 = 0 57 while i < a_len { 58 scratch[i] = a[i] 59 i = i + 1 60 } 61 scratch[a_len] = 0x3A 62 i = 0 63 while i < b_len { 64 scratch[a_len + 1 + i] = b[i] 65 i = i + 1 66 } 67 let digest: *u8 = sys_mmap(32) 68 md5(scratch, scratch_len, digest) 69 hex_encode(digest, 16, out) 70 return 32 71} 72 73// Compute HA1 = MD5(user : realm : pass) as 32 hex chars. 74func digest_auth_ha1(user: *u8, user_len: i64, 75 realm: *u8, realm_len: i64, 76 pass: *u8, pass_len: i64, 77 out: *u8) -> i64 { 78 let total: i64 = user_len + 1 + realm_len + 1 + pass_len 79 let scratch: *u8 = sys_mmap(total + 16) 80 var p: i64 = 0 81 var i: i64 = 0 82 while i < user_len { scratch[p] = user[i]; p = p + 1; i = i + 1 } 83 scratch[p] = 0x3A; p = p + 1 84 i = 0 85 while i < realm_len { scratch[p] = realm[i]; p = p + 1; i = i + 1 } 86 scratch[p] = 0x3A; p = p + 1 87 i = 0 88 while i < pass_len { scratch[p] = pass[i]; p = p + 1; i = i + 1 } 89 let digest: *u8 = sys_mmap(32) 90 md5(scratch, total, digest) 91 hex_encode(digest, 16, out) 92 return 32 93} 94 95// Compute HA2 = MD5(method : uri) as 32 hex chars. 96func digest_auth_ha2(method: *u8, method_len: i64, 97 uri: *u8, uri_len: i64, 98 out: *u8) -> i64 { 99 return da_md5_two(method, method_len, uri, uri_len, out) 100} 101 102// Compute response = MD5(HA1 : nonce : nc : cnonce : qop : HA2) 103// as 32 hex chars. HA1 + HA2 are 32-byte hex-encoded inputs. 104func digest_auth_response(ha1: *u8, 105 nonce: *u8, nonce_len: i64, 106 nc: *u8, nc_len: i64, 107 cnonce: *u8, cnonce_len: i64, 108 qop: *u8, qop_len: i64, 109 ha2: *u8, 110 out: *u8) -> i64 { 111 // Total = 32 + 1 + nonce + 1 + nc + 1 + cnonce + 1 + qop + 1 + 32 112 let total: i64 = 69 + nonce_len + nc_len + cnonce_len + qop_len // 32(HA1)+32(HA2)+5 colons; was 37 (HA2's 32 bytes dropped) -> md5 hashed a truncated buffer 113 let scratch: *u8 = sys_mmap(total + 16) 114 var p: i64 = 0 115 var i: i64 = 0 116 while i < 32 { scratch[p] = ha1[i]; p = p + 1; i = i + 1 } 117 scratch[p] = 0x3A; p = p + 1 118 i = 0 119 while i < nonce_len { scratch[p] = nonce[i]; p = p + 1; i = i + 1 } 120 scratch[p] = 0x3A; p = p + 1 121 i = 0 122 while i < nc_len { scratch[p] = nc[i]; p = p + 1; i = i + 1 } 123 scratch[p] = 0x3A; p = p + 1 124 i = 0 125 while i < cnonce_len { scratch[p] = cnonce[i]; p = p + 1; i = i + 1 } 126 scratch[p] = 0x3A; p = p + 1 127 i = 0 128 while i < qop_len { scratch[p] = qop[i]; p = p + 1; i = i + 1 } 129 scratch[p] = 0x3A; p = p + 1 130 i = 0 131 while i < 32 { scratch[p] = ha2[i]; p = p + 1; i = i + 1 } 132 let digest: *u8 = sys_mmap(32) 133 md5(scratch, total, digest) 134 hex_encode(digest, 16, out) 135 return 32 136} 137 138// Compile-only smoke: RFC 2617 §3.5 worked example. 139// user = "Mufasa" 140// realm = "testrealm@host.com" 141// pass = "Circle Of Life" 142// method = "GET" 143// uri = "/dir/index.html" 144// nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093" 145// nc = "00000001" 146// cnonce = "0a4f113b" 147// qop = "auth" 148// -> response = 6629fae49393a05397450978507c4ef1 149func main() -> i64 { 150 let ha1: *u8 = sys_mmap(64) 151 digest_auth_ha1("Mufasa", 6, 152 "testrealm@host.com", 18, 153 "Circle Of Life", 14, 154 ha1) 155 // HA1 per RFC = 939e7578ed9e3c518a452acee763bce9 156 if ha1[0] != 0x39 { return 1 } // '9' 157 if ha1[1] != 0x33 { return 2 } // '3' 158 if ha1[31] != 0x39 { return 3 } // '9' 159 160 let ha2: *u8 = sys_mmap(64) 161 digest_auth_ha2("GET", 3, "/dir/index.html", 15, ha2) 162 // HA2 per RFC = 39aff3a2bab6126f332b942af96d3366 163 if ha2[0] != 0x33 { return 4 } // '3' 164 if ha2[1] != 0x39 { return 5 } // '9' 165 166 let resp: *u8 = sys_mmap(64) 167 digest_auth_response(ha1, 168 "dcd98b7102dd2f0e8b11d0f600bfb0c093", 34, 169 "00000001", 8, 170 "0a4f113b", 8, 171 "auth", 4, 172 ha2, 173 resp) 174 // Response = 6629fae49393a05397450978507c4ef1 175 if resp[0] != 0x36 { return 6 } // '6' 176 if resp[1] != 0x36 { return 7 } // '6' 177 if resp[31] != 0x31 { return 8 } // '1' 178 return 0 179}