This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/10.1.x by this push:
     new a102a2a157 Switch AJP secret comparison to a constant time algorithm.
a102a2a157 is described below

commit a102a2a157868ca51d83eaf5a119ccd9976a113e
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Apr 23 17:56:48 2026 +0100

    Switch AJP secret comparison to a constant time algorithm.
    
    # Conflicts:
    #       webapps/docs/changelog.xml
---
 .../realm/DigestCredentialHandlerBase.java         |  40 ++----
 java/org/apache/coyote/ajp/AjpProcessor.java       |   3 +-
 .../apache/tomcat/util/security/ConstantTime.java  | 142 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   3 +
 4 files changed, 156 insertions(+), 32 deletions(-)

diff --git a/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java 
b/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java
index eda0aa816a..a3edbeb547 100644
--- a/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java
+++ b/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java
@@ -25,6 +25,7 @@ import org.apache.catalina.CredentialHandler;
 import org.apache.juli.logging.Log;
 import org.apache.tomcat.util.buf.HexUtils;
 import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.util.security.ConstantTime;
 
 /**
  * Base implementation for the Tomcat provided {@link CredentialHandler}s.
@@ -286,38 +287,12 @@ public abstract class DigestCredentialHandlerBase 
implements CredentialHandler {
      *                       here is only guaranteed to work with plain ASCII 
characters.
      *
      * @return <code>true</code> if the strings are equal to each other, 
<code>false</code> otherwise.
+     *
+     * @deprecated Use {@link ConstantTime#equals(String, String, boolean)}. 
This method will be removed in Tomcat 12.
      */
+    @Deprecated
     public static boolean equals(final String s1, final String s2, final 
boolean ignoreCase) {
-        if (s1 == s2) {
-            return true;
-        }
-        if (s1 == null || s2 == null) {
-            return false;
-        }
-
-        final int len1 = s1.length();
-        final int len2 = s2.length();
-
-        if (len2 == 0) {
-            return len1 == 0;
-        }
-
-        int result = 0;
-        result |= len1 - len2;
-
-        // time-constant comparison
-        for (int i = 0; i < len1; i++) {
-            // If i >= len2, index2 is 0; otherwise, i.
-            final int index2 = ((i - len2) >>> 31) * i;
-            char c1 = s1.charAt(i);
-            char c2 = s2.charAt(index2);
-            if (ignoreCase) {
-                c1 = Character.toLowerCase(c1);
-                c2 = Character.toLowerCase(c2);
-            }
-            result |= c1 ^ c2;
-        }
-        return result == 0;
+        return ConstantTime.equals(s1, s2, ignoreCase);
     }
 
     /**
@@ -333,8 +308,11 @@ public abstract class DigestCredentialHandlerBase 
implements CredentialHandler {
      * @param b2 The second array to compare.
      *
      * @return <code>true</code> if the arrays are equal to each other, 
<code>false</code> otherwise.
+     *
+     * @deprecated Use {@link ConstantTime#equals(byte[], byte[])}. This 
method will be removed in Tomcat 12.
      */
+    @Deprecated
     public static boolean equals(final byte[] b1, final byte[] b2) {
-        return MessageDigest.isEqual(b1, b2);
+        return ConstantTime.equals(b1, b2);
     }
 }
diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java 
b/java/org/apache/coyote/ajp/AjpProcessor.java
index b766a4ee6d..ead435ca57 100644
--- a/java/org/apache/coyote/ajp/AjpProcessor.java
+++ b/java/org/apache/coyote/ajp/AjpProcessor.java
@@ -55,6 +55,7 @@ import org.apache.tomcat.util.net.ApplicationBufferHandler;
 import org.apache.tomcat.util.net.SSLSupport;
 import org.apache.tomcat.util.net.SocketWrapperBase;
 import org.apache.tomcat.util.res.StringManager;
+import org.apache.tomcat.util.security.ConstantTime;
 
 /**
  * AJP Processor implementation.
@@ -836,7 +837,7 @@ public class AjpProcessor extends AbstractProcessor {
                     requestHeaderMessage.getBytes(tmpMB);
                     if (secret != null && !secret.isEmpty()) {
                         secretPresentInRequest = true;
-                        if (!tmpMB.equals(secret)) {
+                        if (!ConstantTime.equals(tmpMB.getByteChunk(), 
secret)) {
                             response.setStatus(403);
                             setErrorState(ErrorState.CLOSE_CLEAN, null);
                         }
diff --git a/java/org/apache/tomcat/util/security/ConstantTime.java 
b/java/org/apache/tomcat/util/security/ConstantTime.java
new file mode 100644
index 0000000000..9a9d16b365
--- /dev/null
+++ b/java/org/apache/tomcat/util/security/ConstantTime.java
@@ -0,0 +1,142 @@
+/*
+ * 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.util.security;
+
+import java.security.MessageDigest;
+
+import org.apache.tomcat.util.buf.ByteChunk;
+
+/**
+ * Utility class for methods that, for security reasons, need to run in - as 
far as practical - constant time.
+ */
+public class ConstantTime {
+
+    private ConstantTime() {
+        // Hide default constructor for this utility class
+    }
+
+
+    /**
+     * Implements String equality which always compares all characters in the 
string, without stopping early if any
+     * characters do not match.
+     * <p>
+     * <i>Note:</i> This implementation was adapted from {@link 
MessageDigest#isEqual} which we assume is as
+     * optimizer-defeating as possible.
+     *
+     * @param s1         The first string to compare.
+     * @param s2         The second string to compare.
+     * @param ignoreCase <code>true</code> if the strings should be compared 
without regard to case. Note that "true"
+     *                       here is only guaranteed to work with plain ASCII 
characters.
+     *
+     * @return <code>true</code> if the strings are equal to each other, 
<code>false</code> otherwise.
+     */
+    public static boolean equals(final String s1, final String s2, final 
boolean ignoreCase) {
+        if (s1 == s2) {
+            return true;
+        }
+        if (s1 == null || s2 == null) {
+            return false;
+        }
+
+        final int len1 = s1.length();
+        final int len2 = s2.length();
+
+        if (len2 == 0) {
+            return len1 == 0;
+        }
+
+        int result = 0;
+        result |= len1 - len2;
+
+        // time-constant comparison
+        for (int i = 0; i < len1; i++) {
+            // If i >= len2, index2 is 0; otherwise, i.
+            final int index2 = ((i - len2) >>> 31) * i;
+            char c1 = s1.charAt(i);
+            char c2 = s2.charAt(index2);
+            if (ignoreCase) {
+                c1 = Character.toLowerCase(c1);
+                c2 = Character.toLowerCase(c2);
+            }
+            result |= c1 ^ c2;
+        }
+        return result == 0;
+    }
+
+
+    /**
+     * Implements ByteChunk / String equality which always compares all 
characters, without stopping early if any
+     * characters do not match.
+     * <p>
+     * <i>Note:</i> This implementation was adapted from {@link 
MessageDigest#isEqual} which we assume is as
+     * optimizer-defeating as possible.
+     *
+     * @param bc         The ByteChunk to compare.
+     * @param s          The string to compare.
+     *
+     * @return <code>true</code> if the strings are equal to each other, 
<code>false</code> otherwise.
+     */
+    public static boolean equals(final ByteChunk bc, final String s) {
+        if (bc == null && s == null) {
+            return true;
+        }
+        if (bc == null || s == null) {
+            return false;
+        }
+
+        final int len1 = bc.getLength();
+        final int len2 = s.length();
+
+        byte[] bytes = bc.getBytes();
+
+        if (len2 == 0) {
+            return len1 == 0;
+        }
+
+        boolean result = true;
+        result &= (len1 == len2);
+
+        // time-constant comparison
+        for (int i = 0; i < len1; i++) {
+            // If i >= len2, index2 is 0; otherwise, i.
+            final int index2 = ((i - len2) >>> 31) * i;
+            byte b = bytes[i];
+            char c = s.charAt(index2);
+            result &= (b == c);
+        }
+        return result;
+    }
+
+
+    /**
+     * Implements byte-array equality which always compares all bytes in the 
array, without stopping early if any bytes
+     * do not match.
+     * <p>
+     * <i>Note:</i> Implementation note: this method delegates to {@link 
MessageDigest#isEqual} under the assumption
+     * that it provides a constant-time comparison of the bytes in the arrays. 
Java 7+ has such an implementation, but
+     * neither the Javadoc nor any specification requires it. Therefore, 
Tomcat should continue to use <i>this</i>
+     * method internally in case the JDK implementation changes so this method 
can be re-implemented properly.
+     *
+     * @param b1 The first array to compare.
+     * @param b2 The second array to compare.
+     *
+     * @return <code>true</code> if the arrays are equal to each other, 
<code>false</code> otherwise.
+     */
+    public static boolean equals(final byte[] b1, final byte[] b2) {
+        return MessageDigest.isEqual(b1, b2);
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 3deec51653..b451df6f2a 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -231,6 +231,9 @@
         connection level window update to individual HTTP/2 streams. Based on
         <pr>996</pr> by Mike Tingey Jr. (markt)
       </fix>
+      <fix>
+        Switch AJP secret comparison to a constant time algorithm. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="WebSocket">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to