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