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}