This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c2293eefb1c0ba8893e7f433b895df983be44368 Author: opencode <[email protected]> AuthorDate: Fri Oct 16 23:00:22 2026 +0200 Fix WebSocket client digest response calculation for qop auth-int When the server challenge used qop="auth-int", the WebSocket client's DigestAuthenticator computed A2 as Method ":" digest-uri-value, omitting the H(entity-body) term required by RFC 2617 section 3.3.2 and RFC 7616. A WebSocket handshake has no entity body but the hash of the empty string still has to be included. Without it, the generated response value was always rejected by such servers, making the client unable to authenticate to third-party servers that advertise auth-int. Append the hash of the empty entity body to A2 when the challenge qop is auth-int and correct the comment that claimed the term was not needed. Add a unit test that checks the generated response value against an independent, RFC based calculation done the same way a server would, for both qop=auth and qop=auth-int. The auth-int case fails against the previous code. Found by code review. --- .../tomcat/websocket/DigestAuthenticator.java | 9 ++- .../tomcat/websocket/TestDigestAuthenticator.java | 93 ++++++++++++++++++++++ webapps/docs/changelog.xml | 6 ++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/java/org/apache/tomcat/websocket/DigestAuthenticator.java b/java/org/apache/tomcat/websocket/DigestAuthenticator.java index 405d654159..f885b55607 100644 --- a/java/org/apache/tomcat/websocket/DigestAuthenticator.java +++ b/java/org/apache/tomcat/websocket/DigestAuthenticator.java @@ -129,10 +129,15 @@ public class DigestAuthenticator extends Authenticator { } /* - * If the "qop" value is "auth-int", then A2 is: A2 = Method ":" digest-uri-value ":" H(entity-body) since we do - * not have an entity-body, A2 = Method ":" digest-uri-value for auth and auth_int + * If the "qop" value is "auth-int", then A2 is: + * A2 = Method ":" digest-uri-value ":" H(entity-body) + * There is no entity-body for a WebSocket handshake but the hash of + * the empty body still has to be included. */ String A2 = method + ":" + requestUri; + if ("auth-int".equals(qop)) { + A2 = A2 + ":" + encode(algorithm, ""); + } preDigest.append(encode(algorithm, A1)); preDigest.append(':'); diff --git a/test/org/apache/tomcat/websocket/TestDigestAuthenticator.java b/test/org/apache/tomcat/websocket/TestDigestAuthenticator.java new file mode 100644 index 0000000000..682c1973f4 --- /dev/null +++ b/test/org/apache/tomcat/websocket/TestDigestAuthenticator.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.websocket; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.util.buf.HexUtils; + +public class TestDigestAuthenticator { + + private static final Pattern RESPONSE_PATTERN = Pattern.compile("response=\"([^\"]+)\""); + private static final Pattern CNONCE_PATTERN = Pattern.compile("cnonce=\"([^\"]+)\""); + private static final Pattern NC_PATTERN = Pattern.compile("nc=([0-9a-fA-F]{8})"); + + @Test + public void testAuthorizationQopAuth() throws Exception { + doTestAuthorization("auth"); + } + + @Test + public void testAuthorizationQopAuthInt() throws Exception { + doTestAuthorization("auth-int"); + } + + private void doTestAuthorization(String qop) throws Exception { + String method = "GET"; + String requestUri = "/ws/echo"; + String userName = "user"; + String password = "password"; + String realm = "realm"; + String nonce = "0000f844a605d5c0cb0270f1e2f10ca0"; + String algorithm = "MD5"; + + String authenticateHeader = "Digest realm=\"" + realm + "\", qop=\"" + qop + "\", nonce=\"" + nonce + + "\", algorithm=" + algorithm; + + DigestAuthenticator authenticator = new DigestAuthenticator(); + String authorization = + authenticator.getAuthorization(method, requestUri, authenticateHeader, userName, password, null); + + String response = extract(RESPONSE_PATTERN, authorization); + String cnonce = extract(CNONCE_PATTERN, authorization); + String nc = extract(NC_PATTERN, authorization); + Assert.assertEquals("00000001", nc); + + // Calculate the expected response independently of the code under + // test, in the same way as a server would, per RFC 2617 section + // 3.2.2.1 and RFC 7616 section 3.4.2. + String a1 = userName + ":" + realm + ":" + password; + String a2 = method + ":" + requestUri; + if ("auth-int".equals(qop)) { + // RFC 7616: A2 includes H(entity-body). A WebSocket handshake + // does not have an entity body so use the hash of the empty + // string. + a2 = a2 + ":" + encode(""); + } + String expected = encode(encode(a1) + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + encode(a2)); + + Assert.assertEquals(expected, response); + } + + private static String extract(Pattern pattern, String authorization) { + Matcher m = pattern.matcher(authorization); + Assert.assertTrue(m.find()); + return m.group(1); + } + + private static String encode(String value) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(value.getBytes(StandardCharsets.ISO_8859_1)); + return HexUtils.toHexString(digest); + } +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 28bac2a609..f9e6869338 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -398,6 +398,12 @@ write operations so a stalled peer cannot block the write thread indefinitely. (remm) </fix> + <fix> + Correct the calculation of the digest <code>response</code> value by + the WebSocket client when the server uses <code>qop="auth-int"</code>. + The hash of the (empty) entity body was omitted, resulting in an + incorrect response that the server always rejected. (remm) + </fix> </changelog> </subsection> <subsection name="Web applications"> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
