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 NewSessionTicketMessage(HandshakeContext context, int ticketLifetime, SecureRandom generator, byte[] ticketNonce, byte[] ticket)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 NewSessionTicketMessage(HandshakeContext context, ByteBuffer m)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 throw 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 throw 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 throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER, 105 "No ticket in the NewSessionTicket handshake message"); 106 } 107 108 if (m.remaining() < 2) { 109 throw 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 handshakeType()120 public SSLHandshake handshakeType() { 121 return SSLHandshake.NEW_SESSION_TICKET; 122 } 123 124 @Override messageLength()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 send(HandshakeOutStream hos)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 toString()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 derivePreSharedKey(CipherSuite.HashAlg hashAlg, SecretKey resumptionMasterSecret, byte[] nonce)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. NewSessionTicketKickstartProducer()193 private NewSessionTicketKickstartProducer() { 194 // blank 195 } 196 197 @Override produce(ConnectionContext context)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 = 264 new SSLSessionImpl(shc.handshakeSession, newId); 265 shc.handshakeSession.addChild(sessionCopy); 266 sessionCopy.setPreSharedKey(psk); 267 sessionCopy.setPskIdentity(newId.getId()); 268 sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd); 269 sessionCache.put(sessionCopy); 270 271 // Output the handshake message. 272 nstm.write(shc.handshakeOutput); 273 shc.handshakeOutput.flush(); 274 275 // The message has been delivered. 276 return null; 277 } 278 } 279 280 /** 281 * The "NewSessionTicket" handshake message producer. 282 */ 283 private static final class NewSessionTicketProducer 284 implements HandshakeProducer { 285 286 // Prevent instantiation of this class. NewSessionTicketProducer()287 private NewSessionTicketProducer() { 288 // blank 289 } 290 291 @Override produce(ConnectionContext context, HandshakeMessage message)292 public byte[] produce(ConnectionContext context, 293 HandshakeMessage message) throws IOException { 294 295 // NSTM may be sent in response to handshake messages. 296 // For example: key update 297 298 throw new ProviderException( 299 "NewSessionTicket handshake producer not implemented"); 300 } 301 } 302 303 private static final 304 class NewSessionTicketConsumer implements SSLConsumer { 305 // Prevent instantiation of this class. NewSessionTicketConsumer()306 private NewSessionTicketConsumer() { 307 // blank 308 } 309 310 @Override consume(ConnectionContext context, ByteBuffer message)311 public void consume(ConnectionContext context, 312 ByteBuffer message) throws IOException { 313 314 // Note: Although the resumption master secret depends on the 315 // client's second flight, servers which do not request client 316 // authentication MAY compute the remainder of the transcript 317 // independently and then send a NewSessionTicket immediately 318 // upon sending its Finished rather than waiting for the client 319 // Finished. 320 // 321 // The consuming happens in client side only. As the server 322 // may send the NewSessionTicket before handshake complete, the 323 // context may be a PostHandshakeContext or HandshakeContext 324 // instance. 325 HandshakeContext hc = (HandshakeContext)context; 326 NewSessionTicketMessage nstm = 327 new NewSessionTicketMessage(hc, message); 328 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { 329 SSLLogger.fine( 330 "Consuming NewSessionTicket message", nstm); 331 } 332 333 // discard tickets with timeout 0 334 if (nstm.ticketLifetime <= 0 || 335 nstm.ticketLifetime > MAX_TICKET_LIFETIME) { 336 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { 337 SSLLogger.fine( 338 "Discarding NewSessionTicket with lifetime " 339 + nstm.ticketLifetime, nstm); 340 } 341 return; 342 } 343 344 SSLSessionContextImpl sessionCache = (SSLSessionContextImpl) 345 hc.sslContext.engineGetClientSessionContext(); 346 347 if (sessionCache.getSessionTimeout() > MAX_TICKET_LIFETIME) { 348 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { 349 SSLLogger.fine( 350 "Session cache lifetime is too long. Discarding ticket."); 351 } 352 return; 353 } 354 355 SSLSessionImpl sessionToSave = hc.conContext.conSession; 356 357 Optional<SecretKey> resumptionMasterSecret = 358 sessionToSave.getResumptionMasterSecret(); 359 if (!resumptionMasterSecret.isPresent()) { 360 if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { 361 SSLLogger.fine( 362 "Session has no resumption master secret. Ignoring ticket."); 363 } 364 return; 365 } 366 367 // derive the PSK 368 SecretKey psk = derivePreSharedKey( 369 sessionToSave.getSuite().hashAlg, resumptionMasterSecret.get(), 370 nstm.ticketNonce); 371 372 // create and cache the new session 373 // The new session must be a child of the existing session so 374 // they will be invalidated together, etc. 375 SessionId newId = 376 new SessionId(true, hc.sslContext.getSecureRandom()); 377 SSLSessionImpl sessionCopy = new SSLSessionImpl(sessionToSave, 378 newId); 379 sessionToSave.addChild(sessionCopy); 380 sessionCopy.setPreSharedKey(psk); 381 sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd); 382 sessionCopy.setPskIdentity(nstm.ticket); 383 sessionCache.put(sessionCopy); 384 385 // clean handshake context 386 hc.conContext.finishPostHandshake(); 387 } 388 } 389 } 390 391