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