/* Copyright (c) 2012, Isode Limited, London, England.
* All rights reserved.
*/
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.base.NotNull;
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;
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;
}
}
/**
* 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 byteStream 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; ibyteStream 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= 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> 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[] 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 */
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)
{
NotNull.exceptIfNull(x509Cert,"x509Cert");
x509Certificate = x509Cert;
dnsNames_ = new ArrayList();
srvNames_ = new ArrayList();
xmppNames_ = new ArrayList();
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 getCommonNames() {
ArrayList result = new ArrayList();
/*
* 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 cnMap = new HashMap();
/*
* 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 getSRVNames() {
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 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 getXMPPAddresses() {
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() {
try {
byte[] r = x509Certificate.getEncoded();
return new ByteArray(r);
}
catch (CertificateEncodingException e) {
return null;
}
}
@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 dnsNames_ = null;
private List srvNames_ = null;
private List 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";
/* 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;
}