View Javadoc
1   /*
2    * Copyright 2024 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.internal.PlatformDependent;
19  import io.netty.util.internal.SuppressJava6Requirement;
20  
21  import java.net.Socket;
22  import java.security.cert.Certificate;
23  import java.security.cert.CertificateException;
24  import java.security.cert.X509Certificate;
25  import java.util.Collections;
26  import java.util.Set;
27  import java.util.WeakHashMap;
28  import java.util.concurrent.atomic.AtomicReference;
29  import javax.net.ssl.SSLEngine;
30  import javax.net.ssl.SSLPeerUnverifiedException;
31  import javax.net.ssl.SSLSession;
32  import javax.net.ssl.TrustManager;
33  import javax.net.ssl.X509ExtendedTrustManager;
34  
35  final class ResumptionController {
36      private final Set<SSLEngine> confirmedValidations;
37      private final AtomicReference<ResumableX509ExtendedTrustManager> resumableTm;
38  
39      ResumptionController() {
40          confirmedValidations = Collections.synchronizedSet(
41                  Collections.newSetFromMap(new WeakHashMap<SSLEngine, Boolean>()));
42          resumableTm = new AtomicReference<ResumableX509ExtendedTrustManager>();
43      }
44  
45      @SuppressJava6Requirement(reason = "Guarded by version check")
46      public TrustManager wrapIfNeeded(TrustManager tm) {
47          if (tm instanceof ResumableX509ExtendedTrustManager) {
48              if (PlatformDependent.javaVersion() < 7 || !(tm instanceof X509ExtendedTrustManager)) {
49                  throw new IllegalStateException("ResumableX509ExtendedTrustManager implementation must be a " +
50                          "subclass of X509ExtendedTrustManager, found: " + (tm == null ? null : tm.getClass()));
51              }
52              if (!resumableTm.compareAndSet(null, (ResumableX509ExtendedTrustManager) tm)) {
53                  throw new IllegalStateException(
54                          "Only one ResumableX509ExtendedTrustManager can be configured for resumed sessions");
55              }
56              return new X509ExtendedWrapTrustManager((X509ExtendedTrustManager) tm, confirmedValidations);
57          }
58          return tm;
59      }
60  
61      public void remove(SSLEngine engine) {
62          if (resumableTm.get() != null) {
63              confirmedValidations.remove(unwrapEngine(engine));
64          }
65      }
66  
67      public boolean validateResumeIfNeeded(SSLEngine engine)
68              throws CertificateException, SSLPeerUnverifiedException {
69          ResumableX509ExtendedTrustManager tm;
70          SSLSession session = engine.getSession();
71          boolean valid = session.isValid();
72  
73          // Look for resumption if the session is valid, and we expect to authenticate our peer:
74          //   1.   Clients always authenticate the server.
75          //   2.a. Servers only authenticate the client if they need auth,
76          //   2.b. or if they requested auth and the client provided.
77          //
78          // If a server only "want" but don't "need" auth (ClientAuth.OPTIONAL) and the client didn't provide
79          // any certificates, then `session.getPeerCertificates()` will throw `SSLPeerUnverifiedException`.
80          if (valid && (engine.getUseClientMode() || engine.getNeedClientAuth() || engine.getWantClientAuth()) &&
81                  (tm = resumableTm.get()) != null) {
82              // Unwrap JdkSslEngines because they add their inner JDK SSLEngine objects to the set.
83              engine = unwrapEngine(engine);
84  
85              if (!confirmedValidations.remove(engine)) {
86                  Certificate[] peerCertificates;
87                  try {
88                      peerCertificates = session.getPeerCertificates();
89                  } catch (SSLPeerUnverifiedException e) {
90                      if (engine.getUseClientMode() || engine.getNeedClientAuth()) {
91                          // Auth is required, and we got none.
92                          throw e;
93                      }
94                      // Auth is optional, and none were provided. Skip out; session resumed but nothing to authenticate.
95                      return false;
96                  }
97  
98                  // This is a resumed session.
99                  if (engine.getUseClientMode()) {
100                     // We are the client, resuming a session trusting the server
101                     tm.resumeServerTrusted(chainOf(peerCertificates), engine);
102                 } else {
103                     // We are the server, resuming a session trusting the client
104                     tm.resumeClientTrusted(chainOf(peerCertificates), engine);
105                 }
106                 return true;
107             }
108         }
109         return false;
110     }
111 
112     private static SSLEngine unwrapEngine(SSLEngine engine) {
113         if (engine instanceof JdkSslEngine) {
114             return ((JdkSslEngine) engine).getWrappedEngine();
115         }
116         return engine;
117     }
118 
119     private static X509Certificate[] chainOf(Certificate[] peerCertificates) {
120         if (peerCertificates instanceof X509Certificate[]) {
121             //noinspection SuspiciousArrayCast
122             return (X509Certificate[]) peerCertificates;
123         }
124         X509Certificate[] chain = new X509Certificate[peerCertificates.length];
125         for (int i = 0; i < peerCertificates.length; i++) {
126             Certificate cert = peerCertificates[i];
127             if (cert instanceof X509Certificate || cert == null) {
128                 chain[i] = (X509Certificate) cert;
129             } else {
130                 throw new IllegalArgumentException("Only X509Certificates are supported, found: " + cert.getClass());
131             }
132         }
133         return chain;
134     }
135 
136     @SuppressJava6Requirement(reason = "Guarded by version check")
137     private static final class X509ExtendedWrapTrustManager extends X509ExtendedTrustManager {
138         private final X509ExtendedTrustManager trustManager;
139         private final Set<SSLEngine> confirmedValidations;
140 
141         X509ExtendedWrapTrustManager(X509ExtendedTrustManager trustManager, Set<SSLEngine> confirmedValidations) {
142             this.trustManager = trustManager;
143             this.confirmedValidations = confirmedValidations;
144         }
145 
146         private static void unsupported() throws CertificateException {
147             throw new CertificateException(
148                     new UnsupportedOperationException("Resumable trust managers require the SSLEngine parameter"));
149         }
150 
151         @Override
152         public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
153                 throws CertificateException {
154             unsupported();
155         }
156 
157         @Override
158         public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
159                 throws CertificateException {
160             unsupported();
161         }
162 
163         @Override
164         public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
165             unsupported();
166         }
167 
168         @Override
169         public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
170             unsupported();
171         }
172 
173         @Override
174         public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
175                 throws CertificateException {
176             trustManager.checkClientTrusted(chain, authType, engine);
177             confirmedValidations.add(engine);
178         }
179 
180         @Override
181         public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
182                 throws CertificateException {
183             trustManager.checkServerTrusted(chain, authType, engine);
184             confirmedValidations.add(engine);
185         }
186 
187         @Override
188         public X509Certificate[] getAcceptedIssuers() {
189             return trustManager.getAcceptedIssuers();
190         }
191     }
192 }