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 }