diff options
author | Nick Hudson <nick.hudson@isode.com> | 2013-02-22 12:49:11 (GMT) |
---|---|---|
committer | Nick Hudson <nick.hudson@isode.com> | 2013-03-26 11:04:42 (GMT) |
commit | 00a381a2121fe68d318cddceed32d8bb230935ea (patch) | |
tree | 30eb70265b6eacaf2c08069ca1718024e0a525f1 /src/com | |
parent | ebef58ff180acc9d760ea0137216ef6b4f16a9b6 (diff) | |
download | stroke-00a381a2121fe68d318cddceed32d8bb230935ea.zip stroke-00a381a2121fe68d318cddceed32d8bb230935ea.tar.bz2 |
Support TLS use of certificates from CAPI keystores
This patch adds a new "CAPICertificate" class, which can be used to
configure TLS connections that use a client certificate from a Windows
CAPI keystore, including certificates on smart cards.
The JSSEContext class is updated so that "setClientCertificate()"
checks to see whether the CertificateWithKey object that it's been
given is a PKCS12Certificate or a CAPICertificate, and initializes the
appropriate type of KeyStore.
Note that the default behaviour of the KeyStore returned by SunMSCAPI
when choosing a client certificate for TLS authentication is for it to
choose the "most suitable" certificate it finds.
This "most suitable" certificate may not be the one that the user has
chosen, and in fact various certificates in CAPI are not considered by
SunMSCAPI in this case - for example, certificates issued by CAs who
don't appear in the list of acceptable CAs in the server's
CertificateRequest (RFC5246 7.4.4).
The CAPIKeyManager class provided here allows a caller to override the
default behaviour, and force the use of a specific client certificate
(whether it's "suitable" or not) based on the value specified by the
caller when the CAPICertificate object was created.
This also means that it is possible for a user to specify a particular
certificate and use that, even if SunMSCAPI would have thought a "more
suitable" one was found in CAPI.
Test-information:
Tested that P12 based TLS still works
Tested on Windows that I can specify a "CAPICertificate" which is a
reference to a certificate in the Windows keystore whose private key
is held on a smartcard, and that I am prompted to insert the card (if
necessary() and enter the PIN before the TLS handshake proceeds.
Tested on Windows that I can specify a "CAPICertificate" which is a
reference to an imported P12 file where certificate and key are in
CAPI, and the TLS handshake proceeds without asking me for a PIN
Tested that the "CAPIKeyManager" class is correctly forcing use of the
certificate specified by the user, rather than the one which would be
returned by the default SunMSCAPI implementation.
Tested that I can still use "PKCS12Certificate"s to authenticate
Tested that if I try and use a CAPICertificate on a non-Windows
platform, then I can't authenticate, and get errors emitted from Stroke
complaining of "no such provider: SunMSCAPI"
Change-Id: Iff38e459f60c0806755820f6989c516be37cbf08
Signed-off-by: Nick Hudson <nick.hudson@isode.com>
Diffstat (limited to 'src/com')
-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 |