From 438873aa59ce330b15622e4619a126146c496035 Mon Sep 17 00:00:00 2001 From: Diana Krepinska Date: Thu, 27 Apr 2023 00:09:14 +0200 Subject: [PATCH] [ELY-2359] Support HTTP Digest when fronted by load balancer --- .../auth/server/MechanismConfiguration.java | 18 +- .../auth/server/MechanismInformation.java | 1 + .../wildfly/security/http/HttpConstants.java | 1 + .../digest/DigestAuthenticationMechanism.java | 32 ++- .../http/digest/DigestMechanismFactory.java | 11 +- .../security/http/digest/NonceManager.java | 152 ++---------- .../http/digest/NonceManagerUtils.java | 231 ++++++++++++++++++ .../http/digest/PersistentNonceManager.java | 204 ++++++++++++++++ .../DigestAuthenticationMechanismTest.java | 53 ++++ .../http/impl/AbstractBaseHttpTest.java | 11 + 10 files changed, 571 insertions(+), 143 deletions(-) create mode 100644 http/digest/src/main/java/org/wildfly/security/http/digest/NonceManagerUtils.java create mode 100644 http/digest/src/main/java/org/wildfly/security/http/digest/PersistentNonceManager.java diff --git a/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismConfiguration.java b/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismConfiguration.java index 2ebc1585f9d..c31dc511470 100644 --- a/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismConfiguration.java +++ b/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismConfiguration.java @@ -49,13 +49,15 @@ public final class MechanismConfiguration { private final RealmMapper realmMapper; private final Map mechanismRealms; private final CredentialSource serverCredentialSource; + private final boolean sessionBasedNonceManager; - MechanismConfiguration(final Function preRealmRewriter, final Function postRealmRewriter, final Function finalRewriter, final RealmMapper realmMapper, final Collection mechanismRealms, final CredentialSource serverCredentialSource) { + MechanismConfiguration(final Function preRealmRewriter, final Function postRealmRewriter, final Function finalRewriter, final RealmMapper realmMapper, final Collection mechanismRealms, final CredentialSource serverCredentialSource, final boolean sessionBasedNonceManager) { checkNotNullParam("mechanismRealms", mechanismRealms); this.preRealmRewriter = preRealmRewriter; this.postRealmRewriter = postRealmRewriter; this.finalRewriter = finalRewriter; this.realmMapper = realmMapper; + this.sessionBasedNonceManager = sessionBasedNonceManager; final Iterator iterator = mechanismRealms.iterator(); if (! iterator.hasNext()) { // zero @@ -146,6 +148,9 @@ public MechanismRealmConfiguration getMechanismRealmConfiguration(String realmNa return mechanismRealms.get(realmName); } + public boolean getUseSessionBasedNonceManager() { + return sessionBasedNonceManager; + } /** * Obtain a new {@link Builder} capable of building a {@link MechanismConfiguration}. * @@ -167,6 +172,7 @@ public static final class Builder { private RealmMapper realmMapper; private List mechanismRealms; private CredentialSource serverCredentialSource = CredentialSource.NONE; + private boolean useSessionBasedNonceManager = false; /** * Construct a new instance. @@ -271,6 +277,12 @@ public Builder setServerCredentialSource(final CredentialSource serverCredential return this; } + public Builder setUseSessionBasedNonceManager(final Boolean useSessionBasedNonceManager) { + checkNotNullParam("useSessionBasedNonceManager", useSessionBasedNonceManager); + this.useSessionBasedNonceManager = useSessionBasedNonceManager; + return this; + } + /** * Build a new instance. If no mechanism realms are offered, an empty collection should be provided for * {@code mechanismRealms}; otherwise, if the mechanism only supports one realm, the first will be used. If the @@ -285,12 +297,12 @@ public MechanismConfiguration build() { } else { mechanismRealms = unmodifiableList(asList(mechanismRealms.toArray(NO_REALM_CONFIGS))); } - return new MechanismConfiguration(preRealmRewriter, postRealmRewriter, finalRewriter, realmMapper, mechanismRealms, serverCredentialSource); + return new MechanismConfiguration(preRealmRewriter, postRealmRewriter, finalRewriter, realmMapper, mechanismRealms, serverCredentialSource, useSessionBasedNonceManager); } } /** * An empty mechanism configuration.. */ - public static final MechanismConfiguration EMPTY = new MechanismConfiguration(Function.identity(), Function.identity(), Function.identity(), null, emptyList(), CredentialSource.NONE); + public static final MechanismConfiguration EMPTY = new MechanismConfiguration(Function.identity(), Function.identity(), Function.identity(), null, emptyList(), CredentialSource.NONE, false); } diff --git a/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismInformation.java b/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismInformation.java index a077f186c61..0d909bfb1b1 100644 --- a/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismInformation.java +++ b/auth/server/base/src/main/java/org/wildfly/security/auth/server/MechanismInformation.java @@ -74,6 +74,7 @@ public String getMechanismName() { public String getHostName() { return null; } + }; } diff --git a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java index cb3d074a74d..8173e3ae994 100644 --- a/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java +++ b/http/base/src/main/java/org/wildfly/security/http/HttpConstants.java @@ -51,6 +51,7 @@ private HttpConstants() { public static final String CONFIG_VALIDATE_DIGEST_URI = CONFIG_BASE + ".validate-digest-uri"; public static final String CONFIG_SKIP_CERTIFICATE_VERIFICATION = CONFIG_BASE + ".skip-certificate-verification"; + public static final String CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER = CONFIG_BASE + ".use-session-based-digest-nonce-manager"; /** * The context relative path of the login page. diff --git a/http/digest/src/main/java/org/wildfly/security/http/digest/DigestAuthenticationMechanism.java b/http/digest/src/main/java/org/wildfly/security/http/digest/DigestAuthenticationMechanism.java index 6cc61c1b258..e7827c06292 100644 --- a/http/digest/src/main/java/org/wildfly/security/http/digest/DigestAuthenticationMechanism.java +++ b/http/digest/src/main/java/org/wildfly/security/http/digest/DigestAuthenticationMechanism.java @@ -66,6 +66,7 @@ import org.wildfly.security.http.HttpServerMechanismsResponder; import org.wildfly.security.http.HttpServerRequest; import org.wildfly.security.http.HttpServerResponse; +import org.wildfly.security.http.Scope; import org.wildfly.security.mechanism.AuthenticationMechanismException; import org.wildfly.security.mechanism.digest.DigestQuote; import org.wildfly.security.mechanism.digest.PasswordDigestObtainer; @@ -80,6 +81,7 @@ final class DigestAuthenticationMechanism implements HttpServerAuthenticationMec private static final String CHALLENGE_PREFIX = "Digest "; private static final String OPAQUE_VALUE = "00000000000000000000000000000000"; private static final byte COLON = ':'; + public static final String PERSISTENT_NONCE_MANAGER = "persistentNonceManager"; private final Supplier providers; private final CallbackHandler callbackHandler; @@ -114,6 +116,17 @@ public String getMechanismName() { @Override public void evaluateRequest(final HttpServerRequest request) throws HttpAuthenticationException { + + if (nonceManager instanceof PersistentNonceManager) { + if (request.getScope(Scope.SESSION) == null || !request.getScope(Scope.SESSION).exists()) { + request.getScope(Scope.SESSION).create(); + } + PersistentNonceManager persistentNonceManager = (PersistentNonceManager) request.getScope(Scope.SESSION).getAttachment(PERSISTENT_NONCE_MANAGER); + if (persistentNonceManager != null) { + ((PersistentNonceManager) nonceManager).refreshInfoFromSessionNonceManager(persistentNonceManager); + } + } + List authorizationValues = request.getRequestHeaderValues(AUTHORIZATION); if (authorizationValues != null) { @@ -126,14 +139,13 @@ public void evaluateRequest(final HttpServerRequest request) throws HttpAuthenti return; } catch (AuthenticationMechanismException e) { httpDigest.trace("Failed to parse or validate the response", e); - request.badRequest(e.toHttpAuthenticationException(), response -> prepareResponse(selectRealm(), response, false)); + request.badRequest(e.toHttpAuthenticationException(), response -> prepareResponse(selectRealm(), response, false, request)); return; } } } } - - request.noAuthenticationInProgress(response -> prepareResponse(selectRealm(), response, false)); + request.noAuthenticationInProgress(response -> prepareResponse(selectRealm(), response, false, request)); } private void validateResponse(HashMap responseTokens, final HttpServerRequest request) throws AuthenticationMechanismException, HttpAuthenticationException { @@ -211,7 +223,7 @@ private void validateResponse(HashMap responseTokens, final Http if (username.length() == 0) { httpDigest.trace("Failed: no username"); fail(); - request.authenticationFailed(httpDigest.authenticationFailed(), httpResponse -> prepareResponse(selectedRealm, httpResponse, false)); + request.authenticationFailed(httpDigest.authenticationFailed(), httpResponse -> prepareResponse(selectedRealm, httpResponse, false, request)); return; } @@ -220,7 +232,7 @@ private void validateResponse(HashMap responseTokens, final Http if (hA1 == null) { httpDigest.trace("Failed: unable to get expected proof"); fail(); - request.authenticationFailed(httpDigest.authenticationFailed(), httpResponse -> prepareResponse(selectedRealm, httpResponse, false)); + request.authenticationFailed(httpDigest.authenticationFailed(), httpResponse -> prepareResponse(selectedRealm, httpResponse, false, request)); return; } @@ -229,13 +241,13 @@ private void validateResponse(HashMap responseTokens, final Http if (MessageDigest.isEqual(response, calculatedResponse) == false) { httpDigest.trace("Failed: invalid proof"); fail(); - request.authenticationFailed(httpDigest.mechResponseTokenMismatch(getMechanismName()), httpResponse -> prepareResponse(selectedRealm, httpResponse, false)); + request.authenticationFailed(httpDigest.mechResponseTokenMismatch(getMechanismName()), httpResponse -> prepareResponse(selectedRealm, httpResponse, false, request)); return; } if (nonceValid == false) { httpDigest.trace("Failed: invalid nonce"); - request.authenticationInProgress(httpResponse -> prepareResponse(selectedRealm, httpResponse, true)); + request.authenticationInProgress(httpResponse -> prepareResponse(selectedRealm, httpResponse, true, request)); return; } @@ -379,7 +391,7 @@ private String[] getAvailableRealms() throws AuthenticationMechanismException { } } - private void prepareResponse(String realmName, HttpServerResponse response, boolean stale) throws HttpAuthenticationException { + private void prepareResponse(String realmName, HttpServerResponse response, boolean stale, HttpServerRequest request) throws HttpAuthenticationException { StringBuilder sb = new StringBuilder(CHALLENGE_PREFIX); sb.append(REALM).append("=\"").append(DigestQuote.quote(realmName)).append("\""); @@ -396,6 +408,10 @@ private void prepareResponse(String realmName, HttpServerResponse response, bool response.addResponseHeader(WWW_AUTHENTICATE, sb.toString()); response.setStatusCode(UNAUTHORIZED); + + if ((nonceManager instanceof PersistentNonceManager) && request.getScope(Scope.SESSION) != null) { + request.getScope(Scope.SESSION).setAttachment(PERSISTENT_NONCE_MANAGER, this.nonceManager); + } } private boolean authorize(String username) throws AuthenticationMechanismException { diff --git a/http/digest/src/main/java/org/wildfly/security/http/digest/DigestMechanismFactory.java b/http/digest/src/main/java/org/wildfly/security/http/digest/DigestMechanismFactory.java index 3a64c3fe130..f338b910a01 100644 --- a/http/digest/src/main/java/org/wildfly/security/http/digest/DigestMechanismFactory.java +++ b/http/digest/src/main/java/org/wildfly/security/http/digest/DigestMechanismFactory.java @@ -21,6 +21,7 @@ import static org.wildfly.common.Assert.checkNotNullParam; import static org.wildfly.security.http.HttpConstants.CONFIG_CONTEXT_PATH; import static org.wildfly.security.http.HttpConstants.CONFIG_REALM; +import static org.wildfly.security.http.HttpConstants.CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER; import static org.wildfly.security.http.HttpConstants.DIGEST_NAME; import static org.wildfly.security.http.HttpConstants.DIGEST_SHA256_NAME; import static org.wildfly.security.http.HttpConstants.DIGEST_SHA512_256_NAME; @@ -58,7 +59,7 @@ public DigestMechanismFactory() { } public DigestMechanismFactory(final Provider provider) { - this(new Provider[] { provider }); + this(new Provider[]{provider}); } public DigestMechanismFactory(final Provider... providers) { @@ -90,7 +91,7 @@ public String[] getMechanismNames(Map properties) { } /** - * @see org.wildfly.security.http.HttpServerAuthenticationMechanismFactory#createAuthenticationMechanism(java.lang.String, java.util.Map, javax.security.auth.callback.CallbackHandler) + * @see HttpServerAuthenticationMechanismFactory#createAuthenticationMechanism(String, Map, CallbackHandler) */ @Override public HttpServerAuthenticationMechanism createAuthenticationMechanism(String mechanismName, Map properties, CallbackHandler callbackHandler) throws HttpAuthenticationException { @@ -100,6 +101,12 @@ public HttpServerAuthenticationMechanism createAuthenticationMechanism(String me if (properties.containsKey("nonceManager")) { nonceManager = (NonceManager) properties.get("nonceManager"); + } else if (properties.get(CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER) != null) { + Object configSessionDigest = properties.get(CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER); + String sessionDigest = configSessionDigest instanceof String ? (String) configSessionDigest : null; + if (Boolean.parseBoolean(sessionDigest)) { + nonceManager = new PersistentNonceManager(NonceManagerUtils.DEFAULT_VALIDITY_PERIOD, NonceManagerUtils.DEFAULT_NONCE_SESSION_TIME, true, NonceManagerUtils.DEFAULT_KEY_SIZE, SHA256, ElytronMessages.httpDigest); + } } switch (mechanismName) { diff --git a/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManager.java b/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManager.java index ce998f9c6d5..45cabc981fd 100644 --- a/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManager.java +++ b/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManager.java @@ -18,26 +18,21 @@ package org.wildfly.security.http.digest; -import java.nio.ByteBuffer; import java.security.DigestException; -import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.wildfly.common.iteration.ByteIterator; -import org.wildfly.common.iteration.CodePointIterator; import org.wildfly.security.mechanism._private.ElytronMessages; import org.wildfly.security.mechanism.AuthenticationMechanismException; +import static org.wildfly.security.http.HttpConstants.SHA256; + /** * A utility responsible for managing nonces. * @@ -45,11 +40,9 @@ */ public class NonceManager { - private static final int PREFIX_LENGTH = Integer.BYTES + Long.BYTES; - private final ScheduledExecutorService executor; private final AtomicInteger nonceCounter = new AtomicInteger(); - private final Map usedNonces = new HashMap<>(); + private final Map usedNonces = new HashMap<>(); private final byte[] privateKey; @@ -59,6 +52,22 @@ public class NonceManager { private final String algorithm; private ElytronMessages log; + /** + * initialize with default values + */ + NonceManager() { + this.validityPeriodNano = NonceManagerUtils.DEFAULT_VALIDITY_PERIOD * 1000000; + nonceSessionTime = NonceManagerUtils.DEFAULT_NONCE_SESSION_TIME; + singleUse = true; + this.privateKey = new byte[NonceManagerUtils.DEFAULT_KEY_SIZE]; + new SecureRandom().nextBytes(privateKey); + algorithm = SHA256; + log = ElytronMessages.httpDigest; + ScheduledThreadPoolExecutor INSTANCE = new ScheduledThreadPoolExecutor(1); + INSTANCE.setRemoveOnCancelPolicy(true); + INSTANCE.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor = INSTANCE; + } /** * @param validityPeriod the time in ms that nonces are valid for in ms. @@ -152,32 +161,11 @@ String generateNonce() { * @return a new encoded nonce to send to the client. */ String generateNonce(byte[] salt) { - try { - MessageDigest messageDigest = MessageDigest.getInstance(algorithm); - - ByteBuffer byteBuffer = ByteBuffer.allocate(PREFIX_LENGTH + messageDigest.getDigestLength()); - byteBuffer.putInt(nonceCounter.incrementAndGet()); - byteBuffer.putLong(System.nanoTime()); - byteBuffer.put(digest(byteBuffer.array(), 0, PREFIX_LENGTH, salt, messageDigest)); - - String nonce = ByteIterator.ofBytes(byteBuffer.array()).base64Encode().drainToString(); - if (log.isTraceEnabled()) { - String saltString = salt == null ? "null" : ByteIterator.ofBytes(salt).hexEncode().drainToString(); - log.tracef("New nonce generated %s, using seed %s", nonce, saltString); - } - return nonce; - } catch (GeneralSecurityException e) { - throw new IllegalStateException(e); - } + return NonceManagerUtils.generateNonce(salt, algorithm, nonceCounter, privateKey); } private byte[] digest(byte[] prefix, int prefixOffset, int prefixLength, byte[] salt, MessageDigest messageDigest) throws DigestException { - messageDigest.update(prefix, prefixOffset, prefixLength); - if (salt != null) { - messageDigest.update(salt); - } - - return messageDigest.digest(privateKey); + return NonceManagerUtils.digest(prefix, prefixOffset, prefixLength, salt, messageDigest, privateKey); } /** @@ -217,106 +205,10 @@ boolean useNonce(String nonce, int nonceCount) throws AuthenticationMechanismExc * @throws AuthenticationMechanismException */ boolean useNonce(final String nonce, byte[] salt, int nonceCount) throws AuthenticationMechanismException { - try { - MessageDigest messageDigest = MessageDigest.getInstance(algorithm); - ByteIterator byteIterator = CodePointIterator.ofChars(nonce.toCharArray()).base64Decode(); - byte[] nonceBytes = byteIterator.drain(); - if (nonceBytes.length != PREFIX_LENGTH + messageDigest.getDigestLength()) { - throw log.invalidNonceLength(); - } - - byte[] nonceBytesWithoutPrefix = Arrays.copyOfRange(nonceBytes, PREFIX_LENGTH, nonceBytes.length); - byte[] expectedNonce = digest(nonceBytes, 0, PREFIX_LENGTH, salt, messageDigest); - if (MessageDigest.isEqual(nonceBytesWithoutPrefix, expectedNonce) == false) { - if (log.isTraceEnabled()) { - String saltString = salt == null ? "null" : ByteIterator.ofBytes(salt).hexEncode().drainToString(); - log.tracef("Nonce %s rejected due to failed comparison using secret key with seed %s.", nonce, - saltString); - } - return false; - } - - long age = System.nanoTime() - ByteBuffer.wrap(nonceBytes, Integer.BYTES, Long.BYTES).getLong(); - if(nonceCount > 0) { - synchronized (usedNonces) { - NonceState nonceState = usedNonces.get(nonce); - if (nonceState != null && nonceState.highestNonceCount < 0) { - log.tracef("Nonce %s rejected due to previously being used without a nonce count", nonce); - return false; - } else if (nonceState != null) { - if (nonceCount > nonceState.highestNonceCount) { - if (nonceState.futureCleanup.cancel(true)) { - nonceState.highestNonceCount = nonceCount; - } else { - log.tracef("Nonce %s rejected as unable to cancel clean up, likely at expiration time", nonce); - return false; - } - } else { - log.tracef("Nonce %s rejected due to highest seen nonce count %d being equal to or higher than the nonce count received %d", - nonce, nonceState.highestNonceCount, nonceCount); - return false; - } - } else { - if (age < 0 || age > validityPeriodNano) { - log.tracef("Nonce %s rejected due to age %d (ns) being less than 0 or greater than the validity period %d (ns)", - nonce, age, validityPeriodNano); - return false; - } - nonceState = new NonceState(); - nonceState.highestNonceCount = nonceCount; - usedNonces.put(nonce, nonceState); - if (log.isTraceEnabled()) { - log.tracef("Currently %d nonces being tracked", usedNonces.size()); - } - } - - nonceState.futureCleanup = executor.schedule(() -> { - synchronized (usedNonces) { - usedNonces.remove(nonce); - } - }, nonceSessionTime, TimeUnit.MILLISECONDS); - } - } else { - if (age < 0 || age > validityPeriodNano) { - log.tracef("Nonce %s rejected due to age %d (ns) being less than 0 or greater than the validity period %d (ns)", nonce, age, validityPeriodNano); - return false; - } - - if (singleUse) { - synchronized(usedNonces) { - NonceState nonceState = usedNonces.get(nonce); - if (nonceState != null) { - log.tracef("Nonce %s rejected due to previously being used", nonce); - return false; - } else { - nonceState = new NonceState(); - usedNonces.put(nonce, nonceState); - if (log.isTraceEnabled()) { - log.tracef("Currently %d nonces being tracked", usedNonces.size()); - } - executor.schedule(() -> { - synchronized(usedNonces) { - usedNonces.remove(nonce); - } - }, validityPeriodNano - age, TimeUnit.NANOSECONDS); - } - } - } - } - - return true; - - } catch (GeneralSecurityException e) { - throw new IllegalStateException(e); - } + return NonceManagerUtils.useNonce(nonce, salt, nonceCount, algorithm, privateKey, usedNonces, validityPeriodNano, executor, singleUse, nonceSessionTime); } public void shutdown() { if (executor != null) { executor.shutdown(); } } - - private static class NonceState { - private ScheduledFuture futureCleanup; - private int highestNonceCount = -1; - } } diff --git a/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManagerUtils.java b/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManagerUtils.java new file mode 100644 index 00000000000..b0e0fc3f0f1 --- /dev/null +++ b/http/digest/src/main/java/org/wildfly/security/http/digest/NonceManagerUtils.java @@ -0,0 +1,231 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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. + */ +package org.wildfly.security.http.digest; + +import org.wildfly.common.iteration.ByteIterator; +import org.wildfly.common.iteration.CodePointIterator; +import org.wildfly.security.mechanism.AuthenticationMechanismException; +import org.wildfly.security.mechanism._private.ElytronMessages; + +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.security.DigestException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Utility methods used by Nonce Manager classes + */ +public class NonceManagerUtils { + + public static final int DEFAULT_VALIDITY_PERIOD = 300000; + public static final int DEFAULT_NONCE_SESSION_TIME = 900000; + public static final int DEFAULT_KEY_SIZE = 20; + + private static final int PREFIX_LENGTH = Integer.BYTES + Long.BYTES; + private static ElytronMessages log = ElytronMessages.httpDigest; + /** + * Generate a new encoded nonce to send to the client. + * + * @return a new encoded nonce to send to the client. + */ + static String generateNonce( String algorithm, AtomicInteger nonceCounter, byte[] privateKey) { + return generateNonce(null, algorithm, nonceCounter, privateKey); + } + + /** + * Generate a new encoded nonce to send to the client. + * + * @param salt additional data to use when creating the overall signature for the nonce. + * @return a new encoded nonce to send to the client. + */ + static String generateNonce(byte[] salt, String algorithm, AtomicInteger nonceCounter, byte[] privateKey ) { + try { + MessageDigest messageDigest = MessageDigest.getInstance(algorithm); + + ByteBuffer byteBuffer = ByteBuffer.allocate(PREFIX_LENGTH + messageDigest.getDigestLength()); + byteBuffer.putInt(nonceCounter.incrementAndGet()); + byteBuffer.putLong(System.nanoTime()); + byteBuffer.put(digest(byteBuffer.array(), 0, PREFIX_LENGTH, salt, messageDigest, privateKey)); + + String nonce = ByteIterator.ofBytes(byteBuffer.array()).base64Encode().drainToString(); + if (log.isTraceEnabled()) { + String saltString = salt == null ? "null" : ByteIterator.ofBytes(salt).hexEncode().drainToString(); + log.tracef("New nonce generated %s, using seed %s", nonce, saltString); + } + return nonce; + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } + + static byte[] digest(byte[] prefix, int prefixOffset, int prefixLength, byte[] salt, MessageDigest messageDigest, byte[] privateKey) throws DigestException { + messageDigest.update(prefix, prefixOffset, prefixLength); + if (salt != null) { + messageDigest.update(salt); + } + + return messageDigest.digest(privateKey); + } + + /** + * Attempt to use the supplied nonce. + * + * A nonce might not be usable for a couple of different reasons: - + * + *
    + *
  • It was created too far in the past. + *
  • Validation of the signature fails. + *
  • The nonce has been used previously and re-use is disabled. + *
+ * + * @param nonce the nonce supplied by the client. + * @param nonceCount the nonce count, or -1 if not present + * @return {@code true} if the nonce can be used, {@code false} otherwise. + * @throws AuthenticationMechanismException + */ + static boolean useNonce(String nonce, int nonceCount, String algorithm, byte[] privateKey, Map usedNonces, long validityPeriodNano, ScheduledExecutorService executor, boolean singleUse, long nonceSessionTim) throws AuthenticationMechanismException { + return useNonce(nonce, null, nonceCount, algorithm, privateKey, usedNonces, validityPeriodNano, executor, singleUse, nonceSessionTim); + } + + /** + * Attempt to use the supplied nonce. + * + * A nonce might not be usable for a couple of different reasons: - + * + *
    + *
  • It was created too far in the past. + *
  • Validation of the signature fails. + *
  • The nonce has been used previously and re-use is disabled. + *
+ * + * @param nonce the nonce supplied by the client. + * @param salt additional data to use when creating the overall signature for the nonce. + * @return {@code true} if the nonce can be used, {@code false} otherwise. + * @throws AuthenticationMechanismException + */ + static boolean useNonce(final String nonce, byte[] salt, int nonceCount, String algorithm, byte[] privateKey, Map usedNonces, long validityPeriodNano, ScheduledExecutorService executor, boolean singleUse, long nonceSessionTime) throws AuthenticationMechanismException { + try { + MessageDigest messageDigest = MessageDigest.getInstance(algorithm); + ByteIterator byteIterator = CodePointIterator.ofChars(nonce.toCharArray()).base64Decode(); + byte[] nonceBytes = byteIterator.drain(); + if (nonceBytes.length != PREFIX_LENGTH + messageDigest.getDigestLength()) { + throw log.invalidNonceLength(); + } + + byte[] nonceBytesWithoutPrefix = Arrays.copyOfRange(nonceBytes, PREFIX_LENGTH, nonceBytes.length); + byte[] expectedNonce = digest(nonceBytes, 0, PREFIX_LENGTH, salt, messageDigest, privateKey); + if (MessageDigest.isEqual(nonceBytesWithoutPrefix, expectedNonce) == false) { + if (log.isTraceEnabled()) { + String saltString = salt == null ? "null" : ByteIterator.ofBytes(salt).hexEncode().drainToString(); + log.tracef("Nonce %s rejected due to failed comparison using secret key with seed %s.", nonce, + saltString); + } + return false; + } + + long age = System.nanoTime() - ByteBuffer.wrap(nonceBytes, Integer.BYTES, Long.BYTES).getLong(); + if(nonceCount > 0) { + synchronized (usedNonces) { + NonceState nonceState = usedNonces.get(nonce); + if (nonceState != null && nonceState.highestNonceCount < 0) { + log.tracef("Nonce %s rejected due to previously being used without a nonce count", nonce); + return false; + } else if (nonceState != null) { + if (nonceCount > nonceState.highestNonceCount) { + if (nonceState.futureCleanup.cancel(true)) { + nonceState.highestNonceCount = nonceCount; + } else { + log.tracef("Nonce %s rejected as unable to cancel clean up, likely at expiration time", nonce); + return false; + } + } else { + log.tracef("Nonce %s rejected due to highest seen nonce count %d being equal to or higher than the nonce count received %d", + nonce, nonceState.highestNonceCount, nonceCount); + return false; + } + } else { + if (age < 0 || age > validityPeriodNano) { + log.tracef("Nonce %s rejected due to age %d (ns) being less than 0 or greater than the validity period %d (ns)", + nonce, age, validityPeriodNano); + return false; + } + nonceState = new NonceState(); + nonceState.highestNonceCount = nonceCount; + usedNonces.put(nonce, nonceState); + if (log.isTraceEnabled()) { + log.tracef("Currently %d nonces being tracked", usedNonces.size()); + } + } + + nonceState.futureCleanup = executor.schedule(() -> { + synchronized (usedNonces) { + usedNonces.remove(nonce); + } + }, nonceSessionTime, TimeUnit.MILLISECONDS); + } + } else { + if (age < 0 || age > validityPeriodNano) { + log.tracef("Nonce %s rejected due to age %d (ns) being less than 0 or greater than the validity period %d (ns)", nonce, age, validityPeriodNano); + return false; + } + + if (singleUse) { + synchronized(usedNonces) { + NonceState nonceState = usedNonces.get(nonce); + if (nonceState != null) { + log.tracef("Nonce %s rejected due to previously being used", nonce); + return false; + } else { + nonceState = new NonceState(); + usedNonces.put(nonce, nonceState); + if (log.isTraceEnabled()) { + log.tracef("Currently %d nonces being tracked", usedNonces.size()); + } + executor.schedule(() -> { + synchronized(usedNonces) { + usedNonces.remove(nonce); + } + }, validityPeriodNano - age, TimeUnit.NANOSECONDS); + } + } + } + } + + return true; + + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } + + public void shutdown(ScheduledExecutorService executor) { + if (executor != null) { executor.shutdown(); } + } + + static class NonceState implements Serializable { + private transient ScheduledFuture futureCleanup; + private int highestNonceCount = -1; + } +} diff --git a/http/digest/src/main/java/org/wildfly/security/http/digest/PersistentNonceManager.java b/http/digest/src/main/java/org/wildfly/security/http/digest/PersistentNonceManager.java new file mode 100644 index 00000000000..28fe36e6855 --- /dev/null +++ b/http/digest/src/main/java/org/wildfly/security/http/digest/PersistentNonceManager.java @@ -0,0 +1,204 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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. + */ +package org.wildfly.security.http.digest; + +import java.io.Serializable; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; + +import org.wildfly.security.mechanism.AuthenticationMechanismException; +import org.wildfly.security.mechanism._private.ElytronMessages; + +/** + * A utility responsible for managing nonces that can be stored in an HTTP session. + */ +public class PersistentNonceManager extends NonceManager implements Serializable { + + private transient ScheduledExecutorService executor; + private AtomicInteger nonceCounter = new AtomicInteger(); + private Map usedNonces = new HashMap<>(); + private byte[] privateKey; + private long validityPeriodNano; + private long nonceSessionTime; + private boolean singleUse; + private String algorithm; + private volatile ElytronMessages log; + + /** + * @param validityPeriod the time in ms that nonces are valid for in ms. + * @param nonceSessionTime the time in ms a nonce is usable for after it's last use where nonce counts are in use. + * @param singleUse are nonces single use? + * @param keySize the number of bytes to use in the private key of this node. + * @param algorithm the message digest algorithm to use when creating the digest portion of the nonce. + * @param log mechanism specific logger. + */ + PersistentNonceManager(long validityPeriod, long nonceSessionTime, boolean singleUse, int keySize, String algorithm, ElytronMessages log) { + this.validityPeriodNano = validityPeriod * 1000000; + this.nonceSessionTime = nonceSessionTime; + this.singleUse = singleUse; + this.algorithm = algorithm; + this.log = log; + this.privateKey = new byte[keySize]; + new SecureRandom().nextBytes(privateKey); + ScheduledThreadPoolExecutor INSTANCE = new ScheduledThreadPoolExecutor(1); + INSTANCE.setRemoveOnCancelPolicy(true); + INSTANCE.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor = INSTANCE; + } + + /** + * @param validityPeriod the time in ms that nonces are valid for in ms. + * @param nonceSessionTime the time in ms a nonce is usable for after it's last use where nonce counts are in use. + * @param singleUse are nonces single use? + * @param keySize the number of bytes to use in the private key of this node. + * @param algorithm the message digest algorithm to use when creating the digest portion of the nonce. + * @param log mechanism specific logger. + * @param customExecutor a custom ScheduledExecutorService to be used + */ + PersistentNonceManager(long validityPeriod, long nonceSessionTime, boolean singleUse, int keySize, String algorithm, ElytronMessages log, ScheduledExecutorService customExecutor) { + this.validityPeriodNano = validityPeriod * 1000000; + this.nonceSessionTime = nonceSessionTime; + this.singleUse = singleUse; + this.algorithm = algorithm; + this.log = log; + this.privateKey = new byte[keySize]; + new SecureRandom().nextBytes(privateKey); + if (customExecutor == null) { + ScheduledThreadPoolExecutor INSTANCE = new ScheduledThreadPoolExecutor(1); + INSTANCE.setRemoveOnCancelPolicy(true); + INSTANCE.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor = INSTANCE; + } + else { + executor = customExecutor; + } + } + + public void shutdown() { + if (this.executor != null) { this.executor.shutdown(); } + } + + String generateNonce() { + return generateNonce(null); + } + + /** + * Generate a new encoded nonce to send to the client. + * + * @param salt additional data to use when creating the overall signature for the nonce. + * @return a new encoded nonce to send to the client. + */ + String generateNonce(byte[] salt) { + return NonceManagerUtils.generateNonce(salt, algorithm, nonceCounter, privateKey ); + } + + /** + * Attempt to use the supplied nonce. + * + * A nonce might not be usable for a couple of different reasons: - + * + *
    + *
  • It was created too far in the past. + *
  • Validation of the signature fails. + *
  • The nonce has been used previously and re-use is disabled. + *
+ * + * @param nonce the nonce supplied by the client. + * @param nonceCount the nonce count, or -1 if not present + * @return {@code true} if the nonce can be used, {@code false} otherwise. + * @throws AuthenticationMechanismException + */ + boolean useNonce(String nonce, int nonceCount) throws AuthenticationMechanismException { + return useNonce(nonce, null, nonceCount); + } + + /** + * Attempt to use the supplied nonce. + * + * A nonce might not be usable for a couple of different reasons: - + * + *
    + *
  • It was created too far in the past. + *
  • Validation of the signature fails. + *
  • The nonce has been used previously and re-use is disabled. + *
+ * + * @param nonce the nonce supplied by the client. + * @param salt additional data to use when creating the overall signature for the nonce. + * @return {@code true} if the nonce can be used, {@code false} otherwise. + * @throws AuthenticationMechanismException + */ + boolean useNonce(final String nonce, byte[] salt, int nonceCount) throws AuthenticationMechanismException { + return NonceManagerUtils.useNonce(nonce, salt, nonceCount, algorithm, privateKey, usedNonces, validityPeriodNano, executor, singleUse, nonceSessionTime); + } + + ScheduledExecutorService getExecutor() { + return executor; + } + + AtomicInteger getNonceCounter() { + return nonceCounter; + } + + Map getUsedNonces() { + return usedNonces; + } + + byte[] getPrivateKey() { + return privateKey; + } + long getValidityPeriodNano() { + return validityPeriodNano; + } + + long getNonceSessionTime() { + return nonceSessionTime; + } + + String getAlgorithm() { + return algorithm; + } + + void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + boolean isSingleUse() { + return singleUse; + } + + /** + * Update attributes from provided PersistentNonceManager to this instance + * + * @param persistentNonceManager PersistentNonceManager instance to update the attributes from + */ + void refreshInfoFromSessionNonceManager(PersistentNonceManager persistentNonceManager) { + this.validityPeriodNano = persistentNonceManager.getValidityPeriodNano(); + this.nonceSessionTime = persistentNonceManager.getNonceSessionTime(); + this.algorithm = persistentNonceManager.getAlgorithm(); + this.nonceCounter = persistentNonceManager.getNonceCounter(); + this.usedNonces = persistentNonceManager.getUsedNonces(); + this.privateKey = persistentNonceManager.getPrivateKey(); + this.singleUse = persistentNonceManager.isSingleUse(); + } +} diff --git a/tests/base/src/test/java/org/wildfly/security/http/digest/DigestAuthenticationMechanismTest.java b/tests/base/src/test/java/org/wildfly/security/http/digest/DigestAuthenticationMechanismTest.java index 5f8964d460b..16e55ca51b7 100644 --- a/tests/base/src/test/java/org/wildfly/security/http/digest/DigestAuthenticationMechanismTest.java +++ b/tests/base/src/test/java/org/wildfly/security/http/digest/DigestAuthenticationMechanismTest.java @@ -28,6 +28,7 @@ import org.wildfly.security.http.HttpServerAuthenticationMechanism; import org.wildfly.security.http.impl.AbstractBaseHttpTest; +import java.lang.reflect.Field; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -38,6 +39,7 @@ import java.util.Map; import static org.wildfly.security.http.HttpConstants.CONFIG_REALM; +import static org.wildfly.security.http.HttpConstants.CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER; import static org.wildfly.security.http.HttpConstants.DIGEST_NAME; import static org.wildfly.security.http.HttpConstants.SHA256; import static org.wildfly.security.http.HttpConstants.SHA512_256; @@ -259,6 +261,57 @@ public void testSha256WithDigestPassword() throws Exception { Assert.assertEquals(Status.COMPLETE, request2.getResult()); } + @Test + public void testSessionDigestAttributeUsesPersistedNonceManager() throws Exception { + mockDigestNonce("AAAAAQABsxiWa25/kpFxsPCrpDCFsjkTzs/Xr7RPsi/VVN6faYp21Hia3h4="); + Map props = new HashMap<>(); + props.put(CONFIG_REALM, "testrealm@host.com"); + props.put("org.wildfly.security.http.validate-digest-uri", "false"); + props.put(CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER, "true"); // persist nonce manager in HTTP session + digestFactory.createAuthenticationMechanism(DIGEST_NAME, props, getCallbackHandler("Mufasa", "testrealm@host.com", "Circle Of Life")); + + Field f = DigestMechanismFactory.class.getDeclaredField("nonceManager"); + f.setAccessible(true); + Assert.assertTrue(f.get(NonceManager.class) instanceof PersistentNonceManager); + } + + @Test + public void testPersistedNonceManager() throws Exception { + + mockDigestNonce("AAAAAQABsxiWa25/kpFxsPCrpDCFsjkTzs/Xr7RPsi/VVN6faYp21Hia3h4="); + Map props = new HashMap<>(); + props.put(CONFIG_REALM, "testrealm@host.com"); + props.put("org.wildfly.security.http.validate-digest-uri", "false"); + props.put(CONFIG_SESSION_BASED_DIGEST_NONCE_MANAGER, "true"); // persist nonce manager in HTTP session + HttpServerAuthenticationMechanism mechanism = digestFactory.createAuthenticationMechanism(DIGEST_NAME, props, getCallbackHandler("Mufasa", "testrealm@host.com", "Circle Of Life")); + + Field f = DigestMechanismFactory.class.getDeclaredField("nonceManager"); + f.setAccessible(true); + Assert.assertTrue(f.get(NonceManager.class) instanceof PersistentNonceManager); + + TestingHttpServerRequest request1 = new TestingHttpServerRequest(null); + mechanism.evaluateRequest(request1); + Assert.assertEquals(Status.NO_AUTH, request1.getResult()); + TestingHttpServerResponse response = request1.getResponse(); + Assert.assertEquals(UNAUTHORIZED, response.getStatusCode()); + Assert.assertEquals("Digest realm=\"testrealm@host.com\", nonce=\"AAAAAQABsxiWa25/kpFxsPCrpDCFsjkTzs/Xr7RPsi/VVN6faYp21Hia3h4=\", opaque=\"00000000000000000000000000000000\", algorithm=MD5, qop=auth", response.getAuthenticateHeader()); + + TestingHttpServerRequest request2 = new TestingHttpServerRequest(new String[] { + "Digest username=\"Mufasa\",\n" + + " realm=\"testrealm@host.com\",\n" + + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",\n" + + " uri=\"/dir/index.html\",\n" + + " qop=auth,\n" + + " nc=00000001,\n" + + " cnonce=\"0a4f113b\",\n" + + " response=\"6629fae49393a05397450978507c4ef1\",\n" + + " opaque=\"00000000000000000000000000000000\",\n" + + " algorithm=MD5" + }); + mechanism.evaluateRequest(request2); + Assert.assertEquals(Status.COMPLETE, request2.getResult()); + } + private String computeDigest(String uri, String nonce, String cnonce, String nc, String username, String password, String algorithm, String realm, String qop, String method) throws NoSuchAlgorithmException { String A1, HashA1, A2, HashA2; MessageDigest md = MessageDigest.getInstance(algorithm); diff --git a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java index 079841980d8..26cc286ee8d 100644 --- a/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java +++ b/tests/base/src/test/java/org/wildfly/security/http/impl/AbstractBaseHttpTest.java @@ -81,6 +81,7 @@ import org.wildfly.security.http.bearer.BearerMechanismFactory; import org.wildfly.security.http.digest.DigestMechanismFactory; import org.wildfly.security.http.digest.NonceManager; +import org.wildfly.security.http.digest.PersistentNonceManager; import org.wildfly.security.http.external.ExternalMechanismFactory; import org.wildfly.security.password.Password; import org.wildfly.security.password.PasswordFactory; @@ -114,6 +115,16 @@ boolean useNonce(final String nonce, byte[] salt, int nonceCount) { return true; } }; + new MockUp(){ + @Mock + String generateNonce(byte[] salt) { + return nonce; + } + @Mock + boolean useNonce(final String nonce, byte[] salt, int nonceCount) { + return true; + } + }; } protected SecurityIdentity mockSecurityIdentity(Principal p) {