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  import io.netty.handler.ssl.ClientAuth;
19  import io.netty.handler.ssl.util.LazyJavaxX509Certificate;
20  import io.netty.handler.ssl.util.LazyX509Certificate;
21  import io.netty.util.NetUtil;
22  import io.netty.util.internal.EmptyArrays;
23  import io.netty.util.internal.ObjectUtil;
24  import org.jetbrains.annotations.Nullable;
25  
26  import javax.net.ssl.SNIHostName;
27  import javax.net.ssl.SNIServerName;
28  import javax.net.ssl.SSLEngineResult;
29  import javax.net.ssl.SSLParameters;
30  import javax.net.ssl.SSLPeerUnverifiedException;
31  import javax.net.ssl.SSLSession;
32  import javax.net.ssl.SSLSessionBindingEvent;
33  import javax.net.ssl.SSLSessionBindingListener;
34  import javax.net.ssl.SSLSessionContext;
35  import javax.security.cert.X509Certificate;
36  import java.nio.ByteBuffer;
37  import java.security.Principal;
38  import java.security.cert.Certificate;
39  import java.util.Arrays;
40  import java.util.Collections;
41  import java.util.HashMap;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.function.Consumer;
45  import java.util.function.LongFunction;
46  
47  final class QuicheQuicSslEngine extends QuicSslEngine {
48      QuicheQuicSslContext ctx;
49      private final String peerHost;
50      private final int peerPort;
51      private final QuicheQuicSslSession session = new QuicheQuicSslSession();
52      private volatile Certificate[] localCertificateChain;
53      private List<SNIServerName> sniHostNames;
54      private boolean handshakeFinished;
55      private String applicationProtocol;
56      private boolean sessionReused;
57      final String tlsHostName;
58      volatile QuicheQuicConnection connection;
59  
60      volatile Consumer<String> sniSelectedCallback;
61  
62      QuicheQuicSslEngine(QuicheQuicSslContext ctx, @Nullable String peerHost, int peerPort) {
63          this.ctx = ctx;
64          this.peerHost = peerHost;
65          this.peerPort = peerPort;
66          // Use SNI if peerHost was specified and a valid hostname
67          // See https://github.com/netty/netty/issues/4746
68          if (ctx.isClient() && isValidHostNameForSNI(peerHost)) {
69              tlsHostName = peerHost;
70              sniHostNames = Collections.singletonList(new SNIHostName(tlsHostName));
71          } else {
72              tlsHostName = null;
73          }
74      }
75  
76      long moveTo(String hostname, QuicheQuicSslContext ctx) {
77          // First of remove the engine from its previous QuicheQuicSslContext.
78          this.ctx.remove(this);
79          this.ctx = ctx;
80          long added = ctx.add(this);
81          Consumer<String> sniSelectedCallback = this.sniSelectedCallback;
82          if (sniSelectedCallback != null) {
83              sniSelectedCallback.accept(hostname);
84          }
85          return added;
86      }
87  
88      @Nullable
89      QuicheQuicConnection createConnection(LongFunction<Long> connectionCreator) {
90          return ctx.createConnection(connectionCreator, this);
91      }
92  
93      void setLocalCertificateChain(Certificate[] localCertificateChain) {
94          this.localCertificateChain = localCertificateChain;
95      }
96  
97      /**
98       * Validate that the given hostname can be used in SNI extension.
99       */
100     static boolean isValidHostNameForSNI(@Nullable String hostname) {
101         return hostname != null &&
102                 hostname.indexOf('.') > 0 &&
103                 !hostname.endsWith(".") &&
104                 !NetUtil.isValidIpV4Address(hostname) &&
105                 !NetUtil.isValidIpV6Address(hostname);
106     }
107 
108     @Override
109     public SSLParameters getSSLParameters() {
110         SSLParameters parameters = super.getSSLParameters();
111         parameters.setServerNames(sniHostNames);
112         return parameters;
113     }
114 
115     // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier
116     // java8 version we don't use @Override annotations here.
117     public synchronized String getApplicationProtocol() {
118         return applicationProtocol;
119     }
120 
121     // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier
122     // java8 version we don't use @Override annotations here.
123     public synchronized String getHandshakeApplicationProtocol() {
124         return applicationProtocol;
125     }
126 
127     @Override
128     public SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) {
129         throw new UnsupportedOperationException();
130     }
131 
132     @Override
133     public SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) {
134         throw new UnsupportedOperationException();
135     }
136 
137     @Override
138     @Nullable
139     public Runnable getDelegatedTask() {
140         return null;
141     }
142 
143     @Override
144     public void closeInbound() {
145         throw new UnsupportedOperationException();
146     }
147 
148     @Override
149     public boolean isInboundDone() {
150         return false;
151     }
152 
153     @Override
154     public void closeOutbound() {
155         throw new UnsupportedOperationException();
156     }
157 
158     @Override
159     public boolean isOutboundDone() {
160         return false;
161     }
162 
163     @Override
164     public String[] getSupportedCipherSuites() {
165         return ctx.cipherSuites().toArray(new String[0]);
166     }
167 
168     @Override
169     public String[] getEnabledCipherSuites() {
170         return getSupportedCipherSuites();
171     }
172 
173     @Override
174     public void setEnabledCipherSuites(String[] suites) {
175         throw new UnsupportedOperationException();
176     }
177 
178     @Override
179     public String[] getSupportedProtocols() {
180         // QUIC only supports TLSv1.3
181         return new String[] { "TLSv1.3" };
182     }
183 
184     @Override
185     public String[] getEnabledProtocols() {
186         return getSupportedProtocols();
187     }
188 
189     @Override
190     public void setEnabledProtocols(String[] protocols) {
191         throw new UnsupportedOperationException();
192     }
193 
194     @Override
195     public SSLSession getSession() {
196         return session;
197     }
198 
199     @Override
200     @Nullable
201     public SSLSession getHandshakeSession() {
202         if (handshakeFinished) {
203             return null;
204         }
205         return session;
206     }
207 
208     @Override
209     public void beginHandshake() {
210         // NOOP
211     }
212 
213     @Override
214     public SSLEngineResult.HandshakeStatus getHandshakeStatus() {
215         if (handshakeFinished) {
216             return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
217         }
218         return SSLEngineResult.HandshakeStatus.NEED_WRAP;
219     }
220 
221     @Override
222     public void setUseClientMode(boolean clientMode) {
223         if (clientMode != ctx.isClient()) {
224             throw new UnsupportedOperationException();
225         }
226     }
227 
228     @Override
229     public boolean getUseClientMode() {
230         return ctx.isClient();
231     }
232 
233     @Override
234     public void setNeedClientAuth(boolean b) {
235         throw new UnsupportedOperationException();
236     }
237 
238     @Override
239     public boolean getNeedClientAuth() {
240         return ctx.clientAuth == ClientAuth.REQUIRE;
241     }
242 
243     @Override
244     public void setWantClientAuth(boolean b) {
245         throw new UnsupportedOperationException();
246     }
247 
248     @Override
249     public boolean getWantClientAuth() {
250         return ctx.clientAuth == ClientAuth.OPTIONAL;
251     }
252 
253     @Override
254     public void setEnableSessionCreation(boolean flag) {
255         throw new UnsupportedOperationException();
256     }
257 
258     @Override
259     public boolean getEnableSessionCreation() {
260         return false;
261     }
262 
263     synchronized void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate,
264                                         byte[][] peerCertificateChain,
265                                         long creationTime, long timeout,
266                                         byte @Nullable [] applicationProtocol, boolean sessionReused) {
267         if (applicationProtocol == null) {
268             this.applicationProtocol = null;
269         } else {
270             this.applicationProtocol = new String(applicationProtocol);
271         }
272         session.handshakeFinished(id, cipher, protocol, peerCertificate, peerCertificateChain, creationTime, timeout);
273         this.sessionReused = sessionReused;
274         handshakeFinished = true;
275     }
276 
277     void removeSessionFromCacheIfInvalid() {
278         session.removeFromCacheIfInvalid();
279     }
280 
281     synchronized boolean isSessionReused() {
282         return sessionReused;
283     }
284 
285     private final class QuicheQuicSslSession implements SSLSession {
286         private X509Certificate[] x509PeerCerts;
287         private Certificate[] peerCerts;
288         private String protocol;
289         private String cipher;
290         private byte[] id;
291         private long creationTime = -1;
292         private long timeout = -1;
293         private boolean invalid;
294         private long lastAccessedTime = -1;
295 
296         // lazy init for memory reasons
297         private Map<String, Object> values;
298 
299         private boolean isEmpty(Object @Nullable [] arr) {
300             return arr == null || arr.length == 0;
301         }
302         private boolean isEmpty(byte @Nullable [] arr) {
303             return arr == null || arr.length == 0;
304         }
305 
306         void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate,
307                                byte[][] peerCertificateChain, long creationTime, long timeout) {
308             synchronized (QuicheQuicSslEngine.this) {
309                 initPeerCerts(peerCertificateChain, peerCertificate);
310                 this.id = id;
311                 this.cipher = cipher;
312                 this.protocol = protocol;
313                 this.creationTime = creationTime * 1000L;
314                 this.timeout = timeout * 1000L;
315                 lastAccessedTime = System.currentTimeMillis();
316             }
317         }
318 
319         void removeFromCacheIfInvalid() {
320             if (!isValid()) {
321                 // Shouldn't be re-used again
322                 removeFromCache();
323             }
324         }
325 
326         private void removeFromCache() {
327             // Shouldn't be re-used again
328             QuicClientSessionCache cache = ctx.getSessionCache();
329             if (cache != null) {
330                 cache.removeSession(getPeerHost(), getPeerPort());
331             }
332         }
333 
334         /**
335          * Init peer certificates that can be obtained via {@link #getPeerCertificateChain()}
336          * and {@link #getPeerCertificates()}.
337          */
338         private void initPeerCerts(byte[][] chain, byte[] clientCert) {
339             // Return the full chain from the JNI layer.
340             if (getUseClientMode()) {
341                 if (isEmpty(chain)) {
342                     peerCerts = EmptyArrays.EMPTY_CERTIFICATES;
343                     x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES;
344                 } else {
345                     peerCerts = new Certificate[chain.length];
346                     x509PeerCerts = new X509Certificate[chain.length];
347                     initCerts(chain, 0);
348                 }
349             } else {
350                 // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer
351                 // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our
352                 // array later.
353                 //
354                 // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html
355                 if (isEmpty(clientCert)) {
356                     peerCerts = EmptyArrays.EMPTY_CERTIFICATES;
357                     x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES;
358                 } else {
359                     if (isEmpty(chain)) {
360                         peerCerts = new Certificate[] {new LazyX509Certificate(clientCert)};
361                         x509PeerCerts = new X509Certificate[] {new LazyJavaxX509Certificate(clientCert)};
362                     } else {
363                         peerCerts = new Certificate[chain.length + 1];
364                         x509PeerCerts = new X509Certificate[chain.length + 1];
365                         peerCerts[0] = new LazyX509Certificate(clientCert);
366                         x509PeerCerts[0] = new LazyJavaxX509Certificate(clientCert);
367                         initCerts(chain, 1);
368                     }
369                 }
370             }
371         }
372 
373         private void initCerts(byte[][] chain, int startPos) {
374             for (int i = 0; i < chain.length; i++) {
375                 int certPos = startPos + i;
376                 peerCerts[certPos] = new LazyX509Certificate(chain[i]);
377                 x509PeerCerts[certPos] = new LazyJavaxX509Certificate(chain[i]);
378             }
379         }
380 
381         @Override
382         public byte[] getId() {
383             synchronized (QuicheQuicSslSession.this) {
384                 if (id == null) {
385                     return EmptyArrays.EMPTY_BYTES;
386                 }
387                 return id.clone();
388             }
389         }
390 
391         @Override
392         public SSLSessionContext getSessionContext() {
393             return ctx.sessionContext();
394         }
395 
396         @Override
397         public long getCreationTime() {
398             synchronized (QuicheQuicSslEngine.this) {
399                 return creationTime;
400             }
401         }
402 
403         @Override
404         public long getLastAccessedTime() {
405             return lastAccessedTime;
406         }
407 
408         @Override
409         public void invalidate() {
410             boolean removeFromCache;
411             synchronized (this) {
412                 removeFromCache = !invalid;
413                 invalid = true;
414             }
415             if (removeFromCache) {
416                 removeFromCache();
417             }
418         }
419 
420         @Override
421         public boolean isValid() {
422             synchronized (QuicheQuicSslEngine.this) {
423                 return !invalid && System.currentTimeMillis() - timeout < creationTime;
424             }
425         }
426 
427         @Override
428         public void putValue(String name, Object value) {
429             ObjectUtil.checkNotNull(name, "name");
430             ObjectUtil.checkNotNull(value, "value");
431 
432             final Object old;
433             synchronized (this) {
434                 Map<String, Object> values = this.values;
435                 if (values == null) {
436                     // Use size of 2 to keep the memory overhead small
437                     values = this.values = new HashMap<>(2);
438                 }
439                 old = values.put(name, value);
440             }
441 
442             if (value instanceof SSLSessionBindingListener) {
443                 // Use newSSLSessionBindingEvent so we alway use the wrapper if needed.
444                 ((SSLSessionBindingListener) value).valueBound(newSSLSessionBindingEvent(name));
445             }
446             notifyUnbound(old, name);
447         }
448 
449         @Override
450         @Nullable
451         public Object getValue(String name) {
452             ObjectUtil.checkNotNull(name, "name");
453             synchronized (this) {
454                 if (values == null) {
455                     return null;
456                 }
457                 return values.get(name);
458             }
459         }
460 
461         @Override
462         public void removeValue(String name) {
463             ObjectUtil.checkNotNull(name, "name");
464 
465             final Object old;
466             synchronized (this) {
467                 Map<String, Object> values = this.values;
468                 if (values == null) {
469                     return;
470                 }
471                 old = values.remove(name);
472             }
473 
474             notifyUnbound(old, name);
475         }
476 
477         @Override
478         public String[] getValueNames() {
479             synchronized (this) {
480                 Map<String, Object> values = this.values;
481                 if (values == null || values.isEmpty()) {
482                     return EmptyArrays.EMPTY_STRINGS;
483                 }
484                 return values.keySet().toArray(new String[0]);
485             }
486         }
487 
488         private SSLSessionBindingEvent newSSLSessionBindingEvent(String name) {
489             return new SSLSessionBindingEvent(session, name);
490         }
491 
492         private void notifyUnbound(@Nullable Object value, String name) {
493             if (value instanceof SSLSessionBindingListener) {
494                 // Use newSSLSessionBindingEvent so we alway use the wrapper if needed.
495                 ((SSLSessionBindingListener) value).valueUnbound(newSSLSessionBindingEvent(name));
496             }
497         }
498 
499         @Override
500         public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
501             synchronized (QuicheQuicSslEngine.this) {
502                 if (isEmpty(peerCerts)) {
503                     throw new SSLPeerUnverifiedException("peer not verified");
504                 }
505                 return peerCerts.clone();
506             }
507         }
508 
509         @Override
510         public Certificate @Nullable [] getLocalCertificates() {
511             Certificate[] localCerts = localCertificateChain;
512             if (localCerts == null) {
513                 return null;
514             }
515             return localCerts.clone();
516         }
517 
518         @Override
519         public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
520             synchronized (QuicheQuicSslEngine.this) {
521                 if (isEmpty(x509PeerCerts)) {
522                     throw new SSLPeerUnverifiedException("peer not verified");
523                 }
524                 return x509PeerCerts.clone();
525             }
526         }
527 
528         @Override
529         public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
530             Certificate[] peer = getPeerCertificates();
531             // No need for null or length > 0 is needed as this is done in getPeerCertificates()
532             // already.
533             return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal();
534         }
535 
536         @Override
537         @Nullable
538         public Principal getLocalPrincipal() {
539             Certificate[] local = localCertificateChain;
540             if (local == null || local.length == 0) {
541                 return null;
542             }
543             return ((java.security.cert.X509Certificate) local[0]).getIssuerX500Principal();
544         }
545 
546         @Override
547         public String getCipherSuite() {
548             return cipher;
549         }
550 
551         @Override
552         public String getProtocol() {
553             return protocol;
554         }
555 
556         @Override
557         @Nullable
558         public String getPeerHost() {
559             return peerHost;
560         }
561 
562         @Override
563         public int getPeerPort() {
564             return peerPort;
565         }
566 
567         @Override
568         public int getPacketBufferSize() {
569             return -1;
570         }
571 
572         @Override
573         public int getApplicationBufferSize() {
574             return -1;
575         }
576 
577         @Override
578         public boolean equals(Object o) {
579             if (this == o) {
580                 return true;
581             }
582             if (o == null || getClass() != o.getClass()) {
583                 return false;
584             }
585             QuicheQuicSslSession that = (QuicheQuicSslSession) o;
586             return Arrays.equals(getId(), that.getId());
587         }
588 
589         @Override
590         public int hashCode() {
591             return Arrays.hashCode(getId());
592         }
593     }
594 }