View Javadoc
1   /*
2    * Copyright 2021 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.codec.quic;
17  
18  
19  import io.netty.util.CharsetUtil;
20  import org.jetbrains.annotations.Nullable;
21  
22  import javax.net.ssl.SSLException;
23  import javax.net.ssl.SSLHandshakeException;
24  import javax.net.ssl.X509ExtendedKeyManager;
25  import javax.security.auth.x500.X500Principal;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.security.PrivateKey;
29  import java.security.cert.CertificateEncodingException;
30  import java.security.cert.X509Certificate;
31  import java.util.Arrays;
32  import java.util.Base64;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.LinkedHashSet;
37  import java.util.Map;
38  import java.util.Set;
39  
40  final class BoringSSLCertificateCallback {
41      private static final byte[] BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n".getBytes(CharsetUtil.US_ASCII);
42      private static final byte[] END_PRIVATE_KEY = "\n-----END PRIVATE KEY-----\n".getBytes(CharsetUtil.US_ASCII);
43  
44      /**
45       * The types contained in the {@code keyTypeBytes} array.
46       */
47      // Extracted from https://github.com/openssl/openssl/blob/master/include/openssl/tls1.h
48      private static final byte TLS_CT_RSA_SIGN = 1;
49      private static final byte TLS_CT_DSS_SIGN = 2;
50      private static final byte TLS_CT_RSA_FIXED_DH = 3;
51      private static final byte TLS_CT_DSS_FIXED_DH = 4;
52      private static final byte TLS_CT_ECDSA_SIGN = 64;
53      private static final byte TLS_CT_RSA_FIXED_ECDH = 65;
54      private static final byte TLS_CT_ECDSA_FIXED_ECDH = 66;
55  
56      // Code in this class is inspired by code of conscrypts:
57      // - https://android.googlesource.com/platform/external/
58      //   conscrypt/+/master/src/main/java/org/conscrypt/OpenSSLEngineImpl.java
59      // - https://android.googlesource.com/platform/external/
60      //   conscrypt/+/master/src/main/java/org/conscrypt/SSLParametersImpl.java
61      //
62      static final String KEY_TYPE_RSA = "RSA";
63      static final String KEY_TYPE_DH_RSA = "DH_RSA";
64      static final String KEY_TYPE_EC = "EC";
65      static final String KEY_TYPE_EC_EC = "EC_EC";
66      static final String KEY_TYPE_EC_RSA = "EC_RSA";
67  
68      // key type mappings for types.
69      private static final Map<String, String> DEFAULT_SERVER_KEY_TYPES = new HashMap<String, String>();
70      static {
71          DEFAULT_SERVER_KEY_TYPES.put("RSA", KEY_TYPE_RSA);
72          DEFAULT_SERVER_KEY_TYPES.put("DHE_RSA", KEY_TYPE_RSA);
73          DEFAULT_SERVER_KEY_TYPES.put("ECDHE_RSA", KEY_TYPE_RSA);
74          DEFAULT_SERVER_KEY_TYPES.put("ECDHE_ECDSA", KEY_TYPE_EC);
75          DEFAULT_SERVER_KEY_TYPES.put("ECDH_RSA", KEY_TYPE_EC_RSA);
76          DEFAULT_SERVER_KEY_TYPES.put("ECDH_ECDSA", KEY_TYPE_EC_EC);
77          DEFAULT_SERVER_KEY_TYPES.put("DH_RSA", KEY_TYPE_DH_RSA);
78      }
79  
80      private static final Set<String> DEFAULT_CLIENT_KEY_TYPES = Collections.unmodifiableSet(new LinkedHashSet<>(
81              Arrays.asList(KEY_TYPE_RSA,
82                      KEY_TYPE_DH_RSA,
83                      KEY_TYPE_EC,
84                      KEY_TYPE_EC_RSA,
85                      KEY_TYPE_EC_EC)));
86  
87      // Directly returning this is safe as we never modify it within our JNI code.
88      private static final long[] NO_KEY_MATERIAL_CLIENT_SIDE =  new long[] { 0, 0 };
89  
90      private final QuicheQuicSslEngineMap engineMap;
91      private final X509ExtendedKeyManager keyManager;
92      private final String password;
93      private final Map<String, String> serverKeyTypes;
94      private final Set<String> clientKeyTypes;
95  
96      BoringSSLCertificateCallback(QuicheQuicSslEngineMap engineMap,
97                                   @Nullable X509ExtendedKeyManager keyManager,
98                                   String password,
99                                   Map<String, String> serverKeyTypes,
100                                  Set<String> clientKeyTypes) {
101         this.engineMap = engineMap;
102         this.keyManager = keyManager;
103         this.password = password;
104 
105         this.serverKeyTypes = serverKeyTypes != null ? serverKeyTypes : DEFAULT_SERVER_KEY_TYPES;
106         this.clientKeyTypes = clientKeyTypes != null ? clientKeyTypes : DEFAULT_CLIENT_KEY_TYPES;
107     }
108 
109     @SuppressWarnings("unused")
110     long @Nullable [] handle(long ssl, byte[] keyTypeBytes, byte @Nullable [][] asn1DerEncodedPrincipals,
111                              String[] authMethods) {
112         QuicheQuicSslEngine engine = engineMap.get(ssl);
113         if (engine == null) {
114             return null;
115         }
116 
117         try {
118             if (keyManager == null) {
119                 if (engine.getUseClientMode()) {
120                     return NO_KEY_MATERIAL_CLIENT_SIDE;
121                 }
122                 return null;
123             }
124             if (engine.getUseClientMode()) {
125                 final Set<String> keyTypesSet = supportedClientKeyTypes(keyTypeBytes);
126                 final String[] keyTypes = keyTypesSet.toArray(new String[0]);
127                 final X500Principal[] issuers;
128                 if (asn1DerEncodedPrincipals == null) {
129                     issuers = null;
130                 } else {
131                     issuers = new X500Principal[asn1DerEncodedPrincipals.length];
132                     for (int i = 0; i < asn1DerEncodedPrincipals.length; i++) {
133                         issuers[i] = new X500Principal(asn1DerEncodedPrincipals[i]);
134                     }
135                 }
136                 return removeMappingIfNeeded(ssl, selectKeyMaterialClientSide(ssl, engine, keyTypes, issuers));
137             } else {
138                 // For now we just ignore the asn1DerEncodedPrincipals as this is kind of inline with what the
139                 // OpenJDK SSLEngineImpl does.
140                 return removeMappingIfNeeded(ssl, selectKeyMaterialServerSide(ssl, engine, authMethods));
141             }
142         } catch (SSLException e) {
143             engineMap.remove(ssl);
144             return null;
145         } catch (Throwable cause) {
146             engineMap.remove(ssl);
147             throw cause;
148         }
149     }
150 
151     private long @Nullable [] removeMappingIfNeeded(long ssl, long @Nullable [] result) {
152         if (result == null) {
153             engineMap.remove(ssl);
154         }
155         return result;
156     }
157 
158     private long @Nullable [] selectKeyMaterialServerSide(long ssl, QuicheQuicSslEngine engine, String[] authMethods)
159             throws SSLException {
160         if (authMethods.length == 0) {
161             throw new SSLHandshakeException("Unable to find key material");
162         }
163 
164         // authMethods may contain duplicates or may result in the same type
165         // but call chooseServerAlias(...) may be expensive. So let's ensure
166         // we filter out duplicates.
167         Set<String> typeSet = new HashSet<String>(serverKeyTypes.size());
168         for (String authMethod : authMethods) {
169             String type = serverKeyTypes.get(authMethod);
170             if (type != null && typeSet.add(type)) {
171                 String alias = chooseServerAlias(engine, type);
172                 if (alias != null) {
173                     return selectMaterial(ssl, engine, alias) ;
174                 }
175             }
176         }
177         throw new SSLHandshakeException("Unable to find key material for auth method(s): "
178                 + Arrays.toString(authMethods));
179     }
180 
181     private long @Nullable [] selectKeyMaterialClientSide(long ssl, QuicheQuicSslEngine engine, String[] keyTypes,
182                                                X500Principal @Nullable [] issuer) {
183         String alias = chooseClientAlias(engine, keyTypes, issuer);
184         // Only try to set the keymaterial if we have a match. This is also consistent with what OpenJDK does:
185         // https://hg.openjdk.java.net/jdk/jdk11/file/76072a077ee1/
186         // src/java.base/share/classes/sun/security/ssl/CertificateRequest.java#l362
187         if (alias != null) {
188             return selectMaterial(ssl, engine, alias) ;
189         }
190         return NO_KEY_MATERIAL_CLIENT_SIDE;
191     }
192 
193     private long @Nullable [] selectMaterial(long ssl, QuicheQuicSslEngine engine, String alias)  {
194         X509Certificate[] certificates = keyManager.getCertificateChain(alias);
195         if (certificates == null || certificates.length == 0) {
196             return null;
197         }
198         byte[][] certs = new byte[certificates.length][];
199 
200         for (int i = 0; i < certificates.length; i++) {
201             try {
202                 certs[i] = certificates[i].getEncoded();
203             } catch (CertificateEncodingException e) {
204                 return null;
205             }
206         }
207 
208         final long key;
209         PrivateKey privateKey = keyManager.getPrivateKey(alias);
210         if (privateKey == BoringSSLKeylessPrivateKey.INSTANCE) {
211             key = 0;
212         } else {
213             byte[] pemKey = toPemEncoded(privateKey);
214             if (pemKey == null) {
215                 return null;
216             }
217             key = BoringSSL.EVP_PKEY_parse(pemKey, password);
218         }
219         long chain = BoringSSL.CRYPTO_BUFFER_stack_new(ssl, certs);
220         engine.setLocalCertificateChain(certificates);
221 
222         // Return and signal that the key and chain should be released as well.
223         return new long[] { key,  chain };
224     }
225 
226     private static byte @Nullable [] toPemEncoded(PrivateKey key) {
227         try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
228             out.write(BEGIN_PRIVATE_KEY);
229             out.write(Base64.getEncoder().encode(key.getEncoded()));
230             out.write(END_PRIVATE_KEY);
231             return out.toByteArray();
232         } catch (IOException e) {
233             return null;
234         }
235     }
236 
237     @Nullable
238     private String chooseClientAlias(QuicheQuicSslEngine engine,
239                                      String[] keyTypes, X500Principal @Nullable [] issuer) {
240         return keyManager.chooseEngineClientAlias(keyTypes, issuer, engine);
241     }
242 
243     @Nullable
244     private String chooseServerAlias(QuicheQuicSslEngine engine, String type) {
245         return keyManager.chooseEngineServerAlias(type, null, engine);
246     }
247 
248     /**
249      * Gets the supported key types for client certificates.
250      *
251      * @param clientCertificateTypes {@code ClientCertificateType} values provided by the server.
252      *        See https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml.
253      * @return supported key types that can be used in {@code X509KeyManager.chooseClientAlias} and
254      *         {@code X509ExtendedKeyManager.chooseEngineClientAlias}.
255      */
256     private Set<String> supportedClientKeyTypes(byte @Nullable[] clientCertificateTypes) {
257         if (clientCertificateTypes == null) {
258             // Try all of the supported key types.
259             return clientKeyTypes;
260         }
261         Set<String> result = new HashSet<>(clientCertificateTypes.length);
262         for (byte keyTypeCode : clientCertificateTypes) {
263             String keyType = clientKeyType(keyTypeCode);
264             if (keyType == null) {
265                 // Unsupported client key type -- ignore
266                 continue;
267             }
268             result.add(keyType);
269         }
270         return result;
271     }
272 
273     @Nullable
274     private static String clientKeyType(byte clientCertificateType) {
275         // See also https://www.ietf.org/assignments/tls-parameters/tls-parameters.xml
276         switch (clientCertificateType) {
277             case TLS_CT_RSA_SIGN:
278                 return KEY_TYPE_RSA; // RFC rsa_sign
279             case TLS_CT_RSA_FIXED_DH:
280                 return KEY_TYPE_DH_RSA; // RFC rsa_fixed_dh
281             case TLS_CT_ECDSA_SIGN:
282                 return KEY_TYPE_EC; // RFC ecdsa_sign
283             case TLS_CT_RSA_FIXED_ECDH:
284                 return KEY_TYPE_EC_RSA; // RFC rsa_fixed_ecdh
285             case TLS_CT_ECDSA_FIXED_ECDH:
286                 return KEY_TYPE_EC_EC; // RFC ecdsa_fixed_ecdh
287             default:
288                 return null;
289         }
290     }
291 }