001/* 002 * Copyright 2021-2022 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2021-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) 2021-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.extensions; 037 038 039 040import java.io.BufferedInputStream; 041import java.io.File; 042import java.io.FileInputStream; 043import java.io.IOException; 044import java.io.InputStream; 045import java.util.ArrayList; 046import java.util.Arrays; 047import java.util.Collections; 048import java.util.List; 049 050import com.unboundid.asn1.ASN1Element; 051import com.unboundid.asn1.ASN1OctetString; 052import com.unboundid.asn1.ASN1Sequence; 053import com.unboundid.asn1.ASN1StreamReader; 054import com.unboundid.ldap.sdk.LDAPException; 055import com.unboundid.ldap.sdk.ResultCode; 056import com.unboundid.util.Debug; 057import com.unboundid.util.NotMutable; 058import com.unboundid.util.NotNull; 059import com.unboundid.util.Nullable; 060import com.unboundid.util.StaticUtils; 061import com.unboundid.util.ThreadSafety; 062import com.unboundid.util.ThreadSafetyLevel; 063import com.unboundid.util.Validator; 064import com.unboundid.util.ssl.cert.CertException; 065import com.unboundid.util.ssl.cert.PKCS8PEMFileReader; 066import com.unboundid.util.ssl.cert.PKCS8PrivateKey; 067import com.unboundid.util.ssl.cert.X509Certificate; 068import com.unboundid.util.ssl.cert.X509PEMFileReader; 069 070import static com.unboundid.ldap.sdk.unboundidds.extensions.ExtOpMessages.*; 071 072 073 074/** 075 * This class provides a {@link ReplaceCertificateKeyStoreContent} 076 * implementation to indicate that the certificate chain and private key (in 077 * either PEM or DER format) are provided directly in the extended request. 078 * <BR> 079 * <BLOCKQUOTE> 080 * <B>NOTE:</B> This class, and other classes within the 081 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 082 * supported for use against Ping Identity, UnboundID, and 083 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 084 * for proprietary functionality or for external specifications that are not 085 * considered stable or mature enough to be guaranteed to work in an 086 * interoperable way with other types of LDAP servers. 087 * </BLOCKQUOTE> 088 */ 089@NotMutable() 090@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 091public final class CertificateDataReplaceCertificateKeyStoreContent 092 extends ReplaceCertificateKeyStoreContent 093{ 094 /** 095 * The BER type to use for the ASN.1 element containing an encoded 096 * representation of this key store content object. 097 */ 098 static final byte TYPE_KEY_STORE_CONTENT = (byte) 0xA2; 099 100 101 102 /** 103 * The BER type to use for the ASN.1 element that provides the new 104 * certificate chain. 105 */ 106 private static final byte TYPE_CERTIFICATE_CHAIN = (byte) 0xAE; 107 108 109 110 /** 111 * The BER type to use for the ASN.1 element that provides the private key for 112 * the new certificate. 113 */ 114 private static final byte TYPE_PRIVATE_KEY = (byte) 0xAF; 115 116 117 118 /** 119 * The serial version UID for this serializable class. 120 */ 121 private static final long serialVersionUID = 1771837307666073616L; 122 123 124 125 // An encoded representation of the PKCS #8 private key. 126 @Nullable private final byte[] privateKeyData; 127 128 // An encoded representation of the X.509 certificates in the certificate 129 // chain. 130 @NotNull private final List<byte[]> certificateChainData; 131 132 133 134 /** 135 * Creates a new instance of this key store content object with the provided 136 * information. 137 * 138 * @param certificateChainData A list containing the encoded representations 139 * of the X.509 certificates in the new 140 * certificate chain. Each byte array must 141 * contain the PEM or DER representation of a 142 * single certificate in the chain, with the 143 * first certificate being the end-entity 144 * certificate, and each subsequent certificate 145 * being the issuer for the previous 146 * certificate. This must not be {@code null} 147 * or empty. 148 * @param privateKeyData An array containing the encoded 149 * representation of the PKCS #8 private key 150 * for the end-entity certificate in the chain. 151 * It may be encoded in either PEM or DER 152 * format. This may be {@code null} if the 153 * new end-entity certificate uses the same 154 * private key as the certificate currently in 155 * use in the server. 156 */ 157 public CertificateDataReplaceCertificateKeyStoreContent( 158 @NotNull final List<byte[]> certificateChainData, 159 @Nullable final byte[] privateKeyData) 160 { 161 Validator.ensureNotNullOrEmpty(certificateChainData, 162 "CertificateDataReplaceCertificateKeyStoreContent." + 163 "certificateChainData must not be null or empty."); 164 165 this.certificateChainData = Collections.unmodifiableList( 166 new ArrayList<>(certificateChainData)); 167 this.privateKeyData = privateKeyData; 168 } 169 170 171 172 /** 173 * Creates a new instance of this key store content object with the provided 174 * information. 175 * 176 * @param certificateChainFiles A list containing one or more files from 177 * which to read the PEM or DER representations 178 * of the X.509 certificates to include in 179 * the new certificate chain. The order of 180 * the files, and the order of the certificates 181 * in each file, should be arranged such that 182 * the first certificate read is the end-entity 183 * certificate and each subsequent certificate 184 * is the issuer for the previous. This must 185 * not be {@code null} or empty. 186 * @param privateKeyFile A file from which to read the PEM or DER 187 * representation of the PKCS #8 private key 188 * for the end-entity certificate in the chain. 189 * This may be {@code null} if the new 190 * end-entity certificate uses the same private 191 * key as the certificate currently in use in 192 * the server. 193 * 194 * @throws LDAPException If a problem occurs while trying to read or parse 195 * data contained in any of the provided files. 196 */ 197 public CertificateDataReplaceCertificateKeyStoreContent( 198 @NotNull final List<File> certificateChainFiles, 199 @Nullable final File privateKeyFile) 200 throws LDAPException 201 { 202 this(readCertificateChain(certificateChainFiles), 203 ((privateKeyFile == null) ? null : readPrivateKey(privateKeyFile))); 204 } 205 206 207 208 /** 209 * Reads a certificate chain from the given file or set of files. Each file 210 * must contain the PEM or DER representations of one or more X.509 211 * certificates. If a file contains multiple certificates, all certificates 212 * in that file must be either all PEM-formatted or all DER-formatted. 213 * 214 * @param files The set of files from which the certificate chain should be 215 * read. It must not be {@code null} or empty. 216 * 217 * @return A list containing the encoded representation of the X.509 218 * certificates read from the file, with each byte array containing 219 * the encoded representation for one certificate. 220 * 221 * @throws LDAPException If a problem was encountered while attempting to 222 * read from or parse the content of any of the files. 223 */ 224 @NotNull() 225 public static List<byte[]> readCertificateChain(@NotNull final File... files) 226 throws LDAPException 227 { 228 return readCertificateChain(Arrays.asList(files)); 229 } 230 231 232 233 /** 234 * Reads a certificate chain from the given file or set of files. Each file 235 * must contain the PEM or DER representations of one or more X.509 236 * certificates. If a file contains multiple certificates, all certificates 237 * in that file must be either all PEM-formatted or all DER-formatted. 238 * 239 * @param files The set of files from which the certificate chain should be 240 * read. It must not be {@code null} or empty. 241 * 242 * @return A list containing the encoded representation of the X.509 243 * certificates read from the file, with each byte array containing 244 * the encoded representation for one certificate. 245 * 246 * @throws LDAPException If a problem was encountered while attempting to 247 * read from or parse the content of any of the files. 248 */ 249 @NotNull() 250 public static List<byte[]> readCertificateChain( 251 @NotNull final List<File> files) 252 throws LDAPException 253 { 254 Validator.ensureNotNullOrEmpty(files, 255 "CertificateDataReplaceCertificateKeyStoreContent." + 256 "readCertificateChain.files must not be null or empty."); 257 258 final List<byte[]> encodedCerts = new ArrayList<>(); 259 for (final File f : files) 260 { 261 readCertificates(f, encodedCerts); 262 } 263 264 return Collections.unmodifiableList(encodedCerts); 265 } 266 267 268 269 /** 270 * Reads one or more certificates from the specified file. The certificates 271 * may be in either PEM format or DER format, but if there are multiple 272 * certificates in the file, they must all be in the same format. 273 * 274 * @param file The file to be read. It must not be {@code null}. 275 * @param encodedCerts A list that will be updated with the certificates 276 * that are read. This must not be {@code null} and 277 * must be updatable. 278 * 279 * @throws LDAPException If a problem was encountered while attempting to 280 * read from or parse the content of the specified 281 * file. 282 */ 283 private static void readCertificates(@NotNull final File file, 284 @NotNull final List<byte[]> encodedCerts) 285 throws LDAPException 286 { 287 // Open the file for reading. 288 try (FileInputStream fis = new FileInputStream(file); 289 BufferedInputStream bis = new BufferedInputStream(fis)) 290 { 291 // Peek at the first byte of the file. 292 bis.mark(1); 293 final int firstByte = bis.read(); 294 bis.reset(); 295 296 297 // If the file is empty, then throw an exception. 298 if (firstByte < 0x00) 299 { 300 throw new LDAPException(ResultCode.PARAM_ERROR, 301 ERR_CD_KSC_DECODE_ERR_EMPTY_CERT_FILE.get(file.getAbsolutePath())); 302 } 303 304 305 // If the first byte is 0x30, then that indicates that it's the first byte 306 // of a DER sequence. Assume all the certificates in the file are in the 307 // DER format. 308 if (firstByte == 0x30) 309 { 310 readDERCertificates(file, bis, encodedCerts); 311 return; 312 } 313 314 315 // If the file is PEM-formatted, then the first byte will probably be 316 // 0x2D (which is the ASCII '-' character, which will appear at the start 317 // of the "-----BEGIN CERTIFICATE-----" header). However, we also support 318 // blank lines and comment lines starting with '#', so we'll just fall 319 // back to assuing that it's PEM. 320 readPEMCertificates(file, bis, encodedCerts); 321 } 322 catch (final IOException e) 323 { 324 Debug.debugException(e); 325 throw new LDAPException(ResultCode.LOCAL_ERROR, 326 ERR_CD_KSC_DECODE_ERROR_READING_CERT_FILE.get(file.getAbsolutePath(), 327 StaticUtils.getExceptionMessage(e)), 328 e); 329 } 330 } 331 332 333 334 /** 335 * Reads one or more DER-formatted X.509 certificates from the given input 336 * stream. 337 * 338 * @param file The file with which the provided input stream is 339 * associated. It must not be {@code null}. 340 * @param inputStream The input stream from which the certificates are to 341 * be read. It must not be {@code null}. 342 * @param encodedCerts A list that will be updated with the certificates 343 * that are read. This must not be {@code null} and 344 * must be updatable. 345 * 346 * @throws LDAPException If a problem occurs while trying to read from the 347 * file or parse the data as ASN.1 DER elements. 348 */ 349 private static void readDERCertificates( 350 @NotNull final File file, 351 @NotNull final InputStream inputStream, 352 @NotNull final List<byte[]> encodedCerts) 353 throws LDAPException 354 { 355 try (ASN1StreamReader asn1Reader = new ASN1StreamReader(inputStream)) 356 { 357 while (true) 358 { 359 final ASN1Element element = asn1Reader.readElement(); 360 if (element == null) 361 { 362 return; 363 } 364 365 encodedCerts.add(element.encode()); 366 } 367 } 368 catch (final IOException e) 369 { 370 Debug.debugException(e); 371 372 // Even though it's possible that it's an I/O problem, it's actually much 373 // more likely to be a decoding problem. 374 throw new LDAPException(ResultCode.DECODING_ERROR, 375 ERR_CD_KSC_DECODE_DER_CERT_ERROR.get(file.getAbsolutePath(), 376 StaticUtils.getExceptionMessage(e)), 377 e); 378 } 379 } 380 381 382 383 /** 384 * Reads one or more PEM-formatted X.509 certificates from the given input 385 * stream. 386 * 387 * @param file The file with which the provided input stream is 388 * associated. It must not be {@code null}. 389 * @param inputStream The input stream from which the certificates are to 390 * be read. It must not be {@code null}. 391 * @param encodedCerts A list that will be updated with the certificates 392 * that are read. This must not be {@code null} and 393 * must be updatable. 394 * 395 * @throws IOException If a problem occurs while trying to read from the 396 * file. 397 * 398 * @throws LDAPException If the contents of the file cannot be parsed as a 399 * valid set of PEM-formatted certificates. 400 */ 401 private static void readPEMCertificates( 402 @NotNull final File file, 403 @NotNull final InputStream inputStream, 404 @NotNull final List<byte[]> encodedCerts) 405 throws IOException, LDAPException 406 { 407 try (X509PEMFileReader pemReader = new X509PEMFileReader(inputStream)) 408 { 409 while (true) 410 { 411 final X509Certificate cert = pemReader.readCertificate(); 412 if (cert == null) 413 { 414 return; 415 } 416 417 encodedCerts.add(cert.getX509CertificateBytes()); 418 } 419 } 420 catch (final CertException e) 421 { 422 Debug.debugException(e); 423 throw new LDAPException(ResultCode.DECODING_ERROR, 424 ERR_CD_KSC_DECODE_PEM_CERT_ERROR.get(file.getAbsolutePath(), 425 e.getMessage()), 426 e); 427 } 428 } 429 430 431 432 /** 433 * Reads a PKCS #8 private key from the given file. The file must contain the 434 * PEM or DER representation of a single private key. 435 * 436 * @param file The file from which the private key should be read. It must 437 * not be {@code null}. 438 * 439 * @return The encoded representation of the PKCS #8 private key that was 440 * read. 441 * 442 * @throws LDAPException If a problem occurs while trying to read from 443 * or parse the content of the specified file. 444 */ 445 @NotNull() 446 public static byte[] readPrivateKey(@NotNull final File file) 447 throws LDAPException 448 { 449 Validator.ensureNotNull(file, 450 "CertificateDataReplaceCertificateKeyStoreContent." + 451 "readPrivateKey.file must not be null."); 452 453 454 // Open the file for reading. 455 try (FileInputStream fis = new FileInputStream(file); 456 BufferedInputStream bis = new BufferedInputStream(fis)) 457 { 458 // Read the first byte of the file. 459 bis.mark(1); 460 final int firstByte = bis.read(); 461 bis.reset(); 462 463 464 // If the file is empty, then throw an exception, as that's not allowed. 465 if (firstByte < 0) 466 { 467 throw new LDAPException(ResultCode.PARAM_ERROR, 468 ERR_CD_KSC_DECODE_ERROR_EMPTY_PK_FILE.get(file.getAbsolutePath())); 469 } 470 471 472 // If the first byte is 0x30, then that indicates it's a DER sequence. 473 if (firstByte == 0x30) 474 { 475 return readDERPrivateKey(file, bis); 476 } 477 478 479 // Assume that the file is PEM-formatted. 480 return readPEMPrivateKey(file, bis); 481 } 482 catch (final IOException e) 483 { 484 Debug.debugException(e); 485 throw new LDAPException(ResultCode.DECODING_ERROR, 486 ERR_CD_KSC_DECODE_ERROR_READING_PK_FILE.get(file.getAbsolutePath(), 487 StaticUtils.getExceptionMessage(e)), 488 e); 489 } 490 } 491 492 493 494 /** 495 * Reads a DER-formatted PKCS #8 private key from the provided input stream. 496 * 497 * @param file The file with which the provided input stream is 498 * associated. It must not be {@code null}. 499 * @param inputStream The input stream from which the private key will be 500 * read. It must not be {@code null}. 501 * 502 * @return The bytes that comprise the encoded PKCS #8 private key. 503 * 504 * @throws LDAPException If a problem occurs while attempting to read the 505 * private key data from the given file. 506 */ 507 @NotNull() 508 private static byte[] readDERPrivateKey( 509 @NotNull final File file, 510 @NotNull final InputStream inputStream) 511 throws LDAPException 512 { 513 try (ASN1StreamReader asn1Reader = new ASN1StreamReader(inputStream)) 514 { 515 final ASN1Element element = asn1Reader.readElement(); 516 if (asn1Reader.readElement() != null) 517 { 518 throw new LDAPException(ResultCode.DECODING_ERROR, 519 ERR_CD_KSC_DECODE_MULTIPLE_DER_KEYS_IN_FILE.get( 520 file.getAbsolutePath())); 521 } 522 523 return element.encode(); 524 } 525 catch (final IOException e) 526 { 527 Debug.debugException(e); 528 529 // Even though it's possible that it's an I/O problem, it's actually much 530 // more likely to be a decoding problem. 531 throw new LDAPException(ResultCode.DECODING_ERROR, 532 ERR_CD_KSC_DECODE_DER_PK_ERROR.get(file.getAbsolutePath(), 533 StaticUtils.getExceptionMessage(e)), 534 e); 535 } 536 } 537 538 539 540 /** 541 * Reads a PEM-formatted PKCS #8 private key from the provided input stream. 542 * 543 * @param file The file with which the provided input stream is 544 * associated. It must not be {@code null}. 545 * @param inputStream The input stream from which the private key will be 546 * read. It must not be {@code null}. 547 * 548 * @return The bytes that comprise the encoded PKCS #8 private key. 549 * 550 * @throws IOException If a problem occurs while trying to read from the 551 * file. 552 * 553 * @throws LDAPException If the contents of the file cannot be parsed as a 554 * valid PEM-formatted PKCS #8 private key. 555 */ 556 @NotNull() 557 private static byte[] readPEMPrivateKey( 558 @NotNull final File file, 559 @NotNull final InputStream inputStream) 560 throws IOException, LDAPException 561 { 562 try (PKCS8PEMFileReader pemReader = new PKCS8PEMFileReader(inputStream)) 563 { 564 final PKCS8PrivateKey privateKey = pemReader.readPrivateKey(); 565 if (pemReader.readPrivateKey() != null) 566 { 567 throw new LDAPException(ResultCode.DECODING_ERROR, 568 ERR_CD_KSC_DECODE_MULTIPLE_PEM_KEYS_IN_FILE.get( 569 file.getAbsolutePath())); 570 } 571 572 return privateKey.getPKCS8PrivateKeyBytes(); 573 } 574 catch (final CertException e) 575 { 576 Debug.debugException(e); 577 throw new LDAPException(ResultCode.DECODING_ERROR, 578 ERR_CD_KSC_DECODE_PEM_PK_ERROR.get(file.getAbsolutePath(), 579 e.getMessage()), 580 e); 581 } 582 } 583 584 585 586 /** 587 * Retrieves a list of the DER-formatted or PEM-formatted representations of 588 * the X.509 certificates in the new certificate chain. 589 * 590 * @return A list of the encoded representations of the X.509 certificates 591 * in the new certificate chain. 592 */ 593 @NotNull() 594 public List<byte[]> getCertificateChainData() 595 { 596 return certificateChainData; 597 } 598 599 600 601 /** 602 * Retrieves the DER-formatted or PEM-formatted PKCS #8 private key for the 603 * new certificate, if available. 604 * 605 * @return The encoded representation of the PKCS #8 private key for the new 606 * certificate, or {@code null} if the new certificate should use the 607 * same private key as the current certificate. 608 */ 609 @Nullable() 610 public byte[] getPrivateKeyData() 611 { 612 return privateKeyData; 613 } 614 615 616 617 /** 618 * Decodes a key store file replace certificate key store content object from 619 * the provided ASN.1 element. 620 * 621 * @param element The ASN.1 element containing the encoded representation of 622 * the key store file replace certificate key store content 623 * object. It must not be {@code null}. 624 * 625 * @return The decoded key store content object. 626 * 627 * @throws LDAPException If the provided ASN.1 element cannot be decoded as 628 * a key store file replace certificate key store 629 * content object. 630 */ 631 @NotNull() 632 static CertificateDataReplaceCertificateKeyStoreContent decodeInternal( 633 @NotNull final ASN1Element element) 634 throws LDAPException 635 { 636 try 637 { 638 final ASN1Element[] elements = element.decodeAsSequence().elements(); 639 640 final ASN1Element[] chainElements = 641 elements[0].decodeAsSequence().elements(); 642 final List<byte[]> chainBytes = new ArrayList<>(); 643 for (final ASN1Element e : chainElements) 644 { 645 chainBytes.add(e.decodeAsOctetString().getValue()); 646 } 647 648 byte[] pkBytes = null; 649 for (int i=1; i < elements.length; i++) 650 { 651 if (elements[i].getType() == TYPE_PRIVATE_KEY) 652 { 653 pkBytes = elements[i].decodeAsOctetString().getValue(); 654 } 655 } 656 657 return new CertificateDataReplaceCertificateKeyStoreContent( 658 chainBytes, pkBytes); 659 } 660 catch (final Exception e) 661 { 662 Debug.debugException(e); 663 throw new LDAPException(ResultCode.DECODING_ERROR, 664 ERR_CD_KSC_DECODE_ERROR.get(StaticUtils.getExceptionMessage(e)), 665 e); 666 } 667 } 668 669 670 671 /** 672 * {@inheritDoc} 673 */ 674 @Override() 675 @NotNull() 676 public ASN1Element encode() 677 { 678 final List<ASN1Element> elements = new ArrayList<>(2); 679 680 final List<ASN1Element> chainElements = 681 new ArrayList<>(certificateChainData.size()); 682 for (final byte[] certBytes : certificateChainData) 683 { 684 chainElements.add(new ASN1OctetString(certBytes)); 685 } 686 elements.add(new ASN1Sequence(TYPE_CERTIFICATE_CHAIN, chainElements)); 687 688 if (privateKeyData != null) 689 { 690 elements.add(new ASN1OctetString(TYPE_PRIVATE_KEY, privateKeyData)); 691 } 692 693 return new ASN1Sequence(TYPE_KEY_STORE_CONTENT, elements); 694 } 695 696 697 698 /** 699 * {@inheritDoc} 700 */ 701 @Override() 702 public void toString(@NotNull final StringBuilder buffer) 703 { 704 buffer.append("CertificateDataReplaceCertificateKeyStoreContent(" + 705 "certificateChainLength="); 706 buffer.append(certificateChainData.size()); 707 buffer.append(", privateProvided="); 708 buffer.append(privateKeyData != null); 709 buffer.append(')'); 710 } 711}