summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNick Hudson <nick.hudson@isode.com>2012-02-13 12:24:53 (GMT)
committerKevin Smith <git@kismith.co.uk>2012-02-14 10:06:42 (GMT)
commitf5e722a7f28f3407600e023700739ff3b9cc83c2 (patch)
tree7f0d9054c99f5ddc4dbbaa453a74e3cc09bfe736
parentaeab1723a7efdd2eb989ff29afc7461ee4d27936 (diff)
downloadstroke-f5e722a7f28f3407600e023700739ff3b9cc83c2.zip
stroke-f5e722a7f28f3407600e023700739ff3b9cc83c2.tar.bz2
Add parsing of XMPP and SRV subjectAltNames to JavaCertificate
The JavaCertificate class functionality copies that found in Swiften's OpenSSLCertificate class. One of the things that the class needs to do is make available lists of SRV and XMPP names which are contained in the certificate's subjectAltName extensions. However, unlike OpenSSL, the standard Java classes don't provide support for parsing the different types of subjectAltNames that we want - what Java provides is a method to return the DER encoded values, and so this change adds functions to the JavaCertificate class to parse these values to extract Strings corresponding to either XMPP or SRV subjectAltNames. Also addressed a couple of comments from a previous patch. Test-information: Tested with certificate that has subjectAltNames for - Service Name _xmpp-client.funky.isode.net - Service Name _xmpp-server.funky.isode.net - XMPP address funky.isode.net - DNS Name funky.isode.net And verified that all of these are parsed and made available with the relevant methods. Verified that a certificate which does NOT contain any of these subjectAltNames is parsed with no errors (and the code reports no matching subjectAltNames) Signed-off-by: Nick Hudson <nick.hudson@isode.com>
-rw-r--r--src/com/isode/stroke/tls/java/JavaCertificate.java365
1 files changed, 284 insertions, 81 deletions
diff --git a/src/com/isode/stroke/tls/java/JavaCertificate.java b/src/com/isode/stroke/tls/java/JavaCertificate.java
index 5b326b9..ccfc568 100644
--- a/src/com/isode/stroke/tls/java/JavaCertificate.java
+++ b/src/com/isode/stroke/tls/java/JavaCertificate.java
@@ -1,11 +1,5 @@
/* 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;
@@ -22,6 +16,7 @@ import java.util.Map;
import javax.security.auth.x500.X500Principal;
import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.base.NotNull;
import com.isode.stroke.tls.Certificate;
/**
@@ -31,20 +26,6 @@ import com.isode.stroke.tls.Certificate;
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),
@@ -71,29 +52,207 @@ public class JavaCertificate extends Certificate {
}
}
+ /**
+ * Returns the computed value of a "length" field for a data structure, i.e.
+ * the size of the data itself, not including its enclosing tag/length.
+ *
+ * @param byteStream an array of bytes containing ASN.1 encoded data
+ *
+ * @param startPos the offset where the "tag" field is to be found
+ *
+ * @return length value
+ *
+ * @throws ArrayIndexOutOfBoundsException if <em>byteStream</em> is
+ * exhausted (most likely it's not valid ASN.1)
+ */
+ private static int getLengthField(byte[] byteStream, int startPos)
+ throws ArrayIndexOutOfBoundsException
+ {
+ int offset = startPos + 1; // skip over "tag"
+
+ /* Since Java treats "byte" values as signed, we use this variable as
+ * a temporary in which a signed value of a given byte may be stored
+ */
+ int unsignedByte;
+
+ /* Extract the "length" byte */
+ unsignedByte = (byteStream[offset] & 0xff);
+
+ /* If it's a "short" length, it will be less than 0x80, and the "length" of
+ * this field can be computed using the value of the length byte
+ */
+ if (unsignedByte < 0x80) {
+ return (unsignedByte);
+ }
+
+ /* Otherwise, the first byte contains information about how the count
+ * of octets that contain length information
+ */
+ int result = 0;
+ offset++; /* advance past the "length-count" byte */
+
+ /* Work out how many octets of length information there are */
+ int numLengthOctets = unsignedByte - 0x80;
+
+ /* Now work out actual "length" */
+ for (int i=0; i<numLengthOctets; i++) {
+ unsignedByte = (byteStream[(i+offset)] & 0xff);
+ result = (result << 8) + unsignedByte;
+ }
+
+ return result;
+
+ }
+ /**
+ * Computes the offset to the "value" in a TLV sequence.
+ *
+ * @param byteStream an array of octets containing BER encoded data.
+ *
+ * @param tagOffset the offset to the "tag" octet inside byteStream.
+ *
+ * @return the offset to the value for the data structure described
+ * by the tag.
+ *
+ * @throws ArrayIndexOutOfBoundsException if <em>byteStream</em> is
+ * exhausted (most likely it's not valid ASN.1)
+ */
+ private final static int getValueOffset(byte[] byteStream, int tagOffset)
+ throws ArrayIndexOutOfBoundsException
+ {
+ int unsignedByte;
+
+ /* Extract the "length" byte */
+ unsignedByte = (byteStream[(tagOffset+1)] & 0xff);
+
+ /*
+ * If it's a "short" length, it will be less than 0x80, and the
+ * "length" of this field can be computed using the value of the
+ * length byte
+ */
+ if (unsignedByte < 0x80) {
+ return (tagOffset + 2); /* add 1 for tag and 1 for length */
+ }
+
+ /*
+ * The value is greater than 0x80, which means that the number of octets
+ * containing the length field can be found by subtracting 0x80 from the
+ * length byte
+ */
+ int numOctets = unsignedByte - 0x80;
+
+ /* Work out where the data must start */
+ return (tagOffset + numOctets + 2);
+ }
+
+ /**
+ * Look for the encoded OID of a value in stream of bytes, and return the
+ * corresponding String that's attached to it
+ *
+ * @param encodedData data stream that may contain the OID
+ *
+ * @param encodedOID the encoded value of the OID to look for
+ *
+ * @return the decoded String corresponding to the requested OID, or null
+ * if it cannot be found, or if an error occurs when decoding
+ */
+ private static String getStringValueForOID(byte[] encodedData, byte[] encodedOID) {
+ /* The "encodedData" comes from X509Certificate.getSubjectAlternativeNames,
+ * which will return something like this:
+ * 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 (LBER_UTF8STRING), 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
+ *
+ * Note that the above isn't what you see inside the certificate;
+ * specifically the "CONTEXT[0], length = 19" wrapper appears to have
+ * been synthesized by the Java method.
+ * So this method will look for an embedded UTF8STRING (XMPP) or
+ * IA5STRING (SRVName) that follows the specified OID, regardless of
+ * how many levels of embedding exist.
+ */
+ try {
+ /* Expect the whole thing is a SEQUENCE */
+ if (encodedData[0] != SEQUENCE_TAG) {
+ return null;
+ }
+ int length = getLengthField(encodedData, 0);
+ int oidOffset = getValueOffset(encodedData, 0);
+
+ /* Now we expect the encoded OID */
+ for (int i=0; i<encodedOID.length; i++) {
+ int j = oidOffset + i;
+ if (j >= length) {
+ /* Gone beyond the end of encoded data */
+ return null;
+ }
+ if (encodedOID[i] != encodedData[j]) {
+ /* Found a mismatch in encoded OID */
+ return null;
+ }
+ }
+
+ /* Got this far, so the OID matches */
+ int valueOffset = oidOffset + encodedOID.length;
+
+ /* Now look for a primitive String tag */
+ int curPos = getValueOffset(encodedData, valueOffset);
+ while (curPos < encodedData.length) {
+ byte tag = encodedData[curPos];
+ int componentPos = getValueOffset(encodedData, curPos);
+ int componentLength = getLengthField(encodedData, curPos);
+ if (tag == UTF8STRING_TAG || tag == IA5STRING_TAG) {
+ /*We found a String */
+ String result = new String(encodedData,componentPos, componentLength);
+ return result;
+ }
+ /* It wasn't a String. Move position to next tag */
+ curPos = componentPos;
+ }
+ /* Got through all encoded data without finding a String */
+ return null;
+ }
+ catch (ArrayIndexOutOfBoundsException e) {
+ /* This probably means the data is not properly encoded
+ * ASN.1, or at any rate not in the structure we expected, and
+ * we've blindly followed "length" fields which aren't
+ * really lengths and fallen off the end of the array.
+ */
+ return null;
+ }
+ }
+
private void processSubjectAlternativeNames() {
Collection<List<?>> sans = null;
try {
- // Process subject alternative names. This returns a sequence
- // of general names
+ /* Process subject alternative names. This returns a sequence
+ * of general names
+ */
sans = x509Certificate.getSubjectAlternativeNames();
}
catch (CertificateParsingException e) {
- // Leave all the subjectAltNames unparsed
+ /* Leave all the subjectAltNames unparsed */
return;
}
if (sans == null) {
- // No subjectAltNames
+ /* 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
+ /* 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) {
@@ -101,40 +260,31 @@ public class JavaCertificate extends Certificate {
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.
+ byte[] encoding = (byte[])san.get(1);
+ String xmpp = getStringValueForOID(encoding, ENCODED_ID_ON_XMPPADD_OID);
+ if (xmpp != null) {
+ xmppNames_.add(xmpp);
+ break;
+ }
+ String srv = getStringValueForOID(encoding, ENCODED_ID_ON_DNSSRV_OID);
+ if (srv != null) {
+ srvNames_.add(srv);
+ break;
+ }
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
+ 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
+ /* Other types of subjectalt names are ignored */
break;
}
-
}
-
}
/**
@@ -144,18 +294,15 @@ public class JavaCertificate extends Certificate {
*/
public JavaCertificate(X509Certificate x509Cert)
{
- if (x509Cert == null) {
- throw new NullPointerException("x509Cert must not be null");
- }
+ NotNull.exceptIfNull(x509Cert,"x509Cert");
+
x509Certificate = x509Cert;
dnsNames_ = new ArrayList<String>();
srvNames_ = new ArrayList<String>();
xmppNames_ = new ArrayList<String>();
- processSubjectAlternativeNames();
-
-
+ processSubjectAlternativeNames();
}
/**
@@ -191,18 +338,20 @@ public class JavaCertificate extends Certificate {
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)
-
+ /*
+ * 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
+ /*
+ * 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);
@@ -213,9 +362,11 @@ public class JavaCertificate extends Certificate {
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..
+ /*
+ * 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;
@@ -237,7 +388,7 @@ public class JavaCertificate extends Certificate {
inQuote = ((quoteCount % 2) == 1);
if (!inQuote && !escape) {
- // We got to the end of a field
+ /* We got to the end of a field */
field += element;
if (field.startsWith(cnPrefix)) {
result.add(field.substring(cnPrefix.length()));
@@ -245,8 +396,9 @@ public class JavaCertificate extends Certificate {
field = "";
}
else {
- // the split has consumed a comma that was part of a quoted
- // String.
+ /* the split has consumed a comma that was part of a quoted
+ * String.
+ */
field = field + element + ",";
}
e++;
@@ -264,8 +416,6 @@ public class JavaCertificate extends Certificate {
*/
@Override
public List<String> getSRVNames() {
- // TODO: At the moment it will always return
- // an empty list -see processSubjectAlternativeNames()
return srvNames_;
}
@@ -292,8 +442,6 @@ public class JavaCertificate extends Certificate {
*/
@Override
public List<String> getXMPPAddresses() {
- // TODO: At the moment it will always return
- // an empty list -see processSubjectAlternativeNames()
return xmppNames_;
}
@@ -305,7 +453,6 @@ public class JavaCertificate extends Certificate {
*/
@Override
public ByteArray toDER() {
- // TODO Auto-generated method stub
try {
byte[] r = x509Certificate.getEncoded();
return new ByteArray(r);
@@ -315,6 +462,35 @@ public class JavaCertificate extends Certificate {
}
}
+ @Override
+ public String toString() {
+ String res = "Certificate for \"" + getSubjectName() + "\"";
+ if (dnsNames_.size() != 0) {
+ String dns = "; DNS names :";
+ for (String s:dnsNames_) {
+ dns += " " + s;
+ }
+ res += dns;
+ }
+
+ if (srvNames_.size() != 0) {
+ String srv = "; SRV names :";
+ for (String s:srvNames_) {
+ srv += " "+ s;
+ }
+ res += srv;
+ }
+
+ if (xmppNames_.size() != 0) {
+ String xmpp = "; XMPP names :";
+ for (String s:xmppNames_) {
+ xmpp += " " + s;
+ }
+ res += xmpp;
+ }
+ return res;
+ }
+
private List<String> dnsNames_ = null;
private List<String> srvNames_ = null;
private List<String> xmppNames_ = null;
@@ -328,4 +504,31 @@ public class JavaCertificate extends Certificate {
*/
private final static String cnLabel = "COMMONNAME";
+ /* 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, 0x2B, 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, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x07 };
+
+ /**
+ * The tag expected for a SEQUENCE
+ */
+ protected static final byte SEQUENCE_TAG = 0x30;
+ /**
+ * The tag expected for a UTF8 String
+ */
+ protected static final byte UTF8STRING_TAG = 0x0c;
+ /**
+ * The tag expected for an IA5String
+ */
+ protected static final byte IA5STRING_TAG = 0x16;
}