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 NewSessionTicketMessage(HandshakeContext context, int ticketLifetime, SecureRandom generator, byte[] ticketNonce, byte[] ticket)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 NewSessionTicketMessage(HandshakeContext context, ByteBuffer m)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 handshakeType()119 public SSLHandshake handshakeType() { 120 return SSLHandshake.NEW_SESSION_TICKET; 121 } 122 123 @Override messageLength()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 send(HandshakeOutStream hos)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 toString()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 derivePreSharedKey(CipherSuite.HashAlg hashAlg, SecretKey resumptionMasterSecret, byte[] nonce)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. NewSessionTicketKickstartProducer()192 private NewSessionTicketKickstartProducer() { 193 // blank 194 } 195 196 @Override produce(ConnectionContext context)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. NewSessionTicketProducer()286 private NewSessionTicketProducer() { 287 // blank 288 } 289 290 @Override produce(ConnectionContext context, HandshakeMessage message)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. NewSessionTicketConsumer()305 private NewSessionTicketConsumer() { 306 // blank 307 } 308 309 @Override consume(ConnectionContext context, ByteBuffer message)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 390