Skip to content

Commit

Permalink
Digest improvements:
Browse files Browse the repository at this point in the history
- disable caching of authenticated user in session by default
- track server rather than client nonces
- better handling of stale nonce values

git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@1377794 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
markt-asf committed Aug 27, 2012
1 parent 6b934d8 commit 786df62
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 77 deletions.
127 changes: 75 additions & 52 deletions java/org/apache/catalina/authenticator/DigestAuthenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public class DigestAuthenticator extends AuthenticatorBase {

public DigestAuthenticator() {
super();
setCache(false);
try {
if (md5Helper == null) {
md5Helper = MessageDigest.getInstance("MD5");
Expand All @@ -81,16 +82,16 @@ public DigestAuthenticator() {


/**
* 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;


/**
Expand Down Expand Up @@ -120,13 +121,13 @@ public DigestAuthenticator() {

// ------------------------------------------------------------- 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;
}


Expand Down Expand Up @@ -231,17 +232,19 @@ public boolean authenticate(Request request, HttpServletResponse response)
// 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;
}
}
}

Expand All @@ -252,11 +255,9 @@ public boolean authenticate(Request request, HttpServletResponse response)
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;
}


Expand Down Expand Up @@ -314,7 +315,14 @@ protected String generateNonce(Request request) {
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;
}


Expand Down Expand Up @@ -382,7 +390,7 @@ protected synchronized void startInternal() throws LifecycleException {
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;
Expand All @@ -394,7 +402,7 @@ protected boolean removeEldestEntry(
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()) {
Expand All @@ -415,7 +423,7 @@ private static class DigestInfo {
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;
Expand All @@ -427,16 +435,17 @@ private static class DigestInfo {
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;
}

Expand All @@ -446,7 +455,7 @@ public String getUsername() {
}


public boolean validate(Request request, String authorization) {
public boolean parse(Request request, String authorization) {
// Validate the authorization credentials format
if (authorization == null) {
return false;
Expand All @@ -460,7 +469,6 @@ public boolean validate(Request request, String authorization) {
String[] tokens = authorization.split(",(?=(?:[^\"]*\"[^\"]*\")+$)");

method = request.getMethod();
String opaque = null;

for (int i = 0; i < tokens.length; i++) {
String currentToken = tokens[i];
Expand Down Expand Up @@ -501,10 +509,14 @@ public boolean validate(Request request, String authorization) {
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;
Expand Down Expand Up @@ -547,7 +559,7 @@ public boolean validate(Request request, String authorization) {
}

// Validate the opaque string
if (!this.opaque.equals(opaque)) {
if (!opaque.equals(opaqueReceived)) {
return false;
}

Expand All @@ -566,7 +578,9 @@ public boolean validate(Request request, String authorization) {
long currentTime = System.currentTimeMillis();
if ((currentTime - nonceTime) > nonceValidity) {
nonceStale = true;
return false;
synchronized (nonces) {
nonces.remove(nonce);
}
}
String serverIpTimeKey =
request.getRemoteAddr() + ":" + nonceTime + ":" + key;
Expand All @@ -586,7 +600,7 @@ public boolean validate(Request request, String authorization) {
}

// 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;
Expand All @@ -607,21 +621,18 @@ public boolean validate(Request request, String authorization) {
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;
}
Expand All @@ -648,19 +659,31 @@ public Principal authenticate(Realm realm) {
}

private static class NonceInfo {
private volatile long count;
private volatile long timestamp;

public void setCount(long l) {
count = l;
private volatile boolean seen[];
private volatile int offset;
private volatile int count = 0;

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() {
Expand Down
90 changes: 90 additions & 0 deletions java/org/apache/catalina/util/ConcurrentMessageDigest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Loading

0 comments on commit 786df62

Please sign in to comment.