You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2022/03/17 13:41:24 UTC

[tomcat] branch main updated: Harden CredentialHandler implementations - constant-time comparisons

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

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


The following commit(s) were added to refs/heads/main by this push:
     new 594ae43  Harden CredentialHandler implementations - constant-time comparisons
594ae43 is described below

commit 594ae43b1ec6d9caf7dbafe64e665711c6be8673
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Thu Mar 17 13:36:20 2022 +0000

    Harden CredentialHandler implementations - constant-time comparisons
    
    Based on a patch proposed by schultz
---
 .../realm/DigestCredentialHandlerBase.java         | 75 +++++++++++++++++++++-
 .../realm/MessageDigestCredentialHandler.java      |  5 +-
 webapps/docs/changelog.xml                         |  4 ++
 3 files changed, 81 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java b/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java
index b6a33d5..122adef 100644
--- a/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java
+++ b/java/org/apache/catalina/realm/DigestCredentialHandlerBase.java
@@ -16,6 +16,7 @@
  */
 package org.apache.catalina.realm;
 
+import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.util.Random;
@@ -200,7 +201,7 @@ public abstract class DigestCredentialHandlerBase implements CredentialHandler {
             return false;
         }
 
-        return storedHexEncoded.equalsIgnoreCase(inputHexEncoded);
+        return DigestCredentialHandlerBase.equals(storedHexEncoded, inputHexEncoded, true);
     }
 
 
@@ -292,4 +293,76 @@ public abstract class DigestCredentialHandlerBase implements CredentialHandler {
      * @return the logger for the CredentialHandler instance.
      */
     protected abstract Log getLog();
+
+    /**
+     * Implements String equality which always compares all characters in the
+     * string, without stopping early if any characters do not match.
+     *
+     * @implNote
+     * 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 byte-array equality which always compares all bytes in the
+     * array, without stopping early if any bytes do not match.
+     *
+     * @implNote
+     * 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/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java b/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java
index 616e0e5..d43465c 100644
--- a/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java
+++ b/java/org/apache/catalina/realm/MessageDigestCredentialHandler.java
@@ -100,7 +100,7 @@ public class MessageDigestCredentialHandler extends DigestCredentialHandlerBase
 
         if (getAlgorithm() == null) {
             // No digests, compare directly
-            return storedCredentials.equals(inputCredentials);
+            return DigestCredentialHandlerBase.equals(inputCredentials, storedCredentials, false);
         } else {
             // Some directories and databases prefix the password with the hash
             // type. The string is in a format compatible with Base64.encode not
@@ -112,7 +112,8 @@ public class MessageDigestCredentialHandler extends DigestCredentialHandlerBase
                 byte[] userDigest = ConcurrentMessageDigest.digest(
                         getAlgorithm(), inputCredentials.getBytes(StandardCharsets.ISO_8859_1));
                 String base64UserDigest = Base64.encodeBase64String(userDigest);
-                return base64UserDigest.equals(base64ServerDigest);
+
+                return DigestCredentialHandlerBase.equals(base64UserDigest, base64ServerDigest, false);
             } else if (storedCredentials.startsWith("{SSHA}")) {
                 // "{SSHA}<sha-1 digest:20><salt:n>"
                 // Need to convert the salt to bytes to apply it to the user's
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 46b2f43..64ff59b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -112,6 +112,10 @@
         renamed for Jakarta EE 10) including the implementation of the new
         methods on <code>AuthConfigFactory</code>. (markt)
       </scode>
+      <scode>
+        Harden the CredentialHandler implementations by switching to a
+        constant-time implementation for credential comparisons. (schultz/markt)
+      </scode>
     </changelog>
   </subsection>
   <subsection name="Web applications">

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org