diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/com/isode/stroke/tls/CAPICertificate.java | 80 | ||||
| -rw-r--r-- | src/com/isode/stroke/tls/java/CAPIConstants.java | 35 | ||||
| -rw-r--r-- | src/com/isode/stroke/tls/java/CAPIKeyManager.java | 110 | ||||
| -rw-r--r-- | src/com/isode/stroke/tls/java/JSSEContext.java | 218 | 
4 files changed, 423 insertions, 20 deletions
diff --git a/src/com/isode/stroke/tls/CAPICertificate.java b/src/com/isode/stroke/tls/CAPICertificate.java new file mode 100644 index 0000000..2609a82 --- /dev/null +++ b/src/com/isode/stroke/tls/CAPICertificate.java @@ -0,0 +1,80 @@ +/*  Copyright (c) 2013, Isode Limited, London, England. + *  All rights reserved. + * + *  Acquisition and use of this software and related materials for any + *  purpose requires a written licence agreement from Isode Limited, + *  or a written licence from an organisation licensed by Isode Limited Limited + *  to grant such a licence. + * + */ +  +package com.isode.stroke.tls; + +import java.security.cert.X509Certificate; + +import com.isode.stroke.base.NotNull; +import com.isode.stroke.tls.java.CAPIConstants; + +/** + * CAPICertificate objects refer to certificate/key pairs that are held by + * CAPI. A CAPICertificate itself doesn't have any key information inside + * it. It doesn't make sense to use these on platforms other than Windows. + */ +public class CAPICertificate extends CertificateWithKey { + +    +    private X509Certificate x509Certificate = null; +    private String keyStoreName = null; +     +    @Override +    public boolean isNull() { +        return (x509Certificate == null); +    } + +    /** +     * Construct a new object. Note that the constructor does not perform any  +     * checking that the specified certificate exists or is usable. Such a  +     * check will take place if/when the certificate and key are needed (for +     * example, to establish a TLS connection), and it will be at this stage  +     * that any prompts may appear to insert a smartcard or enter a PIN etc.. +     *   +     *  +     * @param x509Certificate an X509Certificate corresponding to a certificate +     * that is available in certificate object which has been read from +     * CAPI. Must not be null. +     *  +     * @param keyStoreName the name of the Windows keystore containing this +     * certificate. This may be null, in which case a search will be made of +     * all the stores named in {@link CAPIConstants#knownSunMSCAPIKeyStores} +     * and the first match used. +     */ +    public CAPICertificate(X509Certificate x509Certificate, String keyStoreName) { +        NotNull.exceptIfNull(x509Certificate,"x509Certificate");  +        this.x509Certificate = x509Certificate; +        this.keyStoreName = keyStoreName; +    } +     +    @Override +    public String toString() { +        return "CAPICertificate in " +  +                (keyStoreName == null ? "unspecified keystore" : keyStoreName) + +                 " for " + x509Certificate.getSubjectDN(); +    } +     +    /** +     * Return the X509Certificate associated with this object +     * @return the X509Certificate, which will never be null. +     */ +    public X509Certificate getX509Certificate() { +        return x509Certificate; +    } +     +    /** +     * Return the name of the KeyStore associated with this object, if any. +     * @return the KeyStore name, which may be null +     */ +    public String getKeyStoreName() { +        return keyStoreName; +    } + +} diff --git a/src/com/isode/stroke/tls/java/CAPIConstants.java b/src/com/isode/stroke/tls/java/CAPIConstants.java new file mode 100644 index 0000000..9ef086a --- /dev/null +++ b/src/com/isode/stroke/tls/java/CAPIConstants.java @@ -0,0 +1,35 @@ +/*  Copyright (c) 2013, Isode Limited, London, England. + *  All rights reserved. + * + *  Acquisition and use of this software and related materials for any + *  purpose requires a written licence agreement from Isode Limited, + *  or a written licence from an organisation licensed by Isode Limited Limited + *  to grant such a licence. + * + */ +  +package com.isode.stroke.tls.java; +/** + * Defines various constant values used in the CAPI implementation + */ +public class CAPIConstants { +    /** +     * The name of the Sun MSCAPI provider +     */ +    final public static String sunMSCAPIProvider = "SunMSCAPI"; +     +    /** +     * The list of KeyStores available in the SunMSCAPI provider +     * as per Oracle's  +     * <a href=http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html> +     * JCA documentation</a>. +     * The list is in order of preference +     * I can't see a reliable programmatic way of asking the provider what +     * keystores it supports. +     *  +     */ +    final public static String[] knownSunMSCAPIKeyStores = new String[]  +            {"Windows-MY", "Windows-ROOT"}; + + +} diff --git a/src/com/isode/stroke/tls/java/CAPIKeyManager.java b/src/com/isode/stroke/tls/java/CAPIKeyManager.java new file mode 100644 index 0000000..84e0d97 --- /dev/null +++ b/src/com/isode/stroke/tls/java/CAPIKeyManager.java @@ -0,0 +1,110 @@ +/*  Copyright (c) 2013, Isode Limited, London, England. + *  All rights reserved. + * + *  Acquisition and use of this software and related materials for any + *  purpose requires a written licence agreement from Isode Limited, + *  or a written licence from an organisation licensed by Isode Limited Limited + *  to grant such a licence. + * + */ +  +package com.isode.stroke.tls.java; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; + +import com.isode.stroke.base.NotNull; + +/** + * This class is used to provide a way of overriding the behaviour of a KeyManager + * returned from SunMSCAPI. + * <p>Specifically, this implementation allows callers to specify what should + * be returned by {@link #chooseEngineClientAlias(String[], Principal[], SSLEngine) + */ +public class CAPIKeyManager extends X509ExtendedKeyManager { + +    X509ExtendedKeyManager parentKeyManager = null; +    String engineClientAlias = null; +     +    /** +     * Create a new object.  +     * @param parent the actual X509ExtendedKeyManager to which work will +     * be delegated unless overridden by caller-specified values. Must +     * not be null. +     */ +    public CAPIKeyManager(X509ExtendedKeyManager parent) { +        NotNull.exceptIfNull(parent,"parent");  +        this.parentKeyManager = parent; +    } +     +    /** +     * Set the value which should be returned by +     * {@link #chooseEngineClientAlias(String[], Principal[], SSLEngine)}. +     *  +     * <p>The default behaviour of the SunMSCAPI KeyManager is to pick what it +     * thinks is the most suitable client certificate for the session.  +     * However, this may not be the same as the certificate which was specified +     * by the client. This method allows callers to override the default  +     * behaviour and force a specific certificate to be used. +     *  +     * @param engineClientAlias the alias of an entry in the KeyStore. This +     * may be null, in which case when +     * {@link #chooseEngineClientAlias(String[], Principal[], SSLEngine) is +     * called, it will return whatever value the original KeyManager returns. +     */ +    public void setEngineClientAlias(String engineClientAlias) { +        this.engineClientAlias = engineClientAlias; +    } +     +    @Override +    public String[] getServerAliases(String keyType, Principal[] issuers) { +        return parentKeyManager.getServerAliases(keyType, issuers); +    } +     +    @Override +    public PrivateKey getPrivateKey(String alias) { +        return parentKeyManager.getPrivateKey(alias); +    } +     +    @Override +    public String[] getClientAliases(String keyType, Principal[] issuers) { +        return parentKeyManager.getClientAliases(keyType, issuers); +    } +     +    @Override +    public X509Certificate[] getCertificateChain(String alias) { +        return parentKeyManager.getCertificateChain(alias); +    } +     +    @Override +    public String chooseServerAlias(String keyType, Principal[] issuers, +            Socket socket) { +        return parentKeyManager.chooseServerAlias(keyType, issuers, socket); + +    } +     +    @Override +    public String chooseClientAlias(String[] keyType, Principal[] issuers, +            Socket socket) { +        return parentKeyManager.chooseClientAlias(keyType, issuers, socket); +    } +     +    @Override +    public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { +        if (engineClientAlias != null) { +            return engineClientAlias; +        } +        return parentKeyManager.chooseEngineClientAlias(keyType, issuers, engine); +    } +    @Override +    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) { +        return parentKeyManager.chooseEngineServerAlias(keyType, issuers, engine); + +    }         + +} diff --git a/src/com/isode/stroke/tls/java/JSSEContext.java b/src/com/isode/stroke/tls/java/JSSEContext.java index 58fd7f8..9cb0109 100644 --- a/src/com/isode/stroke/tls/java/JSSEContext.java +++ b/src/com/isode/stroke/tls/java/JSSEContext.java @@ -20,6 +20,7 @@ import java.security.KeyManagementException;  import java.security.KeyStore;  import java.security.KeyStoreException;  import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException;  import java.security.UnrecoverableKeyException;  import java.security.cert.CertificateException;  import java.security.cert.CertificateExpiredException; @@ -37,8 +38,10 @@ import javax.net.ssl.SSLEngineResult;  import javax.net.ssl.SSLEngineResult.HandshakeStatus;  import javax.net.ssl.SSLEngineResult.Status;  import javax.net.ssl.SSLException; +import javax.net.ssl.X509ExtendedKeyManager;  import com.isode.stroke.base.ByteArray; +import com.isode.stroke.tls.CAPICertificate;  import com.isode.stroke.tls.Certificate;  import com.isode.stroke.tls.CertificateVerificationError;  import com.isode.stroke.tls.CertificateVerificationError.Type; @@ -47,12 +50,15 @@ import com.isode.stroke.tls.PKCS12Certificate;  import com.isode.stroke.tls.TLSContext; +  /**   * Concrete implementation of a TLSContext which uses SSLEngine    * and maybe other stuff? ..tbs...   *    */  public class JSSEContext extends TLSContext { + +	      private static class JSSEContextError {          public final Exception exception; @@ -425,6 +431,7 @@ public class JSSEContext extends TLSContext {          if (byteArray != null) {              int s = byteArray.getSize(); +              onDataForNetwork.emit(byteArray);              bytesSentToSocket += s;              byteArray = null; @@ -584,25 +591,17 @@ public class JSSEContext extends TLSContext {      } -     -    @Override -    public boolean setClientCertificate(CertificateWithKey cert) { -        if (cert == null || cert.isNull()) { -            emitError(null,cert + " has no useful contents"); -            return false; -        } -        if (!(cert instanceof PKCS12Certificate)) { -            emitError(null,"setClientCertificate can only work with PKCS12 objects"); -            return false; -        } -        PKCS12Certificate p12 = (PKCS12Certificate)cert; -        if (!p12.isPrivateKeyExportable()) { -            emitError(null,cert + " does not have exportable private key"); +    /** +     * Private method to handle PKCS12Certificate case for setClientCertificate +     */ +    private boolean setClientCertificatePKCS12(PKCS12Certificate p12Cert) { +        if (!p12Cert.isPrivateKeyExportable()) { +            emitError(null,p12Cert + " does not have exportable private key");              return false;          }          /* Get a reference that can be used in any error messages */ -        File p12File = new File(p12.getCertStoreName()); +        File p12File = new File(p12Cert.getCertStoreName());          /* Attempt to build a usable identity from the P12 file. This set of           * operations can result in a variety of exceptions, all of which @@ -618,14 +617,14 @@ public class JSSEContext extends TLSContext {              kmf = KeyManagerFactory.getInstance("SunX509");              /* The PKCS12Certificate object has read the file contents already */ -            ByteArray ba = p12.getData(); +            ByteArray ba = p12Cert.getData();              byte[] p12Bytes = ba.getData();              ByteArrayInputStream bis = new ByteArrayInputStream(p12Bytes);              /* Both of the next two calls require that we supply the password */ -            keyStore.load(bis, p12.getPassword()); -            kmf.init(keyStore, p12.getPassword()); +            keyStore.load(bis, p12Cert.getPassword()); +            kmf.init(keyStore, p12Cert.getPassword());              KeyManager[] keyManagers = kmf.getKeyManagers();              if (keyManagers == null || keyManagers.length == 0) { @@ -662,6 +661,185 @@ public class JSSEContext extends TLSContext {          /* Fall through here after any exception */          return false; +         +    } + +    /** +     * Structure used to keep track of a KeyStore/alias tuple +     */ +    private static class KeyStoreAndAlias { +        public KeyStore keyStore; +        public String alias; +        KeyStoreAndAlias(KeyStore keyStore, String alias) { +            this.keyStore = keyStore; +            this.alias = alias; +        } +    } +     +     +    /** +     * See if a given X509Certificate can be found in a specific CAPI keystore.  +     * @param x509Cert the certificate to look for. Must not be null. +     * @param keyStoreName the name of the keystore to search. Must not be null. +     * @return a StoreAndAlias object containing references the keystore and +     * alias which match, or null if <em>x509Cert</em> was not found. +     */ +    private KeyStoreAndAlias findCAPIKeyStoreForCertificate( +            X509Certificate x509Cert,  +            String keyStoreName) { +         +        KeyStore ks = null; +              +        /* Try to instantiate a CAPI keystore. This will fail on non-Windows +         * platforms +         */       +        try { +            ks = KeyStore.getInstance(keyStoreName, CAPIConstants.sunMSCAPIProvider); +        } +        catch (NoSuchProviderException e) {   +            /* Quite likely we're not on Windows */ +            emitError(e, "Unable to instantiate " + CAPIConstants.sunMSCAPIProvider + " provider"); +            return null; +        } +        catch (KeyStoreException e) { +            /* The keystore name is not right. Most likely the caller specified +             * an unrecognized keystore name when creating the CAPICertificate. +             */ +            emitError(e, "Cannot load " + keyStoreName + " from " + CAPIConstants.sunMSCAPIProvider); +            return null; +        } + +        /* All the exceptions that might be thrown here need to be caught but +         * indicate something unexpected has happened, so the catch clauses  +         * all emit errors +         */ +        try { +            /* For a CAPI keystore, no parameters are required for loading */        +            ks.load(null,null);             +            String alias = ks.getCertificateAlias(x509Cert); +             +            return (alias == null ? null : new KeyStoreAndAlias(ks, alias)); + +        } catch (CertificateException e) { +            emitError(e, "Unexpected exception when loading CAPI keystore");             +            return null;              +        } catch (NoSuchAlgorithmException e) { +            emitError(e, "Unexpected exception when loading CAPI keystore");             +            return null;              +        } catch (IOException e) { +            /* This exception is meant to be for when you're loading a keystore +             * from a file, and so isn't expected for a CAPI, so emit an error  +             * error  +             */ +            emitError(e, "Unexpected exception when loading CAPI keystore"); +            return null; +        } catch (KeyStoreException e) { +            /* Thrown by KeyStore.getCertificateAlias when the keystore  +             * hasn't been initialized, so not expected here +             */ +            emitError(e, "Unexpected exception when reading CAPI keystore"); +            return null;             +        } +    } +     + +     +    /** +     * Private method to handle CAPICertificate case for setClientCertificate +     * @param capiCert a CAPICertificate, not null. +     * @return <em>true</em> if the operation was successful, <em>false</em> +     * otherwise. +     */ +    private boolean setClientCertificateCAPI(CAPICertificate capiCert) { +        KeyStoreAndAlias keyStoreAndAlias = null; + +        X509Certificate x509Cert = capiCert.getX509Certificate(); +        String keyStoreName = capiCert.getKeyStoreName(); +         +        if (keyStoreName != null) { +            keyStoreAndAlias = findCAPIKeyStoreForCertificate(x509Cert, keyStoreName); +        } +        else { +            /* Try the list of predefined values, looking for the first match */             +            for (String keyStore:CAPIConstants.knownSunMSCAPIKeyStores) { +                keyStoreAndAlias = findCAPIKeyStoreForCertificate(x509Cert, keyStore); +                if (keyStoreAndAlias != null) { +                    break; +                } +            } +        } +        if (keyStoreAndAlias == null) { +            emitError(null,"Unable to load " + capiCert + " from CAPI"); +            return false; +        } + +        KeyManagerFactory kmf = null; + +        try {     + +            String defaultAlg = KeyManagerFactory.getDefaultAlgorithm(); + +            kmf = KeyManagerFactory.getInstance(defaultAlg); +            kmf.init(keyStoreAndAlias.keyStore,null); +            KeyManager[] kms = kmf.getKeyManagers(); +            if (kms != null && kms.length > 0) { +                /* Successfully loaded the KeyManager. Look for the first  +                 * one which is suitable for our use (there's almost certainly +                 * only one in the list in any case) +                 */ +                for (KeyManager km:kms) {  +                    if (km instanceof X509ExtendedKeyManager) { +                        CAPIKeyManager ckm = new CAPIKeyManager( +                                (X509ExtendedKeyManager)km); +                         +                        /* Make sure that the alias used for client certificate +                         * is the one that the caller asked for  +                         */ +                        ckm.setEngineClientAlias(keyStoreAndAlias.alias); +                        myKeyManager_ = ckm; +                        return true; +                    } +                } +                emitError(null,"Unable to find suitable X509ExtendedKeyManager"); +                return false;                 +            } +            return false; + +        } catch (NoSuchAlgorithmException e) { +            /* From KeyManagerFactory.getInstance() or KeyManagerFactory.init() */ +            return false; +        } catch (UnrecoverableKeyException e) { +            /* From KeyManagerFactory.init() */ +            return false; +        } catch (KeyStoreException e) { +            /* From KeyManagerFactory.init() */ +            return false; +        }  +    } +     +    @Override +    public boolean setClientCertificate(CertificateWithKey cert) { +        if (cert == null || cert.isNull()) { +            emitError(null,cert + " has no useful contents"); +            return false; +        } +         +        /* Use subclass-specific method depending on what subclass it is */ +        if (cert instanceof PKCS12Certificate) { +            return setClientCertificatePKCS12((PKCS12Certificate)cert); +        } +         +        if (cert instanceof CAPICertificate) { +            return setClientCertificateCAPI((CAPICertificate)cert); +        } +         +        /* Not a type that is recognised  +         */ +        emitError(null,"setClientCertificate cannot work with "  +                + cert.getClass() + " objects"); + +        return false; +      }      @Override @@ -959,8 +1137,8 @@ public class JSSEContext extends TLSContext {          SSLContext sslContext = null;          for (String protocol:protocols) { -            try { -                sslContext = SSLContext.getInstance(protocol); +            try {            	 +				sslContext = SSLContext.getInstance(protocol);                  /* If a KeyManager has been set up in setClientCertificate()                   * then use it; otherwise the "default" implementation will be   | 
 Swift