From ce1331f550d2c1e00e17562f86e8cd583f841632 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Mon, 22 Aug 2022 08:43:59 -0500 Subject: [PATCH] Remove bouncycastle dependency from keycloak-services (#13489) Closes #12857 Co-authored-by: mposolda --- .../adapters/KeycloakDeploymentBuilder.java | 1 - .../crypto/CertificateUtilsProvider.java | 16 + .../common/crypto/CryptoConstants.java | 3 + .../common/crypto/CryptoProvider.java | 7 + .../common/crypto/ECDSACryptoProvider.java | 13 + .../common/crypto/UserIdentityExtractor.java | 45 ++ .../crypto/UserIdentityExtractorProvider.java | 147 ++++++ crypto/default/pom.xml | 14 +- .../def/BCCertificateUtilsProvider.java | 154 +++++- .../crypto/def/BCECDSACryptoProvider.java | 64 +++ .../keycloak/crypto/def/BCOCSPProvider.java | 212 +++------ .../def/BCUserIdentityExtractorProvider.java | 138 +----- .../crypto/def/DefaultCryptoProvider.java | 18 + crypto/fips1402/pom.xml | 13 + .../fips/BCFIPSCertificateUtilsProvider.java | 153 ++++++ .../fips/BCFIPSECDSACryptoProvider.java | 64 +++ .../crypto/fips/BCFIPSOCSPProvider.java | 449 ++++++++++++++++++ .../BCFIPSUserIdentityExtractorProvider.java | 201 ++++++++ .../crypto/fips/FIPS1402Provider.java | 18 + crypto/pom.xml | 2 +- .../keycloak-crypto-default/main/module.xml | 3 + .../keycloak-model-jpa/main/module.xml | 1 - .../main/module.xml | 1 - .../keycloak-services/main/module.xml | 1 - .../keycloak-crypto-default/main/module.xml | 3 + .../keycloak-model-jpa/main/module.xml | 1 - .../main/module.xml | 1 - .../keycloak-services/main/module.xml | 1 - model/jpa/pom.xml | 4 - model/legacy-private/pom.xml | 4 - model/legacy/pom.xml | 4 - model/map/pom.xml | 4 - server-spi-private/pom.xml | 2 +- .../java/org/keycloak/utils/OCSPProvider.java | 213 +++++++++ services/pom.xml | 8 - ...actX509ClientCertificateAuthenticator.java | 60 +-- .../x509/CertificateValidator.java | 47 +- .../crypto/ECDSASignatureProvider.java | 48 +- .../compose/DockerComposeCertsDirectory.java | 9 +- .../java/org/keycloak/utils/CRLUtils.java | 56 --- .../CertificatePemIdentityExtractorTest.java | 6 +- .../x509/CertificateValidatorTest.java | 141 ++---- .../SubjectAltNameIdentityExtractorTest.java | 6 +- ...erComposeYamlInstallationProviderTest.java | 15 +- 44 files changed, 1765 insertions(+), 606 deletions(-) create mode 100644 common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java create mode 100644 common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractor.java create mode 100644 common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractorProvider.java create mode 100644 crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java rename services/src/main/java/org/keycloak/authentication/authenticators/x509/OCSPUtils.java => crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java (71%) rename services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java => crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java (57%) create mode 100644 crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java create mode 100644 crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java create mode 100644 crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index c83c0f073f..827dda9d77 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -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; diff --git a/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java b/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java index 4163baa466..5000d64f61 100755 --- a/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/CertificateUtilsProvider.java @@ -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}. * @@ -56,5 +62,15 @@ public interface CertificateUtilsProvider { public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject); public X509Certificate generateV1SelfSignedCertificate(KeyPair caKeyPair, String subject, BigInteger serialNumber); + + public List getCertificatePolicyList(X509Certificate cert) throws GeneralSecurityException; + + public List getCRLDistributionPoints(X509Certificate cert) throws IOException; + + public X509Certificate createServicesTestCertificate(String dn, + Date startDate, + Date expiryDate, + KeyPair keyPair, + String... certificatePolicyOid); } diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java index a8d55144d1..7e4cd54f5d 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoConstants.java @@ -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"; diff --git a/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java b/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java index bffb7eaa7f..2609e5ff35 100644 --- a/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java +++ b/common/src/main/java/org/keycloak/common/crypto/CryptoProvider.java @@ -42,6 +42,13 @@ public interface CryptoProvider { */ PemUtilsProvider getPemUtils(); + T getOCSPProver(Class clazz); + + + public UserIdentityExtractorProvider getIdentityExtractorProvider(); + + public ECDSACryptoProvider getEcdsaCryptoProvider(); + /** * Create the param spec for the EC curve diff --git a/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java b/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java new file mode 100644 index 0000000000..4a63c0b385 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/crypto/ECDSACryptoProvider.java @@ -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; + + +} diff --git a/common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractor.java b/common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractor.java new file mode 100644 index 0000000000..dbb307d6c9 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractor.java @@ -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 Peter Nalyvayko + * @version $Revision: 1 $ + * @date 7/30/2016 + */ + +public interface UserIdentityExtractor { + + + public Object extractUserIdentity(X509Certificate[] certs); + +} diff --git a/common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractorProvider.java b/common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractorProvider.java new file mode 100644 index 0000000000..c393c3fbd5 --- /dev/null +++ b/common/src/main/java/org/keycloak/common/crypto/UserIdentityExtractorProvider.java @@ -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 Peter Nalyvayko + * @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 _f; + PatternMatcher(String pattern, Function 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 valueToMatch) { + return new PatternMatcher(pattern, valueToMatch); + } + + public abstract UserIdentityExtractor getX500NameExtractor(String identifier, Function x500Name); + + /** + * Obtains the subjectAltName given a generalName. + * + * @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); +} diff --git a/crypto/default/pom.xml b/crypto/default/pom.xml index 217bad4cdb..fbcb65d813 100644 --- a/crypto/default/pom.xml +++ b/crypto/default/pom.xml @@ -41,7 +41,19 @@ test test-jar - + + org.keycloak + keycloak-server-spi + + + org.keycloak + keycloak-server-spi-private + + + org.apache.httpcomponents + httpclient + provided + org.bouncycastle bcprov-jdk15on diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java index dc243cec0b..e7aa605f86 100755 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCCertificateUtilsProvider.java @@ -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 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 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 CRL validation + * @param cert + * @return + * @throws IOException + */ + public List getCRLDistributionPoints(X509Certificate cert) throws IOException { + byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); + if (data == null) { + return Collections.emptyList(); + } + + List 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 certPolicyExtensions(String... certificatePolicyOid) { + List certificatePolicies = new LinkedList<>(); + + if (certificatePolicyOid != null && certificatePolicyOid.length > 0) + { + List 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; + } + } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java new file mode 100644 index 0000000000..9cf773175b --- /dev/null +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCECDSACryptoProvider.java @@ -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; + } + + +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/OCSPUtils.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java similarity index 71% rename from services/src/main/java/org/keycloak/authentication/authenticators/x509/OCSPUtils.java rename to crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java index d4106e005f..114d220546 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/OCSPUtils.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCOCSPProvider.java @@ -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 Peter Nalyvayko @@ -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 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 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 responderURIs, X509Certificate responderCert, Date date) throws CertPathValidatorException { + @Override + protected OCSPRevocationStatus check(KeycloakSession session, X509Certificate cert, X509Certificate issuerCertificate, List 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 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 getResponderURIs(X509Certificate cert) throws CertificateEncodingException { + @Override + protected List getResponderURIs(X509Certificate cert) throws CertificateEncodingException { LinkedList responderURIs = new LinkedList<>(); JcaX509CertificateHolder holder = new JcaX509CertificateHolder(cert); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java b/crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java similarity index 57% rename from services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java rename to crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java index 1362b6b5a4..f0ce43b848 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/UserIdentityExtractor.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/BCUserIdentityExtractorProvider.java @@ -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 Peter Nalyvayko @@ -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 x500Name; - X500NameRDNExtractor(ASN1ObjectIdentifier x500NameStyle, Function x500Name) { - this.x500NameStyle = x500NameStyle; + Function x500Name; + + public X500NameRDNExtractorBCProvider(String attrName, Function 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 _f; - PatternMatcher(String pattern, Function 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 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 func) { - return new PatternMatcher(pattern, func); - } - - public static UserIdentityExtractor getX500NameExtractor(ASN1ObjectIdentifier identifier, Function x500Name) { - return new X500NameRDNExtractor(identifier, x500Name); - } - - /** - * Obtains the subjectAltName given a generalName. - * - * @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; - } - }; - } } diff --git a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java index 57b46910c4..a937c8d7a4 100644 --- a/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java +++ b/crypto/default/src/main/java/org/keycloak/crypto/def/DefaultCryptoProvider.java @@ -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 Marek Posolda @@ -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 getOCSPProver(Class clazz) { + return clazz.cast(new BCOCSPProvider()); + } + } diff --git a/crypto/fips1402/pom.xml b/crypto/fips1402/pom.xml index 386e9c897a..30e91ca31e 100644 --- a/crypto/fips1402/pom.xml +++ b/crypto/fips1402/pom.xml @@ -41,6 +41,19 @@ test test-jar + + org.keycloak + keycloak-server-spi + + + org.keycloak + keycloak-server-spi-private + + + org.apache.httpcomponents + httpclient + provided + org.bouncycastle diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java index a1b11b0d20..0f505d9218 100755 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSCertificateUtilsProvider.java @@ -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 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 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 CRL validation + * @param cert + * @return + * @throws IOException + */ + public List getCRLDistributionPoints(X509Certificate cert) throws IOException { + byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); + if (data == null) { + return Collections.emptyList(); + } + + List 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 certPolicyExtensions(String... certificatePolicyOid) { + List certificatePolicies = new LinkedList<>(); + + if (certificatePolicyOid != null && certificatePolicyOid.length > 0) + { + List 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; + } + } diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java new file mode 100644 index 0000000000..1b6dfd35b7 --- /dev/null +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSECDSACryptoProvider.java @@ -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; + } + + +} diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java new file mode 100644 index 0000000000..37f8d26a14 --- /dev/null +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSOCSPProvider.java @@ -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 Peter Nalyvayko + * @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 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 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 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 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 getResponderURIs(X509Certificate cert) throws CertificateEncodingException { + + LinkedList 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; + } +} diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java new file mode 100644 index 0000000000..69c40369be --- /dev/null +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/BCFIPSUserIdentityExtractorProvider.java @@ -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 Peter Nalyvayko + * @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 x500Name; + + public X500NameRDNExtractorBCProvider(String attrName, Function 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> subjectAlternativeNames = certs[0].getSubjectAlternativeNames(); + + if (subjectAlternativeNames == null) { + return null; + } + + Iterator> 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 x500Name) { + return new X500NameRDNExtractorBCProvider(identifier, x500Name); + } + + @Override + public SubjectAltNameExtractor getSubjectAltNameExtractor(int generalName) { + return new SubjectAltNameExtractorBCProvider(generalName); + } + +} diff --git a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java index 2282c8ac2d..1dd04127aa 100644 --- a/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java +++ b/crypto/fips1402/src/main/java/org/keycloak/crypto/fips/FIPS1402Provider.java @@ -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 getOCSPProver(Class clazz) { + return clazz.cast(new BCFIPSOCSPProvider()); + } } diff --git a/crypto/pom.xml b/crypto/pom.xml index 077717570e..ab4f21bc10 100644 --- a/crypto/pom.xml +++ b/crypto/pom.xml @@ -31,7 +31,7 @@ pom - fips1402 default + fips1402 \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml index 8cbcac41aa..d9962f6b12 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml @@ -27,8 +27,11 @@ + + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml index 31b4564344..dec63a67de 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml @@ -37,7 +37,6 @@ - diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml index ffb6b94fd5..ff949884d8 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml @@ -60,7 +60,6 @@ - diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml index 5a59a5c2c6..272d65a5b6 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml @@ -66,7 +66,6 @@ - diff --git a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml index 8cbcac41aa..d9962f6b12 100644 --- a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml +++ b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-crypto-default/main/module.xml @@ -27,8 +27,11 @@ + + + diff --git a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml index 31b4564344..dec63a67de 100755 --- a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml +++ b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml @@ -37,7 +37,6 @@ - diff --git a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml index ffb6b94fd5..ff949884d8 100755 --- a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml +++ b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-legacy-services/main/module.xml @@ -60,7 +60,6 @@ - diff --git a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml index af8e448073..1cb0a2a4e9 100755 --- a/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml +++ b/distribution/galleon-feature-packs/server-galleon-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml @@ -74,7 +74,6 @@ - diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index 8090e84d3e..dd35a78442 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -42,10 +42,6 @@ - - org.bouncycastle - bcprov-jdk15on - org.keycloak keycloak-core diff --git a/model/legacy-private/pom.xml b/model/legacy-private/pom.xml index 17a51eb061..69db388d27 100644 --- a/model/legacy-private/pom.xml +++ b/model/legacy-private/pom.xml @@ -12,10 +12,6 @@ - - org.bouncycastle - bcprov-jdk15on - org.keycloak keycloak-core diff --git a/model/legacy/pom.xml b/model/legacy/pom.xml index 914b199459..1fadf7eccb 100644 --- a/model/legacy/pom.xml +++ b/model/legacy/pom.xml @@ -12,10 +12,6 @@ - - org.bouncycastle - bcprov-jdk15on - org.keycloak keycloak-core diff --git a/model/map/pom.xml b/model/map/pom.xml index cabf124c13..eb7bcb5687 100644 --- a/model/map/pom.xml +++ b/model/map/pom.xml @@ -44,10 +44,6 @@ - - org.bouncycastle - bcprov-jdk15on - org.keycloak keycloak-core diff --git a/server-spi-private/pom.xml b/server-spi-private/pom.xml index 6e6af97184..5d927bdde4 100755 --- a/server-spi-private/pom.xml +++ b/server-spi-private/pom.xml @@ -59,7 +59,7 @@ org.apache.httpcomponents httpclient - provided + com.google.guava diff --git a/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java new file mode 100644 index 0000000000..09a0a7588c --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/utils/OCSPProvider.java @@ -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 Peter Nalyvayko + * @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 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 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 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 getResponderURIs(X509Certificate cert) throws CertificateEncodingException; + + +} diff --git a/services/pom.xml b/services/pom.xml index 01a2e2a0d8..ab20473358 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -35,14 +35,6 @@ - - org.bouncycastle - bcprov-jdk15on - - - org.bouncycastle - bcpkix-jdk15on - org.keycloak keycloak-core diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java index 09c0fca901..50383bc796 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/AbstractX509ClientCertificateAuthenticator.java @@ -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 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 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 subject = certs -> { + return certs[0].getSubjectX500Principal(); }; private static Function 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 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()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java index 9a7086334a..ae8788508a 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/CertificateValidator.java @@ -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 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 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 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 policyList = new LinkedList<>(); - Arrays.stream(policies.getPolicyInformation()).forEach(p -> policyList.add(p.getPolicyIdentifier().toString().toLowerCase())); + List 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 getCRLDistributionPoints(X509Certificate cert) { try { - return CRLUtils.getCRLDistributionPoints(cert); + return CryptoIntegration.getProvider().getCertificateUtils().getCRLDistributionPoints(cert); } catch(IOException e) { logger.error(e.getMessage()); diff --git a/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java b/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java index d26500aa59..134dc2b58e 100644 --- a/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java +++ b/services/src/main/java/org/keycloak/crypto/ECDSASignatureProvider.java @@ -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 { diff --git a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java index 9d607f4be5..01ffa9516a 100644 --- a/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java +++ b/services/src/main/java/org/keycloak/protocol/docker/installation/compose/DockerComposeCertsDirectory.java @@ -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); } diff --git a/services/src/main/java/org/keycloak/utils/CRLUtils.java b/services/src/main/java/org/keycloak/utils/CRLUtils.java index 96b0bada27..cded47fc3a 100644 --- a/services/src/main/java/org/keycloak/utils/CRLUtils.java +++ b/services/src/main/java/org/keycloak/utils/CRLUtils.java @@ -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 CRL validation - * @param cert - * @return - * @throws IOException - */ - public static List getCRLDistributionPoints(X509Certificate cert) throws IOException { - byte[] data = cert.getExtensionValue(CRL_DISTRIBUTION_POINTS_OID); - if (data == null) { - return Collections.emptyList(); - } - - List 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. diff --git a/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificatePemIdentityExtractorTest.java b/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificatePemIdentityExtractorTest.java index b0cab146a2..d4b12b20cd 100644 --- a/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificatePemIdentityExtractorTest.java +++ b/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificatePemIdentityExtractorTest.java @@ -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}); diff --git a/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificateValidatorTest.java b/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificateValidatorTest.java index 1717cbafbc..80b1c0ef40 100644 --- a/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificateValidatorTest.java +++ b/services/src/test/java/org/keycloak/authentication/authenticators/x509/CertificateValidatorTest.java @@ -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 certificatePolicies = null; + - if (certificatePolicyOid != null && certificatePolicyOid.length > 0) - { - certificatePolicies = new LinkedList<>(); - - List 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 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); - } - } } diff --git a/services/src/test/java/org/keycloak/authentication/authenticators/x509/SubjectAltNameIdentityExtractorTest.java b/services/src/test/java/org/keycloak/authentication/authenticators/x509/SubjectAltNameIdentityExtractorTest.java index 8052d3b16d..e31e4e47c8 100644 --- a/services/src/test/java/org/keycloak/authentication/authenticators/x509/SubjectAltNameIdentityExtractorTest.java +++ b/services/src/test/java/org/keycloak/authentication/authenticators/x509/SubjectAltNameIdentityExtractorTest.java @@ -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(); diff --git a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java index d007be5dbc..d704a927f1 100644 --- a/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java +++ b/services/src/test/java/org/keycloak/procotol/docker/installation/DockerComposeYamlInstallationProviderTest.java @@ -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; @@ -47,19 +46,15 @@ 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(); }