mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-28 23:34:52 +02:00
8211018: Session Resumption without Server-Side State
Reviewed-by: xuelei, jnimeh, jjiang
This commit is contained in:
parent
9597144fb2
commit
94e1d7530f
21 changed files with 1620 additions and 150 deletions
|
@ -35,7 +35,6 @@ import java.util.Collections;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
|
@ -969,11 +968,24 @@ final class ClientHello {
|
|||
}
|
||||
}
|
||||
|
||||
// Is it an abbreviated handshake?
|
||||
if (clientHello.sessionId.length() != 0) {
|
||||
SSLSessionImpl previous = ((SSLSessionContextImpl)shc.sslContext
|
||||
.engineGetServerSessionContext())
|
||||
.get(clientHello.sessionId.getId());
|
||||
// Consume a Session Ticket Extension if it exists
|
||||
SSLExtension[] ext = new SSLExtension[]{
|
||||
SSLExtension.CH_SESSION_TICKET
|
||||
};
|
||||
clientHello.extensions.consumeOnLoad(shc, ext);
|
||||
|
||||
// Does the client want to resume a session?
|
||||
if (clientHello.sessionId.length() != 0 || shc.statelessResumption) {
|
||||
SSLSessionContextImpl cache = (SSLSessionContextImpl)shc.sslContext
|
||||
.engineGetServerSessionContext();
|
||||
|
||||
SSLSessionImpl previous;
|
||||
// Use the stateless session ticket if provided
|
||||
if (shc.statelessResumption) {
|
||||
previous = shc.resumingSession;
|
||||
} else {
|
||||
previous = cache.get(clientHello.sessionId.getId());
|
||||
}
|
||||
|
||||
boolean resumingSession =
|
||||
(previous != null) && previous.isRejoinable();
|
||||
|
@ -1051,14 +1063,20 @@ final class ClientHello {
|
|||
// the resuming options later.
|
||||
shc.isResumption = resumingSession;
|
||||
shc.resumingSession = resumingSession ? previous : null;
|
||||
|
||||
if (!resumingSession && SSLLogger.isOn &&
|
||||
SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Session not resumed.");
|
||||
}
|
||||
}
|
||||
|
||||
// cache the client random number for further using
|
||||
shc.clientHelloRandom = clientHello.clientRandom;
|
||||
|
||||
// Check and launch ClientHello extensions.
|
||||
SSLExtension[] extTypes = shc.sslConfig.getEnabledExtensions(
|
||||
SSLHandshake.CLIENT_HELLO);
|
||||
SSLExtension[] extTypes = shc.sslConfig.getExclusiveExtensions(
|
||||
SSLHandshake.CLIENT_HELLO,
|
||||
Arrays.asList(SSLExtension.CH_SESSION_TICKET));
|
||||
clientHello.extensions.consumeOnLoad(shc, extTypes);
|
||||
|
||||
//
|
||||
|
@ -1276,11 +1294,25 @@ final class ClientHello {
|
|||
}
|
||||
}
|
||||
|
||||
// Is it an abbreviated handshake?
|
||||
|
||||
// Does the client want to resume a session?
|
||||
if (clientHello.sessionId.length() != 0) {
|
||||
SSLSessionImpl previous = ((SSLSessionContextImpl)shc.sslContext
|
||||
.engineGetServerSessionContext())
|
||||
.get(clientHello.sessionId.getId());
|
||||
SSLSessionContextImpl cache = (SSLSessionContextImpl)shc.sslContext
|
||||
.engineGetServerSessionContext();
|
||||
|
||||
// Consume a Session Ticket Extension if it exists
|
||||
SSLExtension[] ext = new SSLExtension[]{
|
||||
SSLExtension.CH_SESSION_TICKET
|
||||
};
|
||||
clientHello.extensions.consumeOnLoad(shc, ext);
|
||||
|
||||
SSLSessionImpl previous;
|
||||
// Use stateless session ticket if provided.
|
||||
if (shc.statelessResumption) {
|
||||
previous = shc.resumingSession;
|
||||
} else {
|
||||
previous = cache.get(clientHello.sessionId.getId());
|
||||
}
|
||||
|
||||
boolean resumingSession =
|
||||
(previous != null) && previous.isRejoinable();
|
||||
|
|
|
@ -410,6 +410,10 @@ final class Finished {
|
|||
chc.conContext.clientVerifyData = fm.verifyData;
|
||||
}
|
||||
|
||||
if (chc.statelessResumption) {
|
||||
chc.handshakeConsumers.put(
|
||||
SSLHandshake.NEW_SESSION_TICKET.id, SSLHandshake.NEW_SESSION_TICKET);
|
||||
}
|
||||
// update the consumers and producers
|
||||
if (!chc.isResumption) {
|
||||
chc.conContext.consumers.put(ContentType.CHANGE_CIPHER_SPEC.id,
|
||||
|
@ -441,6 +445,10 @@ final class Finished {
|
|||
|
||||
private byte[] onProduceFinished(ServerHandshakeContext shc,
|
||||
HandshakeMessage message) throws IOException {
|
||||
if (shc.statelessResumption) {
|
||||
NewSessionTicket.handshake12Producer.produce(shc, message);
|
||||
}
|
||||
|
||||
// Refresh handshake hash
|
||||
shc.handshakeHash.update();
|
||||
|
||||
|
@ -473,7 +481,8 @@ final class Finished {
|
|||
SSLHandshake.FINISHED.id, SSLHandshake.FINISHED);
|
||||
shc.conContext.inputRecord.expectingFinishFlight();
|
||||
} else {
|
||||
if (shc.handshakeSession.isRejoinable()) {
|
||||
if (shc.handshakeSession.isRejoinable() &&
|
||||
!shc.statelessResumption) {
|
||||
((SSLSessionContextImpl)shc.sslContext.
|
||||
engineGetServerSessionContext()).put(
|
||||
shc.handshakeSession);
|
||||
|
@ -591,7 +600,8 @@ final class Finished {
|
|||
}
|
||||
|
||||
if (shc.isResumption) {
|
||||
if (shc.handshakeSession.isRejoinable()) {
|
||||
if (shc.handshakeSession.isRejoinable() &&
|
||||
!shc.statelessResumption) {
|
||||
((SSLSessionContextImpl)shc.sslContext.
|
||||
engineGetServerSessionContext()).put(
|
||||
shc.handshakeSession);
|
||||
|
@ -915,9 +925,9 @@ final class Finished {
|
|||
|
||||
// save the session
|
||||
if (!chc.isResumption && chc.handshakeSession.isRejoinable()) {
|
||||
SSLSessionContextImpl sessionContext = (SSLSessionContextImpl)
|
||||
chc.sslContext.engineGetClientSessionContext();
|
||||
sessionContext.put(chc.handshakeSession);
|
||||
((SSLSessionContextImpl)chc.sslContext.
|
||||
engineGetClientSessionContext()).
|
||||
put(chc.handshakeSession);
|
||||
}
|
||||
|
||||
// derive salt secret
|
||||
|
@ -1028,10 +1038,11 @@ final class Finished {
|
|||
shc.negotiatedProtocol);
|
||||
}
|
||||
|
||||
// save the session
|
||||
if (!shc.isResumption && shc.handshakeSession.isRejoinable()) {
|
||||
// Save the session if possible and not stateless
|
||||
if (!shc.statelessResumption && !shc.isResumption &&
|
||||
shc.handshakeSession.isRejoinable()) {
|
||||
SSLSessionContextImpl sessionContext = (SSLSessionContextImpl)
|
||||
shc.sslContext.engineGetServerSessionContext();
|
||||
shc.sslContext.engineGetServerSessionContext();
|
||||
sessionContext.put(shc.handshakeSession);
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,8 @@ abstract class HandshakeContext implements ConnectionContext {
|
|||
// Resumption
|
||||
boolean isResumption;
|
||||
SSLSessionImpl resumingSession;
|
||||
// Session is using stateless resumption
|
||||
boolean statelessResumption = false;
|
||||
|
||||
final Queue<Map.Entry<Byte, ByteBuffer>> delegatedActions;
|
||||
volatile boolean taskDelegated = false;
|
||||
|
@ -551,7 +553,7 @@ abstract class HandshakeContext implements ConnectionContext {
|
|||
|
||||
List<SNIServerName> getRequestedServerNames() {
|
||||
if (requestedServerNames == null) {
|
||||
return Collections.<SNIServerName>emptyList();
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return requestedServerNames;
|
||||
}
|
||||
|
|
|
@ -28,40 +28,139 @@ import java.io.IOException;
|
|||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.ProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Locale;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import sun.security.ssl.PskKeyExchangeModesExtension.PskKeyExchangeModesSpec;
|
||||
|
||||
import sun.security.ssl.SessionTicketExtension.SessionTicketSpec;
|
||||
import sun.security.ssl.SSLHandshake.HandshakeMessage;
|
||||
import sun.security.util.HexDumpEncoder;
|
||||
|
||||
import static sun.security.ssl.SSLHandshake.NEW_SESSION_TICKET;
|
||||
|
||||
/**
|
||||
* Pack of the NewSessionTicket handshake message.
|
||||
*/
|
||||
final class NewSessionTicket {
|
||||
private static final int MAX_TICKET_LIFETIME = 604800; // seconds, 7 days
|
||||
static final int MAX_TICKET_LIFETIME = 604800; // seconds, 7 days
|
||||
|
||||
static final SSLConsumer handshakeConsumer =
|
||||
new NewSessionTicketConsumer();
|
||||
new T13NewSessionTicketConsumer();
|
||||
static final SSLConsumer handshake12Consumer =
|
||||
new T12NewSessionTicketConsumer();
|
||||
static final SSLProducer kickstartProducer =
|
||||
new NewSessionTicketKickstartProducer();
|
||||
static final HandshakeProducer handshakeProducer =
|
||||
new NewSessionTicketProducer();
|
||||
static final HandshakeProducer handshake12Producer =
|
||||
new T12NewSessionTicketProducer();
|
||||
|
||||
/**
|
||||
* The NewSessionTicketMessage handshake message.
|
||||
* The NewSessionTicketMessage handshake messages.
|
||||
*/
|
||||
static final class NewSessionTicketMessage extends HandshakeMessage {
|
||||
final int ticketLifetime;
|
||||
final int ticketAgeAdd;
|
||||
final byte[] ticketNonce;
|
||||
final byte[] ticket;
|
||||
final SSLExtensions extensions;
|
||||
abstract static class NewSessionTicketMessage extends HandshakeMessage {
|
||||
int ticketLifetime;
|
||||
byte[] ticket;
|
||||
|
||||
NewSessionTicketMessage(HandshakeContext context,
|
||||
NewSessionTicketMessage(HandshakeContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLHandshake handshakeType() {
|
||||
return NEW_SESSION_TICKET;
|
||||
}
|
||||
|
||||
// For TLS 1.3 only
|
||||
int getTicketAgeAdd() throws IOException {
|
||||
throw handshakeContext.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
||||
"TicketAgeAdd not part of RFC 5077.");
|
||||
}
|
||||
|
||||
// For TLS 1.3 only
|
||||
byte[] getTicketNonce() throws IOException {
|
||||
throw handshakeContext.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
||||
"TicketNonce not part of RFC 5077.");
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* NewSessionTicket for TLS 1.2 and below (RFC 5077)
|
||||
*/
|
||||
static final class T12NewSessionTicketMessage extends NewSessionTicketMessage {
|
||||
|
||||
T12NewSessionTicketMessage(HandshakeContext context,
|
||||
int ticketLifetime, byte[] ticket) {
|
||||
super(context);
|
||||
|
||||
this.ticketLifetime = ticketLifetime;
|
||||
this.ticket = ticket;
|
||||
}
|
||||
|
||||
T12NewSessionTicketMessage(HandshakeContext context,
|
||||
ByteBuffer m) throws IOException {
|
||||
|
||||
// RFC5077 struct {
|
||||
// uint32 ticket_lifetime;
|
||||
// opaque ticket<1..2^16-1>;
|
||||
// } NewSessionTicket;
|
||||
|
||||
super(context);
|
||||
if (m.remaining() < 14) {
|
||||
throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
||||
"Invalid NewSessionTicket message: no sufficient data");
|
||||
}
|
||||
|
||||
this.ticketLifetime = Record.getInt32(m);
|
||||
this.ticket = Record.getBytes16(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLHandshake handshakeType() {
|
||||
return NEW_SESSION_TICKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int messageLength() {
|
||||
return 4 + // ticketLifetime
|
||||
2 + ticket.length; // len of ticket + ticket
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(HandshakeOutStream hos) throws IOException {
|
||||
hos.putInt32(ticketLifetime);
|
||||
hos.putBytes16(ticket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
MessageFormat messageFormat = new MessageFormat(
|
||||
"\"NewSessionTicket\": '{'\n" +
|
||||
" \"ticket_lifetime\" : \"{0}\",\n" +
|
||||
" \"ticket\" : '{'\n" +
|
||||
"{1}\n" +
|
||||
" '}'" +
|
||||
"'}'",
|
||||
Locale.ENGLISH);
|
||||
|
||||
HexDumpEncoder hexEncoder = new HexDumpEncoder();
|
||||
Object[] messageFields = {
|
||||
ticketLifetime,
|
||||
Utilities.indent(hexEncoder.encode(ticket), " "),
|
||||
};
|
||||
return messageFormat.format(messageFields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NewSessionTicket defined by the TLS 1.3
|
||||
*/
|
||||
static final class T13NewSessionTicketMessage extends NewSessionTicketMessage {
|
||||
int ticketAgeAdd;
|
||||
byte[] ticketNonce;
|
||||
SSLExtensions extensions;
|
||||
|
||||
T13NewSessionTicketMessage(HandshakeContext context,
|
||||
int ticketLifetime, SecureRandom generator,
|
||||
byte[] ticketNonce, byte[] ticket) {
|
||||
super(context);
|
||||
|
@ -73,7 +172,7 @@ final class NewSessionTicket {
|
|||
this.extensions = new SSLExtensions(this);
|
||||
}
|
||||
|
||||
NewSessionTicketMessage(HandshakeContext context,
|
||||
T13NewSessionTicketMessage(HandshakeContext context,
|
||||
ByteBuffer m) throws IOException {
|
||||
super(context);
|
||||
|
||||
|
@ -84,6 +183,7 @@ final class NewSessionTicket {
|
|||
// opaque ticket<1..2^16-1>;
|
||||
// Extension extensions<0..2^16-2>;
|
||||
// } NewSessionTicket;
|
||||
|
||||
if (m.remaining() < 14) {
|
||||
throw context.conContext.fatal(Alert.ILLEGAL_PARAMETER,
|
||||
"Invalid NewSessionTicket message: no sufficient data");
|
||||
|
@ -111,24 +211,36 @@ final class NewSessionTicket {
|
|||
|
||||
SSLExtension[] supportedExtensions =
|
||||
context.sslConfig.getEnabledExtensions(
|
||||
SSLHandshake.NEW_SESSION_TICKET);
|
||||
NEW_SESSION_TICKET);
|
||||
this.extensions = new SSLExtensions(this, m, supportedExtensions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLHandshake handshakeType() {
|
||||
return SSLHandshake.NEW_SESSION_TICKET;
|
||||
return NEW_SESSION_TICKET;
|
||||
}
|
||||
|
||||
int getTicketAgeAdd() {
|
||||
return ticketAgeAdd;
|
||||
}
|
||||
|
||||
byte[] getTicketNonce() {
|
||||
return ticketNonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int messageLength() {
|
||||
|
||||
int extLen = extensions.length();
|
||||
if (extLen == 0) {
|
||||
extLen = 2; // empty extensions
|
||||
}
|
||||
|
||||
return 8 + ticketNonce.length + 1 +
|
||||
ticket.length + 2 + extLen;
|
||||
return 4 +// ticketLifetime
|
||||
4 + // ticketAgeAdd
|
||||
1 + ticketNonce.length + // len of nonce + nonce
|
||||
2 + ticket.length + // len of ticket + ticket
|
||||
extLen;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -153,18 +265,21 @@ final class NewSessionTicket {
|
|||
" \"ticket_lifetime\" : \"{0}\",\n" +
|
||||
" \"ticket_age_add\" : \"{1}\",\n" +
|
||||
" \"ticket_nonce\" : \"{2}\",\n" +
|
||||
" \"ticket\" : \"{3}\",\n" +
|
||||
" \"ticket\" : '{'\n" +
|
||||
"{3}\n" +
|
||||
" '}'" +
|
||||
" \"extensions\" : [\n" +
|
||||
"{4}\n" +
|
||||
" ]\n" +
|
||||
"'}'",
|
||||
Locale.ENGLISH);
|
||||
|
||||
HexDumpEncoder hexEncoder = new HexDumpEncoder();
|
||||
Object[] messageFields = {
|
||||
ticketLifetime,
|
||||
"<omitted>", //ticketAgeAdd should not be logged
|
||||
Utilities.toHexString(ticketNonce),
|
||||
Utilities.toHexString(ticket),
|
||||
Utilities.indent(hexEncoder.encode(ticket), " "),
|
||||
Utilities.indent(extensions.toString(), " ")
|
||||
};
|
||||
|
||||
|
@ -248,25 +363,46 @@ final class NewSessionTicket {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
NewSessionTicketMessage nstm = new NewSessionTicketMessage(shc,
|
||||
sessionTimeoutSeconds, shc.sslContext.getSecureRandom(),
|
||||
nonceArr, newId.getId());
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Produced NewSessionTicket handshake message", nstm);
|
||||
}
|
||||
|
||||
// create and cache the new session
|
||||
// The new session must be a child of the existing session so
|
||||
// they will be invalidated together, etc.
|
||||
NewSessionTicketMessage nstm;
|
||||
|
||||
SSLSessionImpl sessionCopy =
|
||||
new SSLSessionImpl(shc.handshakeSession, newId);
|
||||
shc.handshakeSession.addChild(sessionCopy);
|
||||
sessionCopy.setPreSharedKey(psk);
|
||||
sessionCopy.setPskIdentity(newId.getId());
|
||||
sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd);
|
||||
sessionCache.put(sessionCopy);
|
||||
|
||||
if (shc.statelessResumption) {
|
||||
try {
|
||||
nstm = new T13NewSessionTicketMessage(shc,
|
||||
sessionTimeoutSeconds, shc.sslContext.getSecureRandom(),
|
||||
nonceArr, new SessionTicketSpec().encrypt(shc, sessionCopy));
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Produced NewSessionTicket stateless " +
|
||||
"handshake message", nstm);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Error with NST ticket, abort NST
|
||||
shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
nstm = new T13NewSessionTicketMessage(shc, sessionTimeoutSeconds,
|
||||
shc.sslContext.getSecureRandom(), nonceArr,
|
||||
newId.getId());
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Produced NewSessionTicket handshake message",
|
||||
nstm);
|
||||
}
|
||||
|
||||
// create and cache the new session
|
||||
// The new session must be a child of the existing session so
|
||||
// they will be invalidated together, etc.
|
||||
shc.handshakeSession.addChild(sessionCopy);
|
||||
sessionCopy.setTicketAgeAdd(nstm.getTicketAgeAdd());
|
||||
sessionCache.put(sessionCopy);
|
||||
}
|
||||
// Output the handshake message.
|
||||
nstm.write(shc.handshakeOutput);
|
||||
shc.handshakeOutput.flush();
|
||||
|
@ -277,13 +413,13 @@ final class NewSessionTicket {
|
|||
}
|
||||
|
||||
/**
|
||||
* The "NewSessionTicket" handshake message producer.
|
||||
* The "NewSessionTicket" handshake message producer for RFC 5077
|
||||
*/
|
||||
private static final class NewSessionTicketProducer
|
||||
private static final class T12NewSessionTicketProducer
|
||||
implements HandshakeProducer {
|
||||
|
||||
// Prevent instantiation of this class.
|
||||
private NewSessionTicketProducer() {
|
||||
private T12NewSessionTicketProducer() {
|
||||
// blank
|
||||
}
|
||||
|
||||
|
@ -291,24 +427,65 @@ final class NewSessionTicket {
|
|||
public byte[] produce(ConnectionContext context,
|
||||
HandshakeMessage message) throws IOException {
|
||||
|
||||
// NSTM may be sent in response to handshake messages.
|
||||
// For example: key update
|
||||
ServerHandshakeContext shc = (ServerHandshakeContext)context;
|
||||
|
||||
throw new ProviderException(
|
||||
"NewSessionTicket handshake producer not implemented");
|
||||
// Is this session resumable?
|
||||
if (!shc.handshakeSession.isRejoinable()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get a new session ID
|
||||
SessionId newId = shc.handshakeSession.getSessionId();
|
||||
|
||||
SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
|
||||
shc.sslContext.engineGetServerSessionContext();
|
||||
int sessionTimeoutSeconds = sessionCache.getSessionTimeout();
|
||||
if (sessionTimeoutSeconds > MAX_TICKET_LIFETIME) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Session timeout is too long. No ticket sent.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
NewSessionTicketMessage nstm;
|
||||
|
||||
SSLSessionImpl sessionCopy =
|
||||
new SSLSessionImpl(shc.handshakeSession, newId);
|
||||
sessionCopy.setPskIdentity(newId.getId());
|
||||
|
||||
try {
|
||||
nstm = new T12NewSessionTicketMessage(shc, sessionTimeoutSeconds,
|
||||
new SessionTicketSpec().encrypt(shc, sessionCopy));
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Produced NewSessionTicket stateless handshake message", nstm);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Abort on error with NST ticket
|
||||
shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Output the handshake message.
|
||||
nstm.write(shc.handshakeOutput);
|
||||
shc.handshakeOutput.flush();
|
||||
|
||||
// The message has been delivered.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final
|
||||
class NewSessionTicketConsumer implements SSLConsumer {
|
||||
class T13NewSessionTicketConsumer implements SSLConsumer {
|
||||
// Prevent instantiation of this class.
|
||||
private NewSessionTicketConsumer() {
|
||||
private T13NewSessionTicketConsumer() {
|
||||
// blank
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ConnectionContext context,
|
||||
ByteBuffer message) throws IOException {
|
||||
ByteBuffer message) throws IOException {
|
||||
|
||||
// Note: Although the resumption master secret depends on the
|
||||
// client's second flight, servers which do not request client
|
||||
|
@ -317,13 +494,12 @@ final class NewSessionTicket {
|
|||
// upon sending its Finished rather than waiting for the client
|
||||
// Finished.
|
||||
//
|
||||
// The consuming happens in client side only. As the server
|
||||
// may send the NewSessionTicket before handshake complete, the
|
||||
// context may be a PostHandshakeContext or HandshakeContext
|
||||
// instance.
|
||||
// The consuming happens in client side only and is received after
|
||||
// the server's Finished message with PostHandshakeContext.
|
||||
|
||||
HandshakeContext hc = (HandshakeContext)context;
|
||||
NewSessionTicketMessage nstm =
|
||||
new NewSessionTicketMessage(hc, message);
|
||||
new T13NewSessionTicketMessage(hc, message);
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Consuming NewSessionTicket message", nstm);
|
||||
|
@ -352,37 +528,95 @@ final class NewSessionTicket {
|
|||
}
|
||||
|
||||
SSLSessionImpl sessionToSave = hc.conContext.conSession;
|
||||
|
||||
SecretKey resumptionMasterSecret =
|
||||
sessionToSave.getResumptionMasterSecret();
|
||||
if (resumptionMasterSecret == null) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Session has no resumption master secret. Ignoring ticket.");
|
||||
SecretKey psk = null;
|
||||
if (hc.negotiatedProtocol.useTLS13PlusSpec()) {
|
||||
SecretKey resumptionMasterSecret =
|
||||
sessionToSave.getResumptionMasterSecret();
|
||||
if (resumptionMasterSecret == null) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Session has no resumption master secret." +
|
||||
" Ignoring ticket.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// derive the PSK
|
||||
SecretKey psk = derivePreSharedKey(
|
||||
sessionToSave.getSuite().hashAlg, resumptionMasterSecret,
|
||||
nstm.ticketNonce);
|
||||
// derive the PSK
|
||||
psk = derivePreSharedKey(
|
||||
sessionToSave.getSuite().hashAlg,
|
||||
resumptionMasterSecret, nstm.getTicketNonce());
|
||||
}
|
||||
|
||||
// create and cache the new session
|
||||
// The new session must be a child of the existing session so
|
||||
// they will be invalidated together, etc.
|
||||
SessionId newId =
|
||||
new SessionId(true, hc.sslContext.getSecureRandom());
|
||||
new SessionId(true, hc.sslContext.getSecureRandom());
|
||||
SSLSessionImpl sessionCopy = new SSLSessionImpl(sessionToSave,
|
||||
newId);
|
||||
sessionToSave.addChild(sessionCopy);
|
||||
sessionCopy.setPreSharedKey(psk);
|
||||
sessionCopy.setTicketAgeAdd(nstm.ticketAgeAdd);
|
||||
sessionCopy.setTicketAgeAdd(nstm.getTicketAgeAdd());
|
||||
sessionCopy.setPskIdentity(nstm.ticket);
|
||||
sessionCache.put(sessionCopy);
|
||||
|
||||
// clean handshake context
|
||||
hc.conContext.finishPostHandshake();
|
||||
if (hc.negotiatedProtocol.useTLS13PlusSpec()) {
|
||||
hc.conContext.finishPostHandshake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final
|
||||
class T12NewSessionTicketConsumer implements SSLConsumer {
|
||||
// Prevent instantiation of this class.
|
||||
private T12NewSessionTicketConsumer() {
|
||||
// blank
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ConnectionContext context,
|
||||
ByteBuffer message) throws IOException {
|
||||
|
||||
HandshakeContext hc = (HandshakeContext)context;
|
||||
hc.handshakeConsumers.remove(NEW_SESSION_TICKET.id);
|
||||
|
||||
NewSessionTicketMessage nstm = new T12NewSessionTicketMessage(hc,
|
||||
message);
|
||||
if (nstm.ticket.length == 0) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("NewSessionTicket ticket was empty");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// discard tickets with timeout 0
|
||||
if (nstm.ticketLifetime <= 0 ||
|
||||
nstm.ticketLifetime > MAX_TICKET_LIFETIME) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Discarding NewSessionTicket with lifetime "
|
||||
+ nstm.ticketLifetime, nstm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
|
||||
hc.sslContext.engineGetClientSessionContext();
|
||||
|
||||
if (sessionCache.getSessionTimeout() > MAX_TICKET_LIFETIME) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Session cache lifetime is too long. Discarding ticket.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
hc.handshakeSession.setPskIdentity(nstm.ticket);
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Consuming NewSessionTicket\n" +
|
||||
nstm.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import java.util.List;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Collection;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
|
@ -42,6 +41,9 @@ import sun.security.ssl.ClientHello.ClientHelloMessage;
|
|||
import sun.security.ssl.SSLExtension.ExtensionConsumer;
|
||||
import sun.security.ssl.SSLExtension.SSLExtensionSpec;
|
||||
import sun.security.ssl.SSLHandshake.HandshakeMessage;
|
||||
import sun.security.ssl.SessionTicketExtension.SessionTicketSpec;
|
||||
import sun.security.util.HexDumpEncoder;
|
||||
|
||||
import static sun.security.ssl.SSLExtension.*;
|
||||
|
||||
/**
|
||||
|
@ -88,7 +90,7 @@ final class PreSharedKeyExtension {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{" + Utilities.toHexString(identity) + "," +
|
||||
return "{" + Utilities.toHexString(identity) + ", " +
|
||||
obfuscatedAge + "}";
|
||||
}
|
||||
}
|
||||
|
@ -208,8 +210,10 @@ final class PreSharedKeyExtension {
|
|||
public String toString() {
|
||||
MessageFormat messageFormat = new MessageFormat(
|
||||
"\"PreSharedKey\": '{'\n" +
|
||||
" \"identities\" : \"{0}\",\n" +
|
||||
" \"binders\" : \"{1}\",\n" +
|
||||
" \"identities\": '{'\n" +
|
||||
"{0}\n" +
|
||||
" '}'" +
|
||||
" \"binders\": \"{1}\",\n" +
|
||||
"'}'",
|
||||
Locale.ENGLISH);
|
||||
|
||||
|
@ -222,9 +226,13 @@ final class PreSharedKeyExtension {
|
|||
}
|
||||
|
||||
String identitiesString() {
|
||||
HexDumpEncoder hexEncoder = new HexDumpEncoder();
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (PskIdentity curId : identities) {
|
||||
result.append(curId.toString() + "\n");
|
||||
result.append(" {\n"+ Utilities.indent(
|
||||
hexEncoder.encode(curId.identity), " ") +
|
||||
"\n }\n");
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
|
@ -278,7 +286,7 @@ final class PreSharedKeyExtension {
|
|||
this.selectedIdentity = Record.getInt16(m);
|
||||
}
|
||||
|
||||
byte[] getEncoded() throws IOException {
|
||||
byte[] getEncoded() {
|
||||
return new byte[] {
|
||||
(byte)((selectedIdentity >> 8) & 0xFF),
|
||||
(byte)(selectedIdentity & 0xFF)
|
||||
|
@ -368,8 +376,36 @@ final class PreSharedKeyExtension {
|
|||
SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
|
||||
shc.sslContext.engineGetServerSessionContext();
|
||||
int idIndex = 0;
|
||||
SSLSessionImpl s = null;
|
||||
|
||||
for (PskIdentity requestedId : pskSpec.identities) {
|
||||
SSLSessionImpl s = sessionCache.get(requestedId.identity);
|
||||
// If we are keeping state, see if the identity is in the cache
|
||||
if (requestedId.identity.length == SessionId.MAX_LENGTH) {
|
||||
s = sessionCache.get(requestedId.identity);
|
||||
}
|
||||
// See if the identity is a stateless ticket
|
||||
if (s == null &&
|
||||
requestedId.identity.length > SessionId.MAX_LENGTH &&
|
||||
sessionCache.statelessEnabled()) {
|
||||
ByteBuffer b =
|
||||
new SessionTicketSpec(requestedId.identity).
|
||||
decrypt(shc);
|
||||
if (b != null) {
|
||||
try {
|
||||
s = new SSLSessionImpl(shc, b);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
s = null;
|
||||
}
|
||||
}
|
||||
if (b == null || s == null) {
|
||||
if (SSLLogger.isOn &&
|
||||
SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine(
|
||||
"Stateless session ticket invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s != null && canRejoin(clientHello, shc, s)) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Resuming session: ", s);
|
||||
|
@ -391,7 +427,6 @@ final class PreSharedKeyExtension {
|
|||
shc.resumingSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
// update the context
|
||||
shc.handshakeExtensions.put(
|
||||
SSLExtension.CH_PRE_SHARED_KEY, pskSpec);
|
||||
|
@ -708,7 +743,8 @@ final class PreSharedKeyExtension {
|
|||
int hashLength, List<PskIdentity> identities) {
|
||||
List<byte[]> binders = new ArrayList<>();
|
||||
byte[] binderProto = new byte[hashLength];
|
||||
for (PskIdentity curId : identities) {
|
||||
int i = identities.size();
|
||||
while (i-- > 0) {
|
||||
binders.add(binderProto);
|
||||
}
|
||||
|
||||
|
|
|
@ -71,11 +71,13 @@ public abstract class SSLContextImpl extends SSLContextSpi {
|
|||
private volatile StatusResponseManager statusResponseManager;
|
||||
|
||||
private final ReentrantLock contextLock = new ReentrantLock();
|
||||
final HashMap<Integer, SessionTicketExtension.StatelessKey> keyHashMap = new HashMap<>();
|
||||
|
||||
|
||||
SSLContextImpl() {
|
||||
ephemeralKeyManager = new EphemeralKeyManager();
|
||||
clientCache = new SSLSessionContextImpl();
|
||||
serverCache = new SSLSessionContextImpl();
|
||||
clientCache = new SSLSessionContextImpl(false);
|
||||
serverCache = new SSLSessionContextImpl(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -309,8 +309,28 @@ enum SSLExtension implements SSLStringizer {
|
|||
// extensions defined in RFC 7924
|
||||
CACHED_INFO (0x0019, "cached_info"),
|
||||
|
||||
// extensions defined in RFC 4507/5077
|
||||
SESSION_TICKET (0x0023, "session_ticket"),
|
||||
// extensions defined in RFC 5077
|
||||
CH_SESSION_TICKET (0x0023, "session_ticket",
|
||||
SSLHandshake.CLIENT_HELLO,
|
||||
ProtocolVersion.PROTOCOLS_10_12,
|
||||
SessionTicketExtension.chNetworkProducer,
|
||||
SessionTicketExtension.chOnLoadConsumer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
SessionTicketExtension.steStringizer),
|
||||
//null),
|
||||
|
||||
SH_SESSION_TICKET (0x0023, "session_ticket",
|
||||
SSLHandshake.SERVER_HELLO,
|
||||
ProtocolVersion.PROTOCOLS_10_12,
|
||||
SessionTicketExtension.shNetworkProducer,
|
||||
SessionTicketExtension.shOnLoadConsumer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
SessionTicketExtension.steStringizer),
|
||||
//null),
|
||||
|
||||
// extensions defined in TLS 1.3
|
||||
CH_EARLY_DATA (0x002A, "early_data"),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2006, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 2006, 2019, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
|
@ -115,15 +115,19 @@ enum SSLHandshake implements SSLConsumer, HandshakeProducer {
|
|||
NEW_SESSION_TICKET ((byte)0x04, "new_session_ticket",
|
||||
(Map.Entry<SSLConsumer, ProtocolVersion[]>[])(new Map.Entry[] {
|
||||
new SimpleImmutableEntry<SSLConsumer, ProtocolVersion[]>(
|
||||
NewSessionTicket.handshakeConsumer,
|
||||
ProtocolVersion.PROTOCOLS_OF_13
|
||||
)
|
||||
NewSessionTicket.handshake12Consumer,
|
||||
ProtocolVersion.PROTOCOLS_TO_12
|
||||
),
|
||||
new SimpleImmutableEntry<SSLConsumer, ProtocolVersion[]>(
|
||||
NewSessionTicket.handshakeConsumer,
|
||||
ProtocolVersion.PROTOCOLS_OF_13
|
||||
)
|
||||
}),
|
||||
(Map.Entry<HandshakeProducer, ProtocolVersion[]>[])(new Map.Entry[] {
|
||||
new SimpleImmutableEntry<HandshakeProducer, ProtocolVersion[]>(
|
||||
NewSessionTicket.handshakeProducer,
|
||||
ProtocolVersion.PROTOCOLS_OF_13
|
||||
)
|
||||
NewSessionTicket.handshake12Producer,
|
||||
ProtocolVersion.PROTOCOLS_TO_12
|
||||
)
|
||||
})),
|
||||
END_OF_EARLY_DATA ((byte)0x05, "end_of_early_data"),
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 1999, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 1999, 2019, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
|
@ -33,11 +33,34 @@ import javax.net.ssl.SSLSession;
|
|||
import javax.net.ssl.SSLSessionContext;
|
||||
|
||||
import sun.security.action.GetIntegerAction;
|
||||
import sun.security.action.GetPropertyAction;
|
||||
import sun.security.util.Cache;
|
||||
|
||||
|
||||
/**
|
||||
* @systemProperty jdk.tls.server.enableSessionTicketExtension} determines if the
|
||||
* server will provide stateless session tickets, if the client supports it,
|
||||
* as described in RFC 5077 and RFC 8446. a stateless session ticket
|
||||
* contains the encrypted server's state which saves server resources.
|
||||
*
|
||||
* {@systemProperty jdk.tls.client.enableSessionTicketExtension} determines if the
|
||||
* client will send an extension in the ClientHello in the pre-TLS 1.3.
|
||||
* This extension allows the client to accept the server's session state for
|
||||
* Server Side stateless resumption (RFC 5077). Setting the property to
|
||||
* "true" turns this on, by default it is false. For TLS 1.3, the system
|
||||
* property is not needed as this support is part of the spec.
|
||||
*
|
||||
* {@systemProperty jdk.tls.server.sessionTicketTimeout} determines how long
|
||||
* a session in the server cache or the stateless resumption tickets are
|
||||
* available for use. The value set by the property can be modified by
|
||||
* {@code SSLSessionContext.setSessionTimeout()} during runtime.
|
||||
*
|
||||
*/
|
||||
|
||||
final class SSLSessionContextImpl implements SSLSessionContext {
|
||||
private final static int DEFAULT_MAX_CACHE_SIZE = 20480;
|
||||
// Default lifetime of a session. 24 hours
|
||||
final static int DEFAULT_SESSION_TIMEOUT = 86400;
|
||||
|
||||
private final Cache<SessionId, SSLSessionImpl> sessionCache;
|
||||
// session cache, session id as key
|
||||
|
@ -46,16 +69,24 @@ final class SSLSessionContextImpl implements SSLSessionContext {
|
|||
private int cacheLimit; // the max cache size
|
||||
private int timeout; // timeout in seconds
|
||||
|
||||
// Does this context support stateless session (RFC 5077)
|
||||
private boolean statelessSession = true;
|
||||
|
||||
// package private
|
||||
SSLSessionContextImpl() {
|
||||
cacheLimit = getDefaultCacheLimit(); // default cache size
|
||||
timeout = 86400; // default, 24 hours
|
||||
SSLSessionContextImpl(boolean server) {
|
||||
timeout = DEFAULT_SESSION_TIMEOUT;
|
||||
cacheLimit = getDefaults(server); // default cache size
|
||||
|
||||
// use soft reference
|
||||
sessionCache = Cache.newSoftMemoryCache(cacheLimit, timeout);
|
||||
sessionHostPortCache = Cache.newSoftMemoryCache(cacheLimit, timeout);
|
||||
}
|
||||
|
||||
// Stateless sessions when available, but there is a cache
|
||||
boolean statelessEnabled() {
|
||||
return statelessSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the <code>SSLSession</code> bound to the specified session id.
|
||||
*/
|
||||
|
@ -163,8 +194,7 @@ final class SSLSessionContextImpl implements SSLSessionContext {
|
|||
}
|
||||
|
||||
private static String getKey(String hostname, int port) {
|
||||
return (hostname + ":" +
|
||||
String.valueOf(port)).toLowerCase(Locale.ENGLISH);
|
||||
return (hostname + ":" + port).toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
// cache a SSLSession
|
||||
|
@ -197,8 +227,51 @@ final class SSLSessionContextImpl implements SSLSessionContext {
|
|||
}
|
||||
}
|
||||
|
||||
private static int getDefaultCacheLimit() {
|
||||
private int getDefaults(boolean server) {
|
||||
try {
|
||||
String st;
|
||||
|
||||
// Property for Session Cache state
|
||||
if (server) {
|
||||
st = GetPropertyAction.privilegedGetProperty(
|
||||
"jdk.tls.server.enableSessionTicketExtension", "true");
|
||||
} else {
|
||||
st = GetPropertyAction.privilegedGetProperty(
|
||||
"jdk.tls.client.enableSessionTicketExtension", "true");
|
||||
}
|
||||
if (st.compareToIgnoreCase("false") == 0) {
|
||||
statelessSession = false;
|
||||
}
|
||||
|
||||
// Property for Session Ticket Timeout. The value can be changed
|
||||
// by SSLSessionContext.setSessionTimeout(int)
|
||||
String s = GetPropertyAction.privilegedGetProperty(
|
||||
"jdk.tls.server.sessionTicketTimeout");
|
||||
if (s != null) {
|
||||
try {
|
||||
int t = Integer.parseInt(s);
|
||||
if (t < 0 ||
|
||||
t > NewSessionTicket.MAX_TICKET_LIFETIME) {
|
||||
timeout = DEFAULT_SESSION_TIMEOUT;
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
|
||||
SSLLogger.warning("Invalid timeout given " +
|
||||
"jdk.tls.server.sessionTicketTimeout: " + t +
|
||||
". Set to default value " + timeout);
|
||||
}
|
||||
} else {
|
||||
timeout = t;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
setSessionTimeout(DEFAULT_SESSION_TIMEOUT);
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
|
||||
SSLLogger.warning("Invalid timeout for " +
|
||||
"jdk.tls.server.sessionTicketTimeout: " + s +
|
||||
". Set to default value " + timeout);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int defaultCacheLimit = GetIntegerAction.privilegedGetProperty(
|
||||
"javax.net.ssl.sessionCacheSize", DEFAULT_MAX_CACHE_SIZE);
|
||||
|
||||
|
|
|
@ -24,8 +24,12 @@
|
|||
*/
|
||||
package sun.security.ssl;
|
||||
|
||||
import sun.security.x509.X509CertImpl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.Principal;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
|
@ -40,8 +44,11 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.net.ssl.ExtendedSSLSession;
|
||||
import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLPermission;
|
||||
import javax.net.ssl.SSLSessionBindingEvent;
|
||||
|
@ -251,6 +258,371 @@ final class SSLSessionImpl extends ExtendedSSLSession {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* < 2 bytes > protocolVersion
|
||||
* < 2 bytes > cipherSuite
|
||||
* < 2 bytes > localSupportedSignAlgs entries
|
||||
* < 2 bytes per entries > localSupportedSignAlgs
|
||||
* < 2 bytes > preSharedKey length
|
||||
* < length in bytes > preSharedKey
|
||||
* < 1 byte > pskIdentity length
|
||||
* < length in bytes > pskIdentity
|
||||
* < 1 byte > masterSecret length
|
||||
* < 1 byte > masterSecret algorithm length
|
||||
* < length in bytes > masterSecret algorithm
|
||||
* < 2 bytes > masterSecretKey length
|
||||
* < length in bytes> masterSecretKey
|
||||
* < 1 byte > useExtendedMasterSecret
|
||||
* < 1 byte > identificationProtocol length
|
||||
* < length in bytes > identificationProtocol
|
||||
* < 1 byte > serverNameIndication length
|
||||
* < length in bytes > serverNameIndication
|
||||
* < 1 byte > Number of requestedServerNames entries
|
||||
* < 1 byte > ServerName length
|
||||
* < length in bytes > ServerName
|
||||
* < 4 bytes > creationTime
|
||||
* < 1 byte > Length of peer host
|
||||
* < length in bytes > peer host
|
||||
* < 2 bytes> peer port
|
||||
* < 1 byte > Number of peerCerts entries
|
||||
* < 4 byte > peerCert length
|
||||
* < length in bytes > peerCert
|
||||
* < 1 byte > localCerts type (Cert, PSK, Anonymous)
|
||||
* Certificate
|
||||
* < 1 byte > Number of Certificate entries
|
||||
* < 4 byte> Certificate length
|
||||
* < length in bytes> Certificate
|
||||
* PSK
|
||||
* < 1 byte > Number of PSK entries
|
||||
* < 1 bytes > PSK algorithm length
|
||||
* < length in bytes > PSK algorithm string
|
||||
* < 4 bytes > PSK key length
|
||||
* < length in bytes> PSK key
|
||||
* < 4 bytes > PSK identity length
|
||||
* < length in bytes> PSK identity
|
||||
* Anonymous
|
||||
* < 1 byte >
|
||||
*/
|
||||
|
||||
SSLSessionImpl(HandshakeContext hc, ByteBuffer buf) throws IOException {
|
||||
int i = 0;
|
||||
byte[] b;
|
||||
|
||||
this.localSupportedSignAlgs = new ArrayList<>();
|
||||
|
||||
boundValues = null;
|
||||
|
||||
this.protocolVersion = ProtocolVersion.valueOf(Short.toUnsignedInt(buf.getShort()));
|
||||
|
||||
if (protocolVersion.useTLS13PlusSpec()) {
|
||||
this.sessionId = new SessionId(false, null);
|
||||
} else {
|
||||
// The CH session id may reset this if it's provided
|
||||
this.sessionId = new SessionId(true,
|
||||
hc.sslContext.getSecureRandom());
|
||||
}
|
||||
|
||||
this.cipherSuite = CipherSuite.valueOf(Short.toUnsignedInt(buf.getShort()));
|
||||
|
||||
// Local Supported signature algorithms
|
||||
i = Short.toUnsignedInt(buf.getShort());
|
||||
while (i-- > 0) {
|
||||
this.localSupportedSignAlgs.add(SignatureScheme.valueOf(
|
||||
Short.toUnsignedInt(buf.getShort())));
|
||||
}
|
||||
|
||||
// PSK
|
||||
i = Short.toUnsignedInt(buf.getShort());
|
||||
if (i > 0) {
|
||||
b = new byte[i];
|
||||
// Get algorithm string
|
||||
buf.get(b, 0, i);
|
||||
// Encoded length
|
||||
i = Short.toUnsignedInt(buf.getShort());
|
||||
// Encoded SecretKey
|
||||
b = new byte[i];
|
||||
buf.get(b);
|
||||
this.preSharedKey = new SecretKeySpec(b, "TlsMasterSecret");
|
||||
} else {
|
||||
this.preSharedKey = null;
|
||||
}
|
||||
|
||||
// PSK identity
|
||||
i = buf.get();
|
||||
if (i > 0) {
|
||||
b = new byte[i];
|
||||
buf.get(b);
|
||||
this.pskIdentity = b;
|
||||
} else {
|
||||
this.pskIdentity = null;
|
||||
}
|
||||
|
||||
// Master secret length of secret key algorithm (one byte)
|
||||
i = buf.get();
|
||||
if (i > 0) {
|
||||
b = new byte[i];
|
||||
// Get algorithm string
|
||||
buf.get(b, 0, i);
|
||||
// Encoded length
|
||||
i = Short.toUnsignedInt(buf.getShort());
|
||||
// Encoded SecretKey
|
||||
b = new byte[i];
|
||||
buf.get(b);
|
||||
this.masterSecret = new SecretKeySpec(b, "TlsMasterSecret");
|
||||
} else {
|
||||
this.masterSecret = null;
|
||||
}
|
||||
// Use extended master secret
|
||||
this.useExtendedMasterSecret = (buf.get() != 0);
|
||||
|
||||
// Identification Protocol
|
||||
i = buf.get();
|
||||
if (i == 0) {
|
||||
identificationProtocol = null;
|
||||
} else {
|
||||
b = new byte[i];
|
||||
identificationProtocol =
|
||||
buf.get(b, 0, i).asCharBuffer().toString();
|
||||
}
|
||||
|
||||
// SNI
|
||||
i = buf.get(); // length
|
||||
if (i == 0) {
|
||||
serverNameIndication = null;
|
||||
} else {
|
||||
b = new byte[i];
|
||||
buf.get(b, 0, b.length);
|
||||
serverNameIndication = new SNIHostName(b);
|
||||
}
|
||||
|
||||
// List of SNIServerName
|
||||
int len = Short.toUnsignedInt(buf.getShort());
|
||||
if (len == 0) {
|
||||
this.requestedServerNames = Collections.<SNIServerName>emptyList();
|
||||
} else {
|
||||
requestedServerNames = new ArrayList<>();
|
||||
while (len > 0) {
|
||||
int l = buf.get();
|
||||
b = new byte[l];
|
||||
buf.get(b, 0, l);
|
||||
requestedServerNames.add(new SNIHostName(new String(b)));
|
||||
len--;
|
||||
}
|
||||
}
|
||||
|
||||
// Get creation time
|
||||
this.creationTime = buf.getLong();
|
||||
|
||||
// Get Peer host & port
|
||||
i = Byte.toUnsignedInt(buf.get());
|
||||
if (i == 0) {
|
||||
this.host = new String();
|
||||
} else {
|
||||
b = new byte[i];
|
||||
this.host = buf.get(b).toString();
|
||||
}
|
||||
this.port = Short.toUnsignedInt(buf.getShort());
|
||||
|
||||
// Peer certs
|
||||
i = buf.get();
|
||||
if (i == 0) {
|
||||
this.peerCerts = null;
|
||||
} else {
|
||||
this.peerCerts = new X509Certificate[i];
|
||||
int j = 0;
|
||||
while (i > j) {
|
||||
b = new byte[buf.getInt()];
|
||||
buf.get(b);
|
||||
try {
|
||||
this.peerCerts[j] = new X509CertImpl(b);
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
// Get local certs of PSK
|
||||
switch (buf.get()) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
// number of certs
|
||||
len = buf.get();
|
||||
this.localCerts = new X509Certificate[len];
|
||||
i = 0;
|
||||
while (len > i) {
|
||||
b = new byte[buf.getInt()];
|
||||
buf.get(b);
|
||||
try {
|
||||
this.localCerts[i] = new X509CertImpl(b);
|
||||
} catch (Exception e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// pre-shared key
|
||||
// Length of pre-shared key algorithm (one byte)
|
||||
i = buf.get();
|
||||
b = new byte[i];
|
||||
String alg = buf.get(b, 0, i).asCharBuffer().toString();
|
||||
// Get length of encoding
|
||||
i = Short.toUnsignedInt(buf.getShort());
|
||||
// Get encoding
|
||||
b = new byte[i];
|
||||
buf.get(b);
|
||||
this.preSharedKey = new SecretKeySpec(b, alg);
|
||||
// Get identity len
|
||||
this.pskIdentity = new byte[buf.get()];
|
||||
buf.get(pskIdentity);
|
||||
break;
|
||||
default:
|
||||
throw new SSLException("Failed local certs of session.");
|
||||
}
|
||||
|
||||
context = (SSLSessionContextImpl)
|
||||
hc.sslContext.engineGetServerSessionContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write out a SSLSessionImpl in a byte array for a stateless session ticket
|
||||
*/
|
||||
byte[] write() throws Exception {
|
||||
byte[] b;
|
||||
HandshakeOutStream hos = new HandshakeOutStream(null);
|
||||
|
||||
hos.putInt16(protocolVersion.id);
|
||||
hos.putInt16(cipherSuite.id);
|
||||
|
||||
// Local Supported signature algorithms
|
||||
int l = localSupportedSignAlgs.size();
|
||||
hos.putInt16(l);
|
||||
SignatureScheme[] sig = new SignatureScheme[l];
|
||||
localSupportedSignAlgs.toArray(sig);
|
||||
for (SignatureScheme s : sig) {
|
||||
hos.putInt16(s.id);
|
||||
}
|
||||
|
||||
// PSK
|
||||
if (preSharedKey == null ||
|
||||
preSharedKey.getAlgorithm() == null) {
|
||||
hos.putInt16(0);
|
||||
} else {
|
||||
hos.putInt16(preSharedKey.getAlgorithm().length());
|
||||
if (preSharedKey.getAlgorithm().length() != 0) {
|
||||
hos.write(preSharedKey.getAlgorithm().getBytes());
|
||||
}
|
||||
b = preSharedKey.getEncoded();
|
||||
hos.putInt16(b.length);
|
||||
hos.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
// PSK Identity
|
||||
if (pskIdentity == null) {
|
||||
hos.putInt8(0);
|
||||
} else {
|
||||
hos.putInt8(pskIdentity.length);
|
||||
hos.write(pskIdentity, 0, pskIdentity.length);
|
||||
}
|
||||
|
||||
// Master Secret
|
||||
if (getMasterSecret() == null ||
|
||||
getMasterSecret().getAlgorithm() == null) {
|
||||
hos.putInt8(0);
|
||||
} else {
|
||||
hos.putInt8(getMasterSecret().getAlgorithm().length());
|
||||
if (getMasterSecret().getAlgorithm().length() != 0) {
|
||||
hos.write(getMasterSecret().getAlgorithm().getBytes());
|
||||
}
|
||||
b = getMasterSecret().getEncoded();
|
||||
hos.putInt16(b.length);
|
||||
hos.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
hos.putInt8(useExtendedMasterSecret ? 1 : 0);
|
||||
|
||||
// Identification Protocol
|
||||
if (identificationProtocol == null) {
|
||||
hos.putInt8(0);
|
||||
} else {
|
||||
hos.putInt8(identificationProtocol.length());
|
||||
hos.write(identificationProtocol.getBytes(), 0,
|
||||
identificationProtocol.length());
|
||||
}
|
||||
|
||||
// SNI
|
||||
if (serverNameIndication == null) {
|
||||
hos.putInt8(0);
|
||||
} else {
|
||||
b = serverNameIndication.getEncoded();
|
||||
hos.putInt8(b.length);
|
||||
hos.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
// List of SNIServerName
|
||||
hos.putInt16(requestedServerNames.size());
|
||||
if (requestedServerNames.size() > 0) {
|
||||
for (SNIServerName host: requestedServerNames) {
|
||||
b = host.getEncoded();
|
||||
hos.putInt8(b.length);
|
||||
hos.write(b, 0, b.length);
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
|
||||
hos.writeBytes(buffer.putLong(creationTime).array());
|
||||
|
||||
// peer Host & Port
|
||||
if (host == null || host.length() == 0) {
|
||||
hos.putInt8(0);
|
||||
} else {
|
||||
hos.putInt8(host.length());
|
||||
hos.writeBytes(host.getBytes());
|
||||
}
|
||||
hos.putInt16(port);
|
||||
|
||||
// Peer cert
|
||||
if (peerCerts == null || peerCerts.length == 0) {
|
||||
hos.putInt8(0);
|
||||
} else {
|
||||
hos.putInt8(peerCerts.length);
|
||||
for (X509Certificate c : peerCerts) {
|
||||
b = c.getEncoded();
|
||||
hos.putInt32(b.length);
|
||||
hos.writeBytes(b);
|
||||
}
|
||||
}
|
||||
|
||||
// Client identity
|
||||
if (localCerts != null && localCerts.length > 0) {
|
||||
// certificate based
|
||||
hos.putInt8(1);
|
||||
hos.putInt8(localCerts.length);
|
||||
for (X509Certificate c : localCerts) {
|
||||
b = c.getEncoded();
|
||||
hos.putInt32(b.length);
|
||||
hos.writeBytes(b);
|
||||
}
|
||||
} else if (preSharedKey != null) {
|
||||
// pre-shared key
|
||||
hos.putInt8(2);
|
||||
hos.putInt8(preSharedKey.getAlgorithm().length());
|
||||
hos.write(preSharedKey.getAlgorithm().getBytes());
|
||||
b = preSharedKey.getEncoded();
|
||||
hos.putInt32(b.length);
|
||||
hos.writeBytes(b);
|
||||
hos.putInt32(pskIdentity.length);
|
||||
hos.writeBytes(pskIdentity);
|
||||
} else {
|
||||
// anonymous
|
||||
hos.putInt8(0);
|
||||
}
|
||||
|
||||
return hos.toByteArray();
|
||||
}
|
||||
|
||||
void setMasterSecret(SecretKey secret) {
|
||||
masterSecret = secret;
|
||||
}
|
||||
|
@ -333,6 +705,10 @@ final class SSLSessionImpl extends ExtendedSSLSession {
|
|||
}
|
||||
}
|
||||
|
||||
byte[] getPskIdentity() {
|
||||
return pskIdentity;
|
||||
}
|
||||
|
||||
void setPeerCertificates(X509Certificate[] peer) {
|
||||
if (peerCerts == null) {
|
||||
peerCerts = peer;
|
||||
|
@ -400,8 +776,12 @@ final class SSLSessionImpl extends ExtendedSSLSession {
|
|||
* maximum lifetime in any case.
|
||||
*/
|
||||
boolean isRejoinable() {
|
||||
// TLS 1.3 can have no session id
|
||||
if (protocolVersion.useTLS13PlusSpec()) {
|
||||
return (!invalidated && isLocalAuthenticationValid());
|
||||
}
|
||||
return sessionId != null && sessionId.length() != 0 &&
|
||||
!invalidated && isLocalAuthenticationValid();
|
||||
!invalidated && isLocalAuthenticationValid();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -47,6 +47,8 @@ import sun.security.ssl.SSLCipher.SSLWriteCipher;
|
|||
import sun.security.ssl.SSLHandshake.HandshakeMessage;
|
||||
import sun.security.ssl.SupportedVersionsExtension.SHSupportedVersionsSpec;
|
||||
|
||||
import static sun.security.ssl.SSLExtension.SH_SESSION_TICKET;
|
||||
|
||||
/**
|
||||
* Pack of the ServerHello/HelloRetryRequest handshake message.
|
||||
*/
|
||||
|
@ -337,6 +339,15 @@ final class ServerHello {
|
|||
shc.handshakeProducers.put(SSLHandshake.SERVER_HELLO_DONE.id,
|
||||
SSLHandshake.SERVER_HELLO_DONE);
|
||||
} else {
|
||||
// stateless and use the client session id (RFC 5077 3.4)
|
||||
if (shc.statelessResumption) {
|
||||
shc.resumingSession = new SSLSessionImpl(shc.resumingSession,
|
||||
(clientHello.sessionId.length() == 0) ?
|
||||
new SessionId(true,
|
||||
shc.sslContext.getSecureRandom()) :
|
||||
new SessionId(clientHello.sessionId.getId())
|
||||
);
|
||||
}
|
||||
shc.handshakeSession = shc.resumingSession;
|
||||
shc.negotiatedProtocol =
|
||||
shc.resumingSession.getProtocolVersion();
|
||||
|
@ -491,6 +502,9 @@ final class ServerHello {
|
|||
ServerHandshakeContext shc = (ServerHandshakeContext)context;
|
||||
ClientHelloMessage clientHello = (ClientHelloMessage)message;
|
||||
|
||||
SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
|
||||
shc.sslContext.engineGetServerSessionContext();
|
||||
|
||||
// If client hasn't specified a session we can resume, start a
|
||||
// new one and choose its cipher suite and compression options,
|
||||
// unless new session creation is disabled for this connection!
|
||||
|
@ -546,8 +560,6 @@ final class ServerHello {
|
|||
shc.resumingSession.consumePreSharedKey());
|
||||
|
||||
// The session can't be resumed again---remove it from cache
|
||||
SSLSessionContextImpl sessionCache = (SSLSessionContextImpl)
|
||||
shc.sslContext.engineGetServerSessionContext();
|
||||
sessionCache.remove(shc.resumingSession.getSessionId());
|
||||
}
|
||||
|
||||
|
@ -679,6 +691,11 @@ final class ServerHello {
|
|||
// Update the context for master key derivation.
|
||||
shc.handshakeKeyDerivation = kd;
|
||||
|
||||
// Check if the server supports stateless resumption
|
||||
if (sessionCache.statelessEnabled()) {
|
||||
shc.statelessResumption = true;
|
||||
}
|
||||
|
||||
// The handshake message has been delivered.
|
||||
return null;
|
||||
}
|
||||
|
@ -1098,9 +1115,23 @@ final class ServerHello {
|
|||
throw chc.conContext.fatal(Alert.PROTOCOL_VERSION,
|
||||
"New session creation is disabled");
|
||||
}
|
||||
chc.handshakeSession = new SSLSessionImpl(chc,
|
||||
chc.negotiatedCipherSuite,
|
||||
serverHello.sessionId);
|
||||
|
||||
if (serverHello.sessionId.length() == 0 &&
|
||||
chc.statelessResumption) {
|
||||
SessionId newId = new SessionId(true,
|
||||
chc.sslContext.getSecureRandom());
|
||||
chc.handshakeSession = new SSLSessionImpl(chc,
|
||||
chc.negotiatedCipherSuite, newId);
|
||||
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Locally assigned Session Id: " +
|
||||
newId.toString());
|
||||
}
|
||||
} else {
|
||||
chc.handshakeSession = new SSLSessionImpl(chc,
|
||||
chc.negotiatedCipherSuite,
|
||||
serverHello.sessionId);
|
||||
}
|
||||
chc.handshakeSession.setMaximumPacketSize(
|
||||
chc.sslConfig.maximumPacketSize);
|
||||
}
|
||||
|
@ -1127,6 +1158,11 @@ final class ServerHello {
|
|||
chc.conContext.consumers.putIfAbsent(
|
||||
ContentType.CHANGE_CIPHER_SPEC.id,
|
||||
ChangeCipherSpec.t10Consumer);
|
||||
if (chc.statelessResumption) {
|
||||
chc.handshakeConsumers.putIfAbsent(
|
||||
SSLHandshake.NEW_SESSION_TICKET.id,
|
||||
SSLHandshake.NEW_SESSION_TICKET);
|
||||
}
|
||||
chc.handshakeConsumers.put(
|
||||
SSLHandshake.FINISHED.id,
|
||||
SSLHandshake.FINISHED);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 1996, 2018, Oracle and/or its affiliates. All rights reserved.
|
||||
* Copyright (c) 1996, 2019, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
|
@ -36,7 +36,7 @@ import javax.net.ssl.SSLProtocolException;
|
|||
* @author David Brownell
|
||||
*/
|
||||
final class SessionId {
|
||||
private static final int MAX_LENGTH = 32;
|
||||
static final int MAX_LENGTH = 32;
|
||||
private final byte[] sessionId; // max 32 bytes
|
||||
|
||||
// Constructs a new session ID ... perhaps for a rejoinable session
|
||||
|
|
|
@ -0,0 +1,539 @@
|
|||
/*
|
||||
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package sun.security.ssl;
|
||||
|
||||
import sun.security.action.GetPropertyAction;
|
||||
import sun.security.ssl.SSLExtension.ExtensionConsumer;
|
||||
import sun.security.ssl.SSLExtension.SSLExtensionSpec;
|
||||
import sun.security.ssl.SSLHandshake.HandshakeMessage;
|
||||
import sun.security.ssl.SupportedGroupsExtension.SupportedGroups;
|
||||
import sun.security.util.HexDumpEncoder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.net.ssl.SSLProtocolException;
|
||||
|
||||
import static sun.security.ssl.SSLExtension.CH_SESSION_TICKET;
|
||||
import static sun.security.ssl.SSLExtension.SH_SESSION_TICKET;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* SessionTicketExtension is an implementation of RFC 5077 with some internals
|
||||
* that are used for stateless operation in TLS 1.3.
|
||||
*
|
||||
* {@systemProperty jdk.tls.server.statelessKeyTimeout} can override the default
|
||||
* amount of time, in seconds, for how long a randomly-generated key and
|
||||
* parameters can be used before being regenerated. The key material is used
|
||||
* to encrypt the stateless session ticket that is sent to the client that will
|
||||
* be used during resumption. Default is 3600 seconds (1 hour)
|
||||
*
|
||||
*/
|
||||
|
||||
final class SessionTicketExtension {
|
||||
|
||||
static final HandshakeProducer chNetworkProducer =
|
||||
new T12CHSessionTicketProducer();
|
||||
static final ExtensionConsumer chOnLoadConsumer =
|
||||
new T12CHSessionTicketConsumer();
|
||||
static final HandshakeProducer shNetworkProducer =
|
||||
new T12SHSessionTicketProducer();
|
||||
static final ExtensionConsumer shOnLoadConsumer =
|
||||
new T12SHSessionTicketConsumer();
|
||||
|
||||
static final SSLStringizer steStringizer = new SessionTicketStringizer();
|
||||
|
||||
// Time in milliseconds until key is changed for encrypting session state
|
||||
private static final int TIMEOUT_DEFAULT = 3600 * 1000;
|
||||
private static final int keyTimeout;
|
||||
private static int currentKeyID = new SecureRandom().nextInt();
|
||||
private static final int KEYLEN = 256;
|
||||
|
||||
static {
|
||||
String s = GetPropertyAction.privilegedGetProperty(
|
||||
"jdk.tls.server.statelessKeyTimeout");
|
||||
if (s != null) {
|
||||
int kt;
|
||||
try {
|
||||
kt = Integer.parseInt(s) * 1000; // change to ms
|
||||
if (kt < 0 ||
|
||||
kt > NewSessionTicket.MAX_TICKET_LIFETIME) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
|
||||
SSLLogger.warning("Invalid timeout for " +
|
||||
"jdk.tls.server.statelessKeyTimeout: " +
|
||||
kt + ". Set to default value " +
|
||||
TIMEOUT_DEFAULT + "sec");
|
||||
}
|
||||
kt = TIMEOUT_DEFAULT;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
kt = TIMEOUT_DEFAULT;
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
|
||||
SSLLogger.warning("Invalid timeout for " +
|
||||
"jdk.tls.server.statelessKeyTimeout: " + s +
|
||||
". Set to default value " + TIMEOUT_DEFAULT +
|
||||
"sec");
|
||||
}
|
||||
}
|
||||
keyTimeout = kt;
|
||||
} else {
|
||||
keyTimeout = TIMEOUT_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
// Crypto key context for session state. Used with stateless operation.
|
||||
final static class StatelessKey {
|
||||
final long timeout;
|
||||
final SecretKey key;
|
||||
final int num;
|
||||
|
||||
StatelessKey(HandshakeContext hc, int newNum) {
|
||||
SecretKey k = null;
|
||||
try {
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
kg.init(KEYLEN, hc.sslContext.getSecureRandom());
|
||||
k = kg.generateKey();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// should not happen;
|
||||
}
|
||||
key = k;
|
||||
timeout = System.currentTimeMillis() + keyTimeout;
|
||||
num = newNum;
|
||||
hc.sslContext.keyHashMap.put(Integer.valueOf(num), this);
|
||||
}
|
||||
|
||||
// Check if key needs to be changed
|
||||
boolean isExpired() {
|
||||
return ((System.currentTimeMillis()) > timeout);
|
||||
}
|
||||
|
||||
// Check if this key is ready for deletion.
|
||||
boolean isInvalid(long sessionTimeout) {
|
||||
return ((System.currentTimeMillis()) > (timeout + sessionTimeout));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class KeyState {
|
||||
|
||||
// Get a key with a specific key number
|
||||
static StatelessKey getKey(HandshakeContext hc, int num) {
|
||||
StatelessKey ssk = hc.sslContext.keyHashMap.get(num);
|
||||
|
||||
if (ssk == null || ssk.isInvalid(getSessionTimeout(hc))) {
|
||||
return null;
|
||||
}
|
||||
return ssk;
|
||||
}
|
||||
|
||||
// Get the current valid key, this will generate a new key if needed
|
||||
static StatelessKey getCurrentKey(HandshakeContext hc) {
|
||||
StatelessKey ssk = hc.sslContext.keyHashMap.get(currentKeyID);
|
||||
|
||||
if (ssk != null && !ssk.isExpired()) {
|
||||
return ssk;
|
||||
}
|
||||
return nextKey(hc);
|
||||
}
|
||||
|
||||
// This method locks when the first getCurrentKey() finds it to be too
|
||||
// old and create a new key to replace the current key. After the new
|
||||
// key established, the lock can be released so following
|
||||
// operations will start using the new key.
|
||||
// The first operation will take a longer code path by generating the
|
||||
// next key and cleaning up old keys.
|
||||
private static StatelessKey nextKey(HandshakeContext hc) {
|
||||
StatelessKey ssk;
|
||||
|
||||
synchronized (hc.sslContext.keyHashMap) {
|
||||
// If the current key is no longer expired, it was already
|
||||
// updated by a previous operation and we can return.
|
||||
ssk = hc.sslContext.keyHashMap.get(currentKeyID);
|
||||
if (ssk != null && !ssk.isExpired()) {
|
||||
return ssk;
|
||||
}
|
||||
int newNum;
|
||||
if (currentKeyID == Integer.MAX_VALUE) {
|
||||
newNum = 0;
|
||||
} else {
|
||||
newNum = currentKeyID + 1;
|
||||
}
|
||||
// Get new key
|
||||
ssk = new StatelessKey(hc, newNum);
|
||||
currentKeyID = newNum;
|
||||
// Release lock since the new key is ready to be used.
|
||||
}
|
||||
|
||||
// Clean up any old keys, then return the current key
|
||||
cleanup(hc);
|
||||
return ssk;
|
||||
}
|
||||
|
||||
// Deletes any invalid SessionStateKeys.
|
||||
static void cleanup(HandshakeContext hc) {
|
||||
int sessionTimeout = getSessionTimeout(hc);
|
||||
|
||||
StatelessKey ks;
|
||||
for (Object o : hc.sslContext.keyHashMap.keySet().toArray()) {
|
||||
Integer i = (Integer)o;
|
||||
ks = hc.sslContext.keyHashMap.get(i);
|
||||
if (ks.isInvalid(sessionTimeout)) {
|
||||
try {
|
||||
ks.key.destroy();
|
||||
} catch (Exception e) {
|
||||
// Suppress
|
||||
}
|
||||
hc.sslContext.keyHashMap.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int getSessionTimeout(HandshakeContext hc) {
|
||||
return hc.sslContext.engineGetServerSessionContext().
|
||||
getSessionTimeout() * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class contains the session state that is in the session ticket.
|
||||
* Using the key associated with the ticket, the class encrypts and
|
||||
* decrypts the data, but does not interpret the data.
|
||||
*/
|
||||
static final class SessionTicketSpec implements SSLExtensionSpec {
|
||||
private static final int GCM_TAG_LEN = 128;
|
||||
ByteBuffer data;
|
||||
static final ByteBuffer zero = ByteBuffer.wrap(new byte[0]);
|
||||
|
||||
SessionTicketSpec() {
|
||||
data = zero;
|
||||
}
|
||||
|
||||
SessionTicketSpec(byte[] b) throws IOException {
|
||||
this(ByteBuffer.wrap(b));
|
||||
}
|
||||
|
||||
SessionTicketSpec(ByteBuffer buf) throws IOException {
|
||||
if (buf == null) {
|
||||
throw new SSLProtocolException(
|
||||
"SessionTicket buffer too small");
|
||||
}
|
||||
if (buf.remaining() > 65536) {
|
||||
throw new SSLProtocolException(
|
||||
"SessionTicket buffer too large. " + buf.remaining());
|
||||
}
|
||||
|
||||
data = buf;
|
||||
}
|
||||
|
||||
public byte[] encrypt(HandshakeContext hc, SSLSessionImpl session)
|
||||
throws IOException {
|
||||
byte[] encrypted;
|
||||
StatelessKey key = KeyState.getCurrentKey(hc);
|
||||
byte[] iv = new byte[16];
|
||||
|
||||
try {
|
||||
SecureRandom random = hc.sslContext.getSecureRandom();
|
||||
random.nextBytes(iv);
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.ENCRYPT_MODE, key.key,
|
||||
new GCMParameterSpec(GCM_TAG_LEN, iv));
|
||||
c.updateAAD(new byte[] {
|
||||
(byte)(key.num >>> 24),
|
||||
(byte)(key.num >>> 16),
|
||||
(byte)(key.num >>> 8),
|
||||
(byte)(key.num)}
|
||||
);
|
||||
encrypted = c.doFinal(session.write());
|
||||
|
||||
byte[] result = new byte[encrypted.length + Integer.BYTES +
|
||||
iv.length];
|
||||
result[0] = (byte)(key.num >>> 24);
|
||||
result[1] = (byte)(key.num >>> 16);
|
||||
result[2] = (byte)(key.num >>> 8);
|
||||
result[3] = (byte)(key.num);
|
||||
System.arraycopy(iv, 0, result, Integer.BYTES, iv.length);
|
||||
System.arraycopy(encrypted, 0, result,
|
||||
Integer.BYTES + iv.length, encrypted.length);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw hc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, e);
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer decrypt(HandshakeContext hc) {
|
||||
int keyID;
|
||||
byte[] iv;
|
||||
try {
|
||||
keyID = data.getInt();
|
||||
StatelessKey key = KeyState.getKey(hc, keyID);
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
iv = new byte[16];
|
||||
data.get(iv);
|
||||
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
c.init(Cipher.DECRYPT_MODE, key.key,
|
||||
new GCMParameterSpec(GCM_TAG_LEN, iv));
|
||||
c.updateAAD(new byte[] {
|
||||
(byte)(keyID >>> 24),
|
||||
(byte)(keyID >>> 16),
|
||||
(byte)(keyID >>> 8),
|
||||
(byte)(keyID)}
|
||||
);
|
||||
/*
|
||||
return ByteBuffer.wrap(c.doFinal(data,
|
||||
Integer.BYTES + iv.length,
|
||||
data.length - (Integer.BYTES + iv.length)));
|
||||
*/
|
||||
ByteBuffer out;
|
||||
out = ByteBuffer.allocate(data.remaining() - GCM_TAG_LEN / 8);
|
||||
c.doFinal(data, out);
|
||||
out.flip();
|
||||
return out;
|
||||
} catch (Exception e) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Decryption failed." + e.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] getEncoded() {
|
||||
byte[] out = new byte[data.capacity()];
|
||||
data.duplicate().get(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (data == null) {
|
||||
return "<null>";
|
||||
}
|
||||
if (data.capacity() == 0) {
|
||||
return "<empty>";
|
||||
}
|
||||
|
||||
MessageFormat messageFormat = new MessageFormat(
|
||||
" \"ticket\" : '{'\n" +
|
||||
"{0}\n" +
|
||||
" '}'",
|
||||
Locale.ENGLISH);
|
||||
HexDumpEncoder hexEncoder = new HexDumpEncoder();
|
||||
|
||||
Object[] messageFields = {
|
||||
Utilities.indent(hexEncoder.encode(data.duplicate()),
|
||||
" "),
|
||||
};
|
||||
|
||||
return messageFormat.format(messageFields);
|
||||
}
|
||||
}
|
||||
|
||||
static final class SessionTicketStringizer implements SSLStringizer {
|
||||
SessionTicketStringizer() {}
|
||||
|
||||
@Override
|
||||
public String toString(ByteBuffer buffer) {
|
||||
try {
|
||||
return new SessionTicketSpec(buffer).toString();
|
||||
} catch (IOException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class T12CHSessionTicketProducer
|
||||
extends SupportedGroups implements HandshakeProducer {
|
||||
T12CHSessionTicketProducer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] produce(ConnectionContext context,
|
||||
HandshakeMessage message) throws IOException {
|
||||
|
||||
ClientHandshakeContext chc = (ClientHandshakeContext)context;
|
||||
|
||||
// If the context does not allow stateless tickets, exit
|
||||
if (!((SSLSessionContextImpl)chc.sslContext.
|
||||
engineGetClientSessionContext()).statelessEnabled()) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Stateless resumption not supported");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
chc.statelessResumption = true;
|
||||
|
||||
// If resumption is not in progress, return an empty value
|
||||
if (!chc.isResumption || chc.resumingSession == null) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Stateless resumption supported");
|
||||
}
|
||||
return new SessionTicketSpec().getEncoded();
|
||||
}
|
||||
|
||||
if (chc.localSupportedSignAlgs == null) {
|
||||
chc.localSupportedSignAlgs =
|
||||
SignatureScheme.getSupportedAlgorithms(
|
||||
chc.algorithmConstraints, chc.activeProtocols);
|
||||
}
|
||||
|
||||
return chc.resumingSession.getPskIdentity();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class T12CHSessionTicketConsumer
|
||||
implements ExtensionConsumer {
|
||||
T12CHSessionTicketConsumer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ConnectionContext context,
|
||||
HandshakeMessage message, ByteBuffer buffer)
|
||||
throws IOException {
|
||||
ServerHandshakeContext shc = (ServerHandshakeContext) context;
|
||||
|
||||
// Skip if extension is not provided
|
||||
if (!shc.sslConfig.isAvailable(CH_SESSION_TICKET)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip consumption if we are already in stateless resumption
|
||||
if (shc.statelessResumption) {
|
||||
return;
|
||||
}
|
||||
// If the context does not allow stateless tickets, exit
|
||||
SSLSessionContextImpl cache = (SSLSessionContextImpl)shc.sslContext
|
||||
.engineGetServerSessionContext();
|
||||
if (!cache.statelessEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer.remaining() == 0) {
|
||||
shc.statelessResumption = true;
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Client accepts session tickets.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the extension.
|
||||
SessionTicketSpec spec;
|
||||
try {
|
||||
spec = new SessionTicketSpec(buffer);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("SessionTicket data invalid. Doing full " +
|
||||
"handshake.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
ByteBuffer b = spec.decrypt(shc);
|
||||
if (b != null) {
|
||||
shc.resumingSession = new SSLSessionImpl(shc, b);
|
||||
shc.isResumption = true;
|
||||
shc.statelessResumption = true;
|
||||
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
|
||||
SSLLogger.fine("Valid stateless session ticket found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class T12SHSessionTicketProducer
|
||||
extends SupportedGroups implements HandshakeProducer {
|
||||
T12SHSessionTicketProducer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] produce(ConnectionContext context,
|
||||
HandshakeMessage message) {
|
||||
|
||||
ServerHandshakeContext shc = (ServerHandshakeContext)context;
|
||||
|
||||
// If boolean is false, the CH did not have this extension
|
||||
if (!shc.statelessResumption) {
|
||||
return null;
|
||||
}
|
||||
// If the client has sent a SessionTicketExtension and stateless
|
||||
// is enabled on the server, return an empty message.
|
||||
// If the context does not allow stateless tickets, exit
|
||||
SSLSessionContextImpl cache = (SSLSessionContextImpl)shc.sslContext
|
||||
.engineGetServerSessionContext();
|
||||
if (cache.statelessEnabled()) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
shc.statelessResumption = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class T12SHSessionTicketConsumer
|
||||
implements ExtensionConsumer {
|
||||
T12SHSessionTicketConsumer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ConnectionContext context,
|
||||
HandshakeMessage message, ByteBuffer buffer)
|
||||
throws IOException {
|
||||
ClientHandshakeContext chc = (ClientHandshakeContext) context;
|
||||
|
||||
// Skip if extension is not provided
|
||||
if (!chc.sslConfig.isAvailable(SH_SESSION_TICKET)) {
|
||||
chc.statelessResumption = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the context does not allow stateless tickets, exit
|
||||
if (!((SSLSessionContextImpl)chc.sslContext.
|
||||
engineGetClientSessionContext()).statelessEnabled()) {
|
||||
chc.statelessResumption = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (new SessionTicketSpec(buffer) == null) {
|
||||
return;
|
||||
}
|
||||
chc.statelessResumption = true;
|
||||
} catch (IOException e) {
|
||||
throw chc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -159,14 +159,19 @@ class TransportContext implements ConnectionContext {
|
|||
if (handshakeContext == null) {
|
||||
if (type == SSLHandshake.KEY_UPDATE.id ||
|
||||
type == SSLHandshake.NEW_SESSION_TICKET.id) {
|
||||
if (isNegotiated &&
|
||||
protocolVersion.useTLS13PlusSpec()) {
|
||||
handshakeContext = new PostHandshakeContext(this);
|
||||
} else {
|
||||
if (!isNegotiated) {
|
||||
throw fatal(Alert.UNEXPECTED_MESSAGE,
|
||||
"Unexpected unnegotiated post-handshake" +
|
||||
" message: " +
|
||||
SSLHandshake.nameOf(type));
|
||||
}
|
||||
if (type == SSLHandshake.KEY_UPDATE.id &&
|
||||
!protocolVersion.useTLS13PlusSpec()) {
|
||||
throw fatal(Alert.UNEXPECTED_MESSAGE,
|
||||
"Unexpected post-handshake message: " +
|
||||
SSLHandshake.nameOf(type));
|
||||
}
|
||||
handshakeContext = new PostHandshakeContext(this);
|
||||
} else {
|
||||
handshakeContext = sslConfig.isClientMode ?
|
||||
new ClientHandshakeContext(sslContext, this) :
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue