/* ServerHandshake.java -- the server-side handshake.
   Copyright (C) 2006  Free Software Foundation, Inc.

This file is a part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.

GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
USA

Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.

As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent
modules, and to copy and distribute the resulting executable under
terms of your choice, provided that you also meet, for each linked
independent module, the terms and conditions of the license of that
module.  An independent module is a module which is not derived from
or based on this library.  If you modify this library, you may extend
this exception to your version of the library, but you are not
obligated to do so.  If you do not wish to do so, delete this
exception statement from your version.  */


package gnu.javax.net.ssl.provider;

import static gnu.javax.net.ssl.provider.Handshake.Type.*;
import static gnu.javax.net.ssl.provider.KeyExchangeAlgorithm.*;
import static gnu.javax.net.ssl.provider.ServerHandshake.State.*;

import gnu.classpath.debug.Component;
import gnu.java.security.action.GetSecurityPropertyAction;
import gnu.javax.crypto.key.dh.GnuDHPublicKey;
import gnu.javax.net.ssl.AbstractSessionContext;
import gnu.javax.net.ssl.Session;
import gnu.javax.net.ssl.provider.Alert.Description;
import gnu.javax.net.ssl.provider.CertificateRequest.ClientCertificateType;

import java.nio.ByteBuffer;

import java.security.AccessController;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Level;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.security.auth.x500.X500Principal;

class ServerHandshake extends AbstractHandshake
{
  /**
   * Handshake state enumeration.
   */
  static enum State
  {
    WRITE_HELLO_REQUEST (true, false),
    WRITE_SERVER_HELLO (true, false),
    WRITE_CERTIFICATE (true, false),
    WRITE_SERVER_KEY_EXCHANGE (true, false),
    WRITE_CERTIFICATE_REQUEST (true, false),
    WRITE_SERVER_HELLO_DONE (true, false),
    WRITE_FINISHED (true, false),
    READ_CLIENT_HELLO (false, true),
    READ_CERTIFICATE (false, true),
    READ_CLIENT_KEY_EXCHANGE (false, true),
    READ_CERTIFICATE_VERIFY (false, true),
    READ_FINISHED (false, true),
    DONE (false, false);

    private final boolean isWriteState;
    private final boolean isReadState;

    private State(final boolean isWriteState, final boolean isReadState)
    {
      this.isWriteState = isWriteState;
      this.isReadState = isReadState;
    }

    boolean isReadState()
    {
      return isReadState;
    }

    boolean isWriteState()
    {
      return isWriteState;
    }
  }

  private State state;

  /* Handshake result fields. */
  private ByteBuffer outBuffer;
  private boolean clientHadExtensions = false;
  private boolean continuedSession = false;
  private ServerNameList requestedNames = null;
  private String keyAlias = null;
  private X509Certificate clientCert = null;
  private X509Certificate localCert = null;
  private boolean helloV2 = false;
  private KeyPair dhPair;
  private PrivateKey serverKey;

  // Delegated tasks we use.
  private GenDH genDH;
  private CertVerifier certVerifier;
  private CertLoader certLoader;
  private DelegatedTask keyExchangeTask;

  ServerHandshake (boolean writeHelloRequest, final SSLEngineImpl engine)
    throws NoSuchAlgorithmException
  {
    super(engine);
    if (writeHelloRequest)
      state = WRITE_HELLO_REQUEST;
    else
      state = READ_CLIENT_HELLO;
    handshakeOffset = 0;
  }

  /**
   * Choose the protocol version. Here we choose the largest protocol
   * version we support that is not greater than the client's
   * requested version.
   */
  private static ProtocolVersion chooseProtocol (final ProtocolVersion clientVersion,
                                                 final String[] enabledVersions)
    throws SSLException
  {
    ProtocolVersion version = null;
    for (int i = 0; i < enabledVersions.length; i++)
      {
        ProtocolVersion v = ProtocolVersion.forName (enabledVersions[i]);
        if (v.compareTo (clientVersion) <= 0)
          {
            if (version == null
                || v.compareTo (version) > 0)
              version = v;
          }
      }

    // The client requested a protocol version too old, or no protocol
    // versions are enabled.
    if (version == null)
      throw new SSLException ("no acceptable protocol version available");
    return version;
  }

  /**
   * Choose the first cipher suite in the client's requested list that
   * we have enabled.
   */
  private CipherSuite chooseSuite (final CipherSuiteList clientSuites,
                                   final String[] enabledSuites,
                                   final ProtocolVersion version)
    throws SSLException
  {
    // Figure out which SignatureAlgorithms we can support.
    HashSet<KeyExchangeAlgorithm> kexes = new HashSet<KeyExchangeAlgorithm>(8);

    kexes.add(NONE);
    X509ExtendedKeyManager km = engine.contextImpl.keyManager;
    if (km != null)
      {
        if (km.getServerAliases(DH_DSS.name(), null).length > 0)
          kexes.add(DH_DSS);
        if (km.getServerAliases(DH_RSA.name(), null).length > 0)
          kexes.add(DH_RSA);
        if (km.getServerAliases(DHE_DSS.name(), null).length > 0)
          kexes.add(DHE_DSS);
        if (km.getServerAliases(DHE_RSA.name(), null).length > 0)
          kexes.add(DHE_RSA);
        if (km.getServerAliases(RSA.name(), null).length > 0)
          kexes.add(RSA);
        if (km.getServerAliases(RSA_PSK.name(), null).length > 0
            && engine.contextImpl.pskManager != null)
          kexes.add(RSA_PSK);
      }
    if (engine.contextImpl.pskManager != null)
      {
        kexes.add(DHE_PSK);
        kexes.add(PSK);
      }

    if (Debug.DEBUG)
      logger.logv(Component.SSL_HANDSHAKE,
                  "we have certs for key exchange algorithms {0}", kexes);

    HashSet<CipherSuite> suites = new HashSet<CipherSuite>();
    for (String s : enabledSuites)
      {
        CipherSuite suite = CipherSuite.forName(s);
        if (suite == null)
          continue;
        if (!kexes.contains(suite.keyExchangeAlgorithm()))
          continue;
        suites.add(suite);
      }
    for (CipherSuite suite : clientSuites)
      {
        CipherSuite resolved = suite.resolve();
        if (!resolved.isResolved())
          continue;
        if (suites.contains(resolved))
          return resolved;
      }

    // We didn't find a match?
    throw new AlertException(new Alert(Alert.Level.FATAL,
                                       Alert.Description.INSUFFICIENT_SECURITY));
  }

  /**
   * Choose a compression method that we support, among the client's
   * requested compression methods. We prefer ZLIB over NONE in this
   * implementation.
   *
   * XXX Maybe consider implementing lzo (GNUTLS supports that).
   */
  private static CompressionMethod chooseCompression (final CompressionMethodList comps)
    throws SSLException
  {
    GetSecurityPropertyAction gspa
      = new GetSecurityPropertyAction("jessie.enable.compression");
    String enable = AccessController.doPrivileged(gspa);
    // Scan for ZLIB first.
    if (Boolean.valueOf(enable))
      {
        for (CompressionMethod cm : comps)
          {
            if (cm.equals (CompressionMethod.ZLIB))
              return CompressionMethod.ZLIB;
          }
      }
    for (CompressionMethod cm : comps)
      {
        if (cm.equals (CompressionMethod.NULL))
          return CompressionMethod.NULL;
      }

    throw new SSLException ("no supported compression method");
  }

  protected @Override boolean doHash()
  {
    boolean b = helloV2;
    helloV2 = false;
    return (state != WRITE_HELLO_REQUEST) && !b;
  }

  public @Override HandshakeStatus implHandleInput()
    throws SSLException
  {
    if (state == DONE)
      return HandshakeStatus.FINISHED;

    if (state.isWriteState()
        || (outBuffer != null && outBuffer.hasRemaining()))
      return HandshakeStatus.NEED_WRAP;

    // Copy the current buffer, and prepare it for reading.
    ByteBuffer buffer = handshakeBuffer.duplicate ();
    buffer.flip();
    buffer.position(handshakeOffset);
    Handshake handshake = new Handshake(buffer.slice(),
                                        engine.session().suite,
                                        engine.session().version);

    if (Debug.DEBUG)
      logger.logv(Component.SSL_HANDSHAKE, "processing in state {0}:\n{1}",
                  state, handshake);

    switch (state)
      {
        // Client Hello.
        //
        // This message is sent by the client to initiate a new handshake.
        // On a new connection, it is the first handshake message sent.
        //
        // The state of the handshake, after this message is processed,
        // will have a protocol version, cipher suite, compression method,
        // session ID, and various extensions (that the server also
        // supports).
        case READ_CLIENT_HELLO:
          if (handshake.type () != CLIENT_HELLO)
            throw new AlertException(new Alert(Alert.Level.FATAL,
                                               Alert.Description.UNEXPECTED_MESSAGE));

          {
            ClientHello hello = (ClientHello) handshake.body ();
            engine.session().version
              = chooseProtocol (hello.version (),
                                engine.getEnabledProtocols ());
            engine.session().suite =
              chooseSuite (hello.cipherSuites (),
                           engine.getEnabledCipherSuites (),
                           engine.session().version);
            compression = chooseCompression (hello.compressionMethods ());
            if (Debug.DEBUG)
              logger.logv(Component.SSL_HANDSHAKE,
                          "chose version:{0} suite:{1} compression:{2}",
                          engine.session().version, engine.session().suite,
                          compression);
            clientRandom = hello.random().copy();
            byte[] sessionId = hello.sessionId();
            if (hello.hasExtensions())
              {
                ExtensionList exts = hello.extensions();
                clientHadExtensions = exts.size() > 0;
                for (Extension e : hello.extensions())
                  {
                    Extension.Type type = e.type();
                    if (type == null)
                      continue;
                    switch (type)
                    {
                    case TRUNCATED_HMAC:
                      engine.session().setTruncatedMac(true);
                      break;

                    case MAX_FRAGMENT_LENGTH:
                      MaxFragmentLength len = (MaxFragmentLength) e.value();
                      engine.session().maxLength = len;
                      engine.session().setApplicationBufferSize(len.maxLength());
                      break;

                    case SERVER_NAME:
                      requestedNames = (ServerNameList) e.value();
                      List<String> names
                        = new ArrayList<String>(requestedNames.size());
                      for (ServerNameList.ServerName name : requestedNames)
                        names.add(name.name());
                      engine.session().putValue("gnu.javax.net.ssl.RequestedServerNames", names);
                      break;

                    default:
                      logger.log(Level.INFO, "skipping unsupported extension {0}", e);
                    }
                  }
              }
            AbstractSessionContext sessions = (AbstractSessionContext)
              engine.contextImpl.engineGetServerSessionContext();
            SSLSession s = sessions.getSession(sessionId);
            if (Debug.DEBUG)
              logger.logv(Component.SSL_HANDSHAKE, "looked up saved session {0}", s);
            if (s != null && s.isValid() && (s instanceof SessionImpl))
              {
                engine.setSession((SessionImpl) s);
                continuedSession = true;
              }
            else
              {
                // We *may* wind up with a badly seeded PRNG, and emit the
                // same session ID over and over (this did happen to me,
                // so we add this sanity check just in case).
                if (engine.session().id().equals(new Session.ID(sessionId)))
                  {
                    byte[] newId = new byte[32];
                    engine.session().random().nextBytes(newId);
                    engine.session().setId(new Session.ID(newId));
                  }
                sessions.put(engine.session());
              }
            state = WRITE_SERVER_HELLO;
          }
          break;

        // Certificate.
        //
        // This message is sent by the client if the server had previously
        // requested that the client authenticate itself with a certificate,
        // and if the client has an appropriate certificate available.
        //
        // Processing this message will save the client's certificate,
        // rejecting it if the certificate is not trusted, in preparation
        // for the certificate verify message that will follow.
        case READ_CERTIFICATE:
          {
            if (handshake.type() != CERTIFICATE)
              {
                if (engine.getNeedClientAuth()) // XXX throw better exception.
                  throw new SSLException("client auth required");
                state = READ_CLIENT_KEY_EXCHANGE;
                return HandshakeStatus.NEED_UNWRAP;
              }

            Certificate cert = (Certificate) handshake.body();
            try
              {
                engine.session().setPeerVerified(false);
                X509Certificate[] chain
                  = cert.certificates().toArray(new X509Certificate[0]);
                if (chain.length == 0)
                  throw new CertificateException("no certificates in chain");
                certVerifier = new CertVerifier(false, chain);
                tasks.add(certVerifier);
                engine.session().setPeerCertificates(chain);
                clientCert = chain[0];
                // Delay setting 'peerVerified' until CertificateVerify.
              }
            catch (CertificateException ce)
              {
                if (engine.getNeedClientAuth())
                  {
                    SSLPeerUnverifiedException x
                      = new SSLPeerUnverifiedException("client certificates could not be verified");
                    x.initCause(ce);
                    throw x;
                  }
              }
            catch (NoSuchAlgorithmException nsae)
              {
                throw new SSLException(nsae);
              }
            state = READ_CLIENT_KEY_EXCHANGE;
          }
          break;

        // Client Key Exchange.
        //
        // The client's key exchange. This message is sent either following
        // the certificate message, or if no certificate is available or
        // requested, following the server's hello done message.
        //
        // After receipt of this message, the session keys for this
        // session will have been created.
        case READ_CLIENT_KEY_EXCHANGE:
          {
            if (handshake.type() != CLIENT_KEY_EXCHANGE)
              throw new SSLException("expecting client key exchange");
            ClientKeyExchange kex = (ClientKeyExchange) handshake.body();

            KeyExchangeAlgorithm alg = engine.session().suite.keyExchangeAlgorithm();
            switch (alg)
              {
                case DHE_DSS:
                case DHE_RSA:
                case DH_anon:
                  {
                    ClientDiffieHellmanPublic pub = (ClientDiffieHellmanPublic)
                      kex.exchangeKeys();
                    DHPublicKey myKey = (DHPublicKey) dhPair.getPublic();
                    DHPublicKey clientKey =
                      new GnuDHPublicKey(null, myKey.getParams().getP(),
                                         myKey.getParams().getG(),
                                         pub.publicValue());
                    keyExchangeTask = new DHPhase(clientKey);
                    tasks.add(keyExchangeTask);
                  }
                  break;

                case RSA:
                  {
                    EncryptedPreMasterSecret secret = (EncryptedPreMasterSecret)
                      kex.exchangeKeys();
                    keyExchangeTask = new RSAKeyExchange(secret.encryptedSecret());
                    tasks.add(keyExchangeTask);
                  }
                  break;

                case PSK:
                  {
                    ClientPSKParameters params = (ClientPSKParameters)
                      kex.exchangeKeys();
                    generatePSKSecret(params.identity(), null, false);
                  }
                  break;

                case DHE_PSK:
                  {
                    ClientDHE_PSKParameters params = (ClientDHE_PSKParameters)
                      kex.exchangeKeys();
                    DHPublicKey serverKey = (DHPublicKey) dhPair.getPublic();
                    DHPublicKey clientKey =
                      new GnuDHPublicKey(null, serverKey.getParams().getP(),
                                         serverKey.getParams().getG(),
                                         params.params().publicValue());
                    SecretKey psk = null;
                    try
                      {
                        psk = engine.contextImpl.pskManager.getKey(params.identity());
                      }
                    catch (KeyManagementException kme)
                      {
                      }
                    keyExchangeTask = new DHE_PSKGen(clientKey, psk, false);
                    tasks.add(keyExchangeTask);
                  }
                  break;

                case RSA_PSK:
                  {
                    ClientRSA_PSKParameters params = (ClientRSA_PSKParameters)
                      kex.exchangeKeys();
                    SecretKey psk = null;
                    try
                      {
                        psk = engine.contextImpl.pskManager.getKey(params.identity());
                      }
                    catch (KeyManagementException kme)
                      {
                      }
                    if (psk == null)
                      {
                        byte[] fakeKey = new byte[16];
                        engine.session().random().nextBytes(fakeKey);
                        psk = new SecretKeySpec(fakeKey, "DHE_PSK");
                      }
                    keyExchangeTask =
                      new RSA_PSKExchange(params.secret().encryptedSecret(), psk);
                    tasks.add(keyExchangeTask);
                  }
                  break;

                case NONE:
                  {
                    Inflater inflater = null;
                    Deflater deflater = null;
                    if (compression == CompressionMethod.ZLIB)
                      {
                        inflater = new Inflater();
                        deflater = new Deflater();
                      }
                    inParams = new InputSecurityParameters(null, null, inflater,
                                                           engine.session(),
                                                           engine.session().suite);
                    outParams = new OutputSecurityParameters(null, null, deflater,
                                                             engine.session(),
                                                             engine.session().suite);
                    engine.session().privateData.masterSecret = new byte[0];
                  }
                  break;
              }
            // XXX SRP

            if (clientCert != null)
              state = READ_CERTIFICATE_VERIFY;
            else
              state = READ_FINISHED;
          }
          break;

        // Certificate Verify.
        //
        // This message is sent following the client key exchange message,
        // but only when the client included its certificate in a previous
        // message.
        //
        // After receipt of this message, the client's certificate (and,
        // to a degree, the client's identity) will have been verified.
        case READ_CERTIFICATE_VERIFY:
          {
            if (handshake.type() != CERTIFICATE_VERIFY)
              throw new SSLException("expecting certificate verify message");

            CertificateVerify verify = (CertificateVerify) handshake.body();
            try
              {
                verifyClient(verify.signature());
                if (certVerifier != null && certVerifier.verified())
                  engine.session().setPeerVerified(true);
              }
            catch (SignatureException se)
              {
                if (engine.getNeedClientAuth())
                  throw new SSLException("client auth failed", se);
              }
            if (continuedSession)
              {
                engine.changeCipherSpec();
                state = WRITE_FINISHED;
              }
            else
              state = READ_FINISHED;
          }
          break;

        // Finished.
        //
        // This message is sent immediately following the change cipher
        // spec message (which is sent outside of the handshake layer).
        // After receipt of this message, the session keys for the client
        // side will have been verified (this is the first message the
        // client sends encrypted and authenticated with the newly
        // negotiated keys).
        //
        // In the case of a continued session, the client sends its
        // finished message first. Otherwise, the server will send its
        // finished message first.
        case READ_FINISHED:
          {
            if (handshake.type() != FINISHED)
              throw new AlertException(new Alert(Alert.Level.FATAL,
                                                 Description.UNEXPECTED_MESSAGE));

            Finished clientFinished = (Finished) handshake.body();

            MessageDigest md5copy = null;
            MessageDigest shacopy = null;
            try
              {
                md5copy = (MessageDigest) md5.clone();
                shacopy = (MessageDigest) sha.clone();
              }
            catch (CloneNotSupportedException cnse)
              {
                // We're improperly configured to use a non-cloneable
                // md5/sha-1, OR there's a runtime bug.
                throw new SSLException(cnse);
              }
            Finished serverFinished =
              new Finished(generateFinished(md5copy, shacopy,
                                            true, engine.session()),
                                            engine.session().version);

            if (Debug.DEBUG)
              logger.log(Component.SSL_HANDSHAKE, "server finished: {0}",
                         serverFinished);

            if (engine.session().version == ProtocolVersion.SSL_3)
              {
                if (!Arrays.equals(clientFinished.md5Hash(),
                                   serverFinished.md5Hash())
                    || !Arrays.equals(clientFinished.shaHash(),
                                      serverFinished.shaHash()))
                  {
                    engine.session().invalidate();
                    throw new SSLException("session verify failed");
                  }
              }
            else
              {
                if (!Arrays.equals(clientFinished.verifyData(),
                                   serverFinished.verifyData()))
                  {
                    engine.session().invalidate();
                    throw new SSLException("session verify failed");
                  }
              }

            if (continuedSession)
              state = DONE;
            else
              {
                engine.changeCipherSpec();
                state = WRITE_FINISHED;
              }
          }
          break;
      }

    handshakeOffset += handshake.length() + 4;

    if (!tasks.isEmpty())
      return HandshakeStatus.NEED_TASK;
    if (state.isReadState())
      return HandshakeStatus.NEED_UNWRAP;
    if (state.isWriteState())
      return HandshakeStatus.NEED_WRAP;

    return HandshakeStatus.FINISHED;
  }

  public @Override HandshakeStatus implHandleOutput (ByteBuffer fragment)
    throws SSLException
  {
    if (Debug.DEBUG)
      logger.logv(Component.SSL_HANDSHAKE,
                  "handle output state: {0}; output fragment: {1}",
                  state, fragment);

    // Drain the output buffer, if it needs it.
    if (outBuffer != null && outBuffer.hasRemaining())
      {
        int l = Math.min(fragment.remaining(), outBuffer.remaining());
        fragment.put((ByteBuffer) outBuffer.duplicate().limit(outBuffer.position() + l));
        outBuffer.position(outBuffer.position() + l);
      }

    if (!fragment.hasRemaining())
      {
        if (state.isWriteState() || outBuffer.hasRemaining())
          return HandshakeStatus.NEED_WRAP;
        else
          return HandshakeStatus.NEED_UNWRAP;
      }

    // XXX what we need to do here is generate a "stream" of handshake
    // messages, and insert them into fragment amounts that we have available.
    // A handshake message can span multiple records, and we can put
    // multiple records into a single record.
    //
    // So, we can have one of two states:
    //
    // 1) We have enough space in the record we are creating to push out
    //    everything we need to on this round. This is easy; we just
    //    repeatedly fill in these messages in the buffer, so we get something
    //    that looks like this:
    //                 ________________________________
    //       records: |________________________________|
    //    handshakes: |______|__|__________|
    //
    // 2) We can put part of one handshake message in the current record,
    //    but we must put the rest of it in the following record, or possibly
    //    more than one following record. So here, we'd see this:
    //
    //                 ________________________
    //       records: |_______|_______|________|
    //    handshakes: |____|_______|_________|
    //
    // We *could* make this a lot easier by just only ever emitting one
    // record per call, but then we would waste potentially a lot of space
    // and waste a lot of TCP packets by doing it the simple way. What
    // we desire here is that we *maximize* our usage of the resources
    // given to us, and to use as much space in the present fragment as
    // we can.
    //
    // Note that we pretty much have to support this, anyway, because SSL
    // provides no guarantees that the record size is large enough to
    // admit *even one* handshake message. Also, callers could call on us
    // with a short buffer, even though they aren't supposed to.
    //
    // This is somewhat complicated by the fact that we don't know, a priori,
    // how large a handshake message will be until we've built it, and our
    // design builds the message around the byte buffer.
    //
    // Some ways to handle this:
    //
    //  1. Write our outgoing handshake messages to a private buffer,
    //     big enough per message (and, if we run out of space, resize that
    //     buffer) and push (possibly part of) this buffer out to the
    //     outgoing buffer. This isn't that great because we'd need to
    //     store and copy things unnecessarily.
    //
    //  2. Build outgoing handshake objects 'virtually', that is, store them
    //     as collections of objects, then compute the length, and then write
    //     them to a buffer, instead of making the objects views on
    //     ByteBuffers for both input and output. This would complicate the
    //     protocol objects a bit (although, it would amount to doing
    //     separation between client objects and server objects, which is
    //     pretty OK), and we still need to figure out how exactly to chunk
    //     those objects across record boundaries.
    //
    //  3. Try to build these objects on the buffer we're given, but detect
    //     when we run out of space in the output buffer, and split the
    //     overflow message. This sounds like the best, but also probably
    //     the hardest to code.
output_loop:
    while (fragment.remaining() >= 4 && state.isWriteState())
      {
        switch (state)
          {
            // Hello Request.
            //
            // This message is sent by the server to initiate a new
            // handshake, to establish new session keys.
            case WRITE_HELLO_REQUEST:
            {
              Handshake handshake = new Handshake(fragment);
              handshake.setType(Handshake.Type.HELLO_REQUEST);
              handshake.setLength(0);
              fragment.position(fragment.position() + 4);
              if (Debug.DEBUG)
                logger.log(Component.SSL_HANDSHAKE, "{0}", handshake);
              state = READ_CLIENT_HELLO;
            }
            break output_loop; // XXX temporary

            // Server Hello.
            //
            // This message is sent immediately following the client hello.
            // It informs the client of the cipher suite, compression method,
            // session ID (which may have been a continued session), and any
            // supported extensions.
            case WRITE_SERVER_HELLO:
            {
              ServerHelloBuilder hello = new ServerHelloBuilder();
              hello.setVersion(engine.session().version);
              Random r = hello.random();
              r.setGmtUnixTime(Util.unixTime());
              byte[] nonce = new byte[28];
              engine.session().random().nextBytes(nonce);
              r.setRandomBytes(nonce);
              serverRandom = r.copy();
              hello.setSessionId(engine.session().getId());
              hello.setCipherSuite(engine.session().suite);
              hello.setCompressionMethod(compression);
              if (clientHadExtensions)
                {
                  // XXX figure this out.
                }
              else // Don't send any extensions.
                hello.setDisableExtensions(true);

              if (Debug.DEBUG)
                logger.log(Component.SSL_HANDSHAKE, "{0}", hello);

              int typeLen = ((Handshake.Type.SERVER_HELLO.getValue() << 24)
                  | (hello.length() & 0xFFFFFF));
              fragment.putInt(typeLen);

              outBuffer = hello.buffer();
              int l = Math.min(fragment.remaining(), outBuffer.remaining());
              fragment.put((ByteBuffer) outBuffer.duplicate().limit(outBuffer.position() + l));
              outBuffer.position(outBuffer.position() + l);

              CipherSuite cs = engine.session().suite;
              KeyExchangeAlgorithm kex = cs.keyExchangeAlgorithm();
              if (continuedSession)
                {
                  byte[][] keys = generateKeys(clientRandom, serverRandom,
                                               engine.session());
                  setupSecurityParameters(keys, false, engine, compression);
                  engine.changeCipherSpec();
                  state = WRITE_FINISHED;
                }
              else if (kex == DHE_DSS || kex == DHE_RSA || kex == RSA
                       || kex == RSA_PSK)
                {
                  certLoader = new CertLoader();
                  tasks.add(certLoader);
                  state = WRITE_CERTIFICATE;
                  if (kex == DHE_DSS || kex == DHE_RSA)
                    {
                      genDH = new GenDH();
                      tasks.add(genDH);
                    }
                  break output_loop;
                }
              else if (kex == PSK)
                {
                  state = WRITE_SERVER_KEY_EXCHANGE;
                }
              else if (kex == DHE_PSK || kex == DH_anon)
                {
                  genDH = new GenDH();
                  tasks.add(genDH);
                  state = WRITE_SERVER_KEY_EXCHANGE;
                  break output_loop;
                }
              else if (engine.getWantClientAuth() || engine.getNeedClientAuth())
                {
                  state = WRITE_CERTIFICATE_REQUEST;
                }
              else
                state = WRITE_SERVER_HELLO_DONE;
            }
            break;

            // Certificate.
            //
            // This message is sent immediately following the server hello,
            // IF the cipher suite chosen requires that the server identify
            // itself (usually, servers must authenticate).
            case WRITE_CERTIFICATE:
            {
              // We must have scheduled a certificate loader to run.
              assert(certLoader != null);
              assert(certLoader.hasRun());
              if (certLoader.thrown() != null)
                throw new AlertException(new Alert(Alert.Level.FATAL,
                                                   Alert.Description.HANDSHAKE_FAILURE),
                                         certLoader.thrown());
              java.security.cert.Certificate[] chain
                = engine.session().getLocalCertificates();
              CertificateBuilder cert = new CertificateBuilder(CertificateType.X509);
              try
                {
                  cert.setCertificates(Arrays.asList(chain));
                }
              catch (CertificateException ce)
                {
                  throw new SSLException(ce);
                }

              if (Debug.DEBUG)
                {
                  logger.logv(Component.SSL_HANDSHAKE, "my cert:\n{0}", localCert);
                  logger.logv(Component.SSL_HANDSHAKE, "{0}", cert);
                }

              int typeLen = ((CERTIFICATE.getValue() << 24)
                             | (cert.length() & 0xFFFFFF));
              fragment.putInt(typeLen);

              outBuffer = cert.buffer();
              final int l = Math.min(fragment.remaining(), outBuffer.remaining());
              fragment.put((ByteBuffer) outBuffer.duplicate().limit(outBuffer.position() + l));
              outBuffer.position(outBuffer.position() + l);

              CipherSuite s = engine.session().suite;
              KeyExchangeAlgorithm kexalg = s.keyExchangeAlgorithm();
              if (kexalg == DHE_DSS || kexalg == DHE_RSA)
                {
                  genDH = new GenDH();
                  tasks.add(genDH);
                  state = WRITE_SERVER_KEY_EXCHANGE;
                  break output_loop;
                }
              else if (kexalg == RSA_PSK)
                state = WRITE_SERVER_KEY_EXCHANGE;
              else if (engine.getWantClientAuth() || engine.getNeedClientAuth())
                {
                  state = WRITE_CERTIFICATE_REQUEST;
                }
              else
                state = WRITE_SERVER_HELLO_DONE;
            }
            break output_loop; // XXX temporary

            // Server key exchange.
            //
            // This message is sent, following the certificate if sent,
            // otherwise following the server hello, IF the chosen cipher
            // suite requires that the server send explicit key exchange
            // parameters (that is, if the key exchange parameters are not
            // implicit in the server's certificate).
            case WRITE_SERVER_KEY_EXCHANGE:
            {
              KeyExchangeAlgorithm kex = engine.session().suite.keyExchangeAlgorithm();

              ByteBuffer paramBuffer = null;
              ByteBuffer sigBuffer = null;
              if (kex == DHE_DSS || kex == DHE_RSA || kex == DH_anon
                  || kex == DHE_PSK)
                {
                  assert(genDH != null);
                  assert(genDH.hasRun());
                  if (genDH.thrown() != null)
                    throw new AlertException(new Alert(Alert.Level.FATAL,
                                                       Alert.Description.HANDSHAKE_FAILURE),
                                             genDH.thrown());
                  assert(dhPair != null);
                  initDiffieHellman((DHPrivateKey) dhPair.getPrivate(),
                                    engine.session().random());
                  paramBuffer = genDH.paramsBuffer;
                  sigBuffer = genDH.sigBuffer;

                  if (kex == DHE_PSK)
                    {
                      String identityHint
                        = engine.contextImpl.pskManager.chooseIdentityHint();
                      ServerDHE_PSKParameters psk =
                        new ServerDHE_PSKParameters(identityHint, paramBuffer);
                      paramBuffer = psk.buffer();
                    }
                }
              if (kex == RSA_PSK)
                {
                  String idHint = engine.contextImpl.pskManager.chooseIdentityHint();
                  if (idHint != null)
                    {
                      ServerRSA_PSKParameters params
                        = new ServerRSA_PSKParameters(idHint);
                      paramBuffer = params.buffer();
                    }
                }
              if (kex == PSK)
                {
                  String idHint = engine.contextImpl.pskManager.chooseIdentityHint();
                  if (idHint != null)
                    {
                      ServerPSKParameters params
                        = new ServerPSKParameters(idHint);
                      paramBuffer = params.buffer();
                    }
                }
              // XXX handle SRP

              if (paramBuffer != null)
                {
                  ServerKeyExchangeBuilder ske
                    = new ServerKeyExchangeBuilder(engine.session().suite);
                  ske.setParams(paramBuffer);
                  if (sigBuffer != null)
                    ske.setSignature(sigBuffer);

                  if (Debug.DEBUG)
                    logger.log(Component.SSL_HANDSHAKE, "{0}", ske);

                  outBuffer = ske.buffer();
                  int l = Math.min(fragment.remaining(), outBuffer.remaining());
                  fragment.putInt((SERVER_KEY_EXCHANGE.getValue() << 24)
                                  | (ske.length() & 0xFFFFFF));
                  fragment.put((ByteBuffer) outBuffer.duplicate().limit
                               (outBuffer.position() + l));
                  outBuffer.position(outBuffer.position() + l);
                }

              if (engine.getWantClientAuth() || engine.getNeedClientAuth())
                state = WRITE_CERTIFICATE_REQUEST;
              else
                state = WRITE_SERVER_HELLO_DONE;
            }
            break;

            // Certificate Request.
            //
            // This message is sent when the server desires or requires
            // client authentication with a certificate; if it is sent, it
            // will be sent just after the Certificate or Server Key
            // Exchange messages, whichever is sent. If neither of the
            // above are sent, it will be the message that follows the
            // server hello.
            case WRITE_CERTIFICATE_REQUEST:
            {
              CertificateRequestBuilder req = new CertificateRequestBuilder();

              List<ClientCertificateType> types
                = new ArrayList<ClientCertificateType>(4);
              types.add(ClientCertificateType.RSA_SIGN);
              types.add(ClientCertificateType.RSA_FIXED_DH);
              types.add(ClientCertificateType.DSS_SIGN);
              types.add(ClientCertificateType.DSS_FIXED_DH);
              req.setTypes(types);

              X509Certificate[] anchors
                = engine.contextImpl.trustManager.getAcceptedIssuers();
              List<X500Principal> issuers
                = new ArrayList<X500Principal>(anchors.length);
              for (X509Certificate cert : anchors)
                issuers.add(cert.getIssuerX500Principal());
              req.setAuthorities(issuers);

              if (Debug.DEBUG)
                logger.log(Component.SSL_HANDSHAKE, "{0}", req);

              fragment.putInt((CERTIFICATE_REQUEST.getValue() << 24)
                              | (req.length() & 0xFFFFFF));

              outBuffer = req.buffer();
              int l = Math.min(outBuffer.remaining(), fragment.remaining());
              fragment.put((ByteBuffer) outBuffer.duplicate().limit(outBuffer.position() + l));
              outBuffer.position(outBuffer.position() + l);

              state = WRITE_SERVER_HELLO_DONE;
            }
            break;

            // Server Hello Done.
            //
            // This message is always sent by the server, to terminate its
            // side of the handshake. Since the server's handshake message
            // may comprise multiple, optional messages, this sentinel
            // message lets the client know when the server's message stream
            // is complete.
            case WRITE_SERVER_HELLO_DONE:
            {
              // ServerHelloDone is zero-length; just put in the type
              // field.
              fragment.putInt(SERVER_HELLO_DONE.getValue() << 24);
              if (Debug.DEBUG)
                logger.logv(Component.SSL_HANDSHAKE, "writing ServerHelloDone");
              state = READ_CERTIFICATE;
            }
            break output_loop; // XXX temporary

            // Finished.
            //
            // This is always sent by the server to verify the keys that the
            // server will use to encrypt and authenticate. In a full
            // handshake, this message will be sent after the client's
            // finished message; in an abbreviated handshake (with a continued
            // session) the server sends its finished message first.
            //
            // This message follows the change cipher spec message, which is
            // sent out-of-band in a different SSL content-type.
            //
            // This is the first message that the server will send encrypted
            // and authenticated with the newly negotiated session keys.
            case WRITE_FINISHED:
            {
              MessageDigest md5copy = null;
              MessageDigest shacopy = null;
              try
                {
                  md5copy = (MessageDigest) md5.clone();
                  shacopy = (MessageDigest) sha.clone();
                }
              catch (CloneNotSupportedException cnse)
                {
                  // We're improperly configured to use a non-cloneable
                  // md5/sha-1, OR there's a runtime bug.
                  throw new SSLException(cnse);
                }
              outBuffer
                = generateFinished(md5copy, shacopy, false,
                                   engine.session());

              fragment.putInt((FINISHED.getValue() << 24)
                              | outBuffer.remaining() & 0xFFFFFF);

              int l = Math.min(outBuffer.remaining(), fragment.remaining());
              fragment.put((ByteBuffer) outBuffer.duplicate().limit(outBuffer.position() + l));
              outBuffer.position(outBuffer.position() + l);

              if (continuedSession)
                state = READ_FINISHED;
              else
                state = DONE;
            }
            break;
          }
      }
    if (!tasks.isEmpty())
      return HandshakeStatus.NEED_TASK;
    if (state.isWriteState() || outBuffer.hasRemaining())
      return HandshakeStatus.NEED_WRAP;
    if (state.isReadState())
      return HandshakeStatus.NEED_UNWRAP;

    return HandshakeStatus.FINISHED;
  }

  @Override HandshakeStatus status()
  {
    if (!tasks.isEmpty())
      return HandshakeStatus.NEED_TASK;
    if (state.isReadState())
      return HandshakeStatus.NEED_UNWRAP;
    if (state.isWriteState())
      return HandshakeStatus.NEED_WRAP;

    return HandshakeStatus.FINISHED;
  }

  @Override void checkKeyExchange() throws SSLException
  {
    if (continuedSession) // No key exchange needed.
      return;
    KeyExchangeAlgorithm kex = engine.session().suite.keyExchangeAlgorithm();
    if (kex == NONE || kex == PSK || kex == RSA_PSK) // Don't need one.
      return;
    if (keyExchangeTask == null) // An error if we never created one.
      throw new AlertException(new Alert(Alert.Level.FATAL,
                                         Alert.Description.INTERNAL_ERROR));
    if (!keyExchangeTask.hasRun()) // An error if the caller never ran it.
      throw new AlertException(new Alert(Alert.Level.FATAL,
                                         Alert.Description.INTERNAL_ERROR));
    if (keyExchangeTask.thrown() != null) // An error was thrown.
      throw new AlertException(new Alert(Alert.Level.FATAL,
                                         Alert.Description.HANDSHAKE_FAILURE),
                               keyExchangeTask.thrown());
  }

  @Override void handleV2Hello(ByteBuffer hello)
  {
    int len = hello.getShort(0) & 0x7FFF;
    md5.update((ByteBuffer) hello.duplicate().position(2).limit(len+2));
    sha.update((ByteBuffer) hello.duplicate().position(2).limit(len+2));
    helloV2 = true;
  }

  private ByteBuffer signParams(ByteBuffer serverParams)
    throws NoSuchAlgorithmException, InvalidKeyException, SignatureException
  {
    SignatureAlgorithm alg = engine.session().suite.signatureAlgorithm();
    java.security.Signature sig
      = java.security.Signature.getInstance(alg.algorithm());
    PrivateKey key = engine.contextImpl.keyManager.getPrivateKey(keyAlias);
    if (Debug.DEBUG_KEY_EXCHANGE)
      logger.logv(Component.SSL_HANDSHAKE, "server key: {0}", key);
    sig.initSign(key);
    sig.update(clientRandom.buffer());
    sig.update(serverRandom.buffer());
    sig.update(serverParams);
    byte[] sigVal = sig.sign();
    Signature signature = new Signature(sigVal, engine.session().suite.signatureAlgorithm());
    return signature.buffer();
  }

  private void verifyClient(byte[] sigValue) throws SSLException, SignatureException
  {
    MessageDigest md5copy = null;
    MessageDigest shacopy = null;
    try
      {
        md5copy = (MessageDigest) md5.clone();
        shacopy = (MessageDigest) sha.clone();
      }
    catch (CloneNotSupportedException cnse)
      {
        // Mis-configured with non-cloneable digests.
        throw new SSLException(cnse);
      }
    byte[] toSign = null;
    if (engine.session().version == ProtocolVersion.SSL_3)
      toSign = genV3CertificateVerify(md5copy, shacopy, engine.session());
    else
      {
        if (engine.session().suite.signatureAlgorithm() == SignatureAlgorithm.RSA)
          toSign = Util.concat(md5copy.digest(), shacopy.digest());
        else
          toSign = shacopy.digest();
      }

    try
      {
        java.security.Signature sig = java.security.Signature.getInstance(engine.session().suite.signatureAlgorithm().toString());
        sig.initVerify(clientCert);
        sig.update(toSign);
        sig.verify(sigValue);
      }
    catch (InvalidKeyException ike)
      {
        throw new SSLException(ike);
      }
    catch (NoSuchAlgorithmException nsae)
      {
        throw new SSLException(nsae);
      }
  }

  // Delegated tasks.

  class CertLoader extends DelegatedTask
  {
    CertLoader()
    {
    }

    public void implRun() throws SSLException
    {
      KeyExchangeAlgorithm kexalg = engine.session().suite.keyExchangeAlgorithm();
      X509ExtendedKeyManager km = engine.contextImpl.keyManager;
      Principal[] issuers = null; // XXX use TrustedAuthorities extension.
      keyAlias = km.chooseEngineServerAlias(kexalg.name(), issuers, engine);
      if (keyAlias == null)
        throw new SSLException("no certificates available");
      X509Certificate[] chain = km.getCertificateChain(keyAlias);
      engine.session().setLocalCertificates(chain);
      localCert = chain[0];
      serverKey = km.getPrivateKey(keyAlias);
      if (kexalg == DH_DSS || kexalg == DH_RSA)
        dhPair = new KeyPair(localCert.getPublicKey(),
                             km.getPrivateKey(keyAlias));
    }
  }

  /**
   * Delegated task for generating Diffie-Hellman parameters.
   */
  private class GenDH extends DelegatedTask
  {
    ByteBuffer paramsBuffer;
    ByteBuffer sigBuffer;

    protected void implRun()
      throws NoSuchAlgorithmException, InvalidAlgorithmParameterException,
             InvalidKeyException, SignatureException
    {
      KeyPairGenerator dhGen = KeyPairGenerator.getInstance("DH");
      DHParameterSpec dhparams = DiffieHellman.getParams().getParams();
      dhGen.initialize(dhparams, engine.session().random());
      dhPair = dhGen.generateKeyPair();
      DHPublicKey pub = (DHPublicKey) dhPair.getPublic();

      // Generate the parameters message.
      ServerDHParams params = new ServerDHParams(pub.getParams().getP(),
                                                 pub.getParams().getG(),
                                                 pub.getY());
      paramsBuffer = params.buffer();

      // Sign the parameters, if needed.
      if (engine.session().suite.signatureAlgorithm() != SignatureAlgorithm.ANONYMOUS)
        {
          sigBuffer = signParams(paramsBuffer);
          paramsBuffer.rewind();
        }
      if (Debug.DEBUG_KEY_EXCHANGE)
        logger.logv(Component.SSL_KEY_EXCHANGE,
                    "Diffie-Hellman public:{0} private:{1}",
                    dhPair.getPublic(), dhPair.getPrivate());
    }
  }

  class RSAKeyExchange extends DelegatedTask
  {
    private final byte[] encryptedPreMasterSecret;

    RSAKeyExchange(byte[] encryptedPreMasterSecret)
    {
      this.encryptedPreMasterSecret = encryptedPreMasterSecret;
    }

    public void implRun()
      throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException,
             NoSuchAlgorithmException, NoSuchPaddingException, SSLException
    {
      Cipher rsa = Cipher.getInstance("RSA");
      rsa.init(Cipher.DECRYPT_MODE, serverKey);
      rsa.init(Cipher.DECRYPT_MODE, localCert);
      preMasterSecret = rsa.doFinal(encryptedPreMasterSecret);
      generateMasterSecret(clientRandom, serverRandom, engine.session());
      byte[][] keys = generateKeys(clientRandom, serverRandom, engine.session());
      setupSecurityParameters(keys, false, engine, compression);
    }
  }

  class RSA_PSKExchange extends DelegatedTask
  {
    private final byte[] encryptedPreMasterSecret;
    private final SecretKey psKey;

    RSA_PSKExchange(byte[] encryptedPreMasterSecret, SecretKey psKey)
    {
      this.encryptedPreMasterSecret = encryptedPreMasterSecret;
      this.psKey = psKey;
    }

    public @Override void implRun()
      throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException,
             NoSuchAlgorithmException, NoSuchPaddingException, SSLException
    {
      Cipher rsa = Cipher.getInstance("RSA");
      rsa.init(Cipher.DECRYPT_MODE, serverKey);
      rsa.init(Cipher.DECRYPT_MODE, localCert);
      byte[] rsaSecret = rsa.doFinal(encryptedPreMasterSecret);
      byte[] psSecret = psKey.getEncoded();
      preMasterSecret = new byte[rsaSecret.length + psSecret.length + 4];
      preMasterSecret[0] = (byte) (rsaSecret.length >>> 8);
      preMasterSecret[1] = (byte)  rsaSecret.length;
      System.arraycopy(rsaSecret, 0, preMasterSecret, 2, rsaSecret.length);
      preMasterSecret[rsaSecret.length + 2] = (byte) (psSecret.length >>> 8);
      preMasterSecret[rsaSecret.length + 3] = (byte)  psSecret.length;
      System.arraycopy(psSecret, 0, preMasterSecret, rsaSecret.length+4,
                       psSecret.length);

      generateMasterSecret(clientRandom, serverRandom, engine.session());
      byte[][] keys = generateKeys(clientRandom, serverRandom, engine.session());
      setupSecurityParameters(keys, false, engine, compression);
    }
  }
}
