diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsServer.java b/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsServer.java index 0e22993a25..5b4cbca25a 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsServer.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsServer.java @@ -368,4 +368,22 @@ public NewSessionTicket getNewSessionTicket() */ return new NewSessionTicket(0L, TlsUtils.EMPTY_BYTES); } + + + /* + * The next two methods take care about server session handling / resumption. If you need + * this, you have to overwrite the next two methods with your own implementation taking + * care about session caching and resumption + */ + public TlsSession getResumableSession(byte[] sessionID) + throws IOException + { + return null; + } + + public TlsSession getNewResumableSession(byte[] requestedClientSessionID) + throws IOException + { + return null; + } } diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/DTLSClientProtocol.java b/core/src/main/java/org/bouncycastle/crypto/tls/DTLSClientProtocol.java index 4f7f1edd6c..fc243e7aea 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/DTLSClientProtocol.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/DTLSClientProtocol.java @@ -372,7 +372,7 @@ protected DTLSTransport clientHandshake(ClientHandshakeState state, DTLSRecordLa .setPSKIdentity(securityParameters.getPSKIdentity()) .setSRPIdentity(securityParameters.getSRPIdentity()) // TODO Consider filtering extensions that aren't relevant to resumed sessions - .setServerExtensions(state.serverExtensions) + .setPeerExtensions(state.serverExtensions) .build(); state.tlsSession = TlsUtils.importSession(state.tlsSession.getSessionID(), state.sessionParameters); @@ -759,7 +759,7 @@ protected void processServerHello(ClientHandshakeState state, byte[] body) } sessionClientExtensions = null; - sessionServerExtensions = state.sessionParameters.readServerExtensions(); + sessionServerExtensions = state.sessionParameters.readPeerExtensions(); } securityParameters.cipherSuite = selectedCipherSuite; diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/SessionParameters.java b/core/src/main/java/org/bouncycastle/crypto/tls/SessionParameters.java index 983df60bf0..4846147464 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/SessionParameters.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/SessionParameters.java @@ -17,7 +17,7 @@ public static final class Builder private Certificate peerCertificate = null; private byte[] pskIdentity = null; private byte[] srpIdentity = null; - private byte[] encodedServerExtensions = null; + private byte[] encodedPeerExtensions = null; public Builder() { @@ -29,7 +29,7 @@ public SessionParameters build() validate(this.compressionAlgorithm >= 0, "compressionAlgorithm"); validate(this.masterSecret != null, "masterSecret"); return new SessionParameters(cipherSuite, compressionAlgorithm, masterSecret, peerCertificate, pskIdentity, - srpIdentity, encodedServerExtensions); + srpIdentity, encodedPeerExtensions); } public Builder setCipherSuite(int cipherSuite) @@ -77,17 +77,17 @@ public Builder setSRPIdentity(byte[] srpIdentity) return this; } - public Builder setServerExtensions(Hashtable serverExtensions) throws IOException + public Builder setPeerExtensions(Hashtable peerExtensions) throws IOException { - if (serverExtensions == null) + if (peerExtensions == null) { - encodedServerExtensions = null; + encodedPeerExtensions = null; } else { ByteArrayOutputStream buf = new ByteArrayOutputStream(); - TlsProtocol.writeExtensions(buf, serverExtensions); - encodedServerExtensions = buf.toByteArray(); + TlsProtocol.writeExtensions(buf, peerExtensions); + encodedPeerExtensions = buf.toByteArray(); } return this; } @@ -107,10 +107,10 @@ private void validate(boolean condition, String parameter) private Certificate peerCertificate; private byte[] pskIdentity = null; private byte[] srpIdentity = null; - private byte[] encodedServerExtensions; + private byte[] encodedPeerExtensions; private SessionParameters(int cipherSuite, short compressionAlgorithm, byte[] masterSecret, - Certificate peerCertificate, byte[] pskIdentity, byte[] srpIdentity, byte[] encodedServerExtensions) + Certificate peerCertificate, byte[] pskIdentity, byte[] srpIdentity, byte[] encodedPeerExtensions) { this.cipherSuite = cipherSuite; this.compressionAlgorithm = compressionAlgorithm; @@ -118,7 +118,7 @@ private SessionParameters(int cipherSuite, short compressionAlgorithm, byte[] ma this.peerCertificate = peerCertificate; this.pskIdentity = Arrays.clone(pskIdentity); this.srpIdentity = Arrays.clone(srpIdentity); - this.encodedServerExtensions = encodedServerExtensions; + this.encodedPeerExtensions = encodedPeerExtensions; } public void clear() @@ -132,7 +132,7 @@ public void clear() public SessionParameters copy() { return new SessionParameters(cipherSuite, compressionAlgorithm, masterSecret, peerCertificate, pskIdentity, - srpIdentity, encodedServerExtensions); + srpIdentity, encodedPeerExtensions); } public int getCipherSuite() @@ -173,14 +173,14 @@ public byte[] getSRPIdentity() return srpIdentity; } - public Hashtable readServerExtensions() throws IOException + public Hashtable readPeerExtensions() throws IOException { - if (encodedServerExtensions == null) + if (encodedPeerExtensions == null) { return null; } - ByteArrayInputStream buf = new ByteArrayInputStream(encodedServerExtensions); + ByteArrayInputStream buf = new ByteArrayInputStream(encodedPeerExtensions); return TlsProtocol.readExtensions(buf); } } diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/TlsClientProtocol.java b/core/src/main/java/org/bouncycastle/crypto/tls/TlsClientProtocol.java index 2b44cea6fa..50d541895a 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsClientProtocol.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsClientProtocol.java @@ -753,7 +753,7 @@ protected void receiveServerHelloMessage(ByteArrayInputStream buf) } sessionClientExtensions = null; - sessionServerExtensions = this.sessionParameters.readServerExtensions(); + sessionServerExtensions = this.sessionParameters.readPeerExtensions(); } this.securityParameters.cipherSuite = selectedCipherSuite; diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/TlsProtocol.java b/core/src/main/java/org/bouncycastle/crypto/tls/TlsProtocol.java index 36fc14c152..b838e97891 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsProtocol.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsProtocol.java @@ -307,6 +307,7 @@ protected void completeHandshake() { if (this.sessionParameters == null) { + boolean server = getContext().isServer(); this.sessionParameters = new SessionParameters.Builder() .setCipherSuite(this.securityParameters.getCipherSuite()) .setCompressionAlgorithm(this.securityParameters.getCompressionAlgorithm()) @@ -315,7 +316,7 @@ protected void completeHandshake() .setPSKIdentity(this.securityParameters.getPSKIdentity()) .setSRPIdentity(this.securityParameters.getSRPIdentity()) // TODO Consider filtering extensions that aren't relevant to resumed sessions - .setServerExtensions(this.serverExtensions) + .setPeerExtensions(server ? this.clientExtensions : this.serverExtensions) .build(); this.tlsSession = new TlsSessionImpl(this.tlsSession.getSessionID(), this.sessionParameters); diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/TlsServer.java b/core/src/main/java/org/bouncycastle/crypto/tls/TlsServer.java index 5e5883aef7..ad534b07a9 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsServer.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsServer.java @@ -92,4 +92,10 @@ void notifyClientCertificate(Certificate clientCertificate) */ NewSessionTicket getNewSessionTicket() throws IOException; + + TlsSession getResumableSession(byte[] sessionID) + throws IOException; + + TlsSession getNewResumableSession(byte[] requestedClientSessionID) + throws IOException; } diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/TlsServerProtocol.java b/core/src/main/java/org/bouncycastle/crypto/tls/TlsServerProtocol.java index f1faaa06dc..b6413544d7 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsServerProtocol.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsServerProtocol.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.SecureRandom; +import java.util.Hashtable; import java.util.Vector; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; @@ -24,6 +25,7 @@ public class TlsServerProtocol protected short clientCertificateType = -1; protected TlsHandshakeHash prepareFinishHash = null; + protected byte[] requestedClientSessionID = null; /** * Constructor for blocking mode. @@ -138,6 +140,29 @@ protected void handleHandshakeMessage(short type, ByteArrayInputStream buf) recordStream.notifyHelloComplete(); + if (this.resumedSession) + { + // resume previous session - just send ChangeCiperSpec and Finished message... + + // validate if server selected the same parameters... + if (this.securityParameters.cipherSuite != this.sessionParameters.getCipherSuite() + || this.securityParameters.compressionAlgorithm != this.sessionParameters.getCompressionAlgorithm()) + { + throw new TlsFatalAlert(AlertDescription.illegal_parameter); + } + this.securityParameters.masterSecret = this.sessionParameters.getMasterSecret(); + this.securityParameters.pskIdentity = this.sessionParameters.getPSKIdentity(); + this.securityParameters.srpIdentity = this.sessionParameters.getSRPIdentity(); + this.peerCertificate = this.sessionParameters.getPeerCertificate(); + recordStream.setPendingConnectionState(getPeer().getCompression(), getPeer().getCipher()); + + sendChangeCipherSpecMessage(); + sendFinishedMessage(); + this.connection_state = CS_SERVER_FINISHED; + break; + } + + // continue with new session Vector serverSupplementalData = tlsServer.getServerSupplementalData(); if (serverSupplementalData != null) { @@ -373,6 +398,17 @@ else if (TlsUtils.isSSL(getContext())) completeHandshake(); break; } + case CS_SERVER_FINISHED: + { + if (!this.resumedSession) + { + throw new TlsFatalAlert(AlertDescription.unexpected_message); + } + this.connection_state = CS_END; + + completeHandshake(); + break; + } default: throw new TlsFatalAlert(AlertDescription.unexpected_message); } @@ -546,14 +582,19 @@ protected void receiveClientHelloMessage(ByteArrayInputStream buf) * TODO RFC 5077 3.4. If a ticket is presented by the client, the server MUST NOT attempt to * use the Session ID in the ClientHello for stateful session resumption. */ - byte[] sessionID = TlsUtils.readOpaque8(buf); - if (sessionID.length > 32) + requestedClientSessionID = TlsUtils.readOpaque8(buf); + if (requestedClientSessionID.length > 32) { throw new TlsFatalAlert(AlertDescription.illegal_parameter); } + this.tlsSession = tlsServer.getResumableSession(requestedClientSessionID); + if (this.tlsSession != null && this.tlsSession.isResumable()) { + this.sessionParameters = this.tlsSession.exportSessionParameters(); + this.resumedSession = true; + } /* - * TODO RFC 5246 7.4.1.2. If the session_id field is not empty (implying a session + * RFC 5246 7.4.1.2. If the session_id field is not empty (implying a session * resumption request), this vector MUST include at least the cipher_suite from that * session. */ @@ -563,9 +604,16 @@ protected void receiveClientHelloMessage(ByteArrayInputStream buf) throw new TlsFatalAlert(AlertDescription.decode_error); } this.offeredCipherSuites = TlsUtils.readUint16Array(cipher_suites_length / 2, buf); + if (this.resumedSession) + { + if (!Arrays.contains(offeredCipherSuites, this.sessionParameters.getCipherSuite())) + { + throw new TlsFatalAlert(AlertDescription.illegal_parameter); + } + } /* - * TODO RFC 5246 7.4.1.2. If the session_id field is not empty (implying a session + * RFC 5246 7.4.1.2. If the session_id field is not empty (implying a session * resumption request), it MUST include the compression_method from that session. */ int compression_methods_length = TlsUtils.readUint8(buf); @@ -574,13 +622,54 @@ protected void receiveClientHelloMessage(ByteArrayInputStream buf) throw new TlsFatalAlert(AlertDescription.illegal_parameter); } this.offeredCompressionMethods = TlsUtils.readUint8Array(compression_methods_length, buf); + if (this.resumedSession) + { + if (!Arrays.contains(offeredCompressionMethods, this.sessionParameters.getCompressionAlgorithm())) + { + throw new TlsFatalAlert(AlertDescription.illegal_parameter); + } + } /* - * TODO RFC 3546 2.3 If [...] the older session is resumed, then the server MUST ignore + * RFC 3546 2.3 If [...] the older session is resumed, then the server MUST ignore * extensions appearing in the client hello, and send a server hello containing no * extensions. */ - this.clientExtensions = readExtensions(buf); + if (this.resumedSession) + { + Hashtable newClientExtensions = readExtensions(buf); + this.clientExtensions = this.sessionParameters.readPeerExtensions(); + /* + * RFC 3546 indicates we MUST ignore extensions, but RFC 7627 Section 5.3 says: + * o If the original session did not use the "extended_master_secret" + * extension but the new ClientHello contains the extension, then the + * server MUST NOT perform the abbreviated handshake. Instead, it + * SHOULD continue with a full handshake (as described in + * Section 5.2) to negotiate a new session. + * + * o If the original session used the "extended_master_secret" + * extension but the new ClientHello does not contain it, the server + * MUST abort the abbreviated handshake. + */ + boolean prevHasEMS = TlsExtensionsUtils.hasExtendedMasterSecretExtension(clientExtensions); + boolean curHasEMS = TlsExtensionsUtils.hasExtendedMasterSecretExtension(newClientExtensions); + if (curHasEMS != prevHasEMS) { + if (!prevHasEMS) { + /* + * This is the case where we SHOULD continue with a full handshake... + */ + this.sessionParameters = null; + this.tlsSession = null; + this.resumedSession = false; + this.clientExtensions = newClientExtensions; + } else + throw new TlsFatalAlert(AlertDescription.illegal_parameter); + } + } + else + { + this.clientExtensions = readExtensions(buf); + } /* * TODO[session-hash] @@ -732,12 +821,24 @@ protected void sendServerHelloMessage() message.write(this.securityParameters.serverRandom); - /* - * The server may return an empty session_id to indicate that the session will not be cached - * and therefore cannot be resumed. - */ - TlsUtils.writeOpaque8(TlsUtils.EMPTY_BYTES, message); + if (!this.resumedSession) + { + tlsSession = tlsServer.getNewResumableSession(requestedClientSessionID); + } + if (tlsSession != null) + { + TlsUtils.writeOpaque8(tlsSession.getSessionID(), message); + } + else + { + /* + * The server may return an empty session_id to indicate that the session will not be cached + * and therefore cannot be resumed. + */ + + TlsUtils.writeOpaque8(TlsUtils.EMPTY_BYTES, message); + } int selectedCipherSuite = tlsServer.getSelectedCipherSuite(); if (!Arrays.contains(offeredCipherSuites, selectedCipherSuite) || selectedCipherSuite == CipherSuite.TLS_NULL_WITH_NULL_NULL @@ -758,7 +859,25 @@ protected void sendServerHelloMessage() TlsUtils.writeUint16(selectedCipherSuite, message); TlsUtils.writeUint8(selectedCompressionMethod, message); - this.serverExtensions = tlsServer.getServerExtensions(); + securityParameters.prfAlgorithm = getPRFAlgorithm(getContext(), securityParameters.getCipherSuite()); + + /* + * RFC 5246 7.4.9. Any cipher suite which does not explicitly specify verify_data_length has + * a verify_data_length equal to 12. This includes all existing cipher suites. + */ + securityParameters.verifyDataLength = 12; + + /* + * RFC 3546 2.3 If [...] the older session is resumed, then the server MUST ignore + * extensions appearing in the client hello, and send a server hello containing no + * extensions. + * + * The server hello containing no extensions is not entirely true. RFC 5746 explicitly + * states out in 3.6. Server Behavior that the secure_renegotiation extensions is also + * sent during session resumption. So we need to handle this part. + */ + if (!this.resumedSession) + this.serverExtensions = tlsServer.getServerExtensions(); /* * RFC 5746 3.6. Server Behavior: Initial Handshake @@ -793,12 +912,6 @@ protected void sendServerHelloMessage() TlsExtensionsUtils.addExtendedMasterSecretExtension(serverExtensions); } - /* - * TODO RFC 3546 2.3 If [...] the older session is resumed, then the server MUST ignore - * extensions appearing in the client hello, and send a server hello containing no - * extensions. - */ - if (this.serverExtensions != null) { this.securityParameters.encryptThenMAC = TlsExtensionsUtils.hasEncryptThenMACExtension(serverExtensions); @@ -823,14 +936,6 @@ protected void sendServerHelloMessage() writeExtensions(message, serverExtensions); } - securityParameters.prfAlgorithm = getPRFAlgorithm(getContext(), securityParameters.getCipherSuite()); - - /* - * RFC 5246 7.4.9. Any cipher suite which does not explicitly specify verify_data_length has - * a verify_data_length equal to 12. This includes all existing cipher suites. - */ - securityParameters.verifyDataLength = 12; - applyMaxFragmentLengthExtension(); message.writeToRecordStream();