From 7da94048fae6b3922f0625c05bcdf63d5aa7bfed Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 25 Apr 2013 10:26:29 +1000 Subject: [PATCH] 406437 Digest Auth supports out of order nc --- .../authentication/DigestAuthenticator.java | 76 +++++++---- .../jetty/security/ConstraintTest.java | 129 +++++++++++++++++- 2 files changed, 178 insertions(+), 27 deletions(-) diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java index 8b588269447..761080bccea 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DigestAuthenticator.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.security.authentication; import java.io.IOException; import java.security.MessageDigest; import java.security.SecureRandom; +import java.util.BitSet; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -60,17 +61,32 @@ public class DigestAuthenticator extends LoginAuthenticator private static final Logger LOG = Log.getLogger(DigestAuthenticator.class); SecureRandom _random = new SecureRandom(); private long _maxNonceAgeMs = 60*1000; - private ConcurrentMap _nonceCount = new ConcurrentHashMap(); + private int _maxNC=1024; + private ConcurrentMap _nonceMap = new ConcurrentHashMap(); private Queue _nonceQueue = new ConcurrentLinkedQueue(); private static class Nonce { final String _nonce; final long _ts; - AtomicInteger _nc=new AtomicInteger(); - public Nonce(String nonce, long ts) + final BitSet _seen; + + public Nonce(String nonce, long ts, int size) { _nonce=nonce; _ts=ts; + _seen = new BitSet(size); + } + + public boolean seen(int count) + { + synchronized (this) + { + if (count>=_seen.size()) + return true; + boolean s=_seen.get(count); + _seen.set(count); + return s; + } } } @@ -92,19 +108,35 @@ public class DigestAuthenticator extends LoginAuthenticator String mna=configuration.getInitParameter("maxNonceAge"); if (mna!=null) { - synchronized (this) - { - _maxNonceAgeMs=Long.valueOf(mna); - } + _maxNonceAgeMs=Long.valueOf(mna); } } + + + /* ------------------------------------------------------------ */ + public int getMaxNonceCount() + { + return _maxNC; + } + + /* ------------------------------------------------------------ */ + public void setMaxNonceCount(int maxNC) + { + _maxNC = maxNC; + } /* ------------------------------------------------------------ */ - public synchronized void setMaxNonceAge(long maxNonceAgeInMillis) + public void setMaxNonceAge(long maxNonceAgeInMillis) { _maxNonceAgeMs = maxNonceAgeInMillis; } + /* ------------------------------------------------------------ */ + public long getMaxNonceAge() + { + return _maxNonceAgeMs; + } + /* ------------------------------------------------------------ */ public String getAuthMethod() { @@ -233,9 +265,9 @@ public class DigestAuthenticator extends LoginAuthenticator byte[] nounce = new byte[24]; _random.nextBytes(nounce); - nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp()); + nonce = new Nonce(new String(B64Code.encode(nounce)),request.getTimeStamp(),_maxNC); } - while (_nonceCount.putIfAbsent(nonce._nonce,nonce)!=null); + while (_nonceMap.putIfAbsent(nonce._nonce,nonce)!=null); _nonceQueue.add(nonce); return nonce._nonce; @@ -250,36 +282,27 @@ public class DigestAuthenticator extends LoginAuthenticator private int checkNonce(Digest digest, Request request) { // firstly let's expire old nonces - long expired; - synchronized (this) - { - expired = request.getTimeStamp()-_maxNonceAgeMs; - } - + long expired = request.getTimeStamp()-_maxNonceAgeMs; Nonce nonce=_nonceQueue.peek(); while (nonce!=null && nonce._tsInteger.MAX_VALUE) + if (count>=_maxNC) return 0; - int old=nonce._nc.get(); - while (!nonce._nc.compareAndSet(old,(int)count)) - old=nonce._nc.get(); - if (count<=old) + if (nonce.seen((int)count)) return -1; - return 1; } catch (Exception e) @@ -378,6 +401,7 @@ public class DigestAuthenticator extends LoginAuthenticator return false; } + @Override public String toString() { return username + "," + response; diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java index 6ed2800f6e9..474b5eafd67 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java @@ -22,17 +22,21 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.security.MessageDigest; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.eclipse.jetty.security.authentication.DigestAuthenticator; import org.eclipse.jetty.security.authentication.FormAuthenticator; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.LocalConnector; @@ -44,7 +48,10 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.util.security.Credential; import org.eclipse.jetty.util.security.Password; import org.junit.After; import org.junit.Before; @@ -222,7 +229,6 @@ public class ConstraintTest "\r\n"); assertTrue(response.startsWith("HTTP/1.1 200 OK")); - // test admin response = _connector.getResponses("GET /ctx/admin/info HTTP/1.0\r\n\r\n"); assertTrue(response.startsWith("HTTP/1.1 401 Unauthorized")); @@ -250,6 +256,127 @@ public class ConstraintTest assertTrue(response.startsWith("HTTP/1.1 200 OK")); } + + private static String CNONCE="1234567890"; + private String digest(String nonce, String username,String password,String uri,String nc) throws Exception + { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] ha1; + // calc A1 digest + md.update(username.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update("TestRealm".getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(password.getBytes(StringUtil.__ISO_8859_1)); + ha1 = md.digest(); + // calc A2 digest + md.reset(); + md.update("GET".getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(uri.getBytes(StringUtil.__ISO_8859_1)); + byte[] ha2 = md.digest(); + + // calc digest + // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" + // nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) ) + // <"> + // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) + // ) > <"> + + md.update(TypeUtil.toString(ha1, 16).getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(nonce.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(nc.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(CNONCE.getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update("auth".getBytes(StringUtil.__ISO_8859_1)); + md.update((byte) ':'); + md.update(TypeUtil.toString(ha2, 16).getBytes(StringUtil.__ISO_8859_1)); + byte[] digest = md.digest(); + + // check digest + return TypeUtil.toString(digest, 16); + } + + @Test + public void testDigest() throws Exception + { + _security.setAuthenticator(new DigestAuthenticator()); + _security.setStrict(false); + _server.start(); + + String response; + response = _connector.getResponses("GET /ctx/noauth/info HTTP/1.0\r\n\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + response = _connector.getResponses("GET /ctx/forbid/info HTTP/1.0\r\n\r\n"); + assertTrue(response.startsWith("HTTP/1.1 403 Forbidden")); + response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); + assertTrue(response.startsWith("HTTP/1.1 401 Unauthorized")); + assertTrue(response.contains("WWW-Authenticate: Digest realm=\"TestRealm\"")); + + Pattern nonceP = Pattern.compile("nonce=\"([^\"]*)\","); + Matcher matcher = nonceP.matcher(response); + assertTrue(matcher.find()); + String nonce=matcher.group(1); + + + //wrong password + String digest= digest(nonce,"user","WRONG","/ctx/auth/info","1"); + response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n" + + "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", "+ + "nc=1, "+ + "nonce=\""+nonce+"\", "+ + "response=\""+digest+"\"\r\n"+ + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 401 Unauthorized")); + + // right password + digest= digest(nonce,"user","password","/ctx/auth/info","2"); + response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n" + + "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", "+ + "nc=2, "+ + "nonce=\""+nonce+"\", "+ + "response=\""+digest+"\"\r\n"+ + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + + // once only + digest= digest(nonce,"user","password","/ctx/auth/info","2"); + response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n" + + "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", "+ + "nc=2, "+ + "nonce=\""+nonce+"\", "+ + "response=\""+digest+"\"\r\n"+ + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 401 Unauthorized")); + + // increasing + digest= digest(nonce,"user","password","/ctx/auth/info","4"); + response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n" + + "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", "+ + "nc=4, "+ + "nonce=\""+nonce+"\", "+ + "response=\""+digest+"\"\r\n"+ + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + // out of order + digest= digest(nonce,"user","password","/ctx/auth/info","3"); + response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n" + + "Authorization: Digest username=\"user\", qop=auth, cnonce=\"1234567890\", uri=\"/ctx/auth/info\", realm=\"TestRealm\", "+ + "nc=3, "+ + "nonce=\""+nonce+"\", "+ + "response=\""+digest+"\"\r\n"+ + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + } + + @Test public void testFormDispatch() throws Exception {