This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push: new 4f6be52e9a Support RFC 7616. Add support for multiple algorithms. 4f6be52e9a is described below commit 4f6be52e9a83508f465648ad3ae872c5cde7a139 Author: Mark Thomas <ma...@apache.org> AuthorDate: Fri Mar 3 17:58:05 2023 +0000 Support RFC 7616. Add support for multiple algorithms. --- java/org/apache/catalina/Realm.java | 40 +++ .../authenticator/DigestAuthenticator.java | 213 ++++++++++++---- .../catalina/authenticator/LocalStrings.properties | 2 + java/org/apache/catalina/realm/CombinedRealm.java | 4 +- .../apache/catalina/realm/JAASCallbackHandler.java | 7 +- .../catalina/realm/JAASMemoryLoginModule.java | 9 +- java/org/apache/catalina/realm/JAASRealm.java | 6 +- java/org/apache/catalina/realm/JNDIRealm.java | 4 +- .../apache/catalina/realm/LocalStrings.properties | 1 + java/org/apache/catalina/realm/LockOutRealm.java | 4 +- java/org/apache/catalina/realm/RealmBase.java | 48 +++- .../tomcat/websocket/DigestAuthenticator.java | 23 +- .../TestDigestAuthenticatorAlgorithms.java | 279 +++++++++++++++++++++ test/org/apache/catalina/realm/TestJNDIRealm.java | 6 +- webapps/docs/changelog.xml | 6 + webapps/docs/config/valve.xml | 7 + 16 files changed, 581 insertions(+), 78 deletions(-) diff --git a/java/org/apache/catalina/Realm.java b/java/org/apache/catalina/Realm.java index 1d6b35839b..e0c44714cf 100644 --- a/java/org/apache/catalina/Realm.java +++ b/java/org/apache/catalina/Realm.java @@ -101,13 +101,53 @@ public interface Realm extends Contained { * @param digestA2 Second digest calculated as digest(Method + ":" + uri) * * @return the associated principal, or {@code null} if there is none. + * + * @deprecated Unused. Use {@link #authenticate(String, String, String, + * String, String, String, String, String, String)}. Will be removed in + * Tomcat 11. */ + @Deprecated Principal authenticate(String username, String digest, String nonce, String nc, String cnonce, String qop, String realm, String digestA2); + /** + * Try to authenticate with the specified username, which + * matches the digest calculated using the given parameters using the + * method described in RFC 7616. + * <p> + * The default implementation calls {@link #authenticate(String, String, + * String, String, String, String, String, String)} for backwards + * compatibility which effectively forces the use of MD5 regardless of the + * algorithm specified in the call to this method. + * <p> + * Implementations are expected to override the default implementation and + * take account of the algorithm parameter. + * + * @param username Username of the Principal to look up + * @param digest Digest which has been submitted by the client + * @param nonce Unique (or supposedly unique) token which has been used + * for this request + * @param nc the nonce counter + * @param cnonce the client chosen nonce + * @param qop the "quality of protection" ({@code nc} and {@code cnonce} + * will only be used, if {@code qop} is not {@code null}). + * @param realm Realm name + * @param digestA2 Second digest calculated as digest(Method + ":" + uri) + * @param algorithm The message digest algorithm to use + * + * @return the associated principal, or {@code null} if there is none. + */ + default Principal authenticate(String username, String digest, + String nonce, String nc, String cnonce, + String qop, String realm, + String digestA2, String algorithm) { + return authenticate(username, digest, nonce, nc, cnonce, qop, realm, digestA2); + } + + /** * Try to authenticate using a {@link GSSContext}. * diff --git a/java/org/apache/catalina/authenticator/DigestAuthenticator.java b/java/org/apache/catalina/authenticator/DigestAuthenticator.java index 74ffdbee67..5fa8b3e69b 100644 --- a/java/org/apache/catalina/authenticator/DigestAuthenticator.java +++ b/java/org/apache/catalina/authenticator/DigestAuthenticator.java @@ -19,8 +19,14 @@ package org.apache.catalina.authenticator; import java.io.IOException; import java.io.StringReader; import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; @@ -33,12 +39,14 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.HexUtils; import org.apache.tomcat.util.buf.MessageBytes; +import org.apache.tomcat.util.buf.StringUtils; import org.apache.tomcat.util.http.parser.Authorization; import org.apache.tomcat.util.security.ConcurrentMessageDigest; /** - * An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST Authentication (see RFC 2069). + * An <b>Authenticator</b> and <b>Valve</b> implementation of HTTP DIGEST Authentication, as outlined in RFC 7616: "HTTP + * Digest Authentication" * * @author Craig R. McClanahan * @author Remy Maucherat @@ -55,6 +63,20 @@ public class DigestAuthenticator extends AuthenticatorBase { */ protected static final String QOP = "auth"; + private static final AuthDigest FALLBACK_DIGEST = AuthDigest.MD5; + + private static final String NONCE_DIGEST = "SHA-256"; + + // List permitted algorithms and maps them to Java standard names + private static final Map<String, AuthDigest> PERMITTED_ALGORITHMS = new HashMap<>(); + static { + // Allows the digester to be configured with either the Standard Java name or the name used the RFC. + for (AuthDigest authDigest : AuthDigest.values()) { + PERMITTED_ALGORITHMS.put(authDigest.getJavaName(), authDigest); + PERMITTED_ALGORITHMS.put(authDigest.getRfcName(), authDigest); + } + } + // ----------------------------------------------------------- Constructors @@ -115,6 +137,13 @@ public class DigestAuthenticator extends AuthenticatorBase { */ protected boolean validateUri = true; + + /** + * Algorithms to use for WWW-Authenticate challenges. + */ + private List<AuthDigest> algorithms = Arrays.asList(AuthDigest.SHA_256, AuthDigest.MD5); + + // ------------------------------------------------------------- Properties public int getNonceCountWindowSize() { @@ -177,6 +206,50 @@ public class DigestAuthenticator extends AuthenticatorBase { } + public String getAlgorithms() { + StringBuilder result = new StringBuilder(); + StringUtils.join(algorithms, ',', (x) -> x.getRfcName(), result); + return result.toString(); + } + + + public void setAlgorithms(String algorithmsString) { + String[] algorithmsArray = algorithmsString.split(","); + List<AuthDigest> algorithms = new ArrayList<>(); + + // Ignore the new setting if any of the algorithms are invalid + for (String algorithm : algorithmsArray) { + AuthDigest authDigest = PERMITTED_ALGORITHMS.get(algorithm); + if (authDigest == null) { + log.warn(sm.getString("digestAuthenticator.invalidAlgorithm", algorithmsString, algorithm)); + return; + } + algorithms.add(authDigest); + } + + initAlgorithms(algorithms); + this.algorithms = algorithms; + } + + + /* + * Initialise algorithms, removing ones that the JRE does not support + */ + private void initAlgorithms(List<AuthDigest> algorithms) { + Iterator<AuthDigest> algorithmIterator = algorithms.iterator(); + while (algorithmIterator.hasNext()) { + AuthDigest algorithm = algorithmIterator.next(); + try { + ConcurrentMessageDigest.init(algorithm.getJavaName()); + } catch (NoSuchAlgorithmException e) { + // In theory, a JRE can choose not to implement SHA-512/256 + log.warn(sm.getString("digestAuthenticator.unsupportedAlgorithm", algorithm.getJavaName()), e); + algorithmIterator.remove(); + } + } + } + + // --------------------------------------------------------- Public Methods /** @@ -210,7 +283,7 @@ public class DigestAuthenticator extends AuthenticatorBase { DigestInfo digestInfo = new DigestInfo(getOpaque(), getNonceValidity(), getKey(), nonces, isValidateUri()); if (authorization != null) { if (digestInfo.parse(request, authorization)) { - if (digestInfo.validate(request)) { + if (digestInfo.validate(request, algorithms)) { principal = digestInfo.authenticate(context.getRealm()); } @@ -274,8 +347,8 @@ public class DigestAuthenticator extends AuthenticatorBase { } /** - * Generate a unique token. The token is generated according to the following pattern. NOnceToken = Base64 ( MD5 ( - * client-IP ":" time-stamp ":" private-key ) ). + * Generate a unique token. The token is generated according to the following pattern. NOnceToken = Base64 ( + * NONCE_DIGEST ( client-IP ":" time-stamp ":" private-key ) ). * * @param request HTTP Servlet request * @@ -295,7 +368,8 @@ public class DigestAuthenticator extends AuthenticatorBase { String ipTimeKey = request.getRemoteAddr() + ":" + currentTime + ":" + getKey(); - byte[] buffer = ConcurrentMessageDigest.digestMD5(ipTimeKey.getBytes(StandardCharsets.ISO_8859_1)); + // Note: The digest used to generate the nonce is independent of the the digest used for authentication. + byte[] buffer = ConcurrentMessageDigest.digest(NONCE_DIGEST, ipTimeKey.getBytes(StandardCharsets.ISO_8859_1)); String nonce = currentTime + ":" + HexUtils.toHexString(buffer); NonceInfo info = new NonceInfo(currentTime, getNonceCountWindowSize()); @@ -308,26 +382,7 @@ public class DigestAuthenticator extends AuthenticatorBase { /** - * Generates the WWW-Authenticate header. - * <p> - * The header MUST follow this template : - * - * <pre> - * WWW-Authenticate = "WWW-Authenticate" ":" "Digest" - * digest-challenge - * - * digest-challenge = 1#( realm | [ domain ] | nonce | - * [ digest-opaque ] |[ stale ] | [ algorithm ] ) - * - * realm = "realm" "=" realm-value - * realm-value = quoted-string - * domain = "domain" "=" <"> 1#URI <"> - * nonce = "nonce" "=" nonce-value - * nonce-value = quoted-string - * opaque = "opaque" "=" quoted-string - * stale = "stale" "=" ( "true" | "false" ) - * algorithm = "algorithm" "=" ( "MD5" | token ) - * </pre> + * Generates the WWW-Authenticate header(s) as per RFC 7616. * * @param request HTTP Servlet request * @param response HTTP Servlet response @@ -339,17 +394,35 @@ public class DigestAuthenticator extends AuthenticatorBase { String realmName = getRealmName(context); - String authenticateHeader; - if (isNonceStale) { - authenticateHeader = "Digest realm=\"" + realmName + "\", " + "qop=\"" + QOP + "\", nonce=\"" + nonce + - "\", " + "opaque=\"" + getOpaque() + "\", stale=true"; - } else { - authenticateHeader = "Digest realm=\"" + realmName + "\", " + "qop=\"" + QOP + "\", nonce=\"" + nonce + - "\", " + "opaque=\"" + getOpaque() + "\""; - } - - response.setHeader(AUTH_HEADER_NAME, authenticateHeader); + boolean first = true; + for (AuthDigest algorithm : algorithms) { + StringBuilder authenticateHeader = new StringBuilder(200); + authenticateHeader.append("Digest realm=\""); + authenticateHeader.append(realmName); + authenticateHeader.append("\", qop=\""); + authenticateHeader.append(QOP); + authenticateHeader.append("\", nonce=\""); + authenticateHeader.append(nonce); + authenticateHeader.append("\", opaque=\""); + authenticateHeader.append(getOpaque()); + authenticateHeader.append("\""); + if (isNonceStale) { + authenticateHeader.append(", stale=true"); + } + authenticateHeader.append(", algorithm="); + authenticateHeader.append(algorithm.getRfcName()); + if (first) { + response.setHeader(AUTH_HEADER_NAME, authenticateHeader.toString()); + first = false; + } else { + response.addHeader(AUTH_HEADER_NAME, authenticateHeader.toString()); + } + /* + * Note: userhash is not supported by this implementation so don't include it. The clients will use the + * default of false. + */ + } } @@ -402,8 +475,16 @@ public class DigestAuthenticator extends AuthenticatorBase { return false; } }; + + initAlgorithms(algorithms); + try { + ConcurrentMessageDigest.init(NONCE_DIGEST); + } catch (NoSuchAlgorithmException e) { + // Not possible. NONCE_DIGEST uses an algorithm that JREs must support. + } } + public static class DigestInfo { private final String opaque; @@ -424,6 +505,7 @@ public class DigestAuthenticator extends AuthenticatorBase { private String opaqueReceived = null; private boolean nonceStale = false; + private AuthDigest algorithm = null; public DigestInfo(String opaque, long nonceValidity, String key, Map<String, NonceInfo> nonces, @@ -468,11 +550,21 @@ public class DigestAuthenticator extends AuthenticatorBase { uri = directives.get("uri"); response = directives.get("response"); opaqueReceived = directives.get("opaque"); + algorithm = PERMITTED_ALGORITHMS.get(directives.get("algorithm")); + if (algorithm == null) { + algorithm = FALLBACK_DIGEST; + } return true; } + @Deprecated public boolean validate(Request request) { + List<AuthDigest> fallbackList = Arrays.asList(FALLBACK_DIGEST); + return validate(request, fallbackList); + } + + public boolean validate(Request request, List<AuthDigest> algorithms) { if ((userName == null) || (realmName == null) || (nonce == null) || (uri == null) || (response == null)) { return false; } @@ -529,7 +621,7 @@ public class DigestAuthenticator extends AuthenticatorBase { } catch (NumberFormatException nfe) { return false; } - String md5clientIpTimeKey = nonce.substring(i + 1); + String digestclientIpTimeKey = nonce.substring(i + 1); long currentTime = System.currentTimeMillis(); if ((currentTime - nonceTime) > nonceValidity) { nonceStale = true; @@ -538,9 +630,11 @@ public class DigestAuthenticator extends AuthenticatorBase { } } String serverIpTimeKey = request.getRemoteAddr() + ":" + nonceTime + ":" + key; - byte[] buffer = ConcurrentMessageDigest.digestMD5(serverIpTimeKey.getBytes(StandardCharsets.ISO_8859_1)); - String md5ServerIpTimeKey = HexUtils.toHexString(buffer); - if (!md5ServerIpTimeKey.equals(md5clientIpTimeKey)) { + // Note: The digest used to generate the nonce is independent of the the digest used for authentication/ + byte[] buffer = ConcurrentMessageDigest.digest(NONCE_DIGEST, + serverIpTimeKey.getBytes(StandardCharsets.ISO_8859_1)); + String digestServerIpTimeKey = HexUtils.toHexString(buffer); + if (!digestServerIpTimeKey.equals(digestclientIpTimeKey)) { return false; } @@ -584,6 +678,12 @@ public class DigestAuthenticator extends AuthenticatorBase { } } } + + // Validate algorithm is one of the algorithms configured for the authenticator + if (!algorithms.contains(algorithm)) { + return false; + } + return true; } @@ -592,14 +692,14 @@ public class DigestAuthenticator extends AuthenticatorBase { } public Principal authenticate(Realm realm) { - // Second MD5 digest used to calculate the digest : - // MD5(Method + ":" + uri) String a2 = method + ":" + uri; - byte[] buffer = ConcurrentMessageDigest.digestMD5(a2.getBytes(StandardCharsets.ISO_8859_1)); + byte[] buffer = + ConcurrentMessageDigest.digest(algorithm.getJavaName(), a2.getBytes(StandardCharsets.ISO_8859_1)); String digestA2 = HexUtils.toHexString(buffer); - return realm.authenticate(userName, response, nonce, nc, cnonce, qop, realmName, digestA2); + return realm.authenticate( + userName, response, nonce, nc, cnonce, qop, realmName, digestA2, algorithm.getJavaName()); } } @@ -635,4 +735,31 @@ public class DigestAuthenticator extends AuthenticatorBase { return timestamp; } } + + + /** + * This enum exists because RFC 7616 and Java use different names for some digests. + */ + public enum AuthDigest { + + MD5("MD5", "MD5"), + SHA_256("SHA-256", "SHA-256"), + SHA_512_256("SHA-512/256", "SHA-512-256"); + + private final String javaName; + private final String rfcName; + + AuthDigest(String javaName, String rfcName) { + this.javaName = javaName; + this.rfcName = rfcName; + } + + public String getJavaName() { + return javaName; + } + + public String getRfcName() { + return rfcName; + } + } } diff --git a/java/org/apache/catalina/authenticator/LocalStrings.properties b/java/org/apache/catalina/authenticator/LocalStrings.properties index c835736860..4be5aff94f 100644 --- a/java/org/apache/catalina/authenticator/LocalStrings.properties +++ b/java/org/apache/catalina/authenticator/LocalStrings.properties @@ -35,6 +35,8 @@ authenticator.unauthorized=Cannot authenticate with the provided credentials basicAuthenticator.invalidCharset=The only permitted values are null, the empty string or UTF-8 digestAuthenticator.cacheRemove=A valid entry has been removed from client nonce cache to make room for new entries. A replay attack is now possible. To prevent the possibility of replay attacks, reduce nonceValidity or increase nonceCacheSize. Further warnings of this type will be suppressed for 5 minutes. +digestAuthenticator.invalidAlgorithm=Unable to configure DIGEST authentication to use the algorithm [{0}] as it is not permitted by RFC 7616. +digestAuthenticator.unsupportedAlgorithm=Unable to configure DIGEST authentication to use the algorithms [{0}] as [{1}] is not supported by the JRE. formAuthenticator.changeSessionIdLogin=Session ID changed before forwarding to login page during FORM authentication from [{0}] to [{1}] formAuthenticator.forwardErrorFail=Unexpected error forwarding to error page diff --git a/java/org/apache/catalina/realm/CombinedRealm.java b/java/org/apache/catalina/realm/CombinedRealm.java index 08804a29ac..69f50ab8ba 100644 --- a/java/org/apache/catalina/realm/CombinedRealm.java +++ b/java/org/apache/catalina/realm/CombinedRealm.java @@ -89,7 +89,7 @@ public class CombinedRealm extends RealmBase { @Override public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, - String qop, String realmName, String digestA2) { + String qop, String realmName, String digestA2, String algorithm) { Principal authenticatedUser = null; for (Realm realm : realms) { @@ -97,7 +97,7 @@ public class CombinedRealm extends RealmBase { log.debug(sm.getString("combinedRealm.authStart", username, realm.getClass().getName())); } - authenticatedUser = realm.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realmName, digestA2); + authenticatedUser = realm.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realmName, digestA2, algorithm); if (authenticatedUser == null) { if (log.isDebugEnabled()) { diff --git a/java/org/apache/catalina/realm/JAASCallbackHandler.java b/java/org/apache/catalina/realm/JAASCallbackHandler.java index a708befc0a..5d540b01d0 100644 --- a/java/org/apache/catalina/realm/JAASCallbackHandler.java +++ b/java/org/apache/catalina/realm/JAASCallbackHandler.java @@ -61,7 +61,7 @@ public class JAASCallbackHandler implements CallbackHandler { */ public JAASCallbackHandler(JAASRealm realm, String username, String password) { - this(realm, username, password, null, null, null, null, null, null, null); + this(realm, username, password, null, null, null, null, null, null, null, null); } @@ -77,14 +77,15 @@ public class JAASCallbackHandler implements CallbackHandler { * @param qop Quality of protection applied to the message * @param realmName Realm name * @param digestA2 Second digest calculated as digest(Method + ":" + uri) + * @param algorithm The digest algorithm to use * @param authMethod The authentication method in use */ public JAASCallbackHandler(JAASRealm realm, String username, String password, String nonce, String nc, - String cnonce, String qop, String realmName, String digestA2, String authMethod) { + String cnonce, String qop, String realmName, String digestA2, String algorithm, String authMethod) { this.realm = realm; this.username = username; - if (password != null && realm.hasMessageDigest()) { + if (password != null && realm.hasMessageDigest(algorithm)) { this.password = realm.getCredentialHandler().mutate(password); } else { this.password = password; diff --git a/java/org/apache/catalina/realm/JAASMemoryLoginModule.java b/java/org/apache/catalina/realm/JAASMemoryLoginModule.java index c1b6b863a6..35d35ce534 100644 --- a/java/org/apache/catalina/realm/JAASMemoryLoginModule.java +++ b/java/org/apache/catalina/realm/JAASMemoryLoginModule.java @@ -247,7 +247,8 @@ public class JAASMemoryLoginModule extends MemoryRealm implements LoginModule { callbacks[5] = new TextInputCallback("qop"); callbacks[6] = new TextInputCallback("realmName"); callbacks[7] = new TextInputCallback("digestA2"); - callbacks[8] = new TextInputCallback("authMethod"); + callbacks[8] = new TextInputCallback("algorithm"); + callbacks[9] = new TextInputCallback("authMethod"); // Interact with the user to retrieve the username and password String username = null; @@ -258,6 +259,7 @@ public class JAASMemoryLoginModule extends MemoryRealm implements LoginModule { String qop = null; String realmName = null; String digestA2 = null; + String algorithm = null; String authMethod = null; try { @@ -270,7 +272,8 @@ public class JAASMemoryLoginModule extends MemoryRealm implements LoginModule { qop = ((TextInputCallback) callbacks[5]).getText(); realmName = ((TextInputCallback) callbacks[6]).getText(); digestA2 = ((TextInputCallback) callbacks[7]).getText(); - authMethod = ((TextInputCallback) callbacks[8]).getText(); + algorithm = ((TextInputCallback) callbacks[8]).getText(); + authMethod = ((TextInputCallback) callbacks[9]).getText(); } catch (IOException | UnsupportedCallbackException e) { throw new LoginException(sm.getString("jaasMemoryLoginModule.callbackHandlerError", e.toString())); } @@ -280,7 +283,7 @@ public class JAASMemoryLoginModule extends MemoryRealm implements LoginModule { // BASIC or FORM principal = super.authenticate(username, password); } else if (authMethod.equals(HttpServletRequest.DIGEST_AUTH)) { - principal = super.authenticate(username, password, nonce, nc, cnonce, qop, realmName, digestA2); + principal = super.authenticate(username, password, nonce, nc, cnonce, qop, realmName, digestA2, algorithm); } else if (authMethod.equals(HttpServletRequest.CLIENT_CERT_AUTH)) { principal = super.getPrincipal(username); } else { diff --git a/java/org/apache/catalina/realm/JAASRealm.java b/java/org/apache/catalina/realm/JAASRealm.java index 6a4cf5710c..2d61082cec 100644 --- a/java/org/apache/catalina/realm/JAASRealm.java +++ b/java/org/apache/catalina/realm/JAASRealm.java @@ -315,9 +315,9 @@ public class JAASRealm extends RealmBase { @Override public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, - String qop, String realmName, String digestA2) { + String qop, String realmName, String digestA2, String algorithm) { return authenticate(username, new JAASCallbackHandler(this, username, clientDigest, nonce, nc, cnonce, qop, - realmName, digestA2, HttpServletRequest.DIGEST_AUTH)); + realmName, digestA2, algorithm, HttpServletRequest.DIGEST_AUTH)); } @@ -470,7 +470,7 @@ public class JAASRealm extends RealmBase { protected Principal getPrincipal(String username) { return authenticate(username, new JAASCallbackHandler(this, username, null, null, null, null, null, null, null, - HttpServletRequest.CLIENT_CERT_AUTH)); + null, HttpServletRequest.CLIENT_CERT_AUTH)); } diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java index 7f8cd95a33..619a704c99 100644 --- a/java/org/apache/catalina/realm/JNDIRealm.java +++ b/java/org/apache/catalina/realm/JNDIRealm.java @@ -1332,7 +1332,7 @@ public class JNDIRealm extends RealmBase { */ @Override public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, - String qop, String realm, String digestA2) { + String qop, String realm, String digestA2, String algorithm) { ClassLoader ocl = null; Thread currentThread = null; try { @@ -1341,7 +1341,7 @@ public class JNDIRealm extends RealmBase { ocl = currentThread.getContextClassLoader(); currentThread.setContextClassLoader(this.getClass().getClassLoader()); } - return super.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realm, digestA2); + return super.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realm, digestA2, algorithm); } finally { if (currentThread != null) { currentThread.setContextClassLoader(ocl); diff --git a/java/org/apache/catalina/realm/LocalStrings.properties b/java/org/apache/catalina/realm/LocalStrings.properties index 9cb35cc208..261188e404 100644 --- a/java/org/apache/catalina/realm/LocalStrings.properties +++ b/java/org/apache/catalina/realm/LocalStrings.properties @@ -106,6 +106,7 @@ realmBase.createUsernameRetriever.newInstance=Cannot create object of type [{0}] realmBase.credentialNotDelegated=Credential for user [{0}] has not been delegated though storing was requested realmBase.delegatedCredentialFail=Unable to obtain delegated credential for user [{0}] realmBase.digest=Error digesting user credentials +realmBase.digestMismatch=Unable to authenticate user as DIGEST authentication used [{0}] but password was stored in Realm using [{1}] realmBase.forbidden=Access to the requested resource has been denied realmBase.gotX509Username=Got user name from X509 certificate: [{0}] realmBase.gssContextNotEstablished=Authenticator implementation error: the passed security context is not fully established diff --git a/java/org/apache/catalina/realm/LockOutRealm.java b/java/org/apache/catalina/realm/LockOutRealm.java index 28d44e25d2..fc97ee3015 100644 --- a/java/org/apache/catalina/realm/LockOutRealm.java +++ b/java/org/apache/catalina/realm/LockOutRealm.java @@ -104,10 +104,10 @@ public class LockOutRealm extends CombinedRealm { @Override public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, - String qop, String realmName, String digestA2) { + String qop, String realmName, String digestA2, String algorithm) { Principal authenticatedUser = super.authenticate(username, clientDigest, nonce, nc, cnonce, qop, realmName, - digestA2); + digestA2, algorithm); return filterLockedAccounts(username, authenticatedUser); } diff --git a/java/org/apache/catalina/realm/RealmBase.java b/java/org/apache/catalina/realm/RealmBase.java index c9ead2ca0b..969d457769 100644 --- a/java/org/apache/catalina/realm/RealmBase.java +++ b/java/org/apache/catalina/realm/RealmBase.java @@ -328,12 +328,20 @@ public abstract class RealmBase extends LifecycleMBeanBase implements Realm { } + @Deprecated @Override public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, String qop, String realm, String digestA2) { + return authenticate(username, clientDigest, nonce, nc, cnonce, qop, realm, digestA2, "MD5"); + } + + + @Override + public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce, + String qop, String realm, String digestA2, String algorithm) { // In digest auth, digests are always lower case - String digestA1 = getDigest(username, realm); + String digestA1 = getDigest(username, realm, algorithm); if (digestA1 == null) { return null; } @@ -353,7 +361,7 @@ public abstract class RealmBase extends LifecycleMBeanBase implements Realm { uee); } - String serverDigest = HexUtils.toHexString(ConcurrentMessageDigest.digestMD5(valueBytes)); + String serverDigest = HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, valueBytes)); if (log.isDebugEnabled()) { log.debug("Digest : " + clientDigest + " Username:" + username + " ClientDigest:" + clientDigest + @@ -1004,10 +1012,17 @@ public abstract class RealmBase extends LifecycleMBeanBase implements Realm { // ------------------------------------------------------ Protected Methods - protected boolean hasMessageDigest() { + protected boolean hasMessageDigest(String algorithm) { CredentialHandler ch = credentialHandler; if (ch instanceof MessageDigestCredentialHandler) { - return ((MessageDigestCredentialHandler) ch).getAlgorithm() != null; + String realmAlgorithm = ((MessageDigestCredentialHandler) ch).getAlgorithm(); + if (realmAlgorithm != null) { + if (realmAlgorithm.equals(algorithm)) { + return true; + } else { + log.debug(sm.getString("relamBase.digestMismatch", algorithm, realmAlgorithm)); + } + } } return false; } @@ -1016,13 +1031,30 @@ public abstract class RealmBase extends LifecycleMBeanBase implements Realm { /** * Return the digest associated with given principal's user name. * - * @param username the user name - * @param realmName the realm name + * @param username The user name + * @param realmName The realm name * * @return the digest for the specified user + * + * @deprecated Unused. Use {@link #getDigest(String, String, String)}. Will be removed in Tomcat 11. */ + @Deprecated protected String getDigest(String username, String realmName) { - if (hasMessageDigest()) { + return getDigest(username, realmName, "MD5"); + } + + + /** + * Return the digest associated with given principal's user name. + * + * @param username The user name + * @param realmName The realm name + * @param algorithm The name of the message digest algorithm to use + * + * @return the digest for the specified user + */ + protected String getDigest(String username, String realmName, String algorithm) { + if (hasMessageDigest(algorithm)) { // Use pre-generated digest return getPassword(username); } @@ -1037,7 +1069,7 @@ public abstract class RealmBase extends LifecycleMBeanBase implements Realm { uee); } - return HexUtils.toHexString(ConcurrentMessageDigest.digestMD5(valueBytes)); + return HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, valueBytes)); } diff --git a/java/org/apache/tomcat/websocket/DigestAuthenticator.java b/java/org/apache/tomcat/websocket/DigestAuthenticator.java index ac9f2f7040..54d8aa579d 100644 --- a/java/org/apache/tomcat/websocket/DigestAuthenticator.java +++ b/java/org/apache/tomcat/websocket/DigestAuthenticator.java @@ -99,13 +99,19 @@ public class DigestAuthenticator extends Authenticator { private String calculateRequestDigest(String requestUri, String userName, String password, String realm, String nonce, String qop, String algorithm) throws NoSuchAlgorithmException { + boolean session = false; + if (algorithm.endsWith("-sess")) { + algorithm = algorithm.substring(0, algorithm.length() - 5); + session = true; + } + StringBuilder preDigest = new StringBuilder(); String A1; - if (algorithm.equalsIgnoreCase("MD5")) { - A1 = userName + ":" + realm + ":" + password; + if (session) { + A1 = encode(algorithm, userName + ":" + realm + ":" + password) + ":" + nonce + ":" + cNonce; } else { - A1 = encodeMD5(userName + ":" + realm + ":" + password) + ":" + nonce + ":" + cNonce; + A1 = userName + ":" + realm + ":" + password; } /* @@ -114,7 +120,7 @@ public class DigestAuthenticator extends Authenticator { */ String A2 = "GET:" + requestUri; - preDigest.append(encodeMD5(A1)); + preDigest.append(encode(algorithm, A1)); preDigest.append(':'); preDigest.append(nonce); @@ -128,15 +134,14 @@ public class DigestAuthenticator extends Authenticator { } preDigest.append(':'); - preDigest.append(encodeMD5(A2)); - - return encodeMD5(preDigest.toString()); + preDigest.append(encode(algorithm, A2)); + return encode(algorithm, preDigest.toString()); } - private String encodeMD5(String value) throws NoSuchAlgorithmException { + private String encode(String algorithm, String value) throws NoSuchAlgorithmException { byte[] bytesOfMessage = value.getBytes(StandardCharsets.ISO_8859_1); - MessageDigest md = MessageDigest.getInstance("MD5"); + MessageDigest md = MessageDigest.getInstance(algorithm); byte[] thedigest = md.digest(bytesOfMessage); return HexUtils.toHexString(thedigest); diff --git a/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java b/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java new file mode 100644 index 0000000000..5f7defbe18 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestDigestAuthenticatorAlgorithms.java @@ -0,0 +1,279 @@ +/* + * 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.authenticator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.Context; +import org.apache.catalina.authenticator.DigestAuthenticator.AuthDigest; +import org.apache.catalina.realm.LockOutRealm; +import org.apache.catalina.realm.MessageDigestCredentialHandler; +import org.apache.catalina.startup.TesterMapRealm; +import org.apache.catalina.startup.TesterServlet; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; +import org.apache.tomcat.util.buf.HexUtils; +import org.apache.tomcat.util.buf.StringUtils; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import org.apache.tomcat.util.descriptor.web.SecurityCollection; +import org.apache.tomcat.util.descriptor.web.SecurityConstraint; +import org.apache.tomcat.util.security.ConcurrentMessageDigest; + +@RunWith(Parameterized.class) +public class TestDigestAuthenticatorAlgorithms extends TomcatBaseTest { + + private static final String USER = "user"; + private static final String PASSWORD = "password"; + + private static final String URI = "/protected"; + + private static String REALM_NAME = "TestRealm"; + private static String CNONCE = "cnonce"; + + private static final List<List<AuthDigest>> ALGORITHM_PERMUTATIONS = new ArrayList<>(); + static { + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_256, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.MD5, AuthDigest.SHA_512_256, AuthDigest.SHA_256)); + + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.MD5)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.MD5, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_256, AuthDigest.SHA_512_256, AuthDigest.MD5)); + + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.MD5)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.MD5, AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.SHA_256)); + ALGORITHM_PERMUTATIONS.add(Arrays.asList(AuthDigest.SHA_512_256, AuthDigest.SHA_256, AuthDigest.MD5)); + } + + @Parameterized.Parameters(name = "{index}: Algorithms[{0}], Algorithm[{1}], PwdDigest[{2}], AuthExpected[{3}]") + public static Collection<Object[]> parameters() { + List<Object[]> parameterSets = new ArrayList<>(); + + for (List<AuthDigest> algorithmPermutation : ALGORITHM_PERMUTATIONS) { + StringBuilder algorithms = new StringBuilder(); + StringUtils.join(algorithmPermutation, ',', (x) -> x.getRfcName(), algorithms); + for (AuthDigest algorithm : AuthDigest.values()) { + boolean authExpected = algorithmPermutation.contains(algorithm); + for (Boolean digestPassword : booleans) { + String user; + if (digestPassword.booleanValue()) { + user = USER + "-" + algorithm; + } else { + user = USER; + } + parameterSets.add(new Object[] { algorithms.toString(), algorithm, digestPassword, user, Boolean.valueOf(authExpected) }); + } + } + } + + return parameterSets; + } + + @Parameter(0) + public String serverAlgorithms; + + @Parameter(1) + public AuthDigest clientAlgorithm; + + @Parameter(2) + public boolean digestPassword; + + @Parameter(3) + public String user; + + @Parameter(4) + public boolean authExpected; + + + @Test + public void testDigestAuthentication() throws Exception { + // Make sure client algorithm is available for digests + ConcurrentMessageDigest.init(clientAlgorithm.getJavaName()); + + // Configure a context with digest authentication and a single protected resource + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctxt = tomcat.addContext("", null); + + // Add protected servlet + Tomcat.addServlet(ctxt, "TesterServlet", new TesterServlet()); + ctxt.addServletMappingDecoded(URI, "TesterServlet"); + SecurityCollection collection = new SecurityCollection(); + collection.addPatternDecoded(URI); + SecurityConstraint sc = new SecurityConstraint(); + sc.addAuthRole("role"); + sc.addCollection(collection); + ctxt.addConstraint(sc); + + // Configure the Realm + TesterMapRealm realm = new TesterMapRealm(); + String password; + if (digestPassword) { + MessageDigestCredentialHandler mdch = new MessageDigestCredentialHandler(); + mdch.setAlgorithm(clientAlgorithm.getJavaName()); + mdch.setSaltLength(0); + realm.setCredentialHandler(mdch); + password = mdch.mutate(user + ":" + REALM_NAME + ":" + PASSWORD); + } else { + password = PASSWORD; + } + realm.addUser(user, password); + realm.addUserRole(user, "role"); + + LockOutRealm lockOutRealm = new LockOutRealm(); + lockOutRealm.addRealm(realm); + ctxt.setRealm(lockOutRealm); + + // Configure the authenticator + LoginConfig lc = new LoginConfig(); + lc.setAuthMethod("DIGEST"); + lc.setRealmName(REALM_NAME); + ctxt.setLoginConfig(lc); + DigestAuthenticator digestAuthenticator = new DigestAuthenticator(); + digestAuthenticator.setAlgorithms(serverAlgorithms); + ctxt.getPipeline().addValve(digestAuthenticator); + + tomcat.start(); + + // The first request will always fail - but we need the challenge + Map<String, List<String>> respHeaders = new HashMap<>(); + ByteChunk bc = new ByteChunk(); + int rc = getUrl("http://localhost:" + getPort() + URI, bc, respHeaders); + Assert.assertEquals(401, rc); + Assert.assertTrue(bc.getLength() > 0); + bc.recycle(); + + // Second request will succeed depending on client and server algorithms + List<String> auth = new ArrayList<>(); + auth.add(buildDigestResponse(user, PASSWORD, URI, REALM_NAME, clientAlgorithm, + respHeaders.get(AuthenticatorBase.AUTH_HEADER_NAME), "00000001", CNONCE, DigestAuthenticator.QOP)); + Map<String, List<String>> reqHeaders = new HashMap<>(); + reqHeaders.put("authorization", auth); + rc = getUrl("http://localhost:" + getPort() + URI, bc, reqHeaders, null); + + if (authExpected) { + Assert.assertEquals(200, rc); + Assert.assertEquals("OK", bc.toString()); + } else { + Assert.assertEquals(401, rc); + } + } + + + protected static String getNonce(String authHeader) { + int start = authHeader.indexOf("nonce=\"") + 7; + int end = authHeader.indexOf('\"', start); + return authHeader.substring(start, end); + } + + + protected static String getOpaque(String authHeader) { + int start = authHeader.indexOf("opaque=\"") + 8; + int end = authHeader.indexOf('\"', start); + return authHeader.substring(start, end); + } + + + private static String buildDigestResponse(String user, String pwd, String uri, String realm, AuthDigest algorithm, + List<String> authHeaders, String nc, String cnonce, String qop) { + + // Find auth header with correct algorithm + String nonce = null; + String opaque = null; + for (String authHeader : authHeaders) { + nonce = getNonce(authHeader); + opaque = getOpaque(authHeader); + if (authHeader.contains("algorithm=" + algorithm.getRfcName())) { + break; + } + } + if (nonce == null || opaque == null) { + Assert.fail(); + } + + String a1 = user + ":" + realm + ":" + pwd; + String a2 = "GET:" + uri; + + String digestA1 = digest(algorithm.getJavaName(), a1); + String digestA2 = digest(algorithm.getJavaName(), a2); + + String response; + if (qop == null) { + response = digestA1 + ":" + nonce + ":" + digestA2; + } else { + response = digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + digestA2; + } + + String digestResponse = digest(algorithm.getJavaName(), response); + + StringBuilder auth = new StringBuilder(); + auth.append("Digest username=\""); + auth.append(user); + auth.append("\", realm=\""); + auth.append(realm); + auth.append("\", algorithm="); + auth.append(algorithm.getRfcName()); + auth.append(", nonce=\""); + auth.append(nonce); + auth.append("\", uri=\""); + auth.append(uri); + auth.append("\", opaque=\""); + auth.append(opaque); + auth.append("\", response=\""); + auth.append(digestResponse); + auth.append("\""); + if (qop != null) { + auth.append(", qop="); + auth.append(qop); + auth.append(""); + } + if (nc != null) { + auth.append(", nc="); + auth.append(nc); + } + if (cnonce != null) { + auth.append(", cnonce=\""); + auth.append(cnonce); + auth.append("\""); + } + + return auth.toString(); + } + + private static String digest(String algorithm, String input) { + return HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, input.getBytes())); + } +} diff --git a/test/org/apache/catalina/realm/TestJNDIRealm.java b/test/org/apache/catalina/realm/TestJNDIRealm.java index 0d5cae1eff..0d974b00b6 100644 --- a/test/org/apache/catalina/realm/TestJNDIRealm.java +++ b/test/org/apache/catalina/realm/TestJNDIRealm.java @@ -74,7 +74,7 @@ public class TestJNDIRealm { String expectedResponse = HexUtils.toHexString(md5Helper.digest((digestA1() + ":" + NONCE + ":" + DIGEST_A2).getBytes())); Principal principal = - realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2); + realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2, ALGORITHM); // THEN Assert.assertNull(principal); @@ -90,7 +90,7 @@ public class TestJNDIRealm { String expectedResponse = HexUtils.toHexString(md5Helper.digest((digestA1() + ":" + NONCE + ":" + DIGEST_A2).getBytes())); Principal principal = - realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2); + realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2, ALGORITHM); // THEN assertThat(principal, instanceOf(GenericPrincipal.class)); @@ -108,7 +108,7 @@ public class TestJNDIRealm { String expectedResponse = HexUtils.toHexString(md5Helper.digest((digestA1() + ":" + NONCE + ":" + DIGEST_A2).getBytes())); Principal principal = - realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2); + realm.authenticate(USER, expectedResponse, NONCE, null, null, null, REALM, DIGEST_A2, ALGORITHM); // THEN assertThat(principal, instanceOf(GenericPrincipal.class)); diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 15dc6233a6..4d86d13214 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -136,6 +136,12 @@ Reduce the default value of <code>maxParameterCount</code> from 10,000 to 1,000. (markt) </update> + <add> + Update Digest authentication support to align with RFC 7616. This adds a + new configuration attribute, <code>algorithms</code>, to the + <code>DigestAuthenticator</code> with a default of + <code>SHA-256,MD5</code>. (markt) + </add> </changelog> </subsection> <subsection name="Coyote"> diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml index fc9204a8e5..0f09a0353e 100644 --- a/webapps/docs/config/valve.xml +++ b/webapps/docs/config/valve.xml @@ -1556,6 +1556,13 @@ <attributes> + <attribute name="algoirthms" required="false"> + <p>A comma-separated list of digest algorithms to be used for the + authentication process. Algorithms may be specified using the Java + Standard names or the names used by RFC 7616. If not specified, the + default value of <code>SHA-256,MD5</code> will be used.</p> + </attribute> + <attribute name="allowCorsPreflight" required="false"> <p>Are requests that appear to be CORS preflight requests allowed to bypass the authenticator as required by the CORS specification. The --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org