|
41 | 41 |
|
42 | 42 | #define MY_OPAQUE "11733b200778ce33060f31c9af70a870ba96ddd4" |
43 | 43 |
|
44 | | -// v2-digest tracking note (consolidated): |
45 | | -// Under v2, libhttpserver only emits the static 401 Digest challenge; the |
46 | | -// nonce/opaque state machine is not driven, so check_digest_auth_check[_digest]() |
47 | | -// is never reached and get_digested_user() always returns an empty view. |
48 | | -// As a result the following tests are all observationally indistinguishable |
49 | | -// from the canonical `digest_auth` case (both correct- and wrong-pass arms |
50 | | -// see the same 401 + "FAIL"): |
| 44 | +// v2-digest tracking note (updated after TASK-062): |
| 45 | +// After TASK-062, digest_resource and the HA1 resources (digest_ha1_md5_resource, |
| 46 | +// digest_ha1_sha256_resource) all emit full RFC-7616 challenges via |
| 47 | +// http_response::unauthorized(digest_challenge{...}). The nonce/opaque |
| 48 | +// state machine is driven by MHD_queue_auth_required_response3, and the |
| 49 | +// following tests now complete the full digest handshake and assert 200 SUCCESS: |
| 50 | +// - digest_auth, digest_auth_with_ha1_md5, digest_auth_with_ha1_sha256 |
| 51 | +// |
| 52 | +// The following tests still assert the 401 FAIL contract (wrong credentials or |
| 53 | +// no auth provided -- the handshake completes but auth fails): |
51 | 54 | // - digest_auth_wrong_pass |
52 | | -// - digest_auth_with_ha1_md5 / *_wrong_pass |
53 | | -// - digest_auth_with_ha1_sha256 / *_wrong_pass |
54 | | -// - digest_user_cache_with_auth |
55 | | -// They are retained as pins for the static-challenge contract and become |
56 | | -// meaningful again only when full v2 Digest support (MHD nonce/opaque state |
57 | | -// machine) lands. At that point the wrong-pass arms should assert a distinct |
58 | | -// 403/401-with-stale, the HA1-MD5/SHA-256 arms should validate the chosen |
59 | | -// algorithm, and digest_user_cache_with_auth should exercise the cache-hit |
60 | | -// path. See PRD §digest-auth for the follow-up. |
| 55 | +// - digest_auth_with_ha1_md5_wrong_pass, *_sha256_wrong_pass |
| 56 | +// |
| 57 | +// Remaining gap (signal_stale re-challenge path): |
| 58 | +// The NONCE_STALE branch inside digest_resource/digest_ha1_*_resource |
| 59 | +// (signal_stale = true) cannot be reliably triggered in CI because MHD's |
| 60 | +// nonce expiry requires a real time delay and concurrent replay requests. |
| 61 | +// This gap is documented in TASK-062 test-quality-reviewer iter1-2; it will |
| 62 | +// be covered when full nonce-expiry testing infrastructure lands. |
| 63 | +// |
| 64 | +// Also pending: digest_user_cache_with_auth exercises the cache-hit path only |
| 65 | +// once the digest handshake completes (see resource definition below). |
61 | 66 |
|
62 | 67 | using std::shared_ptr; |
63 | 68 | using httpserver::webserver; |
@@ -213,51 +218,77 @@ static const unsigned char PRECOMPUTED_HA1_SHA256[32] = { |
213 | 218 | 0x20, 0x7e, 0x02, 0xd7, 0xc4, 0xbd, 0x8a, 0x05 |
214 | 219 | }; |
215 | 220 |
|
| 221 | +// TASK-062: migrated to digest_challenge so the nonce/opaque handshake can |
| 222 | +// complete and check_digest_auth_digest() is exercised. Previously used the |
| 223 | +// legacy string overload which emitted a static challenge with no nonce, |
| 224 | +// making the handshake impossible and the algorithm parameter meaningless. |
216 | 225 | class digest_ha1_md5_resource : public http_resource { |
217 | 226 | public: |
218 | 227 | http_response render_get(const http_request& req) { |
219 | 228 | using httpserver::http::http_utils; |
| 229 | + using httpserver::digest_challenge; |
220 | 230 | if (req.get_digested_user() == "") { |
221 | | - return http_response::unauthorized("Digest", "examplerealm", "FAIL"); |
| 231 | + return http_response::unauthorized( |
| 232 | + digest_challenge{.realm = "examplerealm", |
| 233 | + .algorithm = http_utils::digest_algorithm::MD5, |
| 234 | + .body = "FAIL"}); |
222 | 235 | } |
223 | 236 | auto result = req.check_digest_auth_digest("examplerealm", PRECOMPUTED_HA1_MD5, |
224 | 237 | http_utils::md5_digest_size, 300, 0, |
225 | 238 | http_utils::digest_algorithm::MD5); |
226 | 239 | if (result == http_utils::digest_auth_result::NONCE_STALE) { |
227 | | - return http_response::unauthorized("Digest", "examplerealm", "FAIL"); |
| 240 | + return http_response::unauthorized( |
| 241 | + digest_challenge{.realm = "examplerealm", |
| 242 | + .algorithm = http_utils::digest_algorithm::MD5, |
| 243 | + .signal_stale = true, |
| 244 | + .body = "FAIL"}); |
228 | 245 | } else if (result != http_utils::digest_auth_result::OK) { |
229 | | - return http_response::unauthorized("Digest", "examplerealm", "FAIL"); |
| 246 | + return http_response::unauthorized( |
| 247 | + digest_challenge{.realm = "examplerealm", |
| 248 | + .algorithm = http_utils::digest_algorithm::MD5, |
| 249 | + .body = "FAIL"}); |
230 | 250 | } |
231 | 251 | return http_response::string("SUCCESS"); |
232 | 252 | } |
233 | 253 | }; |
234 | 254 |
|
| 255 | +// TASK-062: migrated to digest_challenge with algorithm=SHA256 so the |
| 256 | +// nonce/opaque handshake can complete and check_digest_auth_digest() is |
| 257 | +// exercised with the SHA-256 algorithm. Previously used the legacy string |
| 258 | +// overload which made the algorithm parameter meaningless. |
235 | 259 | class digest_ha1_sha256_resource : public http_resource { |
236 | 260 | public: |
237 | 261 | http_response render_get(const http_request& req) { |
238 | 262 | using httpserver::http::http_utils; |
| 263 | + using httpserver::digest_challenge; |
239 | 264 | if (req.get_digested_user() == "") { |
240 | | - return http_response::unauthorized("Digest", "examplerealm", "FAIL"); |
| 265 | + return http_response::unauthorized( |
| 266 | + digest_challenge{.realm = "examplerealm", |
| 267 | + .algorithm = http_utils::digest_algorithm::SHA256, |
| 268 | + .body = "FAIL"}); |
241 | 269 | } |
242 | 270 | auto result = req.check_digest_auth_digest("examplerealm", PRECOMPUTED_HA1_SHA256, |
243 | 271 | http_utils::sha256_digest_size, 300, 0, |
244 | 272 | http_utils::digest_algorithm::SHA256); |
245 | 273 | if (result == http_utils::digest_auth_result::NONCE_STALE) { |
246 | | - return http_response::unauthorized("Digest", "examplerealm", "FAIL"); |
| 274 | + return http_response::unauthorized( |
| 275 | + digest_challenge{.realm = "examplerealm", |
| 276 | + .algorithm = http_utils::digest_algorithm::SHA256, |
| 277 | + .signal_stale = true, |
| 278 | + .body = "FAIL"}); |
247 | 279 | } else if (result != http_utils::digest_auth_result::OK) { |
248 | | - return http_response::unauthorized("Digest", "examplerealm", "FAIL"); |
| 280 | + return http_response::unauthorized( |
| 281 | + digest_challenge{.realm = "examplerealm", |
| 282 | + .algorithm = http_utils::digest_algorithm::SHA256, |
| 283 | + .body = "FAIL"}); |
249 | 284 | } |
250 | 285 | return http_response::string("SUCCESS"); |
251 | 286 | } |
252 | 287 | }; |
253 | 288 |
|
254 | | -// TASK-013 §2 / §10: full digest-auth round-trip is a v1-only behaviour. |
255 | | -// The v1 `digest_auth_fail_response::enqueue_response` path called |
256 | | -// MHD_queue_auth_required_response3 to drive libmicrohttpd's nonce/opaque |
257 | | -// state machine; v2's `unauthorized("Digest", ...)` only emits a static |
258 | | -// WWW-Authenticate challenge (see http_response.hpp:175-180 doxygen). |
259 | | -// These tests now assert the v2 contract: the resource emits FAIL on the |
260 | | -// initial request because curl's nonce roundtrip cannot complete. |
| 289 | +// TASK-062: digest_resource uses http_response::unauthorized(digest_challenge{...}) |
| 290 | +// which routes through MHD_queue_auth_required_response3 so curl --digest can |
| 291 | +// complete the full RFC-7616 nonce/opaque handshake and receive 200 SUCCESS. |
261 | 292 | LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth) |
262 | 293 | webserver ws{create_webserver(PORT) |
263 | 294 | .digest_auth_random("myrandom") |
@@ -305,7 +336,18 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth) |
305 | 336 | ws.stop(); |
306 | 337 | LT_END_AUTO_TEST(digest_auth) |
307 | 338 |
|
308 | | -// See v2-digest tracking note at top of file. |
| 339 | +// Wrong-password case: curl completes the nonce handshake but check_digest_auth() |
| 340 | +// returns NOT_AUTHORIZED, so the resource emits a new 401 challenge (body "FAIL"). |
| 341 | +// |
| 342 | +// TODO(TASK-062 test-quality-reviewer iter1-2): The signal_stale=true re-challenge |
| 343 | +// branch in digest_resource::render_get (NONCE_STALE path) is not covered here. |
| 344 | +// Triggering NONCE_STALE requires sending a replayed/expired nonce. MHD's nonce |
| 345 | +// expiry (nonce_nc_size window) cannot be reliably exhausted in a single-threaded |
| 346 | +// CI test without real-time delays or replay injection. Once nonce-expiry |
| 347 | +// infrastructure is available, add a test that: |
| 348 | +// (a) sends the correct credentials with a replayed/expired nonce, |
| 349 | +// (b) asserts status 401 with WWW-Authenticate containing stale=true, |
| 350 | +// (c) asserts body is "FAIL". |
309 | 351 | LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass) |
310 | 352 | webserver ws{create_webserver(PORT) |
311 | 353 | .digest_auth_random("myrandom") |
@@ -350,7 +392,10 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_wrong_pass) |
350 | 392 | ws.stop(); |
351 | 393 | LT_END_AUTO_TEST(digest_auth_wrong_pass) |
352 | 394 |
|
353 | | -// See v2-digest tracking note at top of file. |
| 395 | +// TASK-062: digest_ha1_md5_resource now emits a full RFC-7616 digest_challenge |
| 396 | +// with algorithm=MD5 so the nonce/opaque handshake can complete. curl --digest |
| 397 | +// authenticates using the precomputed HA1 and the server verifies via |
| 398 | +// check_digest_auth_digest(). On success the resource returns 200 SUCCESS. |
354 | 399 | LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5) |
355 | 400 | webserver ws{create_webserver(PORT) |
356 | 401 | .digest_auth_random("myrandom") |
@@ -387,17 +432,17 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5) |
387 | 432 | res = curl_easy_perform(curl); |
388 | 433 | LT_ASSERT_EQ(res, 0); |
389 | 434 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); |
390 | | - // v2 contract: static 401 Digest challenge, no handshake completes. |
391 | | - LT_CHECK_EQ(http_code, 401); |
392 | | - // TASK-013 §2 / §10: v2 digest auth only emits a static challenge — see |
393 | | - // digest_auth test above. Handshake cannot complete; body remains FAIL. |
394 | | - LT_CHECK_EQ(s, "FAIL"); |
| 435 | + // TASK-062 contract: digest_challenge emits a full RFC-7616 challenge with |
| 436 | + // algorithm=MD5; curl --digest completes the handshake. Final response: 200. |
| 437 | + LT_CHECK_EQ(http_code, 200); |
| 438 | + LT_CHECK_EQ(s, "SUCCESS"); |
395 | 439 | curl_easy_cleanup(curl); |
396 | 440 |
|
397 | 441 | ws.stop(); |
398 | 442 | LT_END_AUTO_TEST(digest_auth_with_ha1_md5) |
399 | 443 |
|
400 | | -// See v2-digest tracking note at top of file. |
| 444 | +// TASK-062: digest_ha1_md5_resource now uses digest_challenge; curl completes |
| 445 | +// the handshake but check_digest_auth_digest() rejects the wrong HA1. 401 FAIL. |
401 | 446 | LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass) |
402 | 447 | webserver ws{create_webserver(PORT) |
403 | 448 | .digest_auth_random("myrandom") |
@@ -434,15 +479,18 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_md5_wrong_pass) |
434 | 479 | res = curl_easy_perform(curl); |
435 | 480 | LT_ASSERT_EQ(res, 0); |
436 | 481 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); |
437 | | - // v2 contract: static 401 Digest challenge, no handshake completes. |
| 482 | + // TASK-062 contract: handshake completes, but wrong HA1 fails check_digest_auth_digest. |
438 | 483 | LT_CHECK_EQ(http_code, 401); |
439 | 484 | LT_CHECK_EQ(s, "FAIL"); |
440 | 485 | curl_easy_cleanup(curl); |
441 | 486 |
|
442 | 487 | ws.stop(); |
443 | 488 | LT_END_AUTO_TEST(digest_auth_with_ha1_md5_wrong_pass) |
444 | 489 |
|
445 | | -// See v2-digest tracking note at top of file. |
| 490 | +// TASK-062: digest_ha1_sha256_resource now emits a full RFC-7616 digest_challenge |
| 491 | +// with algorithm=SHA256 so the nonce/opaque handshake can complete. curl --digest |
| 492 | +// authenticates using the precomputed HA1 and the server verifies via |
| 493 | +// check_digest_auth_digest() with SHA-256. On success the resource returns 200 SUCCESS. |
446 | 494 | LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256) |
447 | 495 | webserver ws{create_webserver(PORT) |
448 | 496 | .digest_auth_random("myrandom") |
@@ -479,17 +527,17 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256) |
479 | 527 | res = curl_easy_perform(curl); |
480 | 528 | LT_ASSERT_EQ(res, 0); |
481 | 529 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); |
482 | | - // v2 contract: static 401 Digest challenge, no handshake completes. |
483 | | - LT_CHECK_EQ(http_code, 401); |
484 | | - // TASK-013 §2 / §10: v2 digest auth only emits a static challenge — see |
485 | | - // digest_auth test above. Handshake cannot complete; body remains FAIL. |
486 | | - LT_CHECK_EQ(s, "FAIL"); |
| 530 | + // TASK-062 contract: digest_challenge emits a full RFC-7616 challenge with |
| 531 | + // algorithm=SHA256; curl --digest completes the handshake. Final response: 200. |
| 532 | + LT_CHECK_EQ(http_code, 200); |
| 533 | + LT_CHECK_EQ(s, "SUCCESS"); |
487 | 534 | curl_easy_cleanup(curl); |
488 | 535 |
|
489 | 536 | ws.stop(); |
490 | 537 | LT_END_AUTO_TEST(digest_auth_with_ha1_sha256) |
491 | 538 |
|
492 | | -// See v2-digest tracking note at top of file. |
| 539 | +// TASK-062: digest_ha1_sha256_resource now uses digest_challenge; curl completes |
| 540 | +// the handshake but check_digest_auth_digest() rejects the wrong HA1. 401 FAIL. |
493 | 541 | LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass) |
494 | 542 | webserver ws{create_webserver(PORT) |
495 | 543 | .digest_auth_random("myrandom") |
@@ -526,7 +574,7 @@ LT_BEGIN_AUTO_TEST(authentication_suite, digest_auth_with_ha1_sha256_wrong_pass) |
526 | 574 | res = curl_easy_perform(curl); |
527 | 575 | LT_ASSERT_EQ(res, 0); |
528 | 576 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); |
529 | | - // v2 contract: static 401 Digest challenge, no handshake completes. |
| 577 | + // TASK-062 contract: handshake completes, but wrong HA1 fails check_digest_auth_digest. |
530 | 578 | LT_CHECK_EQ(http_code, 401); |
531 | 579 | LT_CHECK_EQ(s, "FAIL"); |
532 | 580 | curl_easy_cleanup(curl); |
|
0 commit comments