View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.ssl;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.Unpooled;
21  import io.netty.handler.codec.base64.Base64;
22  import io.netty.util.CharsetUtil;
23  import io.netty.util.internal.PlatformDependent;
24  import io.netty.util.internal.logging.InternalLogger;
25  import io.netty.util.internal.logging.InternalLoggerFactory;
26  
27  import java.io.ByteArrayOutputStream;
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.FileNotFoundException;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.OutputStream;
34  import java.security.KeyException;
35  import java.security.KeyStore;
36  import java.security.cert.CertificateException;
37  import java.util.ArrayList;
38  import java.util.List;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  
42  /**
43   * Reads a PEM file and converts it into a list of DERs so that they are imported into a {@link KeyStore} easily.
44   */
45  final class PemReader {
46  
47      private static final InternalLogger logger = InternalLoggerFactory.getInstance(PemReader.class);
48  
49      private static final Pattern CERT_HEADER = Pattern.compile(
50              "-+BEGIN\\s[^-\\r\\n]*CERTIFICATE[^-\\r\\n]*-+(?:\\s|\\r|\\n)+");
51      private static final Pattern CERT_FOOTER = Pattern.compile(
52              "-+END\\s[^-\\r\\n]*CERTIFICATE[^-\\r\\n]*-+(?:\\s|\\r|\\n)*");
53      private static final Pattern KEY_HEADER = Pattern.compile(
54              "-+BEGIN\\s[^-\\r\\n]*PRIVATE\\s+KEY[^-\\r\\n]*-+(?:\\s|\\r|\\n)+");
55      private static final Pattern KEY_FOOTER = Pattern.compile(
56              "-+END\\s[^-\\r\\n]*PRIVATE\\s+KEY[^-\\r\\n]*-+(?:\\s|\\r|\\n)*");
57      private static final Pattern BODY = Pattern.compile("[a-z0-9+/=][a-z0-9+/=\\r\\n]*", Pattern.CASE_INSENSITIVE);
58  
59      static ByteBuf[] readCertificates(File file) throws CertificateException {
60          try {
61              InputStream in = new FileInputStream(file);
62  
63              try {
64                  return readCertificates(in);
65              } finally {
66                  safeClose(in);
67              }
68          } catch (FileNotFoundException e) {
69              throw new CertificateException("could not find certificate file: " + file);
70          }
71      }
72  
73      static ByteBuf[] readCertificates(InputStream in) throws CertificateException {
74          String content;
75          try {
76              content = readContent(in);
77          } catch (IOException e) {
78              throw new CertificateException("failed to read certificate input stream", e);
79          }
80  
81          List<ByteBuf> certs = new ArrayList<ByteBuf>();
82          Matcher m = CERT_HEADER.matcher(content);
83          int start = 0;
84          try {
85              for (;;) {
86                  if (!m.find(start)) {
87                      break;
88                  }
89  
90                  // Here and below it's necessary to save the position as it is reset
91                  // after calling usePattern() on Android due to a bug.
92                  //
93                  // See https://issuetracker.google.com/issues/293206296
94                  start = m.end();
95                  m.usePattern(BODY);
96                  if (!m.find(start)) {
97                      break;
98                  }
99  
100                 ByteBuf base64 = Unpooled.copiedBuffer(m.group(0), CharsetUtil.US_ASCII);
101                 try {
102                     start = m.end();
103                     m.usePattern(CERT_FOOTER);
104                     if (!m.find(start)) {
105                         // Certificate is incomplete.
106                         break;
107                     }
108                     ByteBuf der = Base64.decode(base64);
109                     certs.add(der);
110                 } finally {
111                     base64.release();
112                 }
113 
114                 start = m.end();
115                 m.usePattern(CERT_HEADER);
116             }
117         } catch (Throwable e) {
118             for (ByteBuf cert : certs) {
119                 cert.release();
120             }
121             PlatformDependent.throwException(e);
122         }
123 
124         if (certs.isEmpty()) {
125             throw new CertificateException("found no certificates in input stream");
126         }
127 
128         return certs.toArray(new ByteBuf[0]);
129     }
130 
131     static ByteBuf readPrivateKey(File file) throws KeyException {
132         try {
133             InputStream in = new FileInputStream(file);
134 
135             try {
136                 return readPrivateKey(in);
137             } finally {
138                 safeClose(in);
139             }
140         } catch (FileNotFoundException e) {
141             throw new KeyException("could not find key file: " + file);
142         }
143     }
144 
145     static ByteBuf readPrivateKey(InputStream in) throws KeyException {
146         String content;
147         try {
148             content = readContent(in);
149         } catch (IOException e) {
150             throw new KeyException("failed to read key input stream", e);
151         }
152         int start = 0;
153         Matcher m = KEY_HEADER.matcher(content);
154         if (!m.find(start)) {
155             throw keyNotFoundException();
156         }
157         start = m.end();
158         m.usePattern(BODY);
159         if (!m.find(start)) {
160             throw keyNotFoundException();
161         }
162 
163         ByteBuf base64 = Unpooled.copiedBuffer(m.group(0), CharsetUtil.US_ASCII);
164         try {
165             start = m.end();
166             m.usePattern(KEY_FOOTER);
167             if (!m.find(start)) {
168                 // Key is incomplete.
169                 throw keyNotFoundException();
170             }
171             return Base64.decode(base64);
172         } finally {
173             base64.release();
174         }
175     }
176 
177     private static KeyException keyNotFoundException() {
178         return new KeyException("could not find a PKCS #8 private key in input stream" +
179                 " (see https://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)");
180     }
181 
182     private static String readContent(InputStream in) throws IOException {
183         ByteArrayOutputStream out = new ByteArrayOutputStream();
184         try {
185             byte[] buf = new byte[8192];
186             for (;;) {
187                 int ret = in.read(buf);
188                 if (ret < 0) {
189                     break;
190                 }
191                 out.write(buf, 0, ret);
192             }
193             return out.toString(CharsetUtil.US_ASCII.name());
194         } finally {
195             safeClose(out);
196         }
197     }
198 
199     private static void safeClose(InputStream in) {
200         try {
201             in.close();
202         } catch (IOException e) {
203             logger.warn("Failed to close a stream.", e);
204         }
205     }
206 
207     private static void safeClose(OutputStream out) {
208         try {
209             out.close();
210         } catch (IOException e) {
211             logger.warn("Failed to close a stream.", e);
212         }
213     }
214 
215     private PemReader() { }
216 }