code wiki / (root) / digest_auth.nx

digest_auth.nx source

↩ module page · 185 lines · 7371 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" 39// 2026-08-01 -- REPOINTED FROM md5.nx TO nx_md5_canonical.nx. SECOND sev-8 OF THE SAME SHAPE AS sha1.nx. 40// 41// md5.nx COMPUTES WRONG DIGESTS. Adjudicated by RFC 2202 section 2 (7 published HMAC-MD5 vectors, in a 42// document already pinned and cross-stack corroborated): 43// nx_hmacmd5_extvec_gate -> nx_hmac_md5.nx -> nx_md5_canonical.nx 7/7 GREEN 44// nx_hmacmd5alt_extvec_gate -> hmac_md5.nx -> md5.nx 0/7 RED 45// One import apart, identical readers, identical document. * A DUPLICATE PAIR IS A COIN FLIP UNTIL AN 46// AUTHORITY ADJUDICATES IT -- and the un-prefixed twin lost, exactly as sha1.nx did. 47// 48// VERIFIED DROP-IN BEFORE EDITING: both files export the SAME symbol set 49// (md5, md5_rotl, md5_s, md5_k, md5_g, md5_process_block, MD5_MASK32) -- nothing is lost by repointing. 50// * A "DROP-IN REPLACEMENT" IS A CLAIM ABOUT SYMBOL SETS; CHECK THEM. 51import "nx_md5_canonical.nx" 52import "hex.nx" 53 54const DA_ERR_SHORT: i64 = -1 55 56// Write a MD5 hash of concat(a, \":\", b) into 32 hex chars. 57func da_md5_two(a: *u8, a_len: i64, 58 b: *u8, b_len: i64, 59 out: *u8) -> i64 { 60 let scratch_len: i64 = a_len + 1 + b_len 61 let scratch: *u8 = sys_mmap(scratch_len + 16) 62 var i: i64 = 0 63 while i < a_len { 64 scratch[i] = a[i] 65 i = i + 1 66 } 67 scratch[a_len] = 0x3A 68 i = 0 69 while i < b_len { 70 scratch[a_len + 1 + i] = b[i] 71 i = i + 1 72 } 73 let digest: *u8 = sys_mmap(32) 74 md5(scratch, scratch_len, digest) 75 hex_encode(digest, 16, out) 76 return 32 77} 78 79// Compute HA1 = MD5(user : realm : pass) as 32 hex chars. 80func digest_auth_ha1(user: *u8, user_len: i64, 81 realm: *u8, realm_len: i64, 82 pass: *u8, pass_len: i64, 83 out: *u8) -> i64 { 84 let total: i64 = user_len + 1 + realm_len + 1 + pass_len 85 let scratch: *u8 = sys_mmap(total + 16) 86 var p: i64 = 0 87 var i: i64 = 0 88 while i < user_len { scratch[p] = user[i]; p = p + 1; i = i + 1 } 89 scratch[p] = 0x3A; p = p + 1 90 i = 0 91 while i < realm_len { scratch[p] = realm[i]; p = p + 1; i = i + 1 } 92 scratch[p] = 0x3A; p = p + 1 93 i = 0 94 while i < pass_len { scratch[p] = pass[i]; p = p + 1; i = i + 1 } 95 let digest: *u8 = sys_mmap(32) 96 md5(scratch, total, digest) 97 hex_encode(digest, 16, out) 98 return 32 99} 100 101// Compute HA2 = MD5(method : uri) as 32 hex chars. 102func digest_auth_ha2(method: *u8, method_len: i64, 103 uri: *u8, uri_len: i64, 104 out: *u8) -> i64 { 105 return da_md5_two(method, method_len, uri, uri_len, out) 106} 107 108// Compute response = MD5(HA1 : nonce : nc : cnonce : qop : HA2) 109// as 32 hex chars. HA1 + HA2 are 32-byte hex-encoded inputs. 110func digest_auth_response(ha1: *u8, 111 nonce: *u8, nonce_len: i64, 112 nc: *u8, nc_len: i64, 113 cnonce: *u8, cnonce_len: i64, 114 qop: *u8, qop_len: i64, 115 ha2: *u8, 116 out: *u8) -> i64 { 117 // Total = 32 + 1 + nonce + 1 + nc + 1 + cnonce + 1 + qop + 1 + 32 118 let total: i64 = 37 + nonce_len + nc_len + cnonce_len + qop_len 119 let scratch: *u8 = sys_mmap(total + 16) 120 var p: i64 = 0 121 var i: i64 = 0 122 while i < 32 { scratch[p] = ha1[i]; p = p + 1; i = i + 1 } 123 scratch[p] = 0x3A; p = p + 1 124 i = 0 125 while i < nonce_len { scratch[p] = nonce[i]; p = p + 1; i = i + 1 } 126 scratch[p] = 0x3A; p = p + 1 127 i = 0 128 while i < nc_len { scratch[p] = nc[i]; p = p + 1; i = i + 1 } 129 scratch[p] = 0x3A; p = p + 1 130 i = 0 131 while i < cnonce_len { scratch[p] = cnonce[i]; p = p + 1; i = i + 1 } 132 scratch[p] = 0x3A; p = p + 1 133 i = 0 134 while i < qop_len { scratch[p] = qop[i]; p = p + 1; i = i + 1 } 135 scratch[p] = 0x3A; p = p + 1 136 i = 0 137 while i < 32 { scratch[p] = ha2[i]; p = p + 1; i = i + 1 } 138 let digest: *u8 = sys_mmap(32) 139 md5(scratch, total, digest) 140 hex_encode(digest, 16, out) 141 return 32 142} 143 144// Compile-only smoke: RFC 2617 §3.5 worked example. 145// user = "Mufasa" 146// realm = "testrealm@host.com" 147// pass = "Circle Of Life" 148// method = "GET" 149// uri = "/dir/index.html" 150// nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093" 151// nc = "00000001" 152// cnonce = "0a4f113b" 153// qop = "auth" 154// -> response = 6629fae49393a05397450978507c4ef1 155func main() -> i64 { 156 let ha1: *u8 = sys_mmap(64) 157 digest_auth_ha1("Mufasa", 6, 158 "testrealm@host.com", 18, 159 "Circle Of Life", 14, 160 ha1) 161 // HA1 per RFC = 939e7578ed9e3c518a452acee763bce9 162 if ha1[0] != 0x39 { return 1 } // '9' 163 if ha1[1] != 0x33 { return 2 } // '3' 164 if ha1[31] != 0x39 { return 3 } // '9' 165 166 let ha2: *u8 = sys_mmap(64) 167 digest_auth_ha2("GET", 3, "/dir/index.html", 15, ha2) 168 // HA2 per RFC = 39aff3a2bab6126f332b942af96d3366 169 if ha2[0] != 0x33 { return 4 } // '3' 170 if ha2[1] != 0x39 { return 5 } // '9' 171 172 let resp: *u8 = sys_mmap(64) 173 digest_auth_response(ha1, 174 "dcd98b7102dd2f0e8b11d0f600bfb0c093", 33, 175 "00000001", 8, 176 "0a4f113b", 8, 177 "auth", 4, 178 ha2, 179 resp) 180 // Response = 6629fae49393a05397450978507c4ef1 181 if resp[0] != 0x36 { return 6 } // '6' 182 if resp[1] != 0x36 { return 7 } // '6' 183 if resp[31] != 0x31 { return 8 } // '1' 184 return 0 185}