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}