diff options
author | Nick Hudson <nick.hudson@isode.com> | 2012-03-08 10:16:55 (GMT) |
---|---|---|
committer | Kevin Smith <git@kismith.co.uk> | 2012-03-08 21:51:02 (GMT) |
commit | c5392b36c368ebdca2e8ab356eb0d1fb0d36a5cb (patch) | |
tree | 38c40c5661bce2b5655d91e6e7dadcc3b536fac5 /src | |
parent | 0470264fd4f9e7e73d1b655dc680e5ca7c10513c (diff) | |
download | stroke-c5392b36c368ebdca2e8ab356eb0d1fb0d36a5cb.zip stroke-c5392b36c368ebdca2e8ab356eb0d1fb0d36a5cb.tar.bz2 |
Implement "CertificateWithKey" and add support for setting client certificates
This change provides the functionality to allow clients to specify a
PKCS#12 file containing client certificate/key for use when starting
TLS sessions.
The PKCS12Certificate class now subclasses "CertificateWithKey"
(matching the Swiften implementation).
Swiften also has "CAPICertificate", which is another subclass of
CertificateWithKey. This has not been provided in this patch.
From a client's point of view, all that's necessary to specify a
certificate to be used for TLS is to do something like
CertificateWithKey myCert = new PKCS12Certificate(
"/home/fred/myp12file.p12",
"secret".toCharArray());
coreClient.setCertificate(myCert);
before calling "CoreClient.connect".
Matching the Swiften functionality, constructing a new
PKCS12Certificate does not actually perform validation of the P12
file/passphrase; that takes place when the p12 file is used.
There is limited scope for returning to the caller errors describing
possible problems, but JSSEContext uses the "emitError" method which
does maintain error information, which is available in a debugger, or
from the JSSEContext.toString() method.
Test-information:
Set up an M-Link server with TLS verified that
- when I specify a client certificate with suitable SAN, the client
sends it and the server reports authentication using the certificate
- when I specify a client certificate without a suitable SAN, the
client sends it but the server rejects it
Diffstat (limited to 'src')
-rw-r--r-- | src/com/isode/stroke/client/CoreClient.java | 9 | ||||
-rw-r--r-- | src/com/isode/stroke/session/SessionStream.java | 7 | ||||
-rw-r--r-- | src/com/isode/stroke/streamstack/TLSLayer.java | 6 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/CertificateWithKey.java | 24 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/PKCS12Certificate.java | 79 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/TLSContext.java | 6 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/java/JSSEContext.java | 104 |
7 files changed, 208 insertions, 27 deletions
diff --git a/src/com/isode/stroke/client/CoreClient.java b/src/com/isode/stroke/client/CoreClient.java index c01d57a..49b6df7 100644 --- a/src/com/isode/stroke/client/CoreClient.java +++ b/src/com/isode/stroke/client/CoreClient.java @@ -31,6 +31,7 @@ import com.isode.stroke.signals.Slot; import com.isode.stroke.signals.Slot1; import com.isode.stroke.tls.CertificateTrustChecker; import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.CertificateWithKey; import com.isode.stroke.tls.PKCS12Certificate; import com.isode.stroke.tls.PlatformTLSFactories; @@ -169,8 +170,8 @@ public class CoreClient { assert (sessionStream_ == null); sessionStream_ = new BasicSessionStream(StreamType.ClientStreamType, connection_, payloadParserFactories_, payloadSerializers_, tlsFactories.getTLSContextFactory(), networkFactories.getTimerFactory(), eventLoop_); - if (certificate_ != null && !certificate_.isEmpty()) { - sessionStream_.setTLSCertificate(new PKCS12Certificate(certificate_, password_)); + if (certificate_ != null && !certificate_.isNull()) { + sessionStream_.setTLSCertificate(certificate_); } sessionStreamDataReadConnection_ = sessionStream_.onDataRead.connect(new Slot1<String>() { @@ -228,7 +229,7 @@ public class CoreClient { } } - public void setCertificate(String certificate) { + public void setCertificate(CertificateWithKey certificate) { certificate_ = certificate; } @@ -454,7 +455,7 @@ public class CoreClient { private Connection connection_; private BasicSessionStream sessionStream_; private ClientSession session_; - private String certificate_; + private CertificateWithKey certificate_; private boolean disconnectRequested_; private ClientOptions options; private CertificateTrustChecker certificateTrustChecker; diff --git a/src/com/isode/stroke/session/SessionStream.java b/src/com/isode/stroke/session/SessionStream.java index ee17a09..5dbb0fc 100644 --- a/src/com/isode/stroke/session/SessionStream.java +++ b/src/com/isode/stroke/session/SessionStream.java @@ -15,6 +15,7 @@ import com.isode.stroke.signals.Signal; import com.isode.stroke.signals.Signal1; import com.isode.stroke.tls.Certificate; import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.CertificateWithKey; import com.isode.stroke.tls.PKCS12Certificate; public abstract class SessionStream { @@ -60,7 +61,7 @@ public abstract class SessionStream { public abstract void resetXMPPParser(); - public void setTLSCertificate(PKCS12Certificate cert) { + public void setTLSCertificate(CertificateWithKey cert) { certificate = cert; } @@ -80,7 +81,7 @@ public abstract class SessionStream { public final Signal onTLSEncrypted = new Signal(); public final Signal1<String> onDataRead = new Signal1<String>(); public final Signal1<String> onDataWritten = new Signal1<String>(); - protected PKCS12Certificate getTLSCertificate() { + protected CertificateWithKey getTLSCertificate() { return certificate; } @@ -94,5 +95,5 @@ public abstract class SessionStream { "; " + (hasTLSCertificate() ? "has" : "no") + " certificate"; } - private PKCS12Certificate certificate; + private CertificateWithKey certificate; } diff --git a/src/com/isode/stroke/streamstack/TLSLayer.java b/src/com/isode/stroke/streamstack/TLSLayer.java index 7051cd3..1f213fc 100644 --- a/src/com/isode/stroke/streamstack/TLSLayer.java +++ b/src/com/isode/stroke/streamstack/TLSLayer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -14,7 +14,7 @@ import com.isode.stroke.signals.Signal; import com.isode.stroke.signals.Slot1; import com.isode.stroke.tls.Certificate; import com.isode.stroke.tls.CertificateVerificationError; -import com.isode.stroke.tls.PKCS12Certificate; +import com.isode.stroke.tls.CertificateWithKey; import com.isode.stroke.tls.TLSContext; import com.isode.stroke.tls.TLSContextFactory; @@ -50,7 +50,7 @@ public class TLSLayer extends StreamLayer { context.handleDataFromNetwork(data); } - public boolean setClientCertificate(PKCS12Certificate certificate) { + public boolean setClientCertificate(CertificateWithKey certificate) { return context.setClientCertificate(certificate); } diff --git a/src/com/isode/stroke/tls/CertificateWithKey.java b/src/com/isode/stroke/tls/CertificateWithKey.java new file mode 100644 index 0000000..9787add --- /dev/null +++ b/src/com/isode/stroke/tls/CertificateWithKey.java @@ -0,0 +1,24 @@ +/* Copyright (c) 2012, 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; +/** + * + */ +public abstract class CertificateWithKey { + public + CertificateWithKey() { + } + + + public abstract boolean isNull(); + + +} diff --git a/src/com/isode/stroke/tls/PKCS12Certificate.java b/src/com/isode/stroke/tls/PKCS12Certificate.java index 0a45f94..06b6b91 100644 --- a/src/com/isode/stroke/tls/PKCS12Certificate.java +++ b/src/com/isode/stroke/tls/PKCS12Certificate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Isode Limited, London, England. + * Copyright (c) 2011-2012 Isode Limited, London, England. * All rights reserved. */ /* @@ -9,20 +9,52 @@ package com.isode.stroke.tls; import com.isode.stroke.base.ByteArray; +import com.isode.stroke.base.NotNull; -public class PKCS12Certificate { +public class PKCS12Certificate extends CertificateWithKey { public PKCS12Certificate() { } - public PKCS12Certificate(String filename, String password) { - password_ = password; + /** + * Construct a new object. + * @param filename the name of the P12 file, must not be null. + * @param password the password for the P12 file. Must not be null, + * but may be empty if no password is to be used. + */ + public PKCS12Certificate(String filename, char[] password) { + + NotNull.exceptIfNull(filename,"filename"); + NotNull.exceptIfNull(password,"password"); + filename_ = filename; + password_ = new char[password.length]; + System.arraycopy(password,0,password_,0,password.length); + data_ = new ByteArray(); data_.readFromFile(filename); } public boolean isNull() { return data_.isEmpty(); } + + public boolean isPrivateKeyExportable() { + /////Hopefully a PKCS12 is never missing a private key + return true; + } + + /** + * This returns the name of the P12 file. + * @return the P12 filename, never null. + */ + public String getCertStoreName() { + return filename_; + } + + public String getCertName() { + /* TODO */ + return null; + } + public ByteArray getData() { return data_; @@ -32,9 +64,44 @@ public class PKCS12Certificate { data_ = data; } - public String getPassword() { + /** + * Returns a reference to the password in this object. If {@link #reset()} + * has been called, then the method will return an empty array. + * @return the password for this object. + */ + public char[] getPassword() { return password_; } + @Override + public String toString() { + return "PKCS12Certificate based on file " + filename_; + } + + /** + * This method may be used once the PKCS12Certificate is no longer + * required, and will attempt to clear the memory containing the + * password in this object. After calling this method, you should + * not expect this object to be usable for subsequent authentication. + * + * <p>Note that this operation does <em>NOT</em> guarantee that all traces + * of the password will have been removed from memory. + */ + public void reset() { + if (password_ != null) { + for (int i=0; i<password_.length; i++) { + password_[i] = 'x'; + } + } + password_ = new char[] {}; + + } + + @Override + protected void finalize() { + reset(); + } + private ByteArray data_; - private String password_; + private char[] password_; + private String filename_; } diff --git a/src/com/isode/stroke/tls/TLSContext.java b/src/com/isode/stroke/tls/TLSContext.java index 49ec307..ec39a3b 100644 --- a/src/com/isode/stroke/tls/TLSContext.java +++ b/src/com/isode/stroke/tls/TLSContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, Isode Limited, London, England. + * Copyright (c) 2011-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -14,10 +14,10 @@ import com.isode.stroke.signals.Signal; import com.isode.stroke.signals.Signal1; public abstract class TLSContext { - //See SSLEngine for real implementation when the time comes + public abstract void connect(); - public abstract boolean setClientCertificate(PKCS12Certificate cert); + public abstract boolean setClientCertificate(CertificateWithKey cert); public abstract void handleDataFromNetwork(ByteArray data); public abstract void handleDataFromApplication(ByteArray data); diff --git a/src/com/isode/stroke/tls/java/JSSEContext.java b/src/com/isode/stroke/tls/java/JSSEContext.java index 887a2b7..c8e0640 100644 --- a/src/com/isode/stroke/tls/java/JSSEContext.java +++ b/src/com/isode/stroke/tls/java/JSSEContext.java @@ -10,11 +10,17 @@ package com.isode.stroke.tls.java; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateNotYetValidException; @@ -23,6 +29,8 @@ import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; @@ -34,6 +42,7 @@ import com.isode.stroke.base.ByteArray; import com.isode.stroke.tls.Certificate; import com.isode.stroke.tls.CertificateVerificationError; import com.isode.stroke.tls.CertificateVerificationError.Type; +import com.isode.stroke.tls.CertificateWithKey; import com.isode.stroke.tls.PKCS12Certificate; import com.isode.stroke.tls.TLSContext; @@ -110,7 +119,6 @@ public class JSSEContext extends TLSContext { } private void doSetup() throws SSLException { - SSLContext sslContext = getSSLContext(); if (sslContext == null) { @@ -592,11 +600,81 @@ public class JSSEContext extends TLSContext { @Override - public boolean setClientCertificate(PKCS12Certificate cert) { - /* TODO: NYI. - * It's possible this is going to change as a result of Alexey's work - * so will leave for now + 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"); + return false; + } + + /* Get a reference that can be used in any error messages */ + File p12File = new File(p12.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 + * mean that the operation is regarded as having failed. + * If it works, then "myKeyManager_" will be initialised for use + * by any subsequent call to getSSLContext() */ + KeyStore keyStore = null; + KeyManagerFactory kmf = null; + + try { + keyStore = KeyStore.getInstance("PKCS12"); + kmf = KeyManagerFactory.getInstance("SunX509"); + + /* The PKCS12Certificate object has read the file contents already */ + ByteArray ba = p12.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()); + + KeyManager[] keyManagers = kmf.getKeyManagers(); + if (keyManagers == null || keyManagers.length == 0) { + emitError(null, "Unable to get KeyManager for SunX509"); + return false; + } + + /* Just take the first one (there probably will only be one) */ + myKeyManager_ = keyManagers[0]; + + return true; + + } + catch (KeyStoreException e) { + emitError(e, "Cannot get PKCS12 KeyStore"); + } + catch (NoSuchAlgorithmException e) { + emitError(e, "Unable to initialise KeyStore from " + p12File); + } + catch (CertificateException e) { + emitError(e, "Unable to load certificates from " + p12File); + } + catch (IOException e) { + if (e.getCause() != null && e.getCause() instanceof UnrecoverableKeyException) { + emitError(e, "Password incorrect for " +p12File); + } + else { + emitError(e, "Unable to read " + p12File); + } + } + catch (UnrecoverableKeyException e) { + emitError(e, "Unable to initialise KeyStore from " + p12File); + } + + /* Fall through here after any exception */ return false; } @@ -900,6 +978,9 @@ public class JSSEContext extends TLSContext { private final Logger logger_ = Logger.getLogger(this.getClass().getName()); + + private KeyManager myKeyManager_ = null; + /** * Set up the SSLContext and JavaTrustManager that will be used for this * JSSEContext. @@ -949,15 +1030,22 @@ public class JSSEContext extends TLSContext { GeneralSecurityException lastException = null; SSLContext sslContext = null; - for (String protocol:protocols) { try { sslContext = SSLContext.getInstance(protocol); - /* That worked */ + /* If a KeyManager has been set up in setClientCertificate() + * then use it; otherwise the "default" implementation will be + * used, which will be sufficient for starting TLS with no + * client certificate + */ + KeyManager[] keyManagers = null; + if (myKeyManager_ != null) { + keyManagers = new KeyManager[] { myKeyManager_ }; + } try { sslContext.init( - null, /* KeyManager[] */ + keyManagers, /* KeyManager[] */ tm, /* TrustManager[] */ null); /* SecureRandom */ |