diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsClient.java b/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsClient.java index 980a9678ac..f8766118dd 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsClient.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/AbstractTlsClient.java @@ -86,12 +86,14 @@ public Hashtable getClientExtensions() */ if (TlsUtils.isSignatureAlgorithmsExtensionAllowed(clientVersion)) { - // TODO Provide a way for the user to specify the acceptable hash/signature algorithms. + // TODO Provide a way for the user to specify the acceptable + // hash/signature algorithms. short[] hashAlgorithms = new short[]{ HashAlgorithm.sha512, HashAlgorithm.sha384, HashAlgorithm.sha256, - HashAlgorithm.sha224, HashAlgorithm.sha1 }; + HashAlgorithm.sha224, HashAlgorithm.sha1 }; - // TODO Sort out ECDSA signatures and add them as the preferred option here + // TODO Sort out ECDSA signatures and add them as the preferred + // option here short[] signatureAlgorithms = new short[]{ SignatureAlgorithm.rsa }; this.supportedSignatureAlgorithms = new Vector(); @@ -99,16 +101,16 @@ public Hashtable getClientExtensions() { for (int j = 0; j < signatureAlgorithms.length; ++j) { - this.supportedSignatureAlgorithms.addElement(new SignatureAndHashAlgorithm(hashAlgorithms[i], - signatureAlgorithms[j])); + this.supportedSignatureAlgorithms + .addElement(new SignatureAndHashAlgorithm(hashAlgorithms[i], signatureAlgorithms[j])); } } /* * RFC 5264 7.4.3. Currently, DSA [DSS] may only be used with SHA-1. */ - this.supportedSignatureAlgorithms.addElement(new SignatureAndHashAlgorithm(HashAlgorithm.sha1, - SignatureAlgorithm.dsa)); + this.supportedSignatureAlgorithms + .addElement(new SignatureAndHashAlgorithm(HashAlgorithm.sha1, SignatureAlgorithm.dsa)); clientExtensions = TlsExtensionsUtils.ensureExtensionsInitialised(clientExtensions); @@ -129,7 +131,7 @@ public Hashtable getClientExtensions() */ this.namedCurves = new int[]{ NamedCurve.secp256r1, NamedCurve.secp384r1 }; this.clientECPointFormats = new short[]{ ECPointFormat.uncompressed, - ECPointFormat.ansiX962_compressed_prime, ECPointFormat.ansiX962_compressed_char2, }; + ECPointFormat.ansiX962_compressed_prime, ECPointFormat.ansiX962_compressed_char2, }; clientExtensions = TlsExtensionsUtils.ensureExtensionsInitialised(clientExtensions); @@ -156,7 +158,7 @@ public void notifyServerVersion(ProtocolVersion serverVersion) public short[] getCompressionMethods() { - return new short[]{CompressionMethod._null}; + return new short[]{ CompressionMethod._null }; } public void notifySessionID(byte[] sessionID) @@ -238,8 +240,18 @@ public TlsCompression getCompression() } } - public void notifyNewSessionTicket(NewSessionTicket newSessionTicket) + public void notifyNewSessionTicket(NewSessionTicket newSessionTicket, SecurityParameters securityParameters) throws IOException { } + + public NewSessionTicket getNewSessionTicket() + { + return null; + } + + public SecurityParameters getSecurityParameters() + { + 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 fd26555670..21260655d3 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/DTLSClientProtocol.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/DTLSClientProtocol.java @@ -602,8 +602,15 @@ protected void processNewSessionTicket(ClientHandshakeState state, byte[] body) NewSessionTicket newSessionTicket = NewSessionTicket.parse(buf); TlsProtocol.assertEmpty(buf); - - state.client.notifyNewSessionTicket(newSessionTicket); + + /* + * RFC 5077 - notify client so it can save the ticket and security + * parameters for session resumption. + */ + SecurityParameters securityParameters = new SecurityParameters(); + securityParameters.copySecurityParametersFrom(state.clientContext.getSecurityParameters()); + + state.client.notifyNewSessionTicket(newSessionTicket, securityParameters); } protected Certificate processServerCertificate(ClientHandshakeState state, byte[] body) diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/NewSessionTicket.java b/core/src/main/java/org/bouncycastle/crypto/tls/NewSessionTicket.java index 8f87a65ee8..e83f190b9e 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/NewSessionTicket.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/NewSessionTicket.java @@ -37,6 +37,13 @@ public void encode(OutputStream output) TlsUtils.writeUint32(ticketLifetimeHint, output); TlsUtils.writeOpaque16(ticket, output); } + + public void encodeWithoutLifetime(OutputStream output) throws IOException + { + // just raw ticket bytes + // length will be added later + output.write(ticket); + } /** * Parse a {@link NewSessionTicket} from an {@link InputStream}. diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/SecurityParameters.java b/core/src/main/java/org/bouncycastle/crypto/tls/SecurityParameters.java index 5241144ee6..fb8062b726 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/SecurityParameters.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/SecurityParameters.java @@ -19,6 +19,24 @@ public class SecurityParameters boolean truncatedHMac = false; boolean encryptThenMAC = false; boolean extendedMasterSecret = false; + + /** + * Copies the security parameters from another instance if it is not null, + * otherwise this is a no-op. + * + * @param other + */ + void copySecurityParametersFrom(SecurityParameters other) + { + if (other != null) { + this.entity = other.entity; + this.cipherSuite = other.cipherSuite; + this.compressionAlgorithm = other.compressionAlgorithm; + this.prfAlgorithm = other.prfAlgorithm; + this.verifyDataLength = other.verifyDataLength; + this.masterSecret = Arrays.clone(other.masterSecret); + } + } void clear() { diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/TlsClient.java b/core/src/main/java/org/bouncycastle/crypto/tls/TlsClient.java index da688b047a..9b57473bf2 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsClient.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsClient.java @@ -73,9 +73,27 @@ Vector getClientSupplementalData() * ticket is opaque to the client and clients MUST NOT examine the ticket under the assumption * that it complies with e.g. RFC 5077 4. Recommended Ticket Construction. * - * @param newSessionTicket The ticket. + * @param newSessionTicket + * The ticket. + * @param sessionParameters * @throws IOException */ - void notifyNewSessionTicket(NewSessionTicket newSessionTicket) + void notifyNewSessionTicket(NewSessionTicket newSessionTicket, SecurityParameters securityParameters) throws IOException; + + /** + * + * @return a {@link NewSessionTicket} + */ + NewSessionTicket getNewSessionTicket(); + + /** + * In the case of TLS resumption using session tickets, {@link TlsClient#getSessionToResume()} + * may return a null TlsSession. Use this method to retrieve the security parameters needed for + * session resumption. + * + * @return A {@link SecurityParameters} object + * @throws IOException + */ + SecurityParameters getSecurityParameters(); } 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 9bf6803ffc..9fd8ce4bb0 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsClientProtocol.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsClientProtocol.java @@ -1,6 +1,7 @@ package org.bouncycastle.crypto.tls; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -252,7 +253,31 @@ protected void handleHandshakeMessage(short type, byte[] data) if (this.resumedSession) { - this.securityParameters.masterSecret = Arrays.clone(this.sessionParameters.getMasterSecret()); + /* + * RFC 5077 - The master secret to use depends on whether + * resumed session using session id or ticket. + * + * If this is resumed session, we expect either session + * parameters to be available or the client to provide us + * with security parameters. If neither gives us master + * secret, we raise an alert. + */ + + if (this.sessionParameters != null) + { + this.securityParameters.masterSecret = Arrays.clone(this.sessionParameters.getMasterSecret()); + } + else if (this.tlsClient.getNewSessionTicket() != null + && this.tlsClient.getSecurityParameters() != null) + { + this.securityParameters.masterSecret = Arrays.clone(this.tlsClient.getSecurityParameters() + .getMasterSecret()); + } + else + { + throw new TlsFatalAlert(AlertDescription.handshake_failure); + } + this.recordStream.setPendingConnectionState(getPeer().getCompression(), getPeer().getCipher()); sendChangeCipherSpecMessage(); @@ -577,7 +602,13 @@ protected void receiveNewSessionTicketMessage(ByteArrayInputStream buf) assertEmpty(buf); - tlsClient.notifyNewSessionTicket(newSessionTicket); + /* + * RFC 5077 - notify client so it can save the ticket and security + * parameters for session resumption. + */ + SecurityParameters securityParameters = new SecurityParameters(); + securityParameters.copySecurityParametersFrom(this.securityParameters); + tlsClient.notifyNewSessionTicket(newSessionTicket, securityParameters); } protected void receiveServerHelloMessage(ByteArrayInputStream buf) @@ -617,10 +648,12 @@ protected void receiveServerHelloMessage(ByteArrayInputStream buf) } this.tlsClient.notifySessionID(this.selectedSessionID); - - this.resumedSession = this.selectedSessionID.length > 0 && this.tlsSession != null - && Arrays.areEqual(this.selectedSessionID, this.tlsSession.getSessionID()); - + + /* + * RFC 5077 - can resume either using session id or using + * NewSessionTicket + */ + this.resumedSession = canResumeUsingSessionId() || canResumeUsingNewSessionTicket(); /* * Find out which CipherSuite the server has chosen and check that it was one of the offered * ones, and is a valid selection for the negotiated version. @@ -765,14 +798,49 @@ protected void receiveServerHelloMessage(ByteArrayInputStream buf) Hashtable sessionClientExtensions = clientExtensions, sessionServerExtensions = serverExtensions; if (this.resumedSession) { - if (selectedCipherSuite != this.sessionParameters.getCipherSuite() - || selectedCompressionMethod != this.sessionParameters.getCompressionAlgorithm()) + /* + * RFC 5077 - We will have session parameters only if there is a TLS + * session (with id). In the case of session tickets, there is no + * TLS session. So the check to ensure whether the selected cipher + * suite and compression algorithm match with expected ones depends + * on whether session resumption using session id or ticket is being + * done. In the case of session id, expected cipher suite and + * compression algorithm are from the session parameters. In the + * case of session ticket, they are from the security parameters + * (expected to be stored in the client). + */ + + int expectedCipherSuite = -1; + short expectedCompressionAlgorithm = -1; + + if (this.selectedSessionID != null && this.selectedSessionID.length > 0) + { + expectedCipherSuite = this.sessionParameters.getCipherSuite(); + expectedCompressionAlgorithm = this.sessionParameters.getCompressionAlgorithm(); + } + else if (this.tlsClient.getNewSessionTicket() != null && this.tlsClient.getSecurityParameters() != null) + { + expectedCipherSuite = this.tlsClient.getSecurityParameters().getCipherSuite(); + expectedCompressionAlgorithm = this.tlsClient.getSecurityParameters().getCompressionAlgorithm(); + } + else + { + throw new TlsFatalAlert(AlertDescription.handshake_failure); + } + + if (selectedCipherSuite != expectedCipherSuite || selectedCompressionMethod != expectedCompressionAlgorithm) { throw new TlsFatalAlert(AlertDescription.illegal_parameter); } sessionClientExtensions = null; - sessionServerExtensions = this.sessionParameters.readServerExtensions(); + + // RFC 5077 - this.sessionParameters can be null if resuming using + // session tickets + if (this.sessionParameters != null) + { + sessionServerExtensions = this.sessionParameters.readServerExtensions(); + } this.securityParameters.extendedMasterSecret = TlsExtensionsUtils.hasExtendedMasterSecretExtension(sessionServerExtensions); } @@ -919,6 +987,23 @@ protected void sendClientHelloMessage() } TlsUtils.writeUint8ArrayWithUint8Length(offeredCompressionMethods, message); + + /* + * RFC 5077 - If the client supports session ticket extension and it has + * a ticket, then put the ticket in the client hello. + */ + byte[] sessionTicketExtData = TlsUtils.getExtensionData(clientExtensions, EXT_SessionTicket); + NewSessionTicket sessionTicket = tlsClient.getNewSessionTicket(); + + boolean sessionTicketExtSupported = sessionTicketExtData != null; + boolean sessionTicketPresent = sessionTicket != null; + + if (sessionTicketExtSupported && sessionTicketPresent) + { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + sessionTicket.encodeWithoutLifetime(output); + clientExtensions.put(EXT_SessionTicket, output.toByteArray()); + } if (clientExtensions != null) { @@ -937,4 +1022,15 @@ protected void sendClientKeyExchangeMessage() message.writeToRecordStream(); } + + private boolean canResumeUsingSessionId() + { + return this.selectedSessionID.length > 0 && this.tlsSession != null + && Arrays.areEqual(this.selectedSessionID, this.tlsSession.getSessionID()); + } + + private boolean canResumeUsingNewSessionTicket() + { + return this.tlsClient.getNewSessionTicket() != null; + } } diff --git a/core/src/main/java/org/bouncycastle/crypto/tls/TlsExtensionsUtils.java b/core/src/main/java/org/bouncycastle/crypto/tls/TlsExtensionsUtils.java index 8e50f57d5f..1d6dc7b80e 100644 --- a/core/src/main/java/org/bouncycastle/crypto/tls/TlsExtensionsUtils.java +++ b/core/src/main/java/org/bouncycastle/crypto/tls/TlsExtensionsUtils.java @@ -16,6 +16,7 @@ public class TlsExtensionsUtils public static final Integer EXT_server_name = Integers.valueOf(ExtensionType.server_name); public static final Integer EXT_status_request = Integers.valueOf(ExtensionType.status_request); public static final Integer EXT_truncated_hmac = Integers.valueOf(ExtensionType.truncated_hmac); + public static final Integer EXT_session_tickets = Integer.valueOf(ExtensionType.session_ticket); public static Hashtable ensureExtensionsInitialised(Hashtable extensions) { @@ -60,6 +61,11 @@ public static void addTruncatedHMacExtension(Hashtable extensions) { extensions.put(EXT_truncated_hmac, createTruncatedHMacExtension()); } + + public static void addSessionTicketExtension(Hashtable extensions) + { + extensions.put(EXT_session_tickets, createEmptyExtensionData()); + } public static HeartbeatExtension getHeartbeatExtension(Hashtable extensions) throws IOException diff --git a/core/src/test/java/org/bouncycastle/crypto/tls/test/MockTlsClient.java b/core/src/test/java/org/bouncycastle/crypto/tls/test/MockTlsClient.java index 3530b4a72c..2d1b727d07 100644 --- a/core/src/test/java/org/bouncycastle/crypto/tls/test/MockTlsClient.java +++ b/core/src/test/java/org/bouncycastle/crypto/tls/test/MockTlsClient.java @@ -12,7 +12,9 @@ import org.bouncycastle.crypto.tls.ClientCertificateType; import org.bouncycastle.crypto.tls.DefaultTlsClient; import org.bouncycastle.crypto.tls.MaxFragmentLength; +import org.bouncycastle.crypto.tls.NewSessionTicket; import org.bouncycastle.crypto.tls.ProtocolVersion; +import org.bouncycastle.crypto.tls.SecurityParameters; import org.bouncycastle.crypto.tls.SignatureAlgorithm; import org.bouncycastle.crypto.tls.SignatureAndHashAlgorithm; import org.bouncycastle.crypto.tls.TlsAuthentication; @@ -26,11 +28,19 @@ class MockTlsClient extends DefaultTlsClient { TlsSession session; + NewSessionTicket sessionTicket; + SecurityParameters securityParameters; MockTlsClient(TlsSession session) { this.session = session; } + + MockTlsClient(NewSessionTicket sessionTicket, SecurityParameters securityParameters) + { + this.sessionTicket = sessionTicket; + this.securityParameters = securityParameters; + } public TlsSession getSessionToResume() { @@ -80,6 +90,7 @@ public Hashtable getClientExtensions() throws IOException // TlsExtensionsUtils.addExtendedMasterSecretExtension(clientExtensions); TlsExtensionsUtils.addMaxFragmentLengthExtension(clientExtensions, MaxFragmentLength.pow2_9); TlsExtensionsUtils.addTruncatedHMacExtension(clientExtensions); + TlsExtensionsUtils.addSessionTicketExtension(clientExtensions); return clientExtensions; } @@ -167,4 +178,22 @@ public void notifyHandshakeComplete() throws IOException this.session = newSession; } } + + public void notifyNewSessionTicket(NewSessionTicket newSessionTicket, SecurityParameters securityParameters) + throws IOException + { + super.notifyNewSessionTicket(newSessionTicket, securityParameters); + this.sessionTicket = newSessionTicket; + this.securityParameters = securityParameters; + } + + public NewSessionTicket getNewSessionTicket() + { + return sessionTicket; + } + + public SecurityParameters getSecurityParameters() + { + return this.securityParameters; + } } diff --git a/core/src/test/java/org/bouncycastle/crypto/tls/test/TlsClientTest.java b/core/src/test/java/org/bouncycastle/crypto/tls/test/TlsClientTest.java index 736913e618..3a897d822d 100644 --- a/core/src/test/java/org/bouncycastle/crypto/tls/test/TlsClientTest.java +++ b/core/src/test/java/org/bouncycastle/crypto/tls/test/TlsClientTest.java @@ -37,6 +37,7 @@ public static void main(String[] args) long time2 = System.currentTimeMillis(); System.out.println("Elapsed 1: " + (time2 - time1) + "ms"); + // session resumption using session id client = new MockTlsClient(client.getSessionToResume()); protocol = openTlsConnection(address, port, client); @@ -57,6 +58,12 @@ public static void main(String[] args) } protocol.close(); + + // session resumption using session tickets + client = new MockTlsClient(client.getNewSessionTicket(), client.getSecurityParameters()); + protocol = openTlsConnection(address, port, client); + protocol.close(); + } static TlsClientProtocol openTlsConnection(InetAddress address, int port, TlsClient client) throws IOException