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.cert;
037
038
039
040import java.io.BufferedReader;
041import java.io.Closeable;
042import java.io.File;
043import java.io.FileInputStream;
044import java.io.IOException;
045import java.io.InputStream;
046import java.io.InputStreamReader;
047
048import com.unboundid.util.Base64;
049import com.unboundid.util.Debug;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.Nullable;
052import com.unboundid.util.StaticUtils;
053import com.unboundid.util.ThreadSafety;
054import com.unboundid.util.ThreadSafetyLevel;
055
056import static com.unboundid.util.ssl.cert.CertMessages.*;
057
058
059
060/**
061 * This class provides a mechanism for reading PEM-encoded X.509 certificates
062 * from a specified file.  The PEM file may contain zero or more certificates.
063 * Each certificate should consist of the following:
064 * <UL>
065 *   <LI>A line containing only the string "-----BEGIN CERTIFICATE-----".</LI>
066 *   <LI>One or more lines representing the base64-encoded representation of the
067 *       bytes that comprise the X.509 certificate.</LI>
068 *   <LI>A line containing only the string "-----END CERTIFICATE-----".</LI>
069 * </UL>
070 * <BR><BR>
071 * Any spaces that appear at the beginning or end of each line will be ignored.
072 * Empty lines and lines that start with the octothorpe (#) character will also
073 * be ignored.
074 */
075@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
076public final class X509PEMFileReader
077       implements Closeable
078{
079  /**
080   * The header string that should appear on a line by itself before the
081   * base64-encoded representation of the bytes that comprise an X.509
082   * certificate.
083   */
084  @NotNull public static final String BEGIN_CERTIFICATE_HEADER =
085       "-----BEGIN CERTIFICATE-----";
086
087
088
089  /**
090   * The footer string that should appear on a line by itself after the
091   * base64-encoded representation of the bytes that comprise an X.509
092   * certificate.
093   */
094  @NotNull public static final String END_CERTIFICATE_FOOTER =
095       "-----END CERTIFICATE-----";
096
097
098
099  // The reader that will be used to consume data from the PEM file.
100  @NotNull private final BufferedReader reader;
101
102
103
104  /**
105   * Creates a new X.509 PEM file reader that will read certificate information
106   * from the specified file.
107   *
108   * @param  pemFilePath  The path to the PEM file from which the certificates
109   *                      should be read.  This must not be {@code null} and the
110   *                      file must exist.
111   *
112   * @throws  IOException  If a problem occurs while attempting to open the file
113   *                       for reading.
114   */
115  public X509PEMFileReader(@NotNull final String pemFilePath)
116         throws IOException
117  {
118    this(new File(pemFilePath));
119  }
120
121
122
123  /**
124   * Creates a new X.509 PEM file reader that will read certificate information
125   * from the specified file.
126   *
127   * @param  pemFile  The PEM file from which the certificates should be read.
128   *                  This must not be {@code null} and the file must
129   *                  exist.
130   *
131   * @throws  IOException  If a problem occurs while attempting to open the file
132   *                       for reading.
133   */
134  public X509PEMFileReader(@NotNull final File pemFile)
135         throws IOException
136  {
137    this(new FileInputStream(pemFile));
138  }
139
140
141
142  /**
143   * Creates a new X.509 PEM file reader that will read certificate information
144   * from the provided input stream.
145   *
146   * @param  inputStream  The input stream from which the certificates should
147   *                      be read.  This must not be {@code null} and it must be
148   *                      open for reading.
149   */
150  public X509PEMFileReader(@NotNull final InputStream inputStream)
151  {
152    reader = new BufferedReader(new InputStreamReader(inputStream));
153  }
154
155
156
157  /**
158   * Reads the next certificate from the PEM file.
159   *
160   * @return  The certificate that was read, or {@code null} if the end of the
161   *          file has been reached.
162   *
163   * @throws  IOException  If a problem occurs while trying to read data from
164   *                       the PEM file.
165   *
166   * @throws  CertException  If a problem occurs while trying to interpret data
167   *                         read from the PEM file as an X.509 certificate.
168   */
169  @Nullable()
170  public X509Certificate readCertificate()
171         throws IOException, CertException
172  {
173    boolean beginFound = false;
174    final StringBuilder base64Buffer = new StringBuilder();
175
176    while (true)
177    {
178      final String line = reader.readLine();
179      if (line == null)
180      {
181        // We hit the end of the file.  If we read a begin header, then that's
182        // an error.
183        if (beginFound)
184        {
185          throw new CertException(ERR_X509_PEM_READER_EOF_WITHOUT_END.get(
186               END_CERTIFICATE_FOOTER, BEGIN_CERTIFICATE_HEADER));
187        }
188
189        return null;
190      }
191
192      final String trimmedLine = line.trim();
193      if (trimmedLine.isEmpty() || trimmedLine.startsWith("#"))
194      {
195        continue;
196      }
197
198      final String upperLine = StaticUtils.toUpperCase(trimmedLine);
199      if (BEGIN_CERTIFICATE_HEADER.equals(upperLine))
200      {
201        if (beginFound)
202        {
203          throw new CertException(ERR_X509_PEM_READER_REPEATED_BEGIN.get(
204               BEGIN_CERTIFICATE_HEADER));
205        }
206        else
207        {
208          beginFound = true;
209        }
210      }
211      else if (END_CERTIFICATE_FOOTER.equals(upperLine))
212      {
213        if (! beginFound)
214        {
215          throw new CertException(ERR_X509_PEM_READER_END_WITHOUT_BEGIN.get(
216               END_CERTIFICATE_FOOTER, BEGIN_CERTIFICATE_HEADER));
217        }
218        else if (base64Buffer.length() == 0)
219        {
220          throw new CertException(ERR_X509_PEM_READER_END_WITHOUT_DATA.get(
221               END_CERTIFICATE_FOOTER, BEGIN_CERTIFICATE_HEADER));
222        }
223        else
224        {
225          final byte[] x509Bytes;
226          try
227          {
228            x509Bytes = Base64.decode(base64Buffer.toString());
229          }
230          catch (final Exception e)
231          {
232            Debug.debugException(e);
233            throw new CertException(
234                 ERR_X509_PEM_READER_CANNOT_BASE64_DECODE.get(), e);
235          }
236
237          return new X509Certificate(x509Bytes);
238        }
239      }
240      else
241      {
242        if (! beginFound)
243        {
244          throw new CertException(ERR_X509_PEM_READER_DATA_WITHOUT_BEGIN.get(
245               BEGIN_CERTIFICATE_HEADER));
246        }
247
248        base64Buffer.append(trimmedLine);
249      }
250    }
251  }
252
253
254
255  /**
256   * Closes this X.509 PEM file reader.
257   *
258   * @throws  IOException  If a problem is encountered while attempting to close
259   *                       the reader.
260   */
261  @Override()
262  public void close()
263         throws IOException
264  {
265    reader.close();
266  }
267}