Author: markt
Date: Mon Aug 27 19:03:04 2012
New Revision: 1377794

URL: http://svn.apache.org/viewvc?rev=1377794&view=rev
Log:
Digest improvements:
- disable caching of authenticated user in session by default
- track server rather than client nonces
- better handling of stale nonce values

Added:
    tomcat/trunk/java/org/apache/catalina/util/ConcurrentMessageDigest.java   
(with props)
Modified:
    tomcat/trunk/java/org/apache/catalina/authenticator/DigestAuthenticator.java
    
tomcat/trunk/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java
    tomcat/trunk/webapps/docs/config/valve.xml

Modified: 
tomcat/trunk/java/org/apache/catalina/authenticator/DigestAuthenticator.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/authenticator/DigestAuthenticator.java?rev=1377794&r1=1377793&r2=1377794&view=diff
==============================================================================
--- 
tomcat/trunk/java/org/apache/catalina/authenticator/DigestAuthenticator.java 
(original)
+++ 
tomcat/trunk/java/org/apache/catalina/authenticator/DigestAuthenticator.java 
Mon Aug 27 19:03:04 2012
@@ -60,6 +60,7 @@ public class DigestAuthenticator extends
 
     public DigestAuthenticator() {
         super();
+        setCache(false);
         try {
             if (md5Helper == null) {
                 md5Helper = MessageDigest.getInstance("MD5");
@@ -81,16 +82,16 @@ public class DigestAuthenticator extends
 
 
     /**
-     * List of client nonce values currently being tracked
+     * List of server nonce values currently being tracked
      */
-    protected Map<String,NonceInfo> cnonces;
+    protected Map<String,NonceInfo> nonces;
 
 
     /**
-     * Maximum number of client nonces to keep in the cache. If not specified,
+     * Maximum number of server nonces to keep in the cache. If not specified,
      * the default value of 1000 is used.
      */
-    protected int cnonceCacheSize = 1000;
+    protected int nonceCacheSize = 1000;
 
 
     /**
@@ -120,13 +121,13 @@ public class DigestAuthenticator extends
 
     // ------------------------------------------------------------- Properties
 
-    public int getCnonceCacheSize() {
-        return cnonceCacheSize;
+    public int getNonceCacheSize() {
+        return nonceCacheSize;
     }
 
 
-    public void setCnonceCacheSize(int cnonceCacheSize) {
-        this.cnonceCacheSize = cnonceCacheSize;
+    public void setNonceCacheSize(int nonceCacheSize) {
+        this.nonceCacheSize = nonceCacheSize;
     }
 
 
@@ -231,17 +232,19 @@ public class DigestAuthenticator extends
         // Validate any credentials already included with this request
         String authorization = request.getHeader("authorization");
         DigestInfo digestInfo = new DigestInfo(getOpaque(), getNonceValidity(),
-                getKey(), cnonces, isValidateUri());
+                getKey(), nonces, isValidateUri());
         if (authorization != null) {
-            if (digestInfo.validate(request, authorization)) {
-                principal = digestInfo.authenticate(context.getRealm());
-            }
+            if (digestInfo.parse(request, authorization)) {
+                if (digestInfo.validate(request)) {
+                    principal = digestInfo.authenticate(context.getRealm());
+                }
 
-            if (principal != null) {
-                String username = digestInfo.getUsername();
-                register(request, response, principal,
-                        HttpServletRequest.DIGEST_AUTH, username, null);
-                return (true);
+                if (principal != null && !digestInfo.isNonceStale()) {
+                    register(request, response, principal,
+                            HttpServletRequest.DIGEST_AUTH,
+                            digestInfo.getUsername(), null);
+                    return true;
+                }
             }
         }
 
@@ -252,11 +255,9 @@ public class DigestAuthenticator extends
         String nonce = generateNonce(request);
 
         setAuthenticateHeader(request, response, nonce,
-                digestInfo.isNonceStale());
+                principal != null && digestInfo.isNonceStale());
         response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
-        //      hres.flushBuffer();
-        return (false);
-
+        return false;
     }
 
 
@@ -314,7 +315,14 @@ public class DigestAuthenticator extends
                     ipTimeKey.getBytes(B2CConverter.ISO_8859_1));
         }
 
-        return currentTime + ":" + MD5Encoder.encode(buffer);
+        String nonce = currentTime + ":" + MD5Encoder.encode(buffer);
+
+        NonceInfo info = new NonceInfo(currentTime, 100);
+        synchronized (nonces) {
+            nonces.put(nonce, info);
+        }
+
+        return nonce;
     }
 
 
@@ -382,7 +390,7 @@ public class DigestAuthenticator extends
             setOpaque(sessionIdGenerator.generateSessionId());
         }
 
-        cnonces = new LinkedHashMap<String, DigestAuthenticator.NonceInfo>() {
+        nonces = new LinkedHashMap<String, DigestAuthenticator.NonceInfo>() {
 
             private static final long serialVersionUID = 1L;
             private static final long LOG_SUPPRESS_TIME = 5 * 60 * 1000;
@@ -394,7 +402,7 @@ public class DigestAuthenticator extends
                     Map.Entry<String,NonceInfo> eldest) {
                 // This is called from a sync so keep it simple
                 long currentTime = System.currentTimeMillis();
-                if (size() > getCnonceCacheSize()) {
+                if (size() > getNonceCacheSize()) {
                     if (lastLog < currentTime &&
                             currentTime - eldest.getValue().getTimestamp() <
                             getNonceValidity()) {
@@ -415,7 +423,7 @@ public class DigestAuthenticator extends
         private final String opaque;
         private final long nonceValidity;
         private final String key;
-        private final Map<String,NonceInfo> cnonces;
+        private final Map<String,NonceInfo> nonces;
         private boolean validateUri = true;
 
         private String userName = null;
@@ -427,16 +435,17 @@ public class DigestAuthenticator extends
         private String cnonce = null;
         private String realmName = null;
         private String qop = null;
+        private String opaqueReceived = null;
 
         private boolean nonceStale = false;
 
 
         public DigestInfo(String opaque, long nonceValidity, String key,
-                Map<String,NonceInfo> cnonces, boolean validateUri) {
+                Map<String,NonceInfo> nonces, boolean validateUri) {
             this.opaque = opaque;
             this.nonceValidity = nonceValidity;
             this.key = key;
-            this.cnonces = cnonces;
+            this.nonces = nonces;
             this.validateUri = validateUri;
         }
 
@@ -446,7 +455,7 @@ public class DigestAuthenticator extends
         }
 
 
-        public boolean validate(Request request, String authorization) {
+        public boolean parse(Request request, String authorization) {
             // Validate the authorization credentials format
             if (authorization == null) {
                 return false;
@@ -460,7 +469,6 @@ public class DigestAuthenticator extends
             String[] tokens = 
authorization.split(",(?=(?:[^\"]*\"[^\"]*\")+$)");
 
             method = request.getMethod();
-            String opaque = null;
 
             for (int i = 0; i < tokens.length; i++) {
                 String currentToken = tokens[i];
@@ -501,10 +509,14 @@ public class DigestAuthenticator extends
                     response = removeQuotes(currentTokenValue);
                 }
                 if ("opaque".equals(currentTokenName)) {
-                    opaque = removeQuotes(currentTokenValue);
+                    opaqueReceived = removeQuotes(currentTokenValue);
                 }
             }
 
+            return true;
+        }
+
+        public boolean validate(Request request) {
             if ( (userName == null) || (realmName == null) || (nonce == null)
                  || (uri == null) || (response == null) ) {
                 return false;
@@ -547,7 +559,7 @@ public class DigestAuthenticator extends
             }
 
             // Validate the opaque string
-            if (!this.opaque.equals(opaque)) {
+            if (!opaque.equals(opaqueReceived)) {
                 return false;
             }
 
@@ -566,7 +578,9 @@ public class DigestAuthenticator extends
             long currentTime = System.currentTimeMillis();
             if ((currentTime - nonceTime) > nonceValidity) {
                 nonceStale = true;
-                return false;
+                synchronized (nonces) {
+                    nonces.remove(nonce);
+                }
             }
             String serverIpTimeKey =
                 request.getRemoteAddr() + ":" + nonceTime + ":" + key;
@@ -586,7 +600,7 @@ public class DigestAuthenticator extends
             }
 
             // Validate cnonce and nc
-            // Check if presence of nc and nonce is consistent with presence 
of qop
+            // Check if presence of nc and Cnonce is consistent with presence 
of qop
             if (qop == null) {
                 if (cnonce != null || nc != null) {
                     return false;
@@ -607,21 +621,18 @@ public class DigestAuthenticator extends
                     return false;
                 }
                 NonceInfo info;
-                synchronized (cnonces) {
-                    info = cnonces.get(cnonce);
+                synchronized (nonces) {
+                    info = nonces.get(nonce);
                 }
                 if (info == null) {
-                    info = new NonceInfo();
+                    // Nonce is valid but not in cache. It must have dropped 
out
+                    // of the cache - force a re-authentication
+                    nonceStale = true;
                 } else {
-                    if (count <= info.getCount()) {
+                    if (!info.nonceCountValid(count)) {
                         return false;
                     }
                 }
-                info.setCount(count);
-                info.setTimestamp(currentTime);
-                synchronized (cnonces) {
-                    cnonces.put(cnonce, info);
-                }
             }
             return true;
         }
@@ -648,19 +659,31 @@ public class DigestAuthenticator extends
     }
 
     private static class NonceInfo {
-        private volatile long count;
         private volatile long timestamp;
+        private volatile boolean seen[];
+        private volatile int offset;
+        private volatile int count = 0;
 
-        public void setCount(long l) {
-            count = l;
+        public NonceInfo(long currentTime, int seenWindowSize) {
+            this.timestamp = currentTime;
+            seen = new boolean[seenWindowSize];
+            offset = seenWindowSize / 2;
         }
 
-        public long getCount() {
-            return count;
-        }
-
-        public void setTimestamp(long l) {
-            timestamp = l;
+        public synchronized boolean nonceCountValid(long nonceCount) {
+            if ((count - offset) >= nonceCount ||
+                    (nonceCount > count - offset + seen.length)) {
+                return false;
+            }
+            int checkIndex = (int) ((nonceCount + offset) % seen.length);
+            if (seen[checkIndex]) {
+                return false;
+            } else {
+                seen[checkIndex] = true;
+                seen[count % seen.length] = false;
+                count++;
+                return true;
+            }
         }
 
         public long getTimestamp() {

Added: tomcat/trunk/java/org/apache/catalina/util/ConcurrentMessageDigest.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/util/ConcurrentMessageDigest.java?rev=1377794&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/util/ConcurrentMessageDigest.java 
(added)
+++ tomcat/trunk/java/org/apache/catalina/util/ConcurrentMessageDigest.java Mon 
Aug 27 19:03:04 2012
@@ -0,0 +1,90 @@
+/*
+ * 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.catalina.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * A thread safe wrapper around {@link MessageDigest} that does not make use
+ * of ThreadLocal and - broadly - only creates enough MessageDigest objects
+ * to satisfy the concurrency requirements.
+ */
+public class ConcurrentMessageDigest {
+
+    private static final Map<String,Queue<MessageDigest>> queues =
+            new HashMap<>();
+
+
+    private ConcurrentMessageDigest() {
+        // Hide default constructor for this utility class
+    }
+
+
+    public static byte[] digest(String algorithm, byte[] input) {
+
+        Queue<MessageDigest> queue = queues.get(algorithm);
+        if (queue == null) {
+            throw new IllegalStateException("Must call init() first");
+        }
+
+        MessageDigest md = queue.poll();
+        if (md == null) {
+            try {
+                md = MessageDigest.getInstance(algorithm);
+            } catch (NoSuchAlgorithmException e) {
+                // Ignore. Impossible if init() has been successfully called
+                // first.
+                throw new IllegalStateException("Must call init() first");
+            }
+        }
+
+        byte[] result = md.digest(input);
+
+        queue.add(md);
+
+        return result;
+    }
+
+
+    /**
+     * Ensures that {@link #digest(String, byte[])} and
+     * {@link #digestAsHex(String, byte[])} will support the specified
+     * algorithm. This method <b>must</b> be called and return successfully
+     * before using {@link #digest(String, byte[])} or
+     * {@link #digestAsHex(String, byte[])}.
+     *
+     * @param algorithm The message digest algorithm to be supported
+     *
+     * @throws NoSuchAlgorithmException If the algorithm is not supported by 
the
+     *                                  JVM
+     */
+    public static void init(String algorithm) throws NoSuchAlgorithmException {
+        synchronized (queues) {
+            if (!queues.containsKey(algorithm)) {
+                MessageDigest md = MessageDigest.getInstance(algorithm);
+                Queue<MessageDigest> queue = new ConcurrentLinkedQueue<>();
+                queue.add(md);
+                queues.put(algorithm, queue);
+            }
+        }
+    }
+}

Propchange: 
tomcat/trunk/java/org/apache/catalina/util/ConcurrentMessageDigest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: 
tomcat/trunk/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java?rev=1377794&r1=1377793&r2=1377794&view=diff
==============================================================================
--- 
tomcat/trunk/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java
 (original)
+++ 
tomcat/trunk/test/org/apache/catalina/authenticator/TesterDigestAuthenticatorPerformance.java
 Mon Aug 27 19:03:04 2012
@@ -17,8 +17,7 @@
 package org.apache.catalina.authenticator;
 
 import java.io.IOException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.http.HttpServletResponse;
 
@@ -33,6 +32,7 @@ import org.apache.catalina.core.Standard
 import org.apache.catalina.deploy.LoginConfig;
 import org.apache.catalina.filters.TesterResponse;
 import org.apache.catalina.startup.TestTomcat.MapRealm;
+import org.apache.catalina.util.ConcurrentMessageDigest;
 import org.apache.catalina.util.MD5Encoder;
 
 public class TesterDigestAuthenticatorPerformance {
@@ -47,6 +47,8 @@ public class TesterDigestAuthenticatorPe
     private static String REALM = "TestRealm";
     private static String QOP = "auth";
 
+    private static final AtomicInteger nonceCount = new AtomicInteger(0);
+
     private DigestAuthenticator authenticator = new DigestAuthenticator();
 
 
@@ -60,9 +62,11 @@ public class TesterDigestAuthenticatorPe
         TesterRunnable runnables[] = new TesterRunnable[threadCount];
         Thread threads[] = new Thread[threadCount];
 
+        String nonce = authenticator.generateNonce(new TesterDigestRequest());
+
         // Create the runnables & threads
         for (int i = 0; i < threadCount; i++) {
-            runnables[i] = new TesterRunnable(requestCount);
+            runnables[i] = new TesterRunnable(nonce, requestCount);
             threads[i] = new Thread(runnables[i]);
         }
 
@@ -100,6 +104,8 @@ public class TesterDigestAuthenticatorPe
     @Before
     public void setUp() throws Exception {
 
+        ConcurrentMessageDigest.init("MD5");
+
         // Configure the Realm
         MapRealm realm = new MapRealm();
         realm.addUser(USER, PWD);
@@ -118,15 +124,14 @@ public class TesterDigestAuthenticatorPe
         // Make the Context and Realm visible to the Authenticator
         authenticator.setContainer(context);
 
-        // Prevent caching of cnonces so we can the same one for all requests
-        authenticator.setCnonceCacheSize(0);
         authenticator.start();
     }
 
 
     private class TesterRunnable implements Runnable {
 
-        // Number of valid requests required
+
+        private String nonce;
         private int requestCount;
 
         private int success = 0;
@@ -136,12 +141,11 @@ public class TesterDigestAuthenticatorPe
         private HttpServletResponse response;
 
         // All init code should be in here. run() needs to be quick
-        public TesterRunnable(int requestCount) throws Exception {
+        public TesterRunnable(String nonce, int requestCount) throws Exception 
{
+            this.nonce = nonce;
             this.requestCount = requestCount;
 
             request = new TesterDigestRequest();
-            String nonce = authenticator.generateNonce(request);
-            request.setAuthHeader(buildDigestResponse(nonce));
             request.setContext(authenticator.context);
 
             response = new TesterResponse();
@@ -152,6 +156,7 @@ public class TesterDigestAuthenticatorPe
             long start = System.currentTimeMillis();
             for (int i = 0; i < requestCount; i++) {
                 try {
+                    request.setAuthHeader(buildDigestResponse(nonce));
                     if (authenticator.authenticate(request, response)) {
                         success++;
                     }
@@ -172,25 +177,25 @@ public class TesterDigestAuthenticatorPe
             return time;
         }
 
-        private String buildDigestResponse(String nonce)
-                throws NoSuchAlgorithmException {
+        private String buildDigestResponse(String nonce) {
 
-            String ncString = "00000001";
+            String ncString = String.format("%1$08x",
+                    Integer.valueOf(nonceCount.incrementAndGet()));
             String cnonce = "cnonce";
 
             String a1 = USER + ":" + REALM + ":" + PWD;
             String a2 = METHOD + ":" + CONTEXT_PATH + URI;
 
-            MessageDigest digester = MessageDigest.getInstance("MD5");
-
-            String md5a1 = MD5Encoder.encode(digester.digest(a1.getBytes()));
-            String md5a2 = MD5Encoder.encode(digester.digest(a2.getBytes()));
+            String md5a1 = MD5Encoder.encode(
+                    ConcurrentMessageDigest.digest("MD5", a1.getBytes()));
+            String md5a2 = MD5Encoder.encode(
+                    ConcurrentMessageDigest.digest("MD5", a2.getBytes()));
 
             String response = md5a1 + ":" + nonce + ":" + ncString + ":" +
                     cnonce + ":" + QOP + ":" + md5a2;
 
-            String md5response =
-                    MD5Encoder.encode(digester.digest(response.getBytes()));
+            String md5response = MD5Encoder.encode(
+                    ConcurrentMessageDigest.digest("MD5", 
response.getBytes()));
 
             StringBuilder auth = new StringBuilder();
             auth.append("Digest username=\"");
@@ -257,6 +262,5 @@ public class TesterDigestAuthenticatorPe
         public String getRequestURI() {
             return CONTEXT_PATH + URI;
         }
-
     }
 }

Modified: tomcat/trunk/webapps/docs/config/valve.xml
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/valve.xml?rev=1377794&r1=1377793&r2=1377794&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/config/valve.xml (original)
+++ tomcat/trunk/webapps/docs/config/valve.xml Mon Aug 27 19:03:04 2012
@@ -847,12 +847,6 @@
         
<strong>org.apache.catalina.authenticator.DigestAuthenticator</strong>.</p>
       </attribute>
 
-      <attribute name="cnonceCacheSize" required="false">
-        <p>To protect against replay attacks, the DIGEST authenticator tracks
-        client nonce and nonce count values. This attribute controls the size
-        of that cache. If not specified, the default value of 1000 is used.</p>
-      </attribute>
-
       <attribute name="disableProxyCaching" required="false">
         <p>Controls the caching of pages that are protected by security
         constraints. Setting this to <code>false</code> may help work around
@@ -870,6 +864,12 @@
         and/or across a cluster.</p>
       </attribute>
 
+      <attribute name="nonceCacheSize" required="false">
+        <p>To protect against replay attacks, the DIGEST authenticator tracks
+        server nonce and nonce count values. This attribute controls the size
+        of that cache. If not specified, the default value of 1000 is used.</p>
+      </attribute>
+
       <attribute name="nonceValidity" required="false">
         <p>The time, in milliseconds, that a server generated nonce will be
         considered valid for use in authentication. If not specified, the



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

Reply via email to