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