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