001/* 002 * Copyright 2008-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2008-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2008-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.util.ssl; 037 038 039 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.Serializable; 043import java.security.KeyStore; 044import java.security.KeyStoreException; 045import java.security.cert.Certificate; 046import java.security.cert.X509Certificate; 047import java.util.Date; 048import java.util.Enumeration; 049import javax.net.ssl.KeyManager; 050import javax.net.ssl.KeyManagerFactory; 051import javax.security.auth.x500.X500Principal; 052 053import com.unboundid.util.CryptoHelper; 054import com.unboundid.util.Debug; 055import com.unboundid.util.NotMutable; 056import com.unboundid.util.NotNull; 057import com.unboundid.util.Nullable; 058import com.unboundid.util.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061import com.unboundid.util.Validator; 062 063import static com.unboundid.util.ssl.SSLMessages.*; 064 065 066 067/** 068 * This class provides an SSL key manager that may be used to retrieve 069 * certificates from a key store file. By default it will use the default key 070 * store format for the JVM (e.g., "JKS" for Sun-provided Java implementations), 071 * but alternate formats like PKCS12 may be used. 072 */ 073@NotMutable() 074@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 075public final class KeyStoreKeyManager 076 extends WrapperKeyManager 077 implements Serializable 078{ 079 /** 080 * The serial version UID for this serializable class. 081 */ 082 private static final long serialVersionUID = -5202641256733094253L; 083 084 085 086 // The path to the key store file. 087 @NotNull private final String keyStoreFile; 088 089 // The format to use for the key store file. 090 @NotNull private final String keyStoreFormat; 091 092 093 094 /** 095 * Creates a new instance of this key store key manager that provides the 096 * ability to retrieve certificates from the specified key store file. It 097 * will use the default key store format. 098 * 099 * @param keyStoreFile The path to the key store file to use. It must not 100 * be {@code null}. 101 * @param keyStorePIN The PIN to use to access the contents of the key 102 * store. It may be {@code null} if no PIN is required. 103 * 104 * @throws KeyStoreException If a problem occurs while initializing this key 105 * manager. 106 */ 107 public KeyStoreKeyManager(@NotNull final File keyStoreFile, 108 @Nullable final char[] keyStorePIN) 109 throws KeyStoreException 110 { 111 this(keyStoreFile.getAbsolutePath(), keyStorePIN, null, null); 112 } 113 114 115 116 /** 117 * Creates a new instance of this key store key manager that provides the 118 * ability to retrieve certificates from the specified key store file. It 119 * will use the default key store format. 120 * 121 * @param keyStoreFile The path to the key store file to use. It must not 122 * be {@code null}. 123 * @param keyStorePIN The PIN to use to access the contents of the key 124 * store. It may be {@code null} if no PIN is required. 125 * 126 * @throws KeyStoreException If a problem occurs while initializing this key 127 * manager. 128 */ 129 public KeyStoreKeyManager(@NotNull final String keyStoreFile, 130 @Nullable final char[] keyStorePIN) 131 throws KeyStoreException 132 { 133 this(keyStoreFile, keyStorePIN, null, null); 134 } 135 136 137 138 /** 139 * Creates a new instance of this key store key manager that provides the 140 * ability to retrieve certificates from the specified key store file. 141 * 142 * @param keyStoreFile The path to the key store file to use. It must 143 * not be {@code null}. 144 * @param keyStorePIN The PIN to use to access the contents of the key 145 * store. It may be {@code null} if no PIN is 146 * required. 147 * @param keyStoreFormat The format to use for the key store. It may be 148 * {@code null} if the default format should be 149 * used. 150 * @param certificateAlias The nickname of the certificate that should be 151 * selected. It may be {@code null} if any 152 * acceptable certificate found in the keystore may 153 * be used. 154 * 155 * @throws KeyStoreException If a problem occurs while initializing this key 156 * manager. 157 */ 158 public KeyStoreKeyManager(@NotNull final File keyStoreFile, 159 @Nullable final char[] keyStorePIN, 160 @Nullable final String keyStoreFormat, 161 @Nullable final String certificateAlias) 162 throws KeyStoreException 163 { 164 this(keyStoreFile.getAbsolutePath(), keyStorePIN, keyStoreFormat, 165 certificateAlias); 166 } 167 168 169 170 /** 171 * Creates a new instance of this key store key manager that provides the 172 * ability to retrieve certificates from the specified key store file. 173 * 174 * @param keyStoreFile The path to the key store file to use. It must 175 * not be {@code null}. 176 * @param keyStorePIN The PIN to use to access the contents of the key 177 * store. It may be {@code null} if no PIN is 178 * required. 179 * @param keyStoreFormat The format to use for the key store. It may be 180 * {@code null} if the default format should be 181 * used. 182 * @param certificateAlias The nickname of the certificate that should be 183 * selected. It may be {@code null} if any 184 * acceptable certificate found in the keystore may 185 * be used. 186 * 187 * @throws KeyStoreException If a problem occurs while initializing this key 188 * manager. 189 */ 190 public KeyStoreKeyManager(@NotNull final String keyStoreFile, 191 @Nullable final char[] keyStorePIN, 192 @Nullable final String keyStoreFormat, 193 @Nullable final String certificateAlias) 194 throws KeyStoreException 195 { 196 this(keyStoreFile, keyStorePIN, keyStoreFormat, certificateAlias, false); 197 } 198 199 200 201 /** 202 * Creates a new instance of this key store key manager that provides the 203 * ability to retrieve certificates from the specified key store file. 204 * 205 * @param keyStoreFile The path to the key store file to use. It must 206 * not be {@code null}. 207 * @param keyStorePIN The PIN to use to access the contents of the key 208 * store. It may be {@code null} if no PIN is 209 * required. 210 * @param keyStoreFormat The format to use for the key store. It may be 211 * {@code null} if the default format should be 212 * used. 213 * @param certificateAlias The nickname of the certificate that should be 214 * selected. It may be {@code null} if any 215 * acceptable certificate found in the keystore may 216 * be used. 217 * @param validateKeyStore Indicates whether to validate that the provided 218 * key store is acceptable and can actually be used 219 * to obtain a valid certificate. If a certificate 220 * alias was specified, then this will ensure that 221 * the key store contains a valid private key entry 222 * with that alias. If no certificate alias was 223 * specified, then this will ensure that the key 224 * store contains at least one valid private key 225 * entry. 226 * 227 * @throws KeyStoreException If a problem occurs while initializing this key 228 * manager, or if validation fails. 229 */ 230 public KeyStoreKeyManager(@NotNull final File keyStoreFile, 231 @Nullable final char[] keyStorePIN, 232 @Nullable final String keyStoreFormat, 233 @Nullable final String certificateAlias, 234 final boolean validateKeyStore) 235 throws KeyStoreException 236 { 237 this(keyStoreFile.getAbsolutePath(), keyStorePIN, keyStoreFormat, 238 certificateAlias, validateKeyStore); 239 } 240 241 242 243 /** 244 * Creates a new instance of this key store key manager that provides the 245 * ability to retrieve certificates from the specified key store file. 246 * 247 * @param keyStoreFile The path to the key store file to use. It must 248 * not be {@code null}. 249 * @param keyStorePIN The PIN to use to access the contents of the key 250 * store. It may be {@code null} if no PIN is 251 * required. 252 * @param keyStoreFormat The format to use for the key store. It may be 253 * {@code null} if the default format should be 254 * used. 255 * @param certificateAlias The nickname of the certificate that should be 256 * selected. It may be {@code null} if any 257 * acceptable certificate found in the keystore may 258 * be used. 259 * @param validateKeyStore Indicates whether to validate that the provided 260 * key store is acceptable and can actually be used 261 * to obtain a valid certificate. If a certificate 262 * alias was specified, then this will ensure that 263 * the key store contains a valid private key entry 264 * with that alias. If no certificate alias was 265 * specified, then this will ensure that the key 266 * store contains at least one valid private key 267 * entry. 268 * 269 * @throws KeyStoreException If a problem occurs while initializing this key 270 * manager, or if validation fails. 271 */ 272 public KeyStoreKeyManager(@NotNull final String keyStoreFile, 273 @Nullable final char[] keyStorePIN, 274 @Nullable final String keyStoreFormat, 275 @Nullable final String certificateAlias, 276 final boolean validateKeyStore) 277 throws KeyStoreException 278 { 279 super( 280 getKeyManagers(keyStoreFile, keyStorePIN, keyStoreFormat, 281 certificateAlias, validateKeyStore), 282 certificateAlias); 283 284 this.keyStoreFile = keyStoreFile; 285 286 if (keyStoreFormat == null) 287 { 288 this.keyStoreFormat = CryptoHelper.getDefaultKeyStoreType(); 289 } 290 else 291 { 292 this.keyStoreFormat = keyStoreFormat; 293 } 294 } 295 296 297 298 /** 299 * Retrieves the set of key managers that will be wrapped by this key manager. 300 * 301 * @param keyStoreFile The path to the key store file to use. It must 302 * not be {@code null}. 303 * @param keyStorePIN The PIN to use to access the contents of the key 304 * store. It may be {@code null} if no PIN is 305 * required. 306 * @param keyStoreFormat The format to use for the key store. It may be 307 * {@code null} if the default format should be 308 * used. 309 * @param certificateAlias The nickname of the certificate that should be 310 * selected. It may be {@code null} if any 311 * acceptable certificate found in the keystore may 312 * be used. 313 * @param validateKeyStore Indicates whether to validate that the provided 314 * key store is acceptable and can actually be used 315 * to obtain a valid certificate. If a certificate 316 * alias was specified, then this will ensure that 317 * the key store contains a valid private key entry 318 * with that alias. If no certificate alias was 319 * specified, then this will ensure that the key 320 * store contains at least one valid private key 321 * entry. 322 * 323 * @return The set of key managers that will be wrapped by this key manager. 324 * 325 * @throws KeyStoreException If a problem occurs while initializing this key 326 * manager, or if validation fails. 327 */ 328 @NotNull() 329 private static KeyManager[] getKeyManagers( 330 @NotNull final String keyStoreFile, 331 @Nullable final char[] keyStorePIN, 332 @Nullable final String keyStoreFormat, 333 @Nullable final String certificateAlias, 334 final boolean validateKeyStore) 335 throws KeyStoreException 336 { 337 Validator.ensureNotNull(keyStoreFile); 338 339 String type = keyStoreFormat; 340 if (type == null) 341 { 342 type = CryptoHelper.getDefaultKeyStoreType(); 343 } 344 345 final File f = new File(keyStoreFile); 346 if (! f.exists()) 347 { 348 throw new KeyStoreException(ERR_KEYSTORE_NO_SUCH_FILE.get(keyStoreFile)); 349 } 350 351 final KeyStore ks = CryptoHelper.getKeyStore(type); 352 FileInputStream inputStream = null; 353 try 354 { 355 inputStream = new FileInputStream(f); 356 ks.load(inputStream, keyStorePIN); 357 } 358 catch (final Exception e) 359 { 360 Debug.debugException(e); 361 362 throw new KeyStoreException( 363 ERR_KEYSTORE_CANNOT_LOAD.get(keyStoreFile, type, String.valueOf(e)), 364 e); 365 } 366 finally 367 { 368 if (inputStream != null) 369 { 370 try 371 { 372 inputStream.close(); 373 } 374 catch (final Exception e) 375 { 376 Debug.debugException(e); 377 } 378 } 379 } 380 381 if (validateKeyStore) 382 { 383 validateKeyStore(ks, f, keyStorePIN, certificateAlias); 384 } 385 386 try 387 { 388 final KeyManagerFactory factory = CryptoHelper.getKeyManagerFactory(); 389 factory.init(ks, keyStorePIN); 390 return factory.getKeyManagers(); 391 } 392 catch (final Exception e) 393 { 394 Debug.debugException(e); 395 396 throw new KeyStoreException( 397 ERR_KEYSTORE_CANNOT_GET_KEY_MANAGERS.get(keyStoreFile, 398 keyStoreFormat, StaticUtils.getExceptionMessage(e)), 399 e); 400 } 401 } 402 403 404 405 /** 406 * Validates that the provided key store has an appropriate private key entry 407 * in which all certificates in the chain are currently within the validity 408 * window. 409 * 410 * @param keyStore The key store to examine. It must not be 411 * {@code null}. 412 * @param keyStoreFile The file that backs the key store. It must not 413 * be {@code null}. 414 * @param keyStorePIN The PIN to use to access the contents of the key 415 * store. It may be {@code null} if no PIN is 416 * required. 417 * @param certificateAlias The nickname of the certificate that should be 418 * selected. It may be {@code null} if any 419 * acceptable certificate found in the keystore may 420 * be used. 421 * 422 * @throws KeyStoreException If a validation error was encountered. 423 */ 424 private static void validateKeyStore(@NotNull final KeyStore keyStore, 425 @NotNull final File keyStoreFile, 426 @Nullable final char[] keyStorePIN, 427 @Nullable final String certificateAlias) 428 throws KeyStoreException 429 { 430 final KeyStore.ProtectionParameter protectionParameter; 431 if (keyStorePIN == null) 432 { 433 protectionParameter = null; 434 } 435 else 436 { 437 protectionParameter = new KeyStore.PasswordProtection(keyStorePIN); 438 } 439 440 try 441 { 442 if (certificateAlias == null) 443 { 444 final StringBuilder invalidMessages = new StringBuilder(); 445 final Enumeration<String> aliases = keyStore.aliases(); 446 while (aliases.hasMoreElements()) 447 { 448 final String alias = aliases.nextElement(); 449 if (! keyStore.isKeyEntry(alias)) 450 { 451 continue; 452 } 453 454 try 455 { 456 final KeyStore.PrivateKeyEntry entry = 457 (KeyStore.PrivateKeyEntry) 458 keyStore.getEntry(alias, protectionParameter); 459 ensureAllCertificatesInChainAreValid(alias, entry); 460 461 // We found a private key entry in which all certificates in the 462 // chain are within their validity window, so we'll assume that 463 // it's acceptable. 464 return; 465 } 466 catch (final Exception e) 467 { 468 Debug.debugException(e); 469 if (invalidMessages.length() > 0) 470 { 471 invalidMessages.append(" "); 472 } 473 invalidMessages.append(e.getMessage()); 474 } 475 } 476 477 if ( invalidMessages.length() > 0) 478 { 479 // The key store has at least one private key entry, but none of 480 // them are currently valid. 481 throw new KeyStoreException( 482 ERR_KEYSTORE_NO_VALID_PRIVATE_KEY_ENTRIES.get( 483 keyStoreFile.getAbsolutePath(), 484 invalidMessages.toString())); 485 } 486 else 487 { 488 // The key store doesn't have any private key entries. 489 throw new KeyStoreException(ERR_KEYSTORE_NO_PRIVATE_KEY_ENTRIES.get( 490 keyStoreFile.getAbsolutePath())); 491 } 492 } 493 else 494 { 495 if (! keyStore.containsAlias(certificateAlias)) 496 { 497 throw new KeyStoreException(ERR_KEYSTORE_NO_ENTRY_WITH_ALIAS.get( 498 keyStoreFile.getAbsolutePath(), certificateAlias)); 499 } 500 501 if (! keyStore.isKeyEntry(certificateAlias)) 502 { 503 throw new KeyStoreException(ERR_KEYSTORE_ENTRY_NOT_PRIVATE_KEY.get( 504 certificateAlias, keyStoreFile.getAbsolutePath())); 505 } 506 507 final KeyStore.PrivateKeyEntry entry = 508 (KeyStore.PrivateKeyEntry) 509 keyStore.getEntry(certificateAlias, protectionParameter); 510 ensureAllCertificatesInChainAreValid(certificateAlias, entry); 511 } 512 } 513 catch (final KeyStoreException e) 514 { 515 Debug.debugException(e); 516 throw e; 517 } 518 catch (final Exception e) 519 { 520 Debug.debugException(e); 521 throw new KeyStoreException( 522 ERR_KEYSTORE_CANNOT_VALIDATE.get(keyStoreFile.getAbsolutePath(), 523 StaticUtils.getExceptionMessage(e)), 524 e); 525 } 526 } 527 528 529 530 /** 531 * Ensures that all certificates in the provided private key entry's chain are 532 * currently within their validity window. 533 * 534 * @param alias The alias from which the entry was read. It must not be 535 * {@code null}. 536 * @param entry The private key entry to examine. It must not be 537 * {@code null}. 538 * 539 * @throws KeyStoreException If any certificate in the chain is expired or 540 * not yet valid. 541 */ 542 private static void ensureAllCertificatesInChainAreValid( 543 @NotNull final String alias, 544 @NotNull final KeyStore.PrivateKeyEntry entry) 545 throws KeyStoreException 546 { 547 final Date currentTime = new Date(); 548 for (final Certificate cert : entry.getCertificateChain()) 549 { 550 if (cert instanceof X509Certificate) 551 { 552 final X509Certificate c = (X509Certificate) cert; 553 if (currentTime.before(c.getNotBefore())) 554 { 555 throw new KeyStoreException( 556 ERR_KEYSTORE_CERT_NOT_YET_VALID.get(alias, 557 c.getSubjectX500Principal().getName( 558 X500Principal.RFC2253), 559 String.valueOf(c.getNotBefore()))); 560 } 561 else if (currentTime.after(c.getNotAfter())) 562 { 563 throw new KeyStoreException( 564 ERR_KEYSTORE_CERT_EXPIRED.get(alias, 565 c.getSubjectX500Principal().getName( 566 X500Principal.RFC2253), 567 String.valueOf(c.getNotAfter()))); 568 } 569 } 570 } 571 } 572 573 574 575 /** 576 * Retrieves the path to the key store file to use. 577 * 578 * @return The path to the key store file to use. 579 */ 580 @NotNull() 581 public String getKeyStoreFile() 582 { 583 return keyStoreFile; 584 } 585 586 587 588 /** 589 * Retrieves the name of the key store file format. 590 * 591 * @return The name of the key store file format. 592 */ 593 @NotNull() 594 public String getKeyStoreFormat() 595 { 596 return keyStoreFormat; 597 } 598}