summaryrefslogtreecommitdiffstats
blob: ccfc5684509995ee936c6f000d58f7b158d8a28b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
/*  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 <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
             */
            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<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() {
        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() {
        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<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";

    /* 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;
}