View Javadoc
1   /*
2    * Copyright 2022 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  package io.netty.handler.ssl;
17  
18  import io.netty.util.CharsetUtil;
19  import io.netty.util.internal.ThrowableUtil;
20  import io.netty.util.internal.logging.InternalLogger;
21  import io.netty.util.internal.logging.InternalLoggerFactory;
22  import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
23  import org.bouncycastle.openssl.PEMDecryptorProvider;
24  import org.bouncycastle.openssl.PEMEncryptedKeyPair;
25  import org.bouncycastle.openssl.PEMKeyPair;
26  import org.bouncycastle.openssl.PEMParser;
27  import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
28  import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
29  import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
30  import org.bouncycastle.operator.InputDecryptorProvider;
31  import org.bouncycastle.operator.OperatorCreationException;
32  import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
33  import org.bouncycastle.pkcs.PKCSException;
34  
35  import java.io.File;
36  import java.io.FileNotFoundException;
37  import java.io.FileReader;
38  import java.io.IOException;
39  import java.io.InputStream;
40  import java.io.InputStreamReader;
41  import java.security.AccessController;
42  import java.security.PrivateKey;
43  import java.security.PrivilegedAction;
44  import java.security.Provider;
45  import java.security.Security;
46  
47  final class BouncyCastlePemReader {
48      private static final String BC_PROVIDER_NAME = "BC";
49      private static final String BC_PROVIDER = "org.bouncycastle.jce.provider.BouncyCastleProvider";
50      private static final String BC_FIPS_PROVIDER_NAME = "BCFIPS";
51      private static final String BC_FIPS_PROVIDER = "org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider";
52      private static final String BC_PEMPARSER = "org.bouncycastle.openssl.PEMParser";
53      private static final InternalLogger logger = InternalLoggerFactory.getInstance(BouncyCastlePemReader.class);
54  
55      private static volatile Throwable unavailabilityCause;
56      private static volatile Provider bcProvider;
57      private static volatile boolean attemptedLoading;
58  
59      public static boolean hasAttemptedLoading() {
60          return attemptedLoading;
61      }
62  
63      public static boolean isAvailable() {
64          if (!hasAttemptedLoading()) {
65              tryLoading();
66          }
67          return unavailabilityCause == null;
68      }
69  
70      /**
71       * @return the cause if unavailable. {@code null} if available.
72       */
73      public static Throwable unavailabilityCause() {
74          return unavailabilityCause;
75      }
76  
77      private static void tryLoading() {
78          AccessController.doPrivileged(new PrivilegedAction<Void>() {
79              @Override
80              public Void run() {
81                  try {
82                      ClassLoader classLoader = getClass().getClassLoader();
83                      // Check for bcpkix-jdk15on:
84                      Class.forName(BC_PEMPARSER, true, classLoader);
85                      // Check for bcprov-jdk15on or bc-fips:
86                      bcProvider = Security.getProvider(BC_PROVIDER_NAME);
87                      if (bcProvider == null) {
88                          bcProvider = Security.getProvider(BC_FIPS_PROVIDER_NAME);
89                      }
90                      if (bcProvider == null) {
91                          Class<Provider> bcProviderClass;
92                          try {
93                              bcProviderClass = (Class<Provider>) Class.forName(BC_PROVIDER, true, classLoader);
94                          } catch (ClassNotFoundException e) {
95                              try {
96                                  bcProviderClass = (Class<Provider>) Class.forName(BC_FIPS_PROVIDER, true, classLoader);
97                              } catch (ClassNotFoundException ex) {
98                                  ThrowableUtil.addSuppressed(e, ex);
99                                  throw e;
100                             }
101                         }
102                         bcProvider = bcProviderClass.getConstructor().newInstance();
103                     }
104                     logger.debug("Bouncy Castle provider available");
105                     attemptedLoading = true;
106                 } catch (Throwable e) {
107                     logger.debug("Cannot load Bouncy Castle provider", e);
108                     unavailabilityCause = e;
109                     attemptedLoading = true;
110                 }
111                 return null;
112             }
113         });
114     }
115 
116     /**
117      * Allows to test {@link #attemptedLoading} under different conditions.
118      *
119      * @return previous {@link #bcProvider} value
120      */
121     static Provider resetBcProvider() {
122         Provider previousProvider = bcProvider;
123         bcProvider = null;
124         attemptedLoading = false;
125         unavailabilityCause = null;
126         return previousProvider;
127     }
128 
129     /**
130      * Generates a new {@link PrivateKey}.
131      *
132      * @param keyInputStream an input stream for a PKCS#1 or PKCS#8 private key in PEM format.
133      * @param keyPassword the password of the {@code keyFile}.
134      *                    {@code null} if it's not password-protected.
135      * @return generated {@link PrivateKey}.
136      */
137     public static PrivateKey getPrivateKey(InputStream keyInputStream, String keyPassword) {
138         if (!isAvailable()) {
139             if (logger.isDebugEnabled()) {
140                 logger.debug("Bouncy castle provider is unavailable.", unavailabilityCause());
141             }
142             return null;
143         }
144         try {
145             PEMParser parser = newParser(keyInputStream);
146             return getPrivateKey(parser, keyPassword);
147         } catch (Exception e) {
148             logger.debug("Unable to extract private key", e);
149             return null;
150         }
151     }
152 
153     /**
154      * Generates a new {@link PrivateKey}.
155      *
156      * @param keyFile a PKCS#1 or PKCS#8 private key file in PEM format.
157      * @param keyPassword the password of the {@code keyFile}.
158      *                    {@code null} if it's not password-protected.
159      * @return generated {@link PrivateKey}.
160      */
161     public static PrivateKey getPrivateKey(File keyFile, String keyPassword) {
162         if (!isAvailable()) {
163             if (logger.isDebugEnabled()) {
164                 logger.debug("Bouncy castle provider is unavailable.", unavailabilityCause());
165             }
166             return null;
167         }
168         try {
169             PEMParser parser = newParser(keyFile);
170             return getPrivateKey(parser, keyPassword);
171         } catch (Exception e) {
172             logger.debug("Unable to extract private key", e);
173             return null;
174         }
175     }
176 
177     private static JcaPEMKeyConverter newConverter() {
178         return new JcaPEMKeyConverter().setProvider(bcProvider);
179     }
180 
181     private static PrivateKey getPrivateKey(PEMParser pemParser, String keyPassword) throws IOException,
182             PKCSException, OperatorCreationException {
183         try {
184             JcaPEMKeyConverter converter = newConverter();
185             PrivateKey pk = null;
186 
187             Object object = pemParser.readObject();
188             while (object != null && pk == null) {
189                 if (logger.isDebugEnabled()) {
190                     logger.debug("Parsed PEM object of type {} and assume " +
191                                  "key is {}encrypted", object.getClass().getName(), keyPassword == null? "not " : "");
192                 }
193 
194                 if (keyPassword == null) {
195                     // assume private key is not encrypted
196                     if (object instanceof PrivateKeyInfo) {
197                         pk = converter.getPrivateKey((PrivateKeyInfo) object);
198                     } else if (object instanceof PEMKeyPair) {
199                         pk = converter.getKeyPair((PEMKeyPair) object).getPrivate();
200                     } else {
201                         logger.debug("Unable to handle PEM object of type {} as a non encrypted key",
202                                      object.getClass());
203                     }
204                 } else {
205                     // assume private key is encrypted
206                     if (object instanceof PEMEncryptedKeyPair) {
207                         PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder()
208                                 .setProvider(bcProvider)
209                                 .build(keyPassword.toCharArray());
210                         pk = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate();
211                     } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
212                         InputDecryptorProvider pkcs8InputDecryptorProvider =
213                                 new JceOpenSSLPKCS8DecryptorProviderBuilder()
214                                         .setProvider(bcProvider)
215                                         .build(keyPassword.toCharArray());
216                         pk = converter.getPrivateKey(((PKCS8EncryptedPrivateKeyInfo) object)
217                                                              .decryptPrivateKeyInfo(pkcs8InputDecryptorProvider));
218                     } else {
219                         logger.debug("Unable to handle PEM object of type {} as a encrypted key", object.getClass());
220                     }
221                 }
222 
223                 // Try reading next entry in the pem file if private key is not yet found
224                 if (pk == null) {
225                     object = pemParser.readObject();
226                 }
227             }
228 
229             if (pk == null) {
230                 if (logger.isDebugEnabled()) {
231                     logger.debug("No key found");
232                 }
233             }
234 
235             return pk;
236         } finally {
237             if (pemParser != null) {
238                 try {
239                     pemParser.close();
240                 } catch (Exception exception) {
241                     logger.debug("Failed closing pem parser", exception);
242                 }
243             }
244         }
245     }
246 
247     private static PEMParser newParser(File keyFile) throws FileNotFoundException {
248         return new PEMParser(new FileReader(keyFile));
249     }
250 
251     private static PEMParser newParser(InputStream keyInputStream) {
252         return new PEMParser(new InputStreamReader(keyInputStream, CharsetUtil.US_ASCII));
253     }
254 
255     private BouncyCastlePemReader() { }
256 }