8281561: Disable http DIGEST mechanism with MD5 and SHA-1 by default

Reviewed-by: weijun, dfuchs
This commit is contained in:
Michael McMahon 2022-03-28 13:51:55 +00:00
parent 0c472c8a4f
commit 7f2a3ca289
14 changed files with 571 additions and 95 deletions
src/java.base/share
classes
java/net/doc-files
sun/net/www/protocol/http
conf/security
test/jdk

@ -224,6 +224,14 @@ of proxies.</P>
property is defined, then its value will be used as the domain
name.</P>
</OL>
<LI><P><B>{@systemProperty http.auth.digest.reEnabledAlgorithms}</B> (default: &lt;none&gt;)<BR>
By default, certain message digest algorithms are disabled for use in HTTP Digest
authentication due to their proven security limitations. This only applies to proxy
authentication and plain-text HTTP server authentication. Disabled algorithms are still
usable for HTTPS server authentication. The default list of disabled algorithms is specified
in the {@code java.security} properties file and currently comprises {@code MD5} and
{@code SHA-1}. If it is still required to use one of these algorithms, then they can be
re-enabled by setting this property to a comma separated list of the algorithm names.</P>
<LI><P><B>{@systemProperty jdk.https.negotiate.cbt}</B> (default: &lt;never&gt;)<BR>
This controls the generation and sending of TLS channel binding tokens (CBT) when Kerberos
or the Negotiate authentication scheme using Kerberos are employed over HTTPS with

@ -29,17 +29,32 @@ import java.io.*;
import java.net.PasswordAuthentication;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.Security;
import java.text.Normalizer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.function.BiConsumer;
import sun.net.NetProperties;
import sun.net.www.HeaderParser;
import sun.nio.cs.ISO_8859_1;
import sun.security.util.KnownOIDs;
import sun.util.logging.PlatformLogger;
import static sun.net.www.protocol.http.HttpURLConnection.HTTP_CONNECT;
@ -57,22 +72,67 @@ class DigestAuthentication extends AuthenticationInfo {
private String authMethod;
private static final String compatPropName = "http.auth.digest." +
private static final String propPrefix = "http.auth.digest.";
private static final String compatPropName = propPrefix +
"quoteParameters";
// Takes a set and input string containing comma separated values. converts to upper
// case, and trims each value, then applies given function to set and value
// (either add or delete element from set)
private static void processPropValue(String input,
Set<String> theSet,
BiConsumer<Set<String>,String> consumer)
{
if (input == null) {
return;
}
String[] values = input.toUpperCase(Locale.ROOT).split(",");
for (String v : values) {
consumer.accept(theSet, v.trim());
}
}
private static final String secPropName =
propPrefix + "disabledAlgorithms";
// A net property which overrides the disabled set above.
private static final String enabledAlgPropName =
propPrefix + "reEnabledAlgorithms";
// Set of disabled message digest algorithms
private static final Set<String> disabledDigests;
// true if http.auth.digest.quoteParameters Net property is true
private static final boolean delimCompatFlag;
private static final PlatformLogger logger =
HttpURLConnection.getHttpLogger();
static {
@SuppressWarnings("removal")
Boolean b = AccessController.doPrivileged(
new PrivilegedAction<>() {
public Boolean run() {
return NetProperties.getBoolean(compatPropName);
}
}
(PrivilegedAction<Boolean>) () -> NetProperties.getBoolean(compatPropName)
);
delimCompatFlag = (b == null) ? false : b.booleanValue();
@SuppressWarnings("removal")
String secprops = AccessController.doPrivileged(
(PrivilegedAction<String>) () -> Security.getProperty(secPropName)
);
Set<String> algs = new HashSet<>();
// add the default insecure algorithms to set
processPropValue(secprops, algs, (set, elem) -> set.add(elem));
@SuppressWarnings("removal")
String netprops = AccessController.doPrivileged(
(PrivilegedAction<String>) () -> NetProperties.get(enabledAlgPropName)
);
// remove any algorithms from disabled set that were opted-in by user
processPropValue(netprops, algs, (set, elem) -> set.remove(elem));
disabledDigests = Set.copyOf(algs);
}
// Authentication parameters defined in RFC2617.
@ -90,9 +150,18 @@ class DigestAuthentication extends AuthenticationInfo {
private String cnonce;
private String nonce;
private String algorithm;
// Normally same as algorithm, but excludes the -SESS suffix if present
private String digestName;
private String charset;
private int NCcount=0;
// The H(A1) string used for MD5-sess
// true if the server supports user hashing
// in which case the username returned to server
// will be H(unq(username) ":" unq(realm))
// meaning the username doesn't appear in the clear
private boolean userhash;
// The H(A1) string used for XXX-sess
private String cachedHA1;
// Force the HA1 value to be recalculated because the nonce has changed
@ -112,8 +181,10 @@ class DigestAuthentication extends AuthenticationInfo {
serverQop = false;
opaque = null;
algorithm = null;
digestName = null;
cachedHA1 = null;
nonce = null;
charset = null;
setNewCnonce();
}
@ -151,6 +222,24 @@ class DigestAuthentication extends AuthenticationInfo {
redoCachedHA1 = true;
}
synchronized boolean getUserhash() {
return userhash;
}
synchronized void setUserhash(boolean userhash) {
this.userhash = userhash;
}
synchronized Charset getCharset() {
return "UTF-8".equals(charset)
? StandardCharsets.UTF_8
: StandardCharsets.ISO_8859_1;
}
synchronized void setCharset(String charset) {
this.charset = charset;
}
synchronized void setQop (String qop) {
if (qop != null) {
String items[] = qop.split(",");
@ -191,7 +280,13 @@ class DigestAuthentication extends AuthenticationInfo {
}
synchronized String getAlgorithm () { return algorithm;}
synchronized String getDigestName () {
return digestName;
}
synchronized void setAlgorithm (String s) { algorithm=s;}
synchronized void setDigestName (String s) {
this.digestName = s;
}
}
Parameters params;
@ -309,6 +404,16 @@ class DigestAuthentication extends AuthenticationInfo {
params.setNonce (p.findValue("nonce"));
params.setOpaque (p.findValue("opaque"));
params.setQop (p.findValue("qop"));
params.setUserhash (Boolean.valueOf(p.findValue("userhash")));
String charset = p.findValue("charset");
if (charset == null) {
charset = "ISO_8859_1";
} else if (!charset.equalsIgnoreCase("UTF-8")) {
// UTF-8 is only valid value. ISO_8859_1 represents default behavior
// when the parameter is not set.
return false;
}
params.setCharset(charset.toUpperCase(Locale.ROOT));
String uri="";
String method;
@ -333,11 +438,9 @@ class DigestAuthentication extends AuthenticationInfo {
authMethod = Character.toUpperCase(authMethod.charAt(0))
+ authMethod.substring(1).toLowerCase();
}
String algorithm = p.findValue("algorithm");
if (algorithm == null || algorithm.isEmpty()) {
algorithm = "MD5"; // The default, accoriding to rfc2069
}
params.setAlgorithm (algorithm);
if (!setAlgorithmNames(p, params))
return false;
// If authQop is true, then the server is doing RFC2617 and
// has offered qop=auth. We do not support any other modes
@ -356,6 +459,38 @@ class DigestAuthentication extends AuthenticationInfo {
}
}
// Algorithm name is stored in two separate fields (of Paramaeters)
// This allows for variations in digest algorithm name (aliases)
// and also allow for the -sess variant defined in HTTP Digest protocol
// returns false if algorithm not supported
private static boolean setAlgorithmNames(HeaderParser p, Parameters params) {
String algorithm = p.findValue("algorithm");
String digestName = algorithm;
if (algorithm == null || algorithm.isEmpty()) {
algorithm = "MD5"; // The default, accoriding to rfc2069
digestName = "MD5";
} else {
algorithm = algorithm.toUpperCase(Locale.ROOT);
digestName = algorithm;
}
if (algorithm.endsWith("-SESS")) {
digestName = algorithm.substring(0, algorithm.length() - 5);
algorithm = digestName + "-sess"; // suffix lower case
}
if (digestName.equals("SHA-512-256")) {
digestName = "SHA-512/256";
}
var oid = KnownOIDs.findMatch(digestName);
if (oid == null) {
log("unknown algorithm: " + algorithm);
return false;
}
digestName = oid.stdName();
params.setAlgorithm (algorithm);
params.setDigestName (digestName);
return true;
}
/* Calculate the Authorization header field given the request URI
* and based on the authorization information in params
*/
@ -367,6 +502,14 @@ class DigestAuthentication extends AuthenticationInfo {
String cnonce = params.getCnonce ();
String nonce = params.getNonce ();
String algorithm = params.getAlgorithm ();
String digest = params.getDigestName ();
try {
validateDigest(digest);
} catch (IOException e) {
return null;
}
Charset charset = params.getCharset();
boolean userhash = params.getUserhash ();
params.incrementNC ();
int nccount = params.getNCCount ();
String ncstring=null;
@ -378,10 +521,14 @@ class DigestAuthentication extends AuthenticationInfo {
ncstring = zeroPad [len] + ncstring;
}
boolean session = algorithm.endsWith ("-sess");
try {
response = computeDigest(true, pw.getUserName(),passwd,realm,
method, uri, nonce, cnonce, ncstring);
} catch (NoSuchAlgorithmException ex) {
method, uri, nonce, cnonce, ncstring,
digest, session, charset);
} catch (CharacterCodingException | NoSuchAlgorithmException ex) {
log(ex.getMessage());
return null;
}
@ -402,11 +549,24 @@ class DigestAuthentication extends AuthenticationInfo {
qopS = ", qop=auth";
}
String user = pw.getUserName();
String userhashField = "";
try {
if (userhash) {
user = computeUserhash(digest, user, realm, charset);
userhashField = ", userhash=true";
}
} catch (CharacterCodingException | NoSuchAlgorithmException ex) {
log(ex.getMessage());
return null;
}
String value = authMethod
+ " username=\"" + pw.getUserName()
+ " username=\"" + user
+ "\", realm=\"" + realm
+ "\", nonce=\"" + nonce
+ ncfield
+ userhashField
+ ", uri=\"" + uri
+ "\", response=\"" + response + "\""
+ algoS;
@ -427,6 +587,27 @@ class DigestAuthentication extends AuthenticationInfo {
checkResponse (header, method, url.getFile());
}
private static void log(String msg) {
if (logger.isLoggable(PlatformLogger.Level.INFO)) {
logger.info(msg);
}
}
private void validateDigest(String name) throws IOException {
if (getAuthType() == AuthCacheValue.Type.Server &&
getProtocolScheme().equals("https")) {
// HTTPS server authentication can use any algorithm
return;
}
if (disabledDigests.contains(name)) {
String msg = "Rejecting digest authentication with insecure algorithm: "
+ name;
log(msg + " This constraint may be relaxed by setting " +
"the \"http.auth.digest.reEnabledAlgorithms\" system property.");
throw new IOException(msg);
}
}
public void checkResponse (String header, String method, String uri)
throws IOException {
char[] passwd = pw.getPassword();
@ -436,6 +617,9 @@ class DigestAuthentication extends AuthenticationInfo {
String cnonce = params.cnonce;
String nonce = params.getNonce ();
String algorithm = params.getAlgorithm ();
String digest = params.getDigestName ();
Charset charset = params.getCharset();
validateDigest(digest);
int nccount = params.getNCCount ();
String ncstring=null;
@ -443,15 +627,21 @@ class DigestAuthentication extends AuthenticationInfo {
throw new ProtocolException ("No authentication information in response");
}
boolean session = algorithm.endsWith ("-SESS");
if (session) {
algorithm = algorithm.substring(0, algorithm.length() - 5);
}
if (nccount != -1) {
ncstring = Integer.toHexString (nccount).toUpperCase();
ncstring = Integer.toHexString (nccount).toUpperCase(Locale.ROOT);
int len = ncstring.length();
if (len < 8)
ncstring = zeroPad [len] + ncstring;
}
try {
String expected = computeDigest(false, username,passwd,realm,
method, uri, nonce, cnonce, ncstring);
String expected = computeDigest(false, username,passwd,realm, method, uri,
nonce, cnonce, ncstring, digest,
session, charset);
HeaderParser p = new HeaderParser (header);
String rspauth = p.findValue ("rspauth");
if (rspauth == null) {
@ -468,34 +658,45 @@ class DigestAuthentication extends AuthenticationInfo {
} catch (NoSuchAlgorithmException ex) {
throw new ProtocolException ("Unsupported algorithm in response");
} catch (CharacterCodingException ex) {
throw new ProtocolException ("Invalid characters in username or password");
}
}
private String computeUserhash(String digest, String user,
String realm, Charset charset)
throws NoSuchAlgorithmException, CharacterCodingException
{
MessageDigest md = MessageDigest.getInstance(digest);
String s = user + ":" + realm;
return encode(s, null, md, charset);
}
private String computeDigest(
boolean isRequest, String userName, char[] password,
String realm, String connMethod,
String requestURI, String nonceString,
String cnonce, String ncValue
) throws NoSuchAlgorithmException
String cnonce, String ncValue,
String algorithm, boolean session,
Charset charset
) throws NoSuchAlgorithmException, CharacterCodingException
{
String A1, HashA1;
String algorithm = params.getAlgorithm ();
boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
MessageDigest md = MessageDigest.getInstance(algorithm);
if (md5sess) {
if (session) {
if ((HashA1 = params.getCachedHA1 ()) == null) {
String s = userName + ":" + realm + ":";
String s1 = encode (s, password, md);
String s1 = encode (s, password, md, charset);
A1 = s1 + ":" + nonceString + ":" + cnonce;
HashA1 = encode(A1, null, md);
HashA1 = encode(A1, null, md, charset);
params.setCachedHA1 (HashA1);
}
} else {
A1 = userName + ":" + realm + ":";
HashA1 = encode(A1, password, md);
HashA1 = encode(A1, password, md, charset);
}
String A2;
@ -504,7 +705,7 @@ class DigestAuthentication extends AuthenticationInfo {
} else {
A2 = ":" + requestURI;
}
String HashA2 = encode(A2, null, md);
String HashA2 = encode(A2, null, md, ISO_8859_1.INSTANCE);
String combo, finalHash;
if (params.authQop()) { /* RRC2617 when qop=auth */
@ -516,7 +717,7 @@ class DigestAuthentication extends AuthenticationInfo {
nonceString + ":" +
HashA2;
}
finalHash = encode(combo, null, md);
finalHash = encode(combo, null, md, ISO_8859_1.INSTANCE);
return finalHash;
}
@ -530,17 +731,28 @@ class DigestAuthentication extends AuthenticationInfo {
"00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
};
private String encode(String src, char[] passwd, MessageDigest md) {
md.update(src.getBytes(ISO_8859_1.INSTANCE));
private String encode(String src, char[] passwd, MessageDigest md, Charset charset)
throws CharacterCodingException
{
boolean isUtf8 = charset.equals(StandardCharsets.UTF_8);
if (isUtf8) {
src = Normalizer.normalize(src, Normalizer.Form.NFC);
}
md.update(src.getBytes(charset));
if (passwd != null) {
byte[] passwdBytes = new byte[passwd.length];
for (int i=0; i<passwd.length; i++)
passwdBytes[i] = (byte)passwd[i];
byte[] passwdBytes;
if (isUtf8) {
passwdBytes = getUtf8Bytes(passwd);
} else {
passwdBytes = new byte[passwd.length];
for (int i=0; i<passwd.length; i++)
passwdBytes[i] = (byte)passwd[i];
}
md.update(passwdBytes);
Arrays.fill(passwdBytes, (byte)0x00);
}
byte[] digest = md.digest();
StringBuilder res = new StringBuilder(digest.length * 2);
for (int i = 0; i < digest.length; i++) {
int hashchar = ((digest[i] >>> 4) & 0xf);
@ -550,4 +762,15 @@ class DigestAuthentication extends AuthenticationInfo {
}
return res.toString();
}
private static byte[] getUtf8Bytes(char[] passwd) throws CharacterCodingException {
CharBuffer cb = CharBuffer.wrap(passwd);
CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
ByteBuffer bb = encoder.encode(cb);
byte[] buf = new byte[bb.remaining()];
bb.get(buf);
if (bb.hasArray())
Arrays.fill(bb.array(), bb.arrayOffset(), bb.capacity(), (byte)0);
return buf;
}
}

@ -701,6 +701,16 @@ jdk.security.legacyAlgorithms=SHA1, \
jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \
DSA keySize < 1024, SHA1 denyAfter 2019-01-01
#
# Disabled message digest algorithms for use with plaintext
# HTTP Digest authentication (java.net.HttpURLConnection).
# This includes HTTPS Digest authentication to proxies.
# This may be overridden by setting the networking (or system)
# property "http.auth.digest.reEnabledAlgorithms" to a comma
# separated list of algorithms to be allowed.
#
http.auth.digest.disabledAlgorithms = MD5, SHA-1
#
# Algorithm restrictions for Secure Socket Layer/Transport Layer Security
# (SSL/TLS/DTLS) processing

@ -25,7 +25,7 @@
* @test
* @bug 4722333
* @library /test/lib
* @run main/othervm B4722333
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 B4722333
* @summary JRE Proxy Authentication Not Working with ISA2000
*/

@ -25,8 +25,8 @@
* @test
* @bug 4759514
* @library /test/lib
* @run main/othervm B4759514
* @run main/othervm -Djava.net.preferIPv6Addresses=true B4759514
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 B4759514
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 -Djava.net.preferIPv6Addresses=true B4759514
* @summary Digest Authentication is erroniously quoting the nc value, contrary to RFC 2617
*/

@ -25,8 +25,10 @@
* @test
* @bug 6870935
* @modules java.base/sun.net.www
* @run main/othervm -Dhttp.nonProxyHosts="" -Dhttp.auth.digest.validateProxy=true B6870935
* @run main/othervm -Djava.net.preferIPv6Addresses=true
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Dhttp.nonProxyHosts="" -Dhttp.auth.digest.validateProxy=true B6870935
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Djava.net.preferIPv6Addresses=true
* -Dhttp.nonProxyHosts="" -Dhttp.auth.digest.validateProxy=true B6870935
*/

@ -31,9 +31,11 @@ import jdk.test.lib.net.URIBuilder;
* @bug 8034170
* @summary Digest authentication interop issue
* @library /test/lib
* @run main/othervm B8034170 unquoted
* @run main/othervm -Dhttp.auth.digest.quoteParameters=true B8034170 quoted
* @run main/othervm -Djava.net.preferIPv6Addresses=true B8034170 unquoted
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 B8034170 unquoted
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Dhttp.auth.digest.quoteParameters=true B8034170 quoted
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Djava.net.preferIPv6Addresses=true B8034170 unquoted
*/
public class B8034170 {

@ -63,10 +63,10 @@ import java.util.stream.Stream;
* no real difference between BASICSERVER and BASIC - it should
* be transparent on the client side.
* @run main/othervm HTTPSetAuthenticatorTest NONE SERVER PROXY SERVER307 PROXY305
* @run main/othervm HTTPSetAuthenticatorTest DIGEST SERVER
* @run main/othervm HTTPSetAuthenticatorTest DIGEST PROXY
* @run main/othervm HTTPSetAuthenticatorTest DIGEST PROXY305
* @run main/othervm HTTPSetAuthenticatorTest DIGEST SERVER307
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPSetAuthenticatorTest DIGEST SERVER
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPSetAuthenticatorTest DIGEST PROXY
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPSetAuthenticatorTest DIGEST PROXY305
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPSetAuthenticatorTest DIGEST SERVER307
* @run main/othervm HTTPSetAuthenticatorTest BASIC SERVER
* @run main/othervm HTTPSetAuthenticatorTest BASIC PROXY
* @run main/othervm HTTPSetAuthenticatorTest BASIC PROXY305

@ -63,15 +63,16 @@ import static java.net.Proxy.NO_PROXY;
* server that perform Digest authentication;
* PROXY305: The server attempts to redirect
* the client to a proxy using 305 code;
* @run main/othervm HTTPTest SERVER
* @run main/othervm HTTPTest PROXY
* @run main/othervm HTTPTest SERVER307
* @run main/othervm HTTPTest PROXY305
* @run main/othervm -Dtest.debug=true -Dtest.digest.algorithm=SHA-512 HTTPTest SERVER
* @run main/othervm -Dtest.debug=true -Dtest.digest.algorithm=SHA-256 HTTPTest SERVER
* @run main/othervm -Dtest.debug=true -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPTest SERVER
* @run main/othervm -Dtest.debug=true -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPTest PROXY
* @run main/othervm -Dtest.debug=true -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPTest SERVER307
* @run main/othervm -Dtest.debug=true -Dhttp.auth.digest.reEnabledAlgorithms=MD5 HTTPTest PROXY305
*
* @author danielfuchs
*/
public class HTTPTest {
public static final boolean DEBUG =
Boolean.parseBoolean(System.getProperty("test.debug", "false"));
public static enum HttpAuthType { SERVER, PROXY, SERVER307, PROXY305 };
@ -194,6 +195,10 @@ public class HTTPTest {
// silently skip unsupported test combination
return;
}
String digestalg = System.getProperty("test.digest.algorithm");
if (digestalg == null || "".equals(digestalg))
digestalg = "MD5";
System.out.println("\n**** Testing " + protocol + " "
+ mode + " mode ****\n");
int authCount = AUTHENTICATOR.count.get();
@ -205,7 +210,9 @@ public class HTTPTest {
HTTPTestServer.create(protocol,
mode,
AUTHENTICATOR,
getHttpSchemeType());
getHttpSchemeType(),
null,
digestalg);
try {
expectedIncrement += run(server, protocol, mode);
} finally {

@ -117,12 +117,22 @@ public class HTTPTestServer extends HTTPTest {
HttpSchemeType schemeType,
HttpHandler delegate)
throws IOException {
return create(protocol, authType, auth, schemeType, null, "MD5");
}
public static HTTPTestServer create(HttpProtocolType protocol,
HttpAuthType authType,
HttpTestAuthenticator auth,
HttpSchemeType schemeType,
HttpHandler delegate,
String algorithm)
throws IOException {
Objects.requireNonNull(authType);
Objects.requireNonNull(auth);
switch(authType) {
// A server that performs Server Digest authentication.
case SERVER: return createServer(protocol, authType, auth,
schemeType, delegate, "/");
schemeType, delegate, algorithm, "/");
// A server that pretends to be a Proxy and performs
// Proxy Digest authentication. If protocol is HTTPS,
// then this will create a HttpsProxyTunnel that will
@ -327,6 +337,7 @@ public class HTTPTestServer extends HTTPTest {
HttpTestAuthenticator auth,
HttpSchemeType schemeType,
HttpHandler delegate,
String algorithm,
String path)
throws IOException {
Objects.requireNonNull(authType);
@ -336,7 +347,7 @@ public class HTTPTestServer extends HTTPTest {
final HTTPTestServer server = new HTTPTestServer(impl, null, delegate);
final HttpHandler hh = server.createHandler(schemeType, auth, authType);
HttpContext ctxt = impl.createContext(path, hh);
server.configureAuthentication(ctxt, schemeType, auth, authType);
server.configureAuthentication(ctxt, schemeType, auth, authType, algorithm);
impl.start();
return server;
}
@ -357,7 +368,7 @@ public class HTTPTestServer extends HTTPTest {
: new HTTPTestServer(impl, null, delegate);
final HttpHandler hh = server.createHandler(schemeType, auth, authType);
HttpContext ctxt = impl.createContext(path, hh);
server.configureAuthentication(ctxt, schemeType, auth, authType);
server.configureAuthentication(ctxt, schemeType, auth, authType, null);
impl.start();
return server;
@ -385,7 +396,7 @@ public class HTTPTestServer extends HTTPTest {
? createProxy(protocol, targetAuthType,
auth, schemeType, targetDelegate, "/")
: createServer(targetProtocol, targetAuthType,
auth, schemeType, targetDelegate, "/");
auth, schemeType, targetDelegate, "MD5", "/");
HttpServer impl = createHttpServer(protocol);
final HTTPTestServer redirectingServer =
new HTTPTestServer(impl, redirectTarget, null);
@ -431,11 +442,11 @@ public class HTTPTestServer extends HTTPTest {
private void configureAuthentication(HttpContext ctxt,
HttpSchemeType schemeType,
HttpTestAuthenticator auth,
HttpAuthType authType) {
HttpAuthType authType, String algorithm) {
switch(schemeType) {
case DIGEST:
// DIGEST authentication is handled by the handler.
ctxt.getFilters().add(new HttpDigestFilter(auth, authType));
ctxt.getFilters().add(new HttpDigestFilter(auth, authType, algorithm));
break;
case BASIC:
// BASIC authentication is handled by the filter.
@ -603,15 +614,21 @@ public class HTTPTestServer extends HTTPTest {
public static String computeDigest(boolean isRequest,
String reqMethod,
char[] password,
String expectedAlgorithm,
DigestResponse params)
throws NoSuchAlgorithmException
{
String A1, HashA1;
String algorithm = params.getAlgorithm("MD5");
boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
if (algorithm.endsWith("-sess")) {
algorithm = algorithm.substring(0, algorithm.length() - 5);
}
if (!algorithm.equalsIgnoreCase(expectedAlgorithm)) {
throw new IllegalArgumentException("unexpected algorithm");
}
MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
MessageDigest md = MessageDigest.getInstance(algorithm);
if (params.username == null) {
throw new IllegalArgumentException("missing username");
@ -776,13 +793,15 @@ public class HTTPTestServer extends HTTPTest {
private final HttpTestAuthenticator auth;
private final byte[] nonce;
private final String ns;
public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType) {
private final String algorithm;
public HttpDigestFilter(HttpTestAuthenticator auth, HttpAuthType authType, String algorithm) {
super(authType, authType == HttpAuthType.SERVER
? "Digest Server" : "Digest Proxy");
this.auth = auth;
nonce = new byte[16];
new Random(Instant.now().toEpochMilli()).nextBytes(nonce);
ns = new BigInteger(1, nonce).toString(16);
this.algorithm = (algorithm == null) ? "MD5" : algorithm;
}
@Override
@ -790,7 +809,7 @@ public class HTTPTestServer extends HTTPTest {
throws IOException {
he.getResponseHeaders().add(getAuthenticate(),
"Digest realm=\"" + auth.getRealm() + "\","
+ "\r\n qop=\"auth\","
+ "\r\n qop=\"auth\", " + "algorithm=\"" + algorithm + "\", "
+ "\r\n nonce=\"" + ns +"\"");
System.out.println(type + ": Requesting Digest Authentication "
+ he.getResponseHeaders().getFirst(getAuthenticate()));
@ -823,7 +842,7 @@ public class HTTPTestServer extends HTTPTest {
}
boolean validate(String reqMethod, DigestResponse dg) {
if (!"MD5".equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
if (!this.algorithm.equalsIgnoreCase(dg.getAlgorithm("MD5"))) {
System.out.println(type + ": Unsupported algorithm "
+ dg.algorithm);
return false;
@ -854,7 +873,7 @@ public class HTTPTestServer extends HTTPTest {
boolean verify(String reqMethod, DigestResponse dg, char[] pw)
throws NoSuchAlgorithmException {
String response = DigestResponse.computeDigest(true, reqMethod, pw, dg);
String response = DigestResponse.computeDigest(true, reqMethod, pw, algorithm, dg);
if (!dg.response.equals(response)) {
System.out.println(type + ": bad response returned by client: "
+ dg.response + " expected " + response);

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -24,6 +24,7 @@
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
@ -34,24 +35,51 @@ import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import static java.util.Map.entry;
/*
* @test
* @bug 8138990
* @bug 8138990 8281561
* @summary Tests for HTTP Digest auth
* The impl maintains a cache for auth info,
* the testcases run in a separate JVM to avoid cache hits
* @modules jdk.httpserver
* @run main/othervm DigestAuth good
* @run main/othervm DigestAuth only_nonce
* @run main/othervm DigestAuth sha1
* @run main/othervm DigestAuth no_header
* @run main/othervm DigestAuth no_nonce
* @run main/othervm DigestAuth no_qop
* @run main/othervm DigestAuth invalid_alg
* @run main/othervm DigestAuth validate_server
* @run main/othervm DigestAuth validate_server_no_qop
* @run main/othervm DigestAuth bad
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth good
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth only_nonce
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=SHA-1 DigestAuth sha1-good
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth sha1-bad
* @run main/othervm DigestAuth sha256
* @run main/othervm DigestAuth sha512
* @run main/othervm DigestAuth sha256-userhash
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth sha256
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth no_header
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth no_nonce
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth no_qop
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth invalid_alg
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth validate_server
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth validate_server_no_qop
*/
/*
* The sha512-256-userhash case must be run manually. It needs to run with sudo as the
* test must bind to port 80. You also need a modified JDK where
* sun.net.www.protocol.http.DigestAuthentication.getCnonce
* returns the hardcoded cnonce value below (normally it is chosen at random)
* "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"
* It can be run from the command line directly as follows:
* sudo java -Djdk.net.hosts.file=hosts DigestAuth sha512-256-userhash port80
* assuming you are running in the test source directory
*/
public class DigestAuth {
@ -88,6 +116,28 @@ public class DigestAuth {
+ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ "algorithm=\"SHA1\"";
static final String WWW_AUTH_HEADER_SHA256 = "Digest "
+ "nonce=\"a69ae8a2e17c219bc6c118b673e93601616a6a"
+ "4d8fde3a19996748d77ad0464b\", qop=\"auth\", "
+ "opaque=\"efc62777cff802cb29252f626b041f381cd360"
+ "7187115871ca25e7b51a3757e9\", algorithm=SHA-256";
static final String WWW_AUTH_HEADER_SHA512 = "Digest "
+ "nonce=\"9aaa8d3ae53b54ce653a5d52d895afcd9c0e430"
+ "a17bdf98bb34235af84fba268d31376a63e0c39079b519"
+ "c14baa0429754266f35b62a47b9c8b5d3d36c638282\","
+ " qop=\"auth\", opaque=\"28cdc6bae6c5dd7ec89dbf"
+ "af4d4f26b70f41ebbb83dc7af0950d6de016c40f412224"
+ "676cd45ebcf889a70e65a2b055a8b5232e50281272ba7c"
+ "67628cc3bb3492\", algorithm=SHA-512";
static final String WWW_AUTH_HEADER_SHA_256_UHASH = "Digest "
+ "realm=\"testrealm@host.com\", "
+ "qop=\"auth\", algorithm=SHA-256,"
+ "nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC"
+ "/RVvkK\", opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGP"
+ "ChXYjwrI2QmXDnsOS\", charset=UTF-8, userhash=true";
static final String WWW_AUTH_HEADER_INVALID_ALGORITHM = "Digest "
+ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ "algorithm=\"SHA123\"";
@ -106,23 +156,77 @@ public class DigestAuth {
+ "nc=00000001, "
+ "qop=auth";
// These two must be run manually with a modified JDK
// that generates the exact cnonce given below.
static final String SHA_512_256_FIRST = "Digest "
+ "realm=\"api@example.org\", "
+ "qop=\"auth\", "
+ "algorithm=SHA-512-256, "
+ "nonce=\"5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK\", "
+ "opaque=\"HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS\", "
+ "charset=UTF-8, "
+ "userhash=true ";
// Below taken from corrected version of RFC 7616
static final Map<String,String> SHA_512_256_EXPECTED =
Map.ofEntries(
entry("username", "793263caabb707a56211940d90411ea4a575adeccb"
+ "7e360aeb624ed06ece9b0b"),
entry("realm", "api@example.org"),
entry("uri", "/doe.json"),
entry("algorithm", "SHA-512-256"),
entry("nonce", "5TsQWLVdgBdmrQ0XsxbDODV+57QdFR34I9HAbC/RVvkK"),
entry("nc", "00000001"),
entry("cnonce", "NTg6RKcb9boFIAS3KrFK9BGeh+iDa/sm6jUMp2wds69v"),
entry("qop", "auth"),
entry("response", "3798d4131c277846293534c3edc11bd8a5e4cdcbff78"
+ "b05db9d95eeb1cec68a5"),
entry("opaque", "HRPCssKJSGjCrkzDg8OhwpzCiGPChXYjwrI2QmXDnsOS"),
entry("userhash", "true"));
public static void main(String[] args) throws Exception {
if (args.length == 0) {
throw new RuntimeException("No testcase specified");
}
String testcase = args[0];
System.out.println("Running test: " + testcase);
boolean usePort80 = args.length > 1 && args[1].equals("port80");
// start a local HTTP server
try (LocalHttpServer server = LocalHttpServer.startServer()) {
try (LocalHttpServer server = LocalHttpServer.startServer(usePort80)) {
// set authenticator
AuthenticatorImpl auth = new AuthenticatorImpl();
Authenticator.setDefault(auth);
String url = String.format("http://%s/test/", server.getAuthority());
boolean success = true;
switch (testcase) {
case "sha512-256-userhash":
auth = new AuthenticatorImpl("J\u00e4s\u00f8n Doe", "Secret, or not?");
// file based name service must be used so domain
// below resolves to localhost
if (usePort80) {
url = "http://api.example.org/doe.json";
} else {
url = "http://api.example.org:" + server.getPort() + "/doe.json";
}
server.setWWWAuthHeader(SHA_512_256_FIRST);
server.setExpectedRequestParams(SHA_512_256_EXPECTED);
success = testAuth(url, auth, EXPECT_DIGEST);
break;
case "bad":
// server returns a good WWW-Authenticate header with MD5
// but MD5 is disallowed by default
server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
success = testAuth(url, auth, EXPECT_FAILURE);
if (auth.lastRequestedPrompt == null ||
!auth.lastRequestedPrompt.equals(REALM)) {
System.out.println("Unexpected realm: "
+ auth.lastRequestedPrompt);
success = false;
}
break;
case "good":
// server returns a good WWW-Authenticate header
server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
@ -201,11 +305,36 @@ public class DigestAuth {
success = false;
}
break;
case "sha1":
case "sha1-good":
// server returns a good WWW-Authenticate header with SHA-1
server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1);
success = testAuth(url, auth, EXPECT_DIGEST);
break;
case "sha1-bad":
// server returns a WWW-Authenticate header with SHA-1
// but SHA-1 disabled
server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1);
success = testAuth(url, auth, EXPECT_FAILURE);
break;
case "sha256":
// server returns a good WWW-Authenticate header with SHA-256
server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA256);
success = testAuth(url, auth, EXPECT_DIGEST);
break;
case "sha512":
// server returns a good WWW-Authenticate header with SHA-512
server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA512);
success = testAuth(url, auth, EXPECT_DIGEST);
break;
case "sha256-userhash":
// server returns a good WWW-Authenticate header with SHA-256
// also sets the userhash=true parameter
server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA_256_UHASH);
success = testAuth(url, auth, EXPECT_DIGEST);
// make sure the userhash parameter was set correctly
// and the username itself is the correct hash
server.checkUserHash(getUserHash("SHA-256", "Mufasa", REALM));
break;
case "no_header":
// server returns no WWW-Authenticate header
success = testAuth(url, auth, EXPECT_FAILURE);
@ -251,7 +380,7 @@ public class DigestAuth {
try {
System.out.printf("Connect to %s, expected auth scheme is '%s'%n",
url, expectedScheme);
load(url);
load(url, auth);
if (expectedScheme == null) {
System.out.println("Unexpected successful connection");
@ -276,8 +405,9 @@ public class DigestAuth {
return true;
}
static void load(String url) throws IOException {
URLConnection conn = new URL(url).openConnection();
static void load(String url, Authenticator auth) throws IOException {
HttpURLConnection conn = (HttpURLConnection)(new URL(url).openConnection());
conn.setAuthenticator(auth);
conn.setUseCaches(false);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
@ -292,20 +422,47 @@ public class DigestAuth {
}
}
public static String getUserHash(String alg, String user, String realm) {
try {
MessageDigest md = MessageDigest.getInstance(alg);
String msg = user + ":" + realm;
//String msg = "Mufasa:testrealm@host.com";
byte[] output = md.digest(msg.getBytes(StandardCharsets.ISO_8859_1));
StringBuilder sb = new StringBuilder();
for (int i=0; i<output.length; i++) {
String s1 = Integer.toHexString(output[i] & 0xf);
String s2 = Integer.toHexString(Byte.toUnsignedInt(output[i]) >>> 4);
sb.append(s2).append(s1);
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static class AuthenticatorImpl extends Authenticator {
private String lastRequestedScheme;
private String lastRequestedPrompt;
private final String user, pass;
AuthenticatorImpl() {
this("Mufasa", "Circle Of Life");
}
AuthenticatorImpl(String user, String pass) {
this.user = user;
this.pass = pass;
}
@Override
public PasswordAuthentication getPasswordAuthentication() {
lastRequestedScheme = getRequestingScheme();
lastRequestedPrompt = getRequestingPrompt();
System.out.println("AuthenticatorImpl: requested "
+ lastRequestedScheme);
return new PasswordAuthentication("Mufasa",
"Circle Of Life".toCharArray());
return new PasswordAuthentication(user, pass.toCharArray());
}
}
@ -316,6 +473,9 @@ public class DigestAuth {
private volatile String wwwAuthHeader = null;
private volatile String authInfoHeader = null;
private volatile String lastRequestedNonce;
private volatile String lastRequestedUser;
private volatile String lastRequestedUserhash;
private volatile Map<String,String> expectedParams;
private LocalHttpServer(HttpServer server) {
this.server = server;
@ -335,14 +495,49 @@ public class DigestAuth {
this.wwwAuthHeader = wwwAuthHeader;
}
void setExpectedRequestParams(Map<String,String> params) {
this.expectedParams = params;
}
void setAuthInfoHeader(String authInfoHeader) {
this.authInfoHeader = authInfoHeader;
}
static LocalHttpServer startServer() throws IOException {
void checkUserHash(String expectedUser) {
boolean pass = true;
if (!expectedUser.equals(lastRequestedUser)) {
System.out.println("Username mismatch:");
System.out.println("Expected: " + expectedUser);
System.out.println("Received: " + lastRequestedUser);
pass = false;
}
if (!lastRequestedUserhash.equalsIgnoreCase("true")) {
System.out.println("Userhash mismatch:");
pass = false;
}
if (!pass) {
throw new RuntimeException("Test failed: checkUserHash");
}
}
void checkExpectedParams(String header) {
if (expectedParams == null)
return;
expectedParams.forEach((name, value) -> {
String rxValue = findParameter(header, name);
if (!rxValue.equalsIgnoreCase(value)) {
throw new RuntimeException("value mismatch "
+ "name = " + name + " (" + rxValue + "/"
+ value + ")");
}
});
}
static LocalHttpServer startServer(boolean usePort80) throws IOException {
int port = usePort80 ? 80 : 0;
InetAddress loopback = InetAddress.getLoopbackAddress();
HttpServer httpServer = HttpServer.create(
new InetSocketAddress(loopback, 0), 0);
new InetSocketAddress(loopback, port), 0);
LocalHttpServer localHttpServer = new LocalHttpServer(httpServer);
localHttpServer.start();
@ -351,6 +546,7 @@ public class DigestAuth {
void start() {
server.createContext("/test", this);
server.createContext("/", this);
server.start();
System.out.println("HttpServer: started on port " + getAuthority());
}
@ -385,7 +581,10 @@ public class DigestAuth {
t.getResponseHeaders().add("Authentication-Info",
authInfoHeader);
}
checkExpectedParams(header);
lastRequestedNonce = findParameter(header, "nonce");
lastRequestedUser = findParameter(header, "username");
lastRequestedUserhash = findParameter(header, "userhash");
byte[] output = "hello".getBytes();
t.sendResponseHeaders(200, output.length);
t.getResponseBody().write(output);

@ -0,0 +1 @@
127.0.0.1 api.example.org

@ -25,14 +25,18 @@
* @test
* @bug 4432213
* @modules java.base/sun.net.www
* @run main/othervm -Dhttp.auth.digest.validateServer=true DigestTest
* @run main/othervm -Djava.net.preferIPv6Addresses=true
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Dhttp.auth.digest.validateServer=true DigestTest
* @run main/othervm -Dhttp.auth.digest.validateServer=true
-Dtest.succeed=true DigestTest
* @run main/othervm -Djava.net.preferIPv6Addresses=true
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Djava.net.preferIPv6Addresses=true
* -Dhttp.auth.digest.validateServer=true DigestTest
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Dhttp.auth.digest.validateServer=true
-Dtest.succeed=true DigestTest
* -Dtest.succeed=true DigestTest
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Djava.net.preferIPv6Addresses=true
* -Dhttp.auth.digest.validateServer=true
* -Dtest.succeed=true DigestTest
* @summary Need to support Digest Authentication for Proxies
*/

@ -27,8 +27,9 @@
* @summary Sanity check that NTLM will not be selected by the http protocol
* handler when running on a profile that does not support NTLM
* @modules java.base/sun.net.www.protocol.http:open
* @run main/othervm NoNTLM
* @run main/othervm -Djava.net.preferIPv6Addresses=true NoNTLM
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 NoNTLM
* @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5
* -Djava.net.preferIPv6Addresses=true NoNTLM
*/
import java.io.IOException;