001/* 002 * Copyright 2020-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2022 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) 2020-2022 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.ldap.sdk.unboundidds; 037 038 039 040import java.io.ByteArrayInputStream; 041import java.io.File; 042import java.io.IOException; 043import java.io.Serializable; 044import java.security.cert.CertificateException; 045import java.security.cert.X509Certificate; 046import java.util.Collections; 047import java.util.HashSet; 048import java.util.Set; 049import java.util.concurrent.atomic.AtomicLong; 050import java.util.concurrent.atomic.AtomicReference; 051import javax.net.ssl.X509TrustManager; 052import javax.security.auth.x500.X500Principal; 053 054import com.unboundid.ldap.sdk.Attribute; 055import com.unboundid.ldap.sdk.Entry; 056import com.unboundid.ldif.LDIFException; 057import com.unboundid.ldif.LDIFReader; 058import com.unboundid.util.Base64; 059import com.unboundid.util.CryptoHelper; 060import com.unboundid.util.Debug; 061import com.unboundid.util.NotNull; 062import com.unboundid.util.StaticUtils; 063import com.unboundid.util.ThreadSafety; 064import com.unboundid.util.ThreadSafetyLevel; 065 066import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 067 068 069 070/** 071 * This class provides an implementation of an X.509 trust manager that can be 072 * used to trust certificates listed in the topology registry of a Ping Identity 073 * Directory Server instance. It will read the topology registry from the 074 * server's configuration file rather than communicating with it over LDAP, so 075 * it is only available for use when run from LDAP tools provided with the 076 * Ping Identity Directory Server. 077 * <BR> 078 * <BLOCKQUOTE> 079 * <B>NOTE:</B> This class, and other classes within the 080 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 081 * supported for use against Ping Identity, UnboundID, and 082 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 083 * for proprietary functionality or for external specifications that are not 084 * considered stable or mature enough to be guaranteed to work in an 085 * interoperable way with other types of LDAP servers. 086 * </BLOCKQUOTE> 087 */ 088@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 089public final class TopologyRegistryTrustManager 090 implements X509TrustManager, Serializable 091{ 092 /** 093 * The name of the object class that will be used in entries that may provide 094 * information about inter-server certificates in the topology registry. 095 */ 096 @NotNull private static final String INTER_SERVER_CERT_OC = 097 "ds-cfg-server-instance"; 098 099 100 101 /** 102 * The name of the attribute type for attributes that provide information 103 * about inter-server certificates in the topology registry. 104 */ 105 @NotNull private static final String INTER_SERVER_CERT_AT = 106 "ds-cfg-inter-server-certificate"; 107 108 109 110 /** 111 * The name of the object class that will be used in entries that may provide 112 * information about listener certificates in the topology registry. 113 */ 114 @NotNull private static final String LISTENER_CERT_OC = 115 "ds-cfg-server-instance-listener"; 116 117 118 119 /** 120 * The name of the attribute type for attributes that provide information 121 * about listener certificates in the topology registry. 122 */ 123 @NotNull private static final String LISTENER_CERT_AT = 124 "ds-cfg-listener-certificate"; 125 126 127 128 /** 129 * A pre-allocated empty certificate array. 130 */ 131 @NotNull static final X509Certificate[] NO_CERTIFICATES = 132 new X509Certificate[0]; 133 134 135 136 /** 137 * The serial version UID for this serializable class. 138 */ 139 private static final long serialVersionUID = -1535917071172094611L; 140 141 142 143 // The time that the cached certificates will expire. 144 @NotNull private final AtomicLong cacheExpirationTime; 145 146 // The certificates that have been cached. 147 @NotNull private final AtomicReference<Set<X509Certificate>> 148 cachedCertificates; 149 150 // The configuration file from which the certificate records will be read. 151 @NotNull private final File configurationFile; 152 153 // The maximum length of time in milliseconds that previously loaded 154 // certificates may be cached. 155 private final long cacheDurationMillis; 156 157 158 159 /** 160 * Creates a new instance of this trust manager with the provided settings. 161 * 162 * @param configurationFile The configuration file for the Ping Identity 163 * Directory Server instance that holds the 164 * topology registry data. 165 * @param cacheDurationMillis The maximum length of time in milliseconds 166 * that previously loaded certificates may be 167 * cached. If this is less than or equal to 168 * zero, then certificates will not be cached. 169 */ 170 public TopologyRegistryTrustManager(@NotNull final File configurationFile, 171 final long cacheDurationMillis) 172 { 173 this.configurationFile = configurationFile; 174 this.cacheDurationMillis = cacheDurationMillis; 175 176 cacheExpirationTime = new AtomicLong(0L); 177 cachedCertificates = new AtomicReference<>( 178 Collections.<X509Certificate>emptySet()); 179 } 180 181 182 183 /** 184 * Checks to determine whether the provided client certificate chain should be 185 * trusted. 186 * 187 * @param chain The client certificate chain for which to make the 188 * determination. 189 * @param authType The authentication type based on the client certificate. 190 * 191 * @throws CertificateException If the provided client certificate chain 192 * should not be trusted. 193 */ 194 @Override() 195 public void checkClientTrusted(@NotNull final X509Certificate[] chain, 196 @NotNull final String authType) 197 throws CertificateException 198 { 199 checkTrusted(chain); 200 } 201 202 203 204 /** 205 * Checks to determine whether the provided server certificate chain should be 206 * trusted. 207 * 208 * @param chain The server certificate chain for which to make the 209 * determination. 210 * @param authType The key exchange algorithm used. 211 * 212 * @throws CertificateException If the provided server certificate chain 213 * should not be trusted. 214 */ 215 @Override() 216 public void checkServerTrusted(@NotNull final X509Certificate[] chain, 217 @NotNull final String authType) 218 throws CertificateException 219 { 220 checkTrusted(chain); 221 } 222 223 224 225 /** 226 * Ensures that the provided certificate chain should be trusted. 227 * 228 * @param chain The certificate chain to validated. 229 * 230 * @throws CertificateException If the certificate chain should not be 231 * trusted. 232 */ 233 private void checkTrusted(@NotNull final X509Certificate[] chain) 234 throws CertificateException 235 { 236 // Make sure that the chain is not null or empty. 237 if ((chain == null) || (chain.length == 0)) 238 { 239 throw new CertificateException(ERR_TR_TM_NO_CHAIN.get()); 240 } 241 242 243 // Validate that the peer certificate is currently within its validity 244 // window. 245 final long currentTime = System.currentTimeMillis(); 246 final X509Certificate peerCert = chain[0]; 247 if (currentTime < peerCert.getNotBefore().getTime()) 248 { 249 throw new CertificateException(ERR_TR_TM_PEER_NOT_YET_VALID.get( 250 peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253), 251 String.valueOf(peerCert.getNotBefore()))); 252 } 253 254 if (currentTime > peerCert.getNotAfter().getTime()) 255 { 256 throw new CertificateException(ERR_TR_TM_PEER_EXPIRED.get( 257 peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253), 258 String.valueOf(peerCert.getNotAfter()))); 259 } 260 261 262 // Validate that all of the issuer certificates are also valid. 263 for (int i=1; i < chain.length; i++) 264 { 265 final X509Certificate issuerCert = chain[i]; 266 if (currentTime < issuerCert.getNotBefore().getTime()) 267 { 268 throw new CertificateException(ERR_TR_TM_ISSUER_NOT_YET_VALID.get( 269 peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253), 270 issuerCert.getSubjectX500Principal().getName( 271 X500Principal.RFC2253), 272 String.valueOf(peerCert.getNotBefore()))); 273 } 274 275 if (currentTime > issuerCert.getNotAfter().getTime()) 276 { 277 throw new CertificateException(ERR_TR_TM_ISSUER_EXPIRED.get( 278 peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253), 279 issuerCert.getSubjectX500Principal().getName( 280 X500Principal.RFC2253), 281 String.valueOf(peerCert.getNotAfter()))); 282 } 283 } 284 285 286 // If the cache is valid and it contains the certificate, then we'll trust 287 // it. 288 if ((cacheExpirationTime.get() >= currentTime) && 289 cachedCertificates.get().contains(peerCert)) 290 { 291 // The certificate is trusted. Return without throwing an exception. 292 return; 293 } 294 295 296 // Read the config file and get the certificates it contains. 297 final Set<X509Certificate> topologyRegistryCertificates = 298 readTopologyRegistryCertificates(peerCert); 299 if (cacheDurationMillis > 0L) 300 { 301 cachedCertificates.set(topologyRegistryCertificates); 302 cacheExpirationTime.set(currentTime + cacheDurationMillis); 303 } 304 305 if (topologyRegistryCertificates.contains(peerCert)) 306 { 307 // The certificate is trusted. Return without throwing an exception. 308 return; 309 } 310 else 311 { 312 throw new CertificateException(ERR_TP_TM_PEER_NOT_FOUND.get( 313 peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253))); 314 } 315 } 316 317 318 319 /** 320 * Reads the certificates defined in the topology registry. 321 * 322 * @param peerCert The peer certificate presented for evaluation. 323 * 324 * @return A set containing the certificates defined in the topology 325 * registry, or an empty set if no certificates are found. 326 * 327 * @throws CertificateException If a problem is encountered while reading 328 * certificates from the topology registry. 329 */ 330 @NotNull() 331 private Set<X509Certificate> readTopologyRegistryCertificates( 332 @NotNull final X509Certificate peerCert) 333 throws CertificateException 334 { 335 try (LDIFReader ldifReader = new LDIFReader(configurationFile)) 336 { 337 final Set<X509Certificate> certs = new HashSet<>(); 338 while (true) 339 { 340 final Entry entry; 341 try 342 { 343 entry = ldifReader.readEntry(); 344 } 345 catch (final LDIFException e) 346 { 347 Debug.debugException(e); 348 if (e.mayContinueReading()) 349 { 350 continue; 351 } 352 else 353 { 354 throw new CertificateException( 355 ERR_TP_TM_MALFORMED_CONFIG.get( 356 peerCert.getSubjectX500Principal().getName( 357 X500Principal.RFC2253), 358 configurationFile.getAbsolutePath(), 359 StaticUtils.getExceptionMessage(e)), 360 e); 361 } 362 } 363 364 if (entry == null) 365 { 366 return Collections.unmodifiableSet(certs); 367 } 368 369 if (entry.hasObjectClass(INTER_SERVER_CERT_OC) && 370 entry.hasAttribute(INTER_SERVER_CERT_AT)) 371 { 372 parseCertificates(certs, entry.getAttribute(INTER_SERVER_CERT_AT)); 373 } 374 else if (entry.hasObjectClass(LISTENER_CERT_OC) && 375 entry.hasAttribute(LISTENER_CERT_AT)) 376 { 377 parseCertificates(certs, entry.getAttribute(LISTENER_CERT_AT)); 378 } 379 } 380 } 381 catch (final IOException e) 382 { 383 Debug.debugException(e); 384 throw new CertificateException( 385 ERR_TP_TM_ERROR_READING_CONFIG_FILE.get( 386 peerCert.getSubjectX500Principal().getName( 387 X500Principal.RFC2253), 388 configurationFile.getAbsolutePath(), 389 StaticUtils.getExceptionMessage(e)), 390 e); 391 } 392 } 393 394 395 396 /** 397 * Parses any values of the provided attribute as a set of X.509 certificates. 398 * 399 * @param certs The set that should be updated with the certificates that 400 * are parsed. 401 * @param attr The attribute whose values should be parsed. 402 */ 403 private void parseCertificates(@NotNull final Set<X509Certificate> certs, 404 @NotNull final Attribute attr) 405 { 406 final StringBuilder certBase64 = new StringBuilder(); 407 for (final String value : attr.getValues()) 408 { 409 try 410 { 411 for (final String line : StaticUtils.stringToLines(value)) 412 { 413 if (line.equalsIgnoreCase("-----BEGIN CERTIFICATE-----")) 414 { 415 continue; 416 } 417 else if (line.equalsIgnoreCase("-----END CERTIFICATE-----")) 418 { 419 final byte[] certBytes = Base64.decode(certBase64.toString()); 420 certBase64.setLength(0); 421 422 certs.add((X509Certificate) CryptoHelper.getCertificateFactory( 423 "X.509").generateCertificate(new ByteArrayInputStream( 424 certBytes))); 425 } 426 else 427 { 428 certBase64.append(line); 429 } 430 } 431 } 432 catch (final Exception e) 433 { 434 Debug.debugException(e); 435 } 436 } 437 } 438 439 440 441 /** 442 * Retrieves the accepted issuer certificates for this trust manager. 443 * 444 * @return The accepted issuer certificates for this trust manager, or an 445 * empty set of accepted issuers if a problem was encountered while 446 * initializing this trust manager. 447 */ 448 @Override() 449 @NotNull() 450 public X509Certificate[] getAcceptedIssuers() 451 { 452 return NO_CERTIFICATES; 453 } 454}