src/java.base/share/classes/sun/security/ssl/NewSessionTicket.java
branchJDK-8145252-TLS13-branch
changeset 56542 56aaa6cb3693
child 56558 4a3deb6759b1
equal deleted inserted replaced
56541:92cbbfc996f3 56542:56aaa6cb3693
       
     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 
       
    39 import sun.security.ssl.SSLHandshake.HandshakeMessage;
       
    40 
       
    41 /**
       
    42  * Pack of the NewSessionTicket handshake message.
       
    43  */
       
    44 final class NewSessionTicket {
       
    45 
       
    46     private static final int SEVEN_DAYS_IN_SECONDS = 604800;
       
    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             this.ticketLifetime = Record.getInt32(m);
       
    82             this.ticketAgeAdd = Record.getInt32(m);
       
    83             this.ticketNonce = Record.getBytes8(m);
       
    84             this.ticket = Record.getBytes16(m);
       
    85             if (this.ticket.length == 0) {
       
    86                 context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
       
    87                 "Ticket has length 0");
       
    88             }
       
    89 
       
    90             SSLExtension[] supportedExtensions =
       
    91                 context.sslConfig.getEnabledExtensions(
       
    92                 SSLHandshake.NEW_SESSION_TICKET);
       
    93 
       
    94             if (m.hasRemaining()) {
       
    95                 this.extensions =
       
    96                     new SSLExtensions(this, m, supportedExtensions);
       
    97             } else {
       
    98                 this.extensions = new SSLExtensions(this);
       
    99             }
       
   100         }
       
   101 
       
   102         @Override
       
   103         public SSLHandshake handshakeType() {
       
   104             return SSLHandshake.NEW_SESSION_TICKET;
       
   105         }
       
   106 
       
   107         @Override
       
   108         public int messageLength() {
       
   109             return 8 + ticketNonce.length + 1 + ticket.length
       
   110                 + 2 + extensions.length();
       
   111         }
       
   112 
       
   113         @Override
       
   114         public void send(HandshakeOutStream hos) throws IOException {
       
   115             hos.putInt32(ticketLifetime);
       
   116             hos.putInt32(ticketAgeAdd);
       
   117             hos.putBytes8(ticketNonce);
       
   118             hos.putBytes16(ticket);
       
   119             extensions.send(hos);
       
   120         }
       
   121 
       
   122         @Override
       
   123         public String toString() {
       
   124             MessageFormat messageFormat = new MessageFormat(
       
   125                 "\"NewSessionTicket\": '{'\n" +
       
   126                 "  \"ticket_lifetime\"      : \"{0}\",\n" +
       
   127                 "  \"ticket_age_add\"       : \"{1}\",\n" +
       
   128                 "  \"ticket_nonce\"         : \"{2}\",\n" +
       
   129                 "  \"ticket\"               : \"{3}\",\n" +
       
   130                 "  \"extensions\"           : [\n" +
       
   131                 "{5}\n" +
       
   132                 "  ]\n" +
       
   133                 "'}'",
       
   134                 Locale.ENGLISH);
       
   135 
       
   136             Object[] messageFields = {
       
   137                 ticketLifetime,
       
   138                 "omitted", //ticketAgeAdd should not be logged
       
   139                 Utilities.toHexString(ticketNonce),
       
   140                 Utilities.toHexString(ticket),
       
   141                 Utilities.indent(extensions.toString(), "    ")
       
   142             };
       
   143 
       
   144             return messageFormat.format(messageFields);
       
   145         }
       
   146     }
       
   147 
       
   148     private static SecretKey derivePreSharedKey(CipherSuite.HashAlg hashAlg,
       
   149                                                 SecretKey resumptionMasterSecret,
       
   150                                                 byte[] nonce) throws IOException {
       
   151 
       
   152         try {
       
   153             HKDF hkdf = new HKDF(hashAlg.name);
       
   154             byte[] hkdfInfo = SSLSecretDerivation.createHkdfInfo(
       
   155                 "tls13 resumption".getBytes(), nonce, hashAlg.hashLength);
       
   156             return hkdf.expand(resumptionMasterSecret, hkdfInfo,
       
   157                 hashAlg.hashLength, "TlsPreSharedKey");
       
   158 
       
   159         } catch  (GeneralSecurityException gse) {
       
   160             throw (SSLHandshakeException) new SSLHandshakeException(
       
   161                 "Could not derive PSK").initCause(gse);
       
   162         }
       
   163     }
       
   164 
       
   165     private static final
       
   166     class NewSessionTicketKickstartProducer implements SSLProducer {
       
   167 
       
   168         @Override
       
   169         public byte[] produce(ConnectionContext context) throws IOException {
       
   170             // The producing happens in server side only.
       
   171             ServerHandshakeContext shc = (ServerHandshakeContext)context;
       
   172 
       
   173             if (shc.pskKeyExchangeModes.isEmpty()) {
       
   174                 // client doesn't support PSK
       
   175                 return null;
       
   176             }
       
   177             if (!shc.handshakeSession.isRejoinable()) {
       
   178                 return null;
       
   179             }
       
   180 
       
   181             // get a new session ID
       
   182             SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
       
   183                 shc.sslContext.engineGetServerSessionContext();
       
   184             SessionId newId = new SessionId(true,
       
   185                 shc.sslContext.getSecureRandom());
       
   186 
       
   187             Optional<SecretKey> resumptionMasterSecret =
       
   188                 shc.handshakeSession.getResumptionMasterSecret();
       
   189             if (!resumptionMasterSecret.isPresent()) {
       
   190                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   191                     SSLLogger.fine(
       
   192                     "Session has no resumption secret. No ticket sent.");
       
   193                 }
       
   194                 return null;
       
   195             }
       
   196 
       
   197             // construct the PSK and handshake message
       
   198             BigInteger nonce = shc.handshakeSession.incrTicketNonceCounter();
       
   199             byte[] nonceArr = nonce.toByteArray();
       
   200             SecretKey psk = derivePreSharedKey(shc.negotiatedCipherSuite.hashAlg,
       
   201                 resumptionMasterSecret.get(), nonceArr);
       
   202 
       
   203             int sessionTimeoutSeconds = sessionCache.getSessionTimeout();
       
   204             if (sessionTimeoutSeconds > SEVEN_DAYS_IN_SECONDS) {
       
   205                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   206                     SSLLogger.fine(
       
   207                     "Session timeout is too long. No NewSessionTicket sent.");
       
   208                 }
       
   209                 return null;
       
   210             }
       
   211             NewSessionTicketMessage nstm = new NewSessionTicketMessage(shc,
       
   212                 sessionTimeoutSeconds, shc.sslContext.getSecureRandom(),
       
   213                 nonceArr, newId.getId());
       
   214             if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   215                 SSLLogger.fine(
       
   216                         "Produced NewSessionTicket handshake message", nstm);
       
   217             }
       
   218 
       
   219             // cache the new session
       
   220             SSLSessionImpl sessionCopy = new SSLSessionImpl(shc,
       
   221                 shc.handshakeSession.getSuite(), newId,
       
   222                 shc.handshakeSession.getCreationTime());
       
   223             sessionCopy.setPreSharedKey(psk);
       
   224             sessionCopy.setPskIdentity(newId.getId());
       
   225             sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd);
       
   226             sessionCache.put(sessionCopy);
       
   227 
       
   228             // Output the handshake message.
       
   229             nstm.write(shc.handshakeOutput);
       
   230             shc.handshakeOutput.flush();
       
   231 
       
   232             // The message has been delivered.
       
   233             return null;
       
   234         }
       
   235     }
       
   236 
       
   237     /**
       
   238      * The "NewSessionTicket" handshake message producer.
       
   239      */
       
   240     private static final class NewSessionTicketProducer
       
   241             implements HandshakeProducer {
       
   242 
       
   243         // Prevent instantiation of this class.
       
   244         private NewSessionTicketProducer() {
       
   245             // blank
       
   246         }
       
   247 
       
   248         @Override
       
   249         public byte[] produce(ConnectionContext context,
       
   250                 HandshakeMessage message) throws IOException {
       
   251 
       
   252             // NSTM may be sent in response to handshake messages.
       
   253             // For example: key update
       
   254 
       
   255             throw new ProviderException(
       
   256                 "NewSessionTicket handshake producer not implemented");
       
   257         }
       
   258     }
       
   259 
       
   260 
       
   261 
       
   262     private static final
       
   263             class NewSessionTicketConsumer implements SSLConsumer {
       
   264         // Prevent instantiation of this class.
       
   265         private NewSessionTicketConsumer() {
       
   266             // blank
       
   267         }
       
   268 
       
   269         @Override
       
   270         public void consume(ConnectionContext context,
       
   271                             ByteBuffer message) throws IOException {
       
   272             // The consuming happens in client side only.
       
   273             ClientHandshakeContext chc = (ClientHandshakeContext)context;
       
   274             NewSessionTicketMessage nstm = new NewSessionTicketMessage(chc, message);
       
   275             if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   276                 SSLLogger.fine(
       
   277                 "Consuming NewSessionTicket message", nstm);
       
   278             }
       
   279 
       
   280             // discard tickets with timeout 0
       
   281             if (nstm.ticketLifetime <= 0 ||
       
   282                 nstm.ticketLifetime > SEVEN_DAYS_IN_SECONDS) {
       
   283                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   284                     SSLLogger.fine(
       
   285                     "Discarding NewSessionTicket with lifetime "
       
   286                         + nstm.ticketLifetime, nstm);
       
   287                 }
       
   288                 return;
       
   289             }
       
   290 
       
   291             SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
       
   292                 chc.sslContext.engineGetClientSessionContext();
       
   293 
       
   294             if (sessionCache.getSessionTimeout() > SEVEN_DAYS_IN_SECONDS) {
       
   295                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   296                     SSLLogger.fine(
       
   297                     "Session cache lifetime is too long. Discarding ticket.");
       
   298                 }
       
   299                 return;
       
   300             }
       
   301 
       
   302             SSLSessionImpl sessionToSave = chc.conContext.conSession;
       
   303 
       
   304             Optional<SecretKey> resumptionMasterSecret =
       
   305                 sessionToSave.getResumptionMasterSecret();
       
   306             if (!resumptionMasterSecret.isPresent()) {
       
   307                 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
       
   308                     SSLLogger.fine(
       
   309                     "Session has no resumption master secret. Ignoring ticket.");
       
   310                 }
       
   311                 return;
       
   312             }
       
   313 
       
   314             // derive the PSK
       
   315             SecretKey psk = derivePreSharedKey(
       
   316                 sessionToSave.getSuite().hashAlg, resumptionMasterSecret.get(),
       
   317                 nstm.ticketNonce);
       
   318 
       
   319             // create the new session from the context
       
   320             chc.negotiatedProtocol = chc.conContext.protocolVersion;
       
   321             SessionId newId =
       
   322                 new SessionId(true, chc.sslContext.getSecureRandom());
       
   323             SSLSessionImpl sessionCopy =
       
   324                 new SSLSessionImpl(chc, sessionToSave.getSuite(), newId,
       
   325                 sessionToSave.getCreationTime());
       
   326             sessionCopy.setPreSharedKey(psk);
       
   327             sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd);
       
   328             sessionCopy.setPskIdentity(nstm.ticket);
       
   329             sessionCache.put(sessionCopy);
       
   330 
       
   331             // The handshakeContext is no longer needed
       
   332             chc.conContext.handshakeContext = null;
       
   333         }
       
   334     }
       
   335 
       
   336 }
       
   337