001/* 002 * $HeadURL: file:///opt/dev/not-yet-commons-ssl-SVN-repo/tags/commons-ssl-0.3.17/src/java/org/apache/commons/ssl/Certificates.java $ 003 * $Revision: 180 $ 004 * $Date: 2014-09-23 11:33:47 -0700 (Tue, 23 Sep 2014) $ 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 javax.naming.InvalidNameException; 035import javax.naming.NamingException; 036import javax.naming.directory.Attribute; 037import javax.naming.directory.Attributes; 038import javax.naming.ldap.LdapName; 039import javax.naming.ldap.Rdn; 040import javax.security.auth.x500.X500Principal; 041 042import javax.net.ssl.HttpsURLConnection; 043import java.io.*; 044import java.math.BigInteger; 045import java.net.URL; 046import java.net.URLConnection; 047import java.net.HttpURLConnection; 048import java.security.MessageDigest; 049import java.security.NoSuchAlgorithmException; 050import java.security.cert.*; 051import java.text.DateFormat; 052import java.text.SimpleDateFormat; 053import java.util.*; 054import java.lang.reflect.Method; 055 056/** 057 * @author Credit Union Central of British Columbia 058 * @author <a href="http://www.cucbc.com/">www.cucbc.com</a> 059 * @author <a href="mailto:juliusdavies@cucbc.com">juliusdavies@cucbc.com</a> 060 * @since 19-Aug-2005 061 */ 062public class Certificates { 063 064 public final static CertificateFactory CF; 065 public final static String LINE_ENDING = System.getProperty("line.separator"); 066 067 private final static HashMap crl_cache = new HashMap(); 068 069 public final static String CRL_EXTENSION = "2.5.29.31"; 070 public final static String OCSP_EXTENSION = "1.3.6.1.5.5.7.1.1"; 071 private final static DateFormat DF = new SimpleDateFormat("yyyy/MMM/dd"); 072 073 public interface SerializableComparator extends Comparator, Serializable { 074 } 075 076 public final static SerializableComparator COMPARE_BY_EXPIRY = 077 new SerializableComparator() { 078 public int compare(Object o1, Object o2) { 079 X509Certificate c1 = (X509Certificate) o1; 080 X509Certificate c2 = (X509Certificate) o2; 081 if (c1 == c2) // this deals with case where both are null 082 { 083 return 0; 084 } 085 if (c1 == null) // non-null is always bigger than null 086 { 087 return -1; 088 } 089 if (c2 == null) { 090 return 1; 091 } 092 if (c1.equals(c2)) { 093 return 0; 094 } 095 Date d1 = c1.getNotAfter(); 096 Date d2 = c2.getNotAfter(); 097 int c = d1.compareTo(d2); 098 if (c == 0) { 099 String s1 = JavaImpl.getSubjectX500(c1); 100 String s2 = JavaImpl.getSubjectX500(c2); 101 c = s1.compareTo(s2); 102 if (c == 0) { 103 s1 = JavaImpl.getIssuerX500(c1); 104 s2 = JavaImpl.getIssuerX500(c2); 105 c = s1.compareTo(s2); 106 if (c == 0) { 107 BigInteger big1 = c1.getSerialNumber(); 108 BigInteger big2 = c2.getSerialNumber(); 109 c = big1.compareTo(big2); 110 if (c == 0) { 111 try { 112 byte[] b1 = c1.getEncoded(); 113 byte[] b2 = c2.getEncoded(); 114 int len1 = b1.length; 115 int len2 = b2.length; 116 int i = 0; 117 for (; i < len1 && i < len2; i++) { 118 c = ((int) b1[i]) - ((int) b2[i]); 119 if (c != 0) { 120 break; 121 } 122 } 123 if (c == 0) { 124 c = b1.length - b2.length; 125 } 126 } 127 catch (CertificateEncodingException cee) { 128 // I give up. They can be equal if they 129 // really want to be this badly. 130 c = 0; 131 } 132 } 133 } 134 } 135 } 136 return c; 137 } 138 }; 139 140 static { 141 CertificateFactory cf = null; 142 try { 143 cf = CertificateFactory.getInstance("X.509"); 144 } 145 catch (CertificateException ce) { 146 ce.printStackTrace(System.out); 147 } 148 finally { 149 CF = cf; 150 } 151 } 152 153 public static String toPEMString(X509Certificate cert) 154 throws CertificateEncodingException { 155 return toString(cert.getEncoded()); 156 } 157 158 public static String toString(byte[] x509Encoded) { 159 byte[] encoded = Base64.encodeBase64(x509Encoded); 160 StringBuffer buf = new StringBuffer(encoded.length + 100); 161 buf.append("-----BEGIN CERTIFICATE-----\n"); 162 for (int i = 0; i < encoded.length; i += 64) { 163 if (encoded.length - i >= 64) { 164 buf.append(new String(encoded, i, 64)); 165 } else { 166 buf.append(new String(encoded, i, encoded.length - i)); 167 } 168 buf.append(LINE_ENDING); 169 } 170 buf.append("-----END CERTIFICATE-----"); 171 buf.append(LINE_ENDING); 172 return buf.toString(); 173 } 174 175 public static String toString(X509Certificate cert) { 176 return toString(cert, false); 177 } 178 179 public static String toString(X509Certificate cert, boolean htmlStyle) { 180 String cn = getCN(cert); 181 String startStart = DF.format(cert.getNotBefore()); 182 String endDate = DF.format(cert.getNotAfter()); 183 String subject = JavaImpl.getSubjectX500(cert); 184 String issuer = JavaImpl.getIssuerX500(cert); 185 Iterator crls = getCRLs(cert).iterator(); 186 if (subject.equals(issuer)) { 187 issuer = "self-signed"; 188 } 189 StringBuffer buf = new StringBuffer(128); 190 if (htmlStyle) { 191 buf.append("<strong class=\"cn\">"); 192 } 193 buf.append(cn); 194 if (htmlStyle) { 195 buf.append("</strong>"); 196 } 197 buf.append(LINE_ENDING); 198 buf.append("Valid: "); 199 buf.append(startStart); 200 buf.append(" - "); 201 buf.append(endDate); 202 buf.append(LINE_ENDING); 203 buf.append("s: "); 204 buf.append(subject); 205 buf.append(LINE_ENDING); 206 buf.append("i: "); 207 buf.append(issuer); 208 while (crls.hasNext()) { 209 buf.append(LINE_ENDING); 210 buf.append("CRL: "); 211 buf.append((String) crls.next()); 212 } 213 buf.append(LINE_ENDING); 214 return buf.toString(); 215 } 216 217 public static List getCRLs(X509Extension cert) { 218 // What follows is a poor man's CRL extractor, for those lacking 219 // a BouncyCastle "bcprov.jar" in their classpath. 220 221 // It's a very basic state-machine: look for a standard URL scheme 222 // (such as http), and then start looking for a terminator. After 223 // running hexdump a few times on these things, it looks to me like 224 // the UTF-8 value "65533" seems to happen near where these things 225 // terminate. (Of course this stuff is ASN.1 and not UTF-8, but 226 // I happen to like some of the functions available to the String 227 // object). - juliusdavies@cucbc.com, May 10th, 2006 228 byte[] bytes = cert.getExtensionValue(CRL_EXTENSION); 229 LinkedList httpCRLS = new LinkedList(); 230 LinkedList ftpCRLS = new LinkedList(); 231 LinkedList otherCRLS = new LinkedList(); 232 if (bytes == null) { 233 // just return empty list 234 return httpCRLS; 235 } else { 236 String s; 237 try { 238 s = new String(bytes, "UTF-8"); 239 } 240 catch (UnsupportedEncodingException uee) { 241 // We're screwed if this thing has more than one CRL, because 242 // the "indeOf( (char) 65533 )" below isn't going to work. 243 s = new String(bytes); 244 } 245 int pos = 0; 246 while (pos >= 0) { 247 int x = -1, y; 248 int[] indexes = new int[4]; 249 indexes[0] = s.indexOf("http", pos); 250 indexes[1] = s.indexOf("ldap", pos); 251 indexes[2] = s.indexOf("file", pos); 252 indexes[3] = s.indexOf("ftp", pos); 253 Arrays.sort(indexes); 254 for (int i = 0; i < indexes.length; i++) { 255 if (indexes[i] >= 0) { 256 x = indexes[i]; 257 break; 258 } 259 } 260 if (x >= 0) { 261 y = s.indexOf((char) 65533, x); 262 String crl = y > x ? s.substring(x, y - 1) : s.substring(x); 263 if (y > x && crl.endsWith("0")) { 264 crl = crl.substring(0, crl.length() - 1); 265 } 266 String crlTest = crl.trim().toLowerCase(); 267 if (crlTest.startsWith("http")) { 268 httpCRLS.add(crl); 269 } else if (crlTest.startsWith("ftp")) { 270 ftpCRLS.add(crl); 271 } else { 272 otherCRLS.add(crl); 273 } 274 pos = y; 275 } else { 276 pos = -1; 277 } 278 } 279 } 280 281 httpCRLS.addAll(ftpCRLS); 282 httpCRLS.addAll(otherCRLS); 283 return httpCRLS; 284 } 285 286 public static void checkCRL(X509Certificate cert) 287 throws CertificateException { 288 // String name = cert.getSubjectX500Principal().toString(); 289 byte[] bytes = cert.getExtensionValue("2.5.29.31"); 290 if (bytes == null) { 291 // log.warn( "Cert doesn't contain X509v3 CRL Distribution Points (2.5.29.31): " + name ); 292 } else { 293 List crlList = getCRLs(cert); 294 Iterator it = crlList.iterator(); 295 while (it.hasNext()) { 296 String url = (String) it.next(); 297 CRLHolder holder = (CRLHolder) crl_cache.get(url); 298 if (holder == null) { 299 holder = new CRLHolder(url); 300 crl_cache.put(url, holder); 301 } 302 // success == false means we couldn't actually load the CRL 303 // (probably due to an IOException), so let's try the next one in 304 // our list. 305 boolean success = holder.checkCRL(cert); 306 if (success) { 307 break; 308 } 309 } 310 } 311 312 } 313 314 public static BigInteger getFingerprint(X509Certificate x509) 315 throws CertificateEncodingException { 316 return getFingerprint(x509.getEncoded()); 317 } 318 319 public static BigInteger getFingerprint(byte[] x509) 320 throws CertificateEncodingException { 321 MessageDigest sha1; 322 try { 323 sha1 = MessageDigest.getInstance("SHA1"); 324 } 325 catch (NoSuchAlgorithmException nsae) { 326 throw JavaImpl.newRuntimeException(nsae); 327 } 328 329 sha1.reset(); 330 byte[] result = sha1.digest(x509); 331 return new BigInteger(result); 332 } 333 334 private static class CRLHolder { 335 private final String urlString; 336 337 private File tempCRLFile; 338 private long creationTime; 339 private Set passedTest = new HashSet(); 340 private Set failedTest = new HashSet(); 341 342 CRLHolder(String urlString) { 343 if (urlString == null) { 344 throw new NullPointerException("urlString can't be null"); 345 } 346 this.urlString = urlString; 347 } 348 349 public synchronized boolean checkCRL(X509Certificate cert) 350 throws CertificateException { 351 CRL crl = null; 352 long now = System.currentTimeMillis(); 353 if (now - creationTime > 24 * 60 * 60 * 1000) { 354 // Expire cache every 24 hours 355 if (tempCRLFile != null && tempCRLFile.exists()) { 356 tempCRLFile.delete(); 357 } 358 tempCRLFile = null; 359 passedTest.clear(); 360 361 /* 362 Note: if any certificate ever fails the check, we will 363 remember that fact. 364 365 This breaks with temporary "holds" that CRL's can issue. 366 Apparently a certificate can have a temporary "hold" on its 367 validity, but I'm not interested in supporting that. If a "held" 368 certificate is suddenly "unheld", you're just going to need 369 to restart your JVM. 370 */ 371 // failedTest.clear(); <-- DO NOT UNCOMMENT! 372 } 373 374 BigInteger fingerprint = getFingerprint(cert); 375 if (failedTest.contains(fingerprint)) { 376 throw new CertificateException("Revoked by CRL (cached response)"); 377 } 378 if (passedTest.contains(fingerprint)) { 379 return true; 380 } 381 382 if (tempCRLFile == null) { 383 try { 384 // log.info( "Trying to load CRL [" + urlString + "]" ); 385 386 // java.net.URL blocks forever by default, so CRL-checking 387 // is freezing some systems. Below we go to great pains 388 // to enforce timeouts for CRL-checking (5 seconds). 389 URL url = new URL(urlString); 390 URLConnection urlConn = url.openConnection(); 391 if (urlConn instanceof HttpsURLConnection) { 392 393 // HTTPS sites will use special CRLSocket.getInstance() SocketFactory 394 // that is configured to timeout after 5 seconds: 395 HttpsURLConnection httpsConn = (HttpsURLConnection) urlConn; 396 httpsConn.setSSLSocketFactory(CRLSocket.getSecureInstance()); 397 398 } else if (urlConn instanceof HttpURLConnection) { 399 400 // HTTP timeouts can only be set on Java 1.5 and up. :-( 401 // The code required to set it for Java 1.4 and Java 1.3 is just too painful. 402 HttpURLConnection httpConn = (HttpURLConnection) urlConn; 403 try { 404 // Java 1.5 and up support these, so using reflection. UGH!!! 405 Class c = httpConn.getClass(); 406 Method setConnTimeOut = c.getDeclaredMethod("setConnectTimeout", new Class[]{Integer.TYPE}); 407 Method setReadTimeout = c.getDeclaredMethod("setReadTimeout", new Class[]{Integer.TYPE}); 408 setConnTimeOut.invoke(httpConn, Integer.valueOf(5000)); 409 setReadTimeout.invoke(httpConn, Integer.valueOf(5000)); 410 } catch (NoSuchMethodException nsme) { 411 // oh well, java 1.4 users can suffer. 412 } catch (Exception e) { 413 throw new RuntimeException("can't set timeout", e); 414 } 415 } 416 417 File tempFile = File.createTempFile("crl", ".tmp"); 418 tempFile.deleteOnExit(); 419 420 OutputStream out = new FileOutputStream(tempFile); 421 out = new BufferedOutputStream(out); 422 InputStream in = new BufferedInputStream(urlConn.getInputStream()); 423 try { 424 Util.pipeStream(in, out); 425 } 426 catch (IOException ioe) { 427 // better luck next time 428 tempFile.delete(); 429 throw ioe; 430 } 431 this.tempCRLFile = tempFile; 432 this.creationTime = System.currentTimeMillis(); 433 } 434 catch (IOException ioe) { 435 // log.warn( "Cannot check CRL: " + e ); 436 } 437 } 438 439 if (tempCRLFile != null && tempCRLFile.exists()) { 440 try { 441 InputStream in = new FileInputStream(tempCRLFile); 442 in = new BufferedInputStream(in); 443 synchronized (CF) { 444 crl = CF.generateCRL(in); 445 } 446 in.close(); 447 if (crl.isRevoked(cert)) { 448 // log.warn( "Revoked by CRL [" + urlString + "]: " + name ); 449 passedTest.remove(fingerprint); 450 failedTest.add(fingerprint); 451 throw new CertificateException("Revoked by CRL"); 452 } else { 453 passedTest.add(fingerprint); 454 } 455 } 456 catch (IOException ioe) { 457 // couldn't load CRL that's supposed to be stored in Temp file. 458 // log.warn( ); 459 } 460 catch (CRLException crle) { 461 // something is wrong with the CRL 462 // log.warn( ); 463 } 464 } 465 return crl != null; 466 } 467 } 468 469 public static String getCN(X509Certificate cert) { 470 String[] cns = getCNs(cert); 471 boolean foundSomeCNs = cns != null && cns.length >= 1; 472 return foundSomeCNs ? cns[0] : null; 473 } 474 475 public static String[] getCNs(X509Certificate cert) { 476 try { 477 final String subjectPrincipal = cert.getSubjectX500Principal().getName(X500Principal.RFC2253); 478 final LinkedList<String> cnList = new LinkedList<String>(); 479 final LdapName subjectDN = new LdapName(subjectPrincipal); 480 for (final Rdn rds : subjectDN.getRdns()) { 481 final Attributes attributes = rds.toAttributes(); 482 final Attribute cn = attributes.get("cn"); 483 if (cn != null) { 484 try { 485 final Object value = cn.get(); 486 if (value != null) { 487 cnList.add(value.toString()); 488 } 489 } catch (NoSuchElementException ignore) { 490 } catch (NamingException ignore) { 491 } 492 } 493 } 494 if (!cnList.isEmpty()) { 495 return cnList.toArray(new String[cnList.size()]); 496 } 497 } catch (InvalidNameException ignore) { 498 } 499 return null; 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 Certificate[] trimChain(Certificate[] 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}