keycloak-scim/services/src/main/java/org/keycloak/utils/CRLUtils.java
Dmitry Telegin 79074aa380 KEYCLOAK-12162 Modularize config backends (#6499)
* KEYCLOAK-12162 - Modularize configuration backends

* - Use JsonSerialization
- simplify backend selection (no fallbacks)

* Remove unused org.wildfly.core:wildfly-controller dependency
2019-11-22 15:23:04 +01:00

203 lines
9 KiB
Java

/*
* Copyright 2017 Red Hat, 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.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
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.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.jboss.logging.Logger;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.models.KeycloakSession;
import org.keycloak.truststore.TruststoreProvider;
/**
* @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
* @version $Revision: 1 $
* @since 10/31/2016
*/
public final class CRLUtils {
private static final Logger log = Logger.getLogger(CRLUtils.class);
static {
BouncyIntegration.init();
}
private static final String CRL_DISTRIBUTION_POINTS_OID = "2.5.29.31";
/**
* Retrieves a list of CRL distribution points from CRLDP v3 certificate extension
* See <a href="www.nakov.com/blog/2009/12/01/x509-certificate-validation-in-java-build-and-verify-cchain-and-verify-clr-with-bouncy-castle/">CRL validation</a>
* @param cert
* @return
* @throws IOException
*/
public static List<String> getCRLDistributionPoints(X509Certificate cert) throws IOException {
byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID);
if (data == null) {
return Collections.emptyList();
}
List<String> distributionPointUrls = new LinkedList<>();
DEROctetString octetString;
try (ASN1InputStream crldpExtensionInputStream = new ASN1InputStream(new ByteArrayInputStream(data))) {
octetString = (DEROctetString)crldpExtensionInputStream.readObject();
}
byte[] octets = octetString.getOctets();
CRLDistPoint crlDP;
try (ASN1InputStream crldpInputStream = new ASN1InputStream(new ByteArrayInputStream(octets))) {
crlDP = CRLDistPoint.getInstance(crldpInputStream.readObject());
}
for (DistributionPoint dp : crlDP.getDistributionPoints()) {
DistributionPointName dpn = dp.getDistributionPoint();
if (dpn != null && dpn.getType() == DistributionPointName.FULL_NAME) {
GeneralName[] names = GeneralNames.getInstance(dpn.getName()).getNames();
for (GeneralName gn : names) {
if (gn.getTagNo() == GeneralName.uniformResourceIdentifier) {
String url = DERIA5String.getInstance(gn.getName()).getString();
distributionPointUrls.add(url);
}
}
}
}
return distributionPointUrls;
}
/**
* Check the signature on CRL and check if 1st certificate from the chain ((The actual certificate from the client)) is valid and not available on CRL.
*
* @param certs The 1st certificate is the actual certificate of the user. The other certificates represents the certificate chain
* @param crl Given CRL
* @throws GeneralSecurityException if some error in validation happens. Typically certificate not valid, or CRL signature not valid
*/
public static void check(X509Certificate[] certs, X509CRL crl, KeycloakSession session) throws GeneralSecurityException {
if (certs.length < 2) {
throw new GeneralSecurityException("Not possible to verify signature on CRL. X509 certificate doesn't have CA chain available on it");
}
X500Principal crlIssuerPrincipal = crl.getIssuerX500Principal();
X509Certificate crlSignatureCertificate = null;
// Try to find the certificate in the CA chain, which was used to sign the CRL
for (int i=1 ; i<certs.length ; i++) {
X509Certificate currentCACert = certs[i];
if (crlIssuerPrincipal.equals(currentCACert.getSubjectX500Principal())) {
crlSignatureCertificate = currentCACert;
log.tracef("Found certificate used to sign CRL in the CA chain of the certificate. CRL issuer: %s", crlIssuerPrincipal);
break;
}
}
// Try to find the CRL issuer certificate in the truststore
if (crlSignatureCertificate == null) {
log.tracef("Not found CRL issuer '%s' in the CA chain of the certificate. Fallback to lookup CRL issuer in the truststore", crlIssuerPrincipal);
crlSignatureCertificate = findCRLSignatureCertificateInTruststore(session, certs, crlIssuerPrincipal);
}
// Verify signature on CRL
// TODO: It will be nice to cache CRLs and also verify their signatures just once at the time when CRL is loaded, rather than in every request
crl.verify(crlSignatureCertificate.getPublicKey());
// Finally check if
if (crl.isRevoked(certs[0])) {
String message = String.format("Certificate has been revoked, certificate's subject: %s", certs[0].getSubjectDN().getName());
log.debug(message);
throw new GeneralSecurityException(message);
}
}
private static X509Certificate findCRLSignatureCertificateInTruststore(KeycloakSession session, X509Certificate[] certs, X500Principal crlIssuerPrincipal) throws GeneralSecurityException {
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
if (truststoreProvider == null || truststoreProvider.getTruststore() == null) {
throw new GeneralSecurityException("Truststore not available");
}
Map<X500Principal, X509Certificate> rootCerts = truststoreProvider.getRootCertificates();
Map<X500Principal, X509Certificate> intermediateCerts = truststoreProvider.getIntermediateCertificates();
X509Certificate crlSignatureCertificate = intermediateCerts.get(crlIssuerPrincipal);
if (crlSignatureCertificate == null) {
crlSignatureCertificate = rootCerts.get(crlIssuerPrincipal);
}
if (crlSignatureCertificate == null) {
throw new GeneralSecurityException("Not available certificate for CRL issuer '" + crlIssuerPrincipal + "' in the truststore, nor in the CA chain");
} else {
log.tracef("Found CRL issuer certificate with subject '%s' in the truststore. Verifying trust anchor", crlIssuerPrincipal);
}
// Check if CRL issuer has trust anchor with the checked certificate (See https://tools.ietf.org/html/rfc5280#section-6.3.3 , paragraph (f))
Set<X500Principal> certificateCAPrincipals = Arrays.asList(certs).stream()
.map(X509Certificate::getSubjectX500Principal)
.collect(Collectors.toSet());
// Remove the checked certificate itself
certificateCAPrincipals.remove(certs[0].getSubjectX500Principal());
X509Certificate currentCRLAnchorCertificate = crlSignatureCertificate;
X500Principal currentCRLAnchorPrincipal = crlIssuerPrincipal;
while (true) {
if (certificateCAPrincipals.contains(currentCRLAnchorPrincipal)) {
log.tracef("Found trust anchor of the CRL issuer '%s' in the CA chain. Anchor is '%s'", crlIssuerPrincipal, currentCRLAnchorPrincipal);
break;
}
// Try to see the anchor
currentCRLAnchorPrincipal = currentCRLAnchorCertificate.getIssuerX500Principal();
currentCRLAnchorCertificate = intermediateCerts.get(currentCRLAnchorPrincipal);
if (currentCRLAnchorCertificate == null) {
currentCRLAnchorCertificate = rootCerts.get(currentCRLAnchorPrincipal);
}
if (currentCRLAnchorCertificate == null) {
throw new GeneralSecurityException("Certificate for CRL issuer '" + crlIssuerPrincipal + "' available in the truststore, but doesn't have trust anchors with the CA chain.");
}
}
return crlSignatureCertificate;
}
}