/*
* Copyright 2016 Analytical Graphics, Inc. and/or its affiliates
* and other 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.keycloak.authentication.authenticators.x509;
import org.keycloak.common.util.CRLUtils;
import org.keycloak.common.util.OCSPUtils;
import org.keycloak.services.ServicesLogger;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
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.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.cert.CertPathValidatorException;
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.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;
/**
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
* @version $Revision: 1 $
* @date 7/30/2016
*/
public class CertificateValidator {
private static final ServicesLogger logger = ServicesLogger.LOGGER;
enum KeyUsageBits {
DIGITAL_SIGNATURE(0, "digitalSignature"),
NON_REPUDIATION(1, "nonRepudiation"),
KEY_ENCIPHERMENT(2, "keyEncipherment"),
DATA_ENCIPHERMENT(3, "dataEncipherment"),
KEY_AGREEMENT(4, "keyAgreement"),
KEYCERTSIGN(5, "keyCertSign"),
CRLSIGN(6, "cRLSign"),
ENCIPHERMENT_ONLY(7, "encipherOnly"),
DECIPHER_ONLY(8, "decipherOnly");
private int value;
private String name;
KeyUsageBits(int value, String name) {
if (value < 0 || value > 8)
throw new IllegalArgumentException("value");
if (name == null || name.trim().length() == 0)
throw new IllegalArgumentException("name");
this.value = value;
this.name = name.trim();
}
public int getInt() { return this.value; }
public String getName() { return this.name; }
static public KeyUsageBits parse(String name) throws IllegalArgumentException, IndexOutOfBoundsException {
if (name == null || name.trim().length() == 0)
throw new IllegalArgumentException("name");
for (KeyUsageBits bit : KeyUsageBits.values()) {
if (bit.getName().equalsIgnoreCase(name))
return bit;
}
throw new IndexOutOfBoundsException("name");
}
static public KeyUsageBits fromValue(int value) throws IndexOutOfBoundsException {
if (value < 0 || value > 8)
throw new IndexOutOfBoundsException("value");
for (KeyUsageBits bit : KeyUsageBits.values())
if (bit.getInt() == value)
return bit;
throw new IndexOutOfBoundsException("value");
}
}
public static class LdapContext {
private final String ldapFactoryClassName;
public LdapContext() {
ldapFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory";
}
public LdapContext(String ldapFactoryClassName) {
this.ldapFactoryClassName = ldapFactoryClassName;
}
public String getLdapFactoryClassName() {
return ldapFactoryClassName;
}
}
public static abstract class OCSPChecker {
/**
* Requests certificate revocation status using OCSP. The OCSP responder URI
* is obtained from the certificate's AIA extension.
* @param cert the certificate to be checked
* @param issuerCertificate The issuer certificate
* @return revocation status
*/
public abstract OCSPUtils.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException;
}
public static abstract class CRLLoaderImpl {
/**
* Returns a collection of {@link X509CRL}
* @return
* @throws GeneralSecurityException
*/
public abstract Collection<X509CRL> getX509CRLs() throws GeneralSecurityException;
}
public static class BouncyCastleOCSPChecker extends OCSPChecker {
private final String responderUri;
BouncyCastleOCSPChecker(String responderUri) {
this.responderUri = responderUri;
}
@Override
public OCSPUtils.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
OCSPUtils.OCSPRevocationStatus ocspRevocationStatus = null;
if (responderUri == null || responderUri.trim().length() == 0) {
// Obtains revocation status of a certificate using OCSP and assuming
// most common defaults. If responderUri is not specified,
// then OCS responder URI is retrieved from the
// certificate's AIA extension.
// OCSP responses must be signed with the issuer certificate
// or with another certificate that must be:
// 1) signed by the issuer certificate,
// 2) Includes the value of OCSPsigning in ExtendedKeyUsage v3 extension
// 3) Certificate is valid at the time
ocspRevocationStatus = OCSPUtils.check(cert, issuerCertificate);
}
else {
URI uri;
try {
uri = new URI(responderUri);
} catch (URISyntaxException e) {
String message = String.format("Unable to check certificate revocation status using OCSP.\n%s", e.getMessage());
throw new CertPathValidatorException(message, e);
}
logger.tracef("Responder URI \"%s\" will be used to verify revocation status of the certificate using OCSP", uri.toString());
// Obtains the revocation status of a certificate using OCSP.
// OCSP responder's certificate is assumed to be the issuer's certificate
// certificate.
// responderUri overrides the contents (if any) of the certificate's AIA extension
ocspRevocationStatus = OCSPUtils.check(cert, issuerCertificate, uri, null, null);
}
return ocspRevocationStatus;
}
}
public static class CRLLoaderProxy extends CRLLoaderImpl {
private final X509CRL _crl;
public CRLLoaderProxy(X509CRL crl) {
_crl = crl;
}
public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
return Collections.singleton(_crl);
}
}
public static class CRLFileLoader extends CRLLoaderImpl {
private final String cRLPath;
private final LdapContext ldapContext;
public CRLFileLoader(String cRLPath) {
this.cRLPath = cRLPath;
ldapContext = new LdapContext();
}
public CRLFileLoader(String cRLPath, LdapContext ldapContext) {
this.cRLPath = cRLPath;
this.ldapContext = ldapContext;
if (ldapContext == null)
throw new NullPointerException("Context cannot be null");
}
public Collection<X509CRL> getX509CRLs() throws GeneralSecurityException {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Collection<X509CRL> crlColl = null;
if (cRLPath != null) {
if (cRLPath.startsWith("http") || cRLPath.startsWith("https")) {
// load CRL using remote URI
try {
crlColl = loadFromURI(cf, new URI(cRLPath));
} catch (URISyntaxException e) {
logger.error(e.getMessage());
}
} else if (cRLPath.startsWith("ldap")) {
// load CRL from LDAP
try {
crlColl = loadCRLFromLDAP(cf, new URI(cRLPath));
} catch(URISyntaxException e) {
logger.error(e.getMessage());
}
} else {
// load CRL from file
crlColl = loadCRLFromFile(cf, cRLPath);
}
}
if (crlColl == null || crlColl.size() == 0) {
String message = String.format("Unable to load CRL from \"%s\"", cRLPath);
throw new GeneralSecurityException(message);
}
return crlColl;
}
private Collection<X509CRL> loadFromURI(CertificateFactory cf, URI remoteURI) throws GeneralSecurityException {
try {
logger.debugf("Loading CRL from %s", remoteURI.toString());
URLConnection conn = remoteURI.toURL().openConnection();
conn.setDoInput(true);
conn.setUseCaches(false);
X509CRL crl = loadFromStream(cf, conn.getInputStream());
return Collections.singleton(crl);
}
catch(IOException ex) {
logger.errorf(ex.getMessage());
}
return Collections.emptyList();
}
private Collection<X509CRL> loadCRLFromLDAP(CertificateFactory cf, URI remoteURI) throws GeneralSecurityException {
Hashtable<String, String> env = new Hashtable<>(2);
env.put(Context.INITIAL_CONTEXT_FACTORY, ldapContext.getLdapFactoryClassName());
env.put(Context.PROVIDER_URL, remoteURI.toString());
try {
DirContext ctx = new InitialDirContext(env);
try {
Attributes attrs = ctx.getAttributes("");
Attribute cRLAttribute = attrs.get("certificateRevocationList;binary");
byte[] data = (byte[])cRLAttribute.get();
if (data == null || data.length == 0) {
throw new CertificateException(String.format("Failed to download CRL from \"%s\"", remoteURI.toString()));
}
X509CRL crl = loadFromStream(cf, new ByteArrayInputStream(data));
return Collections.singleton(crl);
} finally {
ctx.close();
}
} catch (NamingException e) {
logger.error(e.getMessage());
} catch(IOException e) {
logger.error(e.getMessage());
}
return Collections.emptyList();
}
private Collection<X509CRL> loadCRLFromFile(CertificateFactory cf, String relativePath) throws GeneralSecurityException {
try {
String configDir = System.getProperty("jboss.server.config.dir");
if (configDir != null) {
File f = new File(configDir + File.separator + relativePath);
if (f.isFile()) {
logger.debugf("Loading CRL from %s", f.getAbsolutePath());
if (!f.canRead()) {
throw new IOException(String.format("Unable to read CRL from \"%path\"", f.getAbsolutePath()));
}
X509CRL crl = loadFromStream(cf, new FileInputStream(f.getAbsolutePath()));
return Collections.singleton(crl);
}
}
}
catch(IOException ex) {
logger.errorf(ex.getMessage());
}
return Collections.emptyList();
}
private X509CRL loadFromStream(CertificateFactory cf, InputStream is) throws IOException, CRLException {
DataInputStream dis = new DataInputStream(is);
X509CRL crl = (X509CRL)cf.generateCRL(dis);
dis.close();
return crl;
}
}
X509Certificate[] _certChain;
int _keyUsageBits;
List<String> _extendedKeyUsage;
boolean _crlCheckingEnabled;
boolean _crldpEnabled;
CRLLoaderImpl _crlLoader;
boolean _ocspEnabled;
OCSPChecker ocspChecker;
public CertificateValidator() {
}
protected CertificateValidator(X509Certificate[] certChain,
int keyUsageBits, List<String> extendedKeyUsage,
boolean cRLCheckingEnabled,
boolean cRLDPCheckingEnabled,
CRLLoaderImpl crlLoader,
boolean oCSPCheckingEnabled,
OCSPChecker ocspChecker) {
_certChain = certChain;
_keyUsageBits = keyUsageBits;
_extendedKeyUsage = extendedKeyUsage;
_crlCheckingEnabled = cRLCheckingEnabled;
_crldpEnabled = cRLDPCheckingEnabled;
_crlLoader = crlLoader;
_ocspEnabled = oCSPCheckingEnabled;
this.ocspChecker = ocspChecker;
if (ocspChecker == null)
throw new IllegalArgumentException("ocspChecker");
}
private static void validateKeyUsage(X509Certificate[] certs, int expected) throws GeneralSecurityException {
boolean[] keyUsageBits = certs[0].getKeyUsage();
if (keyUsageBits == null) {
if (expected != 0) {
String message = "Key usage extension is expected, but unavailable.";
throw new GeneralSecurityException(message);
}
return;
}
boolean isCritical = false;
Set critSet = certs[0].getCriticalExtensionOIDs();
if (critSet != null) {
isCritical = critSet.contains("2.5.29.15");
}
int n = expected;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < keyUsageBits.length; i++, n >>= 1) {
boolean value = keyUsageBits[i];
if ((n & 1) == 1 && !value) {
String message = String.format("Key Usage bit \'%s\' is not set.", CertificateValidator.KeyUsageBits.fromValue(i).getName());
if (sb.length() > 0) sb.append("\n");
sb.append(message);
logger.warn(message);
}
}
if (sb.length() > 0) {
if (isCritical) {
throw new GeneralSecurityException(sb.toString());
}
}
}
private static void validateExtendedKeyUsage(X509Certificate[] certs, List<String> expectedEKU) throws GeneralSecurityException {
if (expectedEKU == null || expectedEKU.size() == 0) {
logger.debug("Extended Key Usage validation is not enabled.");
return;
}
List<String> extendedKeyUsage = certs[0].getExtendedKeyUsage();
if (extendedKeyUsage == null) {
String message = "Extended key usage extension is expected, but unavailable";
throw new GeneralSecurityException(message);
}
boolean isCritical = false;
Set critSet = certs[0].getCriticalExtensionOIDs();
if (critSet != null) {
isCritical = critSet.contains("2.5.29.37");
}
List<String> ekuList = new LinkedList<>();
extendedKeyUsage.forEach(s -> ekuList.add(s.toLowerCase()));
for (String eku : expectedEKU) {
if (!ekuList.contains(eku.toLowerCase())) {
String message = String.format("Extended Key Usage \'%s\' is missing.", eku);
if (isCritical) {
throw new GeneralSecurityException(message);
}
logger.warn(message);
}
}
}
public CertificateValidator validateKeyUsage() throws GeneralSecurityException {
validateKeyUsage(_certChain, _keyUsageBits);
return this;
}
public CertificateValidator validateExtendedKeyUsage() throws GeneralSecurityException {
validateExtendedKeyUsage(_certChain, _extendedKeyUsage);
return this;
}
private void checkRevocationUsingOCSP(X509Certificate[] certs) throws GeneralSecurityException {
if (certs.length < 2) {
// OCSP requires a responder certificate to verify OCSP
// signed response.
String message = "OCSP requires a responder certificate. OCSP cannot be used to verify the revocation status of self-signed certificates.";
throw new GeneralSecurityException(message);
}
for (X509Certificate cert : certs) {
logger.debugf("Certificate: %s", cert.getSubjectDN().getName());
}
OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(certs[0], certs[1]);
if (rs == null) {
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) {
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());
}
}
private static void checkRevocationStatusUsingCRL(X509Certificate[] certs, CRLLoaderImpl crLoader) throws GeneralSecurityException {
Collection<X509CRL> crlColl = crLoader.getX509CRLs();
if (crlColl != null && crlColl.size() > 0) {
for (X509CRL it : crlColl) {
if (it.isRevoked(certs[0])) {
String message = String.format("Certificate has been revoked, certificate's subject: %s", certs[0].getSubjectDN().getName());
logger.debug(message);
throw new GeneralSecurityException(message);
}
}
}
}
private static List<String> getCRLDistributionPoints(X509Certificate cert) {
try {
return CRLUtils.getCRLDistributionPoints(cert);
}
catch(IOException e) {
logger.error(e.getMessage());
}
return new ArrayList<>();
}
private static void checkRevocationStatusUsingCRLDistributionPoints(X509Certificate[] certs) throws GeneralSecurityException {
List<String> distributionPoints = getCRLDistributionPoints(certs[0]);
if (distributionPoints == null || distributionPoints.size() == 0) {
throw new GeneralSecurityException("Could not find any CRL distribution points in the certificate, unable to check the certificate revocation status using CRL/DP.");
}
for (String dp : distributionPoints) {
logger.tracef("CRL Distribution point: \"%s\"", dp);
checkRevocationStatusUsingCRL(certs, new CRLFileLoader(dp));
}
}
public CertificateValidator checkRevocationStatus() throws GeneralSecurityException {
if (!(_crlCheckingEnabled || _ocspEnabled)) {
return this;
}
if (_crlCheckingEnabled) {
if (!_crldpEnabled) {
checkRevocationStatusUsingCRL(_certChain, _crlLoader /*"crl.pem"*/);
} else {
checkRevocationStatusUsingCRLDistributionPoints(_certChain);
}
}
if (_ocspEnabled) {
checkRevocationUsingOCSP(_certChain);
}
return this;
}
/**
* Configure Certificate validation
*/
public static class CertificateValidatorBuilder {
// A hand written DSL that walks through successive steps to configure
// instances of CertificateValidator type. The design is an adaption of
// the approach described in http://programmers.stackexchange.com/questions/252067/learning-to-write-dsls-utilities-for-unit-tests-and-am-worried-about-extensablit
int _keyUsageBits;
List<String> _extendedKeyUsage;
boolean _crlCheckingEnabled;
boolean _crldpEnabled;
CRLLoaderImpl _crlLoader;
boolean _ocspEnabled;
String _responderUri;
public CertificateValidatorBuilder() {
_extendedKeyUsage = new LinkedList<>();
_keyUsageBits = 0;
}
public class KeyUsageValidationBuilder {
CertificateValidatorBuilder _parent;
KeyUsageValidationBuilder(CertificateValidatorBuilder parent) {
_parent = parent;
}
public KeyUsageValidationBuilder enableDigitalSignatureBit() {
_keyUsageBits |= 1 << KeyUsageBits.DIGITAL_SIGNATURE.getInt();
return this;
}
public KeyUsageValidationBuilder enablecRLSignBit() {
_keyUsageBits |= 1 << KeyUsageBits.CRLSIGN.getInt();
return this;
}
public KeyUsageValidationBuilder enableDataEncriphermentBit() {
_keyUsageBits |= 1 << KeyUsageBits.DATA_ENCIPHERMENT.getInt();
return this;
}
public KeyUsageValidationBuilder enableDecipherOnlyBit() {
_keyUsageBits |= 1 << KeyUsageBits.DECIPHER_ONLY.getInt();
return this;
}
public KeyUsageValidationBuilder enableEnciphermentOnlyBit() {
_keyUsageBits |= 1 << KeyUsageBits.ENCIPHERMENT_ONLY.getInt();
return this;
}
public KeyUsageValidationBuilder enableKeyAgreementBit() {
_keyUsageBits |= 1 << KeyUsageBits.KEY_AGREEMENT.getInt();
return this;
}
public KeyUsageValidationBuilder enableKeyEnciphermentBit() {
_keyUsageBits |= 1 << KeyUsageBits.KEY_ENCIPHERMENT.getInt();
return this;
}
public KeyUsageValidationBuilder enableKeyCertSign() {
_keyUsageBits |= 1 << KeyUsageBits.KEYCERTSIGN.getInt();
return this;
}
public KeyUsageValidationBuilder enableNonRepudiationBit() {
_keyUsageBits |= 1 << KeyUsageBits.NON_REPUDIATION.getInt();
return this;
}
public CertificateValidatorBuilder back() {
return _parent;
}
CertificateValidatorBuilder parse(String keyUsage) {
if (keyUsage == null || keyUsage.trim().length() == 0)
return _parent;
String[] strs = keyUsage.split("[,]");
for (String s : strs) {
try {
KeyUsageBits bit = KeyUsageBits.parse(s.trim());
switch(bit) {
case CRLSIGN: enablecRLSignBit(); break;
case DATA_ENCIPHERMENT: enableDataEncriphermentBit(); break;
case DECIPHER_ONLY: enableDecipherOnlyBit(); break;
case DIGITAL_SIGNATURE: enableDigitalSignatureBit(); break;
case ENCIPHERMENT_ONLY: enableEnciphermentOnlyBit(); break;
case KEY_AGREEMENT: enableKeyAgreementBit(); break;
case KEY_ENCIPHERMENT: enableKeyEnciphermentBit(); break;
case KEYCERTSIGN: enableKeyCertSign(); break;
case NON_REPUDIATION: enableNonRepudiationBit(); break;
}
}
catch(IllegalArgumentException e) {
logger.warnf("Unable to parse key usage bit: \"%s\"", s);
}
catch(IndexOutOfBoundsException e) {
logger.warnf("Invalid key usage bit: \"%s\"", s);
}
}
return _parent;
}
}
public class ExtendedKeyUsageValidationBuilder {
CertificateValidatorBuilder _parent;
protected ExtendedKeyUsageValidationBuilder(CertificateValidatorBuilder parent) {
_parent = parent;
}
public CertificateValidatorBuilder parse(String extendedKeyUsage) {
if (extendedKeyUsage == null || extendedKeyUsage.trim().length() == 0)
return _parent;
String[] strs = extendedKeyUsage.split("[,;:]]");
for (String str : strs) {
_extendedKeyUsage.add(str.trim());
}
return _parent;
}
}
public class RevocationStatusCheckBuilder {
CertificateValidatorBuilder _parent;
protected RevocationStatusCheckBuilder(CertificateValidatorBuilder parent) {
_parent = parent;
}
public GotCRL cRLEnabled(boolean value) {
_crlCheckingEnabled = value;
return new GotCRL();
}
public class GotCRL {
public GotCRLDP cRLDPEnabled(boolean value) {
_crldpEnabled = value;
return new GotCRLDP();
}
}
public class GotCRLRelativePath {
public GotOCSP oCSPEnabled(boolean value) {
_ocspEnabled = value;
return new GotOCSP();
}
}
public class GotCRLDP {
public GotCRLRelativePath cRLrelativePath(String value) {
if (value != null)
_crlLoader = new CRLFileLoader(value);
return new GotCRLRelativePath();
}
public GotCRLRelativePath cRLLoader(CRLLoaderImpl cRLLoader) {
if (cRLLoader != null)
_crlLoader = cRLLoader;
return new GotCRLRelativePath();
}
}
public class GotOCSP {
public CertificateValidatorBuilder oCSPResponderURI(String responderURI) {
_responderUri = responderURI;
return _parent;
}
}
}
public KeyUsageValidationBuilder keyUsage() {
return new KeyUsageValidationBuilder(this);
}
public ExtendedKeyUsageValidationBuilder extendedKeyUsage() {
return new ExtendedKeyUsageValidationBuilder(this);
}
public RevocationStatusCheckBuilder revocation() {
return new RevocationStatusCheckBuilder(this);
}
public CertificateValidator build(X509Certificate[] certs) {
if (_crlLoader == null) {
_crlLoader = new CRLFileLoader("");
}
return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage,
_crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled, new BouncyCastleOCSPChecker(_responderUri));
}
}
}