package org.bouncycastle.tls;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import org.bouncycastle.tls.crypto.TlsStreamSigner;
import org.bouncycastle.util.Arrays;

public class TlsClientProtocol
    extends TlsProtocol
{
    protected TlsClient tlsClient = null;
    TlsClientContextImpl tlsClientContext = null;

    protected byte[] selectedSessionID = null;

    protected TlsKeyExchange keyExchange = null;
    protected TlsAuthentication authentication = null;

    protected CertificateStatus certificateStatus = null;
    protected CertificateRequest certificateRequest = null;

    /**
     * Constructor for non-blocking mode.<br>
     * <br>
     * When data is received, use {@link #offerInput(byte[])} to provide the received ciphertext,
     * then use {@link #readInput(byte[], int, int)} to read the corresponding cleartext.<br>
     * <br>
     * Similarly, when data needs to be sent, use {@link #writeApplicationData(byte[], int, int)} to
     * provide the cleartext, then use {@link #readOutput(byte[], int, int)} to get the
     * corresponding ciphertext.
     */
    public TlsClientProtocol()
    {
        super();
    }

    /**
     * Constructor for blocking mode.
     * @param input The stream of data from the server
     * @param output The stream of data to the server
     */
    public TlsClientProtocol(InputStream input, OutputStream output)
    {
        super(input, output);
    }

    /**
     * Initiates a TLS handshake in the role of client.<br>
     * <br>
     * In blocking mode, this will not return until the handshake is complete.
     * In non-blocking mode, use {@link TlsPeer#notifyHandshakeComplete()} to
     * receive a callback when the handshake is complete.
     *
     * @param tlsClient The {@link TlsClient} to use for the handshake.
     * @throws IOException If in blocking mode and handshake was not successful.
     */
    public void connect(TlsClient tlsClient) throws IOException
    {
        if (tlsClient == null)
        {
            throw new IllegalArgumentException("'tlsClient' cannot be null");
        }
        if (this.tlsClient != null)
        {
            throw new IllegalStateException("'connect' can only be called once");
        }

        this.tlsClient = tlsClient;

        this.securityParameters = new SecurityParameters();
        this.securityParameters.entity = ConnectionEnd.client;

        this.tlsClientContext = new TlsClientContextImpl(tlsClient.getCrypto(), securityParameters);

        this.securityParameters.clientRandom = createRandomBlock(tlsClient.shouldUseGMTUnixTime(), tlsClientContext);
        this.securityParameters.extendedPadding = tlsClient.shouldUseExtendedPadding();

        this.tlsClient.init(tlsClientContext);
        this.recordStream.init(tlsClientContext);

        TlsSession sessionToResume = tlsClient.getSessionToResume();
        if (sessionToResume != null && sessionToResume.isResumable())
        {
            SessionParameters sessionParameters = sessionToResume.exportSessionParameters();
            if (sessionParameters != null)
            {
                this.tlsSession = sessionToResume;
                this.sessionParameters = sessionParameters;
            }
        }

        sendClientHelloMessage();
        this.connection_state = CS_CLIENT_HELLO;

        if (blocking)
        {
            blockForHandshake();
        }
    }

    protected void cleanupHandshake()
    {
        super.cleanupHandshake();

        this.selectedSessionID = null;
        this.keyExchange = null;
        this.authentication = null;
        this.certificateStatus = null;
        this.certificateRequest = null;
    }

    protected TlsContext getContext()
    {
        return tlsClientContext;
    }

    AbstractTlsContext getContextAdmin()
    {
        return tlsClientContext;
    }
    
    protected TlsPeer getPeer()
    {
        return tlsClient;
    }

    protected void handleHandshakeMessage(short type, ByteArrayInputStream buf)
        throws IOException
    {
        if (this.resumedSession)
        {
            if (type != HandshakeType.finished || this.connection_state != CS_SERVER_HELLO)
            {
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }

            processFinishedMessage(buf);
            this.connection_state = CS_SERVER_FINISHED;

            sendChangeCipherSpecMessage();
            sendFinishedMessage();
            this.connection_state = CS_CLIENT_FINISHED;

            completeHandshake();
            return;
        }

        switch (type)
        {
        case HandshakeType.certificate:
        {
            switch (this.connection_state)
            {
            case CS_SERVER_HELLO:
            {
                handleSupplementalData(null);
                // NB: Fall through to next case label
            }
            case CS_SERVER_SUPPLEMENTAL_DATA:
            {
                // Parse the Certificate message and send to cipher suite

                ByteArrayOutputStream endPointHash = new ByteArrayOutputStream();

                this.peerCertificate = Certificate.parse(getContext(), buf, endPointHash);

                assertEmpty(buf);

                securityParameters.tlsServerEndPoint = endPointHash.toByteArray();

                // TODO[RFC 3546] Check whether empty certificates is possible, allowed, or excludes CertificateStatus
                if (this.peerCertificate == null || this.peerCertificate.isEmpty())
                {
                    this.allowCertificateStatus = false;
                }

                this.authentication = tlsClient.getAuthentication();
                if (this.authentication == null)
                {
                    throw new TlsFatalAlert(AlertDescription.internal_error);
                }

                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }

            this.connection_state = CS_SERVER_CERTIFICATE;
            break;
        }
        case HandshakeType.certificate_status:
        {
            switch (this.connection_state)
            {
            case CS_SERVER_CERTIFICATE:
            {
                if (!this.allowCertificateStatus)
                {
                    /*
                     * RFC 3546 3.6. If a server returns a "CertificateStatus" message, then the
                     * server MUST have included an extension of type "status_request" with empty
                     * "extension_data" in the extended server hello..
                     */
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                this.certificateStatus = CertificateStatus.parse(buf);

                assertEmpty(buf);

                this.connection_state = CS_CERTIFICATE_STATUS;
                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }
            break;
        }
        case HandshakeType.finished:
        {
            switch (this.connection_state)
            {
            case CS_CLIENT_FINISHED:
            {
                if (this.expectSessionTicket)
                {
                    /*
                     * RFC 5077 3.3. This message MUST be sent if the server included a
                     * SessionTicket extension in the ServerHello.
                     */
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                // NB: Fall through to next case label
            }
            case CS_SERVER_SESSION_TICKET:
            {
                processFinishedMessage(buf);
                this.connection_state = CS_SERVER_FINISHED;

                completeHandshake();
                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }
            break;
        }
        case HandshakeType.server_hello:
        {
            switch (this.connection_state)
            {
            case CS_CLIENT_HELLO:
            {
                receiveServerHelloMessage(buf);
                this.connection_state = CS_SERVER_HELLO;

                this.recordStream.notifyHelloComplete();

                applyMaxFragmentLengthExtension();

                if (this.resumedSession)
                {
                    this.securityParameters.masterSecret = getContext().getCrypto().adoptSecret(sessionParameters.getMasterSecret());
                    this.recordStream.setPendingConnectionState(getPeer().getCompression(), getPeer().getCipher());
                }
                else
                {
                    invalidateSession();

                    this.tlsSession = TlsUtils.importSession(this.selectedSessionID, null);
                    this.sessionParameters = null;
                }

                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }
            break;
        }
        case HandshakeType.supplemental_data:
        {
            switch (this.connection_state)
            {
            case CS_SERVER_HELLO:
            {
                handleSupplementalData(readSupplementalDataMessage(buf));
                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }
            break;
        }
        case HandshakeType.server_hello_done:
        {
            switch (this.connection_state)
            {
            case CS_SERVER_HELLO:
            {
                handleSupplementalData(null);
                // NB: Fall through to next case label
            }
            case CS_SERVER_SUPPLEMENTAL_DATA:
            {
                this.authentication = null;

                // NB: Fall through to next case label
            }
            case CS_SERVER_CERTIFICATE:
            case CS_CERTIFICATE_STATUS:
            {
                handleServerCertificate();

                // There was no server key exchange message; check it's OK
                this.keyExchange.skipServerKeyExchange();

                // NB: Fall through to next case label
            }
            case CS_SERVER_KEY_EXCHANGE:
            case CS_CERTIFICATE_REQUEST:
            {
                assertEmpty(buf);

                this.connection_state = CS_SERVER_HELLO_DONE;

                Vector clientSupplementalData = tlsClient.getClientSupplementalData();
                if (clientSupplementalData != null)
                {
                    sendSupplementalDataMessage(clientSupplementalData);
                }
                this.connection_state = CS_CLIENT_SUPPLEMENTAL_DATA;

                TlsCredentials clientCredentials = null;
                TlsCredentialedSigner credentialedSigner = null;
                TlsStreamSigner streamSigner = null;

                if (certificateRequest == null)
                {
                    this.keyExchange.skipClientCredentials();
                }
                else
                {
                    clientCredentials = validateCredentials(this.authentication.getClientCredentials(certificateRequest));

                    if (clientCredentials == null)
                    {
                        this.keyExchange.skipClientCredentials();

                        /*
                         * RFC 5246 If no suitable certificate is available, the client MUST send a
                         * certificate message containing no certificates.
                         * 
                         * NOTE: In previous RFCs, this was SHOULD instead of MUST.
                         */
                        sendCertificateMessage(Certificate.EMPTY_CHAIN, null);
                    }
                    else
                    {
                        this.keyExchange.processClientCredentials(clientCredentials);

                        sendCertificateMessage(clientCredentials.getCertificate(), null);

                        if (clientCredentials instanceof TlsCredentialedSigner)
                        {
                            credentialedSigner = (TlsCredentialedSigner)clientCredentials;
                            streamSigner = credentialedSigner.getStreamSigner();
                        }
                    }
                }

                this.connection_state = CS_CLIENT_CERTIFICATE;

                boolean forceBuffering = streamSigner != null;
                TlsUtils.sealHandshakeHash(getContext(), this.recordStream.getHandshakeHash(), forceBuffering);

                /*
                 * Send the client key exchange message, depending on the key exchange we are using
                 * in our CipherSuite.
                 */
                sendClientKeyExchangeMessage();
                this.connection_state = CS_CLIENT_KEY_EXCHANGE;

                TlsHandshakeHash prepareFinishHash = recordStream.prepareToFinish();
                this.securityParameters.sessionHash = TlsUtils.getCurrentPRFHash(prepareFinishHash);

                establishMasterSecret(getContext(), keyExchange);

                recordStream.setPendingConnectionState(getPeer().getCompression(), getPeer().getCipher());

                if (credentialedSigner != null)
                {
                    DigitallySigned certificateVerify = TlsUtils.generateCertificateVerify(getContext(),
                        credentialedSigner, streamSigner, prepareFinishHash);
                    sendCertificateVerifyMessage(certificateVerify);
                    this.connection_state = CS_CERTIFICATE_VERIFY;
                }

                sendChangeCipherSpecMessage();
                sendFinishedMessage();
                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }

            this.connection_state = CS_CLIENT_FINISHED;
            break;
        }
        case HandshakeType.server_key_exchange:
        {
            switch (this.connection_state)
            {
            case CS_SERVER_HELLO:
            {
                handleSupplementalData(null);
                // NB: Fall through to next case label
            }
            case CS_SERVER_SUPPLEMENTAL_DATA:
            {
                this.authentication = null;

                // NB: Fall through to next case label
            }
            case CS_SERVER_CERTIFICATE:
            case CS_CERTIFICATE_STATUS:
            {
                handleServerCertificate();

                this.keyExchange.processServerKeyExchange(buf);

                assertEmpty(buf);
                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }

            this.connection_state = CS_SERVER_KEY_EXCHANGE;
            break;
        }
        case HandshakeType.certificate_request:
        {
            switch (this.connection_state)
            {
            case CS_SERVER_CERTIFICATE:
            case CS_CERTIFICATE_STATUS:
            {
                handleServerCertificate();

                // There was no server key exchange message; check it's OK
                this.keyExchange.skipServerKeyExchange();

                // NB: Fall through to next case label
            }
            case CS_SERVER_KEY_EXCHANGE:
            {
                if (this.authentication == null)
                {
                    /*
                     * RFC 2246 7.4.4. It is a fatal handshake_failure alert for an anonymous server
                     * to request client identification.
                     */
                    throw new TlsFatalAlert(AlertDescription.handshake_failure);
                }

                this.certificateRequest = CertificateRequest.parse(getContext(), buf);

                assertEmpty(buf);

                this.certificateRequest = TlsUtils.validateCertificateRequest(this.certificateRequest, this.keyExchange);

                /*
                 * TODO Give the client a chance to immediately select the CertificateVerify hash
                 * algorithm here to avoid tracking the other hash algorithms unnecessarily?
                 */
                TlsUtils.trackHashAlgorithms(this.recordStream.getHandshakeHash(),
                    this.certificateRequest.getSupportedSignatureAlgorithms());

                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }

            this.connection_state = CS_CERTIFICATE_REQUEST;
            break;
        }
        case HandshakeType.session_ticket:
        {
            switch (this.connection_state)
            {
            case CS_CLIENT_FINISHED:
            {
                if (!this.expectSessionTicket)
                {
                    /*
                     * RFC 5077 3.3. This message MUST NOT be sent if the server did not include a
                     * SessionTicket extension in the ServerHello.
                     */
                    throw new TlsFatalAlert(AlertDescription.unexpected_message);
                }

                /*
                 * RFC 5077 3.4. If the client receives a session ticket from the server, then it
                 * discards any Session ID that was sent in the ServerHello.
                 */
                invalidateSession();

                receiveNewSessionTicketMessage(buf);
                break;
            }
            default:
                throw new TlsFatalAlert(AlertDescription.unexpected_message);
            }

            this.connection_state = CS_SERVER_SESSION_TICKET;
            break;
        }
        case HandshakeType.hello_request:
        {
            assertEmpty(buf);

            /*
             * RFC 2246 7.4.1.1 Hello request This message will be ignored by the client if the
             * client is currently negotiating a session. This message may be ignored by the client
             * if it does not wish to renegotiate a session, or the client may, if it wishes,
             * respond with a no_renegotiation alert.
             */
            if (this.connection_state == CS_END)
            {
                refuseRenegotiation();
            }
            break;
        }
        case HandshakeType.client_hello:
        case HandshakeType.client_key_exchange:
        case HandshakeType.certificate_verify:
        case HandshakeType.hello_verify_request:
        default:
            throw new TlsFatalAlert(AlertDescription.unexpected_message);
        }
    }

    protected void handleServerCertificate()
        throws IOException
    {
        if (this.authentication == null)
        {
            // There was no server certificate message; check it's OK
            securityParameters.tlsServerEndPoint = TlsUtils.EMPTY_BYTES;
            this.keyExchange.skipServerCredentials();
        }
        else
        {
            TlsUtils.processServerCertificate(peerCertificate, certificateStatus, keyExchange, authentication,
                clientExtensions, serverExtensions);
        }
    }

    protected void handleSupplementalData(Vector serverSupplementalData)
        throws IOException
    {
        this.tlsClient.processServerSupplementalData(serverSupplementalData);
        this.connection_state = CS_SERVER_SUPPLEMENTAL_DATA;

        this.keyExchange = tlsClient.getKeyExchange();
        this.keyExchange.init(getContext());
    }

    protected void receiveNewSessionTicketMessage(ByteArrayInputStream buf)
        throws IOException
    {
        NewSessionTicket newSessionTicket = NewSessionTicket.parse(buf);

        assertEmpty(buf);

        tlsClient.notifyNewSessionTicket(newSessionTicket);
    }

    protected void receiveServerHelloMessage(ByteArrayInputStream buf)
        throws IOException
    {
        ProtocolVersion server_version = TlsUtils.readVersion(buf);

        {
            if (!TlsUtils.isTLSv10(server_version))
            {
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            }
    
            // Check that this matches what the server is sending in the record layer
            if (!server_version.equals(this.recordStream.getReadVersion()))
            {
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            }
    
            ProtocolVersion client_version = getContext().getClientVersion();
            if (!server_version.isEqualOrEarlierVersionOf(client_version))
            {
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            }
    
            this.recordStream.setWriteVersion(server_version);
            getContextAdmin().setServerVersion(server_version);
            this.tlsClient.notifyServerVersion(server_version);
        }

        /*
         * Read the server random
         */
        this.securityParameters.serverRandom = TlsUtils.readFully(32, buf);

        this.selectedSessionID = TlsUtils.readOpaque8(buf);
        if (this.selectedSessionID.length > 32)
        {
            throw new TlsFatalAlert(AlertDescription.illegal_parameter);
        }
        this.tlsClient.notifySessionID(this.selectedSessionID);
        this.resumedSession = this.selectedSessionID.length > 0 && this.tlsSession != null
            && Arrays.areEqual(this.selectedSessionID, this.tlsSession.getSessionID());

        /*
         * 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.
         */
        int selectedCipherSuite = TlsUtils.readUint16(buf);
        if (!Arrays.contains(this.offeredCipherSuites, selectedCipherSuite)
            || selectedCipherSuite == CipherSuite.TLS_NULL_WITH_NULL_NULL
            || CipherSuite.isSCSV(selectedCipherSuite)
            || !TlsUtils.isValidCipherSuiteForVersion(selectedCipherSuite, getContext().getServerVersion()))
        {
            throw new TlsFatalAlert(AlertDescription.illegal_parameter);
        }
        this.tlsClient.notifySelectedCipherSuite(selectedCipherSuite);

        /*
         * Find out which CompressionMethod the server has chosen and check that it was one of the
         * offered ones.
         */
        short selectedCompressionMethod = TlsUtils.readUint8(buf);
        if (!Arrays.contains(this.offeredCompressionMethods, selectedCompressionMethod))
        {
            throw new TlsFatalAlert(AlertDescription.illegal_parameter);
        }
        this.tlsClient.notifySelectedCompressionMethod(selectedCompressionMethod);

        /*
         * RFC3546 2.2 The extended server hello message format MAY be sent in place of the server
         * hello message when the client has requested extended functionality via the extended
         * client hello message specified in Section 2.1. ... Note that the extended server hello
         * message is only sent in response to an extended client hello message. This prevents the
         * possibility that the extended server hello message could "break" existing TLS 1.0
         * clients.
         */
        this.serverExtensions = readExtensions(buf);

        /*
         * RFC 3546 2.2 Note that the extended server hello message is only sent in response to an
         * extended client hello message.
         * 
         * However, see RFC 5746 exception below. We always include the SCSV, so an Extended Server
         * Hello is always allowed.
         */
        if (this.serverExtensions != null)
        {
            Enumeration e = this.serverExtensions.keys();
            while (e.hasMoreElements())
            {
                Integer extType = (Integer)e.nextElement();

                /*
                 * RFC 5746 3.6. Note that sending a "renegotiation_info" extension in response to a
                 * ClientHello containing only the SCSV is an explicit exception to the prohibition
                 * in RFC 5246, Section 7.4.1.4, on the server sending unsolicited extensions and is
                 * only allowed because the client is signaling its willingness to receive the
                 * extension via the TLS_EMPTY_RENEGOTIATION_INFO_SCSV SCSV.
                 */
                if (extType.equals(EXT_RenegotiationInfo))
                {
                    continue;
                }

                /*
                 * RFC 5246 7.4.1.4 An extension type MUST NOT appear in the ServerHello unless the
                 * same extension type appeared in the corresponding ClientHello. If a client
                 * receives an extension type in ServerHello that it did not request in the
                 * associated ClientHello, it MUST abort the handshake with an unsupported_extension
                 * fatal alert.
                 */
                if (null == TlsUtils.getExtensionData(this.clientExtensions, extType))
                {
                    throw new TlsFatalAlert(AlertDescription.unsupported_extension);
                }

                /*
                 * 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.resumedSession)
                {
                    // TODO[compat-gnutls] GnuTLS test server sends server extensions e.g. ec_point_formats
                    // TODO[compat-openssl] OpenSSL test server sends server extensions e.g. ec_point_formats
                    // TODO[compat-polarssl] PolarSSL test server sends server extensions e.g. ec_point_formats
//                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);
                }
            }
        }

        /*
         * RFC 5746 3.4. Client Behavior: Initial Handshake
         */
        {
            /*
             * When a ServerHello is received, the client MUST check if it includes the
             * "renegotiation_info" extension:
             */
            byte[] renegExtData = TlsUtils.getExtensionData(this.serverExtensions, EXT_RenegotiationInfo);
            if (renegExtData != null)
            {
                /*
                 * If the extension is present, set the secure_renegotiation flag to TRUE. The
                 * client MUST then verify that the length of the "renegotiated_connection"
                 * field is zero, and if it is not, MUST abort the handshake (by sending a fatal
                 * handshake_failure alert).
                 */
                this.secure_renegotiation = true;

                if (!Arrays.constantTimeAreEqual(renegExtData, createRenegotiationInfo(TlsUtils.EMPTY_BYTES)))
                {
                    throw new TlsFatalAlert(AlertDescription.handshake_failure);
                }
            }
        }

        // TODO[compat-gnutls] GnuTLS test server fails to send renegotiation_info extension when resuming
        this.tlsClient.notifySecureRenegotiation(this.secure_renegotiation);

        Hashtable sessionClientExtensions = clientExtensions, sessionServerExtensions = serverExtensions;
        if (this.resumedSession)
        {
            if (selectedCipherSuite != this.sessionParameters.getCipherSuite()
                || selectedCompressionMethod != this.sessionParameters.getCompressionAlgorithm()
                || !server_version.equals(this.sessionParameters.getNegotiatedVersion()))
            {
                throw new TlsFatalAlert(AlertDescription.illegal_parameter);
            }

            sessionClientExtensions = null;
            sessionServerExtensions = this.sessionParameters.readServerExtensions();
        }

        this.securityParameters.cipherSuite = selectedCipherSuite;
        this.securityParameters.compressionAlgorithm = selectedCompressionMethod;

        if (sessionServerExtensions != null)
        {
            {
                /*
                 * RFC 7366 3. If a server receives an encrypt-then-MAC request extension from a client
                 * and then selects a stream or Authenticated Encryption with Associated Data (AEAD)
                 * ciphersuite, it MUST NOT send an encrypt-then-MAC response extension back to the
                 * client.
                 */
                boolean serverSentEncryptThenMAC = TlsExtensionsUtils.hasEncryptThenMACExtension(sessionServerExtensions);
                if (serverSentEncryptThenMAC && !TlsUtils.isBlockCipherSuite(selectedCipherSuite))
                {
                    throw new TlsFatalAlert(AlertDescription.illegal_parameter);
                }
                this.securityParameters.encryptThenMAC = serverSentEncryptThenMAC;
            }

            this.securityParameters.extendedMasterSecret = TlsExtensionsUtils.hasExtendedMasterSecretExtension(sessionServerExtensions);

            this.securityParameters.maxFragmentLength = processMaxFragmentLengthExtension(sessionClientExtensions,
                sessionServerExtensions, AlertDescription.illegal_parameter);

            this.securityParameters.truncatedHMac = TlsExtensionsUtils.hasTruncatedHMacExtension(sessionServerExtensions);

            /*
             * TODO It's surprising that there's no provision to allow a 'fresh' CertificateStatus to be sent in
             * a session resumption handshake.
             */
            this.allowCertificateStatus = !this.resumedSession
                && TlsUtils.hasExpectedEmptyExtensionData(sessionServerExtensions,
                    TlsExtensionsUtils.EXT_status_request, AlertDescription.illegal_parameter);

            this.expectSessionTicket = !this.resumedSession
                && TlsUtils.hasExpectedEmptyExtensionData(sessionServerExtensions, TlsProtocol.EXT_SessionTicket,
                    AlertDescription.illegal_parameter);
        }

        /*
         * TODO[session-hash]
         * 
         * draft-ietf-tls-session-hash-04 4. Clients and servers SHOULD NOT accept handshakes
         * that do not use the extended master secret [..]. (and see 5.2, 5.3)
         */
        
        if (sessionClientExtensions != null)
        {
            this.tlsClient.processServerExtensions(sessionServerExtensions);
        }

        this.securityParameters.prfAlgorithm = getPRFAlgorithm(getContext(),
            this.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.
         */
        this.securityParameters.verifyDataLength = 12;
    }

    protected void sendCertificateVerifyMessage(DigitallySigned certificateVerify)
        throws IOException
    {
        HandshakeMessage message = new HandshakeMessage(HandshakeType.certificate_verify);

        certificateVerify.encode(message);

        message.writeToRecordStream();
    }

    protected void sendClientHelloMessage()
        throws IOException
    {
        this.recordStream.setWriteVersion(this.tlsClient.getClientHelloRecordLayerVersion());

        ProtocolVersion client_version = this.tlsClient.getClientVersion();
        if (!TlsUtils.isTLSv10(client_version))
        {
            throw new TlsFatalAlert(AlertDescription.internal_error);
        }

        getContextAdmin().setClientVersion(client_version);

        /*
         * TODO RFC 5077 3.4. When presenting a ticket, the client MAY generate and include a
         * Session ID in the TLS ClientHello.
         */
        byte[] session_id = TlsUtils.EMPTY_BYTES;
        if (this.tlsSession != null)
        {
            session_id = this.tlsSession.getSessionID();
            if (session_id == null || session_id.length > 32)
            {
                session_id = TlsUtils.EMPTY_BYTES;
            }
        }

        boolean fallback = this.tlsClient.isFallback();

        this.offeredCipherSuites = this.tlsClient.getCipherSuites();

        this.offeredCompressionMethods = this.tlsClient.getCompressionMethods();

        if (session_id.length > 0 && this.sessionParameters != null)
        {
            if (!Arrays.contains(this.offeredCipherSuites, sessionParameters.getCipherSuite())
                || !Arrays.contains(this.offeredCompressionMethods, sessionParameters.getCompressionAlgorithm()))
            {
                session_id = TlsUtils.EMPTY_BYTES;
            }
        }

        this.clientExtensions = this.tlsClient.getClientExtensions();

        HandshakeMessage message = new HandshakeMessage(HandshakeType.client_hello);

        TlsUtils.writeVersion(client_version, message);

        message.write(this.securityParameters.getClientRandom());

        TlsUtils.writeOpaque8(session_id, message);

        // Cipher Suites (and SCSV)
        {
            /*
             * RFC 5746 3.4. The client MUST include either an empty "renegotiation_info" extension,
             * or the TLS_EMPTY_RENEGOTIATION_INFO_SCSV signaling cipher suite value in the
             * ClientHello. Including both is NOT RECOMMENDED.
             */
            byte[] renegExtData = TlsUtils.getExtensionData(clientExtensions, EXT_RenegotiationInfo);
            boolean noRenegExt = (null == renegExtData);

            boolean noRenegSCSV = !Arrays.contains(offeredCipherSuites, CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV);

            if (noRenegExt && noRenegSCSV)
            {
                // TODO Consider whether to default to a client extension instead
//                this.clientExtensions = TlsExtensionsUtils.ensureExtensionsInitialised(this.clientExtensions);
//                this.clientExtensions.put(EXT_RenegotiationInfo, createRenegotiationInfo(TlsUtils.EMPTY_BYTES));
                this.offeredCipherSuites = Arrays.append(offeredCipherSuites, CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV);
            }

            /*
             * RFC 7507 4. If a client sends a ClientHello.client_version containing a lower value
             * than the latest (highest-valued) version supported by the client, it SHOULD include
             * the TLS_FALLBACK_SCSV cipher suite value in ClientHello.cipher_suites [..]. (The
             * client SHOULD put TLS_FALLBACK_SCSV after all cipher suites that it actually intends
             * to negotiate.)
             */
            if (fallback && !Arrays.contains(offeredCipherSuites, CipherSuite.TLS_FALLBACK_SCSV))
            {
                this.offeredCipherSuites = Arrays.append(offeredCipherSuites, CipherSuite.TLS_FALLBACK_SCSV);
            }

            TlsUtils.writeUint16ArrayWithUint16Length(offeredCipherSuites, message);
        }

        TlsUtils.writeUint8ArrayWithUint8Length(offeredCompressionMethods, message);

        if (clientExtensions != null)
        {
            writeExtensions(message, clientExtensions);
        }

        message.writeToRecordStream();
    }

    protected void sendClientKeyExchangeMessage()
        throws IOException
    {
        HandshakeMessage message = new HandshakeMessage(HandshakeType.client_key_exchange);

        this.keyExchange.generateClientKeyExchange(message);

        message.writeToRecordStream();
    }
}
