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}