Skip to content

Commit

Permalink
Support RFC 7616 compliance in DigestScheme with extended hash algori…
Browse files Browse the repository at this point in the history
…thm 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.
  • Loading branch information
arturobernalg committed Nov 3, 2024
1 parent 40d6ba4 commit a4a3930
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
* <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;
Expand All @@ -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());
Expand Down Expand Up @@ -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.
* <p>
* 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.
* </p>
*
* @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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<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);
}



}

0 comments on commit a4a3930

Please sign in to comment.