From f48a75ed6f80ef9e269ddc42ab176c2afdf6d4c3 Mon Sep 17 00:00:00 2001 From: Pavel Ptashyts <49400901+pavel-ptashyts@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:25:07 +0200 Subject: [PATCH] Avoid duplicate preemptive Digest hashing Preemptive auth-int built temporary Realm objects and recalculated HA1 before rebuilding the realm again. The final build also overwrote the response that included the request body. Pass the entity-body hash to the request-specific realm builder and build it once for both origin and proxy authentication. Remove the duplicate digest calculation path and verify the emitted response independently. Codex on behalf of Pavel Ptashyts Co-Authored-By: Codex --- .../util/AuthenticatorUtils.java | 88 +------------------ .../util/AuthenticatorUtilsTest.java | 51 +++++++++++ 2 files changed, 53 insertions(+), 86 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 66d2b73b3..c2cf701b8 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -207,66 +207,6 @@ public static String computeRspAuth(Realm realm) { } } - /** - * Calculates the digest response value for HTTP Digest Authentication. - * This method computes HA1 and HA2 (including entity-body hash for auth-int). - * - * @param realm The authentication realm containing credentials and challenge info - * @param request The HTTP request (needed for method, uri, and body) - * @return The computed response hex string - * @throws UnsupportedOperationException if qop=auth-int but body cannot be hashed - */ - static String computeDigestResponse(Realm realm, Request request) { - String algorithm = realm.getAlgorithm() != null ? realm.getAlgorithm() : "MD5"; - String qop = realm.getQop() != null ? realm.getQop() : "auth"; - - String hashAlgorithm = algorithm.replace("-sess", ""); - Charset wireCharset = realm.getCharset() != null ? - realm.getCharset() : StandardCharsets.ISO_8859_1; - - // Calculate HA1 - String ha1 = calculateHA1(realm, algorithm); - - // Get request URI - Uri uri = request.getUri(); - String requestUri = uri.getPath() + - (uri.getQuery() != null ? "?" + uri.getQuery() : ""); - - // Calculate HA2 - String ha2; - if ("auth-int".equals(qop)) { - String bodyHash = computeBodyHash(request, realm); - ha2 = calculateHA2AuthInt(request, requestUri, bodyHash, hashAlgorithm, wireCharset); - } else { - // Regular auth: HA2 = H(method:uri) - String a2Plain = request.getMethod() + ":" + requestUri; - MessageDigest md = MessageDigestUtils.pooledMessageDigest(hashAlgorithm); - try { - md.update(a2Plain.getBytes(wireCharset)); - ha2 = MessageDigestUtils.bytesToHex(md.digest()); - } finally { - md.reset(); - } - } - - // Build final response - String nc = realm.getNc() != null ? realm.getNc() : "00000001"; - String cnonce = realm.getCnonce(); - String nonce = realm.getNonce(); - - // response = H(HA1:nonce:nc:cnonce:qop:HA2) - String responseInput = ha1 + ":" + nonce + ":" + nc + ":" + - cnonce + ":" + qop + ":" + ha2; - - MessageDigest md = MessageDigestUtils.pooledMessageDigest(hashAlgorithm); - try { - md.update(responseInput.getBytes(StandardCharsets.ISO_8859_1)); - return MessageDigestUtils.bytesToHex(md.digest()); - } finally { - md.reset(); - } - } - /** * Calculates the HA1 value for HTTP Digest Authentication. * This method handles both regular and session-based HA1 calculations. @@ -300,28 +240,6 @@ private static String calculateHA1(Realm realm, String algorithm) { } } - /** - * Calculates the HA2 value for HTTP Digest Authentication. - * This method handles both auth and auth-int cases. - * - * @param request The HTTP request (needed for method, uri, and body) - * @param requestUri The request URI - * @param bodyHash The entity-body hash (for auth-int, can be empty for auth) - * @param hashAlgorithm The digest algorithm (e.g., "MD5") - * @param wireCs The charset used for wire encoding - * @return The computed HA2 hex string - */ - private static String calculateHA2AuthInt(Request request, String requestUri, String bodyHash, String hashAlgorithm, Charset wireCs) { - String a2Plain = request.getMethod() + ':' + requestUri + ':' + bodyHash; - MessageDigest md = MessageDigestUtils.pooledMessageDigest(hashAlgorithm); - try { - md.update(a2Plain.getBytes(wireCs)); - return MessageDigestUtils.bytesToHex(md.digest()); - } finally { - md.reset(); // return clean to pool - } - } - public static String computeBodyHash(Request request, Realm realm) { if (request.getStringData() == null && @@ -561,8 +479,7 @@ private static void appendQuotedStringContent(StringBuilder builder, @Nullable S .setMethodName(request.getMethod()); if ("auth-int".equals(proxyRealm.getQop())) { - String response = computeDigestResponse(proxyRealm, request); - realmBuilder.setResponse(response); + realmBuilder.setEntityBodyHash(computeBodyHash(request, proxyRealm)); } proxyRealm = realmBuilder.build(); @@ -644,8 +561,7 @@ private static void appendQuotedStringContent(StringBuilder builder, @Nullable S .setUri(uri) .setMethodName(request.getMethod()); if ("auth-int".equals(realm.getQop())) { - String response = computeDigestResponse(realmBuilder.build(), request); - realmBuilder.setResponse(response); + realmBuilder.setEntityBodyHash(computeBodyHash(request, realm)); } realm = realmBuilder.build(); diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java index c98f19483..971025275 100644 --- a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -494,6 +494,52 @@ void computeRspAuth_basic() throws Exception { assertEquals(32, rspauth.length()); // MD5 hex is 32 chars } + @Test + void preemptiveDigestAuthIntUsesRequestBody() throws Exception { + assertPreemptiveDigestAuthIntUsesRequestBody(false); + } + + @Test + void preemptiveProxyDigestAuthIntUsesRequestBody() throws Exception { + assertPreemptiveDigestAuthIntUsesRequestBody(true); + } + + private static void assertPreemptiveDigestAuthIntUsesRequestBody(boolean proxy) throws Exception { + String principal = "user"; + String password = "pass"; + String realmName = "testrealm"; + String nonce = "testnonce"; + String body = "request body"; + Realm realm = new Realm.Builder(principal, password) + .setScheme(Realm.AuthScheme.DIGEST) + .setUsePreemptiveAuth(true) + .setRealmName(realmName) + .setNonce(nonce) + .setAlgorithm("MD5") + .setQop("auth-int") + .build(); + Request request = new RequestBuilder("POST") + .setUrl("http://example.com/dir/index.html") + .setBody(body) + .build(); + + String header = proxy + ? AuthenticatorUtils.perRequestProxyAuthorizationHeader(request, realm) + : AuthenticatorUtils.perRequestAuthorizationHeader(request, realm); + + assertNotNull(header); + String nc = Realm.Builder.matchParam(header, "nc"); + String cnonce = Realm.Builder.matchParam(header, "cnonce"); + String response = Realm.Builder.matchParam(header, "response"); + assertNotNull(nc); + assertNotNull(cnonce); + assertNotNull(response); + String ha1 = md5(principal + ':' + realmName + ':' + password); + String bodyHash = md5(body); + String ha2 = md5("POST:/dir/index.html:" + bodyHash); + assertEquals(md5(ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ":auth-int:" + ha2), response); + } + // A server-controlled challenge value (nonce/realm/opaque) must be backslash-escaped when echoed // into the quoted-string params of the outgoing Authorization header, so a bare " cannot close the // value early and inject extra auth-params. @@ -518,4 +564,9 @@ void digestChallengeValuesEscapedInAuthorizationHeader() { assertTrue(header.contains("opaque=\"op\\\\aque\""), header); assertFalse(header.contains("nonce=\"abc\"x\""), header); } + + private static String md5(String value) throws Exception { + return MessageDigestUtils.bytesToHex( + MessageDigest.getInstance("MD5").digest(value.getBytes(StandardCharsets.ISO_8859_1))); + } }