diff options
Diffstat (limited to 'libjava/classpath/gnu/javax/net/ssl/provider/ServerHandshake.java')
-rw-r--r-- | libjava/classpath/gnu/javax/net/ssl/provider/ServerHandshake.java | 1377 |
1 files changed, 1377 insertions, 0 deletions
diff --git a/libjava/classpath/gnu/javax/net/ssl/provider/ServerHandshake.java b/libjava/classpath/gnu/javax/net/ssl/provider/ServerHandshake.java new file mode 100644 index 000000000..d69fa120d --- /dev/null +++ b/libjava/classpath/gnu/javax/net/ssl/provider/ServerHandshake.java @@ -0,0 +1,1377 @@ +/* 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); + } + } +} |