001/* 002 * Copyright 2026 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2026 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) 2026 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.Serializable; 041import java.security.cert.CertificateException; 042import java.util.HashSet; 043import java.util.Iterator; 044import java.util.Set; 045import javax.net.ssl.X509TrustManager; 046 047import com.unboundid.util.Debug; 048import com.unboundid.util.NotMutable; 049import com.unboundid.util.NotNull; 050import com.unboundid.util.StaticUtils; 051import com.unboundid.util.ThreadSafety; 052import com.unboundid.util.ThreadSafetyLevel; 053import com.unboundid.util.ssl.cert.CertException; 054import com.unboundid.util.ssl.cert.X509Certificate; 055 056import static com.unboundid.util.ssl.SSLMessages.*; 057 058 059 060/** 061 * This class provides an X.509 trust manager implementation that can be used to 062 * verify signatures in a certificate chain. It will verify that self-signed 063 * certificates have valid signatures, and that if multiple certificates are 064 * provided, they form a valid chain in which each subsequent certificate has 065 * signed the previous certificate. This implementation is somewhat lenient 066 * when it comes to certificate order: the first certificate must be the 067 * end-entity certificate, but subsequent certificates can appear in any order. 068 * Further, if the chain includes duplicate certificates, then the duplicates 069 * will be ignored so that only one instance of each certificate will be 070 * examined. 071 * <BR><BR> 072 * Note that this trust manager should not be used on its own, since it only 073 * validates the certificate chain that was actually presented by the peer. 074 * If the presented chain is incomplete (which is acceptable if there is reason 075 * to expect that the peer can complete the chain using its own set of trusted 076 * issuers), then this trust manager will not attempt to verify that the last 077 * certificate presented by the peer was actually signed by one of those 078 * trusted issuers. And even in the case where the full chain is presented, 079 * this trust manager only attempts to verify that each subsequent certificate 080 * is the one used to sign the previous one, and does not attempt to make any 081 * determination about whether any of the end-entity or issuer certificates are 082 * trustworthy. 083 */ 084@NotMutable() 085@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 086 public final class VerifyChainSignaturesTrustManager 087 implements X509TrustManager, Serializable 088{ 089 /** 090 * A pre-allocated empty certificate array. 091 */ 092 @NotNull private static final java.security.cert.X509Certificate[] 093 NO_CERTIFICATES = new java.security.cert.X509Certificate[0]; 094 095 096 097 /** 098 * The singleton instance of this trust manager. 099 */ 100 @NotNull private static final VerifyChainSignaturesTrustManager INSTANCE = 101 new VerifyChainSignaturesTrustManager(); 102 103 104 105 /** 106 * The serial version UID for this serializable class. 107 */ 108 private static final long serialVersionUID = 2712657207894506440L; 109 110 111 112 /** 113 * Creates a new instance of this trust manager. 114 */ 115 private VerifyChainSignaturesTrustManager() 116 { 117 // No implementation is required. 118 } 119 120 121 122 /** 123 * Retrieves a singleton instance of this issuer chain trust manager. 124 * 125 * @return A singleton instance of this issuer chain trust manager. 126 */ 127 @NotNull() 128 public static VerifyChainSignaturesTrustManager getInstance() 129 { 130 return INSTANCE; 131 } 132 133 134 135 /** 136 * Checks to determine whether the provided client certificate chain should be 137 * trusted. 138 * 139 * @param chain The client certificate chain for which to make the 140 * determination. 141 * @param authType The authentication type based on the client certificate. 142 * 143 * @throws CertificateException If the provided client certificate chain 144 * should not be trusted. 145 */ 146 @Override() 147 public void checkClientTrusted( 148 @NotNull final java.security.cert.X509Certificate[] chain, 149 @NotNull final String authType) 150 throws CertificateException 151 { 152 validateIssuerChain(chain); 153 } 154 155 156 157 /** 158 * Checks to determine whether the provided server certificate chain should be 159 * trusted. 160 * 161 * @param chain The server certificate chain for which to make the 162 * determination. 163 * @param authType The key exchange algorithm used. 164 * 165 * @throws CertificateException If the provided server certificate chain 166 * should not be trusted. 167 */ 168 @Override() 169 public void checkServerTrusted( 170 @NotNull final java.security.cert.X509Certificate[] chain, 171 @NotNull final String authType) 172 throws CertificateException 173 { 174 validateIssuerChain(chain); 175 } 176 177 178 179 /** 180 * Ensures that the certificates in the provided array represent a valid chain 181 * of issuer certificates. 182 * 183 * @param chain The certificate chain to validate. 184 * 185 * @throws CertificateException If the provided array does not represent a 186 * valid chain of issuer certificates. 187 */ 188 private void validateIssuerChain( 189 @NotNull final java.security.cert.X509Certificate[] chain) 190 throws CertificateException 191 { 192 // If the provided chain is null or empty, then we will not accept the 193 // certificate chain. 194 if ((chain == null) || (chain.length < 1)) 195 { 196 throw new CertificateException( 197 ERR_VERIFY_CHAIN_SIGNATURES_TRUST_NO_CHAIN.get()); 198 } 199 200 201 // Convert the certificates in the provided chain to use the LDAP SDK's 202 // representation of the certificates. The first certificate is one that 203 // we will treat as the end-entity certificate, and the others will be 204 // treated as potential issuers. Technically, they should be in the right 205 // order, but we'll be lenient in that regard. 206 final X509Certificate endEntityCertificate = parseCertificate( 207 chain[0].getEncoded(), 208 String.valueOf(chain[0].getSubjectX500Principal()), 0); 209 final Set<X509Certificate> issuerCertificates = new HashSet<>(); 210 for (int i=1; i < chain.length; i++) 211 { 212 issuerCertificates.add(parseCertificate(chain[i].getEncoded(), 213 String.valueOf(chain[i].getSubjectX500Principal()), i)); 214 } 215 216 217 // If the end-entity certificate happened to be included multiple times in 218 // the chain, then remove it from the set of issuer certificates. 219 issuerCertificates.remove(endEntityCertificate); 220 221 222 // If the end-entity certificate is self-signed, then validate its 223 // signature. Also, make sure that the chain didn't include any other 224 // certificates, since they wouldn't be related to the chain. 225 if (endEntityCertificate.isSelfSigned()) 226 { 227 try 228 { 229 endEntityCertificate.verifySignature(endEntityCertificate); 230 } 231 catch (final CertException e) 232 { 233 Debug.debugException(e); 234 throw new CertificateException(e.getMessage(), e); 235 } 236 237 if (! issuerCertificates.isEmpty()) 238 { 239 throw new CertificateException( 240 ERR_VERIFY_CHAIN_SIGNATURES_TRUST_UNRELATED_CERTS.get( 241 String.valueOf(endEntityCertificate.getSubjectDN()))); 242 } 243 244 return; 245 } 246 247 248 // If the chain only has a single certificate, then we don't need to 249 // perform any other validation. This can happen if the end-entity 250 // certificate is self-signed (which we've already accounted for), or if 251 // we're supposed to be able to complete the chain from a known set of 252 // trusted issuers (which is outside the scope of this trust manager). 253 if (issuerCertificates.isEmpty()) 254 { 255 return; 256 } 257 258 259 // At this point, we know that the end-entity certificate isn't self-signed, 260 // and that at least one more certificate was included in the presented 261 // chain. Make sure that we can form a chain from the presented 262 // certificates. 263 X509Certificate previousCertificate = endEntityCertificate; 264 while (! issuerCertificates.isEmpty()) 265 { 266 // Try to find the issuer of the certificate we most recently examined. 267 boolean foundIssuer = false; 268 final Iterator<X509Certificate> iterator = issuerCertificates.iterator(); 269 while (iterator.hasNext()) 270 { 271 final X509Certificate potentialIssuer = iterator.next(); 272 if (potentialIssuer.isIssuerFor(previousCertificate)) 273 { 274 try 275 { 276 previousCertificate.verifySignature(potentialIssuer); 277 previousCertificate = potentialIssuer; 278 iterator.remove(); 279 foundIssuer = true; 280 } 281 catch (final Exception e) 282 { 283 Debug.debugException(e); 284 throw new CertificateException( 285 ERR_VERIFY_CHAIN_SIGNATURES_ISSUER_SIGNATURE_INVALID.get( 286 String.valueOf(previousCertificate.getSubjectDN()), 287 String.valueOf(potentialIssuer.getSubjectDN())), 288 e); 289 } 290 } 291 } 292 293 294 // If we didn't find the issuer in the set of certificates, then that's an 295 // error. 296 if (! foundIssuer) 297 { 298 throw new CertificateException( 299 ERR_VERIFY_CHAIN_SIGNATURES_COULD_NOT_FIND_ISSUER.get( 300 String.valueOf(previousCertificate.getSubjectDN()))); 301 } 302 303 304 // If the issuer certificate is self-signed, then make sure that its 305 // signature is valid, and also that there aren't any other remaining 306 // certificates. 307 if (previousCertificate.isSelfSigned()) 308 { 309 try 310 { 311 previousCertificate.verifySignature(previousCertificate); 312 } 313 catch (final CertException e) 314 { 315 Debug.debugException(e); 316 throw new CertificateException(e.getMessage(), e); 317 } 318 319 if (! issuerCertificates.isEmpty()) 320 { 321 throw new CertificateException( 322 ERR_VERIFY_CHAIN_SIGNATURES_TRUST_UNRELATED_CERTS.get( 323 String.valueOf(endEntityCertificate.getSubjectDN()))); 324 } 325 326 return; 327 } 328 } 329 330 331 // If we've gotten here, then it means that we've gone through all of the 332 // issuers included in the chain, and that the last certificate we found 333 // wasn't self-signed. This just means that the peer didn't provide the 334 // complete certificate chain, and expects us to complete it from a set of 335 // trusted issuers. That's outside the scope of this trust manager, so 336 // we're fine exiting without throwing an exception. 337 } 338 339 340 341 /** 342 * Parses the contents of the provided byte array as an X.509 certificate. 343 * 344 * @param certBytes The byte array containing the certificate data to 345 * parse. 346 * @param certSubjectDN The string representation of the expected subject DN 347 * for the certificate. 348 * @param index The index of the certificate in the chain. 349 * 350 * @return The parsed certificate. 351 * 352 * @throws CertificateException If the contents of the provided byte array 353 * can't be parsed as an X.509 certificate. 354 */ 355 @NotNull() 356 static X509Certificate parseCertificate( 357 @NotNull final byte[] certBytes, 358 @NotNull final String certSubjectDN, 359 final int index) 360 throws CertificateException 361 { 362 try 363 { 364 return new X509Certificate(certBytes); 365 } 366 catch (final Exception e) 367 { 368 Debug.debugException(e); 369 throw new CertificateException( 370 ERR_VERIFY_CHAIN_SIGNATURES_TRUST_CANNOT_PARSE_CERT.get( 371 certSubjectDN, index, StaticUtils.getExceptionMessage(e))); 372 } 373 } 374 375 376 377 /** 378 * Retrieves the accepted issuer certificates for this trust manager. This 379 * will always return an empty array. 380 * 381 * @return The accepted issuer certificates for this trust manager. 382 */ 383 @Override() 384 @NotNull() 385 public java.security.cert.X509Certificate[] getAcceptedIssuers() 386 { 387 return NO_CERTIFICATES; 388 } 389}