001/* 002 * Copyright 2021-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2021-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) 2021-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.IOException; 042import java.io.Serializable; 043import java.security.KeyStoreException; 044import java.security.cert.CertificateException; 045import java.security.cert.X509Certificate; 046import java.util.ArrayList; 047import java.util.Collections; 048import java.util.HashMap; 049import java.util.List; 050import java.util.Map; 051import javax.net.ssl.X509TrustManager; 052import javax.security.auth.x500.X500Principal; 053 054import com.unboundid.util.Debug; 055import com.unboundid.util.NotMutable; 056import com.unboundid.util.NotNull; 057import com.unboundid.util.StaticUtils; 058import com.unboundid.util.ThreadSafety; 059import com.unboundid.util.ThreadSafetyLevel; 060import com.unboundid.util.Validator; 061import com.unboundid.util.ssl.cert.CertException; 062import com.unboundid.util.ssl.cert.X509PEMFileReader; 063 064import static com.unboundid.util.ssl.SSLMessages.*; 065 066 067 068/** 069 * This class provides an implementation of an X.509 trust manager that can 070 * obtain information about trusted issuers from one or more PEM files. 071 */ 072@NotMutable() 073@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 074public final class PEMFileTrustManager 075 implements X509TrustManager, Serializable 076{ 077 /** 078 * The serial version UID for this serializable class. 079 */ 080 private static final long serialVersionUID = 1973401278035832777L; 081 082 083 084 // The map of trusted certificates read from the PEM files. 085 @NotNull private final Map<com.unboundid.util.ssl.cert.X509Certificate, 086 X509Certificate> trustedCertificates; 087 088 089 090 /** 091 * Creates a new PEM file trust manager that will read trusted certificate 092 * information from the specified PEM files. 093 * 094 * @param pemFiles The PEM files from which to read the trusted certificate 095 * information. It must not be {@code null} or empty, and 096 * all files must exist. Each element may be a file (which 097 * may contain one or more PEM-formatted certificates) or a 098 * directory (in which case all of the files in that 099 * directory, including subdirectories will be recursively 100 * processed). 101 * 102 * @throws KeyStoreException If a problem occurs while trying to read or 103 * decode any of the certificates. 104 */ 105 public PEMFileTrustManager(@NotNull final File... pemFiles) 106 throws KeyStoreException 107 { 108 this(StaticUtils.toList(pemFiles)); 109 } 110 111 112 113 /** 114 * Creates a new PEM file trust manager that will read trusted certificate 115 * information from the specified PEM files. 116 * 117 * @param pemFiles The PEM files from which to read the trusted certificate 118 * information. It must not be {@code null} or empty, and 119 * all files must exist. Each element may be a file (which 120 * may contain one or more PEM-formatted certificates) or a 121 * directory (in which case all of the files in that 122 * directory, including subdirectories will be recursively 123 * processed). 124 * 125 * @throws KeyStoreException If a problem occurs while trying to read or 126 * decode any of the certificates. 127 */ 128 public PEMFileTrustManager(@NotNull final List<File> pemFiles) 129 throws KeyStoreException 130 { 131 Validator.ensureNotNullWithMessage(pemFiles, 132 "PEMFileTrustManager.pemFiles must not be null."); 133 Validator.ensureFalse(pemFiles.isEmpty(), 134 "PEMFileTrustManager.pemFiles must not be empty."); 135 136 final Map<com.unboundid.util.ssl.cert.X509Certificate,X509Certificate> 137 certMap = new HashMap<>(); 138 for (final File f : pemFiles) 139 { 140 readTrustedCertificates(f, certMap); 141 } 142 143 trustedCertificates = Collections.unmodifiableMap(certMap); 144 } 145 146 147 148 /** 149 * Reads trusted certificate information from the specified PEM file. 150 * 151 * @param f The PEM file to examine. It must not be {@code null}, and it 152 * must reference a file that exists. If it is a directory, then 153 * all files contained in it (including subdirectories) will be 154 * recursively processed. 155 * @param m The map to be updated wth the certificates read from the PEM 156 * files. It must not be {@code null} and must be updatable. 157 * 158 * @throws KeyStoreException If a problem is encountered while reading 159 * trusted certificate information from the 160 * specified file. 161 */ 162 private static void readTrustedCertificates(@NotNull final File f, 163 @NotNull final Map<com.unboundid.util.ssl.cert.X509Certificate, 164 X509Certificate> m) 165 throws KeyStoreException 166 { 167 if (! f.exists()) 168 { 169 throw new KeyStoreException( 170 ERR_PEM_FILE_TRUST_MANAGER_NO_SUCH_FILE.get(f.getAbsolutePath())); 171 } 172 173 try 174 { 175 if (f.isDirectory()) 176 { 177 for (final File fileInDir : f.listFiles()) 178 { 179 readTrustedCertificates(fileInDir, m); 180 } 181 } 182 else 183 { 184 try (X509PEMFileReader r = new X509PEMFileReader(f)) 185 { 186 boolean readCert = false; 187 while (true) 188 { 189 final com.unboundid.util.ssl.cert.X509Certificate cert = 190 r.readCertificate(); 191 if (cert == null) 192 { 193 if (! readCert) 194 { 195 throw new KeyStoreException( 196 ERR_PEM_FILE_TRUST_MANAGER_EMPTY_FILE.get( 197 f.getAbsolutePath())); 198 } 199 200 break; 201 } 202 203 readCert = true; 204 205 final X509Certificate c = (X509Certificate) cert.toCertificate(); 206 m.put(cert, c); 207 } 208 } 209 } 210 } 211 catch (final KeyStoreException e) 212 { 213 Debug.debugException(e); 214 throw e; 215 } 216 catch (final IOException e) 217 { 218 Debug.debugException(e); 219 throw new KeyStoreException( 220 ERR_PEM_FILE_TRUST_MANAGER_ERROR_READING_FILE.get( 221 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)), 222 e); 223 } 224 catch (final CertException e) 225 { 226 Debug.debugException(e); 227 throw new KeyStoreException( 228 ERR_PEM_FILE_TRUST_MANAGER_ERROR_PARSING_CERT.get( 229 f.getAbsolutePath(), e.getMessage()), 230 e); 231 } 232 catch (final Exception e) 233 { 234 Debug.debugException(e); 235 throw new KeyStoreException( 236 ERR_PEM_FILE_TRUST_MANAGER_ERROR_PROCESSING_FILE.get( 237 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)), 238 e); 239 } 240 } 241 242 243 244 /** 245 * Determines whether the provided client certificate chain should be 246 * considered trusted based on the trusted certificate information read from 247 * PEM files. 248 * 249 * @param chain The client certificate chain for which to make the 250 * determination. It must not be {@code null} or empty. 251 * @param authType The type of authentication to use based on the client 252 * certificate. It must not be {@code null}. 253 * 254 * @throws CertificateException If the provided certificate chain should not 255 * be considered trusted. 256 */ 257 @Override() 258 public void checkClientTrusted(@NotNull final X509Certificate[] chain, 259 @NotNull final String authType) 260 throws CertificateException 261 { 262 try 263 { 264 checkTrusted(chain); 265 } 266 catch (final CertificateException e) 267 { 268 Debug.debugException(e); 269 throw new CertificateException( 270 ERR_PEM_FILE_TRUST_MANAGER_CLIENT_NOT_TRUSTED.get(e.getMessage()), 271 e); 272 } 273 } 274 275 276 277 /** 278 * Determines whether the provided server certificate chain should be 279 * considered trusted based on the trusted certificate information read from 280 * PEM files. 281 * 282 * @param chain The server certificate chain for which to make the 283 * determination. It must not be {@code null} or empty. 284 * @param authType The type of authentication to use based on the server 285 * certificate. It must not be {@code null}. 286 * 287 * @throws CertificateException If the provided certificate chain should not 288 * be considered trusted. 289 */ 290 @Override() 291 public void checkServerTrusted(@NotNull final X509Certificate[] chain, 292 @NotNull final String authType) 293 throws CertificateException 294 { 295 try 296 { 297 checkTrusted(chain); 298 } 299 catch (final CertificateException e) 300 { 301 Debug.debugException(e); 302 throw new CertificateException( 303 ERR_PEM_FILE_TRUST_MANAGER_SERVER_NOT_TRUSTED.get(e.getMessage()), 304 e); 305 } 306 } 307 308 309 310 /** 311 * Determines whether the provided certificate chain should be considered 312 * trusted based on the trusted certificate information read from PEM files. 313 * Note that this method assumes that the trusted certificate information read 314 * from PEM files should be authoritative, and therefore doesn't perform some 315 * types of validation (like ensuring that all issuer certificates are trusted 316 * rather than validating that at least one is trusted, or checking extensions 317 * like basic constraints). 318 * 319 * @param chain The certificate chain for which to make the determination. 320 * It must not be {@code null} or empty. 321 * 322 * @throws CertificateException If the provided certificate chain should not 323 * be considered trusted. 324 */ 325 private void checkTrusted(@NotNull final X509Certificate[] chain) 326 throws CertificateException 327 { 328 // If the chain is null or empty, then it cannot be trusted. 329 if ((chain == null) || (chain.length == 0)) 330 { 331 throw new CertificateException( 332 ERR_PEM_FILE_TRUST_MANAGER_EMPTY_CHAIN.get()); 333 } 334 335 336 // Iterate through all the certificates in the chain, parsing them using the 337 // LDAP SDK's X.509 certificate representation, and performing all of the 338 // following validation: 339 // 340 // - Make sure that the certificate is within the validity window. 341 // 342 // - Make sure that each subsequent certificate in the chain is the issuer 343 // for the previous certificate. 344 // 345 // - Check to see whether at least one of the certificates in the chain 346 // matches one read from the set of PEM files. 347 boolean foundCertificate = false; 348 com.unboundid.util.ssl.cert.X509Certificate firstCertificate = null; 349 com.unboundid.util.ssl.cert.X509Certificate previousCertificate = null; 350 for (final X509Certificate c : chain) 351 { 352 final com.unboundid.util.ssl.cert.X509Certificate parsedCertificate; 353 try 354 { 355 parsedCertificate = new com.unboundid.util.ssl.cert.X509Certificate( 356 c.getEncoded()); 357 } 358 catch (final CertException e) 359 { 360 Debug.debugException(e); 361 throw new CertificateException( 362 ERR_PEM_FILE_TRUST_MANAGER_CANNOT_PARSE_CERT_FROM_CHAIN.get( 363 c.getSubjectX500Principal().getName(X500Principal.RFC2253), 364 StaticUtils.getExceptionMessage(e)), 365 e); 366 } 367 368 if (firstCertificate == null) 369 { 370 firstCertificate = parsedCertificate; 371 } 372 373 if (! parsedCertificate.isWithinValidityWindow()) 374 { 375 throw new CertificateException( 376 ERR_PEM_FILE_TRUST_MANAGER_CERT_NOT_VALID.get( 377 String.valueOf(parsedCertificate.getSubjectDN()), 378 StaticUtils.encodeRFC3339Time( 379 parsedCertificate.getNotBeforeDate()), 380 StaticUtils.encodeRFC3339Time( 381 parsedCertificate.getNotAfterDate()))); 382 } 383 384 if ((previousCertificate != null) && 385 (! parsedCertificate.isIssuerFor(previousCertificate))) 386 { 387 throw new CertificateException( 388 ERR_PEM_FILE_TRUST_MANAGER_CERT_NOT_ISSUER.get( 389 String.valueOf(parsedCertificate.getSubjectDN()), 390 String.valueOf(previousCertificate.getSubjectDN()))); 391 } 392 393 foundCertificate |= trustedCertificates.containsKey(parsedCertificate); 394 previousCertificate = parsedCertificate; 395 } 396 397 398 // If we didn't find any of the presented certificates in the trust store, 399 // then it may be that an incomplete chain was presented. If the last 400 // certificate in the chain is not self-signed, then check to see if any of 401 // the certificates in the trust store were an issuer for that certificate. 402 if ((! foundCertificate) && (! previousCertificate.isSelfSigned())) 403 { 404 for (final com.unboundid.util.ssl.cert.X509Certificate c : 405 trustedCertificates.keySet()) 406 { 407 if (c.isIssuerFor(previousCertificate)) 408 { 409 foundCertificate = true; 410 break; 411 } 412 } 413 } 414 415 if (! foundCertificate) 416 { 417 throw new CertificateException(ERR_PEM_FILE_TRUST_MANAGER_NOT_TRUSTED.get( 418 String.valueOf(firstCertificate.getSubjectDN()))); 419 } 420 } 421 422 423 424 /** 425 * Retrieves an array of the issuer certificates that will be considered 426 * trusted. 427 * 428 * @return An array of the issuer certificates that will be considered 429 * trusted, or an empty array if no issuers will be trusted. 430 */ 431 @Override() 432 @NotNull() 433 public X509Certificate[] getAcceptedIssuers() 434 { 435 // Include all certificates that are currently within their validity window. 436 final long currentTime = System.currentTimeMillis(); 437 final List<X509Certificate> certList = 438 new ArrayList<>(trustedCertificates.size()); 439 for (final Map.Entry<com.unboundid.util.ssl.cert.X509Certificate, 440 X509Certificate> e : trustedCertificates.entrySet()) 441 { 442 if (e.getKey().isWithinValidityWindow(currentTime)) 443 { 444 certList.add(e.getValue()); 445 } 446 } 447 448 final X509Certificate[] certArray = new X509Certificate[certList.size()]; 449 return certList.toArray(certArray); 450 } 451}