View Javadoc
1   /*
2    * Copyright 2025 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.internal.tcnative.SSLPrivateKeyMethod;
19  import io.netty.util.collection.IntCollections;
20  import io.netty.util.collection.IntObjectHashMap;
21  import io.netty.util.collection.IntObjectMap;
22  import io.netty.util.internal.ObjectUtil;
23  import io.netty.util.internal.logging.InternalLogger;
24  import io.netty.util.internal.logging.InternalLoggerFactory;
25  
26  import java.security.InvalidAlgorithmParameterException;
27  import java.security.InvalidKeyException;
28  import java.security.NoSuchAlgorithmException;
29  import java.security.PrivateKey;
30  import java.security.Provider;
31  import java.security.Security;
32  import java.security.Signature;
33  import java.security.spec.MGF1ParameterSpec;
34  import java.security.spec.PSSParameterSpec;
35  import java.util.Objects;
36  import java.util.concurrent.ConcurrentHashMap;
37  import java.util.concurrent.ConcurrentMap;
38  
39  /**
40   * Implementation of {@link OpenSslAsyncPrivateKeyMethod} that delegates cryptographic operations
41   * to JDK signature providers for keys that cannot be accessed directly by OpenSSL.
42   */
43  final class JdkDelegatingPrivateKeyMethod implements SSLPrivateKeyMethod {
44  
45      private static final InternalLogger logger =
46              InternalLoggerFactory.getInstance(JdkDelegatingPrivateKeyMethod.class);
47  
48      private static final IntObjectMap<String> SSL_TO_JDK_SIGNATURE_ALGORITHM;
49      private static final ConcurrentMap<CacheKey, String> PROVIDER_CACHE = new ConcurrentHashMap<>();
50  
51      static {
52          IntObjectMap<String> algorithmMap = new IntObjectHashMap<>();
53  
54          // RSA PKCS#1 signatures
55          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA1, "SHA1withRSA");
56          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA256, "SHA256withRSA");
57          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA384, "SHA384withRSA");
58          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_SHA512, "SHA512withRSA");
59          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PKCS1_MD5_SHA1, "MD5andSHA1withRSA");
60  
61          // ECDSA signatures
62          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_ECDSA_SHA1, "SHA1withECDSA");
63          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_ECDSA_SECP256R1_SHA256, "SHA256withECDSA");
64          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_ECDSA_SECP384R1_SHA384, "SHA384withECDSA");
65          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_ECDSA_SECP521R1_SHA512, "SHA512withECDSA");
66  
67          // RSA-PSS signatures
68          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256, "RSASSA-PSS");
69          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384, "RSASSA-PSS");
70          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512, "RSASSA-PSS");
71  
72          // EdDSA signatures
73          algorithmMap.put(OpenSslAsyncPrivateKeyMethod.SSL_SIGN_ED25519, "EdDSA");
74  
75          SSL_TO_JDK_SIGNATURE_ALGORITHM = IntCollections.unmodifiableMap(algorithmMap);
76      }
77  
78      private final PrivateKey privateKey;
79      private final String privateKeyTypeName;
80  
81      /**
82       * Creates a new JDK delegating async private key method.
83       *
84       * @param privateKey the private key to use for cryptographic operations
85       */
86      JdkDelegatingPrivateKeyMethod(PrivateKey privateKey) {
87          this.privateKey = ObjectUtil.checkNotNull(privateKey, "privateKey");
88          this.privateKeyTypeName = privateKey.getClass().getName();
89      }
90  
91      @Override
92      public byte[] sign(long ssl, int signatureAlgorithm, byte[] input) throws Exception {
93          Signature signature = createSignature(signatureAlgorithm);
94          signature.update(input);
95          byte[] result = signature.sign();
96  
97          if (logger.isDebugEnabled()) {
98              logger.debug("Signing operation completed successfully, result length: {}", result.length);
99          }
100         return result;
101     }
102 
103     @Override
104     public byte[] decrypt(long ssl, byte[] input) {
105         // Modern handshake techniques don't use the private key to decrypt, only to sign in order to verify
106         // identity. As such, we don't currently support decrypting using the private key.
107         throw new UnsupportedOperationException("Direct decryption is not supported");
108     }
109 
110     private Signature createSignature(int opensslAlgorithm)
111             throws NoSuchAlgorithmException {
112         String jdkAlgorithm = SSL_TO_JDK_SIGNATURE_ALGORITHM.get(opensslAlgorithm);
113         if (jdkAlgorithm == null) {
114             String errorMsg = "Unsupported signature algorithm: " + opensslAlgorithm;
115             throw new NoSuchAlgorithmException(errorMsg);
116         }
117 
118         CacheKey cacheKey = new CacheKey(jdkAlgorithm, privateKeyTypeName);
119 
120         // Try cached provider first
121         String cachedProviderName = PROVIDER_CACHE.get(cacheKey);
122         if (cachedProviderName != null) {
123             try {
124                 Signature signature = Signature.getInstance(jdkAlgorithm, cachedProviderName);
125                 configureOpenSslAlgorithmParameters(signature, opensslAlgorithm);
126                 signature.initSign(privateKey);
127                 if (logger.isDebugEnabled()) {
128                     logger.debug("Using cached provider {} for OpenSSL algorithm {} ({}) with key type {}",
129                             cachedProviderName, opensslAlgorithm, jdkAlgorithm, privateKeyTypeName);
130                 }
131                 return signature;
132             } catch (Exception e) {
133                 // Cache is stale, remove it and try full discovery
134                 PROVIDER_CACHE.remove(cacheKey);
135                 if (logger.isDebugEnabled()) {
136                     logger.debug("Cached provider {} failed for key type {}, removing from cache: {}",
137                             cachedProviderName, privateKeyTypeName, e.getMessage());
138                 }
139             }
140         }
141 
142         // Do full discovery and cache result
143         Signature signature = findCompatibleSignature(opensslAlgorithm, jdkAlgorithm);
144         PROVIDER_CACHE.put(cacheKey, signature.getProvider().getName());
145 
146         if (logger.isDebugEnabled()) {
147             logger.debug("Discovered and cached provider {} for OpenSSL algorithm {} ({}) with key type {}",
148                     signature.getProvider().getName(), opensslAlgorithm, jdkAlgorithm, privateKeyTypeName);
149         }
150 
151         return signature;
152     }
153 
154     private Signature findCompatibleSignature(int opensslAlgorithm,
155                                               String jdkAlgorithm) throws NoSuchAlgorithmException {
156 
157         // 1. Try default provider first (optimization)
158         try {
159             Signature signature = Signature.getInstance(jdkAlgorithm);
160             configureOpenSslAlgorithmParameters(signature, opensslAlgorithm);
161             signature.initSign(privateKey);
162             if (logger.isDebugEnabled()) {
163                 logger.debug("Default provider {} handles key type {} for OpenSSL algorithm {} ({})",
164                         signature.getProvider().getName(), privateKey.getClass().getName(),
165                         opensslAlgorithm, jdkAlgorithm);
166             }
167             return signature; // Success!
168         } catch (InvalidKeyException e) {
169             // Default provider can't handle this key type, continue with full search
170             if (logger.isDebugEnabled()) {
171                 logger.debug("Default provider cannot handle key type {} for OpenSSL algorithm {} ({}): {}",
172                         privateKey.getClass().getName(), opensslAlgorithm, jdkAlgorithm, e.getMessage());
173             }
174         } catch (Exception e) {
175             // Other issues with default provider, continue searching
176             if (logger.isDebugEnabled()) {
177                 logger.debug("Default provider failed for OpenSSL algorithm {} ({}): {}",
178                         opensslAlgorithm, jdkAlgorithm, e.getMessage());
179             }
180         }
181 
182         // 2. Iterate through all providers. Note this iteration goes in order of priority.
183         Provider[] providers = Security.getProviders();
184         for (Provider provider : providers) {
185             try {
186                 Signature signature = Signature.getInstance(jdkAlgorithm, provider);
187                 configureOpenSslAlgorithmParameters(signature, opensslAlgorithm);
188                 signature.initSign(privateKey); // Test compatibility
189 
190                 if (logger.isDebugEnabled()) {
191                     logger.debug("Found compatible provider {} for key type {} with OpenSSL algorithm {} ({})",
192                             provider.getName(), privateKey.getClass().getName(), opensslAlgorithm, jdkAlgorithm);
193                 }
194                 return signature; // Found compatible provider!
195             } catch (InvalidKeyException e) {
196                 // This provider can't handle this key type, try next
197                 if (logger.isTraceEnabled()) {
198                     logger.trace("Provider {} cannot handle key type {}: {}",
199                             provider.getName(), privateKey.getClass().getName(), e.getMessage());
200                 }
201             } catch (Exception e) {
202                 // Other issues, try next provider
203                 if (logger.isTraceEnabled()) {
204                     logger.trace("Provider {} failed for OpenSSL algorithm {} ({}): {}",
205                             provider.getName(), opensslAlgorithm, jdkAlgorithm, e.getMessage());
206                 }
207             }
208         }
209 
210         throw new NoSuchAlgorithmException("No provider found for OpenSSL algorithm " + opensslAlgorithm +
211                 " (" + jdkAlgorithm + ") with private key type: " + privateKey.getClass().getName());
212     }
213 
214     private static void configureOpenSslAlgorithmParameters(Signature signature, int opensslAlgorithm)
215             throws InvalidAlgorithmParameterException {
216         // Use the OpenSSL algorithm constants for precise parameter configuration
217         if (opensslAlgorithm == OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA256) {
218             // SHA-256 based PSS with MGF1-SHA256, salt length 32
219             configurePssParameters(signature, MGF1ParameterSpec.SHA256, 32);
220         } else if (opensslAlgorithm == OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA384) {
221             // SHA-384 based PSS with MGF1-SHA384, salt length 48
222             configurePssParameters(signature, MGF1ParameterSpec.SHA384, 48);
223         } else if (opensslAlgorithm == OpenSslAsyncPrivateKeyMethod.SSL_SIGN_RSA_PSS_RSAE_SHA512) {
224             // SHA-512 based PSS with MGF1-SHA512, salt length 64
225             configurePssParameters(signature, MGF1ParameterSpec.SHA512, 64);
226         } else if (SSL_TO_JDK_SIGNATURE_ALGORITHM.containsKey(opensslAlgorithm)) {
227             // All other supported algorithms don't require special parameter configuration
228             if (logger.isTraceEnabled()) {
229                 logger.trace("No parameter configuration needed for OpenSSL algorithm {}", opensslAlgorithm);
230             }
231         } else {
232             if (logger.isDebugEnabled()) {
233                 logger.debug("Unknown OpenSSL algorithm {}, using default configuration", opensslAlgorithm);
234             }
235         }
236     }
237 
238     private static void configurePssParameters(Signature signature,
239                                                MGF1ParameterSpec mgfSpec, int saltLength)
240             throws InvalidAlgorithmParameterException {
241         PSSParameterSpec pssSpec = new PSSParameterSpec(mgfSpec.getDigestAlgorithm(), "MGF1", mgfSpec, saltLength, 1);
242         signature.setParameter(pssSpec);
243 
244         if (logger.isDebugEnabled()) {
245             logger.debug("Configured PSS parameters: hash={}, saltLength={}", mgfSpec.getDigestAlgorithm(), saltLength);
246         }
247     }
248 
249     private static final class CacheKey {
250         private final String jdkAlgorithm;
251         private final String keyTypeName;
252         private final int hashCode;
253 
254         @Override
255         public boolean equals(Object o) {
256             if (o == null || getClass() != o.getClass()) {
257                 return false;
258             }
259             CacheKey cacheKey = (CacheKey) o;
260             return Objects.equals(cacheKey.jdkAlgorithm, jdkAlgorithm)
261                     && Objects.equals(cacheKey.keyTypeName, keyTypeName);
262         }
263 
264         @Override
265         public int hashCode() {
266             return hashCode;
267         }
268 
269         CacheKey(String jdkAlgorithm, String keyTypeName) {
270             this.jdkAlgorithm = jdkAlgorithm;
271             this.keyTypeName = keyTypeName;
272             this.hashCode = 31 * jdkAlgorithm.hashCode() + keyTypeName.hashCode();
273         }
274     }
275 }