001 /*
002 * $HeadURL: http://juliusdavies.ca/svn/not-yet-commons-ssl/tags/commons-ssl-0.3.11/src/java/org/apache/commons/ssl/Certificates.java $
003 * $Revision: 158 $
004 * $Date: 2009-09-17 14:47:27 -0700 (Thu, 17 Sep 2009) $
005 *
006 * ====================================================================
007 * Licensed to the Apache Software Foundation (ASF) under one
008 * or more contributor license agreements. See the NOTICE file
009 * distributed with this work for additional information
010 * regarding copyright ownership. The ASF licenses this file
011 * to you under the Apache License, Version 2.0 (the
012 * "License"); you may not use this file except in compliance
013 * with the License. You may obtain a copy of the License at
014 *
015 * http://www.apache.org/licenses/LICENSE-2.0
016 *
017 * Unless required by applicable law or agreed to in writing,
018 * software distributed under the License is distributed on an
019 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
020 * KIND, either express or implied. See the License for the
021 * specific language governing permissions and limitations
022 * under the License.
023 * ====================================================================
024 *
025 * This software consists of voluntary contributions made by many
026 * individuals on behalf of the Apache Software Foundation. For more
027 * information on the Apache Software Foundation, please see
028 * <http://www.apache.org/>.
029 *
030 */
031
032 package org.apache.commons.ssl;
033
034 import javax.net.ssl.HttpsURLConnection;
035 import java.io.*;
036 import java.math.BigInteger;
037 import java.net.URL;
038 import java.net.URLConnection;
039 import java.net.HttpURLConnection;
040 import java.security.MessageDigest;
041 import java.security.NoSuchAlgorithmException;
042 import java.security.cert.*;
043 import java.text.DateFormat;
044 import java.text.SimpleDateFormat;
045 import java.util.*;
046 import java.lang.reflect.Method;
047
048 /**
049 * @author Credit Union Central of British Columbia
050 * @author <a href="http://www.cucbc.com/">www.cucbc.com</a>
051 * @author <a href="mailto:juliusdavies@cucbc.com">juliusdavies@cucbc.com</a>
052 * @since 19-Aug-2005
053 */
054 public class Certificates {
055
056 public final static CertificateFactory CF;
057 public final static String LINE_ENDING = System.getProperty("line.separator");
058
059 private final static HashMap crl_cache = new HashMap();
060
061 public final static String CRL_EXTENSION = "2.5.29.31";
062 public final static String OCSP_EXTENSION = "1.3.6.1.5.5.7.1.1";
063 private final static DateFormat DF = new SimpleDateFormat("yyyy/MMM/dd");
064
065 public interface SerializableComparator extends Comparator, Serializable {
066 }
067
068 public final static SerializableComparator COMPARE_BY_EXPIRY =
069 new SerializableComparator() {
070 public int compare(Object o1, Object o2) {
071 X509Certificate c1 = (X509Certificate) o1;
072 X509Certificate c2 = (X509Certificate) o2;
073 if (c1 == c2) // this deals with case where both are null
074 {
075 return 0;
076 }
077 if (c1 == null) // non-null is always bigger than null
078 {
079 return -1;
080 }
081 if (c2 == null) {
082 return 1;
083 }
084 if (c1.equals(c2)) {
085 return 0;
086 }
087 Date d1 = c1.getNotAfter();
088 Date d2 = c2.getNotAfter();
089 int c = d1.compareTo(d2);
090 if (c == 0) {
091 String s1 = JavaImpl.getSubjectX500(c1);
092 String s2 = JavaImpl.getSubjectX500(c2);
093 c = s1.compareTo(s2);
094 if (c == 0) {
095 s1 = JavaImpl.getIssuerX500(c1);
096 s2 = JavaImpl.getIssuerX500(c2);
097 c = s1.compareTo(s2);
098 if (c == 0) {
099 BigInteger big1 = c1.getSerialNumber();
100 BigInteger big2 = c2.getSerialNumber();
101 c = big1.compareTo(big2);
102 if (c == 0) {
103 try {
104 byte[] b1 = c1.getEncoded();
105 byte[] b2 = c2.getEncoded();
106 int len1 = b1.length;
107 int len2 = b2.length;
108 int i = 0;
109 for (; i < len1 && i < len2; i++) {
110 c = ((int) b1[i]) - ((int) b2[i]);
111 if (c != 0) {
112 break;
113 }
114 }
115 if (c == 0) {
116 c = b1.length - b2.length;
117 }
118 }
119 catch (CertificateEncodingException cee) {
120 // I give up. They can be equal if they
121 // really want to be this badly.
122 c = 0;
123 }
124 }
125 }
126 }
127 }
128 return c;
129 }
130 };
131
132 static {
133 CertificateFactory cf = null;
134 try {
135 cf = CertificateFactory.getInstance("X.509");
136 }
137 catch (CertificateException ce) {
138 ce.printStackTrace(System.out);
139 }
140 finally {
141 CF = cf;
142 }
143 }
144
145 public static String toPEMString(X509Certificate cert)
146 throws CertificateEncodingException {
147 return toString(cert.getEncoded());
148 }
149
150 public static String toString(byte[] x509Encoded) {
151 byte[] encoded = Base64.encodeBase64(x509Encoded);
152 StringBuffer buf = new StringBuffer(encoded.length + 100);
153 buf.append("-----BEGIN CERTIFICATE-----\n");
154 for (int i = 0; i < encoded.length; i += 64) {
155 if (encoded.length - i >= 64) {
156 buf.append(new String(encoded, i, 64));
157 } else {
158 buf.append(new String(encoded, i, encoded.length - i));
159 }
160 buf.append(LINE_ENDING);
161 }
162 buf.append("-----END CERTIFICATE-----");
163 buf.append(LINE_ENDING);
164 return buf.toString();
165 }
166
167 public static String toString(X509Certificate cert) {
168 return toString(cert, false);
169 }
170
171 public static String toString(X509Certificate cert, boolean htmlStyle) {
172 String cn = getCN(cert);
173 String startStart = DF.format(cert.getNotBefore());
174 String endDate = DF.format(cert.getNotAfter());
175 String subject = JavaImpl.getSubjectX500(cert);
176 String issuer = JavaImpl.getIssuerX500(cert);
177 Iterator crls = getCRLs(cert).iterator();
178 if (subject.equals(issuer)) {
179 issuer = "self-signed";
180 }
181 StringBuffer buf = new StringBuffer(128);
182 if (htmlStyle) {
183 buf.append("<strong class=\"cn\">");
184 }
185 buf.append(cn);
186 if (htmlStyle) {
187 buf.append("</strong>");
188 }
189 buf.append(LINE_ENDING);
190 buf.append("Valid: ");
191 buf.append(startStart);
192 buf.append(" - ");
193 buf.append(endDate);
194 buf.append(LINE_ENDING);
195 buf.append("s: ");
196 buf.append(subject);
197 buf.append(LINE_ENDING);
198 buf.append("i: ");
199 buf.append(issuer);
200 while (crls.hasNext()) {
201 buf.append(LINE_ENDING);
202 buf.append("CRL: ");
203 buf.append((String) crls.next());
204 }
205 buf.append(LINE_ENDING);
206 return buf.toString();
207 }
208
209 public static List getCRLs(X509Extension cert) {
210 // What follows is a poor man's CRL extractor, for those lacking
211 // a BouncyCastle "bcprov.jar" in their classpath.
212
213 // It's a very basic state-machine: look for a standard URL scheme
214 // (such as http), and then start looking for a terminator. After
215 // running hexdump a few times on these things, it looks to me like
216 // the UTF-8 value "65533" seems to happen near where these things
217 // terminate. (Of course this stuff is ASN.1 and not UTF-8, but
218 // I happen to like some of the functions available to the String
219 // object). - juliusdavies@cucbc.com, May 10th, 2006
220 byte[] bytes = cert.getExtensionValue(CRL_EXTENSION);
221 LinkedList httpCRLS = new LinkedList();
222 LinkedList ftpCRLS = new LinkedList();
223 LinkedList otherCRLS = new LinkedList();
224 if (bytes == null) {
225 // just return empty list
226 return httpCRLS;
227 } else {
228 String s;
229 try {
230 s = new String(bytes, "UTF-8");
231 }
232 catch (UnsupportedEncodingException uee) {
233 // We're screwed if this thing has more than one CRL, because
234 // the "indeOf( (char) 65533 )" below isn't going to work.
235 s = new String(bytes);
236 }
237 int pos = 0;
238 while (pos >= 0) {
239 int x = -1, y;
240 int[] indexes = new int[4];
241 indexes[0] = s.indexOf("http", pos);
242 indexes[1] = s.indexOf("ldap", pos);
243 indexes[2] = s.indexOf("file", pos);
244 indexes[3] = s.indexOf("ftp", pos);
245 Arrays.sort(indexes);
246 for (int i = 0; i < indexes.length; i++) {
247 if (indexes[i] >= 0) {
248 x = indexes[i];
249 break;
250 }
251 }
252 if (x >= 0) {
253 y = s.indexOf((char) 65533, x);
254 String crl = y > x ? s.substring(x, y - 1) : s.substring(x);
255 if (y > x && crl.endsWith("0")) {
256 crl = crl.substring(0, crl.length() - 1);
257 }
258 String crlTest = crl.trim().toLowerCase();
259 if (crlTest.startsWith("http")) {
260 httpCRLS.add(crl);
261 } else if (crlTest.startsWith("ftp")) {
262 ftpCRLS.add(crl);
263 } else {
264 otherCRLS.add(crl);
265 }
266 pos = y;
267 } else {
268 pos = -1;
269 }
270 }
271 }
272
273 httpCRLS.addAll(ftpCRLS);
274 httpCRLS.addAll(otherCRLS);
275 return httpCRLS;
276 }
277
278 public static void checkCRL(X509Certificate cert)
279 throws CertificateException {
280 // String name = cert.getSubjectX500Principal().toString();
281 byte[] bytes = cert.getExtensionValue("2.5.29.31");
282 if (bytes == null) {
283 // log.warn( "Cert doesn't contain X509v3 CRL Distribution Points (2.5.29.31): " + name );
284 } else {
285 List crlList = getCRLs(cert);
286 Iterator it = crlList.iterator();
287 while (it.hasNext()) {
288 String url = (String) it.next();
289 CRLHolder holder = (CRLHolder) crl_cache.get(url);
290 if (holder == null) {
291 holder = new CRLHolder(url);
292 crl_cache.put(url, holder);
293 }
294 // success == false means we couldn't actually load the CRL
295 // (probably due to an IOException), so let's try the next one in
296 // our list.
297 boolean success = holder.checkCRL(cert);
298 if (success) {
299 break;
300 }
301 }
302 }
303
304 }
305
306 public static BigInteger getFingerprint(X509Certificate x509)
307 throws CertificateEncodingException {
308 return getFingerprint(x509.getEncoded());
309 }
310
311 public static BigInteger getFingerprint(byte[] x509)
312 throws CertificateEncodingException {
313 MessageDigest sha1;
314 try {
315 sha1 = MessageDigest.getInstance("SHA1");
316 }
317 catch (NoSuchAlgorithmException nsae) {
318 throw JavaImpl.newRuntimeException(nsae);
319 }
320
321 sha1.reset();
322 byte[] result = sha1.digest(x509);
323 return new BigInteger(result);
324 }
325
326 private static class CRLHolder {
327 private final String urlString;
328
329 private File tempCRLFile;
330 private long creationTime;
331 private Set passedTest = new HashSet();
332 private Set failedTest = new HashSet();
333
334 CRLHolder(String urlString) {
335 if (urlString == null) {
336 throw new NullPointerException("urlString can't be null");
337 }
338 this.urlString = urlString;
339 }
340
341 public synchronized boolean checkCRL(X509Certificate cert)
342 throws CertificateException {
343 CRL crl = null;
344 long now = System.currentTimeMillis();
345 if (now - creationTime > 24 * 60 * 60 * 1000) {
346 // Expire cache every 24 hours
347 if (tempCRLFile != null && tempCRLFile.exists()) {
348 tempCRLFile.delete();
349 }
350 tempCRLFile = null;
351 passedTest.clear();
352
353 /*
354 Note: if any certificate ever fails the check, we will
355 remember that fact.
356
357 This breaks with temporary "holds" that CRL's can issue.
358 Apparently a certificate can have a temporary "hold" on its
359 validity, but I'm not interested in supporting that. If a "held"
360 certificate is suddenly "unheld", you're just going to need
361 to restart your JVM.
362 */
363 // failedTest.clear(); <-- DO NOT UNCOMMENT!
364 }
365
366 BigInteger fingerprint = getFingerprint(cert);
367 if (failedTest.contains(fingerprint)) {
368 throw new CertificateException("Revoked by CRL (cached response)");
369 }
370 if (passedTest.contains(fingerprint)) {
371 return true;
372 }
373
374 if (tempCRLFile == null) {
375 try {
376 // log.info( "Trying to load CRL [" + urlString + "]" );
377
378 // java.net.URL blocks forever by default, so CRL-checking
379 // is freezing some systems. Below we go to great pains
380 // to enforce timeouts for CRL-checking (5 seconds).
381 URL url = new URL(urlString);
382 URLConnection urlConn = url.openConnection();
383 if (urlConn instanceof HttpsURLConnection) {
384
385 // HTTPS sites will use special CRLSocket.getInstance() SocketFactory
386 // that is configured to timeout after 5 seconds:
387 HttpsURLConnection httpsConn = (HttpsURLConnection) urlConn;
388 httpsConn.setSSLSocketFactory(CRLSocket.getSecureInstance());
389
390 } else if (urlConn instanceof HttpURLConnection) {
391
392 // HTTP timeouts can only be set on Java 1.5 and up. :-(
393 // The code required to set it for Java 1.4 and Java 1.3 is just too painful.
394 HttpURLConnection httpConn = (HttpURLConnection) urlConn;
395 try {
396 // Java 1.5 and up support these, so using reflection. UGH!!!
397 Class c = httpConn.getClass();
398 Method setConnTimeOut = c.getDeclaredMethod("setConnectTimeout", new Class[]{Integer.TYPE});
399 Method setReadTimeout = c.getDeclaredMethod("setReadTimeout", new Class[]{Integer.TYPE});
400 setConnTimeOut.invoke(httpConn, new Integer[]{new Integer(5000)});
401 setReadTimeout.invoke(httpConn, new Integer[]{new Integer(5000)});
402 } catch (NoSuchMethodException nsme) {
403 // oh well, java 1.4 users can suffer.
404 } catch (Exception e) {
405 throw new RuntimeException("can't set timeout", e);
406 }
407 }
408
409 File tempFile = File.createTempFile("crl", ".tmp");
410 tempFile.deleteOnExit();
411
412 OutputStream out = new FileOutputStream(tempFile);
413 out = new BufferedOutputStream(out);
414 InputStream in = new BufferedInputStream(urlConn.getInputStream());
415 try {
416 Util.pipeStream(in, out);
417 }
418 catch (IOException ioe) {
419 // better luck next time
420 tempFile.delete();
421 throw ioe;
422 }
423 this.tempCRLFile = tempFile;
424 this.creationTime = System.currentTimeMillis();
425 }
426 catch (IOException ioe) {
427 // log.warn( "Cannot check CRL: " + e );
428 }
429 }
430
431 if (tempCRLFile != null && tempCRLFile.exists()) {
432 try {
433 InputStream in = new FileInputStream(tempCRLFile);
434 in = new BufferedInputStream(in);
435 synchronized (CF) {
436 crl = CF.generateCRL(in);
437 }
438 in.close();
439 if (crl.isRevoked(cert)) {
440 // log.warn( "Revoked by CRL [" + urlString + "]: " + name );
441 passedTest.remove(fingerprint);
442 failedTest.add(fingerprint);
443 throw new CertificateException("Revoked by CRL");
444 } else {
445 passedTest.add(fingerprint);
446 }
447 }
448 catch (IOException ioe) {
449 // couldn't load CRL that's supposed to be stored in Temp file.
450 // log.warn( );
451 }
452 catch (CRLException crle) {
453 // something is wrong with the CRL
454 // log.warn( );
455 }
456 }
457 return crl != null;
458 }
459 }
460
461 public static String getCN(X509Certificate cert) {
462 String[] cns = getCNs(cert);
463 boolean foundSomeCNs = cns != null && cns.length >= 1;
464 return foundSomeCNs ? cns[0] : null;
465 }
466
467 public static String[] getCNs(X509Certificate cert) {
468 LinkedList cnList = new LinkedList();
469 /*
470 Sebastian Hauer's original StrictSSLProtocolSocketFactory used
471 getName() and had the following comment:
472
473 Parses a X.500 distinguished name for the value of the
474 "Common Name" field. This is done a bit sloppy right
475 now and should probably be done a bit more according to
476 <code>RFC 2253</code>.
477
478 I've noticed that toString() seems to do a better job than
479 getName() on these X500Principal objects, so I'm hoping that
480 addresses Sebastian's concern.
481
482 For example, getName() gives me this:
483 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
484
485 whereas toString() gives me this:
486 EMAILADDRESS=juliusdavies@cucbc.com
487
488 Looks like toString() even works with non-ascii domain names!
489 I tested it with "花子.co.jp" and it worked fine.
490 */
491 String subjectPrincipal = cert.getSubjectX500Principal().toString();
492 StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
493 while (st.hasMoreTokens()) {
494 String tok = st.nextToken();
495 int x = tok.indexOf("CN=");
496 if (x >= 0) {
497 cnList.add(tok.substring(x + 3));
498 }
499 }
500 if (!cnList.isEmpty()) {
501 String[] cns = new String[cnList.size()];
502 cnList.toArray(cns);
503 return cns;
504 } else {
505 return null;
506 }
507 }
508
509
510 /**
511 * Extracts the array of SubjectAlt DNS names from an X509Certificate.
512 * Returns null if there aren't any.
513 * <p/>
514 * Note: Java doesn't appear able to extract international characters
515 * from the SubjectAlts. It can only extract international characters
516 * from the CN field.
517 * <p/>
518 * (Or maybe the version of OpenSSL I'm using to test isn't storing the
519 * international characters correctly in the SubjectAlts?).
520 *
521 * @param cert X509Certificate
522 * @return Array of SubjectALT DNS names stored in the certificate.
523 */
524 public static String[] getDNSSubjectAlts(X509Certificate cert) {
525 LinkedList subjectAltList = new LinkedList();
526 Collection c = null;
527 try {
528 c = cert.getSubjectAlternativeNames();
529 }
530 catch (CertificateParsingException cpe) {
531 // Should probably log.debug() this?
532 cpe.printStackTrace();
533 }
534 if (c != null) {
535 Iterator it = c.iterator();
536 while (it.hasNext()) {
537 List list = (List) it.next();
538 int type = ((Integer) list.get(0)).intValue();
539 // If type is 2, then we've got a dNSName
540 if (type == 2) {
541 String s = (String) list.get(1);
542 subjectAltList.add(s);
543 }
544 }
545 }
546 if (!subjectAltList.isEmpty()) {
547 String[] subjectAlts = new String[subjectAltList.size()];
548 subjectAltList.toArray(subjectAlts);
549 return subjectAlts;
550 } else {
551 return null;
552 }
553 }
554
555 /**
556 * Trims off any null entries on the array. Returns a shrunk array.
557 *
558 * @param chain X509Certificate[] chain to trim
559 * @return Shrunk array with all trailing null entries removed.
560 */
561 public static Certificate[] trimChain(Certificate[] chain) {
562 for (int i = 0; i < chain.length; i++) {
563 if (chain[i] == null) {
564 X509Certificate[] newChain = new X509Certificate[i];
565 System.arraycopy(chain, 0, newChain, 0, i);
566 return newChain;
567 }
568 }
569 return chain;
570 }
571
572 /**
573 * Returns a chain of type X509Certificate[].
574 *
575 * @param chain Certificate[] chain to cast to X509Certificate[]
576 * @return chain of type X509Certificate[].
577 */
578 public static X509Certificate[] x509ifyChain(Certificate[] chain) {
579 if (chain instanceof X509Certificate[]) {
580 return (X509Certificate[]) chain;
581 } else {
582 X509Certificate[] x509Chain = new X509Certificate[chain.length];
583 System.arraycopy(chain, 0, x509Chain, 0, chain.length);
584 return x509Chain;
585 }
586 }
587
588 public static void main(String[] args) throws Exception {
589 for (int i = 0; i < args.length; i++) {
590 FileInputStream in = new FileInputStream(args[i]);
591 TrustMaterial tm = new TrustMaterial(in);
592 Iterator it = tm.getCertificates().iterator();
593 while (it.hasNext()) {
594 X509Certificate x509 = (X509Certificate) it.next();
595 System.out.println(toString(x509));
596 }
597 }
598 }
599 }