Add certificate timestamp validation (#6330)
KEYCLOAK-11818 Add certificate timestamp validation
This commit is contained in:
parent
d6c5f79f2c
commit
b90a0307ea
7 changed files with 230 additions and 15 deletions
|
@ -61,6 +61,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled";
|
||||
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";
|
||||
public static final String SERIALNUMBER_HEX = "x509-cert-auth.serialnumber-hex-enabled";
|
||||
public static final String CRL_RELATIVE_PATH = "x509-cert-auth.crl-relative-path";
|
||||
public static final String OCSPRESPONDER_URI = "x509-cert-auth.ocsp-responder-uri";
|
||||
|
|
|
@ -140,6 +140,13 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
|
|||
attributeOrPropertyValue.setHelpText("A name of user attribute to map the extracted user identity to existing user. The name must be a valid, existing user attribute if User Mapping Method is set to Custom Attribute Mapper. " +
|
||||
"Multiple values are relevant when attribute mapping is related to multiple values, e.g. 'Certificate Serial Number and IssuerDN'");
|
||||
|
||||
ProviderConfigProperty timestampValidationValue = new ProviderConfigProperty();
|
||||
timestampValidationValue.setType(BOOLEAN_TYPE);
|
||||
timestampValidationValue.setName(TIMESTAMP_VALIDATION);
|
||||
timestampValidationValue.setLabel("Check certificate validity");
|
||||
timestampValidationValue.setDefaultValue(true);
|
||||
timestampValidationValue.setHelpText("Will verify that the certificate has not expired yet and is already valid by checking the attributes 'notBefore' and 'notAfter'.");
|
||||
|
||||
ProviderConfigProperty crlCheckingEnabled = new ProviderConfigProperty();
|
||||
crlCheckingEnabled.setType(BOOLEAN_TYPE);
|
||||
crlCheckingEnabled.setName(ENABLE_CRL);
|
||||
|
@ -206,6 +213,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
|
|||
regExp,
|
||||
userMapperList,
|
||||
attributeOrPropertyValue,
|
||||
timestampValidationValue,
|
||||
crlCheckingEnabled,
|
||||
crlDPEnabled,
|
||||
cRLRelativePath,
|
||||
|
|
|
@ -18,11 +18,15 @@
|
|||
|
||||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.utils.CRLUtils;
|
||||
import org.keycloak.common.util.OCSPUtils;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
import org.keycloak.utils.CRLUtils;
|
||||
|
||||
import javax.naming.Context;
|
||||
import javax.naming.NamingException;
|
||||
|
@ -30,37 +34,33 @@ import javax.naming.directory.Attribute;
|
|||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.InitialDirContext;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLConnection;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.CRLException;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509CRL;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CRLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
||||
import org.keycloak.saml.processing.core.util.XMLSignatureUtil;
|
||||
import org.keycloak.truststore.TruststoreProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
|
||||
|
@ -468,6 +468,32 @@ public class CertificateValidator {
|
|||
return this;
|
||||
}
|
||||
|
||||
public CertificateValidator validateTimestamps(boolean isValidationEnabled) throws GeneralSecurityException {
|
||||
if (!isValidationEnabled) {
|
||||
return this;
|
||||
}
|
||||
for (int i = 0; i < _certChain.length; i++)
|
||||
{
|
||||
X509Certificate x509Certificate = _certChain[i];
|
||||
if (x509Certificate.getNotBefore().getTime() > Time.currentTimeMillis()) {
|
||||
String serialNumber = x509Certificate.getSerialNumber().toString(16).replaceAll("..(?!$)",
|
||||
"$0 ");
|
||||
String message =
|
||||
"certificate with serialnumber '" + serialNumber
|
||||
+ "' is not valid yet: " + x509Certificate.getNotBefore().toString();
|
||||
throw new GeneralSecurityException(message);
|
||||
}
|
||||
if (x509Certificate.getNotAfter().getTime() < Time.currentTimeMillis()) {
|
||||
String serialNumber = x509Certificate.getSerialNumber().toString(16).replaceAll("..(?!$)",
|
||||
"$0 ");
|
||||
String message = "certificate with serialnumber '" + serialNumber
|
||||
+ "' has expired on: " + x509Certificate.getNotAfter().toString();
|
||||
throw new GeneralSecurityException(message);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private X509Certificate findCAInTruststore(X500Principal issuer) throws GeneralSecurityException {
|
||||
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
|
||||
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
|
||||
|
|
|
@ -254,6 +254,15 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
public boolean isCertValidationEnabled() {
|
||||
return Boolean.parseBoolean(getConfig().get(TIMESTAMP_VALIDATION));
|
||||
}
|
||||
|
||||
public X509AuthenticatorConfigModel setCertValidationEnabled(boolean value) {
|
||||
getConfig().put(TIMESTAMP_VALIDATION, Boolean.toString(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSerialnumberHex() {
|
||||
return Boolean.parseBoolean(getConfig().get(SERIALNUMBER_HEX));
|
||||
}
|
||||
|
|
|
@ -84,7 +84,8 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
|
|||
CertificateValidator validator = builder.build(certs);
|
||||
validator.checkRevocationStatus()
|
||||
.validateKeyUsage()
|
||||
.validateExtendedKeyUsage();
|
||||
.validateExtendedKeyUsage()
|
||||
.validateTimestamps(config.isCertValidationEnabled());
|
||||
} catch(Exception e) {
|
||||
logger.error(e.getMessage(), e);
|
||||
// TODO use specific locale to load error messages
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* author Pascal Knueppel <br>
|
||||
* created at: 07.11.2019 - 16:24 <br>
|
||||
* <br>
|
||||
*
|
||||
*/
|
||||
public class CertificateValidatorTest {
|
||||
|
||||
private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
|
||||
|
||||
/**
|
||||
* will validate that the certificate validation succeeds if the certificate is currently valid
|
||||
*/
|
||||
@Test
|
||||
public void testValidityOfCertificatesSuccess() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(512);
|
||||
KeyPair keyPair = kpg.generateKeyPair();
|
||||
X509Certificate certificate =
|
||||
createCertificate("CN=keycloak-test", new Date(),
|
||||
new Date(System.currentTimeMillis() + 1000L * 60), keyPair);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
CertificateValidator validator = builder.build(new X509Certificate[] { certificate });
|
||||
try {
|
||||
validator.validateTimestamps(true);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
Assert.fail(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* will validate that the certificate validation throws an exception if the certificate is not valid yet
|
||||
*/
|
||||
@Test
|
||||
public void testValidityOfCertificatesNotValidYet() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(512);
|
||||
KeyPair keyPair = kpg.generateKeyPair();
|
||||
X509Certificate certificate =
|
||||
createCertificate("CN=keycloak-test", new Date(System.currentTimeMillis() + 1000L * 60),
|
||||
new Date(System.currentTimeMillis() + 1000L * 60), keyPair);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
CertificateValidator validator = builder.build(new X509Certificate[] { certificate });
|
||||
try {
|
||||
validator.validateTimestamps(true);
|
||||
Assert.fail("certificate validation must fail for certificate is not valid yet");
|
||||
} catch (Exception ex) {
|
||||
MatcherAssert.assertThat(ex.getMessage(), Matchers.containsString("not valid yet"));
|
||||
Assert.assertEquals(GeneralSecurityException.class, ex.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* will validate that the certificate validation throws an exception if the certificate has expired
|
||||
*/
|
||||
@Test
|
||||
public void testValidityOfCertificatesHasExpired() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(512);
|
||||
KeyPair keyPair = kpg.generateKeyPair();
|
||||
X509Certificate certificate =
|
||||
createCertificate("CN=keycloak-test", new Date(System.currentTimeMillis() - 1000L * 60 * 2),
|
||||
new Date(System.currentTimeMillis() - 1000L * 60), keyPair);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
CertificateValidator validator = builder.build(new X509Certificate[] { certificate });
|
||||
try {
|
||||
validator.validateTimestamps(true);
|
||||
Assert.fail("certificate validation must fail for certificate has expired");
|
||||
} catch (Exception ex) {
|
||||
MatcherAssert.assertThat(ex.getMessage(), Matchers.containsString("has expired"));
|
||||
Assert.assertEquals(GeneralSecurityException.class, ex.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* will create a self-signed certificate
|
||||
*
|
||||
* @param dn the DN of the subject and issuer
|
||||
* @param startDate startdate of the validity of the created certificate
|
||||
* @param expiryDate expiration date of the created certificate
|
||||
* @param keyPair the keypair that is used to create the certificate
|
||||
* @return a X509-Certificate in version 3
|
||||
*/
|
||||
public X509Certificate createCertificate(String dn,
|
||||
Date startDate,
|
||||
Date expiryDate,
|
||||
KeyPair keyPair) {
|
||||
X500Name subjectDN = new X500Name(dn);
|
||||
X500Name issuerDN = new X500Name(dn);
|
||||
// @formatter:off
|
||||
SubjectPublicKeyInfo subjPubKeyInfo = SubjectPublicKeyInfo.getInstance(
|
||||
ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
|
||||
// @formatter:on
|
||||
BigInteger serialNumber = new BigInteger(130, new SecureRandom());
|
||||
|
||||
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate,
|
||||
subjectDN, subjPubKeyInfo);
|
||||
ContentSigner contentSigner = null;
|
||||
try {
|
||||
// @formatter:off
|
||||
contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.setProvider(BOUNCY_CASTLE_PROVIDER)
|
||||
.build(keyPair.getPrivate());
|
||||
X509Certificate x509Certificate = new JcaX509CertificateConverter()
|
||||
.setProvider(BOUNCY_CASTLE_PROVIDER)
|
||||
.getCertificate(certGen.build(contentSigner));
|
||||
// @formatter:on
|
||||
return x509Certificate;
|
||||
} catch (CertificateException | OperatorCreationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* author Pascal Knueppel <br>
|
||||
* created at: 02.12.2019 - 10:59 <br>
|
||||
* <br>
|
||||
*
|
||||
*/
|
||||
public class X509AuthenticatorConfigModelTest {
|
||||
|
||||
/**
|
||||
* this test will verify that no exception occurs if no settings are stored for the timestamp validation
|
||||
*/
|
||||
@Test
|
||||
public void testTimestampValidationAttributeReturnsNull() {
|
||||
X509AuthenticatorConfigModel configModel = new X509AuthenticatorConfigModel();
|
||||
Assert.assertNull(configModel.getConfig().get(AbstractX509ClientCertificateAuthenticator.TIMESTAMP_VALIDATION));
|
||||
Assert.assertFalse(configModel.isCertValidationEnabled());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue