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