001 /* 002 * Copyright 2008-2016 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005 /* 006 * Copyright (C) 2008-2016 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021 package com.unboundid.util.ssl; 022 023 024 import java.io.BufferedReader; 025 import java.io.BufferedWriter; 026 import java.io.File; 027 import java.io.FileReader; 028 import java.io.FileWriter; 029 import java.io.InputStream; 030 import java.io.InputStreamReader; 031 import java.io.IOException; 032 import java.io.PrintStream; 033 import java.security.MessageDigest; 034 import java.security.cert.CertificateException; 035 import java.security.cert.X509Certificate; 036 import java.util.Date; 037 import java.util.concurrent.ConcurrentHashMap; 038 import javax.net.ssl.X509TrustManager; 039 import javax.security.auth.x500.X500Principal; 040 041 042 import static com.unboundid.util.Debug.*; 043 import static com.unboundid.util.StaticUtils.*; 044 import static com.unboundid.util.ssl.SSLMessages.*; 045 046 047 048 /** 049 * This class provides an SSL trust manager that will interactively prompt the 050 * user to determine whether to trust any certificate that is presented to it. 051 * It provides the ability to cache information about certificates that had been 052 * previously trusted so that the user is not prompted about the same 053 * certificate repeatedly, and it can be configured to store trusted 054 * certificates in a file so that the trust information can be persisted. 055 */ 056 public final class PromptTrustManager 057 implements X509TrustManager 058 { 059 /** 060 * The message digest that will be used for MD5 hashes. 061 */ 062 private static final MessageDigest MD5; 063 064 065 066 /** 067 * The message digest that will be used for SHA-1 hashes. 068 */ 069 private static final MessageDigest SHA1; 070 071 072 073 static 074 { 075 MessageDigest d = null; 076 try 077 { 078 d = MessageDigest.getInstance("MD5"); 079 } 080 catch (final Exception e) 081 { 082 debugException(e); 083 throw new RuntimeException(e); 084 } 085 MD5 = d; 086 087 d = null; 088 try 089 { 090 d = MessageDigest.getInstance("SHA-1"); 091 } 092 catch (final Exception e) 093 { 094 debugException(e); 095 throw new RuntimeException(e); 096 } 097 SHA1 = d; 098 } 099 100 101 102 // Indicates whether to examine the validity dates for the certificate in 103 // addition to whether the certificate has been previously trusted. 104 private final boolean examineValidityDates; 105 106 // The set of previously-accepted certificates. The certificates will be 107 // mapped from an all-lowercase hexadecimal string representation of the 108 // certificate signature to a flag that indicates whether the certificate has 109 // already been manually trusted even if it is outside of the validity window. 110 private final ConcurrentHashMap<String,Boolean> acceptedCerts; 111 112 // The input stream from which the user input will be read. 113 private final InputStream in; 114 115 // The print stream that will be used to display the prompt. 116 private final PrintStream out; 117 118 // The path to the file to which the set of accepted certificates should be 119 // persisted. 120 private final String acceptedCertsFile; 121 122 123 124 /** 125 * Creates a new instance of this prompt trust manager. It will cache trust 126 * information in memory but not on disk. 127 */ 128 public PromptTrustManager() 129 { 130 this(null, true, null, null); 131 } 132 133 134 135 /** 136 * Creates a new instance of this prompt trust manager. It may optionally 137 * cache trust information on disk. 138 * 139 * @param acceptedCertsFile The path to a file in which the certificates 140 * that have been previously accepted will be 141 * cached. It may be {@code null} if the cache 142 * should only be maintained in memory. 143 */ 144 public PromptTrustManager(final String acceptedCertsFile) 145 { 146 this(acceptedCertsFile, true, null, null); 147 } 148 149 150 151 /** 152 * Creates a new instance of this prompt trust manager. It may optionally 153 * cache trust information on disk, and may also be configured to examine or 154 * ignore validity dates. 155 * 156 * @param acceptedCertsFile The path to a file in which the certificates 157 * that have been previously accepted will be 158 * cached. It may be {@code null} if the cache 159 * should only be maintained in memory. 160 * @param examineValidityDates Indicates whether to reject certificates if 161 * the current time is outside the validity 162 * window for the certificate. 163 * @param in The input stream that will be used to read 164 * input from the user. If this is {@code null} 165 * then {@code System.in} will be used. 166 * @param out The print stream that will be used to display 167 * the prompt to the user. If this is 168 * {@code null} then System.out will be used. 169 */ 170 public PromptTrustManager(final String acceptedCertsFile, 171 final boolean examineValidityDates, 172 final InputStream in, final PrintStream out) 173 { 174 this.acceptedCertsFile = acceptedCertsFile; 175 this.examineValidityDates = examineValidityDates; 176 177 if (in == null) 178 { 179 this.in = System.in; 180 } 181 else 182 { 183 this.in = in; 184 } 185 186 if (out == null) 187 { 188 this.out = System.out; 189 } 190 else 191 { 192 this.out = out; 193 } 194 195 acceptedCerts = new ConcurrentHashMap<String,Boolean>(); 196 197 if (acceptedCertsFile != null) 198 { 199 BufferedReader r = null; 200 try 201 { 202 final File f = new File(acceptedCertsFile); 203 if (f.exists()) 204 { 205 r = new BufferedReader(new FileReader(f)); 206 while (true) 207 { 208 final String line = r.readLine(); 209 if (line == null) 210 { 211 break; 212 } 213 acceptedCerts.put(line, false); 214 } 215 } 216 } 217 catch (Exception e) 218 { 219 debugException(e); 220 } 221 finally 222 { 223 if (r != null) 224 { 225 try 226 { 227 r.close(); 228 } 229 catch (Exception e) 230 { 231 debugException(e); 232 } 233 } 234 } 235 } 236 } 237 238 239 240 /** 241 * Writes an updated copy of the trusted certificate cache to disk. 242 * 243 * @throws IOException If a problem occurs. 244 */ 245 private void writeCacheFile() 246 throws IOException 247 { 248 final File tempFile = new File(acceptedCertsFile + ".new"); 249 250 BufferedWriter w = null; 251 try 252 { 253 w = new BufferedWriter(new FileWriter(tempFile)); 254 255 for (final String certBytes : acceptedCerts.keySet()) 256 { 257 w.write(certBytes); 258 w.newLine(); 259 } 260 } 261 finally 262 { 263 if (w != null) 264 { 265 w.close(); 266 } 267 } 268 269 final File cacheFile = new File(acceptedCertsFile); 270 if (cacheFile.exists()) 271 { 272 final File oldFile = new File(acceptedCertsFile + ".previous"); 273 if (oldFile.exists()) 274 { 275 oldFile.delete(); 276 } 277 278 cacheFile.renameTo(oldFile); 279 } 280 281 tempFile.renameTo(cacheFile); 282 } 283 284 285 286 /** 287 * Indicates whether this trust manager would interactively prompt the user 288 * about whether to trust the provided certificate chain. 289 * 290 * @param chain The chain of certificates for which to make the 291 * determination. 292 * 293 * @return {@code true} if this trust manger would interactively prompt the 294 * user about whether to trust the certificate chain, or 295 * {@code false} if not (e.g., because the certificate is already 296 * known to be trusted). 297 */ 298 public synchronized boolean wouldPrompt(final X509Certificate[] chain) 299 { 300 // See if the certificate is in the cache. If it isn't then we will 301 // prompt no matter what. 302 final X509Certificate c = chain[0]; 303 final String certBytes = toLowerCase(toHex(c.getSignature())); 304 final Boolean acceptedRegardlessOfValidity = acceptedCerts.get(certBytes); 305 if (acceptedRegardlessOfValidity == null) 306 { 307 return true; 308 } 309 310 311 // If we shouldn't check validity dates, or if the certificate has already 312 // been accepted when it's outside the validity window, then we won't 313 // prompt. 314 if (acceptedRegardlessOfValidity || (! examineValidityDates)) 315 { 316 return false; 317 } 318 319 320 // If the certificate is within the validity window, then we won't prompt. 321 // If it's outside the validity window, then we will prompt to make sure the 322 // user still wants to trust it. 323 final Date currentDate = new Date(); 324 return (! (currentDate.before(c.getNotBefore()) || 325 currentDate.after(c.getNotAfter()))); 326 } 327 328 329 330 /** 331 * Performs the necessary validity check for the provided certificate array. 332 * 333 * @param chain The chain of certificates for which to make the 334 * determination. 335 * @param serverCert Indicates whether the certificate was presented as a 336 * server certificate or as a client certificate. 337 * 338 * @throws CertificateException If the provided certificate chain should not 339 * be trusted. 340 */ 341 private synchronized void checkCertificateChain(final X509Certificate[] chain, 342 final boolean serverCert) 343 throws CertificateException 344 { 345 // See if the certificate is currently within the validity window. 346 String validityWarning = null; 347 final Date currentDate = new Date(); 348 final X509Certificate c = chain[0]; 349 if (examineValidityDates) 350 { 351 if (currentDate.before(c.getNotBefore())) 352 { 353 validityWarning = WARN_PROMPT_NOT_YET_VALID.get(); 354 } 355 else if (currentDate.after(c.getNotAfter())) 356 { 357 validityWarning = WARN_PROMPT_EXPIRED.get(); 358 } 359 } 360 361 362 // If the certificate is within the validity window, or if we don't care 363 // about validity dates, then see if it's in the cache. 364 if ((! examineValidityDates) || (validityWarning == null)) 365 { 366 final String certBytes = toLowerCase(toHex(c.getSignature())); 367 final Boolean accepted = acceptedCerts.get(certBytes); 368 if (accepted != null) 369 { 370 if ((validityWarning == null) || (! examineValidityDates) || 371 Boolean.TRUE.equals(accepted)) 372 { 373 // The certificate was found in the cache. It's either in the 374 // validity window, we don't care about the validity window, or has 375 // already been manually trusted outside of the validity window. 376 // We'll consider it trusted without the need to re-prompt. 377 return; 378 } 379 } 380 } 381 382 383 // If we've gotten here, then we need to display a prompt to the user. 384 if (serverCert) 385 { 386 out.println(INFO_PROMPT_SERVER_HEADING.get()); 387 } 388 else 389 { 390 out.println(INFO_PROMPT_CLIENT_HEADING.get()); 391 } 392 393 out.println('\t' + INFO_PROMPT_SUBJECT.get( 394 c.getSubjectX500Principal().getName(X500Principal.CANONICAL))); 395 out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get( 396 getFingerprint(c, MD5))); 397 out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get( 398 getFingerprint(c, SHA1))); 399 400 for (int i=1; i < chain.length; i++) 401 { 402 out.println('\t' + INFO_PROMPT_ISSUER_SUBJECT.get(i, 403 chain[i].getSubjectX500Principal().getName( 404 X500Principal.CANONICAL))); 405 out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get( 406 getFingerprint(chain[i], MD5))); 407 out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get( 408 getFingerprint(chain[i], SHA1))); 409 } 410 411 out.println(INFO_PROMPT_VALIDITY.get(String.valueOf(c.getNotBefore()), 412 String.valueOf(c.getNotAfter()))); 413 414 if (chain.length == 1) 415 { 416 out.println(); 417 out.println(WARN_PROMPT_SELF_SIGNED.get()); 418 } 419 420 if (validityWarning != null) 421 { 422 out.println(); 423 out.println(validityWarning); 424 } 425 426 final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 427 while (true) 428 { 429 try 430 { 431 out.println(); 432 out.print(INFO_PROMPT_MESSAGE.get()); 433 out.flush(); 434 final String line = reader.readLine(); 435 if (line == null) 436 { 437 // The input stream has been closed, so we can't prompt for trust, 438 // and should assume it is not trusted. 439 throw new CertificateException( 440 ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get( 441 SSLUtil.certificateToString(chain[0]))); 442 } 443 else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes")) 444 { 445 // The certificate should be considered trusted. 446 break; 447 } 448 else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no")) 449 { 450 // The certificate should not be trusted. 451 throw new CertificateException( 452 ERR_CERTIFICATE_REJECTED_BY_USER.get( 453 SSLUtil.certificateToString(chain[0]))); 454 } 455 } 456 catch (CertificateException ce) 457 { 458 throw ce; 459 } 460 catch (Exception e) 461 { 462 debugException(e); 463 } 464 } 465 466 final String certBytes = toLowerCase(toHex(c.getSignature())); 467 acceptedCerts.put(certBytes, (validityWarning != null)); 468 469 if (acceptedCertsFile != null) 470 { 471 try 472 { 473 writeCacheFile(); 474 } 475 catch (Exception e) 476 { 477 debugException(e); 478 } 479 } 480 } 481 482 483 484 /** 485 * Computes the fingerprint for the provided certificate using the given 486 * digest. 487 * 488 * @param c The certificate for which to obtain the fingerprint. 489 * @param d The message digest to use when creating the fingerprint. 490 * 491 * @return The generated certificate fingerprint. 492 * 493 * @throws CertificateException If a problem is encountered while generating 494 * the certificate fingerprint. 495 */ 496 private static String getFingerprint(final X509Certificate c, 497 final MessageDigest d) 498 throws CertificateException 499 { 500 final byte[] encodedCertBytes = c.getEncoded(); 501 502 final byte[] digestBytes; 503 synchronized (d) 504 { 505 digestBytes = d.digest(encodedCertBytes); 506 } 507 508 final StringBuilder buffer = new StringBuilder(3 * encodedCertBytes.length); 509 toHex(digestBytes, ":", buffer); 510 return buffer.toString(); 511 } 512 513 514 515 /** 516 * Indicate whether to prompt about certificates contained in the cache if the 517 * current time is outside the validity window for the certificate. 518 * 519 * @return {@code true} if the certificate validity time should be examined 520 * for cached certificates and the user should be prompted if they 521 * are expired or not yet valid, or {@code false} if cached 522 * certificates should be accepted even outside of the validity 523 * window. 524 */ 525 public boolean examineValidityDates() 526 { 527 return examineValidityDates; 528 } 529 530 531 532 /** 533 * Checks to determine whether the provided client certificate chain should be 534 * trusted. 535 * 536 * @param chain The client certificate chain for which to make the 537 * determination. 538 * @param authType The authentication type based on the client certificate. 539 * 540 * @throws CertificateException If the provided client certificate chain 541 * should not be trusted. 542 */ 543 public void checkClientTrusted(final X509Certificate[] chain, 544 final String authType) 545 throws CertificateException 546 { 547 checkCertificateChain(chain, false); 548 } 549 550 551 552 /** 553 * Checks to determine whether the provided server certificate chain should be 554 * trusted. 555 * 556 * @param chain The server certificate chain for which to make the 557 * determination. 558 * @param authType The key exchange algorithm used. 559 * 560 * @throws CertificateException If the provided server certificate chain 561 * should not be trusted. 562 */ 563 public void checkServerTrusted(final X509Certificate[] chain, 564 final String authType) 565 throws CertificateException 566 { 567 checkCertificateChain(chain, true); 568 } 569 570 571 572 /** 573 * Retrieves the accepted issuer certificates for this trust manager. This 574 * will always return an empty array. 575 * 576 * @return The accepted issuer certificates for this trust manager. 577 */ 578 public X509Certificate[] getAcceptedIssuers() 579 { 580 return new X509Certificate[0]; 581 } 582 }