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

View file

@ -224,6 +224,14 @@ of proxies.</P>
property is defined, then its value will be used as the domain property is defined, then its value will be used as the domain
name.</P> name.</P>
</OL> </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> <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 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 or the Negotiate authentication scheme using Kerberos are employed over HTTPS with

View file

@ -29,17 +29,32 @@ import java.io.*;
import java.net.PasswordAuthentication; import java.net.PasswordAuthentication;
import java.net.ProtocolException; import java.net.ProtocolException;
import java.net.URL; 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.AccessController;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.security.Security;
import java.text.Normalizer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.function.BiConsumer;
import sun.net.NetProperties; import sun.net.NetProperties;
import sun.net.www.HeaderParser; import sun.net.www.HeaderParser;
import sun.nio.cs.ISO_8859_1; 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; import static sun.net.www.protocol.http.HttpURLConnection.HTTP_CONNECT;
@ -57,22 +72,67 @@ class DigestAuthentication extends AuthenticationInfo {
private String authMethod; 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"; "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 // true if http.auth.digest.quoteParameters Net property is true
private static final boolean delimCompatFlag; private static final boolean delimCompatFlag;
private static final PlatformLogger logger =
HttpURLConnection.getHttpLogger();
static { static {
@SuppressWarnings("removal") @SuppressWarnings("removal")
Boolean b = AccessController.doPrivileged( Boolean b = AccessController.doPrivileged(
new PrivilegedAction<>() { (PrivilegedAction<Boolean>) () -> NetProperties.getBoolean(compatPropName)
public Boolean run() {
return NetProperties.getBoolean(compatPropName);
}
}
); );
delimCompatFlag = (b == null) ? false : b.booleanValue(); 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. // Authentication parameters defined in RFC2617.
@ -90,9 +150,18 @@ class DigestAuthentication extends AuthenticationInfo {
private String cnonce; private String cnonce;
private String nonce; private String nonce;
private String algorithm; private String algorithm;
// Normally same as algorithm, but excludes the -SESS suffix if present
private String digestName;
private String charset;
private int NCcount=0; 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; private String cachedHA1;
// Force the HA1 value to be recalculated because the nonce has changed // Force the HA1 value to be recalculated because the nonce has changed
@ -112,8 +181,10 @@ class DigestAuthentication extends AuthenticationInfo {
serverQop = false; serverQop = false;
opaque = null; opaque = null;
algorithm = null; algorithm = null;
digestName = null;
cachedHA1 = null; cachedHA1 = null;
nonce = null; nonce = null;
charset = null;
setNewCnonce(); setNewCnonce();
} }
@ -151,6 +222,24 @@ class DigestAuthentication extends AuthenticationInfo {
redoCachedHA1 = true; 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) { synchronized void setQop (String qop) {
if (qop != null) { if (qop != null) {
String items[] = qop.split(","); String items[] = qop.split(",");
@ -191,7 +280,13 @@ class DigestAuthentication extends AuthenticationInfo {
} }
synchronized String getAlgorithm () { return algorithm;} synchronized String getAlgorithm () { return algorithm;}
synchronized String getDigestName () {
return digestName;
}
synchronized void setAlgorithm (String s) { algorithm=s;} synchronized void setAlgorithm (String s) { algorithm=s;}
synchronized void setDigestName (String s) {
this.digestName = s;
}
} }
Parameters params; Parameters params;
@ -309,6 +404,16 @@ class DigestAuthentication extends AuthenticationInfo {
params.setNonce (p.findValue("nonce")); params.setNonce (p.findValue("nonce"));
params.setOpaque (p.findValue("opaque")); params.setOpaque (p.findValue("opaque"));
params.setQop (p.findValue("qop")); 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 uri="";
String method; String method;
@ -333,11 +438,9 @@ class DigestAuthentication extends AuthenticationInfo {
authMethod = Character.toUpperCase(authMethod.charAt(0)) authMethod = Character.toUpperCase(authMethod.charAt(0))
+ authMethod.substring(1).toLowerCase(); + authMethod.substring(1).toLowerCase();
} }
String algorithm = p.findValue("algorithm");
if (algorithm == null || algorithm.isEmpty()) { if (!setAlgorithmNames(p, params))
algorithm = "MD5"; // The default, accoriding to rfc2069 return false;
}
params.setAlgorithm (algorithm);
// If authQop is true, then the server is doing RFC2617 and // If authQop is true, then the server is doing RFC2617 and
// has offered qop=auth. We do not support any other modes // 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 /* Calculate the Authorization header field given the request URI
* and based on the authorization information in params * and based on the authorization information in params
*/ */
@ -367,6 +502,14 @@ class DigestAuthentication extends AuthenticationInfo {
String cnonce = params.getCnonce (); String cnonce = params.getCnonce ();
String nonce = params.getNonce (); String nonce = params.getNonce ();
String algorithm = params.getAlgorithm (); 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 (); params.incrementNC ();
int nccount = params.getNCCount (); int nccount = params.getNCCount ();
String ncstring=null; String ncstring=null;
@ -378,10 +521,14 @@ class DigestAuthentication extends AuthenticationInfo {
ncstring = zeroPad [len] + ncstring; ncstring = zeroPad [len] + ncstring;
} }
boolean session = algorithm.endsWith ("-sess");
try { try {
response = computeDigest(true, pw.getUserName(),passwd,realm, response = computeDigest(true, pw.getUserName(),passwd,realm,
method, uri, nonce, cnonce, ncstring); method, uri, nonce, cnonce, ncstring,
} catch (NoSuchAlgorithmException ex) { digest, session, charset);
} catch (CharacterCodingException | NoSuchAlgorithmException ex) {
log(ex.getMessage());
return null; return null;
} }
@ -402,11 +549,24 @@ class DigestAuthentication extends AuthenticationInfo {
qopS = ", qop=auth"; 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 String value = authMethod
+ " username=\"" + pw.getUserName() + " username=\"" + user
+ "\", realm=\"" + realm + "\", realm=\"" + realm
+ "\", nonce=\"" + nonce + "\", nonce=\"" + nonce
+ ncfield + ncfield
+ userhashField
+ ", uri=\"" + uri + ", uri=\"" + uri
+ "\", response=\"" + response + "\"" + "\", response=\"" + response + "\""
+ algoS; + algoS;
@ -427,6 +587,27 @@ class DigestAuthentication extends AuthenticationInfo {
checkResponse (header, method, url.getFile()); 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) public void checkResponse (String header, String method, String uri)
throws IOException { throws IOException {
char[] passwd = pw.getPassword(); char[] passwd = pw.getPassword();
@ -436,6 +617,9 @@ class DigestAuthentication extends AuthenticationInfo {
String cnonce = params.cnonce; String cnonce = params.cnonce;
String nonce = params.getNonce (); String nonce = params.getNonce ();
String algorithm = params.getAlgorithm (); String algorithm = params.getAlgorithm ();
String digest = params.getDigestName ();
Charset charset = params.getCharset();
validateDigest(digest);
int nccount = params.getNCCount (); int nccount = params.getNCCount ();
String ncstring=null; String ncstring=null;
@ -443,15 +627,21 @@ class DigestAuthentication extends AuthenticationInfo {
throw new ProtocolException ("No authentication information in response"); 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) { if (nccount != -1) {
ncstring = Integer.toHexString (nccount).toUpperCase(); ncstring = Integer.toHexString (nccount).toUpperCase(Locale.ROOT);
int len = ncstring.length(); int len = ncstring.length();
if (len < 8) if (len < 8)
ncstring = zeroPad [len] + ncstring; ncstring = zeroPad [len] + ncstring;
} }
try { try {
String expected = computeDigest(false, username,passwd,realm, String expected = computeDigest(false, username,passwd,realm, method, uri,
method, uri, nonce, cnonce, ncstring); nonce, cnonce, ncstring, digest,
session, charset);
HeaderParser p = new HeaderParser (header); HeaderParser p = new HeaderParser (header);
String rspauth = p.findValue ("rspauth"); String rspauth = p.findValue ("rspauth");
if (rspauth == null) { if (rspauth == null) {
@ -468,34 +658,45 @@ class DigestAuthentication extends AuthenticationInfo {
} catch (NoSuchAlgorithmException ex) { } catch (NoSuchAlgorithmException ex) {
throw new ProtocolException ("Unsupported algorithm in response"); 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( private String computeDigest(
boolean isRequest, String userName, char[] password, boolean isRequest, String userName, char[] password,
String realm, String connMethod, String realm, String connMethod,
String requestURI, String nonceString, String requestURI, String nonceString,
String cnonce, String ncValue String cnonce, String ncValue,
) throws NoSuchAlgorithmException String algorithm, boolean session,
Charset charset
) throws NoSuchAlgorithmException, CharacterCodingException
{ {
String A1, HashA1; 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) { if ((HashA1 = params.getCachedHA1 ()) == null) {
String s = userName + ":" + realm + ":"; String s = userName + ":" + realm + ":";
String s1 = encode (s, password, md); String s1 = encode (s, password, md, charset);
A1 = s1 + ":" + nonceString + ":" + cnonce; A1 = s1 + ":" + nonceString + ":" + cnonce;
HashA1 = encode(A1, null, md); HashA1 = encode(A1, null, md, charset);
params.setCachedHA1 (HashA1); params.setCachedHA1 (HashA1);
} }
} else { } else {
A1 = userName + ":" + realm + ":"; A1 = userName + ":" + realm + ":";
HashA1 = encode(A1, password, md); HashA1 = encode(A1, password, md, charset);
} }
String A2; String A2;
@ -504,7 +705,7 @@ class DigestAuthentication extends AuthenticationInfo {
} else { } else {
A2 = ":" + requestURI; A2 = ":" + requestURI;
} }
String HashA2 = encode(A2, null, md); String HashA2 = encode(A2, null, md, ISO_8859_1.INSTANCE);
String combo, finalHash; String combo, finalHash;
if (params.authQop()) { /* RRC2617 when qop=auth */ if (params.authQop()) { /* RRC2617 when qop=auth */
@ -516,7 +717,7 @@ class DigestAuthentication extends AuthenticationInfo {
nonceString + ":" + nonceString + ":" +
HashA2; HashA2;
} }
finalHash = encode(combo, null, md); finalHash = encode(combo, null, md, ISO_8859_1.INSTANCE);
return finalHash; return finalHash;
} }
@ -530,17 +731,28 @@ class DigestAuthentication extends AuthenticationInfo {
"00000000", "0000000", "000000", "00000", "0000", "000", "00", "0" "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
}; };
private String encode(String src, char[] passwd, MessageDigest md) { private String encode(String src, char[] passwd, MessageDigest md, Charset charset)
md.update(src.getBytes(ISO_8859_1.INSTANCE)); 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) { if (passwd != null) {
byte[] passwdBytes = new byte[passwd.length]; byte[] passwdBytes;
for (int i=0; i<passwd.length; i++) if (isUtf8) {
passwdBytes[i] = (byte)passwd[i]; 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); md.update(passwdBytes);
Arrays.fill(passwdBytes, (byte)0x00); Arrays.fill(passwdBytes, (byte)0x00);
} }
byte[] digest = md.digest(); byte[] digest = md.digest();
StringBuilder res = new StringBuilder(digest.length * 2); StringBuilder res = new StringBuilder(digest.length * 2);
for (int i = 0; i < digest.length; i++) { for (int i = 0; i < digest.length; i++) {
int hashchar = ((digest[i] >>> 4) & 0xf); int hashchar = ((digest[i] >>> 4) & 0xf);
@ -550,4 +762,15 @@ class DigestAuthentication extends AuthenticationInfo {
} }
return res.toString(); 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;
}
} }

View file

@ -701,6 +701,16 @@ jdk.security.legacyAlgorithms=SHA1, \
jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \ jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \
DSA keySize < 1024, SHA1 denyAfter 2019-01-01 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 # Algorithm restrictions for Secure Socket Layer/Transport Layer Security
# (SSL/TLS/DTLS) processing # (SSL/TLS/DTLS) processing

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * 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.HttpExchange;
import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.IOException; import java.io.IOException;
@ -34,24 +35,51 @@ import java.net.InetSocketAddress;
import java.net.PasswordAuthentication; import java.net.PasswordAuthentication;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; 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.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 * @test
* @bug 8138990 * @bug 8138990 8281561
* @summary Tests for HTTP Digest auth * @summary Tests for HTTP Digest auth
* The impl maintains a cache for auth info, * The impl maintains a cache for auth info,
* the testcases run in a separate JVM to avoid cache hits * the testcases run in a separate JVM to avoid cache hits
* @modules jdk.httpserver * @modules jdk.httpserver
* @run main/othervm DigestAuth good * @run main/othervm DigestAuth bad
* @run main/othervm DigestAuth only_nonce * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth good
* @run main/othervm DigestAuth sha1 * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth only_nonce
* @run main/othervm DigestAuth no_header * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=SHA-1 DigestAuth sha1-good
* @run main/othervm DigestAuth no_nonce * @run main/othervm -Dhttp.auth.digest.reEnabledAlgorithms=MD5 DigestAuth sha1-bad
* @run main/othervm DigestAuth no_qop * @run main/othervm DigestAuth sha256
* @run main/othervm DigestAuth invalid_alg * @run main/othervm DigestAuth sha512
* @run main/othervm DigestAuth validate_server * @run main/othervm DigestAuth sha256-userhash
* @run main/othervm DigestAuth validate_server_no_qop * @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 { public class DigestAuth {
@ -88,6 +116,28 @@ public class DigestAuth {
+ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ "algorithm=\"SHA1\""; + "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 " static final String WWW_AUTH_HEADER_INVALID_ALGORITHM = "Digest "
+ "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ "algorithm=\"SHA123\""; + "algorithm=\"SHA123\"";
@ -106,23 +156,77 @@ public class DigestAuth {
+ "nc=00000001, " + "nc=00000001, "
+ "qop=auth"; + "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 { public static void main(String[] args) throws Exception {
if (args.length == 0) { if (args.length == 0) {
throw new RuntimeException("No testcase specified"); throw new RuntimeException("No testcase specified");
} }
String testcase = args[0]; String testcase = args[0];
System.out.println("Running test: " + testcase);
boolean usePort80 = args.length > 1 && args[1].equals("port80");
// start a local HTTP server // start a local HTTP server
try (LocalHttpServer server = LocalHttpServer.startServer()) { try (LocalHttpServer server = LocalHttpServer.startServer(usePort80)) {
// set authenticator // set authenticator
AuthenticatorImpl auth = new AuthenticatorImpl(); AuthenticatorImpl auth = new AuthenticatorImpl();
Authenticator.setDefault(auth);
String url = String.format("http://%s/test/", server.getAuthority()); String url = String.format("http://%s/test/", server.getAuthority());
boolean success = true; boolean success = true;
switch (testcase) { 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": case "good":
// server returns a good WWW-Authenticate header // server returns a good WWW-Authenticate header
server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER); server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER);
@ -201,11 +305,36 @@ public class DigestAuth {
success = false; success = false;
} }
break; break;
case "sha1": case "sha1-good":
// server returns a good WWW-Authenticate header with SHA-1 // server returns a good WWW-Authenticate header with SHA-1
server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1); server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1);
success = testAuth(url, auth, EXPECT_DIGEST); success = testAuth(url, auth, EXPECT_DIGEST);
break; 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": case "no_header":
// server returns no WWW-Authenticate header // server returns no WWW-Authenticate header
success = testAuth(url, auth, EXPECT_FAILURE); success = testAuth(url, auth, EXPECT_FAILURE);
@ -251,7 +380,7 @@ public class DigestAuth {
try { try {
System.out.printf("Connect to %s, expected auth scheme is '%s'%n", System.out.printf("Connect to %s, expected auth scheme is '%s'%n",
url, expectedScheme); url, expectedScheme);
load(url); load(url, auth);
if (expectedScheme == null) { if (expectedScheme == null) {
System.out.println("Unexpected successful connection"); System.out.println("Unexpected successful connection");
@ -276,8 +405,9 @@ public class DigestAuth {
return true; return true;
} }
static void load(String url) throws IOException { static void load(String url, Authenticator auth) throws IOException {
URLConnection conn = new URL(url).openConnection(); HttpURLConnection conn = (HttpURLConnection)(new URL(url).openConnection());
conn.setAuthenticator(auth);
conn.setUseCaches(false); conn.setUseCaches(false);
try (BufferedReader reader = new BufferedReader( try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) { 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 static class AuthenticatorImpl extends Authenticator {
private String lastRequestedScheme; private String lastRequestedScheme;
private String lastRequestedPrompt; 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 @Override
public PasswordAuthentication getPasswordAuthentication() { public PasswordAuthentication getPasswordAuthentication() {
lastRequestedScheme = getRequestingScheme(); lastRequestedScheme = getRequestingScheme();
lastRequestedPrompt = getRequestingPrompt(); lastRequestedPrompt = getRequestingPrompt();
System.out.println("AuthenticatorImpl: requested " System.out.println("AuthenticatorImpl: requested "
+ lastRequestedScheme); + lastRequestedScheme);
return new PasswordAuthentication(user, pass.toCharArray());
return new PasswordAuthentication("Mufasa",
"Circle Of Life".toCharArray());
} }
} }
@ -316,6 +473,9 @@ public class DigestAuth {
private volatile String wwwAuthHeader = null; private volatile String wwwAuthHeader = null;
private volatile String authInfoHeader = null; private volatile String authInfoHeader = null;
private volatile String lastRequestedNonce; private volatile String lastRequestedNonce;
private volatile String lastRequestedUser;
private volatile String lastRequestedUserhash;
private volatile Map<String,String> expectedParams;
private LocalHttpServer(HttpServer server) { private LocalHttpServer(HttpServer server) {
this.server = server; this.server = server;
@ -335,14 +495,49 @@ public class DigestAuth {
this.wwwAuthHeader = wwwAuthHeader; this.wwwAuthHeader = wwwAuthHeader;
} }
void setExpectedRequestParams(Map<String,String> params) {
this.expectedParams = params;
}
void setAuthInfoHeader(String authInfoHeader) { void setAuthInfoHeader(String authInfoHeader) {
this.authInfoHeader = 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(); InetAddress loopback = InetAddress.getLoopbackAddress();
HttpServer httpServer = HttpServer.create( HttpServer httpServer = HttpServer.create(
new InetSocketAddress(loopback, 0), 0); new InetSocketAddress(loopback, port), 0);
LocalHttpServer localHttpServer = new LocalHttpServer(httpServer); LocalHttpServer localHttpServer = new LocalHttpServer(httpServer);
localHttpServer.start(); localHttpServer.start();
@ -351,6 +546,7 @@ public class DigestAuth {
void start() { void start() {
server.createContext("/test", this); server.createContext("/test", this);
server.createContext("/", this);
server.start(); server.start();
System.out.println("HttpServer: started on port " + getAuthority()); System.out.println("HttpServer: started on port " + getAuthority());
} }
@ -385,7 +581,10 @@ public class DigestAuth {
t.getResponseHeaders().add("Authentication-Info", t.getResponseHeaders().add("Authentication-Info",
authInfoHeader); authInfoHeader);
} }
checkExpectedParams(header);
lastRequestedNonce = findParameter(header, "nonce"); lastRequestedNonce = findParameter(header, "nonce");
lastRequestedUser = findParameter(header, "username");
lastRequestedUserhash = findParameter(header, "userhash");
byte[] output = "hello".getBytes(); byte[] output = "hello".getBytes();
t.sendResponseHeaders(200, output.length); t.sendResponseHeaders(200, output.length);
t.getResponseBody().write(output); t.getResponseBody().write(output);

View file

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

View file

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

View file

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