|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
|
|
package sun.security.ssl; |
|
|
|
import java.io.IOException; |
|
import java.nio.ByteBuffer; |
|
import java.security.SecureRandom; |
|
import java.security.cert.X509Certificate; |
|
import java.text.MessageFormat; |
|
import java.util.Arrays; |
|
import java.util.Collections; |
|
import java.util.LinkedList; |
|
import java.util.List; |
|
import java.util.Locale; |
|
import javax.net.ssl.SSLException; |
|
import javax.net.ssl.SSLHandshakeException; |
|
import javax.net.ssl.SSLPeerUnverifiedException; |
|
import javax.net.ssl.SSLProtocolException; |
|
import static sun.security.ssl.ClientAuthType.CLIENT_AUTH_REQUIRED; |
|
import sun.security.ssl.SSLHandshake.HandshakeMessage; |
|
import sun.security.ssl.SupportedVersionsExtension.CHSupportedVersionsSpec; |
|
|
|
|
|
|
|
*/ |
|
final class ClientHello { |
|
static final SSLProducer kickstartProducer = |
|
new ClientHelloKickstartProducer(); |
|
static final SSLConsumer handshakeConsumer = |
|
new ClientHelloConsumer(); |
|
static final HandshakeProducer handshakeProducer = |
|
new ClientHelloProducer(); |
|
|
|
private static final HandshakeConsumer t12HandshakeConsumer = |
|
new T12ClientHelloConsumer(); |
|
private static final HandshakeConsumer t13HandshakeConsumer = |
|
new T13ClientHelloConsumer(); |
|
private static final HandshakeConsumer d12HandshakeConsumer = |
|
new D12ClientHelloConsumer(); |
|
private static final HandshakeConsumer d13HandshakeConsumer = |
|
new D13ClientHelloConsumer(); |
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
static final class ClientHelloMessage extends HandshakeMessage { |
|
private final boolean isDTLS; |
|
|
|
final int clientVersion; |
|
final RandomCookie clientRandom; |
|
final SessionId sessionId; |
|
private byte[] cookie; |
|
final int[] cipherSuiteIds; |
|
final List<CipherSuite> cipherSuites; |
|
final byte[] compressionMethod; |
|
final SSLExtensions extensions; |
|
|
|
private static final byte[] NULL_COMPRESSION = new byte[] {0}; |
|
|
|
ClientHelloMessage(HandshakeContext handshakeContext, |
|
int clientVersion, SessionId sessionId, |
|
List<CipherSuite> cipherSuites, SecureRandom generator) { |
|
super(handshakeContext); |
|
this.isDTLS = handshakeContext.sslContext.isDTLS(); |
|
|
|
this.clientVersion = clientVersion; |
|
this.clientRandom = new RandomCookie(generator); |
|
this.sessionId = sessionId; |
|
if (isDTLS) { |
|
this.cookie = new byte[0]; |
|
} else { |
|
this.cookie = null; |
|
} |
|
|
|
this.cipherSuites = cipherSuites; |
|
this.cipherSuiteIds = getCipherSuiteIds(cipherSuites); |
|
this.extensions = new SSLExtensions(this); |
|
|
|
|
|
this.compressionMethod = NULL_COMPRESSION; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
*/ |
|
static void readPartial(TransportContext tc, |
|
ByteBuffer m) throws IOException { |
|
boolean isDTLS = tc.sslContext.isDTLS(); |
|
|
|
|
|
Record.getInt16(m); |
|
|
|
new RandomCookie(m); |
|
|
|
|
|
Record.getBytes8(m); |
|
|
|
|
|
if (isDTLS) { |
|
Record.getBytes8(m); |
|
} |
|
|
|
|
|
Record.getBytes16(m); |
|
|
|
Record.getBytes8(m); |
|
|
|
if (m.remaining() >= 2) { |
|
int remaining = Record.getInt16(m); |
|
while (remaining > 0) { |
|
int id = Record.getInt16(m); |
|
int extLen = Record.getInt16(m); |
|
remaining -= extLen + 4; |
|
|
|
if (id == SSLExtension.CH_PRE_SHARED_KEY.id) { |
|
|
|
if (remaining > 0) { |
|
tc.fatal(Alert.ILLEGAL_PARAMETER, |
|
"pre_shared_key extension is not last"); |
|
} |
|
|
|
Record.getBytes16(m); |
|
return; |
|
} else { |
|
m.position(m.position() + extLen); |
|
|
|
} |
|
} |
|
} // Otherwise, ignore the remaining bytes. |
|
} |
|
|
|
ClientHelloMessage(HandshakeContext handshakeContext, ByteBuffer m, |
|
SSLExtension[] supportedExtensions) throws IOException { |
|
super(handshakeContext); |
|
this.isDTLS = handshakeContext.sslContext.isDTLS(); |
|
|
|
this.clientVersion = ((m.get() & 0xFF) << 8) | (m.get() & 0xFF); |
|
this.clientRandom = new RandomCookie(m); |
|
this.sessionId = new SessionId(Record.getBytes8(m)); |
|
try { |
|
sessionId.checkLength(clientVersion); |
|
} catch (SSLProtocolException ex) { |
|
handshakeContext.conContext.fatal(Alert.ILLEGAL_PARAMETER, ex); |
|
} |
|
if (isDTLS) { |
|
this.cookie = Record.getBytes8(m); |
|
} else { |
|
this.cookie = null; |
|
} |
|
|
|
byte[] encodedIds = Record.getBytes16(m); |
|
if (encodedIds.length == 0 || (encodedIds.length & 0x01) != 0) { |
|
handshakeContext.conContext.fatal(Alert.ILLEGAL_PARAMETER, |
|
"Invalid ClientHello message"); |
|
} |
|
|
|
this.cipherSuiteIds = new int[encodedIds.length >> 1]; |
|
for (int i = 0, j = 0; i < encodedIds.length; i++, j++) { |
|
cipherSuiteIds[j] = |
|
((encodedIds[i++] & 0xFF) << 8) | (encodedIds[i] & 0xFF); |
|
} |
|
this.cipherSuites = getCipherSuites(cipherSuiteIds); |
|
|
|
this.compressionMethod = Record.getBytes8(m); |
|
|
|
if (m.hasRemaining()) { |
|
this.extensions = |
|
new SSLExtensions(this, m, supportedExtensions); |
|
} else { |
|
this.extensions = new SSLExtensions(this); |
|
} |
|
} |
|
|
|
void setHelloCookie(byte[] cookie) { |
|
this.cookie = cookie; |
|
} |
|
|
|
|
|
byte[] getHelloCookieBytes() { |
|
HandshakeOutStream hos = new HandshakeOutStream(null); |
|
try { |
|
|
|
hos.putInt8((byte)((clientVersion >>> 8) & 0xFF)); |
|
hos.putInt8((byte)(clientVersion & 0xFF)); |
|
hos.write(clientRandom.randomBytes, 0, 32); |
|
hos.putBytes8(sessionId.getId()); |
|
|
|
hos.putBytes16(getEncodedCipherSuites()); |
|
hos.putBytes8(compressionMethod); |
|
extensions.send(hos); |
|
// extensions is mandatory. |
|
} catch (IOException ioe) { |
|
// unlikely |
|
} |
|
|
|
return hos.toByteArray(); |
|
} |
|
|
|
|
|
byte[] getHeaderBytes() { |
|
HandshakeOutStream hos = new HandshakeOutStream(null); |
|
try { |
|
|
|
hos.putInt8((byte)((clientVersion >>> 8) & 0xFF)); |
|
hos.putInt8((byte)(clientVersion & 0xFF)); |
|
hos.write(clientRandom.randomBytes, 0, 32); |
|
hos.putBytes8(sessionId.getId()); |
|
hos.putBytes16(getEncodedCipherSuites()); |
|
hos.putBytes8(compressionMethod); |
|
} catch (IOException ioe) { |
|
// unlikely |
|
} |
|
|
|
return hos.toByteArray(); |
|
} |
|
|
|
private static int[] getCipherSuiteIds( |
|
List<CipherSuite> cipherSuites) { |
|
if (cipherSuites != null) { |
|
int[] ids = new int[cipherSuites.size()]; |
|
int i = 0; |
|
for (CipherSuite cipherSuite : cipherSuites) { |
|
ids[i++] = cipherSuite.id; |
|
} |
|
|
|
return ids; |
|
} |
|
|
|
return new int[0]; |
|
} |
|
|
|
private static List<CipherSuite> getCipherSuites(int[] ids) { |
|
List<CipherSuite> cipherSuites = new LinkedList<>(); |
|
for (int id : ids) { |
|
CipherSuite cipherSuite = CipherSuite.valueOf(id); |
|
if (cipherSuite != null) { |
|
cipherSuites.add(cipherSuite); |
|
} |
|
} |
|
|
|
return Collections.unmodifiableList(cipherSuites); |
|
} |
|
|
|
private List<String> getCipherSuiteNames() { |
|
List<String> names = new LinkedList<>(); |
|
for (int id : cipherSuiteIds) { |
|
names.add(CipherSuite.nameOf(id) + |
|
"(" + Utilities.byte16HexString(id) + ")"); } |
|
|
|
return names; |
|
} |
|
|
|
private byte[] getEncodedCipherSuites() { |
|
byte[] encoded = new byte[cipherSuiteIds.length << 1]; |
|
int i = 0; |
|
for (int id : cipherSuiteIds) { |
|
encoded[i++] = (byte)(id >> 8); |
|
encoded[i++] = (byte)id; |
|
} |
|
return encoded; |
|
} |
|
|
|
@Override |
|
public SSLHandshake handshakeType() { |
|
return SSLHandshake.CLIENT_HELLO; |
|
} |
|
|
|
@Override |
|
public int messageLength() { |
|
|
|
|
|
|
|
*/ |
|
return (2 + 32 + 1 + 2 + 1 |
|
+ sessionId.length() |
|
+ (isDTLS ? (1 + cookie.length) : 0) |
|
+ (cipherSuiteIds.length * 2) |
|
+ compressionMethod.length) |
|
+ extensions.length(); |
|
// extensions is mandatory. |
|
} |
|
|
|
@Override |
|
public void send(HandshakeOutStream hos) throws IOException { |
|
sendCore(hos); |
|
extensions.send(hos); |
|
// extensions is mandatory. |
|
} |
|
|
|
void sendCore(HandshakeOutStream hos) throws IOException { |
|
hos.putInt8((byte) (clientVersion >>> 8)); |
|
hos.putInt8((byte) clientVersion); |
|
hos.write(clientRandom.randomBytes, 0, 32); |
|
hos.putBytes8(sessionId.getId()); |
|
if (isDTLS) { |
|
hos.putBytes8(cookie); |
|
} |
|
hos.putBytes16(getEncodedCipherSuites()); |
|
hos.putBytes8(compressionMethod); |
|
} |
|
|
|
@Override |
|
public String toString() { |
|
if (isDTLS) { |
|
MessageFormat messageFormat = new MessageFormat( |
|
"\"ClientHello\": '{'\n" + |
|
" \"client version\" : \"{0}\",\n" + |
|
" \"random\" : \"{1}\",\n" + |
|
" \"session id\" : \"{2}\",\n" + |
|
" \"cookie\" : \"{3}\",\n" + |
|
" \"cipher suites\" : \"{4}\",\n" + |
|
" \"compression methods\" : \"{5}\",\n" + |
|
" \"extensions\" : [\n" + |
|
"{6}\n" + |
|
" ]\n" + |
|
"'}'", |
|
Locale.ENGLISH); |
|
Object[] messageFields = { |
|
ProtocolVersion.nameOf(clientVersion), |
|
Utilities.toHexString(clientRandom.randomBytes), |
|
sessionId.toString(), |
|
Utilities.toHexString(cookie), |
|
getCipherSuiteNames().toString(), |
|
Utilities.toHexString(compressionMethod), |
|
Utilities.indent(Utilities.indent(extensions.toString())) |
|
}; |
|
|
|
return messageFormat.format(messageFields); |
|
} else { |
|
MessageFormat messageFormat = new MessageFormat( |
|
"\"ClientHello\": '{'\n" + |
|
" \"client version\" : \"{0}\",\n" + |
|
" \"random\" : \"{1}\",\n" + |
|
" \"session id\" : \"{2}\",\n" + |
|
" \"cipher suites\" : \"{3}\",\n" + |
|
" \"compression methods\" : \"{4}\",\n" + |
|
" \"extensions\" : [\n" + |
|
"{5}\n" + |
|
" ]\n" + |
|
"'}'", |
|
Locale.ENGLISH); |
|
Object[] messageFields = { |
|
ProtocolVersion.nameOf(clientVersion), |
|
Utilities.toHexString(clientRandom.randomBytes), |
|
sessionId.toString(), |
|
getCipherSuiteNames().toString(), |
|
Utilities.toHexString(compressionMethod), |
|
Utilities.indent(Utilities.indent(extensions.toString())) |
|
}; |
|
|
|
return messageFormat.format(messageFields); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static final |
|
class ClientHelloKickstartProducer implements SSLProducer { |
|
|
|
private ClientHelloKickstartProducer() { |
|
// blank |
|
} |
|
|
|
|
|
@Override |
|
public byte[] produce(ConnectionContext context) throws IOException { |
|
|
|
ClientHandshakeContext chc = (ClientHandshakeContext)context; |
|
|
|
|
|
chc.handshakeProducers.remove(SSLHandshake.CLIENT_HELLO.id); |
|
|
|
|
|
ProtocolVersion maxProtocolVersion = chc.maximumActiveProtocol; |
|
|
|
|
|
SessionId sessionId = SSLSessionImpl.nullSession.getSessionId(); |
|
|
|
|
|
List<CipherSuite> cipherSuites = chc.activeCipherSuites; |
|
|
|
// |
|
// Try to resume an existing session. |
|
|
|
SSLSessionContextImpl ssci = (SSLSessionContextImpl) |
|
chc.sslContext.engineGetClientSessionContext(); |
|
SSLSessionImpl session = ssci.get( |
|
chc.conContext.transport.getPeerHost(), |
|
chc.conContext.transport.getPeerPort()); |
|
if (session != null) { |
|
// If unsafe server certificate change is not allowed, reserve |
|
// current server certificates if the previous handshake is a |
|
|
|
if (!ClientHandshakeContext.allowUnsafeServerCertChange && |
|
session.isSessionResumption()) { |
|
try { |
|
|
|
chc.reservedServerCerts = |
|
(X509Certificate[])session.getPeerCertificates(); |
|
} catch (SSLPeerUnverifiedException puve) { |
|
// Maybe not certificate-based, ignore the exception. |
|
} |
|
} |
|
|
|
if (!session.isRejoinable()) { |
|
session = null; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, the session is not rejoinable"); |
|
} |
|
} |
|
} |
|
|
|
CipherSuite sessionSuite = null; |
|
if (session != null) { |
|
sessionSuite = session.getSuite(); |
|
if (!chc.isNegotiable(sessionSuite)) { |
|
session = null; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, unavailable session cipher suite"); |
|
} |
|
} |
|
} |
|
|
|
ProtocolVersion sessionVersion = null; |
|
if (session != null) { |
|
sessionVersion = session.getProtocolVersion(); |
|
if (!chc.isNegotiable(sessionVersion)) { |
|
session = null; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, unavailable protocol version"); |
|
} |
|
} |
|
} |
|
|
|
if (session != null && |
|
!sessionVersion.useTLS13PlusSpec() && |
|
SSLConfiguration.useExtendedMasterSecret) { |
|
|
|
boolean isEmsAvailable = chc.sslConfig.isAvailable( |
|
SSLExtension.CH_EXTENDED_MASTER_SECRET, sessionVersion); |
|
if (isEmsAvailable && !session.useExtendedMasterSecret && |
|
!SSLConfiguration.allowLegacyResumption) { |
|
// perform full handshake instead |
|
// |
|
// The client SHOULD NOT offer an abbreviated handshake |
|
// to resume a session that does not use an extended |
|
// master secret. Instead, it SHOULD offer a full |
|
|
|
session = null; |
|
} |
|
|
|
if ((session != null) && |
|
!ClientHandshakeContext.allowUnsafeServerCertChange) { |
|
// It is fine to move on with abbreviate handshake if |
|
|
|
String identityAlg = chc.sslConfig.identificationProtocol; |
|
if ((identityAlg == null || identityAlg.length() == 0)) { |
|
if (isEmsAvailable) { |
|
if (!session.useExtendedMasterSecret) { |
|
|
|
session = null; |
|
} // Otherwise, use extended master secret. |
|
} else { |
|
// The extended master secret extension does not |
|
// apply to SSL 3.0. Perform a full handshake |
|
// instead. |
|
// |
|
// Note that the useExtendedMasterSecret is |
|
// extended to protect SSL 3.0 connections, |
|
|
|
session = null; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (session != null) { |
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest("Try resuming session", session); |
|
} |
|
|
|
|
|
if (!session.getProtocolVersion().useTLS13PlusSpec()) { |
|
sessionId = session.getSessionId(); |
|
} |
|
if (!maxProtocolVersion.equals(sessionVersion)) { |
|
maxProtocolVersion = sessionVersion; |
|
|
|
// Update protocol version number in underlying socket and |
|
// handshake output stream, so that the output records |
|
|
|
chc.setVersion(sessionVersion); |
|
} |
|
|
|
// If no new session is allowed, force use of the previous |
|
// session ciphersuite, and add the renegotiation SCSV if |
|
|
|
if (!chc.sslConfig.enableSessionCreation) { |
|
if (!chc.conContext.isNegotiated && |
|
!sessionVersion.useTLS13PlusSpec() && |
|
cipherSuites.contains( |
|
CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV)) { |
|
cipherSuites = Arrays.asList(sessionSuite, |
|
CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV); |
|
} else { |
|
cipherSuites = Arrays.asList(sessionSuite); |
|
} |
|
|
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"No new session is allowed, so try to resume " + |
|
"the session cipher suite only", sessionSuite); |
|
} |
|
} |
|
|
|
chc.isResumption = true; |
|
chc.resumingSession = session; |
|
} |
|
|
|
if (session == null) { |
|
if (!chc.sslConfig.enableSessionCreation) { |
|
throw new SSLHandshakeException( |
|
"No new session is allowed and " + |
|
"no existing session can be resumed"); |
|
} |
|
|
|
if (maxProtocolVersion.useTLS13PlusSpec() && |
|
SSLConfiguration.useCompatibilityMode) { |
|
// In compatibility mode, the TLS 1.3 legacy_session_id |
|
// field MUST be non-empty, so a client not offering a |
|
|
|
sessionId = |
|
new SessionId(true, chc.sslContext.getSecureRandom()); |
|
} |
|
} |
|
|
|
ProtocolVersion minimumVersion = ProtocolVersion.NONE; |
|
for (ProtocolVersion pv : chc.activeProtocols) { |
|
if (minimumVersion == ProtocolVersion.NONE || |
|
pv.compare(minimumVersion) < 0) { |
|
minimumVersion = pv; |
|
} |
|
} |
|
|
|
|
|
if (!minimumVersion.useTLS13PlusSpec()) { |
|
if (chc.conContext.secureRenegotiation && |
|
cipherSuites.contains( |
|
CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV)) { |
|
|
|
cipherSuites = new LinkedList<>(cipherSuites); |
|
cipherSuites.remove( |
|
CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV); |
|
} |
|
} |
|
|
|
|
|
boolean negotiable = false; |
|
for (CipherSuite suite : cipherSuites) { |
|
if (chc.isNegotiable(suite)) { |
|
negotiable = true; |
|
break; |
|
} |
|
} |
|
if (!negotiable) { |
|
throw new SSLHandshakeException("No negotiable cipher suite"); |
|
} |
|
|
|
|
|
ProtocolVersion clientHelloVersion = maxProtocolVersion; |
|
if (clientHelloVersion.useTLS13PlusSpec()) { |
|
// In (D)TLS 1.3, the client indicates its version preferences |
|
// in the "supported_versions" extension and the client_version |
|
|
|
if (clientHelloVersion.isDTLS) { |
|
clientHelloVersion = ProtocolVersion.DTLS12; |
|
} else { |
|
clientHelloVersion = ProtocolVersion.TLS12; |
|
} |
|
} |
|
|
|
ClientHelloMessage chm = new ClientHelloMessage(chc, |
|
clientHelloVersion.id, sessionId, cipherSuites, |
|
chc.sslContext.getSecureRandom()); |
|
|
|
|
|
chc.clientHelloRandom = chm.clientRandom; |
|
chc.clientHelloVersion = clientHelloVersion.id; |
|
|
|
|
|
SSLExtension[] extTypes = chc.sslConfig.getEnabledExtensions( |
|
SSLHandshake.CLIENT_HELLO, chc.activeProtocols); |
|
chm.extensions.produce(chc, extTypes); |
|
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
|
SSLLogger.fine("Produced ClientHello handshake message", chm); |
|
} |
|
|
|
|
|
chm.write(chc.handshakeOutput); |
|
chc.handshakeOutput.flush(); |
|
|
|
// Reserve the initial ClientHello message for the follow on |
|
|
|
chc.initialClientHelloMsg = chm; |
|
|
|
|
|
chc.handshakeConsumers.put( |
|
SSLHandshake.SERVER_HELLO.id, SSLHandshake.SERVER_HELLO); |
|
if (chc.sslContext.isDTLS() && |
|
!minimumVersion.useTLS13PlusSpec()) { |
|
chc.handshakeConsumers.put( |
|
SSLHandshake.HELLO_VERIFY_REQUEST.id, |
|
SSLHandshake.HELLO_VERIFY_REQUEST); |
|
} |
|
|
|
|
|
return null; |
|
} |
|
} |
|
|
|
private static final |
|
class ClientHelloProducer implements HandshakeProducer { |
|
|
|
private ClientHelloProducer() { |
|
// blank |
|
} |
|
|
|
// Response to one of the following handshake message: |
|
// HelloRequest (SSL 3.0/TLS 1.0/1.1/1.2) |
|
// ServerHello(HelloRetryRequest) (TLS 1.3) |
|
|
|
@Override |
|
public byte[] produce(ConnectionContext context, |
|
HandshakeMessage message) throws IOException { |
|
|
|
ClientHandshakeContext chc = (ClientHandshakeContext)context; |
|
|
|
SSLHandshake ht = message.handshakeType(); |
|
if (ht == null) { |
|
throw new UnsupportedOperationException("Not supported yet."); |
|
} |
|
|
|
switch (ht) { |
|
case HELLO_REQUEST: |
|
|
|
try { |
|
chc.kickstart(); |
|
} catch (IOException ioe) { |
|
chc.conContext.fatal(Alert.HANDSHAKE_FAILURE, ioe); |
|
} |
|
|
|
|
|
return null; |
|
case HELLO_VERIFY_REQUEST: |
|
// DTLS 1.0/1.2 |
|
// |
|
// The HelloVerifyRequest consumer should have updated the |
|
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
|
SSLLogger.fine( |
|
"Produced ClientHello(cookie) handshake message", |
|
chc.initialClientHelloMsg); |
|
} |
|
|
|
|
|
chc.initialClientHelloMsg.write(chc.handshakeOutput); |
|
chc.handshakeOutput.flush(); |
|
|
|
|
|
chc.handshakeConsumers.put(SSLHandshake.SERVER_HELLO.id, |
|
SSLHandshake.SERVER_HELLO); |
|
|
|
ProtocolVersion minimumVersion = ProtocolVersion.NONE; |
|
for (ProtocolVersion pv : chc.activeProtocols) { |
|
if (minimumVersion == ProtocolVersion.NONE || |
|
pv.compare(minimumVersion) < 0) { |
|
minimumVersion = pv; |
|
} |
|
} |
|
if (chc.sslContext.isDTLS() && |
|
!minimumVersion.useTLS13PlusSpec()) { |
|
chc.handshakeConsumers.put( |
|
SSLHandshake.HELLO_VERIFY_REQUEST.id, |
|
SSLHandshake.HELLO_VERIFY_REQUEST); |
|
} |
|
|
|
|
|
return null; |
|
case HELLO_RETRY_REQUEST: |
|
// TLS 1.3 |
|
// The HelloRetryRequest consumer should have updated the |
|
|
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
|
SSLLogger.fine( |
|
"Produced ClientHello(HRR) handshake message", |
|
chc.initialClientHelloMsg); |
|
} |
|
|
|
|
|
chc.initialClientHelloMsg.write(chc.handshakeOutput); |
|
chc.handshakeOutput.flush(); |
|
|
|
|
|
chc.conContext.consumers.putIfAbsent( |
|
ContentType.CHANGE_CIPHER_SPEC.id, |
|
ChangeCipherSpec.t13Consumer); |
|
chc.handshakeConsumers.put(SSLHandshake.SERVER_HELLO.id, |
|
SSLHandshake.SERVER_HELLO); |
|
|
|
|
|
return null; |
|
default: |
|
throw new UnsupportedOperationException( |
|
"Not supported yet."); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static final class ClientHelloConsumer implements SSLConsumer { |
|
|
|
private ClientHelloConsumer() { |
|
// blank |
|
} |
|
|
|
@Override |
|
public void consume(ConnectionContext context, |
|
ByteBuffer message) throws IOException { |
|
|
|
ServerHandshakeContext shc = (ServerHandshakeContext)context; |
|
|
|
|
|
shc.handshakeConsumers.remove(SSLHandshake.CLIENT_HELLO.id); |
|
if (!shc.handshakeConsumers.isEmpty()) { |
|
shc.conContext.fatal(Alert.UNEXPECTED_MESSAGE, |
|
"No more handshake message allowed " + |
|
"in a ClientHello flight"); |
|
} |
|
|
|
|
|
SSLExtension[] enabledExtensions = |
|
shc.sslConfig.getEnabledExtensions( |
|
SSLHandshake.CLIENT_HELLO); |
|
|
|
ClientHelloMessage chm = |
|
new ClientHelloMessage(shc, message, enabledExtensions); |
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
|
SSLLogger.fine("Consuming ClientHello handshake message", chm); |
|
} |
|
|
|
shc.clientHelloVersion = chm.clientVersion; |
|
onClientHello(shc, chm); |
|
} |
|
|
|
private void onClientHello(ServerHandshakeContext context, |
|
ClientHelloMessage clientHello) throws IOException { |
|
// Negotiate protocol version. |
|
// |
|
|
|
SSLExtension[] extTypes = new SSLExtension[] { |
|
SSLExtension.CH_SUPPORTED_VERSIONS |
|
}; |
|
clientHello.extensions.consumeOnLoad(context, extTypes); |
|
|
|
ProtocolVersion negotiatedProtocol; |
|
CHSupportedVersionsSpec svs = |
|
(CHSupportedVersionsSpec)context.handshakeExtensions.get( |
|
SSLExtension.CH_SUPPORTED_VERSIONS); |
|
if (svs != null) { |
|
negotiatedProtocol = |
|
negotiateProtocol(context, svs.requestedProtocols); |
|
} else { |
|
negotiatedProtocol = |
|
negotiateProtocol(context, clientHello.clientVersion); |
|
} |
|
context.negotiatedProtocol = negotiatedProtocol; |
|
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { |
|
SSLLogger.fine( |
|
"Negotiated protocol version: " + negotiatedProtocol.name); |
|
} |
|
|
|
|
|
if (negotiatedProtocol.isDTLS) { |
|
if (negotiatedProtocol.useTLS13PlusSpec()) { |
|
d13HandshakeConsumer.consume(context, clientHello); |
|
} else { |
|
d12HandshakeConsumer.consume(context, clientHello); |
|
} |
|
} else { |
|
if (negotiatedProtocol.useTLS13PlusSpec()) { |
|
t13HandshakeConsumer.consume(context, clientHello); |
|
} else { |
|
t12HandshakeConsumer.consume(context, clientHello); |
|
} |
|
} |
|
} |
|
|
|
// Select a protocol version according to the |
|
|
|
private ProtocolVersion negotiateProtocol( |
|
ServerHandshakeContext context, |
|
int clientHelloVersion) throws SSLException { |
|
|
|
// Per TLS 1.3 specification, server MUST negotiate TLS 1.2 or prior |
|
|
|
int chv = clientHelloVersion; |
|
if (context.sslContext.isDTLS()) { |
|
if (chv < ProtocolVersion.DTLS12.id) { |
|
chv = ProtocolVersion.DTLS12.id; |
|
} |
|
} else { |
|
if (chv > ProtocolVersion.TLS12.id) { |
|
chv = ProtocolVersion.TLS12.id; |
|
} |
|
} |
|
|
|
|
|
ProtocolVersion pv = ProtocolVersion.selectedFrom( |
|
context.activeProtocols, chv); |
|
if (pv == null || pv == ProtocolVersion.NONE || |
|
pv == ProtocolVersion.SSL20Hello) { |
|
context.conContext.fatal(Alert.PROTOCOL_VERSION, |
|
"Client requested protocol " + |
|
ProtocolVersion.nameOf(clientHelloVersion) + |
|
" is not enabled or supported in server context"); |
|
} |
|
|
|
return pv; |
|
} |
|
|
|
// Select a protocol version according to the |
|
|
|
private ProtocolVersion negotiateProtocol( |
|
ServerHandshakeContext context, |
|
int[] clientSupportedVersions) throws SSLException { |
|
|
|
// The client supported protocol versions are present in client |
|
// preference order. This implementation chooses to use the server |
|
|
|
for (ProtocolVersion spv : context.activeProtocols) { |
|
if (spv == ProtocolVersion.SSL20Hello) { |
|
continue; |
|
} |
|
for (int cpv : clientSupportedVersions) { |
|
if (cpv == ProtocolVersion.SSL20Hello.id) { |
|
continue; |
|
} |
|
if (spv.id == cpv) { |
|
return spv; |
|
} |
|
} |
|
} |
|
|
|
|
|
context.conContext.fatal(Alert.PROTOCOL_VERSION, |
|
"The client supported protocol versions " + Arrays.toString( |
|
ProtocolVersion.toStringArray(clientSupportedVersions)) + |
|
" are not accepted by server preferences " + |
|
context.activeProtocols); |
|
|
|
return null; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static final |
|
class T12ClientHelloConsumer implements HandshakeConsumer { |
|
|
|
private T12ClientHelloConsumer() { |
|
// blank |
|
} |
|
|
|
@Override |
|
public void consume(ConnectionContext context, |
|
HandshakeMessage message) throws IOException { |
|
|
|
ServerHandshakeContext shc = (ServerHandshakeContext)context; |
|
ClientHelloMessage clientHello = (ClientHelloMessage)message; |
|
|
|
// |
|
// validate |
|
// |
|
|
|
// Reject client initiated renegotiation? |
|
// |
|
// If server side should reject client-initiated renegotiation, |
|
// send an Alert.HANDSHAKE_FAILURE fatal alert, not a |
|
// no_renegotiation warning alert (no_renegotiation must be a |
|
// warning: RFC 2246). no_renegotiation might seem more |
|
// natural at first, but warnings are not appropriate because |
|
// the sending party does not know how the receiving party |
|
// will behave. This state must be treated as a fatal server |
|
// condition. |
|
// |
|
|
|
if (shc.conContext.isNegotiated) { |
|
if (!shc.conContext.secureRenegotiation && |
|
!HandshakeContext.allowUnsafeRenegotiation) { |
|
shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, |
|
"Unsafe renegotiation is not allowed"); |
|
} |
|
|
|
if (ServerHandshakeContext.rejectClientInitiatedRenego && |
|
!shc.kickstartMessageDelivered) { |
|
shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, |
|
"Client initiated renegotiation is not allowed"); |
|
} |
|
} |
|
|
|
|
|
if (clientHello.sessionId.length() != 0) { |
|
SSLSessionImpl previous = ((SSLSessionContextImpl)shc.sslContext |
|
.engineGetServerSessionContext()) |
|
.get(clientHello.sessionId.getId()); |
|
|
|
boolean resumingSession = |
|
(previous != null) && previous.isRejoinable(); |
|
if (!resumingSession) { |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, " + |
|
"the existing session is not rejoinable"); |
|
} |
|
} |
|
|
|
if (resumingSession) { |
|
ProtocolVersion sessionProtocol = |
|
previous.getProtocolVersion(); |
|
if (sessionProtocol != shc.negotiatedProtocol) { |
|
resumingSession = false; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, not the same protocol version"); |
|
} |
|
} |
|
} |
|
|
|
|
|
if (resumingSession && |
|
(shc.sslConfig.clientAuthType == CLIENT_AUTH_REQUIRED)) { |
|
try { |
|
previous.getPeerPrincipal(); |
|
} catch (SSLPeerUnverifiedException e) { |
|
resumingSession = false; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, " + |
|
"client authentication is required"); |
|
} |
|
} |
|
} |
|
|
|
|
|
if (resumingSession) { |
|
CipherSuite suite = previous.getSuite(); |
|
if ((!shc.isNegotiable(suite)) || |
|
(!clientHello.cipherSuites.contains(suite))) { |
|
resumingSession = false; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, " + |
|
"the session cipher suite is absent"); |
|
} |
|
} |
|
} |
|
|
|
// So far so good. Note that the handshake extensions may reset |
|
|
|
shc.isResumption = resumingSession; |
|
shc.resumingSession = resumingSession ? previous : null; |
|
} |
|
|
|
|
|
shc.clientHelloRandom = clientHello.clientRandom; |
|
|
|
|
|
SSLExtension[] extTypes = shc.sslConfig.getEnabledExtensions( |
|
SSLHandshake.CLIENT_HELLO); |
|
clientHello.extensions.consumeOnLoad(shc, extTypes); |
|
|
|
// |
|
// update |
|
|
|
if (!shc.conContext.isNegotiated) { |
|
shc.conContext.protocolVersion = shc.negotiatedProtocol; |
|
shc.conContext.outputRecord.setVersion(shc.negotiatedProtocol); |
|
} |
|
|
|
// update the responders |
|
// |
|
// Only need to ServerHello, which may add more responders later. |
|
// Note that ServerHello and HelloRetryRequest share the same |
|
// handshake type/id. The ServerHello producer may be replaced |
|
|
|
shc.handshakeProducers.put(SSLHandshake.SERVER_HELLO.id, |
|
SSLHandshake.SERVER_HELLO); |
|
|
|
// |
|
// produce |
|
|
|
SSLHandshake[] probableHandshakeMessages = new SSLHandshake[] { |
|
SSLHandshake.SERVER_HELLO, |
|
|
|
|
|
SSLHandshake.CERTIFICATE, |
|
SSLHandshake.CERTIFICATE_STATUS, |
|
SSLHandshake.SERVER_KEY_EXCHANGE, |
|
SSLHandshake.CERTIFICATE_REQUEST, |
|
SSLHandshake.SERVER_HELLO_DONE, |
|
|
|
|
|
SSLHandshake.FINISHED |
|
}; |
|
|
|
for (SSLHandshake hs : probableHandshakeMessages) { |
|
HandshakeProducer handshakeProducer = |
|
shc.handshakeProducers.remove(hs.id); |
|
if (handshakeProducer != null) { |
|
handshakeProducer.produce(context, clientHello); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static final |
|
class T13ClientHelloConsumer implements HandshakeConsumer { |
|
|
|
private T13ClientHelloConsumer() { |
|
// blank |
|
} |
|
|
|
@Override |
|
public void consume(ConnectionContext context, |
|
HandshakeMessage message) throws IOException { |
|
|
|
ServerHandshakeContext shc = (ServerHandshakeContext)context; |
|
ClientHelloMessage clientHello = (ClientHelloMessage)message; |
|
|
|
// The client may send a dummy change_cipher_spec record |
|
|
|
shc.conContext.consumers.putIfAbsent( |
|
ContentType.CHANGE_CIPHER_SPEC.id, |
|
ChangeCipherSpec.t13Consumer); |
|
|
|
// Is it a resumption? |
|
// |
|
// Check and launch the "psk_key_exchange_modes" and |
|
// "pre_shared_key" extensions first, which will reset the |
|
|
|
shc.isResumption = true; |
|
SSLExtension[] extTypes = new SSLExtension[] { |
|
SSLExtension.PSK_KEY_EXCHANGE_MODES, |
|
SSLExtension.CH_PRE_SHARED_KEY |
|
}; |
|
clientHello.extensions.consumeOnLoad(shc, extTypes); |
|
|
|
// Check and launch ClientHello extensions other than |
|
// "psk_key_exchange_modes", "pre_shared_key", "protocol_version" |
|
// and "key_share" extensions. |
|
// |
|
// These extensions may discard session resumption, or ask for |
|
|
|
extTypes = shc.sslConfig.getExclusiveExtensions( |
|
SSLHandshake.CLIENT_HELLO, |
|
Arrays.asList( |
|
SSLExtension.PSK_KEY_EXCHANGE_MODES, |
|
SSLExtension.CH_PRE_SHARED_KEY, |
|
SSLExtension.CH_SUPPORTED_VERSIONS)); |
|
clientHello.extensions.consumeOnLoad(shc, extTypes); |
|
|
|
if (!shc.handshakeProducers.isEmpty()) { |
|
|
|
goHelloRetryRequest(shc, clientHello); |
|
} else { |
|
goServerHello(shc, clientHello); |
|
} |
|
} |
|
|
|
private void goHelloRetryRequest(ServerHandshakeContext shc, |
|
ClientHelloMessage clientHello) throws IOException { |
|
HandshakeProducer handshakeProducer = |
|
shc.handshakeProducers.remove( |
|
SSLHandshake.HELLO_RETRY_REQUEST.id); |
|
if (handshakeProducer != null) { |
|
handshakeProducer.produce(shc, clientHello); |
|
} else { |
|
|
|
shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, |
|
"No HelloRetryRequest producer: " + shc.handshakeProducers); |
|
} |
|
|
|
if (!shc.handshakeProducers.isEmpty()) { |
|
|
|
shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, |
|
"unknown handshake producers: " + shc.handshakeProducers); |
|
} |
|
} |
|
|
|
private void goServerHello(ServerHandshakeContext shc, |
|
ClientHelloMessage clientHello) throws IOException { |
|
// |
|
// validate |
|
|
|
shc.clientHelloRandom = clientHello.clientRandom; |
|
|
|
// |
|
// update |
|
|
|
if (!shc.conContext.isNegotiated) { |
|
shc.conContext.protocolVersion = shc.negotiatedProtocol; |
|
shc.conContext.outputRecord.setVersion(shc.negotiatedProtocol); |
|
} |
|
|
|
// update the responders |
|
// |
|
// Only ServerHello/HelloRetryRequest producer, which adds |
|
|
|
shc.handshakeProducers.put(SSLHandshake.SERVER_HELLO.id, |
|
SSLHandshake.SERVER_HELLO); |
|
|
|
SSLHandshake[] probableHandshakeMessages = new SSLHandshake[] { |
|
SSLHandshake.SERVER_HELLO, |
|
|
|
|
|
SSLHandshake.ENCRYPTED_EXTENSIONS, |
|
SSLHandshake.CERTIFICATE_REQUEST, |
|
SSLHandshake.CERTIFICATE, |
|
SSLHandshake.CERTIFICATE_VERIFY, |
|
SSLHandshake.FINISHED |
|
}; |
|
|
|
// |
|
// produce |
|
|
|
for (SSLHandshake hs : probableHandshakeMessages) { |
|
HandshakeProducer handshakeProducer = |
|
shc.handshakeProducers.remove(hs.id); |
|
if (handshakeProducer != null) { |
|
handshakeProducer.produce(shc, clientHello); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
*/ |
|
private static final |
|
class D12ClientHelloConsumer implements HandshakeConsumer { |
|
|
|
private D12ClientHelloConsumer() { |
|
// blank |
|
} |
|
|
|
@Override |
|
public void consume(ConnectionContext context, |
|
HandshakeMessage message) throws IOException { |
|
|
|
ServerHandshakeContext shc = (ServerHandshakeContext)context; |
|
ClientHelloMessage clientHello = (ClientHelloMessage)message; |
|
|
|
// |
|
// validate |
|
// |
|
|
|
// Reject client initiated renegotiation? |
|
// |
|
// If server side should reject client-initiated renegotiation, |
|
// send an Alert.HANDSHAKE_FAILURE fatal alert, not a |
|
// no_renegotiation warning alert (no_renegotiation must be a |
|
// warning: RFC 2246). no_renegotiation might seem more |
|
// natural at first, but warnings are not appropriate because |
|
// the sending party does not know how the receiving party |
|
// will behave. This state must be treated as a fatal server |
|
// condition. |
|
// |
|
|
|
if (shc.conContext.isNegotiated) { |
|
if (!shc.conContext.secureRenegotiation && |
|
!HandshakeContext.allowUnsafeRenegotiation) { |
|
shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, |
|
"Unsafe renegotiation is not allowed"); |
|
} |
|
|
|
if (ServerHandshakeContext.rejectClientInitiatedRenego && |
|
!shc.kickstartMessageDelivered) { |
|
shc.conContext.fatal(Alert.HANDSHAKE_FAILURE, |
|
"Client initiated renegotiation is not allowed"); |
|
} |
|
} |
|
|
|
|
|
if (clientHello.sessionId.length() != 0) { |
|
SSLSessionImpl previous = ((SSLSessionContextImpl)shc.sslContext |
|
.engineGetServerSessionContext()) |
|
.get(clientHello.sessionId.getId()); |
|
|
|
boolean resumingSession = |
|
(previous != null) && previous.isRejoinable(); |
|
if (!resumingSession) { |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, " + |
|
"the existing session is not rejoinable"); |
|
} |
|
} |
|
|
|
if (resumingSession) { |
|
ProtocolVersion sessionProtocol = |
|
previous.getProtocolVersion(); |
|
if (sessionProtocol != shc.negotiatedProtocol) { |
|
resumingSession = false; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, not the same protocol version"); |
|
} |
|
} |
|
} |
|
|
|
|
|
if (resumingSession && |
|
(shc.sslConfig.clientAuthType == CLIENT_AUTH_REQUIRED)) { |
|
|
|
try { |
|
previous.getPeerPrincipal(); |
|
} catch (SSLPeerUnverifiedException e) { |
|
resumingSession = false; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, " + |
|
"client authentication is required"); |
|
} |
|
} |
|
} |
|
|
|
|
|
if (resumingSession) { |
|
CipherSuite suite = previous.getSuite(); |
|
if ((!shc.isNegotiable(suite)) || |
|
(!clientHello.cipherSuites.contains(suite))) { |
|
resumingSession = false; |
|
if (SSLLogger.isOn && |
|
SSLLogger.isOn("ssl,handshake,verbose")) { |
|
SSLLogger.finest( |
|
"Can't resume, " + |
|
"the session cipher suite is absent"); |
|
} |
|
} |
|
} |
|
|
|
// So far so good. Note that the handshake extensions may reset |
|
|
|
shc.isResumption = resumingSession; |
|
shc.resumingSession = resumingSession ? previous : null; |
|
} |
|
|
|
HelloCookieManager hcm = |
|
shc.sslContext.getHelloCookieManager(ProtocolVersion.DTLS10); |
|
if (!shc.isResumption && |
|
!hcm.isCookieValid(shc, clientHello, clientHello.cookie)) { |
|
// |
|
// Perform cookie exchange for DTLS handshaking if no cookie |
|
// or the cookie is invalid in the ClientHello message. |
|
// |
|
|
|
shc.handshakeProducers.put( |
|
SSLHandshake.HELLO_VERIFY_REQUEST.id, |
|
SSLHandshake.HELLO_VERIFY_REQUEST); |
|
|
|
// |
|
// produce response handshake message |
|
|
|
SSLHandshake.HELLO_VERIFY_REQUEST.produce(context, clientHello); |
|
|
|
return; |
|
} |
|
|
|
|
|
shc.clientHelloRandom = clientHello.clientRandom; |
|
|
|
|
|
SSLExtension[] extTypes = shc.sslConfig.getEnabledExtensions( |
|
SSLHandshake.CLIENT_HELLO); |
|
clientHello.extensions.consumeOnLoad(shc, extTypes); |
|
|
|
// |
|
// update |
|
|
|
if (!shc.conContext.isNegotiated) { |
|
shc.conContext.protocolVersion = shc.negotiatedProtocol; |
|
shc.conContext.outputRecord.setVersion(shc.negotiatedProtocol); |
|
} |
|
|
|
// update the responders |
|
// |
|
|
|
shc.handshakeProducers.put(SSLHandshake.SERVER_HELLO.id, |
|
SSLHandshake.SERVER_HELLO); |
|
|
|
// |
|
// produce |
|
|
|
SSLHandshake[] probableHandshakeMessages = new SSLHandshake[] { |
|
SSLHandshake.SERVER_HELLO, |
|
|
|
|
|
SSLHandshake.CERTIFICATE, |
|
SSLHandshake.CERTIFICATE_STATUS, |
|
SSLHandshake.SERVER_KEY_EXCHANGE, |
|
SSLHandshake.CERTIFICATE_REQUEST, |
|
SSLHandshake.SERVER_HELLO_DONE, |
|
|
|
|
|
SSLHandshake.FINISHED |
|
}; |
|
|
|
for (SSLHandshake hs : probableHandshakeMessages) { |
|
HandshakeProducer handshakeProducer = |
|
shc.handshakeProducers.remove(hs.id); |
|
if (handshakeProducer != null) { |
|
handshakeProducer.produce(context, clientHello); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
*/ |
|
private static final |
|
class D13ClientHelloConsumer implements HandshakeConsumer { |
|
|
|
private D13ClientHelloConsumer() { |
|
// blank |
|
} |
|
|
|
@Override |
|
public void consume(ConnectionContext context, |
|
HandshakeMessage message) throws IOException { |
|
throw new UnsupportedOperationException("Not supported yet."); |
|
} |
|
} |
|
} |