406437 Digest Auth supports out of order nc

This commit is contained in:
Greg Wilkins 2013-04-25 10:26:29 +10:00
parent c19642baf4
commit 7da94048fa
2 changed files with 178 additions and 27 deletions

View File

@ -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<String, Nonce> _nonceCount = new ConcurrentHashMap<String, Nonce>();
private int _maxNC=1024;
private ConcurrentMap<String, Nonce> _nonceMap = new ConcurrentHashMap<String, Nonce>();
private Queue<Nonce> _nonceQueue = new ConcurrentLinkedQueue<Nonce>();
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._ts<expired)
{
_nonceQueue.remove(nonce);
_nonceCount.remove(nonce._nonce);
_nonceMap.remove(nonce._nonce);
nonce=_nonceQueue.peek();
}
// Now check the requested nonce
try
{
nonce = _nonceCount.get(digest.nonce);
nonce = _nonceMap.get(digest.nonce);
if (nonce==null)
return 0;
long count = Long.parseLong(digest.nc,16);
if (count>Integer.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;

View File

@ -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
{