/* * Copyright (C) 2005-2008 Jive Software. All rights reserved. * * 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.jivesoftware.util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchProviderException; import java.security.Principal; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.cert.CertPath; import java.security.cert.CertPathBuilder; import java.security.cert.CertPathBuilderException; import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidatorException; import java.security.cert.CertStore; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; import java.security.cert.PKIXBuilderParameters; import java.security.cert.X509CertSelector; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bouncycastle.asn1.*; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.cert.CertException; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.openssl.MiscPEMGenerator; import org.bouncycastle.openssl.PEMDecryptorProvider; import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.ContentVerifierProvider; import org.bouncycastle.operator.InputDecryptorProvider; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCSException; import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; import org.bouncycastle.util.io.pem.PemObjectGenerator; import org.bouncycastle.util.io.pem.PemWriter; import org.jivesoftware.openfire.keystore.CertificateStore; import org.jivesoftware.openfire.keystore.CertificateUtils; import org.jivesoftware.util.cert.CNCertificateIdentityMapping; import org.jivesoftware.util.cert.CertificateIdentityMapping; import org.jivesoftware.util.cert.SANCertificateIdentityMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Utility class that provides similar functionality to the keytool tool. Generated certificates * conform to the XMPP spec where domains are kept in the subject alternative names extension. * * @author Gaston Dombiak */ public class CertificateManager { private static final Logger Log = LoggerFactory.getLogger( CertificateManager.class ); private static Pattern valuesPattern = Pattern.compile("(?i)(=)([^,]*)"); private static List<CertificateEventListener> listeners = new CopyOnWriteArrayList<>(); private static List<CertificateIdentityMapping> serverCertMapping = new ArrayList<>(); private static List<CertificateIdentityMapping> clientCertMapping = new ArrayList<>(); static { String serverCertIdentityMapList = JiveGlobals.getProperty("provider.serverCertIdentityMap.classList"); if (serverCertIdentityMapList != null) { StringTokenizer st = new StringTokenizer(serverCertIdentityMapList, " ,\t\n\r\f"); while (st.hasMoreTokens()) { String s_provider = st.nextToken(); try { Class<?> c_provider = ClassUtils.forName(s_provider); CertificateIdentityMapping provider = (CertificateIdentityMapping)(c_provider.newInstance()); Log.debug("CertificateManager: Loaded server identity mapping " + s_provider); serverCertMapping.add(provider); } catch (Exception e) { Log.error("CertificateManager: Error loading CertificateIdentityMapping: " + s_provider + "\n" + e); } } } if (serverCertMapping.isEmpty()) { Log.debug("CertificateManager: No server CertificateIdentityMapping's found. Loading default mappings"); serverCertMapping.add(new SANCertificateIdentityMapping()); serverCertMapping.add(new CNCertificateIdentityMapping()); } String clientCertMapList = JiveGlobals.getProperty("provider.clientCertIdentityMap.classList"); if (clientCertMapList != null) { StringTokenizer st = new StringTokenizer(clientCertMapList, " ,\t\n\r\f"); while (st.hasMoreTokens()) { String s_provider = st.nextToken(); try { Class<?> c_provider = ClassUtils.forName(s_provider); CertificateIdentityMapping provider = (CertificateIdentityMapping)(c_provider.newInstance()); Log.debug("CertificateManager: Loaded client identity mapping " + s_provider); clientCertMapping.add(provider); } catch (Exception e) { Log.error("CertificateManager: Error loading CertificateIdentityMapping: " + s_provider + "\n" + e); } } } if (clientCertMapping.isEmpty()) { Log.debug("CertificateManager: No client CertificateIdentityMapping's found. Loading default mappings"); clientCertMapping.add(new CNCertificateIdentityMapping()); } } /** * Decide whether or not to trust the given supplied certificate chain, returning the * End Entity Certificate in this case where it can, and null otherwise. * A self-signed certificate will, for example, return null. * For certain failures, we SHOULD generate an exception - revocations and the like, * but we currently do not. * * @param chain an array of X509Certificate where the first one is the endEntityCertificate. * @param certStore a keystore containing untrusted certificates (including ICAs, etc). * @param trustStore a keystore containing Trust Anchors (most-trusted CA certificates). * @return trusted end-entity certificate, or null. */ public static X509Certificate getEndEntityCertificate(Certificate chain[], KeyStore certStore, KeyStore trustStore) { if (chain == null || chain.length == 0) { return null; } X509Certificate first = (X509Certificate) chain[0]; try { first.checkValidity(); } catch(CertificateException e) { Log.warn("EE Certificate not valid: " + e.getMessage()); return null; } if (chain.length == 1 && first.getSubjectX500Principal().equals(first.getIssuerX500Principal())) { // Chain is single cert, and self-signed. try { if (trustStore.getCertificateAlias(first) != null) { // Interesting case: trusted self-signed cert. return first; } } catch (KeyStoreException e) { Log.warn("Keystore error while looking for self-signed cert; assuming untrusted."); } return null; } final List<Certificate> all_certs = new ArrayList<>(); try { // First, load up certStore contents into a CertStore. // It's a mystery why these objects are different. for (Enumeration<String> aliases = certStore.aliases(); aliases .hasMoreElements();) { String alias = aliases.nextElement(); if (certStore.isCertificateEntry(alias)) { X509Certificate cert = (X509Certificate) certStore .getCertificate(alias); all_certs.add(cert); } } // Now add the trusted certs. for (Enumeration<String> aliases = trustStore.aliases(); aliases .hasMoreElements();) { String alias = aliases.nextElement(); if (trustStore.isCertificateEntry(alias)) { X509Certificate cert = (X509Certificate) trustStore .getCertificate(alias); all_certs.add(cert); } } // Finally, add all the certs in the chain: for (int i = 0; i < chain.length; ++i) { all_certs.add(chain[i]); } CertStore cs = CertStore.getInstance("Collection", new CollectionCertStoreParameters(all_certs)); X509CertSelector selector = new X509CertSelector(); selector.setCertificate(first); // / selector.setSubject(first.getSubjectX500Principal()); PKIXBuilderParameters params = new PKIXBuilderParameters( trustStore, selector); params.addCertStore(cs); params.setDate(new Date()); params.setRevocationEnabled(false); /* Code here is the right way to do things. */ CertPathBuilder pathBuilder = CertPathBuilder .getInstance(CertPathBuilder.getDefaultType()); CertPath cp = pathBuilder.build(params).getCertPath(); /* * This section is an alternative to using CertPathBuilder which is * not as complete (or safe), but will emit much better errors. If * things break, swap around the code. * **** COMMENTED OUT. **** ArrayList<X509Certificate> ls = new ArrayList<X509Certificate>(); for (int i = 0; i < chain.length; ++i) { ls.add((X509Certificate) chain[i]); } for (X509Certificate last = ls.get(ls.size() - 1); !last .getIssuerX500Principal().equals(last.getSubjectX500Principal()); last = ls .get(ls.size() - 1)) { X509CertSelector sel = new X509CertSelector(); sel.setSubject(last.getIssuerX500Principal()); ls.add((X509Certificate) cs.getCertificates(sel).toArray()[0]); } CertPath cp = CertificateFactory.getInstance("X.509").generateCertPath(ls); ****** END ALTERNATIVE. **** */ // Not entirely sure if I need to do this with CertPathBuilder. // Can't hurt. CertPathValidator pathValidator = CertPathValidator .getInstance("PKIX"); pathValidator.validate(cp, params); return (X509Certificate) cp.getCertificates().get(0); } catch (CertPathBuilderException e) { Log.warn("Path builder: " + e.getMessage()); } catch (CertPathValidatorException e) { Log.warn("Path validator: " + e.getMessage()); } catch (Exception e) { Log.warn("Unkown exception while validating certificate chain: " + e.getMessage()); } return null; } /** * Returns the identities of the remote client as defined in the specified certificate. The * identities are mapped by the classes in the "provider.clientCertIdentityMap.classList" property. * By default, the subjectDN of the certificate is used. * * @param x509Certificate the certificate the holds the identities of the remote server. * @return the identities of the remote client as defined in the specified certificate. */ public static List<String> getClientIdentities(X509Certificate x509Certificate) { List<String> names = new ArrayList<>(); for (CertificateIdentityMapping mapping : clientCertMapping) { List<String> identities = mapping.mapIdentity(x509Certificate); Log.debug("CertificateManager: " + mapping.name() + " returned " + identities.toString()); if (!identities.isEmpty()) { names.addAll(identities); break; } } return names; } /** * Returns the identities of the remote server as defined in the specified certificate. The * identities are mapped by the classes in the "provider.serverCertIdentityMap.classList" property. * By default, the identities are defined in the subjectDN of the certificate and it can also be * defined in the subjectAltName extensions of type "xmpp". When the extension is being used then the * identities defined in the extension are going to be returned. Otherwise, the value stored in * the subjectDN is returned. * * @param x509Certificate the certificate the holds the identities of the remote server. * @return the identities of the remote server as defined in the specified certificate. */ public static List<String> getServerIdentities(X509Certificate x509Certificate) { List<String> names = new ArrayList<>(); for (CertificateIdentityMapping mapping : serverCertMapping) { List<String> identities = mapping.mapIdentity(x509Certificate); Log.debug("CertificateManager: " + mapping.name() + " returned " + identities.toString()); if (!identities.isEmpty()) { names.addAll(identities); break; } } return names; } /** * Returns true if an RSA certificate was found in the specified keystore for the specified domain. * * @param storeConfig the store to use for searching the certificate. * @param domain domain of the server signed by the certificate. * @return true if an RSA certificate was found in the specified keystore for the specified domain. * @throws KeyStoreException */ public static boolean isRSACertificate(CertificateStore storeConfig, String domain) throws KeyStoreException { return isCertificate(storeConfig, domain, "RSA"); } /** * Returns true if an DSA certificate was found in the specified keystore for the specified domain. * * @param storeConfig the store to use for searching the certificate. * @param domain domain of the server signed by the certificate. * @return true if an DSA certificate was found in the specified keystore for the specified domain. * @throws KeyStoreException */ public static boolean isDSACertificate(CertificateStore storeConfig, String domain) throws KeyStoreException { return isCertificate( storeConfig, domain, "DSA" ); } /** * Returns true if the specified certificate is using the DSA algorithm. The DSA algorithm is not * good for encryption but only for authentication. On the other hand, the RSA algorithm is good * for encryption and authentication. * * @param certificate the certificate to analyze. * @return true if the specified certificate is using the DSA algorithm. * @throws KeyStoreException */ public static boolean isDSACertificate(X509Certificate certificate) throws KeyStoreException { return certificate.getPublicKey().getAlgorithm().equals( "DSA" ); } /** * Returns true if a certificate with the specified configuration was found in a certificate store. * * @param storeConfig the store to use for searching the certificate. * @param domain the domain present in the subjectAltName or "*" if anything is accepted. * @param algorithm the DSA or RSA algorithm used by the certificate. * @return true if a certificate with the specified configuration was found in the key store. * @throws KeyStoreException */ private static boolean isCertificate(CertificateStore storeConfig, String domain, String algorithm) throws KeyStoreException { for (Enumeration<String> aliases = storeConfig.getStore().aliases(); aliases.hasMoreElements();) { X509Certificate certificate = (X509Certificate) storeConfig.getStore().getCertificate(aliases.nextElement()); if ( !certificate.getPublicKey().getAlgorithm().equalsIgnoreCase( algorithm ) ) { continue; } if ("*".equals(domain)) { // Any domain certified by the certificate is accepted return true; } else { // Only accept certified domains that match the specified domain // TODO check that domain=foo.bar does not match identitiy "a.longerfoo.bar" for (String identity : getServerIdentities( certificate ) ) { if (identity.endsWith(domain) ) { return true; } } } } return false; } /** * Returns true if the specified certificate is a self-signed certificate. * * @return true if the specified certificate is a self-signed certificate. * @throws KeyStoreException if an error happens while usign the keystore */ public static boolean isSelfSignedCertificate(X509Certificate certificate) throws KeyStoreException { try { certificate.verify(certificate.getPublicKey()); return true; } catch (GeneralSecurityException e) { return false; } } /** * Returns true if the specified certificate is ready to be signed by a Certificate Authority. Self-signed * certificates need to get their issuer information entered to be able to generate a Certificate * Signing Request (CSR). * * @return true if the specified certificate is ready to be signed by a Certificate Authority. * @throws KeyStoreException if an error happens while usign the keystore */ public static boolean isSigningRequestPending(X509Certificate certificate) throws KeyStoreException { // Verify that this is a self-signed certificate if (!isSelfSignedCertificate(certificate)) { return false; } // Verify that the issuer information has been entered Matcher matcher = valuesPattern.matcher(certificate.getIssuerDN().toString()); return matcher.find() && matcher.find(); } /** * Creates and returns the content of a new singing request for the specified certificate. Signing * requests are required by Certificate Authorities as part of their signing process. The signing request * contains information about the certificate issuer, subject DN, subject alternative names and public key. * Private keys are not included. After the Certificate Authority verified and signed the certificate a new * certificate is going to be returned. Use {@link #installReply(java.security.KeyStore, java.security.KeyStore, char[], String, java.io.InputStream)} * to import the CA reply. * * @param cert the certificate to create a signing request. * @param privKey the private key of the certificate. * @return the content of a new singing request for the specified certificate. */ public static String createSigningRequest(X509Certificate cert, PrivateKey privKey) throws OperatorCreationException, IOException { JcaPKCS10CertificationRequestBuilder csrBuilder = new JcaPKCS10CertificationRequestBuilder( // cert.getSubjectX500Principal(), // cert.getPublicKey() // ); String signatureAlgorithm = "SHA256WITH" + cert.getPublicKey().getAlgorithm(); ContentSigner signer = new JcaContentSignerBuilder(signatureAlgorithm).build(privKey); PKCS10CertificationRequest csr = csrBuilder.build(signer); StringWriter string = new StringWriter(); PemWriter pemWriter = new PemWriter(string); PemObjectGenerator objGen = new MiscPEMGenerator(csr); pemWriter.writeObject(objGen); pemWriter.close(); return string.toString(); } /** * Installs the Certificate Authority reply returned as part of the signing request. The certificate * being signed will get its certificate chain updated with the imported certificate(s). An exception * will be thrown if the replied certificate does not match a local certificate or if the signing * authority is not known by the server (i.e. keystore and truststore files) * * The identity of the entity that has signed the reply is verified against the provided trust store. * * The * * @param keyStore key store where the certificate is stored. * @param trustStore key store where ca certificates are stored. * @param keyPassword password of the keystore. * @param alias the alias of the existing certificate being signed. * @param inputStream the stream containing the CA reply. * @return true if the CA reply was successfully processed. * @throws Exception */ public static boolean installReply(KeyStore keyStore, KeyStore trustStore, char[] keyPassword, String alias, InputStream inputStream) throws Exception { // Check that there is a certificate for the specified alias X509Certificate certificate = (X509Certificate) keyStore.getCertificate( alias ); if (certificate == null) { Log.warn("Certificate not found for alias: " + alias); return false; } // Retrieve the private key of the stored certificate PrivateKey privKey = (PrivateKey) keyStore.getKey(alias, keyPassword); // Load certificates found in the PEM input stream Collection<X509Certificate> certs = parseCertificates( inputStream ); if (certs.isEmpty()) { throw new Exception("Reply has no certificates"); } List<X509Certificate> newCerts; if (certs.size() == 1) { // Reply has only one certificate newCerts = establishCertChain(keyStore, trustStore, null, certs.iterator().next()); } else { // Reply has a chain of certificates newCerts = validateReply(keyStore, trustStore, alias, null, certs); } if (newCerts == null) { return false; } keyStore.setKeyEntry(alias, privKey, keyPassword, newCerts.toArray(new X509Certificate[newCerts.size()])); // Notify listeners that a new certificate has been created for (CertificateEventListener listener : listeners) { try { listener.certificateSigned( keyStore, alias, newCerts ); } catch (Exception e) { Log.error(e.getMessage(), e); } } return true; } /** * Imports a new signed certificate and its private key into the keystore. The certificate input * stream may contain the signed certificate as well as its CA chain. * * @param keyStore key store where the certificate will be stored. * @param trustStore key store where ca certificates are stored. * @param keyPassword password of the keystore. * @param alias the alias of the the new signed certificate. * @param pkInputStream the stream containing the private key. * @param passPhrase is the password phrased used when creating the private key. * @param inputStream the stream containing the signed certificate. * @return true if the certificate was successfully imported. * @throws Exception if no certificates were found in the inputStream. */ public static boolean installCert(KeyStore keyStore, KeyStore trustStore, String keyPassword, String alias, InputStream pkInputStream, final String passPhrase, InputStream inputStream) throws Exception { // Check that there is a certificate for the specified alias X509Certificate certificate = (X509Certificate) keyStore.getCertificate(alias); if (certificate != null) { Log.warn("Certificate already exists for alias: " + alias); return false; } PrivateKey privKey = parsePrivateKey( pkInputStream, passPhrase ); Collection<X509Certificate> certs = parseCertificates( inputStream ); if (certs.isEmpty()) { throw new Exception("No certificates were found"); } List<X509Certificate> newCerts; if (certs.size() == 1) { // Reply has only one certificate newCerts = establishCertChain(keyStore, trustStore, certificate, certs.iterator().next() ); } else { // Reply has a chain of certificates newCerts = validateReply(keyStore, trustStore, alias, certificate, certs); } if (newCerts == null) { return false; } keyStore.setKeyEntry( alias, privKey, keyPassword.toCharArray(), newCerts.toArray( new X509Certificate[ newCerts.size() ] ) ); // Notify listeners that a new certificate has been created (and signed) for (CertificateEventListener listener : listeners) { try { listener.certificateCreated( keyStore, alias, newCerts.get( 0 ) ); if (newCerts.size() > 1) { listener.certificateSigned(keyStore, alias, newCerts); } } catch (Exception e) { Log.error(e.getMessage(), e); } } return true; } public static PrivateKey parsePrivateKey(String pemRepresentation, String passPhrase) throws IOException { if (pemRepresentation == null || pemRepresentation.trim().isEmpty()) { throw new IllegalArgumentException("Argument 'pemRepresentation' cannot be null or an empty String."); } ByteArrayInputStream input = new ByteArrayInputStream(pemRepresentation.getBytes(StandardCharsets.UTF_8)); return parsePrivateKey(input, passPhrase); } /** * Parses a PrivateKey instance from a PEM representation. * * When the provided key is encrypted, the provided pass phrase is applied. * * @param pemRepresentation a PEM representation of a private key (cannot be null or empty) * @param passPhrase optional pass phrase (must be present if the private key is encrypted). * @return a PrivateKey instance (never null) */ public static PrivateKey parsePrivateKey(InputStream pemRepresentation, String passPhrase) throws IOException { if ( passPhrase == null ) { passPhrase = ""; } try (Reader reader = new InputStreamReader(pemRepresentation); // PEMParser pemParser = new PEMParser(reader)) { final Object object = pemParser.readObject(); final JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider( "BC" ); final KeyPair kp; if ( object instanceof PEMEncryptedKeyPair ) { // Encrypted key - we will use provided password final PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build( passPhrase.toCharArray() ); kp = converter.getKeyPair( ( (PEMEncryptedKeyPair) object ).decryptKeyPair( decProv ) ); } else if ( object instanceof PKCS8EncryptedPrivateKeyInfo ) { // Encrypted key - we will use provided password try { final PKCS8EncryptedPrivateKeyInfo encryptedInfo = (PKCS8EncryptedPrivateKeyInfo) object; final InputDecryptorProvider provider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build( passPhrase.toCharArray() ); final PrivateKeyInfo privateKeyInfo = encryptedInfo.decryptPrivateKeyInfo( provider ); return converter.getPrivateKey( privateKeyInfo ); } catch ( PKCSException | OperatorCreationException e ) { throw new IOException( "Unable to decrypt private key.", e ); } } else if ( object instanceof PrivateKeyInfo ) { return converter.getPrivateKey( (PrivateKeyInfo) object ); } else { // Unencrypted key - no password needed kp = converter.getKeyPair( (PEMKeyPair) object ); } return kp.getPrivate(); } } public static Collection<X509Certificate> parseCertificates(String pemRepresentation) throws IOException, CertificateException { // The parser is very picky. We should trim each line of the input string. final String pem = pemRepresentation // .replaceAll("(?m) +$", "") // remove trailing whitespace .replaceAll("(?m)^ +", ""); // remove leading whitespace ByteArrayInputStream input = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8)); return parseCertificates(input); } /** * Parses a certificate chain from a PEM representation. * * @param pemRepresentation a PEM representation of a certificate or certificate chain (cannot be null or empty) * @return A collection of certificates (possibly empty, but never null). */ @SuppressWarnings("unchecked") public static Collection<X509Certificate> parseCertificates(InputStream pemRepresentation) throws IOException, CertificateException { CertificateFactory certificateFactory; try { certificateFactory = CertificateFactory.getInstance("X509", "BC"); } catch (NoSuchProviderException e) { certificateFactory = CertificateFactory.getInstance("X509"); } return (Collection<X509Certificate>) certificateFactory.generateCertificates(pemRepresentation); } /** * Registers a listener to receive events. * * @param listener the listener. */ public static void addListener(CertificateEventListener listener) { if (listener == null) { throw new NullPointerException(); } listeners.add( listener ); } /** * Unregisters a listener to receive events. * * @param listener the listener. */ public static void removeListener(CertificateEventListener listener) { listeners.remove( listener ); } private static List<X509Certificate> establishCertChain(KeyStore keyStore, KeyStore trustStore, X509Certificate certificate, X509Certificate certReply) throws Exception { if (certificate != null) { PublicKey publickey = certificate.getPublicKey(); PublicKey publickey1 = certReply.getPublicKey(); if (!publickey.equals(publickey1)) { throw new Exception("Public keys in reply and keystore don't match"); } if (certReply.equals(certificate)) { throw new Exception("Certificate reply and certificate in keystore are identical"); } } Map<String, List<X509Certificate>> knownCerts = new Hashtable<>(); // TODO Figure out why we add keystore issuers. This implies that we always trust the issuer of our identitity (which probably is right, but shouldn't be required) if (keyStore.size() > 0) { knownCerts.putAll(getCertsByIssuer(keyStore)); } if (trustStore.size() > 0) { knownCerts.putAll(getCertsByIssuer(trustStore)); } LinkedList<X509Certificate> answer = new LinkedList<>(); if (buildChain(certReply, answer, knownCerts)) { return answer; } else { throw new Exception("Failed to establish chain from reply"); } } /** * Builds the certificate chain of the specified certificate based on the known list of certificates * that were issued by their respective Principals. Returns true if the entire chain of all certificates * was successfully built. * * @param certificate certificate to build its chain. * @param answer the certificate chain for the corresponding certificate. * @param knownCerts list of known certificates grouped by their issues (i.e. Principals). * @return true if the entire chain of all certificates was successfully built. */ private static boolean buildChain(X509Certificate certificate, LinkedList<X509Certificate> answer, Map<String, List<X509Certificate>> knownCerts) { Principal subject = certificate.getSubjectDN(); Principal issuer = certificate.getIssuerDN(); // Check if the certificate is a root certificate (i.e. was issued by the same Principal that // is present in the subject) if (subject.equals(issuer)) { answer.addFirst(certificate); return true; } // Get the list of known certificates of the certificate's issuer List<X509Certificate> issuerCerts = knownCerts.get(issuer.getName()); if (issuerCerts == null || issuerCerts.isEmpty()) { // No certificates were found so building of chain failed return false; } for (X509Certificate issuerCert : issuerCerts) { PublicKey publickey = issuerCert.getPublicKey(); try { // Verify the certificate with the specified public key certificate.verify(publickey); // Certificate was verified successfully so build chain of issuer's certificate if (!buildChain(issuerCert, answer, knownCerts)) { return false; } } catch (Exception exception) { // Failed to verify certificate return false; } } answer.addFirst( certificate ); return true; } /** * Returns a Map where the key holds the certificate issuers and values the certificates of each issuer. * * @param ks the keystore to get its certs per issuer. * @return a map with the certificates per issuer. * @throws Exception */ private static Map<String, List<X509Certificate>> getCertsByIssuer(KeyStore ks) throws Exception { Map<String, List<X509Certificate>> answer = new HashMap<>(); Enumeration<String> aliases = ks.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); X509Certificate cert = (X509Certificate) ks.getCertificate(alias); if (cert != null) { Principal subjectDN = cert.getSubjectDN(); List<X509Certificate> vec = answer.get(subjectDN); if (vec == null) { vec = new ArrayList<>(); vec.add(cert); } else { if (!vec.contains(cert)) { vec.add(cert); } } answer.put(subjectDN.getName(), vec); } } return answer; } /** * Orders certificates, starting from the entity to be validated and progressing back toward the CA root. * * This implementation matches "issuers" to "subjects" of certificates in such a way that "issuer" value of a * certificate matches the "subject" value of the next certificate. * * When certificates are provided that do not belong to the same chain, a CertificateException is thrown. * * @param certificates an unordered collection of certificates (cannot be null). * @return An ordered list of certificates (possibly empty, but never null). * @deprecated Moved to CertificateUtils */ @Deprecated public static List<X509Certificate> order( Collection<X509Certificate> certificates ) throws CertificateException { return CertificateUtils.order( certificates ); } /** * Validates chain in certification reply, and returns the ordered * elements of the chain (with user certificate first, and root * certificate last in the array). * * @param alias the alias name * @param userCert the user certificate of the alias * @param certs the chain provided in the reply */ private static List<X509Certificate> validateReply(KeyStore keyStore, KeyStore trustStore, String alias, X509Certificate userCert, Collection<X509Certificate> certs) throws Exception { List<X509Certificate> replyCerts = new ArrayList<>(certs); // order the certs in the reply (bottom-up). int i; X509Certificate tmpCert; if (userCert != null) { PublicKey userPubKey = userCert.getPublicKey(); for (i = 0; i < replyCerts.size(); i++) { if (userPubKey.equals(replyCerts.get(i).getPublicKey())) { break; } } if (i == replyCerts.size()) { throw new Exception( "Certificate reply does not contain public key for <alias>: " + alias); } tmpCert = replyCerts.get(0); replyCerts.set(0, replyCerts.get(i)); replyCerts.set(i, tmpCert); } Principal issuer = replyCerts.get(0).getIssuerDN(); for (i = 1; i < replyCerts.size() - 1; i++) { // find a cert in the reply whose "subject" is the same as the // given "issuer" int j; for (j = i; j < replyCerts.size(); j++) { Principal subject = replyCerts.get(j).getSubjectDN(); if (subject.equals(issuer)) { tmpCert = replyCerts.get(i); replyCerts.set(i, replyCerts.get(j)); replyCerts.set(j, tmpCert); issuer = replyCerts.get(i).getIssuerDN(); break; } } if (j == replyCerts.size()) { throw new Exception("Incomplete certificate chain in reply"); } } // now verify each cert in the ordered chain for (i = 0; i < replyCerts.size() - 1; i++) { PublicKey pubKey = replyCerts.get(i + 1).getPublicKey(); try { replyCerts.get(i).verify(pubKey); } catch (Exception e) { throw new Exception( "Certificate chain in reply does not verify: " + e.getMessage()); } } // do we trust the (root) cert at the top? X509Certificate topCert = replyCerts.get(replyCerts.size() - 1); boolean foundInKeyStore = keyStore.getCertificateAlias(topCert) != null; boolean foundInCAStore = trustStore.getCertificateAlias(topCert) != null; if (!foundInKeyStore && !foundInCAStore) { boolean verified = false; X509Certificate rootCert = null; for (Enumeration<String> aliases = trustStore.aliases(); aliases.hasMoreElements();) { String name = aliases.nextElement(); rootCert = (X509Certificate) trustStore.getCertificate(name); if (rootCert != null) { try { topCert.verify(rootCert.getPublicKey()); verified = true; break; } catch (Exception e) { // Ignore } } } if (!verified) { return null; } else { // Check if the cert is a self-signed cert if (!topCert.getSubjectDN().equals(topCert.getIssuerDN())) { // append the (self-signed) root CA cert to the chain replyCerts.add(rootCert); } } } return replyCerts; } /** * Creates an X509 version3 certificate. * * @param kp KeyPair that keeps the public and private keys for the new certificate. * @param days time to live * @param issuerCommonName Issuer CN string * @param subjectCommonName Subject CN string * @param domain Domain of the server. * @param signAlgoritm Signature algorithm. This can be either a name or an OID. * @return X509 V3 Certificate * @throws GeneralSecurityException * @throws IOException */ public static synchronized X509Certificate createX509V3Certificate(KeyPair kp, int days, String issuerCommonName, String subjectCommonName, String domain, String signAlgoritm) throws GeneralSecurityException, IOException { // subjectDN X500NameBuilder subjectBuilder = new X500NameBuilder(); subjectBuilder.addRDN(BCStyle.CN, subjectCommonName); // issuerDN X500NameBuilder issuerBuilder = new X500NameBuilder(); issuerBuilder.addRDN(BCStyle.CN, issuerCommonName); return createX509V3Certificate(kp, days, issuerBuilder, subjectBuilder, domain, signAlgoritm); } /** * Creates an X509 version3 certificate. * * @param kp KeyPair that keeps the public and private keys for the new certificate. * @param days time to live * @param issuerBuilder IssuerDN builder * @param subjectBuilder SubjectDN builder * @param domain Domain of the server. * @param signAlgoritm Signature algorithm. This can be either a name or an OID. * @return X509 V3 Certificate * @throws GeneralSecurityException * @throws IOException */ public static synchronized X509Certificate createX509V3Certificate(KeyPair kp, int days, X500NameBuilder issuerBuilder, X500NameBuilder subjectBuilder, String domain, String signAlgoritm) throws GeneralSecurityException, IOException { PublicKey pubKey = kp.getPublic(); PrivateKey privKey = kp.getPrivate(); byte[] serno = new byte[8]; SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); random.setSeed((new Date().getTime())); random.nextBytes(serno); BigInteger serial = (new java.math.BigInteger(serno)).abs(); X500Name issuerDN = issuerBuilder.build(); X500Name subjectDN = subjectBuilder.build(); // builder JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( // issuerDN, // serial, // new Date(), // new Date(System.currentTimeMillis() + days * (1000L * 60 * 60 * 24)), // subjectDN, // pubKey // ); // add subjectAlternativeName extension boolean critical = subjectDN.getRDNs().length == 0; ASN1Sequence othernameSequence = new DERSequence(new ASN1Encodable[]{ new ASN1ObjectIdentifier("1.3.6.1.5.5.7.8.5"), new DERUTF8String( domain )}); GeneralName othernameGN = new GeneralName(GeneralName.otherName, othernameSequence); GeneralNames subjectAltNames = new GeneralNames(new GeneralName[]{othernameGN}); certBuilder.addExtension(Extension.subjectAlternativeName, critical, subjectAltNames); // add keyIdentifiers extensions JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils(); certBuilder.addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(pubKey)); certBuilder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(pubKey)); try { // build the certificate ContentSigner signer = new JcaContentSignerBuilder(signAlgoritm).build(privKey); X509CertificateHolder cert = certBuilder.build(signer); // verify the validity if (!cert.isValidOn(new Date())) { throw new GeneralSecurityException("Certificate validity not valid"); } // verify the signature (self-signed) ContentVerifierProvider verifierProvider = new JcaContentVerifierProviderBuilder().build(pubKey); if (!cert.isSignatureValid(verifierProvider)) { throw new GeneralSecurityException("Certificate signature not valid"); } return new JcaX509CertificateConverter().getCertificate(cert); } catch (OperatorCreationException | CertException e) { throw new GeneralSecurityException(e); } } }