Add certificate timestamp validation (#6330)

KEYCLOAK-11818 Add certificate timestamp validation
This commit is contained in:
Captain-P-Goldfish 2020-01-22 20:53:06 +01:00 committed by Marek Posolda
parent d6c5f79f2c
commit b90a0307ea
7 changed files with 230 additions and 15 deletions

View file

@ -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";

View file

@ -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,

View file

@ -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) {

View file

@ -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));
}

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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());
}
}