summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/isode/stroke/tls/CAPICertificate.java80
-rw-r--r--src/com/isode/stroke/tls/java/CAPIConstants.java35
-rw-r--r--src/com/isode/stroke/tls/java/CAPIKeyManager.java110
-rw-r--r--src/com/isode/stroke/tls/java/JSSEContext.java218
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