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