summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNick Hudson <nick.hudson@isode.com>2012-01-18 14:22:56 (GMT)
committerKevin Smith <git@kismith.co.uk>2012-02-13 15:12:46 (GMT)
commit9d6f73f13b981fb9430765b171d7b419cbe632cc (patch)
tree5bfdbdfeaa9dc3dc3ff87e92be2612f7aebb03b3 /src/com/isode/stroke/tls/java/JavaCertificate.java
parent12740e2b70c48e478af53de31624c388e1e89e0f (diff)
downloadstroke-9d6f73f13b981fb9430765b171d7b419cbe632cc.zip
stroke-9d6f73f13b981fb9430765b171d7b419cbe632cc.tar.bz2
Initial implementation of TLS support
Note that TLS won't be enabled with this patch unless you uncomment the change in PlatformTLSFactories. With that comment removed, then a new CoreClient session will attempt to negotiate TLS if the server supports it. Further changes are required to support this properly, as there appears not to be comprehensive support in the CoreClient class for dealing with situations when the server's certificate is not acceptable. There's also no support yet for setting up client certificates. Further changes will also be needed (see below) to support full parsing of subjectAltNames from server certificates. Significant changes are as follows - TLSProceed - FIXME comments removed - JavaConnection - changed so that it reads bytes from the socket's InputStream, rather than reading chars and then constructing a String out of them from which a byte array is then extracted. While this seemed to work for non-binary data (e.g. non-encrypted XMPP sessions), it breaks when you start sending binary (i.e. TLS) data. - JavaTLSConnectionFactory - implemented - PlatformTLSFactories - By having this return a JSSEContextFactory, then this will cause the client to try TLS if possible. But because other changes are needed to make this work properly, the current code still returns null. - JSSEContext - new class which uses an SSLEngine to handle TLS handshake and subsequent encryption/decryption. This is the main substance of the SSL implementation Note the "hack" in here to cope with SSLEngine requiring that some data be sent from the application before it will do a TLS handshake - JSSEContextFactory - just creates JSSEContexts - JavaCertificate - this wraps an X509Certificate and does *some* of the parsing of a certificate to look for stuff that is expected when verifying an XMPP server certificate (RFC 6120 and RFC 6125). Note that the JDK classes for parsing certificates don't provide an easy way to decode "OTHER" subjectAltNames, and so this implementation does not find XMPP or SRV subjectaltnames from the server certificate. This will need extra work. - JavaTrustManager - obtains the server certificate from the TLS handshake and verifies it. Currently the only verification done is to check that it's in date. More work will be needed to perform proper validation - Where necessary, Remko's copyright comments were changed from GNU to "All rights reserved". Isode copyright notices updated to "2012" Test-information: Set up XMPP server with its own certificate, and checked that TLS gets negotiated and starts OK (provided the server cert contains e.g. a DNS subjectAltName matching its own name). Subsequent operation appears to be as expected.
Diffstat (limited to 'src/com/isode/stroke/tls/java/JavaCertificate.java')
-rw-r--r--src/com/isode/stroke/tls/java/JavaCertificate.java331
1 files changed, 331 insertions, 0 deletions
diff --git a/src/com/isode/stroke/tls/java/JavaCertificate.java b/src/com/isode/stroke/tls/java/JavaCertificate.java
new file mode 100644
index 0000000..5b326b9
--- /dev/null
+++ b/src/com/isode/stroke/tls/java/JavaCertificate.java
@@ -0,0 +1,331 @@
+/* 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.java;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.security.auth.x500.X500Principal;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.tls.Certificate;
+
+/**
+ * This class wraps a java.security.cert.X509Certificate to fulfil
+ * the requirements of the com.isode.stroke.tls.Certificate class.
+ */
+public class JavaCertificate extends Certificate {
+ private X509Certificate x509Certificate;
+
+ // There's no ASN.1 help for this in standard Java SDK so for the
+ // moment we'll hard-code in the values
+ /**
+ * ASN.1 encoded representation of OID "1.3.6.1.5.5.7.8.5"
+ */
+ protected static final byte[] ENCODED_ID_ON_XMPPADD_OID =
+ new byte[] { 0x06, 0x08, 0x21, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x05 };
+
+ /**
+ * ASN.1 encoded representation of OID "1.3.6.1.5.5.7.8.7"
+ */
+ protected static final byte[] ENCODED_ID_ON_DNSSRV_OID =
+ new byte[] { 0x06, 0x08, 0x21, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x07 };
+
+
+ private static enum GeneralNameType {
+ OTHERNAME(0),
+ RFC822NAME(1),
+ DNSNAME(2),
+ X400ADDRESS(3),
+ DIRECTORYNAME(4),
+ EDIPARTYNAME(5),
+ UNIFORMRESOURCEIDENTIFIER(6),
+ IPADDRESS(7),
+ REGISTEREDID(8);
+
+ private int val;
+ private GeneralNameType(int v) {
+ this.val = v;
+ }
+ static GeneralNameType getValue(int x) {
+ for (GeneralNameType g:values()) {
+ if (g.val == x) {
+ return g;
+ }
+ }
+ return null;
+ }
+ }
+
+ private void processSubjectAlternativeNames() {
+
+ Collection<List<?>> sans = null;
+
+ try {
+ // Process subject alternative names. This returns a sequence
+ // of general names
+ sans = x509Certificate.getSubjectAlternativeNames();
+ }
+ catch (CertificateParsingException e) {
+ // Leave all the subjectAltNames unparsed
+ return;
+ }
+
+ if (sans == null) {
+ // No subjectAltNames
+ return;
+ }
+
+ for (List<?> san : sans) {
+ // Each general name element contains an Integer representing the
+ // name type, and either a String or byte array containing the
+ // value
+ Integer type = (Integer)san.get(0);
+ GeneralNameType nameType = GeneralNameType.getValue(type.intValue());
+ switch (nameType) {
+ case DNSNAME: // String
+ dnsNames_.add((String)san.get(1));
+ break;
+ case OTHERNAME: // DER
+ byte[] encoded = (byte[])san.get(1);
+ // TODO: what you get here is something like
+ // LBER_SEQUENCE, length = 31 :
+ // tag : 0x06 (LBER_OID), length = 8
+ // value = 1.3.6.1.5.5.7.8.5 (i.e. ID_ON_XMPPADDR_OID)
+ // bytes = [00]=2B [01]=06 [02]=01 [03]=05 [04]=05 [05]=07 [06]=08 [07]=05
+ //
+ // CONTEXT[0], length = 19 :
+ // CONTEXT[0], length = 17 :
+ // tag : 0x0C UNIVERSAL[12] primitive, length = 15
+ // value = "funky.isode.net"
+ // bytes = [00]=66 [01]=75 [02]=6E [03]=6B [04]=79 [05]=2E [06]=69 [07]=73
+ // [08]=6F [09]=64 [0A]=65 [0B]=2E [0C]=6E [0D]=65 [0E]=74
+ //
+ // And a corresponding thing for a DNSSRV SAN.
+ // However, there's no general ASN.1 decoder in the standard
+ // java library, so we will have to implement our own. For
+ // now, we ignore these values.
+
+ break;
+ case DIRECTORYNAME: // String
+ case IPADDRESS: // String
+ case REGISTEREDID: // String representation of an OID
+ case RFC822NAME: // String
+ case UNIFORMRESOURCEIDENTIFIER: // String
+ case EDIPARTYNAME: // DER
+ case X400ADDRESS: // DER
+ default:
+ // Other types of subjectalt names are ignored
+ break;
+ }
+
+ }
+
+ }
+
+ /**
+ * Construct a new JavaCertificate by parsing an X509Certificate
+ *
+ * @param x509Cert an X509Certificate, which must not be null
+ */
+ public JavaCertificate(X509Certificate x509Cert)
+ {
+ if (x509Cert == null) {
+ throw new NullPointerException("x509Cert must not be null");
+ }
+ x509Certificate = x509Cert;
+
+ dnsNames_ = new ArrayList<String>();
+ srvNames_ = new ArrayList<String>();
+ xmppNames_ = new ArrayList<String>();
+
+ processSubjectAlternativeNames();
+
+
+ }
+
+ /**
+ * Return a reference to the X509Certificate object that this
+ * JavaCertificate is wrapping.
+ *
+ * @return an X509Certificate (won't be null).
+ */
+ public X509Certificate getX509Certificate() {
+ return x509Certificate;
+ }
+
+ /**
+ * Gets a String representation of the certificate subjectname
+ *
+ * @return certificate subject name, e.g. "CN=harry,O=acme"
+ */
+ @Override
+ public String getSubjectName() {
+ return x509Certificate.getSubjectX500Principal().toString();
+ }
+
+ /**
+ * Returns a list of all the commonname values from the certificate's
+ * subjectDN. For example, if the subjectDN is "CN=fred,O=acme,CN=bill"
+ * then the list returned would contain "fred" and "bill"
+ *
+ * @return a list containing the Strings representing common name values
+ * in the server certificate's subjectDN. Will never return null, but may
+ * return an empty list.
+ */
+ @Override
+ public List<String> getCommonNames() {
+ ArrayList<String> result = new ArrayList<String>();
+
+
+ // There isn't a convenient way to extract commonname values from
+ // the certificate's subject DN (short of parsing the encoded value
+ // ourselves). So instead, we get a String version, ensuring that
+ // any CN values have a prefix we can recognize (we could probably
+ // rely on "CN" but this allows us to have a more distinctive value)
+
+ X500Principal p = x509Certificate.getSubjectX500Principal();
+ Map<String, String> cnMap = new HashMap<String, String>();
+
+ // Request that the returned String will use our label for any values
+ // with the commonName OID
+ cnMap.put(cnOID, cnLabel);
+ String s = p.getName("RFC2253",cnMap);
+
+ String cnPrefix = cnLabel + "=";
+
+ int x = s.indexOf(cnPrefix);
+ if (x == -1) {
+ return result; // No CN values to add
+ }
+
+ // Crude attempt to split, noting that this may result in values
+ // that contain an escaped comma being chopped between more than one
+ // element, so we need to go through this subsequently and handle that..
+ String[] split=s.split(",");
+
+ boolean inQuote = false;
+ boolean escape = false;
+
+ int e = 0;
+ String field = "";
+
+ while (e < split.length) {
+ String element = split[e];
+ int quoteCount = 0;
+ for (int i=0; i<element.length(); i++) {
+ char c = element.charAt(i);
+ if (c == '"') {
+ quoteCount++;
+ }
+ }
+ escape = (element.endsWith("\\"));
+
+ inQuote = ((quoteCount % 2) == 1);
+ if (!inQuote && !escape) {
+ // We got to the end of a field
+ field += element;
+ if (field.startsWith(cnPrefix)) {
+ result.add(field.substring(cnPrefix.length()));
+ }
+ field = "";
+ }
+ else {
+ // the split has consumed a comma that was part of a quoted
+ // String.
+ field = field + element + ",";
+ }
+ e++;
+ }
+ return result;
+ }
+
+ /**
+ * Returns a list of all the SRV values held in "OTHER" type subjectAltName
+ * fields in the server's certificate.
+ *
+ * @return a list containing the Strings representing SRV subjectAltName
+ * values from the server certificate. Will never return null, but may
+ * return an empty list.
+ */
+ @Override
+ public List<String> getSRVNames() {
+ // TODO: At the moment it will always return
+ // an empty list -see processSubjectAlternativeNames()
+ return srvNames_;
+ }
+
+ /**
+ * Returns a list of all the DNS subjectAltName values from the server's
+ * certificate.
+ *
+ * @return a list containing the Strings representing DNS subjectAltName
+ * values from the server certificate. Will never return null, but may
+ * return an empty list.
+ */
+ @Override
+ public List<String> getDNSNames() {
+ return dnsNames_;
+ }
+
+ /**
+ * Returns a list of all the XMPP values held in "OTHER" type subjectAltName
+ * fields in the server's certificate.
+ *
+ * @return a list containing the Strings representing XMPP subjectAltName
+ * values from the server certificate. Will never return null, but may
+ * return an empty list.
+ */
+ @Override
+ public List<String> getXMPPAddresses() {
+ // TODO: At the moment it will always return
+ // an empty list -see processSubjectAlternativeNames()
+ return xmppNames_;
+ }
+
+ /**
+ * Return the encoded representation of the certificate
+ *
+ * @return the DER encoding of the certificate. Will return null if
+ * the certificate is not valid.
+ */
+ @Override
+ public ByteArray toDER() {
+ // TODO Auto-generated method stub
+ try {
+ byte[] r = x509Certificate.getEncoded();
+ return new ByteArray(r);
+ }
+ catch (CertificateEncodingException e) {
+ return null;
+ }
+ }
+
+ private List<String> dnsNames_ = null;
+ private List<String> srvNames_ = null;
+ private List<String> xmppNames_ = null;
+
+ /**
+ * OID for commonName
+ */
+ private final static String cnOID = "2.5.4.3";
+ /**
+ * String to be used to identify commonName values in a DN.
+ */
+ private final static String cnLabel = "COMMONNAME";
+
+}