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 |