code wiki / (root) / digest_auth.nx

digest_auth.nx source

↩ module page · 173 lines · 6488 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 38import "syscalls.nx" 39import "nx_md5_canonical.nx" 40import "hex.nx" 41 42const DA_ERR_SHORT: i64 = -1 43 44// Write a MD5 hash of concat(a, \":\", b) into 32 hex chars. 45func da_md5_two(a: *u8, a_len: i64, 46 b: *u8, b_len: i64, 47 out: *u8) -> i64 { 48 let scratch_len: i64 = a_len + 1 + b_len 49 let scratch: *u8 = sys_mmap(scratch_len + 16) 50 var i: i64 = 0 51 while i < a_len { 52 scratch[i] = a[i] 53 i = i + 1 54 } 55 scratch[a_len] = 0x3A 56 i = 0 57 while i < b_len { 58 scratch[a_len + 1 + i] = b[i] 59 i = i + 1 60 } 61 let digest: *u8 = sys_mmap(32) 62 md5(scratch, scratch_len, digest) 63 hex_encode(digest, 16, out) 64 return 32 65} 66 67// Compute HA1 = MD5(user : realm : pass) as 32 hex chars. 68func digest_auth_ha1(user: *u8, user_len: i64, 69 realm: *u8, realm_len: i64, 70 pass: *u8, pass_len: i64, 71 out: *u8) -> i64 { 72 let total: i64 = user_len + 1 + realm_len + 1 + pass_len 73 let scratch: *u8 = sys_mmap(total + 16) 74 var p: i64 = 0 75 var i: i64 = 0 76 while i < user_len { scratch[p] = user[i]; p = p + 1; i = i + 1 } 77 scratch[p] = 0x3A; p = p + 1 78 i = 0 79 while i < realm_len { scratch[p] = realm[i]; p = p + 1; i = i + 1 } 80 scratch[p] = 0x3A; p = p + 1 81 i = 0 82 while i < pass_len { scratch[p] = pass[i]; p = p + 1; i = i + 1 } 83 let digest: *u8 = sys_mmap(32) 84 md5(scratch, total, digest) 85 hex_encode(digest, 16, out) 86 return 32 87} 88 89// Compute HA2 = MD5(method : uri) as 32 hex chars. 90func digest_auth_ha2(method: *u8, method_len: i64, 91 uri: *u8, uri_len: i64, 92 out: *u8) -> i64 { 93 return da_md5_two(method, method_len, uri, uri_len, out) 94} 95 96// Compute response = MD5(HA1 : nonce : nc : cnonce : qop : HA2) 97// as 32 hex chars. HA1 + HA2 are 32-byte hex-encoded inputs. 98func digest_auth_response(ha1: *u8, 99 nonce: *u8, nonce_len: i64, 100 nc: *u8, nc_len: i64, 101 cnonce: *u8, cnonce_len: i64, 102 qop: *u8, qop_len: i64, 103 ha2: *u8, 104 out: *u8) -> i64 { 105 // Total = 32 + 1 + nonce + 1 + nc + 1 + cnonce + 1 + qop + 1 + 32 106 let total: i64 = 37 + nonce_len + nc_len + cnonce_len + qop_len 107 let scratch: *u8 = sys_mmap(total + 16) 108 var p: i64 = 0 109 var i: i64 = 0 110 while i < 32 { scratch[p] = ha1[i]; p = p + 1; i = i + 1 } 111 scratch[p] = 0x3A; p = p + 1 112 i = 0 113 while i < nonce_len { scratch[p] = nonce[i]; p = p + 1; i = i + 1 } 114 scratch[p] = 0x3A; p = p + 1 115 i = 0 116 while i < nc_len { scratch[p] = nc[i]; p = p + 1; i = i + 1 } 117 scratch[p] = 0x3A; p = p + 1 118 i = 0 119 while i < cnonce_len { scratch[p] = cnonce[i]; p = p + 1; i = i + 1 } 120 scratch[p] = 0x3A; p = p + 1 121 i = 0 122 while i < qop_len { scratch[p] = qop[i]; p = p + 1; i = i + 1 } 123 scratch[p] = 0x3A; p = p + 1 124 i = 0 125 while i < 32 { scratch[p] = ha2[i]; p = p + 1; i = i + 1 } 126 let digest: *u8 = sys_mmap(32) 127 md5(scratch, total, digest) 128 hex_encode(digest, 16, out) 129 return 32 130} 131 132// Compile-only smoke: RFC 2617 §3.5 worked example. 133// user = "Mufasa" 134// realm = "testrealm@host.com" 135// pass = "Circle Of Life" 136// method = "GET" 137// uri = "/dir/index.html" 138// nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093" 139// nc = "00000001" 140// cnonce = "0a4f113b" 141// qop = "auth" 142// -> response = 6629fae49393a05397450978507c4ef1 143func main() -> i64 { 144 let ha1: *u8 = sys_mmap(64) 145 digest_auth_ha1("Mufasa", 6, 146 "testrealm@host.com", 18, 147 "Circle Of Life", 14, 148 ha1) 149 // HA1 per RFC = 939e7578ed9e3c518a452acee763bce9 150 if ha1[0] != 0x39 { return 1 } // '9' 151 if ha1[1] != 0x33 { return 2 } // '3' 152 if ha1[31] != 0x39 { return 3 } // '9' 153 154 let ha2: *u8 = sys_mmap(64) 155 digest_auth_ha2("GET", 3, "/dir/index.html", 15, ha2) 156 // HA2 per RFC = 39aff3a2bab6126f332b942af96d3366 157 if ha2[0] != 0x33 { return 4 } // '3' 158 if ha2[1] != 0x39 { return 5 } // '9' 159 160 let resp: *u8 = sys_mmap(64) 161 digest_auth_response(ha1, 162 "dcd98b7102dd2f0e8b11d0f600bfb0c093", 33, 163 "00000001", 8, 164 "0a4f113b", 8, 165 "auth", 4, 166 ha2, 167 resp) 168 // Response = 6629fae49393a05397450978507c4ef1 169 if resp[0] != 0x36 { return 6 } // '6' 170 if resp[1] != 0x36 { return 7 } // '6' 171 if resp[31] != 0x31 { return 8 } // '1' 172 return 0 173}