diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
index 85ed712d2d..a9b5d6a6e3 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/DigestScheme.java
@@ -315,11 +315,9 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
}
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";
- }
+ final String digAlg = resolveAlgorithm(algorithm);
final MessageDigest digester;
try {
@@ -343,7 +341,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
final String nc = sb.toString();
if (cnonce == null) {
- cnonce = formatHex(createCnonce());
+ cnonce = formatHex(createCnonce(digAlg));
}
if (buffer == null) {
@@ -378,7 +376,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
}
// 3.2.2.2: Calculating digest
- if ("MD5-sess".equalsIgnoreCase(algorithm)) {
+ if ("MD5-sess".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) {
// H( unq(username-value) ":" unq(realm-value) ":" passwd )
// ":" unq(nonce-value)
// ":" unq(cnonce-value)
@@ -517,10 +515,15 @@ String getA2() {
}
/**
- * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long string.
+ * Encodes a byte array digest into a hexadecimal string.
+ *
+ * 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.
+ *
*
- * @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 +534,47 @@ static String formatHex(final byte[] binaryData) {
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 String algorithm) {
final SecureRandom rnd = new SecureRandom();
- final byte[] tmp = new byte[8];
+ final int length;
+ switch (algorithm.toUpperCase()) {
+ case "SHA-256":
+ case "SHA-512/256":
+ length = 32;
+ break;
+ case "MD5":
+ default:
+ length = 8;
+ break;
+ }
+ final byte[] tmp = new byte[length];
rnd.nextBytes(tmp);
return tmp;
}
+ /**
+ * Creates a random cnonce value based on the current time.
+ *
+ * @return The cnonce value as String.
+ * @deprecated Use {@link DigestScheme#createCnonce(String)} instead.
+ */
+ @Deprecated
+ static byte[] createCnonce() {
+ return createCnonce("MD5"); // Default to MD5 to maintain compatibility
+ }
+
private void writeObject(final ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(defaultCharset.name());
@@ -601,4 +629,27 @@ private boolean containsInvalidABNFChars(final String value) {
}
return false;
}
+
+ /**
+ * Resolves the specified algorithm name to a standard form based on recognized algorithm suffixes.
+ *
+ * This method translates session-based algorithms (e.g., "-sess" suffix) to their base forms
+ * for correct MessageDigest usage. If no algorithm is specified or "MD5-sess" is provided,
+ * it defaults to "MD5". The method also maps "SHA-512-256" to "SHA-512/256" to align with
+ * Java's naming for SHA-512/256.
+ *
+ *
+ * @param algorithm the algorithm name to resolve, such as "MD5-sess", "SHA-256-sess", or "SHA-512-256-sess"
+ * @return the resolved base algorithm name, or the original algorithm name if no mapping applies
+ */
+ private String resolveAlgorithm(final String algorithm) {
+ if (algorithm == null || algorithm.equalsIgnoreCase("MD5-sess")) {
+ return "MD5";
+ } else if (algorithm.equalsIgnoreCase("SHA-256-sess")) {
+ return "SHA-256";
+ } else if (algorithm.equalsIgnoreCase("SHA-512-256-sess") || algorithm.equalsIgnoreCase("SHA-512-256")) {
+ return "SHA-512/256";
+ }
+ return algorithm; // If it's already a supported algorithm or doesn't need translating
+ }
}
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
index af8204b85d..f5f010c2d6 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestDigestScheme.java
@@ -987,4 +987,182 @@ void testNoNextNonceUsageFromContext() throws Exception {
}
+ @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 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 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 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 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 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 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 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 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 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 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);
+ }
+
+
+
}