src/java.base/share/classes/sun/security/ssl/NewSessionTicket.java
changeset 50768 68fa3d4026ea
child 52512 1838347a803b
equal deleted inserted replaced
50767:356eaea05bf0 50768:68fa3d4026ea
       
     1 /*
       
     2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 package sun.security.ssl;
       
    26 
       
    27 import java.io.IOException;
       
    28 import java.math.BigInteger;
       
    29 import java.nio.ByteBuffer;
       
    30 import java.security.GeneralSecurityException;
       
    31 import java.security.ProviderException;
       
    32 import java.security.SecureRandom;
       
    33 import java.text.MessageFormat;
       
    34 import java.util.Locale;
       
    35 import java.util.Optional;
       
    36 import javax.crypto.SecretKey;
       
    37 import javax.net.ssl.SSLHandshakeException;
       
    38 import sun.security.ssl.PskKeyExchangeModesExtension.PskKeyExchangeModesSpec;
       
    39 
       
    40 import sun.security.ssl.SSLHandshake.HandshakeMessage;
       
    41 
       
    42 /**
       
    43  * Pack of the NewSessionTicket handshake message.
       
    44  */
       
    45 final class NewSessionTicket {
       
    46     private static final int MAX_TICKET_LIFETIME = 604800;  // seconds, 7 days
       
    47 
       
    48     static final SSLConsumer handshakeConsumer =
       
    49         new NewSessionTicketConsumer();
       
    50     static final SSLProducer kickstartProducer =
       
    51         new NewSessionTicketKickstartProducer();
       
    52     static final HandshakeProducer handshakeProducer =
       
    53         new NewSessionTicketProducer();
       
    54 
       
    55     /**
       
    56      * The NewSessionTicketMessage handshake message.
       
    57      */
       
    58     static final class NewSessionTicketMessage extends HandshakeMessage {
       
    59         final int ticketLifetime;
       
    60         final int ticketAgeAdd;
       
    61         final byte[] ticketNonce;
       
    62         final byte[] ticket;
       
    63         final SSLExtensions extensions;
       
    64 
       
    65         NewSessionTicketMessage(HandshakeContext context,
       
    66                 int ticketLifetime, SecureRandom generator,
       
    67                 byte[] ticketNonce, byte[] ticket) {
       
    68             super(context);
       
    69 
       
    70             this.ticketLifetime = ticketLifetime;
       
    71             this.ticketAgeAdd = generator.nextInt();
       
    72             this.ticketNonce = ticketNonce;
       
    73             this.ticket = ticket;
       
    74             this.extensions = new SSLExtensions(this);
       
    75         }
       
    76 
       
    77         NewSessionTicketMessage(HandshakeContext context,
       
    78                 ByteBuffer m) throws IOException {
       
    79             super(context);
       
    80 
       
    81             // struct {
       
    82             //     uint32 ticket_lifetime;
       
    83             //     uint32 ticket_age_add;
       
    84             //     opaque ticket_nonce<0..255>;
       
    85             //     opaque ticket<1..2^16-1>;
       
    86             //     Extension extensions<0..2^16-2>;
       
    87             // } NewSessionTicket;
       
    88             if (m.remaining() < 14) {
       
    89                 context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
       
    90                     "Invalid NewSessionTicket message: no sufficient data");
       
    91             }
       
    92 
       
    93             this.ticketLifetime = Record.getInt32(m);
       
    94             this.ticketAgeAdd = Record.getInt32(m);
       
    95             this.ticketNonce = Record.getBytes8(m);
       
    96 
       
    97             if (m.remaining() < 5) {
       
    98                 context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
       
    99                     "Invalid NewSessionTicket message: no sufficient data");
       
   100             }
       
   101 
       
   102             this.ticket = Record.getBytes16(m);
       
   103             if (ticket.length == 0) {
       
   104                 context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
       
   105                     "No ticket in the NewSessionTicket handshake message");
       
   106             }
       
   107 
       
   108             if (m.remaining() < 2) {
       
   109                 context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
       
   110                     "Invalid NewSessionTicket message: no sufficient data");
       
   111             }
       
   112 
       
   113             SSLExtension[] supportedExtensions =
       
   114                     context.sslConfig.getEnabledExtensions(
       
   115                             SSLHandshake.NEW_SESSION_TICKET);
       
   116             this.extensions = new SSLExtensions(this, m, supportedExtensions);
       
   117         }
       
   118 
       
   119         @Override
       
   120         public SSLHandshake handshakeType() {
       
   121             return SSLHandshake.NEW_SESSION_TICKET;
       
   122         }
       
   123 
       
   124         @Override
       
   125         public int messageLength() {
       
   126             int extLen = extensions.length();
       
   127             if (extLen == 0) {
       
   128                 extLen = 2;     // empty extensions
       
   129             }
       
   130 
       
   131             return 8 + ticketNonce.length + 1 +
       
   132                        ticket.length + 2 + extLen;
       
   133         }
       
   134 
       
   135         @Override
       
   136         public void send(HandshakeOutStream hos) throws IOException {
       
   137             hos.putInt32(ticketLifetime);
       
   138             hos.putInt32(ticketAgeAdd);
       
   139             hos.putBytes8(ticketNonce);
       
   140             hos.putBytes16(ticket);
       
   141 
       
   142             // Is it an empty extensions?
       
   143             if (extensions.length() == 0) {
       
   144                 hos.putInt16(0);
       
   145             } else {
       
   146                 extensions.send(hos);
       
   147             }
       
   148         }
       
   149 
       
   150         @Override
       
   151         public String toString() {
       
   152             MessageFormat messageFormat = new MessageFormat(
       
   153                 "\"NewSessionTicket\": '{'\n" +
       
   154                 "  \"ticket_lifetime\"      : \"{0}\",\n" +
       
   155                 "  \"ticket_age_add\"       : \"{1}\",\n" +
       
   156                 "  \"ticket_nonce\"         : \"{2}\",\n" +
       
   157                 "  \"ticket\"               : \"{3}\",\n" +
       
   158                 "  \"extensions\"           : [\n" +
       
   159                 "{4}\n" +
       
   160                 "  ]\n" +
       
   161                 "'}'",
       
   162                 Locale.ENGLISH);
       
   163 
       
   164             Object[] messageFields = {
       
   165                 ticketLifetime,
       
   166                 "<omitted>",    //ticketAgeAdd should not be logged
       
   167                 Utilities.toHexString(ticketNonce),
       
   168                 Utilities.toHexString(ticket),
       
   169                 Utilities.indent(extensions.toString(), "    ")
       
   170             };
       
   171 
       
   172             return messageFormat.format(messageFields);
       
   173         }
       
   174     }
       
   175 
       
   176     private static SecretKey derivePreSharedKey(CipherSuite.HashAlg hashAlg,
       
   177             SecretKey resumptionMasterSecret, byte[] nonce) throws IOException {
       
   178         try {
       
   179             HKDF hkdf = new HKDF(hashAlg.name);
       
   180             byte[] hkdfInfo = SSLSecretDerivation.createHkdfInfo(
       
   181                     "tls13 resumption".getBytes(), nonce, hashAlg.hashLength);
       
   182             return hkdf.expand(resumptionMasterSecret, hkdfInfo,
       
   183                     hashAlg.hashLength, "TlsPreSharedKey");
       
   184         } catch  (GeneralSecurityException gse) {
       
   185             throw (SSLHandshakeException) new SSLHandshakeException(
       
   186                     "Could not derive PSK").initCause(gse);
       
   187         }
       
   188     }
       
   189 
       
   190     private static final
       
   191             class NewSessionTicketKickstartProducer implements SSLProducer {
       
   192         // Prevent instantiation of this class.
       
   193         private NewSessionTicketKickstartProducer() {
       
   194             // blank
       
   195         }
       
   196 
       
   197         @Override
       
   198         public byte[] produce(ConnectionContext context) throws IOException {
       
   199             // The producing happens in server side only.
       
   200             ServerHandshakeContext shc = (ServerHandshakeContext)context;
       
   201 
       
   202             // Is this session resumable?
       
   203             if (!shc.handshakeSession.isRejoinable()) {
       
   204                 return null;
       
   205             }
       
   206 
       
   207             // What's the requested PSK key exchange modes?
       
   208             //
       
   209             // Note that currently, the NewSessionTicket post-handshake is
       
   210             // produced and delivered only in the current handshake context
       
   211             // if required.
       
   212             PskKeyExchangeModesSpec pkemSpec =
       
   213                     (PskKeyExchangeModesSpec)shc.handshakeExtensions.get(
       
   214                             SSLExtension.PSK_KEY_EXCHANGE_MODES);
       
   215             if (pkemSpec == null || !pkemSpec.contains(
       
   216                 PskKeyExchangeModesExtension.PskKeyExchangeMode.PSK_DHE_KE)) {
       
   217                 // Client doesn't support PSK with (EC)DHE key establishment.
       
   218                 return null;
       
   219             }
       
   220 
       
   221             // get a new session ID
       
   222             SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
       
   223                 shc.sslContext.engineGetServerSessionContext();
       
   224             SessionId newId = new SessionId(true,
       
   225                 shc.sslContext.getSecureRandom());
       
   226 
       
   227             Optional<SecretKey> resumptionMasterSecret =
       
   228                 shc.handshakeSession.getResumptionMasterSecret();
       
   229             if (!resumptionMasterSecret.isPresent()) {
       
   230                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   231                     SSLLogger.fine(
       
   232                         "Session has no resumption secret. No ticket sent.");
       
   233                 }
       
   234                 return null;
       
   235             }
       
   236 
       
   237             // construct the PSK and handshake message
       
   238             BigInteger nonce = shc.handshakeSession.incrTicketNonceCounter();
       
   239             byte[] nonceArr = nonce.toByteArray();
       
   240             SecretKey psk = derivePreSharedKey(
       
   241                     shc.negotiatedCipherSuite.hashAlg,
       
   242                     resumptionMasterSecret.get(), nonceArr);
       
   243 
       
   244             int sessionTimeoutSeconds = sessionCache.getSessionTimeout();
       
   245             if (sessionTimeoutSeconds > MAX_TICKET_LIFETIME) {
       
   246                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   247                     SSLLogger.fine(
       
   248                         "Session timeout is too long. No ticket sent.");
       
   249                 }
       
   250                 return null;
       
   251             }
       
   252             NewSessionTicketMessage nstm = new NewSessionTicketMessage(shc,
       
   253                 sessionTimeoutSeconds, shc.sslContext.getSecureRandom(),
       
   254                 nonceArr, newId.getId());
       
   255             if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   256                 SSLLogger.fine(
       
   257                         "Produced NewSessionTicket handshake message", nstm);
       
   258             }
       
   259 
       
   260             // create and cache the new session
       
   261             // The new session must be a child of the existing session so
       
   262             // they will be invalidated together, etc.
       
   263             SSLSessionImpl sessionCopy = new SSLSessionImpl(shc,
       
   264                     shc.handshakeSession.getSuite(), newId,
       
   265                     shc.handshakeSession.getCreationTime());
       
   266             shc.handshakeSession.addChild(sessionCopy);
       
   267             sessionCopy.setPreSharedKey(psk);
       
   268             sessionCopy.setPskIdentity(newId.getId());
       
   269             sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd);
       
   270             sessionCache.put(sessionCopy);
       
   271 
       
   272             // Output the handshake message.
       
   273             nstm.write(shc.handshakeOutput);
       
   274             shc.handshakeOutput.flush();
       
   275 
       
   276             // The message has been delivered.
       
   277             return null;
       
   278         }
       
   279     }
       
   280 
       
   281     /**
       
   282      * The "NewSessionTicket" handshake message producer.
       
   283      */
       
   284     private static final class NewSessionTicketProducer
       
   285             implements HandshakeProducer {
       
   286 
       
   287         // Prevent instantiation of this class.
       
   288         private NewSessionTicketProducer() {
       
   289             // blank
       
   290         }
       
   291 
       
   292         @Override
       
   293         public byte[] produce(ConnectionContext context,
       
   294                 HandshakeMessage message) throws IOException {
       
   295 
       
   296             // NSTM may be sent in response to handshake messages.
       
   297             // For example: key update
       
   298 
       
   299             throw new ProviderException(
       
   300                 "NewSessionTicket handshake producer not implemented");
       
   301         }
       
   302     }
       
   303 
       
   304     private static final
       
   305             class NewSessionTicketConsumer implements SSLConsumer {
       
   306         // Prevent instantiation of this class.
       
   307         private NewSessionTicketConsumer() {
       
   308             // blank
       
   309         }
       
   310 
       
   311         @Override
       
   312         public void consume(ConnectionContext context,
       
   313                             ByteBuffer message) throws IOException {
       
   314 
       
   315             // Note: Although the resumption master secret depends on the
       
   316             // client's second flight, servers which do not request client
       
   317             // authentication MAY compute the remainder of the transcript
       
   318             // independently and then send a NewSessionTicket immediately
       
   319             // upon sending its Finished rather than waiting for the client
       
   320             // Finished.
       
   321             //
       
   322             // The consuming happens in client side only.  As the server
       
   323             // may send the NewSessionTicket before handshake complete, the
       
   324             // context may be a PostHandshakeContext or HandshakeContext
       
   325             // instance.
       
   326             HandshakeContext hc = (HandshakeContext)context;
       
   327             NewSessionTicketMessage nstm =
       
   328                     new NewSessionTicketMessage(hc, message);
       
   329             if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   330                 SSLLogger.fine(
       
   331                 "Consuming NewSessionTicket message", nstm);
       
   332             }
       
   333 
       
   334             // discard tickets with timeout 0
       
   335             if (nstm.ticketLifetime <= 0 ||
       
   336                 nstm.ticketLifetime > MAX_TICKET_LIFETIME) {
       
   337                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   338                     SSLLogger.fine(
       
   339                     "Discarding NewSessionTicket with lifetime "
       
   340                         + nstm.ticketLifetime, nstm);
       
   341                 }
       
   342                 return;
       
   343             }
       
   344 
       
   345             SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
       
   346                 hc.sslContext.engineGetClientSessionContext();
       
   347 
       
   348             if (sessionCache.getSessionTimeout() > MAX_TICKET_LIFETIME) {
       
   349                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   350                     SSLLogger.fine(
       
   351                     "Session cache lifetime is too long. Discarding ticket.");
       
   352                 }
       
   353                 return;
       
   354             }
       
   355 
       
   356             SSLSessionImpl sessionToSave = hc.conContext.conSession;
       
   357 
       
   358             Optional<SecretKey> resumptionMasterSecret =
       
   359                 sessionToSave.getResumptionMasterSecret();
       
   360             if (!resumptionMasterSecret.isPresent()) {
       
   361                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   362                     SSLLogger.fine(
       
   363                     "Session has no resumption master secret. Ignoring ticket.");
       
   364                 }
       
   365                 return;
       
   366             }
       
   367 
       
   368             // derive the PSK
       
   369             SecretKey psk = derivePreSharedKey(
       
   370                 sessionToSave.getSuite().hashAlg, resumptionMasterSecret.get(),
       
   371                 nstm.ticketNonce);
       
   372 
       
   373             // create and cache the new session
       
   374             // The new session must be a child of the existing session so
       
   375             // they will be invalidated together, etc.
       
   376             SessionId newId =
       
   377                 new SessionId(true, hc.sslContext.getSecureRandom());
       
   378             SSLSessionImpl sessionCopy = new SSLSessionImpl(
       
   379                     hc, sessionToSave.getSuite(), newId,
       
   380                     sessionToSave.getCreationTime());
       
   381             sessionToSave.addChild(sessionCopy);
       
   382             sessionCopy.setPreSharedKey(psk);
       
   383             sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd);
       
   384             sessionCopy.setPskIdentity(nstm.ticket);
       
   385             sessionCache.put(sessionCopy);
       
   386 
       
   387             // clean handshake context
       
   388             hc.conContext.finishPostHandshake();
       
   389         }
       
   390     }
       
   391 }
       
   392