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.
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 85ed712..0771f9b 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
@@ -242,7 +242,7 @@ public Principal getPrincipal() {
     }
 
     @Override
-    public String generateAuthResponse(
+            public String generateAuthResponse(
             final HttpHost host,
             final HttpRequest request,
             final HttpContext context) throws AuthenticationException {
@@ -315,15 +315,15 @@ 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";
-        }
+
+        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 @@ 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 +378,7 @@ private String createDigestResponse(final HttpRequest request) throws Authentica
         }
 
         // 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 @@ 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.
+     * <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 @@ 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 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 @@ private boolean containsInvalidABNFChars(final String value) {
         }
         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);
+            }
+        }
+    }
+
+
 }
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 af8204b..6b0fc5b 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
@@ -178,14 +178,9 @@ void testDigestAuthenticationWithSHA() throws Exception {
         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 @@ 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<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);
+    }
+
+
+
 }