KEYCLOAK-16456 X509 Auth: add option for OCSP fail-open behavior

This commit is contained in:
Luca Leonardo Scorcia 2020-11-25 05:28:33 -05:00 committed by Marek Posolda
parent b0e5c38775
commit 43a3c676f7
5 changed files with 172 additions and 18 deletions

View file

@ -59,6 +59,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
public static final String REGULAR_EXPRESSION = "x509-cert-auth.regular-expression";
public static final String ENABLE_CRL = "x509-cert-auth.crl-checking-enabled";
public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled";
public static final String OCSP_FAIL_OPEN = "x509-cert-auth.ocsp-fail-open";
public static final String ENABLE_CRLDP = "x509-cert-auth.crldp-checking-enabled";
public static final String CANONICAL_DN = "x509-cert-auth.canonical-dn-enabled";
public static final String TIMESTAMP_VALIDATION = "x509-cert-auth.timestamp-validation-enabled";
@ -116,6 +117,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
.cRLDPEnabled(config.getCRLDistributionPointEnabled())
.cRLrelativePath(config.getCRLRelativePath())
.oCSPEnabled(config.getOCSPEnabled())
.oCSPFailOpen(config.getOCSPFailOpen())
.oCSPResponseCertificate(config.getOCSPResponderCertificate())
.oCSPResponderURI(config.getOCSPResponder())
.trustValidation()

View file

@ -182,6 +182,13 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
oCspCheckingEnabled.setHelpText("Enable Certificate Revocation Checking using OCSP");
oCspCheckingEnabled.setLabel("OCSP Checking Enabled");
ProviderConfigProperty ocspFailOpen = new ProviderConfigProperty();
ocspFailOpen.setType(BOOLEAN_TYPE);
ocspFailOpen.setName(OCSP_FAIL_OPEN);
ocspFailOpen.setDefaultValue(Boolean.toString(false));
ocspFailOpen.setHelpText("Whether to allow or deny authentication for client certificates that have missing/invalid/inconclusive OCSP endpoints. By default a successful OCSP response is required.");
ocspFailOpen.setLabel("OCSP Fail-Open Behavior");
ProviderConfigProperty ocspResponderUri = new ProviderConfigProperty();
ocspResponderUri.setType(STRING_TYPE);
ocspResponderUri.setName(OCSPRESPONDER_URI);
@ -245,6 +252,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
crlDPEnabled,
cRLRelativePath,
oCspCheckingEnabled,
ocspFailOpen,
ocspResponderUri,
ocspResponderCert,
keyUsage,

View file

@ -403,6 +403,7 @@ public class CertificateValidator {
boolean _crldpEnabled;
CRLLoaderImpl _crlLoader;
boolean _ocspEnabled;
boolean _ocspFailOpen;
OCSPChecker ocspChecker;
boolean _timestampValidationEnabled;
boolean _trustValidationEnabled;
@ -417,6 +418,7 @@ public class CertificateValidator {
boolean cRLDPCheckingEnabled,
CRLLoaderImpl crlLoader,
boolean oCSPCheckingEnabled,
boolean ocspFailOpen,
OCSPChecker ocspChecker,
KeycloakSession session,
boolean timestampValidationEnabled,
@ -430,6 +432,7 @@ public class CertificateValidator {
_crldpEnabled = cRLDPCheckingEnabled;
_crlLoader = crlLoader;
_ocspEnabled = oCSPCheckingEnabled;
_ocspFailOpen = ocspFailOpen;
this.ocspChecker = ocspChecker;
this.session = session;
_timestampValidationEnabled = timestampValidationEnabled;
@ -705,25 +708,38 @@ public class CertificateValidator {
}
}
OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);
try {
OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);
if (rs == null) {
throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
}
if (rs == null) {
if (_ocspFailOpen)
logger.warnf("Unable to check client revocation status using OCSP - continuing certificate authentication because of fail-open OCSP configuration setting");
else
throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
}
if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.UNKNOWN) {
throw new GeneralSecurityException("Unable to determine certificate's revocation status.");
}
else if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.REVOKED) {
if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.UNKNOWN) {
if (_ocspFailOpen)
logger.warnf("Unable to determine certificate's revocation status - continuing certificate authentication because of fail-open OCSP configuration setting");
else
throw new GeneralSecurityException("Unable to determine certificate's revocation status.");
}
else if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.REVOKED) {
StringBuilder sb = new StringBuilder();
sb.append("Certificate's been revoked.");
sb.append("\n");
sb.append(rs.getRevocationReason().toString());
sb.append("\n");
sb.append(String.format("Revoked on: %s",rs.getRevocationTime().toString()));
StringBuilder sb = new StringBuilder();
sb.append("Certificate's been revoked.");
sb.append("\n");
sb.append(rs.getRevocationReason().toString());
sb.append("\n");
sb.append(String.format("Revoked on: %s",rs.getRevocationTime().toString()));
throw new GeneralSecurityException(sb.toString());
throw new GeneralSecurityException(sb.toString());
}
} catch (CertPathValidatorException e) {
if (_ocspFailOpen)
logger.warnf("Unable to check client revocation status using OCSP - continuing certificate authentication because of fail-open OCSP configuration setting");
else
throw e;
}
}
@ -792,6 +808,7 @@ public class CertificateValidator {
boolean _crldpEnabled;
CRLLoaderImpl _crlLoader;
boolean _ocspEnabled;
boolean _ocspFailOpen;
String _responderUri;
X509Certificate _responderCert;
boolean _timestampValidationEnabled;
@ -967,7 +984,14 @@ public class CertificateValidator {
}
public class GotOCSP {
public GotOCSP oCSPResponseCertificate(String responderCert) {
public GotOCSPFailOpen oCSPFailOpen(boolean ocspFailOpen) {
_ocspFailOpen = ocspFailOpen;
return new GotOCSPFailOpen();
}
}
public class GotOCSPFailOpen {
public GotOCSPFailOpen oCSPResponseCertificate(String responderCert) {
if (responderCert != null && !responderCert.isEmpty()) {
try {
_responderCert = XMLSignatureUtil.getX509CertificateFromKeyInfoString(responderCert);
@ -979,7 +1003,7 @@ public class CertificateValidator {
throw new RuntimeException(e);
}
}
return new GotOCSP();
return new GotOCSPFailOpen();
}
public CertificateValidatorBuilder oCSPResponderURI(String responderURI) {
@ -1050,7 +1074,7 @@ public class CertificateValidator {
}
return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage,
_certificatePolicy, _certificatePolicyMode,
_crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled,
_crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled, _ocspFailOpen,
new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled, _trustValidationEnabled);
}
}

View file

@ -130,6 +130,15 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
return this;
}
public boolean getOCSPFailOpen() {
return Boolean.parseBoolean(getConfig().getOrDefault(OCSP_FAIL_OPEN, Boolean.toString(false)));
}
public X509AuthenticatorConfigModel setOCSPFailOpen(boolean value) {
getConfig().put(OCSP_FAIL_OPEN, Boolean.toString(value));
return this;
}
public boolean getCRLDistributionPointEnabled() {
return Boolean.parseBoolean(getConfig().get(ENABLE_CRLDP));
}

View file

@ -0,0 +1,111 @@
package org.keycloak.testsuite.x509;
import com.google.common.base.Charsets;
import io.undertow.Undertow;
import io.undertow.server.handlers.BlockingHandler;
import java.nio.file.Paths;
import java.util.function.Supplier;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.testsuite.util.PhantomJSBrowser;
import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel;
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.WebDriver;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL;
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
public class X509OCSPResponderFailOpenTest extends AbstractX509AuthenticationTest {
private static final String OCSP_RESPONDER_HOST = "localhost";
private static final int OCSP_RESPONDER_PORT = 8888;
private Undertow ocspResponder;
@Drone
@PhantomJSBrowser
private WebDriver phantomJS;
@Before
public void replaceTheDefaultDriver() {
replaceDefaultWebDriver(phantomJS);
}
@Test
public void ocspFailCloseLoginFailed() throws Exception {
// Test of OCSP failure (invalid OCSP responder host) when OCSP Fail-Open is set to OFF
// If test is successful, it should return an auth error
X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel()
.setOCSPEnabled(true)
.setOCSPResponder("http://" + OCSP_RESPONDER_HOST + ".invalid.host:" + OCSP_RESPONDER_PORT + "/oscp")
.setOCSPFailOpen(false)
.setMappingSourceType(SUBJECTDN_EMAIL)
.setUserIdentityMapperType(USERNAME_EMAIL);
AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
String cfgId = createConfig(directGrantExecution.getId(), cfg);
Assert.assertNotNull(cfgId);
oauth.clientId("resource-owner");
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatusCode());
assertEquals("invalid_request", response.getError());
// Make sure we got the right error
Assert.assertThat(response.getErrorDescription(), containsString("OCSP check failed"));
}
@Test
public void ocspFailOpenLoginSuccess() throws Exception {
// Test of OCSP failure (invalid OCSP responder host) when OCSP Fail-Open is set to ON
// If test is successful, it should continue the login
X509AuthenticatorConfigModel config =
new X509AuthenticatorConfigModel()
.setOCSPEnabled(true)
.setOCSPFailOpen(true)
.setMappingSourceType(SUBJECTDN_EMAIL)
.setOCSPResponder("http://" + OCSP_RESPONDER_HOST + ".invalid.host:" + OCSP_RESPONDER_PORT + "/oscp")
.setOCSPResponderCertificate(
IOUtils.toString(this.getClass().getResourceAsStream(OcspHandler.OCSP_RESPONDER_CERT_PATH), Charsets.UTF_8)
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", ""))
.setUserIdentityMapperType(USERNAME_EMAIL);
AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
String cfgId = createConfig(directGrantExecution.getId(), cfg);
Assert.assertNotNull(cfgId);
String keyStorePath = Paths.get(System.getProperty("client.certificate.keystore"))
.getParent().resolve("client-ca.jks").toString();
String keyStorePassword = System.getProperty("client.certificate.keystore.passphrase");
String trustStorePath = System.getProperty("client.truststore");
String trustStorePassword = System.getProperty("client.truststore.passphrase");
Supplier<CloseableHttpClient> previous = oauth.getHttpClient();
try {
oauth.clientId("resource-owner");
oauth.httpClient(() -> OAuthClient.newCloseableHttpClientSSL(keyStorePath, keyStorePassword, trustStorePath, trustStorePassword));
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
// Make sure authentication is allowed
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
} finally {
oauth.httpClient(previous);
}
}
}