Remove bouncycastle dependency from keycloak-services (#13489)
Closes #12857 Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
fb978de0d8
commit
ce1331f550
44 changed files with 1765 additions and 606 deletions
|
@ -27,7 +27,6 @@ import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator;
|
|||
import org.keycloak.adapters.rotation.JWKPublicKeyLocator;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.enums.TokenStore;
|
||||
import org.keycloak.representations.adapters.config.AdapterConfig;
|
||||
|
|
|
@ -17,10 +17,14 @@
|
|||
|
||||
package org.keycloak.common.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
|
||||
|
@ -28,6 +32,8 @@ import java.security.cert.X509Certificate;
|
|||
*/
|
||||
public interface CertificateUtilsProvider {
|
||||
|
||||
public static final String CRL_DISTRIBUTION_POINTS_OID = "2.5.29.31";
|
||||
|
||||
/**
|
||||
* Generates version 3 {@link java.security.cert.X509Certificate}.
|
||||
*
|
||||
|
@ -57,4 +63,14 @@ public interface CertificateUtilsProvider {
|
|||
|
||||
public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber);
|
||||
|
||||
public List<String> getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException;
|
||||
|
||||
public List<String> getCRLDistributionPoints(X509Certificate cert) throws IOException;
|
||||
|
||||
public X509Certificate createServicesTestCertificate(String dn,
|
||||
Date startDate,
|
||||
Date expiryDate,
|
||||
KeyPair keyPair,
|
||||
String... certificatePolicyOid);
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ public class CryptoConstants {
|
|||
public static final String RSA_OAEP = "RSA-OAEP";
|
||||
public static final String RSA_OAEP_256 = "RSA-OAEP-256";
|
||||
|
||||
// Constant for the OCSP provider
|
||||
// public static final String OCSP = "OCSP";
|
||||
|
||||
/** Name of Java security provider used with non-fips BouncyCastle. Should be used in non-FIPS environment */
|
||||
public static final String BC_PROVIDER_ID = "BC";
|
||||
|
||||
|
|
|
@ -42,6 +42,13 @@ public interface CryptoProvider {
|
|||
*/
|
||||
PemUtilsProvider getPemUtils();
|
||||
|
||||
<T> T getOCSPProver(Class<T> clazz);
|
||||
|
||||
|
||||
public UserIdentityExtractorProvider getIdentityExtractorProvider();
|
||||
|
||||
public ECDSACryptoProvider getEcdsaCryptoProvider();
|
||||
|
||||
|
||||
/**
|
||||
* Create the param spec for the EC curve
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.keycloak.common.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public interface ECDSACryptoProvider {
|
||||
|
||||
|
||||
public byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException;
|
||||
|
||||
public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException;
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.common.crypto;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
|
||||
* @version $Revision: 1 $
|
||||
* @date 7/30/2016
|
||||
*/
|
||||
|
||||
public interface UserIdentityExtractor {
|
||||
|
||||
|
||||
public Object extractUserIdentity(X509Certificate[] certs);
|
||||
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* 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.common.crypto;
|
||||
|
||||
|
||||
import java.security.Principal;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
|
||||
* @version $Revision: 1 $
|
||||
* @date 7/30/2016
|
||||
*/
|
||||
|
||||
public abstract class UserIdentityExtractorProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(UserIdentityExtractorProvider.class);
|
||||
|
||||
public abstract class SubjectAltNameExtractor implements UserIdentityExtractor {
|
||||
|
||||
}
|
||||
|
||||
public abstract class X500NameRDNExtractor implements UserIdentityExtractor {
|
||||
}
|
||||
|
||||
protected class OrExtractor implements UserIdentityExtractor {
|
||||
|
||||
UserIdentityExtractor extractor;
|
||||
UserIdentityExtractor other;
|
||||
OrExtractor(UserIdentityExtractor extractor, UserIdentityExtractor other) {
|
||||
this.extractor = extractor;
|
||||
this.other = other;
|
||||
|
||||
if (this.extractor == null)
|
||||
throw new IllegalArgumentException("extractor is null");
|
||||
if (this.other == null)
|
||||
throw new IllegalArgumentException("other is null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
Object result = this.extractor.extractUserIdentity(certs);
|
||||
if (result == null)
|
||||
result = this.other.extractUserIdentity(certs);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class PatternMatcher implements UserIdentityExtractor {
|
||||
private final String _pattern;
|
||||
private final Function<X509Certificate[],String> _f;
|
||||
PatternMatcher(String pattern, Function<X509Certificate[],String> valueToMatch) {
|
||||
_pattern = pattern;
|
||||
_f = valueToMatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
String value = Optional.ofNullable(_f.apply(certs)).orElseThrow(IllegalArgumentException::new);
|
||||
|
||||
Pattern r = Pattern.compile(_pattern, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
Matcher m = r.matcher(value);
|
||||
|
||||
if (!m.find()) {
|
||||
logger.debugf("[PatternMatcher:extract] No matches were found for input \"%s\", pattern=\"%s\"", value, _pattern);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (m.groupCount() != 1) {
|
||||
logger.debugf("[PatternMatcher:extract] Match produced more than a single group for input \"%s\", pattern=\"%s\"", value, _pattern);
|
||||
return null;
|
||||
}
|
||||
|
||||
return m.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrBuilder {
|
||||
UserIdentityExtractor extractor;
|
||||
UserIdentityExtractor other;
|
||||
OrBuilder(UserIdentityExtractor extractor) {
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
public UserIdentityExtractor or(UserIdentityExtractor other) {
|
||||
return new OrExtractor(extractor, other);
|
||||
}
|
||||
}
|
||||
|
||||
public OrBuilder either(UserIdentityExtractor extractor) {
|
||||
return new OrBuilder(extractor);
|
||||
}
|
||||
|
||||
public UserIdentityExtractor getCertificatePemIdentityExtractor() {
|
||||
return new UserIdentityExtractor() {
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
if (certs == null || certs.length == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
String pem = PemUtils.encodeCertificate(certs[0]);
|
||||
logger.debugf("Using PEM certificate \"%s\" as user identity.", pem);
|
||||
return pem;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public UserIdentityExtractor getPatternIdentityExtractor(String pattern,
|
||||
Function<X509Certificate[],String> valueToMatch) {
|
||||
return new PatternMatcher(pattern, valueToMatch);
|
||||
}
|
||||
|
||||
public abstract UserIdentityExtractor getX500NameExtractor(String identifier, Function<X509Certificate[],Principal> x500Name);
|
||||
|
||||
/**
|
||||
* Obtains the subjectAltName given a <code>generalName</code>.
|
||||
*
|
||||
* @param generalName an integer representing the general name. See {@link X509Certificate#getSubjectAlternativeNames()}
|
||||
* @return the value from the subjectAltName extension
|
||||
*/
|
||||
public abstract SubjectAltNameExtractor getSubjectAltNameExtractor(int generalName);
|
||||
}
|
|
@ -41,7 +41,19 @@
|
|||
<scope>test</scope>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi-private</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
|
|
|
@ -17,31 +17,54 @@
|
|||
|
||||
package org.keycloak.crypto.def;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.CRLDistPoint;
|
||||
import org.bouncycastle.asn1.x509.CertificatePolicies;
|
||||
import org.bouncycastle.asn1.x509.DistributionPoint;
|
||||
import org.bouncycastle.asn1.x509.DistributionPointName;
|
||||
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.Extensions;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.asn1.x509.PolicyInformation;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.X509v1CertificateBuilder;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.crypto.CertificateUtilsProvider;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
|
||||
|
@ -166,4 +189,133 @@ public class BCCertificateUtilsProvider implements CertificateUtilsProvider {
|
|||
throw new RuntimeException("Could not create content signer.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException {
|
||||
|
||||
Extensions certExtensions = new JcaX509CertificateHolder(cert).getExtensions();
|
||||
if (certExtensions == null)
|
||||
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate extensions were found");
|
||||
|
||||
CertificatePolicies policies = CertificatePolicies.fromExtensions(certExtensions);
|
||||
|
||||
if (policies == null)
|
||||
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate policy extensions were found");
|
||||
|
||||
List<String> policyList = new LinkedList<>();
|
||||
Arrays.stream(policies.getPolicyInformation()).forEach(p -> policyList.add(p.getPolicyIdentifier().toString().toLowerCase()));
|
||||
|
||||
return policyList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
|
||||
public X509Certificate createServicesTestCertificate(String dn,
|
||||
Date startDate,
|
||||
Date expiryDate,
|
||||
KeyPair keyPair,
|
||||
String... certificatePolicyOid) {
|
||||
// Cert data
|
||||
X500Name subjectDN = new X500Name(dn);
|
||||
X500Name issuerDN = new X500Name(dn);
|
||||
|
||||
SubjectPublicKeyInfo subjPubKeyInfo = SubjectPublicKeyInfo.getInstance(
|
||||
ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
BigInteger serialNumber = new BigInteger(130, new SecureRandom());
|
||||
|
||||
// Build the certificate
|
||||
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate,
|
||||
subjectDN, subjPubKeyInfo);
|
||||
|
||||
if (certificatePolicyOid != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (Extension certExtension: certPolicyExtensions(certificatePolicyOid))
|
||||
certGen.addExtension(certExtension);
|
||||
} catch (CertIOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the cert with the private key
|
||||
try {
|
||||
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.setProvider(BouncyIntegration.PROVIDER)
|
||||
.build(keyPair.getPrivate());
|
||||
X509Certificate x509Certificate = new JcaX509CertificateConverter()
|
||||
.setProvider(BouncyIntegration.PROVIDER)
|
||||
.getCertificate(certGen.build(contentSigner));
|
||||
|
||||
return x509Certificate;
|
||||
} catch (CertificateException | OperatorCreationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Extension> certPolicyExtensions(String... certificatePolicyOid) {
|
||||
List<Extension> certificatePolicies = new LinkedList<>();
|
||||
|
||||
if (certificatePolicyOid != null && certificatePolicyOid.length > 0)
|
||||
{
|
||||
List<PolicyInformation> policyInfoList = new LinkedList<>();
|
||||
for (String oid: certificatePolicyOid)
|
||||
{
|
||||
policyInfoList.add(new PolicyInformation(new ASN1ObjectIdentifier(oid)));
|
||||
}
|
||||
|
||||
CertificatePolicies policies = new CertificatePolicies(policyInfoList.toArray(new PolicyInformation[0]));
|
||||
|
||||
try {
|
||||
boolean isCritical = false;
|
||||
Extension extension = new Extension(Extension.certificatePolicies, isCritical, policies.getEncoded());
|
||||
certificatePolicies.add(extension);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
return certificatePolicies;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package org.keycloak.crypto.def;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1Integer;
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERSequenceGenerator;
|
||||
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||
import org.keycloak.common.crypto.ECDSACryptoProvider;
|
||||
|
||||
public class BCECDSACryptoProvider implements ECDSACryptoProvider {
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException {
|
||||
int len = signLength / 2;
|
||||
int arraySize = len + 1;
|
||||
|
||||
byte[] r = new byte[arraySize];
|
||||
byte[] s = new byte[arraySize];
|
||||
System.arraycopy(signature, 0, r, 1, len);
|
||||
System.arraycopy(signature, len, s, 1, len);
|
||||
BigInteger rBigInteger = new BigInteger(r);
|
||||
BigInteger sBigInteger = new BigInteger(s);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DERSequenceGenerator seqGen = new DERSequenceGenerator(bos);
|
||||
|
||||
seqGen.addObject(new ASN1Integer(rBigInteger.toByteArray()));
|
||||
seqGen.addObject(new ASN1Integer(sBigInteger.toByteArray()));
|
||||
seqGen.close();
|
||||
bos.close();
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException {
|
||||
int len = signLength / 2;
|
||||
|
||||
ASN1InputStream asn1InputStream = new ASN1InputStream(derEncodedSignatureValue);
|
||||
ASN1Primitive asn1Primitive = asn1InputStream.readObject();
|
||||
asn1InputStream.close();
|
||||
|
||||
ASN1Sequence asn1Sequence = (ASN1Sequence.getInstance(asn1Primitive));
|
||||
ASN1Integer rASN1 = (ASN1Integer) asn1Sequence.getObjectAt(0);
|
||||
ASN1Integer sASN1 = (ASN1Integer) asn1Sequence.getObjectAt(1);
|
||||
X9IntegerConverter x9IntegerConverter = new X9IntegerConverter();
|
||||
byte[] r = x9IntegerConverter.integerToBytes(rASN1.getValue(), len);
|
||||
byte[] s = x9IntegerConverter.integerToBytes(sASN1.getValue(), len);
|
||||
|
||||
byte[] concatenatedSignatureValue = new byte[signLength];
|
||||
System.arraycopy(r, 0, concatenatedSignatureValue, 0, len);
|
||||
System.arraycopy(s, 0, concatenatedSignatureValue, len, len);
|
||||
|
||||
return concatenatedSignatureValue;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -16,23 +16,14 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
package org.keycloak.crypto.def;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
import org.bouncycastle.asn1.x509.CRLReason;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.Extensions;
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
|
||||
|
@ -44,17 +35,26 @@ import org.bouncycastle.cert.X509CertificateHolder;
|
|||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.ocsp.*;
|
||||
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
|
||||
import org.bouncycastle.cert.ocsp.CertificateID;
|
||||
import org.bouncycastle.cert.ocsp.CertificateStatus;
|
||||
import org.bouncycastle.cert.ocsp.OCSPException;
|
||||
import org.bouncycastle.cert.ocsp.OCSPReq;
|
||||
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
|
||||
import org.bouncycastle.cert.ocsp.OCSPResp;
|
||||
import org.bouncycastle.cert.ocsp.RevokedStatus;
|
||||
import org.bouncycastle.cert.ocsp.SingleResp;
|
||||
import org.bouncycastle.cert.ocsp.UnknownStatus;
|
||||
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
|
||||
import org.bouncycastle.operator.ContentVerifierProvider;
|
||||
import org.bouncycastle.operator.DigestCalculator;
|
||||
import org.bouncycastle.operator.DigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
|
||||
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.utils.OCSPProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
|
@ -63,12 +63,24 @@ import java.security.GeneralSecurityException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.*;
|
||||
import java.util.*;
|
||||
import java.security.cert.CRLReason;
|
||||
import java.security.cert.CertPath;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
|
||||
|
@ -76,115 +88,13 @@ import org.apache.http.impl.client.CloseableHttpClient;
|
|||
* @since 10/29/2016
|
||||
*/
|
||||
|
||||
public final class OCSPUtils {
|
||||
public class BCOCSPProvider extends OCSPProvider {
|
||||
|
||||
private final static Logger logger = Logger.getLogger(""+OCSPUtils.class);
|
||||
private final static Logger logger = Logger.getLogger(BCOCSPProvider.class.getName());
|
||||
|
||||
private static int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec
|
||||
private static final int TIME_SKEW = 900000;
|
||||
|
||||
public enum RevocationStatus {
|
||||
GOOD,
|
||||
REVOKED,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public interface OCSPRevocationStatus {
|
||||
RevocationStatus getRevocationStatus();
|
||||
Date getRevocationTime();
|
||||
CRLReason getRevocationReason();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests certificate revocation status using OCSP.
|
||||
* @param cert the certificate to be checked
|
||||
* @param issuerCertificate The issuer certificate
|
||||
* @param responderURI an address of OCSP responder. Overrides any OCSP responder URIs stored in certificate's AIA extension
|
||||
* @param date
|
||||
* @param responderCert a certificate that OCSP responder uses to sign OCSP responses
|
||||
* @return revocation status
|
||||
*/
|
||||
public static OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, URI responderURI, X509Certificate responderCert, Date date) throws CertPathValidatorException {
|
||||
if (cert == null)
|
||||
throw new IllegalArgumentException("cert cannot be null");
|
||||
if (issuerCertificate == null)
|
||||
throw new IllegalArgumentException("issuerCertificate cannot be null");
|
||||
if (responderURI == null)
|
||||
throw new IllegalArgumentException("responderURI cannot be null");
|
||||
|
||||
return check(session, cert, issuerCertificate, Collections.singletonList(responderURI), responderCert, date);
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
* @param date
|
||||
* @return revocation status
|
||||
*/
|
||||
public static OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, Date date, X509Certificate responderCert) throws CertPathValidatorException {
|
||||
List<String> responderURIs = null;
|
||||
try {
|
||||
responderURIs = getResponderURIs(cert);
|
||||
} catch (CertificateEncodingException e) {
|
||||
logger.log(Level.FINE, "CertificateEncodingException: {0}", e.getMessage());
|
||||
throw new CertPathValidatorException(e.getMessage(), e);
|
||||
}
|
||||
if (responderURIs.size() == 0) {
|
||||
logger.log(Level.INFO, "No OCSP responders in the specified certificate");
|
||||
throw new CertPathValidatorException("No OCSP Responder URI in certificate");
|
||||
}
|
||||
|
||||
List<URI> uris = new LinkedList<>();
|
||||
for (String value : responderURIs) {
|
||||
try {
|
||||
URI responderURI = URI.create(value);
|
||||
uris.add(responderURI);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
logger.log(Level.FINE, "Malformed responder URI {0}", value);
|
||||
}
|
||||
}
|
||||
return check(session, cert, issuerCertificate, Collections.unmodifiableList(uris), responderCert, date);
|
||||
}
|
||||
/**
|
||||
* 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 static OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
|
||||
return check(session, cert, issuerCertificate, null, null);
|
||||
}
|
||||
|
||||
private static OCSPResp getResponse(KeycloakSession session, OCSPReq ocspReq, URI responderUri) throws IOException {
|
||||
CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
|
||||
HttpPost post = new HttpPost(responderUri);
|
||||
post.setHeader(HttpHeaders.CONTENT_TYPE, "application/ocsp-request");
|
||||
|
||||
final RequestConfig params = RequestConfig.custom()
|
||||
.setConnectTimeout(OCSP_CONNECT_TIMEOUT)
|
||||
.setSocketTimeout(OCSP_CONNECT_TIMEOUT)
|
||||
.build();
|
||||
post.setConfig(params);
|
||||
|
||||
post.setEntity(new ByteArrayEntity(ocspReq.getEncoded()));
|
||||
|
||||
//Get Response
|
||||
try (CloseableHttpResponse response = httpClient.execute(post)) {
|
||||
try {
|
||||
if (response.getStatusLine().getStatusCode() / 100 != 2) {
|
||||
String errorMessage = String.format("Connection error, unable to obtain certificate revocation status using OCSP responder \"%s\", code \"%d\"",
|
||||
responderUri.toString(), response.getStatusLine().getStatusCode());
|
||||
throw new IOException(errorMessage);
|
||||
}
|
||||
|
||||
byte[] data = EntityUtils.toByteArray(response.getEntity());
|
||||
return new OCSPResp(data);
|
||||
} finally {
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
}
|
||||
}
|
||||
protected OCSPResp getResponse(KeycloakSession session, OCSPReq ocspReq, URI responderUri) throws IOException {
|
||||
byte[] data = getEncodedOCSPResponse(session, ocspReq.getEncoded(), responderUri);
|
||||
return new OCSPResp(data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,12 +107,15 @@ public final class OCSPUtils {
|
|||
* @return a revocation status
|
||||
* @throws CertPathValidatorException
|
||||
*/
|
||||
private static OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException {
|
||||
@Override
|
||||
protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException {
|
||||
if (responderURIs == null || responderURIs.size() == 0)
|
||||
throw new IllegalArgumentException("Need at least one responder");
|
||||
try {
|
||||
DigestCalculator digCalc = new BcDigestCalculatorProvider()
|
||||
.get(new AlgorithmIdentifier(OIWObjectIdentifiers.idSHA1));
|
||||
|
||||
DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().build();
|
||||
|
||||
DigestCalculator digCalc = dcp.get(CertificateID.HASH_SHA1);
|
||||
|
||||
JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber());
|
||||
|
||||
|
@ -251,13 +164,14 @@ public final class OCSPUtils {
|
|||
throw new CertPathValidatorException("OCSP check failed", e);
|
||||
}
|
||||
}
|
||||
catch(CertificateNotYetValidException | CertificateExpiredException | OperatorCreationException | OCSPException | CertificateEncodingException | NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
catch(CertificateNotYetValidException | CertificateExpiredException | OperatorCreationException | OCSPException |
|
||||
CertificateEncodingException | NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
logger.log(Level.FINE, e.getMessage());
|
||||
throw new CertPathValidatorException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse)
|
||||
private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse)
|
||||
throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
|
||||
SingleResp expectedResponse = null;
|
||||
for (SingleResp singleResponse : basicOcspResponse.getResponses()) {
|
||||
|
@ -275,7 +189,7 @@ public final class OCSPUtils {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) {
|
||||
private boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) {
|
||||
if (idLeft == idRight)
|
||||
return true;
|
||||
if (idLeft == null || idRight == null)
|
||||
|
@ -286,7 +200,7 @@ public final class OCSPUtils {
|
|||
idLeft.getSerialNumber().equals(idRight.getSerialNumber());
|
||||
}
|
||||
|
||||
private static void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
|
||||
private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
|
||||
|
||||
List<X509CertificateHolder> certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts()));
|
||||
X509Certificate signingCert = null;
|
||||
|
@ -444,7 +358,7 @@ public final class OCSPUtils {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean verifySignature(BasicOCSPResp basicOcspResponse, X509Certificate cert) {
|
||||
private boolean verifySignature(BasicOCSPResp basicOcspResponse, X509Certificate cert) {
|
||||
try {
|
||||
ContentVerifierProvider contentVerifier = new JcaContentVerifierProviderBuilder()
|
||||
.setProvider(BouncyIntegration.PROVIDER).build(cert.getPublicKey());
|
||||
|
@ -457,29 +371,10 @@ public final class OCSPUtils {
|
|||
return false;
|
||||
}
|
||||
|
||||
private static OCSPRevocationStatus unknownStatus() {
|
||||
return new OCSPRevocationStatus() {
|
||||
@Override
|
||||
public RevocationStatus getRevocationStatus() {
|
||||
return RevocationStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRevocationTime() {
|
||||
return new Date(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CRLReason getRevocationReason() {
|
||||
return CRLReason.lookup(CRLReason.unspecified);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static OCSPRevocationStatus singleResponseToRevocationStatus(final SingleResp singleResponse) throws CertPathValidatorException {
|
||||
private OCSPRevocationStatus singleResponseToRevocationStatus(final SingleResp singleResponse) throws CertPathValidatorException {
|
||||
final CertificateStatus certStatus = singleResponse.getCertStatus();
|
||||
|
||||
int revocationReason = CRLReason.unspecified;
|
||||
CRLReason revocationReason = CRLReason.UNSPECIFIED;
|
||||
Date revocationTime = null;
|
||||
RevocationStatus status = RevocationStatus.UNKNOWN;
|
||||
if (certStatus == CertificateStatus.GOOD) {
|
||||
|
@ -489,7 +384,7 @@ public final class OCSPUtils {
|
|||
revocationTime = revoked.getRevocationTime();
|
||||
status = RevocationStatus.REVOKED;
|
||||
if (revoked.hasRevocationReason()) {
|
||||
revocationReason = revoked.getRevocationReason();
|
||||
revocationReason = CRLReason.values()[revoked.getRevocationReason()];
|
||||
}
|
||||
} else if (certStatus instanceof UnknownStatus) {
|
||||
status = RevocationStatus.UNKNOWN;
|
||||
|
@ -499,7 +394,7 @@ public final class OCSPUtils {
|
|||
|
||||
final RevocationStatus finalStatus = status;
|
||||
final Date finalRevocationTime = revocationTime;
|
||||
final int finalRevocationReason = revocationReason;
|
||||
final CRLReason finalRevocationReason = revocationReason;
|
||||
return new OCSPRevocationStatus() {
|
||||
@Override
|
||||
public RevocationStatus getRevocationStatus() {
|
||||
|
@ -513,7 +408,7 @@ public final class OCSPUtils {
|
|||
|
||||
@Override
|
||||
public CRLReason getRevocationReason() {
|
||||
return CRLReason.lookup(finalRevocationReason);
|
||||
return finalRevocationReason;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -526,7 +421,8 @@ public final class OCSPUtils {
|
|||
* @return a list of available responder URIs.
|
||||
* @throws CertificateEncodingException
|
||||
*/
|
||||
private static List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException {
|
||||
@Override
|
||||
protected List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException {
|
||||
|
||||
LinkedList<String> responderURIs = new LinkedList<>();
|
||||
JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert);
|
|
@ -16,9 +16,8 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.authentication.authenticators.x509;
|
||||
package org.keycloak.crypto.def;
|
||||
|
||||
import freemarker.template.utility.NullArgumentException;
|
||||
import org.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
|
@ -27,20 +26,20 @@ import org.bouncycastle.asn1.ASN1TaggedObject;
|
|||
import org.bouncycastle.asn1.DERUTF8String;
|
||||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractor;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
|
||||
|
@ -48,41 +47,17 @@ import java.util.regex.Pattern;
|
|||
* @date 7/30/2016
|
||||
*/
|
||||
|
||||
public abstract class UserIdentityExtractor {
|
||||
public class BCUserIdentityExtractorProvider extends UserIdentityExtractorProvider {
|
||||
|
||||
private static final ServicesLogger logger = ServicesLogger.LOGGER;
|
||||
private static final Logger logger = Logger.getLogger(BCUserIdentityExtractorProvider.class.getName());
|
||||
|
||||
public abstract Object extractUserIdentity(X509Certificate[] certs);
|
||||
|
||||
static class OrExtractor extends UserIdentityExtractor {
|
||||
|
||||
UserIdentityExtractor extractor;
|
||||
UserIdentityExtractor other;
|
||||
OrExtractor(UserIdentityExtractor extractor, UserIdentityExtractor other) {
|
||||
this.extractor = extractor;
|
||||
this.other = other;
|
||||
|
||||
if (this.extractor == null)
|
||||
throw new NullArgumentException("extractor");
|
||||
if (this.other == null)
|
||||
throw new NullArgumentException("other");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
Object result = this.extractor.extractUserIdentity(certs);
|
||||
if (result == null)
|
||||
result = this.other.extractUserIdentity(certs);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static class X500NameRDNExtractor extends UserIdentityExtractor {
|
||||
class X500NameRDNExtractorBCProvider extends X500NameRDNExtractor {
|
||||
|
||||
private ASN1ObjectIdentifier x500NameStyle;
|
||||
Function<X509Certificate[],X500Name> x500Name;
|
||||
X500NameRDNExtractor(ASN1ObjectIdentifier x500NameStyle, Function<X509Certificate[],X500Name> x500Name) {
|
||||
this.x500NameStyle = x500NameStyle;
|
||||
Function<X509Certificate[],Principal> x500Name;
|
||||
|
||||
public X500NameRDNExtractorBCProvider(String attrName, Function<X509Certificate[], Principal> x500Name) {
|
||||
this.x500NameStyle = BCStyle.INSTANCE.attrNameToOID(attrName);
|
||||
this.x500Name = x500Name;
|
||||
}
|
||||
|
||||
|
@ -92,7 +67,7 @@ public abstract class UserIdentityExtractor {
|
|||
if (certs == null || certs.length == 0)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
X500Name name = x500Name.apply(certs);
|
||||
X500Name name = new X500Name(x500Name.apply(certs).getName());
|
||||
if (name != null) {
|
||||
RDN[] rnds = name.getRDNs(x500NameStyle);
|
||||
if (rnds != null && rnds.length > 0) {
|
||||
|
@ -107,7 +82,7 @@ public abstract class UserIdentityExtractor {
|
|||
/**
|
||||
* Extracts the subject identifier from the subjectAltName extension.
|
||||
*/
|
||||
static class SubjectAltNameExtractor extends UserIdentityExtractor {
|
||||
class SubjectAltNameExtractorBCProvider extends SubjectAltNameExtractor {
|
||||
|
||||
// User Principal Name. Used typically by Microsoft in certificates for Smart Card Login
|
||||
private static final String UPN_OID = "1.3.6.1.4.1.311.20.2.3";
|
||||
|
@ -119,7 +94,7 @@ public abstract class UserIdentityExtractor {
|
|||
*
|
||||
* @param generalName an integer representing the general name. See {@link X509Certificate#getSubjectAlternativeNames()}
|
||||
*/
|
||||
SubjectAltNameExtractor(int generalName) {
|
||||
SubjectAltNameExtractorBCProvider(int generalName) {
|
||||
this.generalName = generalName;
|
||||
}
|
||||
|
||||
|
@ -213,83 +188,14 @@ public abstract class UserIdentityExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
static class PatternMatcher extends UserIdentityExtractor {
|
||||
private final String _pattern;
|
||||
private final Function<X509Certificate[],String> _f;
|
||||
PatternMatcher(String pattern, Function<X509Certificate[],String> valueToMatch) {
|
||||
_pattern = pattern;
|
||||
_f = valueToMatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
String value = Optional.ofNullable(_f.apply(certs)).orElseThrow(IllegalArgumentException::new);
|
||||
|
||||
Pattern r = Pattern.compile(_pattern, Pattern.CASE_INSENSITIVE);
|
||||
|
||||
Matcher m = r.matcher(value);
|
||||
|
||||
if (!m.find()) {
|
||||
logger.debugf("[PatternMatcher:extract] No matches were found for input \"%s\", pattern=\"%s\"", value, _pattern);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (m.groupCount() != 1) {
|
||||
logger.debugf("[PatternMatcher:extract] Match produced more than a single group for input \"%s\", pattern=\"%s\"", value, _pattern);
|
||||
return null;
|
||||
}
|
||||
|
||||
return m.group(1);
|
||||
}
|
||||
@Override
|
||||
public UserIdentityExtractor getX500NameExtractor(String identifier, Function<X509Certificate[], Principal> x500Name) {
|
||||
return new X500NameRDNExtractorBCProvider(identifier, x500Name);
|
||||
}
|
||||
|
||||
static class OrBuilder {
|
||||
UserIdentityExtractor extractor;
|
||||
UserIdentityExtractor other;
|
||||
OrBuilder(UserIdentityExtractor extractor) {
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
public UserIdentityExtractor or(UserIdentityExtractor other) {
|
||||
return new OrExtractor(extractor, other);
|
||||
}
|
||||
@Override
|
||||
public SubjectAltNameExtractor getSubjectAltNameExtractor(int generalName) {
|
||||
return new SubjectAltNameExtractorBCProvider(generalName);
|
||||
}
|
||||
|
||||
public static UserIdentityExtractor getPatternIdentityExtractor(String pattern,
|
||||
Function<X509Certificate[],String> func) {
|
||||
return new PatternMatcher(pattern, func);
|
||||
}
|
||||
|
||||
public static UserIdentityExtractor getX500NameExtractor(ASN1ObjectIdentifier identifier, Function<X509Certificate[],X500Name> x500Name) {
|
||||
return new X500NameRDNExtractor(identifier, x500Name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the subjectAltName given a <code>generalName</code>.
|
||||
*
|
||||
* @param generalName an integer representing the general name. See {@link X509Certificate#getSubjectAlternativeNames()}
|
||||
* @return the value from the subjectAltName extension
|
||||
*/
|
||||
public static SubjectAltNameExtractor getSubjectAltNameExtractor(int generalName) {
|
||||
return new SubjectAltNameExtractor(generalName);
|
||||
}
|
||||
|
||||
public static OrBuilder either(UserIdentityExtractor extractor) {
|
||||
return new OrBuilder(extractor);
|
||||
}
|
||||
|
||||
public static UserIdentityExtractor getCertificatePemIdentityExtractor(X509AuthenticatorConfigModel config) {
|
||||
return new UserIdentityExtractor() {
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
if (certs == null || certs.length == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
String pem = PemUtils.encodeCertificate(certs[0]);
|
||||
logger.debugf("Using PEM certificate \"%s\" as user identity.", pem);
|
||||
return pem;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -12,8 +12,10 @@ import org.bouncycastle.jce.spec.ECNamedCurveSpec;
|
|||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.keycloak.common.crypto.CryptoProvider;
|
||||
import org.keycloak.common.crypto.CryptoConstants;
|
||||
import org.keycloak.common.crypto.ECDSACryptoProvider;
|
||||
import org.keycloak.common.crypto.CertificateUtilsProvider;
|
||||
import org.keycloak.common.crypto.PemUtilsProvider;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -67,4 +69,20 @@ public class DefaultCryptoProvider implements CryptoProvider {
|
|||
return new ECNamedCurveSpec("prime256v1", spec.getCurve(), spec.getG(), spec.getN());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserIdentityExtractorProvider getIdentityExtractorProvider() {
|
||||
return new BCUserIdentityExtractorProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECDSACryptoProvider getEcdsaCryptoProvider() {
|
||||
return new BCECDSACryptoProvider();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T> T getOCSPProver(Class<T> clazz) {
|
||||
return clazz.cast(new BCOCSPProvider());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -41,6 +41,19 @@
|
|||
<scope>test</scope>
|
||||
<type>test-jar</type>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi-private</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
|
|
|
@ -17,30 +17,54 @@
|
|||
|
||||
package org.keycloak.crypto.fips;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints;
|
||||
import org.bouncycastle.asn1.x509.CRLDistPoint;
|
||||
import org.bouncycastle.asn1.x509.CertificatePolicies;
|
||||
import org.bouncycastle.asn1.x509.DistributionPoint;
|
||||
import org.bouncycastle.asn1.x509.DistributionPointName;
|
||||
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.Extensions;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.asn1.x509.GeneralNames;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.KeyUsage;
|
||||
import org.bouncycastle.asn1.x509.PolicyInformation;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.X509v1CertificateBuilder;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.crypto.CertificateUtilsProvider;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The Class CertificateUtils provides utility functions for generation of V1 and V3 {@link java.security.cert.X509Certificate}
|
||||
|
@ -165,4 +189,133 @@ public class BCFIPSCertificateUtilsProvider implements CertificateUtilsProvider{
|
|||
throw new RuntimeException("Could not create content signer.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException {
|
||||
|
||||
Extensions certExtensions = new JcaX509CertificateHolder(cert).getExtensions();
|
||||
if (certExtensions == null)
|
||||
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate extensions were found");
|
||||
|
||||
CertificatePolicies policies = CertificatePolicies.fromExtensions(certExtensions);
|
||||
|
||||
if (policies == null)
|
||||
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate policy extensions were found");
|
||||
|
||||
List<String> policyList = new LinkedList<>();
|
||||
Arrays.stream(policies.getPolicyInformation()).forEach(p -> policyList.add(p.getPolicyIdentifier().toString().toLowerCase()));
|
||||
|
||||
return policyList;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
|
||||
public X509Certificate createServicesTestCertificate(String dn,
|
||||
Date startDate,
|
||||
Date expiryDate,
|
||||
KeyPair keyPair,
|
||||
String... certificatePolicyOid) {
|
||||
// Cert data
|
||||
X500Name subjectDN = new X500Name(dn);
|
||||
X500Name issuerDN = new X500Name(dn);
|
||||
|
||||
SubjectPublicKeyInfo subjPubKeyInfo = SubjectPublicKeyInfo.getInstance(
|
||||
ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
BigInteger serialNumber = new BigInteger(130, new SecureRandom());
|
||||
|
||||
// Build the certificate
|
||||
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate,
|
||||
subjectDN, subjPubKeyInfo);
|
||||
|
||||
if (certificatePolicyOid != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (Extension certExtension: certPolicyExtensions(certificatePolicyOid))
|
||||
certGen.addExtension(certExtension);
|
||||
} catch (CertIOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the cert with the private key
|
||||
try {
|
||||
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.setProvider(BouncyIntegration.PROVIDER)
|
||||
.build(keyPair.getPrivate());
|
||||
X509Certificate x509Certificate = new JcaX509CertificateConverter()
|
||||
.setProvider(BouncyIntegration.PROVIDER)
|
||||
.getCertificate(certGen.build(contentSigner));
|
||||
|
||||
return x509Certificate;
|
||||
} catch (CertificateException | OperatorCreationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Extension> certPolicyExtensions(String... certificatePolicyOid) {
|
||||
List<Extension> certificatePolicies = new LinkedList<>();
|
||||
|
||||
if (certificatePolicyOid != null && certificatePolicyOid.length > 0)
|
||||
{
|
||||
List<PolicyInformation> policyInfoList = new LinkedList<>();
|
||||
for (String oid: certificatePolicyOid)
|
||||
{
|
||||
policyInfoList.add(new PolicyInformation(new ASN1ObjectIdentifier(oid)));
|
||||
}
|
||||
|
||||
CertificatePolicies policies = new CertificatePolicies(policyInfoList.toArray(new PolicyInformation[0]));
|
||||
|
||||
try {
|
||||
boolean isCritical = false;
|
||||
Extension extension = new Extension(Extension.certificatePolicies, isCritical, policies.getEncoded());
|
||||
certificatePolicies.add(extension);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
return certificatePolicies;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package org.keycloak.crypto.fips;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1Integer;
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERSequenceGenerator;
|
||||
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||
import org.keycloak.common.crypto.ECDSACryptoProvider;
|
||||
|
||||
public class BCFIPSECDSACryptoProvider implements ECDSACryptoProvider {
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException {
|
||||
int len = signLength / 2;
|
||||
int arraySize = len + 1;
|
||||
|
||||
byte[] r = new byte[arraySize];
|
||||
byte[] s = new byte[arraySize];
|
||||
System.arraycopy(signature, 0, r, 1, len);
|
||||
System.arraycopy(signature, len, s, 1, len);
|
||||
BigInteger rBigInteger = new BigInteger(r);
|
||||
BigInteger sBigInteger = new BigInteger(s);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DERSequenceGenerator seqGen = new DERSequenceGenerator(bos);
|
||||
|
||||
seqGen.addObject(new ASN1Integer(rBigInteger.toByteArray()));
|
||||
seqGen.addObject(new ASN1Integer(sBigInteger.toByteArray()));
|
||||
seqGen.close();
|
||||
bos.close();
|
||||
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException {
|
||||
int len = signLength / 2;
|
||||
|
||||
ASN1InputStream asn1InputStream = new ASN1InputStream(derEncodedSignatureValue);
|
||||
ASN1Primitive asn1Primitive = asn1InputStream.readObject();
|
||||
asn1InputStream.close();
|
||||
|
||||
ASN1Sequence asn1Sequence = (ASN1Sequence.getInstance(asn1Primitive));
|
||||
ASN1Integer rASN1 = (ASN1Integer) asn1Sequence.getObjectAt(0);
|
||||
ASN1Integer sASN1 = (ASN1Integer) asn1Sequence.getObjectAt(1);
|
||||
X9IntegerConverter x9IntegerConverter = new X9IntegerConverter();
|
||||
byte[] r = x9IntegerConverter.integerToBytes(rASN1.getValue(), len);
|
||||
byte[] s = x9IntegerConverter.integerToBytes(sASN1.getValue(), len);
|
||||
|
||||
byte[] concatenatedSignatureValue = new byte[signLength];
|
||||
System.arraycopy(r, 0, concatenatedSignatureValue, 0, len);
|
||||
System.arraycopy(s, 0, concatenatedSignatureValue, len, len);
|
||||
|
||||
return concatenatedSignatureValue;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,449 @@
|
|||
/*
|
||||
* 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.crypto.fips;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERIA5String;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.Extensions;
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
|
||||
import org.bouncycastle.asn1.x509.AccessDescription;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
|
||||
import org.bouncycastle.cert.ocsp.CertificateID;
|
||||
import org.bouncycastle.cert.ocsp.CertificateStatus;
|
||||
import org.bouncycastle.cert.ocsp.OCSPException;
|
||||
import org.bouncycastle.cert.ocsp.OCSPReq;
|
||||
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
|
||||
import org.bouncycastle.cert.ocsp.OCSPResp;
|
||||
import org.bouncycastle.cert.ocsp.RevokedStatus;
|
||||
import org.bouncycastle.cert.ocsp.SingleResp;
|
||||
import org.bouncycastle.cert.ocsp.UnknownStatus;
|
||||
import org.bouncycastle.cert.ocsp.jcajce.JcaCertificateID;
|
||||
import org.bouncycastle.operator.ContentVerifierProvider;
|
||||
import org.bouncycastle.operator.DigestCalculator;
|
||||
import org.bouncycastle.operator.DigestCalculatorProvider;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.utils.OCSPProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URI;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CRLReason;
|
||||
import java.security.cert.CertPath;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
|
||||
* @version $Revision: 1 $
|
||||
* @since 10/29/2016
|
||||
*/
|
||||
|
||||
public class BCFIPSOCSPProvider extends OCSPProvider {
|
||||
|
||||
private final static Logger logger = Logger.getLogger(BCFIPSOCSPProvider.class.getName());
|
||||
|
||||
protected OCSPResp getResponse(KeycloakSession session, OCSPReq ocspReq, URI responderUri) throws IOException, InterruptedException {
|
||||
byte[] data = getEncodedOCSPResponse(session, ocspReq.getEncoded(), responderUri);
|
||||
return new OCSPResp(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests certificate revocation status using OCSP.
|
||||
* @param cert the certificate to be checked
|
||||
* @param issuerCertificate the issuer certificate
|
||||
* @param responderURIs the OCSP responder URIs
|
||||
* @param responderCert the OCSP responder certificate
|
||||
* @param date if null, the current time is used.
|
||||
* @return a revocation status
|
||||
* @throws CertPathValidatorException
|
||||
*/
|
||||
@Override
|
||||
protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException {
|
||||
if (responderURIs == null || responderURIs.size() == 0)
|
||||
throw new IllegalArgumentException("Need at least one responder");
|
||||
try {
|
||||
|
||||
DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().build();
|
||||
|
||||
DigestCalculator digCalc = dcp.get(CertificateID.HASH_SHA1);
|
||||
JcaCertificateID certificateID = new JcaCertificateID(digCalc, issuerCertificate, cert.getSerialNumber());
|
||||
|
||||
// Create a nounce extension to protect against replay attacks
|
||||
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
|
||||
BigInteger nounce = BigInteger.valueOf(Math.abs(random.nextInt()));
|
||||
|
||||
DEROctetString derString = new DEROctetString(nounce.toByteArray());
|
||||
Extension nounceExtension = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, false, derString);
|
||||
Extensions extensions = new Extensions(nounceExtension);
|
||||
|
||||
OCSPReq ocspReq = new OCSPReqBuilder().addRequest(certificateID, extensions).build();
|
||||
|
||||
URI responderURI = responderURIs.get(0);
|
||||
logger.log(Level.INFO, "OCSP Responder {0}", responderURI);
|
||||
|
||||
try {
|
||||
OCSPResp resp = getResponse(session, ocspReq, responderURI);
|
||||
logger.log(Level.FINE, "Received a response from OCSP responder {0}, the response status is {1}", new Object[]{responderURI, resp.getStatus()});
|
||||
switch (resp.getStatus()) {
|
||||
case OCSPResp.SUCCESSFUL:
|
||||
if (resp.getResponseObject() instanceof BasicOCSPResp) {
|
||||
return processBasicOCSPResponse(issuerCertificate, responderCert, date, certificateID, nounce, (BasicOCSPResp)resp.getResponseObject());
|
||||
} else {
|
||||
throw new CertPathValidatorException("OCSP responder returned an invalid or unknown OCSP response.");
|
||||
}
|
||||
|
||||
case OCSPResp.INTERNAL_ERROR:
|
||||
case OCSPResp.TRY_LATER:
|
||||
throw new CertPathValidatorException("Internal error/try later. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNDETERMINED_REVOCATION_STATUS);
|
||||
|
||||
case OCSPResp.SIG_REQUIRED:
|
||||
throw new CertPathValidatorException("Invalid or missing signature. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.INVALID_SIGNATURE);
|
||||
|
||||
case OCSPResp.UNAUTHORIZED:
|
||||
throw new CertPathValidatorException("Unauthorized request. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED);
|
||||
|
||||
case OCSPResp.MALFORMED_REQUEST:
|
||||
default:
|
||||
throw new CertPathValidatorException("OCSP request is malformed. OCSP response error: " + resp.getStatus(), (Throwable) null, (CertPath) null, -1, CertPathValidatorException.BasicReason.UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
catch(IOException | InterruptedException e) {
|
||||
logger.log(Level.FINE, "OCSP Responder \"{0}\" failed to return a valid OCSP response\n{1}",
|
||||
new Object[] {responderURI, e.getMessage()});
|
||||
throw new CertPathValidatorException("OCSP check failed", e);
|
||||
}
|
||||
}
|
||||
catch(CertificateNotYetValidException | CertificateExpiredException | OperatorCreationException | OCSPException |
|
||||
CertificateEncodingException | NoSuchAlgorithmException | NoSuchProviderException e) {
|
||||
logger.log(Level.FINE, e.getMessage());
|
||||
throw new CertPathValidatorException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private OCSPRevocationStatus processBasicOCSPResponse(X509Certificate issuerCertificate, X509Certificate responderCertificate, Date date, JcaCertificateID certificateID, BigInteger nounce, BasicOCSPResp basicOcspResponse)
|
||||
throws OCSPException, NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
|
||||
SingleResp expectedResponse = null;
|
||||
for (SingleResp singleResponse : basicOcspResponse.getResponses()) {
|
||||
if (compareCertIDs(certificateID, singleResponse.getCertID())) {
|
||||
expectedResponse = singleResponse;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedResponse != null) {
|
||||
verifyResponse(basicOcspResponse, issuerCertificate, responderCertificate, nounce.toByteArray(), date);
|
||||
return singleResponseToRevocationStatus(expectedResponse);
|
||||
} else {
|
||||
throw new CertPathValidatorException("OCSP response does not include a response for a certificate supplied in the OCSP request");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean compareCertIDs(JcaCertificateID idLeft, CertificateID idRight) {
|
||||
if (idLeft == idRight)
|
||||
return true;
|
||||
if (idLeft == null || idRight == null)
|
||||
return false;
|
||||
|
||||
return Arrays.equals(idLeft.getIssuerKeyHash(), idRight.getIssuerKeyHash()) &&
|
||||
Arrays.equals(idLeft.getIssuerNameHash(), idRight.getIssuerNameHash()) &&
|
||||
idLeft.getSerialNumber().equals(idRight.getSerialNumber());
|
||||
}
|
||||
|
||||
private void verifyResponse(BasicOCSPResp basicOcspResponse, X509Certificate issuerCertificate, X509Certificate responderCertificate, byte[] requestNonce, Date date) throws NoSuchProviderException, NoSuchAlgorithmException, CertificateNotYetValidException, CertificateExpiredException, CertPathValidatorException {
|
||||
|
||||
List<X509CertificateHolder> certs = new ArrayList<>(Arrays.asList(basicOcspResponse.getCerts()));
|
||||
X509Certificate signingCert = null;
|
||||
|
||||
try {
|
||||
certs.add(new JcaX509CertificateHolder(issuerCertificate));
|
||||
if (responderCertificate != null) {
|
||||
certs.add(new JcaX509CertificateHolder(responderCertificate));
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (certs.size() > 0) {
|
||||
|
||||
X500Name responderName = basicOcspResponse.getResponderId().toASN1Primitive().getName();
|
||||
byte[] responderKey = basicOcspResponse.getResponderId().toASN1Primitive().getKeyHash();
|
||||
|
||||
if (responderName != null) {
|
||||
logger.log(Level.INFO, "Responder Name: {0}", responderName.toString());
|
||||
for (X509CertificateHolder certHolder : certs) {
|
||||
try {
|
||||
X509Certificate tempCert = new JcaX509CertificateConverter()
|
||||
.setProvider(BouncyIntegration.PROVIDER).getCertificate(certHolder);
|
||||
X500Name respName = new X500Name(tempCert.getSubjectX500Principal().getName());
|
||||
if (responderName.equals(respName)) {
|
||||
signingCert = tempCert;
|
||||
logger.log(Level.INFO, "Found a certificate whose principal \"{0}\" matches the responder name \"{1}\"",
|
||||
new Object[] {tempCert.getSubjectDN().getName(), responderName.toString()});
|
||||
break;
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
logger.log(Level.FINE, e.getMessage());
|
||||
}
|
||||
}
|
||||
} else if (responderKey != null) {
|
||||
SubjectKeyIdentifier responderSubjectKey = new SubjectKeyIdentifier(responderKey);
|
||||
logger.log(Level.INFO, "Responder Key: {0}", Arrays.toString(responderKey));
|
||||
for (X509CertificateHolder certHolder : certs) {
|
||||
try {
|
||||
X509Certificate tempCert = new JcaX509CertificateConverter()
|
||||
.setProvider(BouncyIntegration.PROVIDER).getCertificate(certHolder);
|
||||
|
||||
SubjectKeyIdentifier subjectKeyIdentifier = null;
|
||||
if (certHolder.getExtensions() != null) {
|
||||
subjectKeyIdentifier = SubjectKeyIdentifier.fromExtensions(certHolder.getExtensions());
|
||||
}
|
||||
|
||||
if (subjectKeyIdentifier != null) {
|
||||
logger.log(Level.INFO, "Certificate: {0}\nSubject Key Id: {1}",
|
||||
new Object[] {tempCert.getSubjectDN().getName(), Arrays.toString(subjectKeyIdentifier.getKeyIdentifier())});
|
||||
}
|
||||
|
||||
if (subjectKeyIdentifier != null && responderSubjectKey.equals(subjectKeyIdentifier)) {
|
||||
signingCert = tempCert;
|
||||
logger.log(Level.INFO, "Found a signer certificate \"{0}\" with the subject key extension value matching the responder key",
|
||||
signingCert.getSubjectDN().getName());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
subjectKeyIdentifier = new JcaX509ExtensionUtils().createSubjectKeyIdentifier(tempCert.getPublicKey());
|
||||
if (responderSubjectKey.equals(subjectKeyIdentifier)) {
|
||||
signingCert = tempCert;
|
||||
logger.log(Level.INFO, "Found a certificate \"{0}\" with the subject key matching the OCSP responder key", signingCert.getSubjectDN().getName());
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (CertificateException e) {
|
||||
logger.log(Level.FINE, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (signingCert != null) {
|
||||
if (signingCert.equals(issuerCertificate)) {
|
||||
logger.log(Level.INFO, "OCSP response is signed by the target''s Issuing CA");
|
||||
} else if (responderCertificate != null && signingCert.equals(responderCertificate)) {
|
||||
// https://www.ietf.org/rfc/rfc2560.txt
|
||||
// 2.6 OCSP Signature Authority Delegation
|
||||
// - The responder certificate is issued to the responder by CA
|
||||
logger.log(Level.INFO, "OCSP response is signed by an authorized responder certificate");
|
||||
} else {
|
||||
// 4.2.2.2 Authorized Responders
|
||||
// 3. Includes a value of id-ad-ocspSigning in an ExtendedKeyUsage
|
||||
// extension and is issued by the CA that issued the certificate in
|
||||
// question."
|
||||
if (!signingCert.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) {
|
||||
logger.log(Level.INFO, "Signer certificate''s Issuer: {0}\nIssuer certificate''s Subject: {1}",
|
||||
new Object[] {signingCert.getIssuerX500Principal().getName(), issuerCertificate.getSubjectX500Principal().getName()});
|
||||
throw new CertPathValidatorException("Responder\'s certificate is not authorized to sign OCSP responses");
|
||||
}
|
||||
try {
|
||||
List<String> purposes = signingCert.getExtendedKeyUsage();
|
||||
if (purposes == null || !purposes.contains(KeyPurposeId.id_kp_OCSPSigning.getId())) {
|
||||
logger.log(Level.INFO, "OCSPSigning extended usage is not set");
|
||||
throw new CertPathValidatorException("Responder\'s certificate not valid for signing OCSP responses");
|
||||
}
|
||||
} catch (CertificateParsingException e) {
|
||||
logger.log(Level.FINE, "Failed to get certificate''s extended key usage extension\n{0}", e.getMessage());
|
||||
}
|
||||
if (date == null) {
|
||||
signingCert.checkValidity();
|
||||
} else {
|
||||
signingCert.checkValidity(date);
|
||||
}
|
||||
try {
|
||||
Extension noOCSPCheck = new JcaX509CertificateHolder(signingCert).getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nocheck);
|
||||
// TODO If the extension is present, the OCSP client can trust the
|
||||
// responder's certificate for the lifetime of the certificate.
|
||||
logger.log(Level.INFO, "OCSP no-check extension is {0} present", noOCSPCheck == null ? "not" : "");
|
||||
} catch (CertificateEncodingException e) {
|
||||
logger.log(Level.FINE, "Certificate encoding exception: {0}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
signingCert.verify(issuerCertificate.getPublicKey());
|
||||
logger.log(Level.INFO, "OCSP response is signed by an Authorized Responder");
|
||||
|
||||
} catch (GeneralSecurityException ex) {
|
||||
signingCert = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (signingCert == null) {
|
||||
throw new CertPathValidatorException("Unable to verify OCSP Response\'s signature");
|
||||
} else {
|
||||
if (!verifySignature(basicOcspResponse, signingCert)) {
|
||||
throw new CertPathValidatorException("Error verifying OCSP Response\'s signature");
|
||||
} else {
|
||||
Extension responseNonce = basicOcspResponse.getExtension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce);
|
||||
if (responseNonce != null && requestNonce != null && !Arrays.equals(requestNonce, responseNonce.getExtnValue().getOctets())) {
|
||||
throw new CertPathValidatorException("Nonces do not match.");
|
||||
} else {
|
||||
// See Sun's OCSP implementation.
|
||||
// https://www.ietf.org/rfc/rfc2560.txt, if nextUpdate is not set,
|
||||
// the responder is indicating that newer update is avilable all the time
|
||||
long current = date == null ? System.currentTimeMillis() : date.getTime();
|
||||
Date stop = new Date(current + (long) TIME_SKEW);
|
||||
Date start = new Date(current - (long) TIME_SKEW);
|
||||
|
||||
Iterator<SingleResp> iter = Arrays.asList(basicOcspResponse.getResponses()).iterator();
|
||||
SingleResp singleRes = null;
|
||||
do {
|
||||
if (!iter.hasNext()) {
|
||||
return;
|
||||
}
|
||||
singleRes = iter.next();
|
||||
}
|
||||
while (!stop.before(singleRes.getThisUpdate()) &&
|
||||
!start.after(singleRes.getNextUpdate() != null ? singleRes.getNextUpdate() : singleRes.getThisUpdate()));
|
||||
|
||||
throw new CertPathValidatorException("Response is unreliable: its validity interval is out-of-date");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifySignature(BasicOCSPResp basicOcspResponse, X509Certificate cert) {
|
||||
try {
|
||||
ContentVerifierProvider contentVerifier = new JcaContentVerifierProviderBuilder()
|
||||
.setProvider(BouncyIntegration.PROVIDER).build(cert.getPublicKey());
|
||||
return basicOcspResponse.isSignatureValid(contentVerifier);
|
||||
} catch (OperatorCreationException e) {
|
||||
logger.log(Level.FINE, "Unable to construct OCSP content signature verifier\n{0}", e.getMessage());
|
||||
} catch (OCSPException e) {
|
||||
logger.log(Level.FINE, "Unable to validate OCSP response signature\n{0}", e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private OCSPRevocationStatus singleResponseToRevocationStatus(final SingleResp singleResponse) throws CertPathValidatorException {
|
||||
final CertificateStatus certStatus = singleResponse.getCertStatus();
|
||||
|
||||
CRLReason revocationReason = CRLReason.UNSPECIFIED;
|
||||
Date revocationTime = null;
|
||||
RevocationStatus status = RevocationStatus.UNKNOWN;
|
||||
if (certStatus == CertificateStatus.GOOD) {
|
||||
status = RevocationStatus.GOOD;
|
||||
} else if (certStatus instanceof RevokedStatus) {
|
||||
RevokedStatus revoked = (RevokedStatus)certStatus;
|
||||
revocationTime = revoked.getRevocationTime();
|
||||
status = RevocationStatus.REVOKED;
|
||||
if (revoked.hasRevocationReason()) {
|
||||
revocationReason = CRLReason.values()[revoked.getRevocationReason()];
|
||||
}
|
||||
} else if (certStatus instanceof UnknownStatus) {
|
||||
status = RevocationStatus.UNKNOWN;
|
||||
} else {
|
||||
throw new CertPathValidatorException("Unrecognized revocation status received from OCSP.");
|
||||
}
|
||||
|
||||
final RevocationStatus finalStatus = status;
|
||||
final Date finalRevocationTime = revocationTime;
|
||||
final CRLReason finalRevocationReason = revocationReason;
|
||||
return new OCSPRevocationStatus() {
|
||||
@Override
|
||||
public RevocationStatus getRevocationStatus() {
|
||||
return finalStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRevocationTime() {
|
||||
return finalRevocationTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CRLReason getRevocationReason() {
|
||||
return finalRevocationReason;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts OCSP responder URI from X509 AIA v3 extension, if available. There can be
|
||||
* multiple responder URIs encoded in the certificate.
|
||||
* @param cert
|
||||
* @return a list of available responder URIs.
|
||||
* @throws CertificateEncodingException
|
||||
*/
|
||||
@Override
|
||||
protected List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException {
|
||||
|
||||
LinkedList<String> responderURIs = new LinkedList<>();
|
||||
JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert);
|
||||
Extension aia = holder.getExtension(Extension.authorityInfoAccess);
|
||||
if (aia != null) {
|
||||
try {
|
||||
ASN1InputStream in = new ASN1InputStream(aia.getExtnValue().getOctetStream());
|
||||
ASN1Sequence seq = (ASN1Sequence)in.readObject();
|
||||
AuthorityInformationAccess authorityInfoAccess = AuthorityInformationAccess.getInstance(seq);
|
||||
for (AccessDescription ad : authorityInfoAccess.getAccessDescriptions()) {
|
||||
if (ad.getAccessMethod().equals(AccessDescription.id_ad_ocsp)) {
|
||||
// See https://www.ietf.org/rfc/rfc2560.txt, 3.1 Certificate Content
|
||||
if (ad.getAccessLocation().getTagNo() == GeneralName.uniformResourceIdentifier) {
|
||||
DERIA5String value = DERIA5String.getInstance(ad.getAccessLocation().getName());
|
||||
responderURIs.add(value.getString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return responderURIs;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* 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.crypto.fips;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.ASN1TaggedObject;
|
||||
import org.bouncycastle.asn1.DERUTF8String;
|
||||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractor;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
|
||||
* @version $Revision: 1 $
|
||||
* @date 7/30/2016
|
||||
*/
|
||||
|
||||
public class BCFIPSUserIdentityExtractorProvider extends UserIdentityExtractorProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(BCFIPSUserIdentityExtractorProvider.class.getName());
|
||||
|
||||
class X500NameRDNExtractorBCProvider extends X500NameRDNExtractor {
|
||||
|
||||
private ASN1ObjectIdentifier x500NameStyle;
|
||||
Function<X509Certificate[],Principal> x500Name;
|
||||
|
||||
public X500NameRDNExtractorBCProvider(String attrName, Function<X509Certificate[], Principal> x500Name) {
|
||||
this.x500NameStyle = BCStyle.INSTANCE.attrNameToOID(attrName);
|
||||
this.x500Name = x500Name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
|
||||
if (certs == null || certs.length == 0)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
X500Name name = new X500Name(x500Name.apply(certs).getName());
|
||||
if (name != null) {
|
||||
RDN[] rnds = name.getRDNs(x500NameStyle);
|
||||
if (rnds != null && rnds.length > 0) {
|
||||
RDN cn = rnds[0];
|
||||
return IETFUtils.valueToString(cn.getFirst().getValue());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the subject identifier from the subjectAltName extension.
|
||||
*/
|
||||
class SubjectAltNameExtractorBCProvider extends SubjectAltNameExtractor {
|
||||
|
||||
// User Principal Name. Used typically by Microsoft in certificates for Smart Card Login
|
||||
private static final String UPN_OID = "1.3.6.1.4.1.311.20.2.3";
|
||||
|
||||
private final int generalName;
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
* @param generalName an integer representing the general name. See {@link X509Certificate#getSubjectAlternativeNames()}
|
||||
*/
|
||||
SubjectAltNameExtractorBCProvider(int generalName) {
|
||||
this.generalName = generalName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object extractUserIdentity(X509Certificate[] certs) {
|
||||
if (certs == null || certs.length == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
try {
|
||||
Collection<List<?>> subjectAlternativeNames = certs[0].getSubjectAlternativeNames();
|
||||
|
||||
if (subjectAlternativeNames == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Iterator<List<?>> iterator = subjectAlternativeNames.iterator();
|
||||
|
||||
boolean foundUpn = false;
|
||||
String tempOtherName = null;
|
||||
String tempOid = null;
|
||||
|
||||
while (iterator.hasNext() && !foundUpn) {
|
||||
List<?> next = iterator.next();
|
||||
|
||||
if (Integer.class.cast(next.get(0)) == generalName) {
|
||||
|
||||
// We will try to find UPN_OID among the subjectAltNames of type 'otherName' . Just if not found, we will fallback to the other type
|
||||
for (int i = 1 ; i<next.size() ; i++) {
|
||||
Object obj = next.get(i);
|
||||
|
||||
// We have Subject Alternative Name of other type than 'otherName' . Just return it directly
|
||||
if (generalName != 0) {
|
||||
logger.tracef("Extracted identity '%s' from Subject Alternative Name of type '%d'", obj, generalName);
|
||||
return obj;
|
||||
}
|
||||
|
||||
byte[] otherNameBytes = (byte[]) obj;
|
||||
|
||||
try {
|
||||
ASN1InputStream asn1Stream = new ASN1InputStream(new ByteArrayInputStream(otherNameBytes));
|
||||
ASN1Encodable asn1otherName = asn1Stream.readObject();
|
||||
asn1otherName = unwrap(asn1otherName);
|
||||
|
||||
ASN1Sequence asn1Sequence = ASN1Sequence.getInstance(asn1otherName);
|
||||
|
||||
if (asn1Sequence != null) {
|
||||
ASN1Encodable encodedOid = asn1Sequence.getObjectAt(0);
|
||||
ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(unwrap(encodedOid));
|
||||
tempOid = oid.getId();
|
||||
|
||||
ASN1Encodable principalNameEncoded = asn1Sequence.getObjectAt(1);
|
||||
DERUTF8String principalName = DERUTF8String.getInstance(unwrap(principalNameEncoded));
|
||||
|
||||
tempOtherName = principalName.getString();
|
||||
|
||||
// We found UPN among the 'otherName' principal. We don't need to look other
|
||||
if (UPN_OID.equals(tempOid)) {
|
||||
foundUpn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to parse subjectAltName", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
logger.tracef("Parsed otherName from subjectAltName. OID: '%s', Principal: '%s'", tempOid, tempOtherName);
|
||||
|
||||
return tempOtherName;
|
||||
|
||||
} catch (CertificateParsingException cause) {
|
||||
logger.errorf(cause, "Failed to obtain identity from subjectAltName extension");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private ASN1Encodable unwrap(ASN1Encodable encodable) {
|
||||
while (encodable instanceof ASN1TaggedObject) {
|
||||
ASN1TaggedObject taggedObj = (ASN1TaggedObject) encodable;
|
||||
encodable = taggedObj.getObject();
|
||||
}
|
||||
|
||||
return encodable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserIdentityExtractor getX500NameExtractor(String identifier, Function<X509Certificate[], Principal> x500Name) {
|
||||
return new X500NameRDNExtractorBCProvider(identifier, x500Name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SubjectAltNameExtractor getSubjectAltNameExtractor(int generalName) {
|
||||
return new SubjectAltNameExtractorBCProvider(generalName);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,9 +18,11 @@ import org.bouncycastle.crypto.fips.FipsSHS;
|
|||
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
|
||||
import org.bouncycastle.math.ec.ECCurve;
|
||||
import org.keycloak.common.crypto.CryptoProvider;
|
||||
import org.keycloak.common.crypto.ECDSACryptoProvider;
|
||||
import org.keycloak.common.crypto.CryptoConstants;
|
||||
import org.keycloak.common.crypto.CertificateUtilsProvider;
|
||||
import org.keycloak.common.crypto.PemUtilsProvider;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -100,4 +102,20 @@ public class FIPS1402Provider implements CryptoProvider {
|
|||
ECPoint point = new ECPoint( params.getG().getXCoord().toBigInteger(), params.getG().getYCoord().toBigInteger());
|
||||
return new ECParameterSpec( c,point, params.getN(), params.getH().intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserIdentityExtractorProvider getIdentityExtractorProvider() {
|
||||
return new BCFIPSUserIdentityExtractorProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECDSACryptoProvider getEcdsaCryptoProvider() {
|
||||
return new BCFIPSECDSACryptoProvider();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public <T> T getOCSPProver(Class<T> clazz) {
|
||||
return clazz.cast(new BCFIPSOCSPProvider());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>fips1402</module>
|
||||
<module>default</module>
|
||||
<module>fips1402</module>
|
||||
</modules>
|
||||
</project>
|
|
@ -27,8 +27,11 @@
|
|||
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
|
||||
<module name="org.keycloak.keycloak-common"/>
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-server-spi"/>
|
||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="org.jboss.logging"/>
|
||||
<module name="org.apache.httpcomponents"/>
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
<module name="org.liquibase" services="import"/>
|
||||
<module name="org.javassist"/>
|
||||
<module name="org.hibernate" services="import"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="javax.api"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
<module name="com.google.zxing.core"/>
|
||||
<module name="com.google.zxing.javase"/>
|
||||
<module name="org.jboss.logging"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="javax.json.api"/>
|
||||
|
|
|
@ -66,7 +66,6 @@
|
|||
<module name="com.google.zxing.core"/>
|
||||
<module name="com.google.zxing.javase"/>
|
||||
<module name="org.jboss.logging"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="javax.json.api"/>
|
||||
|
|
|
@ -27,8 +27,11 @@
|
|||
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
|
||||
<module name="org.keycloak.keycloak-common"/>
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-server-spi"/>
|
||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="org.jboss.logging"/>
|
||||
<module name="org.apache.httpcomponents"/>
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="sun.jdk" optional="true" />
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
<module name="org.liquibase" services="import"/>
|
||||
<module name="org.javassist"/>
|
||||
<module name="org.hibernate" services="import"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="javax.api"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
<module name="com.google.zxing.core"/>
|
||||
<module name="com.google.zxing.javase"/>
|
||||
<module name="org.jboss.logging"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="javax.json.api"/>
|
||||
|
|
|
@ -74,7 +74,6 @@
|
|||
<module name="com.google.zxing.core"/>
|
||||
<module name="com.google.zxing.javase"/>
|
||||
<module name="org.jboss.logging"/>
|
||||
<module name="org.bouncycastle" />
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.activation.api"/>
|
||||
<module name="javax.json.api"/>
|
||||
|
|
|
@ -42,10 +42,6 @@
|
|||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
|
|
|
@ -12,10 +12,6 @@
|
|||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
|
|
|
@ -12,10 +12,6 @@
|
|||
<description/>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
|
|
|
@ -44,10 +44,6 @@
|
|||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* 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.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.cert.CRLReason;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.logging.Logger.Level;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>
|
||||
* @version $Revision: 1 $
|
||||
* @since 10/29/2016
|
||||
*/
|
||||
|
||||
public abstract class OCSPProvider {
|
||||
|
||||
private final static Logger logger = Logger.getLogger(OCSPProvider.class);
|
||||
|
||||
protected static int OCSP_CONNECT_TIMEOUT = 10000; // 10 sec
|
||||
protected static final int TIME_SKEW = 900000;
|
||||
|
||||
public enum RevocationStatus {
|
||||
GOOD,
|
||||
REVOKED,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public interface OCSPRevocationStatus {
|
||||
RevocationStatus getRevocationStatus();
|
||||
Date getRevocationTime();
|
||||
CRLReason getRevocationReason();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests certificate revocation status using OCSP.
|
||||
* @param session Keycloak session
|
||||
* @param cert the certificate to be checked
|
||||
* @param issuerCertificate The issuer certificate
|
||||
* @param responderURI an address of OCSP responder. Overrides any OCSP responder URIs stored in certificate's AIA extension
|
||||
* @param date
|
||||
* @param responderCert a certificate that OCSP responder uses to sign OCSP responses
|
||||
* @return revocation status
|
||||
*/
|
||||
public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, URI responderURI, X509Certificate responderCert, Date date) throws CertPathValidatorException {
|
||||
if (cert == null)
|
||||
throw new IllegalArgumentException("cert cannot be null");
|
||||
if (issuerCertificate == null)
|
||||
throw new IllegalArgumentException("issuerCertificate cannot be null");
|
||||
if (responderURI == null)
|
||||
throw new IllegalArgumentException("responderURI cannot be null");
|
||||
|
||||
return check(session, cert, issuerCertificate, Collections.singletonList(responderURI), responderCert, date);
|
||||
}
|
||||
/**
|
||||
* Requests certificate revocation status using OCSP. The OCSP responder URI
|
||||
* is obtained from the certificate's AIA extension.
|
||||
* @param session Keycloak session
|
||||
* @param cert the certificate to be checked
|
||||
* @param issuerCertificate The issuer certificate
|
||||
* @param date
|
||||
* @return revocation status
|
||||
*/
|
||||
public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, Date date, X509Certificate responderCert) throws CertPathValidatorException {
|
||||
List<String> responderURIs = null;
|
||||
try {
|
||||
responderURIs = getResponderURIs(cert);
|
||||
} catch (CertificateEncodingException e) {
|
||||
logger.log(Level.DEBUG, "CertificateEncodingException: {0}", e);
|
||||
throw new CertPathValidatorException(e.getMessage(), e);
|
||||
}
|
||||
if (responderURIs.size() == 0) {
|
||||
logger.log(Level.INFO, "No OCSP responders in the specified certificate");
|
||||
throw new CertPathValidatorException("No OCSP Responder URI in certificate");
|
||||
}
|
||||
|
||||
List<URI> uris = new LinkedList<>();
|
||||
for (String value : responderURIs) {
|
||||
try {
|
||||
URI responderURI = URI.create(value);
|
||||
uris.add(responderURI);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
logger.log(Level.DEBUG, "Malformed responder URI {0}", value, ex);
|
||||
}
|
||||
}
|
||||
return check(session, cert, issuerCertificate, Collections.unmodifiableList(uris), responderCert, date);
|
||||
}
|
||||
|
||||
protected byte[] getEncodedOCSPResponse(KeycloakSession session, byte[] encodedOCSPReq, URI responderUri) throws IOException {
|
||||
|
||||
CloseableHttpClient httpClient = session.getProvider(HttpClientProvider.class).getHttpClient();
|
||||
HttpPost post = new HttpPost(responderUri);
|
||||
post.setHeader(HttpHeaders.CONTENT_TYPE, "application/ocsp-request");
|
||||
|
||||
final RequestConfig params = RequestConfig.custom()
|
||||
.setConnectTimeout(OCSP_CONNECT_TIMEOUT)
|
||||
.setSocketTimeout(OCSP_CONNECT_TIMEOUT)
|
||||
.build();
|
||||
post.setConfig(params);
|
||||
|
||||
post.setEntity(new ByteArrayEntity(encodedOCSPReq));
|
||||
|
||||
//Get Response
|
||||
try (CloseableHttpResponse response = httpClient.execute(post)) {
|
||||
try {
|
||||
if (response.getStatusLine().getStatusCode() != 200) {
|
||||
String errorMessage = String.format("Connection error, unable to obtain certificate revocation status using OCSP responder \"%s\", code \"%d\"",
|
||||
responderUri.toString(), response.getStatusLine().getStatusCode());
|
||||
throw new IOException(errorMessage);
|
||||
}
|
||||
|
||||
byte[] data = EntityUtils.toByteArray(response.getEntity());
|
||||
return data;
|
||||
} finally {
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests certificate revocation status using OCSP. The OCSP responder URI
|
||||
* is obtained from the certificate's AIA extension.
|
||||
* @param session Keycloak session
|
||||
* @param cert the certificate to be checked
|
||||
* @param issuerCertificate The issuer certificate
|
||||
* @return revocation status
|
||||
*/
|
||||
public OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
|
||||
return check(session, cert, issuerCertificate, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests certificate revocation status using OCSP.
|
||||
* @param session Keycloak session
|
||||
* @param cert the certificate to be checked
|
||||
* @param issuerCertificate the issuer certificate
|
||||
* @param responderURIs the OCSP responder URIs
|
||||
* @param responderCert the OCSP responder certificate
|
||||
* @param date if null, the current time is used.
|
||||
* @return a revocation status
|
||||
* @throws CertPathValidatorException
|
||||
*/
|
||||
protected abstract OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert,
|
||||
X509Certificate issuerCertificate, List<URI> responderURIs, X509Certificate responderCert, Date date)
|
||||
throws CertPathValidatorException;
|
||||
|
||||
|
||||
protected static OCSPRevocationStatus unknownStatus() {
|
||||
return new OCSPRevocationStatus() {
|
||||
@Override
|
||||
public RevocationStatus getRevocationStatus() {
|
||||
return RevocationStatus.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRevocationTime() {
|
||||
return new Date(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CRLReason getRevocationReason() {
|
||||
return CRLReason.UNSPECIFIED;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts OCSP responder URI from X509 AIA v3 extension, if available. There can be
|
||||
* multiple responder URIs encoded in the certificate.
|
||||
* @param cert
|
||||
* @return a list of available responder URIs.
|
||||
* @throws CertificateEncodingException
|
||||
*/
|
||||
protected abstract List<String> getResponderURIs(X509Certificate cert) throws CertificateEncodingException;
|
||||
|
||||
|
||||
}
|
|
@ -35,14 +35,6 @@
|
|||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk15on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.function.Function;
|
||||
|
@ -26,12 +27,13 @@ import java.util.function.Function;
|
|||
import javax.security.auth.x500.X500Principal;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractor;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractorProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.jose.jws.crypto.HashUtils;
|
||||
|
@ -134,27 +136,13 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
|
||||
protected static class UserIdentityExtractorBuilder {
|
||||
|
||||
private static final Function<X509Certificate[],X500Name> subject = certs -> {
|
||||
try {
|
||||
return new JcaX509CertificateHolder(certs[0]).getSubject();
|
||||
} catch (CertificateEncodingException e) {
|
||||
logger.warn("Unable to get certificate Subject", e);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private static final Function<X509Certificate[],X500Name> issuer = certs -> {
|
||||
try {
|
||||
return new JcaX509CertificateHolder(certs[0]).getIssuer();
|
||||
} catch (CertificateEncodingException e) {
|
||||
logger.warn("Unable to get certificate Issuer", e);
|
||||
}
|
||||
return null;
|
||||
private static final Function<X509Certificate[],Principal> subject = certs -> {
|
||||
return certs[0].getSubjectX500Principal();
|
||||
};
|
||||
|
||||
private static Function<X509Certificate[], String> getSerialnumberFunc(X509AuthenticatorConfigModel config) {
|
||||
return config.isSerialnumberHex() ?
|
||||
certs -> Hex.toHexString(certs[0].getSerialNumber().toByteArray()) :
|
||||
certs -> Hex.encodeHexString(certs[0].getSerialNumber().toByteArray()) :
|
||||
certs -> certs[0].getSerialNumber().toString();
|
||||
}
|
||||
|
||||
|
@ -171,24 +159,28 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
|
||||
UserIdentityExtractor extractor = null;
|
||||
Function<X509Certificate[], String> func = null;
|
||||
|
||||
UserIdentityExtractorProvider userIdExtractor = CryptoIntegration.getProvider().getIdentityExtractorProvider();
|
||||
logger.debug("UID Source: " + userIdentitySource);
|
||||
logger.debug("UID Extractor: " + userIdExtractor.getClass().getName());
|
||||
switch(userIdentitySource) {
|
||||
|
||||
case SUBJECTDN:
|
||||
func = config.isCanonicalDnEnabled() ?
|
||||
certs -> certs[0].getSubjectX500Principal().getName(X500Principal.CANONICAL) :
|
||||
certs -> certs[0].getSubjectDN().getName();
|
||||
extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, func);
|
||||
extractor = userIdExtractor.getPatternIdentityExtractor(pattern, func);
|
||||
break;
|
||||
case ISSUERDN:
|
||||
extractor = UserIdentityExtractor.getPatternIdentityExtractor(pattern, getIssuerDNFunc(config));
|
||||
extractor = userIdExtractor.getPatternIdentityExtractor(pattern, getIssuerDNFunc(config));
|
||||
break;
|
||||
case SERIALNUMBER:
|
||||
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, getSerialnumberFunc(config));
|
||||
extractor = userIdExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, getSerialnumberFunc(config));
|
||||
break;
|
||||
case SHA256_THUMBPRINT:
|
||||
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> {
|
||||
extractor = userIdExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, certs -> {
|
||||
try {
|
||||
return Hex.toHexString(HashUtils.hash(JavaAlgorithm.SHA256, certs[0].getEncoded()));
|
||||
return Hex.encodeHexString(HashUtils.hash(JavaAlgorithm.SHA256, certs[0].getEncoded()));
|
||||
} catch (CertificateEncodingException | HashException e) {
|
||||
logger.warn("Unable to get certificate's thumbprint", e);
|
||||
}
|
||||
|
@ -197,24 +189,24 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
break;
|
||||
case SERIALNUMBER_ISSUERDN:
|
||||
func = certs -> getSerialnumberFunc(config).apply(certs) + Constants.CFG_DELIMITER + getIssuerDNFunc(config).apply(certs);
|
||||
extractor = UserIdentityExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, func);
|
||||
extractor = userIdExtractor.getPatternIdentityExtractor(DEFAULT_MATCH_ALL_EXPRESSION, func);
|
||||
break;
|
||||
case SUBJECTDN_CN:
|
||||
extractor = UserIdentityExtractor.getX500NameExtractor(BCStyle.CN, subject);
|
||||
extractor = userIdExtractor.getX500NameExtractor("CN", subject);
|
||||
break;
|
||||
case SUBJECTDN_EMAIL:
|
||||
extractor = UserIdentityExtractor
|
||||
.either(UserIdentityExtractor.getX500NameExtractor(BCStyle.EmailAddress, subject))
|
||||
.or(UserIdentityExtractor.getX500NameExtractor(BCStyle.E, subject));
|
||||
extractor = userIdExtractor
|
||||
.either(userIdExtractor.getX500NameExtractor("EmailAddress", subject))
|
||||
.or(userIdExtractor.getX500NameExtractor("E", subject));
|
||||
break;
|
||||
case SUBJECTALTNAME_EMAIL:
|
||||
extractor = UserIdentityExtractor.getSubjectAltNameExtractor(1);
|
||||
extractor = userIdExtractor.getSubjectAltNameExtractor(1);
|
||||
break;
|
||||
case SUBJECTALTNAME_OTHERNAME:
|
||||
extractor = UserIdentityExtractor.getSubjectAltNameExtractor(0);
|
||||
extractor = userIdExtractor.getSubjectAltNameExtractor(0);
|
||||
break;
|
||||
case CERTIFICATE_PEM:
|
||||
extractor = UserIdentityExtractor.getCertificatePemIdentityExtractor(config);
|
||||
extractor = userIdExtractor.getCertificatePemIdentityExtractor();
|
||||
break;
|
||||
default:
|
||||
logger.warnf("[UserIdentityExtractorBuilder:fromConfig] Unknown or unsupported user identity source: \"%s\"", userIdentitySource.getName());
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
|
||||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
|
||||
import org.keycloak.common.crypto.CryptoConstants;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.utils.OCSPProvider;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.httpclient.HttpClientProvider;
|
||||
|
@ -49,7 +50,6 @@ import java.net.URI;
|
|||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.CertPathBuilder;
|
||||
import java.security.cert.CertPathBuilderException;
|
||||
import java.security.cert.CertPathValidatorException;
|
||||
import java.security.cert.CertStore;
|
||||
import java.security.cert.CertificateException;
|
||||
|
@ -77,10 +77,6 @@ import org.apache.http.client.methods.CloseableHttpResponse;
|
|||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import org.bouncycastle.asn1.x509.Extensions;
|
||||
import org.bouncycastle.asn1.x509.CertificatePolicies;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
|
||||
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_POLICY_MODE_ANY;
|
||||
|
||||
/**
|
||||
|
@ -165,7 +161,7 @@ public class CertificateValidator {
|
|||
* @param issuerCertificate The issuer certificate
|
||||
* @return revocation status
|
||||
*/
|
||||
public abstract OCSPUtils.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException;
|
||||
public abstract OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException;
|
||||
}
|
||||
|
||||
public abstract static class CRLLoaderImpl {
|
||||
|
@ -190,9 +186,9 @@ public class CertificateValidator {
|
|||
}
|
||||
|
||||
@Override
|
||||
public OCSPUtils.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
|
||||
public OCSPProvider.OCSPRevocationStatus check(X509Certificate cert, X509Certificate issuerCertificate) throws CertPathValidatorException {
|
||||
|
||||
OCSPUtils.OCSPRevocationStatus ocspRevocationStatus = null;
|
||||
OCSPProvider.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,
|
||||
|
@ -203,7 +199,8 @@ public class CertificateValidator {
|
|||
// 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(session, cert, issuerCertificate);
|
||||
OCSPProvider ocspProvider = CryptoIntegration.getProvider().getOCSPProver(OCSPProvider.class);
|
||||
ocspRevocationStatus = ocspProvider.check(session, cert, issuerCertificate);
|
||||
}
|
||||
else {
|
||||
URI uri;
|
||||
|
@ -219,7 +216,8 @@ public class CertificateValidator {
|
|||
// 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(session, cert, issuerCertificate, uri, responderCert, null);
|
||||
OCSPProvider ocspProvider = CryptoIntegration.getProvider().getOCSPProver(OCSPProvider.class);
|
||||
ocspRevocationStatus = ocspProvider.check(session, cert, issuerCertificate, uri, responderCert, null);
|
||||
}
|
||||
return ocspRevocationStatus;
|
||||
}
|
||||
|
@ -454,7 +452,7 @@ public class CertificateValidator {
|
|||
}
|
||||
|
||||
boolean isCritical = false;
|
||||
Set critSet = certs[0].getCriticalExtensionOIDs();
|
||||
Set<String> critSet = certs[0].getCriticalExtensionOIDs();
|
||||
if (critSet != null) {
|
||||
isCritical = critSet.contains("2.5.29.15");
|
||||
}
|
||||
|
@ -491,7 +489,7 @@ public class CertificateValidator {
|
|||
}
|
||||
|
||||
boolean isCritical = false;
|
||||
Set critSet = certs[0].getCriticalExtensionOIDs();
|
||||
Set<String> critSet = certs[0].getCriticalExtensionOIDs();
|
||||
if (critSet != null) {
|
||||
isCritical = critSet.contains("2.5.29.37");
|
||||
}
|
||||
|
@ -510,23 +508,14 @@ public class CertificateValidator {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static void validatePolicy(X509Certificate[] certs, List<String> expectedPolicies, String policyCheckMode) throws GeneralSecurityException {
|
||||
if (expectedPolicies == null || expectedPolicies.size() == 0) {
|
||||
logger.debug("Certificate Policy validation is not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
Extensions certExtensions = new JcaX509CertificateHolder(certs[0]).getExtensions();
|
||||
if (certExtensions == null)
|
||||
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate extensions were found");
|
||||
|
||||
CertificatePolicies policies = CertificatePolicies.fromExtensions(certExtensions);
|
||||
|
||||
if (policies == null)
|
||||
throw new GeneralSecurityException("Certificate Policy validation was expected, but no certificate policy extensions were found");
|
||||
|
||||
List<String> policyList = new LinkedList<>();
|
||||
Arrays.stream(policies.getPolicyInformation()).forEach(p -> policyList.add(p.getPolicyIdentifier().toString().toLowerCase()));
|
||||
List<String> policyList = CryptoIntegration.getProvider().getCertificateUtils().getCertificatePolicyList(certs[0]);
|
||||
|
||||
logger.debugf("Certificate policies found: %s", String.join(",", policyList));
|
||||
|
||||
|
@ -710,7 +699,7 @@ public class CertificateValidator {
|
|||
}
|
||||
|
||||
try {
|
||||
OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);
|
||||
OCSPProvider.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);
|
||||
|
||||
if (rs == null) {
|
||||
if (_ocspFailOpen)
|
||||
|
@ -719,13 +708,13 @@ public class CertificateValidator {
|
|||
throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
|
||||
}
|
||||
|
||||
if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.UNKNOWN) {
|
||||
if (rs.getRevocationStatus() == OCSPProvider.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) {
|
||||
else if (rs.getRevocationStatus() == OCSPProvider.RevocationStatus.REVOKED) {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Certificate's been revoked.");
|
||||
|
@ -755,7 +744,7 @@ public class CertificateValidator {
|
|||
|
||||
private static List<String> getCRLDistributionPoints(X509Certificate cert) {
|
||||
try {
|
||||
return CRLUtils.getCRLDistributionPoints(cert);
|
||||
return CryptoIntegration.getProvider().getCertificateUtils().getCRLDistributionPoints(cert);
|
||||
}
|
||||
catch(IOException e) {
|
||||
logger.error(e.getMessage());
|
||||
|
|
|
@ -1,17 +1,10 @@
|
|||
package org.keycloak.crypto;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
import org.bouncycastle.asn1.ASN1Integer;
|
||||
import org.bouncycastle.asn1.ASN1Primitive;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.DERSequenceGenerator;
|
||||
import org.bouncycastle.asn1.x9.X9IntegerConverter;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class ECDSASignatureProvider implements SignatureProvider {
|
||||
|
||||
|
@ -51,46 +44,11 @@ public class ECDSASignatureProvider implements SignatureProvider {
|
|||
}
|
||||
|
||||
public static byte[] concatenatedRSToASN1DER(final byte[] signature, int signLength) throws IOException {
|
||||
int len = signLength / 2;
|
||||
int arraySize = len + 1;
|
||||
|
||||
byte[] r = new byte[arraySize];
|
||||
byte[] s = new byte[arraySize];
|
||||
System.arraycopy(signature, 0, r, 1, len);
|
||||
System.arraycopy(signature, len, s, 1, len);
|
||||
BigInteger rBigInteger = new BigInteger(r);
|
||||
BigInteger sBigInteger = new BigInteger(s);
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DERSequenceGenerator seqGen = new DERSequenceGenerator(bos);
|
||||
|
||||
seqGen.addObject(new ASN1Integer(rBigInteger.toByteArray()));
|
||||
seqGen.addObject(new ASN1Integer(sBigInteger.toByteArray()));
|
||||
seqGen.close();
|
||||
bos.close();
|
||||
|
||||
return bos.toByteArray();
|
||||
return CryptoIntegration.getProvider().getEcdsaCryptoProvider().concatenatedRSToASN1DER(signature, signLength);
|
||||
}
|
||||
|
||||
public static byte[] asn1derToConcatenatedRS(final byte[] derEncodedSignatureValue, int signLength) throws IOException {
|
||||
int len = signLength / 2;
|
||||
|
||||
ASN1InputStream asn1InputStream = new ASN1InputStream(derEncodedSignatureValue);
|
||||
ASN1Primitive asn1Primitive = asn1InputStream.readObject();
|
||||
asn1InputStream.close();
|
||||
|
||||
ASN1Sequence asn1Sequence = (ASN1Sequence.getInstance(asn1Primitive));
|
||||
ASN1Integer rASN1 = (ASN1Integer) asn1Sequence.getObjectAt(0);
|
||||
ASN1Integer sASN1 = (ASN1Integer) asn1Sequence.getObjectAt(1);
|
||||
X9IntegerConverter x9IntegerConverter = new X9IntegerConverter();
|
||||
byte[] r = x9IntegerConverter.integerToBytes(rASN1.getValue(), len);
|
||||
byte[] s = x9IntegerConverter.integerToBytes(sASN1.getValue(), len);
|
||||
|
||||
byte[] concatenatedSignatureValue = new byte[signLength];
|
||||
System.arraycopy(r, 0, concatenatedSignatureValue, 0, len);
|
||||
System.arraycopy(s, 0, concatenatedSignatureValue, len, len);
|
||||
|
||||
return concatenatedSignatureValue;
|
||||
return CryptoIntegration.getProvider().getEcdsaCryptoProvider().asn1derToConcatenatedRS(derEncodedSignatureValue, signLength);
|
||||
}
|
||||
|
||||
public enum ECDSA {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package org.keycloak.protocol.docker.installation.compose;
|
||||
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
|
@ -24,7 +26,7 @@ public class DockerComposeCertsDirectory {
|
|||
|
||||
final KeyPairGenerator keyGen;
|
||||
try {
|
||||
keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen = KeyPairGenerator.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
keyGen.initialize(2048, new SecureRandom());
|
||||
|
||||
final KeyPair keypair = keyGen.generateKeyPair();
|
||||
|
@ -35,10 +37,7 @@ public class DockerComposeCertsDirectory {
|
|||
localhostKeyFile = new AbstractMap.SimpleImmutableEntry<>(registryKeyFilename, DockerCertFileUtils.formatPrivateKeyContents(privateKey).getBytes());
|
||||
idpTrustChainFile = new AbstractMap.SimpleEntry<>(idpCertTrustChainFilename, DockerCertFileUtils.formatCrtFileContents(realmCert).getBytes());
|
||||
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
// TODO throw error here descritively
|
||||
throw new RuntimeException(e);
|
||||
} catch (final CertificateEncodingException e) {
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException e) {
|
||||
// TODO throw error here descritively
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
|
|
@ -17,29 +17,16 @@
|
|||
|
||||
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;
|
||||
|
@ -55,49 +42,6 @@ public final class CRLUtils {
|
|||
|
||||
private static final Logger log = Logger.getLogger(CRLUtils.class);
|
||||
|
||||
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.
|
||||
|
|
|
@ -9,6 +9,8 @@ import java.security.cert.X509Certificate;
|
|||
import org.junit.ClassRule;
|
||||
import org.keycloak.rule.CryptoInitRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractor;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
|
||||
|
@ -23,8 +25,8 @@ public class CertificatePemIdentityExtractorTest {
|
|||
X509Certificate x509Certificate = PemUtils.decodeCertificate(StreamUtil.readString(is, Charset.defaultCharset()));
|
||||
String certificatePem = PemUtils.encodeCertificate(x509Certificate);
|
||||
|
||||
X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel();
|
||||
UserIdentityExtractor extractor = UserIdentityExtractor.getCertificatePemIdentityExtractor(config);
|
||||
//X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel();
|
||||
UserIdentityExtractor extractor = CryptoIntegration.getProvider().getIdentityExtractorProvider().getCertificatePemIdentityExtractor();
|
||||
|
||||
String userIdentity = (String) extractor.extractUserIdentity(new X509Certificate[]{x509Certificate});
|
||||
|
||||
|
|
|
@ -1,35 +1,19 @@
|
|||
package org.keycloak.authentication.authenticators.x509;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x509.CertificatePolicies;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.PolicyInformation;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.CertIOException;
|
||||
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.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.rule.CryptoInitRule;
|
||||
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_POLICY_MODE_ALL;
|
||||
import static org.keycloak.authentication.authenticators.x509.AbstractX509ClientCertificateAuthenticator.CERTIFICATE_POLICY_MODE_ANY;
|
||||
|
@ -42,19 +26,20 @@ import static org.keycloak.authentication.authenticators.x509.AbstractX509Client
|
|||
*/
|
||||
public class CertificateValidatorTest {
|
||||
|
||||
private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
|
||||
@ClassRule
|
||||
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
|
||||
|
||||
/**
|
||||
* will validate that the certificate validation succeeds if the certificate is currently valid
|
||||
*/
|
||||
@Test
|
||||
public void testValidityOfCertificatesSuccess() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
kpg.initialize(512);
|
||||
KeyPair keyPair = kpg.generateKeyPair();
|
||||
X509Certificate certificate =
|
||||
createCertificate("CN=keycloak-test", new Date(),
|
||||
new Date(System.currentTimeMillis() + 1000L * 60), keyPair, null);
|
||||
X509Certificate certificate = CryptoIntegration.getProvider().getCertificateUtils()
|
||||
.createServicesTestCertificate("CN=keycloak-test", new Date(),
|
||||
new Date(System.currentTimeMillis() + 1000L * 60), keyPair);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
|
@ -75,12 +60,12 @@ public class CertificateValidatorTest {
|
|||
*/
|
||||
@Test
|
||||
public void testValidityOfCertificatesNotValidYet() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
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, null);
|
||||
X509Certificate certificate = CryptoIntegration.getProvider().getCertificateUtils()
|
||||
.createServicesTestCertificate("CN=keycloak-test", new Date(System.currentTimeMillis() + 1000L * 60),
|
||||
new Date(System.currentTimeMillis() + 1000L * 60), keyPair);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
|
@ -102,12 +87,13 @@ public class CertificateValidatorTest {
|
|||
*/
|
||||
@Test
|
||||
public void testValidityOfCertificatesHasExpired() throws GeneralSecurityException {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
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, null);
|
||||
X509Certificate certificate = CryptoIntegration.getProvider().getCertificateUtils()
|
||||
.createServicesTestCertificate("CN=keycloak-test",
|
||||
new Date(System.currentTimeMillis() - 1000L * 60 * 2),
|
||||
new Date(System.currentTimeMillis() - 1000L * 60), keyPair);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
|
@ -157,7 +143,7 @@ public class CertificateValidatorTest {
|
|||
*/
|
||||
@Test(expected = GeneralSecurityException.class)
|
||||
public void testCertificatePolicyModeAllOneRequestedAndNotPresent() throws GeneralSecurityException {
|
||||
testCertificatePolicyValidation("1.3.76.16.2.1", CERTIFICATE_POLICY_MODE_ALL, null);
|
||||
testCertificatePolicyValidation("1.3.76.16.2.1", CERTIFICATE_POLICY_MODE_ALL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -265,7 +251,7 @@ public class CertificateValidatorTest {
|
|||
*/
|
||||
@Test(expected = GeneralSecurityException.class)
|
||||
public void testCertificatePolicyModeAnyOneRequestedAndNotPresent() throws GeneralSecurityException {
|
||||
testCertificatePolicyValidation("1.3.76.16.2.1", CERTIFICATE_POLICY_MODE_ANY, null);
|
||||
testCertificatePolicyValidation("1.3.76.16.2.1", CERTIFICATE_POLICY_MODE_ANY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -344,35 +330,15 @@ public class CertificateValidatorTest {
|
|||
private void testCertificatePolicyValidation(String expectedPolicy, String mode, String... certificatePolicyOid)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
List<Extension> certificatePolicies = null;
|
||||
|
||||
if (certificatePolicyOid != null && certificatePolicyOid.length > 0)
|
||||
{
|
||||
certificatePolicies = new LinkedList<>();
|
||||
|
||||
List<PolicyInformation> policyInfoList = new LinkedList<>();
|
||||
for (String oid: certificatePolicyOid)
|
||||
{
|
||||
policyInfoList.add(new PolicyInformation(new ASN1ObjectIdentifier(oid)));
|
||||
}
|
||||
|
||||
CertificatePolicies policies = new CertificatePolicies(policyInfoList.toArray(new PolicyInformation[0]));
|
||||
|
||||
try {
|
||||
boolean isCritical = false;
|
||||
Extension extension = new Extension(Extension.certificatePolicies, isCritical, policies.getEncoded());
|
||||
certificatePolicies.add(extension);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
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, certificatePolicies);
|
||||
X509Certificate certificate = CryptoIntegration.getProvider().getCertificateUtils()
|
||||
.createServicesTestCertificate("CN=keycloak-test",
|
||||
new Date(System.currentTimeMillis() - 1000L * 60 * 2),
|
||||
new Date(System.currentTimeMillis() - 1000L * 60), keyPair, certificatePolicyOid);
|
||||
|
||||
CertificateValidator.CertificateValidatorBuilder builder =
|
||||
new CertificateValidator.CertificateValidatorBuilder();
|
||||
|
@ -385,57 +351,4 @@ public class CertificateValidatorTest {
|
|||
validator.validatePolicy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param extensions optional list of extensions to include in the certificate
|
||||
* @return a X509-Certificate in version 3
|
||||
*/
|
||||
public X509Certificate createCertificate(String dn,
|
||||
Date startDate,
|
||||
Date expiryDate,
|
||||
KeyPair keyPair,
|
||||
List<Extension> extensions) {
|
||||
// Cert data
|
||||
X500Name subjectDN = new X500Name(dn);
|
||||
X500Name issuerDN = new X500Name(dn);
|
||||
|
||||
SubjectPublicKeyInfo subjPubKeyInfo = SubjectPublicKeyInfo.getInstance(
|
||||
ASN1Sequence.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
BigInteger serialNumber = new BigInteger(130, new SecureRandom());
|
||||
|
||||
// Build the certificate
|
||||
X509v3CertificateBuilder certGen = new X509v3CertificateBuilder(issuerDN, serialNumber, startDate, expiryDate,
|
||||
subjectDN, subjPubKeyInfo);
|
||||
|
||||
if (extensions != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (Extension certExtension: extensions)
|
||||
certGen.addExtension(certExtension);
|
||||
} catch (CertIOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sign the cert with the private key
|
||||
try {
|
||||
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
|
||||
.setProvider(BOUNCY_CASTLE_PROVIDER)
|
||||
.build(keyPair.getPrivate());
|
||||
X509Certificate x509Certificate = new JcaX509CertificateConverter()
|
||||
.setProvider(BOUNCY_CASTLE_PROVIDER)
|
||||
.getCertificate(certGen.build(contentSigner));
|
||||
|
||||
return x509Certificate;
|
||||
} catch (CertificateException | OperatorCreationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import java.security.cert.X509Certificate;
|
|||
import org.junit.Assert;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.crypto.CryptoIntegration;
|
||||
import org.keycloak.common.crypto.UserIdentityExtractor;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.rule.CryptoInitRule;
|
||||
|
@ -37,7 +39,7 @@ public class SubjectAltNameIdentityExtractorTest {
|
|||
|
||||
@Test
|
||||
public void testX509SubjectAltName_otherName() throws Exception {
|
||||
UserIdentityExtractor extractor = UserIdentityExtractor.getSubjectAltNameExtractor(0);
|
||||
UserIdentityExtractor extractor = CryptoIntegration.getProvider().getIdentityExtractorProvider().getSubjectAltNameExtractor(0);
|
||||
|
||||
X509Certificate cert = getCertificate();
|
||||
|
||||
|
@ -48,7 +50,7 @@ public class SubjectAltNameIdentityExtractorTest {
|
|||
|
||||
@Test
|
||||
public void testX509SubjectAltName_email() throws Exception {
|
||||
UserIdentityExtractor extractor = UserIdentityExtractor.getSubjectAltNameExtractor(1);
|
||||
UserIdentityExtractor extractor = CryptoIntegration.getProvider().getIdentityExtractorProvider().getSubjectAltNameExtractor(1);
|
||||
|
||||
X509Certificate cert = getCertificate();
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ import java.net.URL;
|
|||
import java.nio.charset.Charset;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
|
@ -31,10 +30,10 @@ import javax.ws.rs.core.Response;
|
|||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.BouncyIntegration;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider;
|
||||
|
@ -48,18 +47,14 @@ public class DockerComposeYamlInstallationProviderTest {
|
|||
DockerComposeYamlInstallationProvider installationProvider;
|
||||
static Certificate certificate;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUp_beforeClass() throws NoSuchAlgorithmException {
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
final KeyPairGenerator keyGen;
|
||||
keyGen = KeyPairGenerator.getInstance("RSA");
|
||||
keyGen = KeyPairGenerator.getInstance("RSA", BouncyIntegration.PROVIDER);
|
||||
keyGen.initialize(2048, new SecureRandom());
|
||||
|
||||
final KeyPair keypair = keyGen.generateKeyPair();
|
||||
certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, "test-realm");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
installationProvider = new DockerComposeYamlInstallationProvider();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue