Enhance DigestScheme for RFC 7616 Compliance and Expanded Hash Algorithm Support (#597)
* Support RFC 7616 compliance in DigestScheme with extended hash algorithm support and charset Enhanced DigestScheme to support SHA-256, SHA-512/256, algorithms in compliance with RFC 7616. Adjusted cnonce generation for adequate entropy in SHA-256 and SHA-512/256 contexts. * Increase MD5 cnonce length to 16 bytes for full 128-bit entropy * Use represent supported algorithms.
This commit is contained in:
parent
40d6ba4ee5
commit
4b2a365c36
|
@ -242,7 +242,7 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String generateAuthResponse(
|
||||
public String generateAuthResponse(
|
||||
final HttpHost host,
|
||||
final HttpRequest request,
|
||||
final HttpContext context) throws AuthenticationException {
|
||||
|
@ -315,15 +315,15 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
}
|
||||
|
||||
final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset);
|
||||
String digAlg = algorithm;
|
||||
|
||||
// If an algorithm is not specified, default to MD5.
|
||||
if (digAlg == null || digAlg.equalsIgnoreCase("MD5-sess")) {
|
||||
digAlg = "MD5";
|
||||
}
|
||||
|
||||
DigestAlgorithm digAlg = null;
|
||||
|
||||
final MessageDigest digester;
|
||||
try {
|
||||
digester = createMessageDigest(digAlg);
|
||||
digAlg = DigestAlgorithm.fromString(algorithm == null ? "MD5" : algorithm);
|
||||
digester = createMessageDigest(digAlg.getBaseAlgorithm());
|
||||
} catch (final UnsupportedDigestAlgorithmException ex) {
|
||||
throw new AuthenticationException("Unsupported digest algorithm: " + digAlg);
|
||||
}
|
||||
|
@ -343,7 +343,7 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
final String nc = sb.toString();
|
||||
|
||||
if (cnonce == null) {
|
||||
cnonce = formatHex(createCnonce());
|
||||
cnonce = formatHex(createCnonce(digAlg));
|
||||
}
|
||||
|
||||
if (buffer == null) {
|
||||
|
@ -378,7 +378,7 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
}
|
||||
|
||||
// 3.2.2.2: Calculating digest
|
||||
if ("MD5-sess".equalsIgnoreCase(algorithm)) {
|
||||
if (digAlg.isSessionBased()) {
|
||||
// H( unq(username-value) ":" unq(realm-value) ":" passwd )
|
||||
// ":" unq(nonce-value)
|
||||
// ":" unq(cnonce-value)
|
||||
|
@ -517,10 +517,15 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long string.
|
||||
* Encodes a byte array digest into a hexadecimal string.
|
||||
* <p>
|
||||
* This method supports digests of various lengths, such as 16 bytes (128-bit) for MD5,
|
||||
* 32 bytes (256-bit) for SHA-256, and SHA-512/256. Each byte is converted to two
|
||||
* hexadecimal characters, so the resulting string length is twice the byte array length.
|
||||
* </p>
|
||||
*
|
||||
* @param binaryData array containing the digest
|
||||
* @return encoded MD5, or {@code null} if encoding failed
|
||||
* @param binaryData the array containing the digest bytes
|
||||
* @return encoded hexadecimal string, or {@code null} if encoding failed
|
||||
*/
|
||||
static String formatHex(final byte[] binaryData) {
|
||||
final int n = binaryData.length;
|
||||
|
@ -531,22 +536,37 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
buffer[i * 2] = HEXADECIMAL[high];
|
||||
buffer[(i * 2) + 1] = HEXADECIMAL[low];
|
||||
}
|
||||
|
||||
return new String(buffer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a random cnonce value based on the current time.
|
||||
* Creates a random cnonce value based on the specified algorithm's expected entropy.
|
||||
* Adjusts the length of the byte array based on the algorithm to ensure sufficient entropy.
|
||||
*
|
||||
* @return The cnonce value as String.
|
||||
* @param algorithm the algorithm for which the cnonce is being generated (e.g., "MD5", "SHA-256", "SHA-512-256").
|
||||
* @return The cnonce value as a byte array.
|
||||
* @since 5.5
|
||||
*/
|
||||
static byte[] createCnonce() {
|
||||
static byte[] createCnonce(final DigestAlgorithm algorithm) {
|
||||
final SecureRandom rnd = new SecureRandom();
|
||||
final byte[] tmp = new byte[8];
|
||||
final int length;
|
||||
switch (algorithm.name().toUpperCase()) {
|
||||
case "SHA-256":
|
||||
case "SHA-512/256":
|
||||
length = 32;
|
||||
break;
|
||||
case "MD5":
|
||||
default:
|
||||
length = 16;
|
||||
break;
|
||||
}
|
||||
final byte[] tmp = new byte[length];
|
||||
rnd.nextBytes(tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
|
||||
private void writeObject(final ObjectOutputStream out) throws IOException {
|
||||
out.defaultWriteObject();
|
||||
out.writeUTF(defaultCharset.name());
|
||||
|
@ -601,4 +621,102 @@ public class DigestScheme implements AuthScheme, Serializable {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing supported digest algorithms for HTTP Digest Authentication,
|
||||
* including session-based variants.
|
||||
*/
|
||||
private enum DigestAlgorithm {
|
||||
|
||||
/**
|
||||
* MD5 digest algorithm.
|
||||
*/
|
||||
MD5("MD5", false),
|
||||
|
||||
/**
|
||||
* MD5 digest algorithm with session-based variant.
|
||||
*/
|
||||
MD5_SESS("MD5", true),
|
||||
|
||||
/**
|
||||
* SHA-256 digest algorithm.
|
||||
*/
|
||||
SHA_256("SHA-256", false),
|
||||
|
||||
/**
|
||||
* SHA-256 digest algorithm with session-based variant.
|
||||
*/
|
||||
SHA_256_SESS("SHA-256", true),
|
||||
|
||||
/**
|
||||
* SHA-512/256 digest algorithm.
|
||||
*/
|
||||
SHA_512_256("SHA-512/256", false),
|
||||
|
||||
/**
|
||||
* SHA-512/256 digest algorithm with session-based variant.
|
||||
*/
|
||||
SHA_512_256_SESS("SHA-512/256", true);
|
||||
|
||||
private final String baseAlgorithm;
|
||||
private final boolean sessionBased;
|
||||
|
||||
/**
|
||||
* Constructor for {@code DigestAlgorithm}.
|
||||
*
|
||||
* @param baseAlgorithm the base name of the algorithm, e.g., "MD5" or "SHA-256"
|
||||
* @param sessionBased indicates if the algorithm is session-based (i.e., includes the "-sess" suffix)
|
||||
*/
|
||||
DigestAlgorithm(final String baseAlgorithm, final boolean sessionBased) {
|
||||
this.baseAlgorithm = baseAlgorithm;
|
||||
this.sessionBased = sessionBased;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the base algorithm name without session suffix.
|
||||
*
|
||||
* @return the base algorithm name
|
||||
*/
|
||||
private String getBaseAlgorithm() {
|
||||
return baseAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the algorithm is session-based.
|
||||
*
|
||||
* @return {@code true} if the algorithm includes the "-sess" suffix, otherwise {@code false}
|
||||
*/
|
||||
private boolean isSessionBased() {
|
||||
return sessionBased;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a string representation of an algorithm to the corresponding enum constant.
|
||||
*
|
||||
* @param algorithm the algorithm name, e.g., "SHA-256" or "SHA-512-256-sess"
|
||||
* @return the corresponding {@code DigestAlgorithm} constant
|
||||
* @throws UnsupportedDigestAlgorithmException if the algorithm is unsupported
|
||||
*/
|
||||
private static DigestAlgorithm fromString(final String algorithm) {
|
||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "MD5":
|
||||
return MD5;
|
||||
case "MD5-SESS":
|
||||
return MD5_SESS;
|
||||
case "SHA-256":
|
||||
return SHA_256;
|
||||
case "SHA-256-SESS":
|
||||
return SHA_256_SESS;
|
||||
case "SHA-512/256":
|
||||
case "SHA-512-256":
|
||||
return SHA_512_256;
|
||||
case "SHA-512-256-SESS":
|
||||
return SHA_512_256_SESS;
|
||||
default:
|
||||
throw new UnsupportedDigestAlgorithmException("Unsupported digest algorithm: " + algorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -178,14 +178,9 @@ class TestDigestScheme {
|
|||
authscheme.processChallenge(authChallenge, null);
|
||||
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse = authscheme.generateAuthResponse(host, request, null);
|
||||
|
||||
final Map<String, String> table = parseAuthResponse(authResponse);
|
||||
Assertions.assertEquals("username", table.get("username"));
|
||||
Assertions.assertEquals("realm1", table.get("realm"));
|
||||
Assertions.assertEquals("/", table.get("uri"));
|
||||
Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
|
||||
Assertions.assertEquals("8769e82e4e28ecc040b969562b9050580c6d186d", table.get("response"));
|
||||
Assertions.assertThrows(AuthenticationException.class, () ->
|
||||
authscheme.generateAuthResponse(host, request, null), "Expected UnsupportedDigestAlgorithmException for unsupported 'SHA' algorithm");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -987,4 +982,182 @@ class TestDigestScheme {
|
|||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testDigestAuthenticationWithSHA256() throws Exception {
|
||||
final HttpRequest request = new BasicHttpRequest("Simple", "/");
|
||||
final HttpHost host = new HttpHost("somehost", 80);
|
||||
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
|
||||
.add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
|
||||
.build();
|
||||
|
||||
final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", algorithm=SHA-256";
|
||||
final AuthChallenge authChallenge = parse(challenge);
|
||||
final DigestScheme authscheme = new DigestScheme();
|
||||
authscheme.processChallenge(authChallenge, null);
|
||||
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse = authscheme.generateAuthResponse(host, request, null);
|
||||
|
||||
final Map<String, String> table = parseAuthResponse(authResponse);
|
||||
Assertions.assertEquals("username", table.get("username"));
|
||||
Assertions.assertEquals("realm1", table.get("realm"));
|
||||
Assertions.assertEquals("/", table.get("uri"));
|
||||
Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
|
||||
Assertions.assertEquals("SHA-256", table.get("algorithm"));
|
||||
Assertions.assertNotNull(table.get("response"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDigestAuthenticationWithSHA512_256() throws Exception {
|
||||
final HttpRequest request = new BasicHttpRequest("Simple", "/");
|
||||
final HttpHost host = new HttpHost("somehost", 80);
|
||||
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
|
||||
.add(new AuthScope(host, "realm1", null), "username", "password".toCharArray())
|
||||
.build();
|
||||
|
||||
final String challenge = StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"f2a3f18799759d4f1a1c068b92b573cb\", algorithm=SHA-512-256";
|
||||
final AuthChallenge authChallenge = parse(challenge);
|
||||
final DigestScheme authscheme = new DigestScheme();
|
||||
authscheme.processChallenge(authChallenge, null);
|
||||
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse = authscheme.generateAuthResponse(host, request, null);
|
||||
|
||||
final Map<String, String> table = parseAuthResponse(authResponse);
|
||||
Assertions.assertEquals("username", table.get("username"));
|
||||
Assertions.assertEquals("realm1", table.get("realm"));
|
||||
Assertions.assertEquals("/", table.get("uri"));
|
||||
Assertions.assertEquals("f2a3f18799759d4f1a1c068b92b573cb", table.get("nonce"));
|
||||
Assertions.assertEquals("SHA-512-256", table.get("algorithm"));
|
||||
Assertions.assertNotNull(table.get("response"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDigestSHA256SessA1AndCnonceConsistency() throws Exception {
|
||||
final HttpHost host = new HttpHost("somehost", 80);
|
||||
final HttpRequest request = new BasicHttpRequest("GET", "/");
|
||||
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
|
||||
.add(new AuthScope(host, "subnet.domain.com", null), "username", "password".toCharArray())
|
||||
.build();
|
||||
|
||||
final String challenge1 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-256-sess, nonce=\"1234567890abcdef\", " +
|
||||
"charset=utf-8, realm=\"subnet.domain.com\"";
|
||||
final AuthChallenge authChallenge1 = parse(challenge1);
|
||||
final DigestScheme authscheme = new DigestScheme();
|
||||
authscheme.processChallenge(authChallenge1, null);
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse1 = authscheme.generateAuthResponse(host, request, null);
|
||||
|
||||
final Map<String, String> table1 = parseAuthResponse(authResponse1);
|
||||
Assertions.assertEquals("00000001", table1.get("nc"));
|
||||
final String cnonce1 = authscheme.getCnonce();
|
||||
final String sessionKey1 = authscheme.getA1();
|
||||
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse2 = authscheme.generateAuthResponse(host, request, null);
|
||||
final Map<String, String> table2 = parseAuthResponse(authResponse2);
|
||||
Assertions.assertEquals("00000002", table2.get("nc"));
|
||||
final String cnonce2 = authscheme.getCnonce();
|
||||
final String sessionKey2 = authscheme.getA1();
|
||||
|
||||
Assertions.assertEquals(cnonce1, cnonce2);
|
||||
Assertions.assertEquals(sessionKey1, sessionKey2);
|
||||
|
||||
final String challenge2 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-256-sess, nonce=\"1234567890abcdef\", " +
|
||||
"charset=utf-8, realm=\"subnet.domain.com\"";
|
||||
final AuthChallenge authChallenge2 = parse(challenge2);
|
||||
authscheme.processChallenge(authChallenge2, null);
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse3 = authscheme.generateAuthResponse(host, request, null);
|
||||
final Map<String, String> table3 = parseAuthResponse(authResponse3);
|
||||
Assertions.assertEquals("00000003", table3.get("nc"));
|
||||
|
||||
final String cnonce3 = authscheme.getCnonce();
|
||||
final String sessionKey3 = authscheme.getA1();
|
||||
|
||||
Assertions.assertEquals(cnonce1, cnonce3);
|
||||
Assertions.assertEquals(sessionKey1, sessionKey3);
|
||||
|
||||
final String challenge3 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-256-sess, nonce=\"fedcba0987654321\", " +
|
||||
"charset=utf-8, realm=\"subnet.domain.com\"";
|
||||
final AuthChallenge authChallenge3 = parse(challenge3);
|
||||
authscheme.processChallenge(authChallenge3, null);
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse4 = authscheme.generateAuthResponse(host, request, null);
|
||||
final Map<String, String> table4 = parseAuthResponse(authResponse4);
|
||||
Assertions.assertEquals("00000001", table4.get("nc"));
|
||||
|
||||
final String cnonce4 = authscheme.getCnonce();
|
||||
final String sessionKey4 = authscheme.getA1();
|
||||
|
||||
Assertions.assertNotEquals(cnonce1, cnonce4);
|
||||
Assertions.assertNotEquals(sessionKey1, sessionKey4);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testDigestSHA512256SessA1AndCnonceConsistency() throws Exception {
|
||||
final HttpHost host = new HttpHost("somehost", 80);
|
||||
final HttpRequest request = new BasicHttpRequest("GET", "/");
|
||||
final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
|
||||
.add(new AuthScope(host, "subnet.domain.com", null), "username", "password".toCharArray())
|
||||
.build();
|
||||
|
||||
final String challenge1 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-512-256-sess, nonce=\"1234567890abcdef\", " +
|
||||
"charset=utf-8, realm=\"subnet.domain.com\"";
|
||||
final AuthChallenge authChallenge1 = parse(challenge1);
|
||||
final DigestScheme authscheme = new DigestScheme();
|
||||
authscheme.processChallenge(authChallenge1, null);
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse1 = authscheme.generateAuthResponse(host, request, null);
|
||||
|
||||
final Map<String, String> table1 = parseAuthResponse(authResponse1);
|
||||
Assertions.assertEquals("00000001", table1.get("nc"));
|
||||
final String cnonce1 = authscheme.getCnonce();
|
||||
final String sessionKey1 = authscheme.getA1();
|
||||
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse2 = authscheme.generateAuthResponse(host, request, null);
|
||||
final Map<String, String> table2 = parseAuthResponse(authResponse2);
|
||||
Assertions.assertEquals("00000002", table2.get("nc"));
|
||||
final String cnonce2 = authscheme.getCnonce();
|
||||
final String sessionKey2 = authscheme.getA1();
|
||||
|
||||
Assertions.assertEquals(cnonce1, cnonce2);
|
||||
Assertions.assertEquals(sessionKey1, sessionKey2);
|
||||
|
||||
final String challenge2 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-512-256-sess, nonce=\"1234567890abcdef\", " +
|
||||
"charset=utf-8, realm=\"subnet.domain.com\"";
|
||||
final AuthChallenge authChallenge2 = parse(challenge2);
|
||||
authscheme.processChallenge(authChallenge2, null);
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse3 = authscheme.generateAuthResponse(host, request, null);
|
||||
final Map<String, String> table3 = parseAuthResponse(authResponse3);
|
||||
Assertions.assertEquals("00000003", table3.get("nc"));
|
||||
|
||||
final String cnonce3 = authscheme.getCnonce();
|
||||
final String sessionKey3 = authscheme.getA1();
|
||||
|
||||
Assertions.assertEquals(cnonce1, cnonce3);
|
||||
Assertions.assertEquals(sessionKey1, sessionKey3);
|
||||
|
||||
final String challenge3 = StandardAuthScheme.DIGEST + " qop=\"auth\", algorithm=SHA-512-256-sess, nonce=\"fedcba0987654321\", " +
|
||||
"charset=utf-8, realm=\"subnet.domain.com\"";
|
||||
final AuthChallenge authChallenge3 = parse(challenge3);
|
||||
authscheme.processChallenge(authChallenge3, null);
|
||||
Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
|
||||
final String authResponse4 = authscheme.generateAuthResponse(host, request, null);
|
||||
final Map<String, String> table4 = parseAuthResponse(authResponse4);
|
||||
Assertions.assertEquals("00000001", table4.get("nc"));
|
||||
|
||||
final String cnonce4 = authscheme.getCnonce();
|
||||
final String sessionKey4 = authscheme.getA1();
|
||||
|
||||
Assertions.assertNotEquals(cnonce1, cnonce4);
|
||||
Assertions.assertNotEquals(sessionKey1, sessionKey4);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue