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