All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.nrc.cadc.auth.SSLUtil Maven / Gradle / Ivy

There is a newer version: 1.11.5
Show newest version
/*
 ************************************************************************
 *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
 **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
 *
 *  (c) 2024.                            (c) 2024.
 *  Government of Canada                 Gouvernement du Canada
 *  National Research Council            Conseil national de recherches
 *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
 *  All rights reserved                  Tous droits réservés
 *
 *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
 *  expressed, implied, or               énoncée, implicite ou légale,
 *  statutory, of any kind with          de quelque nature que ce
 *  respect to the software,             soit, concernant le logiciel,
 *  including without limitation         y compris sans restriction
 *  any warranty of merchantability      toute garantie de valeur
 *  or fitness for a particular          marchande ou de pertinence
 *  purpose. NRC shall not be            pour un usage particulier.
 *  liable in any event for any          Le CNRC ne pourra en aucun cas
 *  damages, whether direct or           être tenu responsable de tout
 *  indirect, special or general,        dommage, direct ou indirect,
 *  consequential or incidental,         particulier ou général,
 *  arising from the use of the          accessoire ou fortuit, résultant
 *  software.  Neither the name          de l'utilisation du logiciel. Ni
 *  of the National Research             le nom du Conseil National de
 *  Council of Canada nor the            Recherches du Canada ni les noms
 *  names of its contributors may        de ses  participants ne peuvent
 *  be used to endorse or promote        être utilisés pour approuver ou
 *  products derived from this           promouvoir les produits dérivés
 *  software without specific prior      de ce logiciel sans autorisation
 *  written permission.                  préalable et particulière
 *                                       par écrit.
 *
 *  This file is part of the             Ce fichier fait partie du projet
 *  OpenCADC project.                    OpenCADC.
 *
 *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
 *  you can redistribute it and/or       vous pouvez le redistribuer ou le
 *  modify it under the terms of         modifier suivant les termes de
 *  the GNU Affero General Public        la “GNU Affero General Public
 *  License as published by the          License” telle que publiée
 *  Free Software Foundation,            par la Free Software Foundation
 *  either version 3 of the              : soit la version 3 de cette
 *  License, or (at your option)         licence, soit (à votre gré)
 *  any later version.                   toute version ultérieure.
 *
 *  OpenCADC is distributed in the       OpenCADC est distribué
 *  hope that it will be useful,         dans l’espoir qu’il vous
 *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
 *  without even the implied             GARANTIE : sans même la garantie
 *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
 *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
 *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
 *  General Public License for           Générale Publique GNU Affero
 *  more details.                        pour plus de détails.
 *
 *  You should have received             Vous devriez avoir reçu une
 *  a copy of the GNU Affero             copie de la Licence Générale
 *  General Public License along         Publique GNU Affero avec
 *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
 *  .      pas le cas, consultez :
 *                                       .
 *
 *  $Revision: 5 $
 *
 ************************************************************************
 */

package ca.nrc.cadc.auth;

import ca.nrc.cadc.util.Base64;
import ca.nrc.cadc.util.FileUtil;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.Subject;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject;

/**
 * Utility class to setup SSL before trying to use HTTPS.
 * 
 * @author pdowler
 */
public class SSLUtil {
    private static Logger log = Logger.getLogger(SSLUtil.class);
    
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    // SSL, SSLv2mm SSLv3, TLS, TLSv1, TLSv1.1
    private static final String SSL_PROTOCOL = "TLS";

    // jceks, jks, pkcs12
    private static final String KEYSTORE_TYPE = "JKS";

    // SunX509
    private static final String KEYMANAGER_ALGORITHM = "SunX509";

    private static final String CERT_ALIAS = "opencadc_x509";

    private static final char[] THE_PASSWORD = CERT_ALIAS.toCharArray();

    /**
     * Set the default SSL socket factory with client credentials. This is in 
     * use in some test code but it's a very bad idea because all threads in the
     * JVM use those same credentials for SSL (https) calls.
     * 
     * @param pemFile
     * @deprecated
     */
    @Deprecated
    public static void initSSL(File pemFile) {
        SSLSocketFactory sf = getSocketFactory(pemFile);
        HttpsURLConnection.setDefaultSSLSocketFactory(sf);
    }

    public static SSLSocketFactory getSocketFactory(File pemFile) {
        X509CertificateChain chain;
        try {
            chain = readPemCertificateAndKey(pemFile);
        } catch (InvalidKeySpecException ex) {
            throw new RuntimeException("failed to read RSA private key from " + pemFile, ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("BUG: failed to create empty KeyStore", ex);
        } catch (IOException ex) {
            throw new RuntimeException("failed to read certificate file " + pemFile, ex);
        } catch (CertificateException ex) {
            throw new RuntimeException("failed to load certificate from file " + pemFile, ex);
        }
        return getSocketFactory(chain);
    }
    
    /**
     * Create an SSLSocketfactory from the credentials in the specified Subject.
     * This method extracts a X509CertificateChain from the public credentials and
     * uses the certificate chain and private key found there to set up a KeyStore
     * for the SSLSocketFactory.
     * 
     * @param s
     * @return an SSLSocketFactory, or null if no X509CertificateChain can be found
     */
    public static SSLSocketFactory getSocketFactory(Subject s) {
        X509CertificateChain chain = null;
        if (s != null) {
            Set certs = s.getPublicCredentials(X509CertificateChain.class);
            for (X509CertificateChain cc : certs) {
                if (cc.getKey() != null) {
                    chain = cc;
                    break;
                }
            }
        }
        if (chain == null) {
            return null;
        }
        
        return getSocketFactory(chain);
    }

    public static SSLSocketFactory getSocketFactory(X509CertificateChain chain) {
        KeyStore ts = null;
        KeyStore ks = null;
        if (chain != null) {
            ks = getKeyStore(chain.getChain(), chain.getPrivateKey());
        }
        
        KeyManagerFactory kmf = getKeyManagerFactory(ks);
        TrustManagerFactory tmf = getTrustManagerFactory(ts);
        SSLContext ctx = getContext(kmf, tmf, ks);
        SSLSocketFactory sf = ctx.getSocketFactory();
        return sf;
    }

    public static Subject createSubject(File certKeyFile) {
        try {
            X509CertificateChain certKey = readPemCertificateAndKey(certKeyFile);
            return AuthenticationUtil.getSubject(certKey);
        } catch (InvalidKeySpecException ex) {
            throw new RuntimeException("failed to read RSA private key from " + certKeyFile, ex);
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("BUG: failed to create empty KeyStore", ex);
        } catch (IOException ex) {
            throw new RuntimeException("failed to read certificate file " + certKeyFile, ex);
        } catch (CertificateException ex) {
            throw new RuntimeException("failed to load certificate from file " + certKeyFile, ex);
        }
    }

    /**
     * Extracts all the certificates from the argument, decodes them from base64 to
     * byte[] and concatenates all the certificates preserving the order.
     * 
     * @param certBuf buffer containing certificates
     * @return decoded certificate chain
     * @throws IOException
     */
    public static byte[] getCertificates(byte[] certBuf) throws IOException {
        BufferedReader rdr = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(certBuf)));
        String line = rdr.readLine();

        List certs = new ArrayList(); // list of byte certificates
        int byteSize = 0;
        if (line != null) {
            line = line.trim();

            if (!line.startsWith("---")) {
                // HAProxy cert from http header: single base64 encoded cert
                byte[] tmp = Base64.decode(line);
                byteSize += tmp.length;
                certs.add(tmp);
            }
            // TODO nginx SSL ternination and client cert forwarding:
            // includes the begin/end markers and arbitrary whitespace in place of EOL

            // TODO apache SSL termination and client cert forwarding: ??
        }
        if (byteSize == 0) {
            while (line != null) {

                StringBuilder base64 = new StringBuilder();
                if (line.startsWith(X509CertificateChain.CERT_BEGIN)) {
                    // log.debug(line);
                    line = rdr.readLine();
                    while (line != null && !line.startsWith(X509CertificateChain.CERT_END)) {
                        // log.debug(line + " (" + line.length() + ")");
                        base64.append(line.trim());
                        line = rdr.readLine();
                    }
                    if (line.startsWith(X509CertificateChain.CERT_END)) {
                        String encoded = base64.toString();
                        // log.debug("CERTIFICATE: " + encoded);
                        byte[] tmp = Base64.decode(encoded);
                        byteSize += tmp.length;
                        certs.add(tmp);
                    }
                    // log.debug(line);
                } else {
                    // blank line
                    line = rdr.readLine();
                }
            }
        }
        rdr.close();

        // flatten out the certificate bytes into one byte[]
        byte[] result = new byte[byteSize];
        byteSize = 0;
        for (byte[] cert : certs) {
            System.arraycopy(cert, 0, result, byteSize, cert.length);
            byteSize += cert.length;
            // log.debug("CERTIFICATE: " + result);
        }
        return result;
    }

    public static X509Certificate[] readCertificateChain(File certFile) throws CertificateException, IOException {
        try {
            X509Certificate[] chain = readCertificateChain(FileUtil.readFile(certFile));
            log.debug("X509 certificate is valid");
            return chain;
        } catch (CertificateException ex) {
            throw new RuntimeException("certificate from file " + certFile + " is not valid", ex);
        }

    }

    public static X509Certificate[] readCertificateChain(byte[] certBuf) throws CertificateException, IOException {
        BufferedInputStream istream = new BufferedInputStream(new ByteArrayInputStream(certBuf));
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        ArrayList certs = new ArrayList();
        while (istream.available() > 0) {
            Certificate cert = cf.generateCertificate(istream);
            // log.debug("found: " + cert);
            certs.add(cert);
        }
        istream.close();

        X509Certificate[] chain = new X509Certificate[certs.size()];
        Iterator i = certs.iterator();
        int c = 0;
        while (i.hasNext()) {
            X509Certificate x509 = (X509Certificate) i.next();
            chain[c++] = x509;
            try {
                x509.checkValidity();
                log.debug("X509 certificate is valid");
            } catch (CertificateExpiredException exp) {
                log.debug("X509 certificate is expired");
                // nothing to be done here
            } catch (CertificateException ex) {
                throw new RuntimeException("certificate byte array is not valid", ex);
            }
        }
        return chain;
    }

    // needed by cadc-cdp-server
    public static PrivateKey readPrivateKey(byte[] bytesPrivateKey)
            throws InvalidKeySpecException, NoSuchAlgorithmException, IOException {
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytesPrivateKey);
        PrivateKey pk = kf.generatePrivate(spec);
        return pk;
    }

    private static KeyStore getKeyStore(Certificate[] chain, PrivateKey pk) {
        try {
            KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
            try {
                ks.load(null, null);
            } catch (Exception ignore) {
                // do nothing
            }
            // @SuppressWarnings("unused") KeyStore.Entry ke = new
            // KeyStore.PrivateKeyEntry(pk, chain);
            ks.setKeyEntry(CERT_ALIAS, pk, THE_PASSWORD, chain);
            log.debug(
                    "added certificate chain to keystore: " + CERT_ALIAS + "," + pk + "," + THE_PASSWORD + "," + chain);
            return ks;
        } catch (KeyStoreException ex) {
            if (ex.getCause() != null && ex.getCause() instanceof java.security.NoSuchAlgorithmException) {
                throw new IllegalArgumentException("Sorry, this implementation of Java, issued by "
                        + System.getProperty("java.vendor") + ", does not support CADC Certificates.");
            }
            throw new RuntimeException("failed to find/load KeyStore of type " + KEYSTORE_TYPE, ex);
        }
    }

    private static KeyManagerFactory getKeyManagerFactory(KeyStore keyStore) {
        String da = KEYMANAGER_ALGORITHM;
        try {
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(da);
            // assume a non-password-protected proxy cert
            kmf.init(keyStore, THE_PASSWORD); 
            return kmf;
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("failed to find KeyManagerFactory for " + da, ex);
        } catch (KeyStoreException ex) {
            throw new RuntimeException("failed to init KeyManagerFactory", ex);
        } catch (UnrecoverableKeyException ex) {
            throw new RuntimeException("failed to init KeyManagerFactory", ex);
        }
    }

    private static TrustManagerFactory getTrustManagerFactory(KeyStore trustStore) {
        try {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX", "SunJSSE");
            tmf.init(trustStore);
            return tmf;
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("BUG: failed to create TrustManagerFactory for algorithm=PKIX", ex);
        } catch (NoSuchProviderException ex) {
            throw new RuntimeException("BUG: failed to create TrustManagerFactory for provider=SunJSSE", ex);
        } catch (KeyStoreException ex) {
            throw new RuntimeException("failed to init trustManagerFactory", ex);
        }
    }

    private static SSLContext getContext(KeyManagerFactory kmf, TrustManagerFactory tmf, KeyStore ks) {
        try {
            KeyManager[] kms = kmf.getKeyManagers();
            for (int i = 0; i < kms.length; i++) {
                // cast is safe since we used KEYMANAGER_ALGORITHM=SunX509
                // above
                BasicX509KeyManager wrapper = new BasicX509KeyManager((X509KeyManager) kms[i], CERT_ALIAS);
                kms[i] = wrapper;
            }
            TrustManager[] tms = tmf.getTrustManagers();
            for (int i = 0; i < tms.length; i++) {
                // safe cast since we used PKIX, SunJSSE above
                BasicX509TrustManager wrapper = new BasicX509TrustManager((X509TrustManager) tms[i]);
                tms[i] = wrapper;
            }
            SSLContext ctx = SSLContext.getInstance(SSL_PROTOCOL);
            log.debug("KMF returned " + kms.length + " KeyManagers");
            log.debug("TMF returned " + tms.length + " TrustManagers");
            ctx.init(kms, tms, null);
            return ctx;
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException("failed to find SSLContext for " + SSL_PROTOCOL, ex);
        } catch (KeyManagementException ex) {
            throw new RuntimeException("failed to init SSLContext", ex);
        }
    }

    @SuppressWarnings("unchecked")
    private static void printKeyStoreInfo(KeyStore keystore) throws KeyStoreException {
        log.debug("Provider : " + keystore.getProvider().getName());
        log.debug("Type : " + keystore.getType());
        log.debug("Size : " + keystore.size());

        Enumeration en = keystore.aliases();
        while (en.hasMoreElements()) {
            System.out.println("Alias: " + en.nextElement());
        }
    }

    /**
     * Convenience method to parse a PEM encoded file and return the corresponding
     * X509 Certificate chain.
     * 
     * @param pemFile
     * @return certificate chain
     * @throws InvalidKeySpecException
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws CertificateException
     */
    public static X509CertificateChain readPemCertificateAndKey(File pemFile)
            throws InvalidKeySpecException, NoSuchAlgorithmException, IOException, CertificateException {
        PEMParser parser = new PEMParser(new FileReader(pemFile));
        return readPEM(parser);
    }

    /**
     * Parses PEM encoded data that contains certificates and a key and returns the
     * corresponding X509CertificateChain that can be used to create an SSL socket.
     * RSA is the only supporting encoding for the key.
     * 
     * @param data content encoded as PEM.
     * @return X509 Certificate chain.
     * @throws InvalidKeySpecException
     * @throws NoSuchAlgorithmException
     * @throws IOException
     * @throws CertificateException
     */
    public static X509CertificateChain readPemCertificateAndKey(byte[] data)
            throws InvalidKeySpecException, NoSuchAlgorithmException, IOException, CertificateException {
        InputStreamReader r = new InputStreamReader(new ByteArrayInputStream(data));
        PEMParser parser = new PEMParser(r);
        return readPEM(parser);
    }
    
    private static X509CertificateChain readPEM(PEMParser parser) 
        throws InvalidKeySpecException, NoSuchAlgorithmException, IOException, CertificateException {
        
        PrivateKey privateKey = null;
        List certs = new ArrayList<>();
        int byteSize = 0;

        Object obj = parser.readObject();
        while (obj != null) {
            log.debug("found: " + obj.getClass().getName());
            // if private key first: PEMKeyPair followed by PemObject(s) with type=certificate
            // if cert/key/cert...: X509CertificateHolder followed by PemObject(s)
            if (obj instanceof PEMKeyPair) {
                PEMKeyPair pkp = (PEMKeyPair) obj;
                PrivateKeyInfo pki = pkp.getPrivateKeyInfo();
                JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
                privateKey = converter.getPrivateKey(pki);
                log.debug(" private key: " + privateKey.getEncoded().length);
            } else if (obj instanceof PrivateKeyInfo) {
                PrivateKeyInfo pki = (PrivateKeyInfo) obj;
                JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
                privateKey = converter.getPrivateKey(pki);
                log.debug(" private key: " + privateKey.getEncoded().length);
            } else if (obj instanceof X509CertificateHolder) {
                X509CertificateHolder xch = (X509CertificateHolder) obj;
                byte[] bytes = xch.toASN1Structure().getEncoded("DER");
                certs.add(bytes);
                byteSize += bytes.length;
                log.debug(" certificate: " + bytes.length);
            } else if (obj instanceof PemObject) {
                PemObject po = (PemObject) obj;
                if ("certificate".equalsIgnoreCase(po.getType())) {
                    certs.add(po.getContent());
                    byteSize += po.getContent().length;
                    log.debug(" certificate: " + po.getContent().length);
                } else if ("rsa private key".equalsIgnoreCase(po.getType())) {
                    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(po.getContent());
                    privateKey = keyFactory.generatePrivate(keySpec);
                    log.debug(" private key: " + po.getContent().length);
                } else {
                    log.warn("readPEM: unexpected PemObject type: " + po.getType() + " aka " + obj.getClass().getName());
                }
            }
            
            obj = parser.readPemObject();
        }
        
        // flatten out the certificate bytes into one byte[]
        byte[] flat = new byte[byteSize];
        byteSize = 0;
        for (byte[] cert : certs) {
            System.arraycopy(cert, 0, flat, byteSize, cert.length);
            byteSize += cert.length;
        }
        X509Certificate[] chain = readCertificateChain(flat);
        
        return new X509CertificateChain(chain, privateKey);
    }
   
    /**
     * Checks whether the subject's certificate credentials are valid at a given
     * date. If date is missing, current time is used as reference.
     * 
     * @param subject Subject to check
     * @param date    Date the certificate is verified against. If null, the
     *                credentials are verified against current time.
     * @throws CertificateException            Subject has no associated certificate
     *                                         credentials or there is a problem
     *                                         with the existing certificate.
     * @throws CertificateExpiredException     Certificate is expired.
     * @throws CertificateNotYetValidException Certificate not valid yet.
     */
    public static void validateSubject(Subject subject, Date date)
            throws CertificateException, CertificateExpiredException, CertificateNotYetValidException {
        if (subject != null) {
            Set certs = subject.getPublicCredentials(X509CertificateChain.class);
            if (certs.isEmpty()) {
                // subject without certs
                throw new CertificateException("No certificates associated with subject");
            }
            X509CertificateChain chain = certs.iterator().next();
            for (X509Certificate c : chain.getChain()) {
                if (date != null) {
                    c.checkValidity(date);
                } else {
                    c.checkValidity();
                }
            }
        }
    }

    /**
     * Renew the provided subject with the principals and credentials provided by the certificate.  This is useful for
     * long running authenticated processes where the expiry of the Subject could occur during processing.  Callers
     * will ideally call this as part of a schedule or during a natural break in job execution.
     *
     * 

This method is NOT threadsafe! Because the provided Subject will be put into an inconsistent state * while it is being updated, ensure that it is wrapped in a synchronized block before calling this method. * * @param currentSubject The Subject to be modified. * @param certificate A new certificate used to replace the existing principals and credentials of the * given Subject. */ public static void renewSubject(final Subject currentSubject, final File certificate) { final Subject subject = SSLUtil.createSubject(certificate); currentSubject.getPrincipals().clear(); currentSubject.getPrincipals().addAll(subject.getPrincipals()); currentSubject.getPublicCredentials().clear(); currentSubject.getPublicCredentials().addAll(subject.getPublicCredentials()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy