This is an automated email from the ASF dual-hosted git repository. afs pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit 1034f3ee9b9f147d7bc0d481892ee546acf08a48 Author: Andy Seaborne <[email protected]> AuthorDate: Mon Jun 8 20:38:26 2026 +0100 GH-3977: Support SHA-256 for digest authentication --- .../main/java/org/apache/jena/http/HttpLib.java | 6 +- .../org/apache/jena/http/auth/AuthChallenge.java | 6 +- .../java/org/apache/jena/http/auth/AuthHttp.java | 51 ++++--- .../java/org/apache/jena/http/auth/AuthLib.java | 12 +- .../org/apache/jena/http/auth/DigestAlgorithm.java | 57 ++++++++ .../java/org/apache/jena/http/auth/DigestLib.java | 29 +++- .../org/apache/jena/http/auth/TestAuthDigest.java | 159 ++++++++++++++++++++- .../jena/http/auth/TestAuthHeaderParser.java | 5 - .../jena/fuseki/main/auth/AuthBearerFilter.java | 4 + 9 files changed, 287 insertions(+), 42 deletions(-) diff --git a/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java b/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java index 422978e154..58cd087d5f 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java +++ b/jena-arq/src/main/java/org/apache/jena/http/HttpLib.java @@ -445,7 +445,11 @@ public class HttpLib { String path = uri.getRawPath(); if ( path == null || path.isEmpty() ) path = "/"; - return path; + String target = path; + String qs = uri.getRawQuery(); + if ( qs != null ) + target = target+"?"+qs; + return target; } /** Return a HttpRequest */ diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthChallenge.java b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthChallenge.java index 41e2fb9072..2a3e37a10c 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthChallenge.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthChallenge.java @@ -38,6 +38,8 @@ public class AuthChallenge { public final String nonce; public final String opaque; public final String qop; + public final String algorithm; + // May be null; public final Map<String, String> authParams; @@ -80,13 +82,14 @@ public class AuthChallenge { get(auth, AuthHttp.strNonce), // Required for digest, not for basic. get(auth, AuthHttp.strOpaque), get(auth, AuthHttp.strQop), + get(auth, AuthHttp.strAlgorithm), auth.getAuthParams()); } catch (NullPointerException ex) { return null; } } - private AuthChallenge(AuthScheme authScheme, AuthHeader authHeader, String realm, String nonce, String opaque, String qop, Map<String, String> authParams) { + private AuthChallenge(AuthScheme authScheme, AuthHeader authHeader, String realm, String nonce, String opaque, String qop, String algorithm, Map<String, String> authParams) { Objects.requireNonNull(authScheme); Objects.requireNonNull(authHeader); this.authScheme = authScheme; @@ -95,6 +98,7 @@ public class AuthChallenge { this.nonce = nonce; this.opaque = opaque; this.qop = qop; + this.algorithm = algorithm; this.authParams = authParams; } diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthHttp.java b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthHttp.java index d2bc4dbbb9..54fee56752 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthHttp.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthHttp.java @@ -23,33 +23,40 @@ package org.apache.jena.http.auth; import java.util.Objects ; -import org.apache.commons.codec.digest.DigestUtils ; - -/** Constants and operations from RFC 2617 (digest, now RFC 7616; basic, now RFC 7617), using MD5 (the default) */ +/** + * Constants and operations from RFC 2617 (digest, now RFC 7616; basic, now RFC 7617) + * Original digest authentication: RFC 2069 + */ class AuthHttp { - public static String strUsername = "username"; - public static String strRealm = "realm"; - public static String strNonce = "nonce"; - public static String strNc = "nc"; - public static String strCNonce = "cnonce"; - public static String strQop = "qop"; - public static String strResponse = "response"; - public static String strOpaque = "opaque"; - public static String strUri = "uri"; + // Field names + public static final String strUsername = "username"; + public static final String strRealm = "realm"; + public static final String strNonce = "nonce"; + public static final String strNc = "nc"; + public static final String strCNonce = "cnonce"; + public static final String strQop = "qop"; + public static final String strResponse = "response"; + public static final String strOpaque = "opaque"; + public static final String strUri = "uri"; + public static final String strAlgorithm = "algorithm"; - public static String KD(String data) { - return H(data) ; - } + // Algorithm names. + public static final String algortihmSHA256 = "SHA-256"; + public static final String algortihmSHA256_sess = "SHA-256-sess"; + public static final String algortihmSHA512_256 = "SHA-512-256"; + public static final String algortihmSHA512_256_sess = "SHA-512-256-sess"; + public static final String algortihmMD5 = "MD5"; + public static final String algortihmMD5_sess = "MD5-sess"; - public static String KD(String secret, String data) { - return H(secret+":"+data) ; + public static String KD(String secret, String data, DigestAlgorithm digestAlgorithm) { + return H(secret+":"+data, digestAlgorithm) ; } - public static String H(String string) { - return DigestUtils.md5Hex(string) ; + public static String H(String string, DigestAlgorithm algorithm) { + return algorithm.digest.apply(string); } - public static String A1_MD5(String username, String realm, String password) { + public static String A1(String username, String realm, String password, DigestAlgorithm digestAlgorithm) { Objects.requireNonNull(username) ; Objects.requireNonNull(realm) ; Objects.requireNonNull(password) ; @@ -57,14 +64,14 @@ class AuthHttp { return s ; } - public static String A1_MD5_sess(String username, String realm, String password, String nonce, String cnonce) { + public static String A1_sess(String username, String realm, String password, String nonce, String cnonce, DigestAlgorithm digestAlgorithm) { Objects.requireNonNull(username) ; Objects.requireNonNull(realm) ; Objects.requireNonNull(password) ; Objects.requireNonNull(nonce) ; Objects.requireNonNull(cnonce) ; String s = username+":"+realm+":"+password ; - String x = H(s)+":"+nonce+":"+cnonce ; + String x = H(s, digestAlgorithm)+":"+nonce+":"+cnonce ; return s ; } diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java index 14a2ac0b75..16a1b28cc9 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/AuthLib.java @@ -83,9 +83,9 @@ public class AuthLib { /* Handle a 401 (authentication challenge). */ private static <T> CompletableFuture<HttpResponse<T>> handle401Async(HttpClient httpClient, - HttpRequest request, - BodyHandler<T> bodyHandler, - HttpResponse<T> httpResponse401) { + HttpRequest request, + BodyHandler<T> bodyHandler, + HttpResponse<T> httpResponse401) { AuthChallenge aHeader = wwwAuthenticateHeader(httpResponse401); if ( aHeader == null ) // No valid header - simply return the original response. @@ -103,7 +103,6 @@ public class AuthLib { throw HttpException.create(httpResponse401); } - // Request target - no query string. AuthRequestModifier authRequestModifier; switch (aHeader.authScheme) { case BASIC : @@ -113,6 +112,8 @@ public class AuthLib { String requestTarget = HttpLib.requestTargetServer(request.uri()); authRequestModifier = digestAuthModifier(aHeader, passwordRecord.getUsername(), passwordRecord.getPassword(), request.method(), requestTarget); + if ( authRequestModifier == null ) + throw HttpException.error("Unrecognized digest algorithm"); break; } case BEARER : { @@ -154,7 +155,6 @@ public class AuthLib { return null; // Choose first digest or bearer, else the first basic. Prefer digest or bearer to basic. AuthChallenge aHeader = null; - String result = null; for ( String headerValue : headers ) { AuthChallenge aHeader2 = AuthChallenge.parse(headerValue); if ( aHeader2 == null ) { @@ -163,7 +163,7 @@ public class AuthLib { } AuthScheme authScheme = aHeader2.authScheme; switch(authScheme) { - case DIGEST : + case DIGEST : return aHeader2; case BASIC: if ( aHeader == null ) diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/DigestAlgorithm.java b/jena-arq/src/main/java/org/apache/jena/http/auth/DigestAlgorithm.java new file mode 100644 index 0000000000..c320d9be17 --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/DigestAlgorithm.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.http.auth; + +import static org.apache.jena.http.auth.AuthHttp.*; +import java.util.function.Function; + +import org.apache.commons.codec.digest.DigestUtils; + +/** Algorithms for digest authentication. */ +public enum DigestAlgorithm { + SHA_256(algortihmSHA256, DigestUtils::sha256Hex), + SHA_256_sess(algortihmSHA256_sess, DigestUtils::sha256Hex), + SHA_512_256(algortihmSHA512_256, DigestUtils::sha512_256Hex), + SHA_512_256_sess(algortihmSHA512_256_sess, DigestUtils::sha512_256Hex), + MD5(algortihmMD5, DigestUtils::md5Hex), + MD5_sess(algortihmMD5, DigestUtils::md5Hex) + ; + public final String algorithmName; + public final Function<String, String> digest; + + private DigestAlgorithm(String algorithmName, Function<String, String> digest) { + this.algorithmName = algorithmName ; + this.digest = digest; + } + + /** Algothm name to enum, or null (unecognized) */ + public static DigestAlgorithm fromString(String name) { + return switch(name) { + case algortihmSHA256 -> SHA_256; + case algortihmSHA256_sess -> SHA_256_sess; + case algortihmSHA512_256 -> SHA_512_256; + case algortihmSHA512_256_sess -> SHA_512_256_sess; + case algortihmMD5 -> MD5; + case algortihmMD5_sess -> MD5_sess; + default -> null; + }; + } +} diff --git a/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java b/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java index 654ce8e6ea..50243a77fa 100644 --- a/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java +++ b/jena-arq/src/main/java/org/apache/jena/http/auth/DigestLib.java @@ -21,7 +21,7 @@ package org.apache.jena.http.auth; -import static org.apache.jena.http.auth.AuthHttp.A1_MD5; +import static org.apache.jena.http.auth.AuthHttp.A1; import static org.apache.jena.http.auth.AuthHttp.A2_auth; import static org.apache.jena.http.auth.AuthHttp.H; import static org.apache.jena.http.auth.AuthHttp.KD; @@ -47,21 +47,34 @@ class DigestLib { String username, String password, String method, String requestTarget, String cnonce, String nc, String authType) { - String a1 = A1_MD5(username, auth.realm, password) ; + DigestAlgorithm dAlgo = getAlgorithm(auth); + if ( dAlgo == null ) + // Unrecognized algorithm name. + return null; + String a1 = A1(username, auth.realm, password, dAlgo) ; if ( auth.qop == null ) { // RFC 2069 // Firefox seems to prefer this form?? - return KD(H(a1), auth.nonce+":"+H(A2_auth(method, requestTarget))) ; + return KD(H(a1, dAlgo), auth.nonce+":"+H(A2_auth(method, requestTarget), dAlgo), dAlgo) ; } else { Objects.nonNull(cnonce) ; Objects.nonNull(nc) ; - return KD(H(a1), - auth.nonce+":"+nc+":"+cnonce+":"+authType+":"+H(A2_auth(method, requestTarget)) + return KD(H(a1, dAlgo), + auth.nonce+":"+nc+":"+cnonce+":"+authType+":"+H(A2_auth(method, requestTarget), dAlgo), + dAlgo ) ; } } + public static DigestAlgorithm getAlgorithm(AuthChallenge challenge) { + if ( challenge.algorithm == null ) + // Default by RFC 7616 + return DigestAlgorithm.MD5; + // null means ignore. + return DigestAlgorithm.fromString(challenge.algorithm); + } + private static Random nonceGenerator = new SecureRandom(); /** @@ -105,10 +118,16 @@ class DigestLib { String responseField = calcDigestChallengeResponse(aHeader, user, password, method, requestTarget, clientNonce, nc, "auth"); + if ( responseField == null ) { + return null; + // Unrecognized algorithm. RFC says "ignore request" + } + StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("Digest "); field(stringBuilder, true, "username", user, true); field(stringBuilder, false, "realm", aHeader.realm, true); + field(stringBuilder, false, "algorithm", aHeader.algorithm, false); field(stringBuilder, false, "nonce", aHeader.nonce, true); field(stringBuilder, false, "uri", requestTarget, true); field(stringBuilder, false, "qop", "auth", false); diff --git a/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthDigest.java b/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthDigest.java index 9ca17e770b..eb02b7bebd 100644 --- a/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthDigest.java +++ b/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthDigest.java @@ -22,13 +22,21 @@ package org.apache.jena.http.auth; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; public class TestAuthDigest { - // Example from RFC 2617 + // Examples from RFC 2617 + + // digest 1 + // No algorithm - use MD5 + // MD5: 6629fae49393a05397450978507c4ef1 + // SHA-256: 5abdd07184ba512a22c53f41470e5eea7dcaa3a93a59b630c13dfe0a5dc6e38b + // SHA-512-256: f23c08ec7334a881f8286e68450ddbd9f0cd91c41481f0e1433604da8113c6dc + @Test - public void digest_1() { + public void digest_1_no_algorithm() { String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\","+ " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\","+ " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; @@ -49,7 +57,84 @@ public class TestAuthDigest { assertEquals("6629fae49393a05397450978507c4ef1", responseField); } + + @Test + public void digest_1_md5() { + String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\","+ + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\","+ + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"" + + " algorithm=MD5"; + AuthChallenge aHeader = AuthChallenge.parse(header); + + String requestTarget = "/dir/index.html"; + String method = "GET"; + String cnonce = "0a4f113b"; + String nc = "00000001"; + String username = "Mufasa"; + String password = "Circle Of Life"; + + String responseField = + DigestLib.calcDigestChallengeResponse(aHeader, username, password, + method, requestTarget, + cnonce, nc, "auth"); + + assertEquals("6629fae49393a05397450978507c4ef1", responseField); + } + + @Test + public void digest_1_sha256() { + String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\","+ + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\","+ + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""+ + " algorithm=SHA-256"; + AuthChallenge aHeader = AuthChallenge.parse(header); + + String requestTarget = "/dir/index.html"; + String method = "GET"; + String cnonce = "0a4f113b"; + String nc = "00000001"; + String username = "Mufasa"; + String password = "Circle Of Life"; + + String responseField = + DigestLib.calcDigestChallengeResponse(aHeader, username, password, + method, requestTarget, + cnonce, nc, "auth"); + + assertEquals("5abdd07184ba512a22c53f41470e5eea7dcaa3a93a59b630c13dfe0a5dc6e38b", responseField); + } + + @Test + public void digest_1_sha512_256() { + String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\","+ + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\","+ + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""+ + " algorithm=SHA-512-256"; + AuthChallenge aHeader = AuthChallenge.parse(header); + + String requestTarget = "/dir/index.html"; + String method = "GET"; + String cnonce = "0a4f113b"; + String nc = "00000001"; + String username = "Mufasa"; + String password = "Circle Of Life"; + + String responseField = + DigestLib.calcDigestChallengeResponse(aHeader, username, password, + method, requestTarget, + cnonce, nc, "auth"); + + assertEquals("f23c08ec7334a881f8286e68450ddbd9f0cd91c41481f0e1433604da8113c6dc", responseField); + } + + // Example from RFC 7616 + + // digest 2 + // MD5: 8ca523f5e9506fed4657c9700eebdbec + // SHA-256 753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1 + // SHA-512-256: 430d05014cecc49cab6fbe03176d41a1da86cbfe24a16580e22aaad928d960d0 + @Test public void digest_2() { String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\"," + @@ -70,4 +155,74 @@ public class TestAuthDigest { assertEquals("8ca523f5e9506fed4657c9700eebdbec", responseField); } + + // Example from RFC 7616 + @Test + public void digest_2_sha256() { + String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\"," + + " nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\","+ + " opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""+ + " algorithm=SHA-256"; + AuthChallenge aHeader = AuthChallenge.parse(header); + String requestTarget = "/dir/index.html"; + String method = "GET"; + String cnonce = "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"; + String nc = "00000001"; + String username = "Mufasa"; + String password = "Circle of Life"; // NB Different to RFC 2617 + + String responseField = + DigestLib.calcDigestChallengeResponse(aHeader, username, password, + method, requestTarget, + cnonce, nc, "auth"); + + assertEquals("753927fa0e85d155564e2e272a28d1802ca10daf4496794697cf8db5856cb6c1", responseField); + } + + // Example from RFC 7616 + @Test + public void digest_2_sha512_256() { + String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\"," + + " nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\","+ + " opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\""+ + " algorithm=SHA-512-256"; + AuthChallenge aHeader = AuthChallenge.parse(header); + String requestTarget = "/dir/index.html"; + String method = "GET"; + String cnonce = "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ"; + String nc = "00000001"; + String username = "Mufasa"; + String password = "Circle of Life"; // NB Different to RFC 2617 + + String responseField = + DigestLib.calcDigestChallengeResponse(aHeader, username, password, + method, requestTarget, + cnonce, nc, "auth"); + + assertEquals("430d05014cecc49cab6fbe03176d41a1da86cbfe24a16580e22aaad928d960d0", responseField); + } + + // Bad algorithm + @Test + public void digest_3_unknown() { + String header = "Digest realm=\"[email protected]\", qop=\"auth,auth-int\","+ + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\","+ + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""+ + " algorithm=UNKNOWN"; + AuthChallenge aHeader = AuthChallenge.parse(header); + + String requestTarget = "/dir/index.html"; + String method = "GET"; + String cnonce = "0a4f113b"; + String nc = "00000001"; + String username = "Mufasa"; + String password = "Circle Of Life"; + + String responseField = + DigestLib.calcDigestChallengeResponse(aHeader, username, password, + method, requestTarget, + cnonce, nc, "auth"); + + assertNull(responseField); + } } diff --git a/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthHeaderParser.java b/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthHeaderParser.java index 5978d48765..0d157992fd 100644 --- a/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthHeaderParser.java +++ b/jena-arq/src/test/java/org/apache/jena/http/auth/TestAuthHeaderParser.java @@ -41,7 +41,6 @@ public class TestAuthHeaderParser { return AuthHeader.parseChallenge(input); } - @Test public void parse_empty() { AuthHeader auth = parseAuth(""); assertNull(auth.getAuthScheme()); @@ -162,7 +161,6 @@ public class TestAuthHeaderParser { assertNull(map); } - @Test public void parse_bearer_bad_01() { AuthHeader auth = parseAuth("Bearer "); assertTrue(auth.isBearerAuth()); @@ -191,8 +189,6 @@ public class TestAuthHeaderParser { assertEquals("a=b", auth.getUnknown()); } - - @Test public void parse_challenge_01() { AuthHeader auth = parseChallenge("Bearer realm=\"somewhere\""); assertEquals(AuthScheme.BEARER, auth.getAuthScheme()); @@ -236,5 +232,4 @@ public class TestAuthHeaderParser { assertNotNull(map); assertEquals("hades", map.get("realm")); } - } \ No newline at end of file diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/auth/AuthBearerFilter.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/auth/AuthBearerFilter.java index 5de9a31542..59d5893687 100644 --- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/auth/AuthBearerFilter.java +++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/auth/AuthBearerFilter.java @@ -133,6 +133,10 @@ public class AuthBearerFilter implements Filter { // ---- "Authorization:" header present. AuthHeader authHeader = getAuthToken(request, auth); + if ( authHeader == null ) { + ServletOps.errorBadRequest("Bad syntax in Authorization header"); + return; + } // Test for Authorization: Bearer ..." if ( ! authHeader.isBearerAuth() ) {
