001package com.hfg.cert; 002 003import java.net.URL; 004import java.security.KeyStore; 005import java.security.PublicKey; 006import java.security.cert.*; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashSet; 012import java.util.List; 013import java.util.Set; 014import java.util.regex.Matcher; 015import java.util.regex.Pattern; 016import java.util.stream.Collectors; 017import javax.net.ssl.HttpsURLConnection; 018import javax.net.ssl.SSLContext; 019import javax.net.ssl.SSLSocket; 020import javax.net.ssl.SSLSocketFactory; 021import javax.net.ssl.TrustManager; 022import javax.net.ssl.TrustManagerFactory; 023import javax.net.ssl.X509TrustManager; 024 025import com.hfg.util.StringBuilderPlus; 026import com.hfg.util.StringUtil; 027import com.hfg.util.collection.OrderedSet; 028 029//------------------------------------------------------------------------------ 030/** 031 * General certificate utility methods. 032 * 033 * @author J. Alex Taylor, hairyfatguy.com 034 */ 035//------------------------------------------------------------------------------ 036// com.hfg XML/HTML Coding Library 037// 038// This library is free software; you can redistribute it and/or 039// modify it under the terms of the GNU Lesser General Public 040// License as published by the Free Software Foundation; either 041// version 2.1 of the License, or (at your option) any later version. 042// 043// This library is distributed in the hope that it will be useful, 044// but WITHOUT ANY WARRANTY; without even the implied warranty of 045// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 046// Lesser General Public License for more details. 047// 048// You should have received a copy of the GNU Lesser General Public 049// License along with this library; if not, write to the Free Software 050// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 051// 052// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 053// jataylor@hairyfatguy.com 054//------------------------------------------------------------------------------ 055 056public class CertificateUtil 057{ 058 059 //-------------------------------------------------------------------------- 060 public static void verifyCertificates(String inURL) 061 throws Exception 062 { 063 Set<X509Certificate> certs; 064 065 if (inURL.startsWith("https:")) 066 { 067 certs = getWebCertificates(inURL); 068 } 069 else if (inURL.startsWith("ldaps:")) 070 { 071 certs = geLdapCertificates(inURL); 072 } 073 else 074 { 075 throw new RuntimeException("Unsupported url protocol: " + StringUtil.singleQuote(inURL) + "!"); 076 } 077 078 079 List<X509Certificate> trustedCerts = getTrustedCertificates(); 080 Set<X509Certificate> rootCAs = new HashSet<>(trustedCerts.size()); 081 Set<X509Certificate> intermediateCerts = new HashSet<>(trustedCerts.size()); 082 for (X509Certificate cert : trustedCerts) 083 { 084 if (isSelfSigned(cert)) 085 { 086 rootCAs.add(cert); 087 } 088 else 089 { 090 intermediateCerts.add(cert); 091 } 092 } 093 094 List<X509Certificate> reorderedCerts = new ArrayList<>(certs); 095 if (reorderedCerts.size() > 1 096 && reorderedCerts.get(0).getIssuerDN().equals(reorderedCerts.get(1).getSubjectDN())) 097 { 098 Collections.reverse(reorderedCerts); 099 } 100 101 for (X509Certificate cert : reorderedCerts) 102 { 103 System.out.println(generateCertSummary(cert)); 104 105 X509CertSelector selector = new X509CertSelector(); 106 selector.setCertificate(cert); 107 108 Set<TrustAnchor> trustAnchors = new HashSet<>(); 109 for (X509Certificate rootCA : rootCAs) 110 { 111 trustAnchors.add(new TrustAnchor(rootCA, null)); 112 } 113 114 PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector); 115 116 pkixParams.setRevocationEnabled(false); 117 118 CertStore intermediateCertStore = CertStore.getInstance("Collection", 119 new CollectionCertStoreParameters(intermediateCerts)); 120 pkixParams.addCertStore(intermediateCertStore); 121 122 CertPathBuilder builder = CertPathBuilder.getInstance("PKIX"); 123 PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) builder.build(pkixParams); 124 125 if (isSelfSigned(cert)) 126 { 127 rootCAs.add(cert); 128 } 129 else 130 { 131 intermediateCerts.add(cert); 132 } 133 } 134 } 135 136 //-------------------------------------------------------------------------- 137 public static String generateCertSummary(X509Certificate inCert) 138 throws CertificateParsingException 139 { 140 List<SAN> sanNames = getSubjectAlternateNames(inCert); 141 142 StringBuilderPlus buffer = new StringBuilderPlus() 143 .appendln("Certificate Info:") 144 .appendln("----------------") 145 .appendln(" Subject DN: " + inCert.getSubjectDN()) 146 .appendln(" Type: " + inCert.getType()) 147 .appendln(" Public Key Algorithm: " + inCert.getPublicKey().getAlgorithm()) 148 .appendln(" Public Key Format: " + inCert.getPublicKey().getFormat()) 149 .appendln(" Subject Alt Name(s): " + (sanNames != null ? StringUtil.join(sanNames, ", ") : "")) 150 .appendln(" Issuer DN: " + inCert.getIssuerDN()) 151 .appendln(" Expires: " + inCert.getNotAfter()); 152 153 return buffer.toString(); 154 } 155 156 //-------------------------------------------------------------------------- 157 public static Set<X509Certificate> getWebCertificates(String inURL) 158 throws Exception 159 { 160 HttpsURLConnection conn = (HttpsURLConnection) new URL(inURL).openConnection(); 161 conn.connect(); 162 163 Set<X509Certificate> certSet = new OrderedSet<>(5); 164 165 Certificate[] certs = conn.getServerCertificates(); 166 if (certs != null) 167 { 168 certSet.addAll((List<X509Certificate>) (Object) Arrays.asList(certs)); 169 } 170 171 return certSet; 172 } 173 174 //-------------------------------------------------------------------------- 175 public static Set<X509Certificate> geLdapCertificates(String inURL) 176 throws Exception 177 { 178 Pattern urlPattern = Pattern.compile("ldap(?:s)?://([\\w\\-\\.]+)(?:\\:(\\d+))?"); 179 Matcher m = urlPattern.matcher(inURL); 180 if (! m.matches()) 181 { 182 throw new RuntimeException("Unrecognized URL pattern: " + inURL); 183 } 184 185 String host = m.group(1); 186 int port = (m.group(2) != null ? Integer.parseInt(m.group(2)) : 636); 187 188 TrustManagerFactory trustMgrfactory = getTrustManagerFactory(); 189 190 X509TrustManager defaultTrustManager = (X509TrustManager) trustMgrfactory.getTrustManagers()[0]; 191 192 SavingTrustManager tm = new SavingTrustManager(defaultTrustManager); 193 194 SSLSocket sslSocket = null; 195 196 SSLContext context = SSLContext.getInstance("TLS"); 197 context.init(null, new TrustManager[]{tm}, null); 198 SSLSocketFactory sslFactory = context.getSocketFactory(); 199 200 try 201 { 202 sslSocket = (SSLSocket) sslFactory.createSocket(host, port); 203 sslSocket.setSoTimeout(5000); 204 System.out.println("Starting SSL handshake..."); 205 sslSocket.startHandshake(); 206 } 207 catch (Exception e) 208 { 209 // Ignore. We want to be able to examine the certificates even if there was 210 // an error thrown because they aren't recognized or valid. 211 } 212 finally 213 { 214 if (sslSocket != null) 215 { 216 sslSocket.close(); 217 } 218 } 219 220 return tm.mNewCertificates; 221 } 222 223 //-------------------------------------------------------------------------- 224 private static List<X509Certificate> getTrustedCertificates() 225 throws Exception 226 { 227 TrustManagerFactory factory = getTrustManagerFactory(); 228 229 List<TrustManager> trustManagers = Arrays.asList(factory.getTrustManagers()); 230 List<X509Certificate> certificates = trustManagers.stream() 231 .filter(X509TrustManager.class::isInstance) 232 .map(X509TrustManager.class::cast) 233 .map(trustManager -> Arrays.asList(trustManager.getAcceptedIssuers())) 234 .flatMap(Collection::stream) 235 .collect(Collectors.toList()); 236 237 return certificates; 238 } 239 240 //-------------------------------------------------------------------------- 241 private static TrustManagerFactory getTrustManagerFactory() 242 throws Exception 243 { 244 TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 245 factory.init((KeyStore) null); 246 247 return factory; 248 } 249 250 //-------------------------------------------------------------------------- 251 public static boolean isSelfSigned(X509Certificate inCert) 252 { 253 boolean result = true; 254 255 try 256 { 257 PublicKey key = inCert.getPublicKey(); 258 inCert.verify(key); 259 } 260 catch (Exception e) 261 { 262 result = false; 263 } 264 265 return result; 266 } 267 268 //-------------------------------------------------------------------------- 269 public static List<SAN> getSubjectAlternateNames(X509Certificate inCertificate) 270 throws CertificateParsingException 271 { 272 List<SAN> values = null; 273 try 274 { 275 /* 276 The ASN.1 definition of the SubjectAltName extension is: 277 SubjectAltName ::= GeneralNames 278 279 GeneralNames :: = SEQUENCE SIZE (1..MAX) OF GeneralName 280 281 GeneralName ::= CHOICE { 282 otherName [0] OtherName, 283 rfc822Name [1] IA5String, 284 dNSName [2] IA5String, 285 x400Address [3] ORAddress, 286 directoryName [4] Name, 287 ediPartyName [5] EDIPartyName, 288 uniformResourceIdentifier [6] IA5String, 289 iPAddress [7] OCTET STRING, 290 registeredID [8] OBJECT IDENTIFIER} 291 292 If this certificate does not contain a SubjectAltName extension, null is returned. 293 Otherwise, a Collection is returned with an entry representing each GeneralName 294 included in the extension. Each entry is a List whose first entry is an Integer 295 (the name type, 0-8) and whose second entry is a String or a byte array (the name, 296 in string or ASN.1 DER encoded form, respectively). 297 */ 298 Collection<List<?>> altNames = inCertificate.getSubjectAlternativeNames(); 299 if (altNames != null) 300 { 301 values = new ArrayList<>(3); 302 for (List item : altNames) 303 { 304 SAN_Type type = SAN_Type.valueOf((Integer) item.get(0)); 305 Object value = item.get(1); 306 String stringValue = null; 307 308 if (SAN_Type.OTHER_NAME.equals(type)) 309 { 310 if (value instanceof String) 311 { 312 stringValue = (String) value; 313 } 314 else if (value instanceof byte[]) 315 { 316 // ASN.1 DER-encoded value 317 // TODO: Decode the value 318 stringValue = "DER-encoded value"; 319 /* 320 try 321 { 322 ASN1InputStream decoder=null; 323 if(item.toArray()[1] instanceof byte[]) 324 decoder = new ASN1InputStream((byte[]) item.toArray()[1]); 325 else if(item.toArray()[1] instanceof String) 326 identities.add( (String) item.toArray()[1] ); 327 if(decoder==null) continue; 328 DEREncodable encoded = decoder.readObject(); 329 encoded = ((DERSequence) encoded).getObjectAt(1); 330 encoded = ((DERTaggedObject) encoded).getObject(); 331 encoded = ((DERTaggedObject) encoded).getObject(); 332 String identity = ((DERUTF8String) encoded).getString(); 333 identities.add(identity); 334 } 335 catch (UnsupportedEncodingException e) { 336 log.error("Error decoding subjectAltName" + e.getLocalizedMessage(),e); 337 } 338 catch (Exception e) { 339 log.error("Error decoding subjectAltName" + e.getLocalizedMessage(),e); 340 } 341 */ 342 } 343 } 344 else if (SAN_Type.RFC822_NAME.equals(type) 345 || SAN_Type.DNS_NAME.equals(type)) 346 { 347 if (value instanceof String) 348 { 349 stringValue = (String) value; 350 } 351 } 352 else if (SAN_Type.X400_ADDRESS.equals(type)) 353 { 354 // TODO: 355 if (value instanceof String) 356 { 357 stringValue = (String) value; 358 } 359 } 360 else if (SAN_Type.DIRECTORY_NAME.equals(type)) 361 { 362 // TODO: 363 if (value instanceof String) 364 { 365 stringValue = (String) value; 366 } 367 } 368 else if (SAN_Type.EDI_PARTY_NAME.equals(type)) 369 { 370 // TODO: 371 if (value instanceof String) 372 { 373 stringValue = (String) value; 374 } 375 } 376 else if (SAN_Type.URI.equals(type)) 377 { 378 // TODO: 379 if (value instanceof String) 380 { 381 stringValue = (String) value; 382 } 383 } 384 else if (SAN_Type.IP_ADDRESS.equals(type)) 385 { 386 // TODO: 387 if (value instanceof String) 388 { 389 stringValue = (String) value; 390 } 391 } 392 else if (SAN_Type.REGISTERED_ID.equals(type)) 393 { 394 // TODO: 395 if (value instanceof String) 396 { 397 stringValue = (String) value; 398 } 399 } 400 401 values.add(new SAN(type, stringValue)); 402 } 403 } 404 } 405 catch (CertificateParsingException e) 406 { 407 throw new CertificateParsingException("Error parsing SubjectAltName in certificate: " + inCertificate,e); 408 } 409 410 return values; 411 } 412 413 private static class SavingTrustManager implements X509TrustManager 414 { 415 private X509TrustManager mParentTrustMgr; 416 private Set<X509Certificate> mAllCertificates = new HashSet<>(3); 417 private Set<X509Certificate> mNewCertificates = new HashSet<>(3); 418 419 420 //----------------------------------------------------------------------- 421 SavingTrustManager(X509TrustManager inParentTrustMgr) 422 { 423 mParentTrustMgr = inParentTrustMgr; 424 } 425 426 //----------------------------------------------------------------------- 427 @Override 428 public void checkClientTrusted(X509Certificate[] inCertChain, String inAuthType) 429 throws CertificateException 430 { 431 mParentTrustMgr.checkClientTrusted(inCertChain, inAuthType); 432 } 433 434 //----------------------------------------------------------------------- 435 @Override 436 public void checkServerTrusted(X509Certificate[] inCertChain, String inAuthType) 437 throws CertificateException 438 { 439 CertificateException exceptionToRethrow = null; 440 // check the certificate chain against the system truststore 441 try 442 { 443 mParentTrustMgr.checkServerTrusted(inCertChain, inAuthType); 444 } 445 catch (CertificateException e) 446 { 447 // The certificate chain was found untrustworthy 448 449 // Check if the first certificate in the chain is not known yet stored 450 if (! this.mAllCertificates.contains(inCertChain[0])) 451 { 452 // Save the exception to be re-thrown later if not known 453 exceptionToRethrow = e; 454 } 455 } 456 457 458 // Save the full chain to both local accumulators 459 for (X509Certificate cert : inCertChain) 460 { 461 mAllCertificates.add(cert); 462 mNewCertificates.add(cert); 463 } 464 465 // check and re-throw the exception if any 466 if (exceptionToRethrow != null) 467 { 468 throw exceptionToRethrow; 469 } 470 } 471 472 //----------------------------------------------------------------------- 473 @Override 474 public X509Certificate[] getAcceptedIssuers() 475 { 476 return mParentTrustMgr.getAcceptedIssuers(); 477 } 478 } 479}