/* SSLEngineImpl.java -- implementation of SSLEngine. 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 gnu.classpath.debug.Component; import gnu.classpath.debug.SystemLogger; import gnu.java.security.util.ByteBufferOutputStream; import gnu.javax.net.ssl.Session; import gnu.javax.net.ssl.SSLRecordHandler; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.zip.DataFormatException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.ShortBufferException; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLEngineResult.Status; public final class SSLEngineImpl extends SSLEngine { final SSLContextImpl contextImpl; private SSLRecordHandler[] handlers; private static final SystemLogger logger = SystemLogger.SYSTEM; private SessionImpl session; private InputSecurityParameters insec; private OutputSecurityParameters outsec; private boolean inClosed; private boolean outClosed; private boolean createSessions; private boolean needClientAuth; private boolean wantClientAuth; private boolean initialHandshakeDone; private AbstractHandshake handshake; private Alert lastAlert; private SSLEngineResult.HandshakeStatus handshakeStatus; private boolean changeCipherSpec; private String[] enabledSuites; private String[] enabledProtocols; /** * We can receive any message chunked across multiple records, * including alerts, even though all alert messages are only two * bytes long. Handshake messages are de-chunked in the handshake * handler, change-cipher-spec messages are always empty, and we * don't care about chunking of application messages. * * This buffer will hold the incomplete alert that we receive, if * any. */ private final ByteBuffer alertBuffer; private Mode mode; private enum Mode { SERVER, CLIENT } SSLEngineImpl (SSLContextImpl contextImpl, String host, int port) { super(host, port); this.contextImpl = contextImpl; handlers = new SSLRecordHandler[256]; session = new SessionImpl(); session.suite = CipherSuite.TLS_NULL_WITH_NULL_NULL; session.version = ProtocolVersion.TLS_1_1; byte[] sid = new byte[32]; contextImpl.random.nextBytes(sid); session.setId(new Session.ID(sid)); session.setRandom(contextImpl.random); if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "generated session ID {0} with random {1}", session.id(), contextImpl.random); // Begin with no encryption. insec = new InputSecurityParameters (null, null, null, session, CipherSuite.TLS_NULL_WITH_NULL_NULL); outsec = new OutputSecurityParameters (null, null, null, session, CipherSuite.TLS_NULL_WITH_NULL_NULL); inClosed = false; outClosed = false; needClientAuth = false; wantClientAuth = false; createSessions = true; initialHandshakeDone = false; alertBuffer = ByteBuffer.wrap (new byte[2]); mode = null; lastAlert = null; handshakeStatus = SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; changeCipherSpec = false; // Set up default protocols and suites. enabledProtocols = new String[] { ProtocolVersion.TLS_1_1.toString(), ProtocolVersion.TLS_1.toString(), ProtocolVersion.SSL_3.toString() }; enabledSuites = defaultSuites(); } static String[] defaultSuites() { return new String[] { CipherSuite.TLS_DHE_DSS_WITH_AES_256_CBC_SHA.toString(), CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA.toString(), CipherSuite.TLS_DH_DSS_WITH_AES_256_CBC_SHA.toString(), CipherSuite.TLS_DH_RSA_WITH_AES_256_CBC_SHA.toString(), CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA.toString(), CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA.toString(), CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA.toString(), CipherSuite.TLS_DH_DSS_WITH_AES_128_CBC_SHA.toString(), CipherSuite.TLS_DH_RSA_WITH_AES_128_CBC_SHA.toString(), CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA.toString(), CipherSuite.TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA.toString(), CipherSuite.TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA.toString(), CipherSuite.TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA.toString(), CipherSuite.TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA.toString(), CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA.toString(), CipherSuite.TLS_RSA_WITH_RC4_128_MD5.toString(), CipherSuite.TLS_RSA_WITH_RC4_128_SHA.toString(), CipherSuite.TLS_DHE_DSS_WITH_DES_CBC_SHA.toString(), CipherSuite.TLS_DHE_RSA_WITH_DES_CBC_SHA.toString(), CipherSuite.TLS_DH_DSS_WITH_DES_CBC_SHA.toString(), CipherSuite.TLS_DH_RSA_WITH_DES_CBC_SHA.toString(), CipherSuite.TLS_RSA_WITH_DES_CBC_SHA.toString(), CipherSuite.TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA.toString(), CipherSuite.TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA.toString(), CipherSuite.TLS_RSA_EXPORT_WITH_DES40_CBC_SHA.toString(), CipherSuite.TLS_RSA_EXPORT_WITH_RC4_40_MD5.toString(), CipherSuite.TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA.toString(), CipherSuite.TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA.toString(), CipherSuite.TLS_RSA_WITH_NULL_MD5.toString(), CipherSuite.TLS_RSA_WITH_NULL_SHA.toString() }; } // XXX implement? /*public void registerHandler (final int contentType, SSLRecordHandler handler) throws SSLException { if (type.equals (ContentType.CHANGE_CIPHER_SPEC) || type.equals (ContentType.ALERT) || type.equals (ContentType.HANDSHAKE) || type.equals (ContentType.APPLICATION_DATA)) throw new SSLException ("can't override handler for content type " + type); int i = type.getValue (); if (i < 0 || i > 255) throw new SSLException ("illegal content type: " + type); handlers[i] = handler; }*/ @Override public void beginHandshake () throws SSLException { if (Debug.DEBUG) logger.log(Component.SSL_HANDSHAKE, "{0} handshake begins", mode); if (mode == null) throw new IllegalStateException("setUseClientMode was never used"); switch (mode) { case SERVER: if (getHandshakeStatus() != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) throw new SSLException("handshake already in progress"); try { handshake = new ServerHandshake(initialHandshakeDone, this); } catch (NoSuchAlgorithmException nsae) { throw new SSLException(nsae); } break; case CLIENT: try { handshake = new ClientHandshake(this); } catch (NoSuchAlgorithmException nsae) { throw new SSLException(nsae); } break; } } @Override public void closeInbound() { inClosed = true; } @Override public void closeOutbound() { lastAlert = new Alert(Alert.Level.WARNING, Alert.Description.CLOSE_NOTIFY); } @Override public Runnable getDelegatedTask() { if (handshake == null) return null; return handshake.getTask(); } @Override public String[] getEnabledCipherSuites() { return (String[]) enabledSuites.clone(); } @Override public String[] getEnabledProtocols() { return (String[]) enabledProtocols.clone(); } @Override public boolean getEnableSessionCreation() { return createSessions; } @Override public HandshakeStatus getHandshakeStatus() { if (handshake == null) return HandshakeStatus.NOT_HANDSHAKING; return handshake.status(); } @Override public boolean getNeedClientAuth() { return needClientAuth; } @Override public SSLSession getSession() { return session; } @Override public boolean getUseClientMode () { return (mode == Mode.CLIENT); } @Override public boolean getWantClientAuth() { return wantClientAuth; } @Override public boolean isInboundDone() { return inClosed; } @Override public boolean isOutboundDone() { return outClosed; } @Override public void setEnableSessionCreation(final boolean createSessions) { this.createSessions = createSessions; } @Override public void setEnabledCipherSuites(final String[] suites) { if (suites.length == 0) throw new IllegalArgumentException("need at least one suite"); enabledSuites = (String[]) suites.clone(); } @Override public void setEnabledProtocols(final String[] protocols) { if (protocols.length == 0) throw new IllegalArgumentException("need at least one protocol"); enabledProtocols = (String[]) protocols.clone(); } @Override public String[] getSupportedCipherSuites() { // XXX if we ever want to support "pluggable" cipher suites, we'll need // to figure this out. return CipherSuite.availableSuiteNames().toArray(new String[0]); } @Override public String[] getSupportedProtocols() { return new String[] { ProtocolVersion.SSL_3.toString(), ProtocolVersion.TLS_1.toString(), ProtocolVersion.TLS_1_1.toString() }; } @Override public void setNeedClientAuth(final boolean needClientAuth) { this.needClientAuth = needClientAuth; } @Override public void setUseClientMode (final boolean clientMode) { if (clientMode) mode = Mode.CLIENT; else mode = Mode.SERVER; } public @Override void setWantClientAuth(final boolean wantClientAuth) { this.wantClientAuth = wantClientAuth; } public @Override SSLEngineResult unwrap (final ByteBuffer source, final ByteBuffer[] sinks, final int offset, final int length) throws SSLException { if (mode == null) throw new IllegalStateException ("setUseClientMode was never called"); if (inClosed) return new SSLEngineResult(SSLEngineResult.Status.CLOSED, handshakeStatus, 0, 0); if (source.remaining() < 5) { return new SSLEngineResult(SSLEngineResult.Status.BUFFER_UNDERFLOW, handshakeStatus, 0, 0); } Record record = null; boolean helloV2 = false; // XXX: messages may be chunked across multiple records; does this // include the SSLv2 message? I don't think it does, but we should // make sure. if (!getUseClientMode() && (source.get(source.position()) & 0x80) == 0x80) { if (handshake == null) beginHandshake(); int hellolen = source.getShort(source.position()) & 0x7FFF; this.handshake.handleV2Hello(source.slice()); if (!insec.cipherSuite().equals (CipherSuite.TLS_NULL_WITH_NULL_NULL)) throw new SSLException ("received SSLv2 client hello in encrypted " + "session; this is invalid."); if (Debug.DEBUG) logger.log (Component.SSL_RECORD_LAYER, "converting SSLv2 client hello to version 3 hello"); source.getShort(); // skip length ClientHelloV2 v2 = new ClientHelloV2(source.slice()); if (Debug.DEBUG) logger.log(Component.SSL_RECORD_LAYER, "v2 hello: {0}", v2); List<CipherSuite> suites = v2.cipherSpecs(); ClientHelloBuilder hello = new ClientHelloBuilder(); hello.setVersion(v2.version ()); Random random = hello.random(); byte[] challenge = v2.challenge(); if (challenge.length < 32) { byte[] b = new byte[32]; System.arraycopy(challenge, 0, b, b.length - challenge.length, challenge.length); challenge = b; } random.setGmtUnixTime((challenge[0] & 0xFF) << 24 | (challenge[1] & 0xFF) << 16 | (challenge[2] & 0xFF) << 8 | (challenge[3] & 0xFF)); random.setRandomBytes(challenge, 4); byte[] sessionId = v2.sessionId(); hello.setSessionId(sessionId, 0, sessionId.length); hello.setCipherSuites(suites); ArrayList<CompressionMethod> comps = new ArrayList<CompressionMethod>(1); comps.add(CompressionMethod.NULL); hello.setCompressionMethods(comps); record = new Record(ByteBuffer.allocate(hello.length() + 9)); record.setContentType(ContentType.HANDSHAKE); record.setVersion(v2.version()); record.setLength(hello.length() + 4); Handshake handshake = new Handshake(record.fragment()); handshake.setLength(hello.length()); handshake.setType(Handshake.Type.CLIENT_HELLO); handshake.bodyBuffer().put(hello.buffer()); source.position(source.position() + hellolen); helloV2 = true; } else record = new Record(source); ContentType type = record.contentType (); if (Debug.DEBUG) logger.log(Component.SSL_RECORD_LAYER, "input record:\n{0}", record); if (record.length() > session.getPacketBufferSize() - 5) { lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.RECORD_OVERFLOW); throw new AlertException(lastAlert); } ByteBufferOutputStream sysMsg = null; ByteBuffer msg = null; int produced = 0; try { // Application data will get decrypted directly into the user's // output buffers. if (record.contentType() == ContentType.APPLICATION_DATA) produced = insec.decrypt(record, sinks, offset, length); else { if (insec.cipherSuite() == CipherSuite.TLS_NULL_WITH_NULL_NULL) msg = record.fragment(); else { sysMsg = new ByteBufferOutputStream(); insec.decrypt(record, sysMsg); } } // Advance the input buffer past the record we just read. if (!helloV2) source.position(source.position() + record.length() + 5); } catch (BufferOverflowException boe) { // We throw this if the output buffers are not large enough; signal // the caller about this. logger.log(Component.SSL_RECORD_LAYER, "buffer overflow when decrypting", boe); return new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, handshakeStatus, 0, 0); } catch (IllegalBlockSizeException ibse) { lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.BAD_RECORD_MAC); throw new AlertException(lastAlert, ibse); } catch (DataFormatException dfe) { lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.DECOMPRESSION_FAILURE); throw new AlertException(lastAlert, dfe); } catch (MacException me) { lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.BAD_RECORD_MAC); throw new AlertException(lastAlert, me); } catch (ShortBufferException sbe) { // We've messed up if this happens. lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.INTERNAL_ERROR); throw new AlertException(lastAlert, sbe); } SSLEngineResult result = null; // If we need to handle the output here, do it. Otherwise, the output // has been stored in the supplied output buffers. if (sysMsg != null) { if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "sysmessage {0}", sysMsg); msg = sysMsg.buffer(); } if (type == ContentType.CHANGE_CIPHER_SPEC) { // We *may* get a partial message, even though the message is only // one byte long. if (msg.remaining() == 0) { result = new SSLEngineResult (SSLEngineResult.Status.OK, handshakeStatus, record.length() + 5, 0); } else { byte b = msg.get(); if (b != 1) throw new SSLException ("unknown ChangeCipherSpec value: " + (b & 0xFF)); InputSecurityParameters params = handshake.getInputParams(); logger.log (Component.SSL_RECORD_LAYER, "switching to input security parameters {0}", params.cipherSuite()); insec = params; result = new SSLEngineResult (SSLEngineResult.Status.OK, handshakeStatus, record.length() + 5, 0); } } else if (type == ContentType.ALERT) { int len = 0; if (alertBuffer.position() > 0) { alertBuffer.put(msg.get()); len = 1; } if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "processing alerts {0}", Util.wrapBuffer(msg)); len += msg.remaining() / 2; Alert[] alerts = new Alert[len]; int i = 0; if (alertBuffer.position() > 0) { alertBuffer.flip(); alerts[0] = new Alert(alertBuffer); i++; } while (i < alerts.length) { alerts[i++] = new Alert(msg.duplicate()); msg.position(msg.position() + 2); } if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "alerts: {0}", alerts.length); for (i = 0; i < alerts.length; i++) { if (alerts[i].level() == Alert.Level.FATAL) throw new AlertException(alerts[i], false); if (alerts[i].description() != Alert.Description.CLOSE_NOTIFY) logger.log(java.util.logging.Level.WARNING, "received alert: {0}", alerts[i]); if (alerts[i].description() == Alert.Description.CLOSE_NOTIFY) inClosed = true; } if (msg.hasRemaining()) alertBuffer.position(0).limit(2); result = new SSLEngineResult (SSLEngineResult.Status.OK, handshakeStatus, record.length() + 5, 0); } else if (type == ContentType.HANDSHAKE) { if (handshake == null) beginHandshake(); try { handshakeStatus = handshake.handleInput(msg); } catch (AlertException ae) { lastAlert = ae.alert(); return new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NEED_WRAP, 0, 0); } if (Debug.DEBUG) logger.logv(Component.SSL_HANDSHAKE, "handshake status {0}", handshakeStatus); result = new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, record.length() + 5, 0); if (handshakeStatus == HandshakeStatus.FINISHED) { handshake = null; handshakeStatus = HandshakeStatus.NOT_HANDSHAKING; } } else if (type == ContentType.APPLICATION_DATA) { // Do nothing more; the application data has been put into // the output buffers. result = new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, record.length() + 5, produced); } else { SSLRecordHandler handler = handlers[type.getValue()]; if (handler != null) { result = new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, record.length() + 5, 0); } else throw new SSLException ("unknown content type: " + type); } if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "return result: {0}", result); return result; } public @Override SSLEngineResult wrap (ByteBuffer[] sources, int offset, int length, ByteBuffer sink) throws SSLException { if (mode == null) throw new IllegalStateException ("setUseClientMode was never called"); if (outClosed) return new SSLEngineResult(SSLEngineResult.Status.CLOSED, handshakeStatus, 0, 0); ContentType type = null; ByteBuffer sysMessage = null; if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "wrap {0} {1} {2} {3} / {4}", sources, offset, length, sink, getHandshakeStatus()); if (lastAlert != null) { type = ContentType.ALERT; sysMessage = ByteBuffer.allocate(2); Alert alert = new Alert(sysMessage); alert.setDescription(lastAlert.description()); alert.setLevel(lastAlert.level()); if (lastAlert.description() == Alert.Description.CLOSE_NOTIFY) outClosed = true; } else if (changeCipherSpec) { type = ContentType.CHANGE_CIPHER_SPEC; sysMessage = ByteBuffer.allocate(1); sysMessage.put(0, (byte) 1); } else if (getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP) { // If we are not encrypting, optimize the handshake to fill // the buffer directly. if (outsec.suite() == CipherSuite.TLS_NULL_WITH_NULL_NULL) { int orig = sink.position(); sink.order(ByteOrder.BIG_ENDIAN); sink.put((byte) ContentType.HANDSHAKE.getValue()); sink.putShort((short) session.version.rawValue()); sink.putShort((short) 0); handshakeStatus = handshake.handleOutput(sink); int produced = sink.position() - orig; sink.putShort(orig + 3, (short) (produced - 5)); if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "emitting record:\n{0}", new Record((ByteBuffer) sink.duplicate().position(orig))); SSLEngineResult result = new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, 0, produced); // Note, this will only happen if we transition from // TLS_NULL_WITH_NULL_NULL *to* TLS_NULL_WITH_NULL_NULL, which // doesn't make a lot of sense, but we support it anyway. if (handshakeStatus == HandshakeStatus.FINISHED) { handshake = null; // finished with it. handshakeStatus = HandshakeStatus.NOT_HANDSHAKING; } return result; } // Rough guideline; XXX. sysMessage = ByteBuffer.allocate(sink.remaining() - 2048); type = ContentType.HANDSHAKE; try { handshakeStatus = handshake.handleOutput(sysMessage); } catch (AlertException ae) { lastAlert = ae.alert(); return new SSLEngineResult(Status.OK, HandshakeStatus.NEED_WRAP, 0, 0); } sysMessage.flip(); if (Debug.DEBUG) logger.logv(Component.SSL_HANDSHAKE, "handshake status {0}", handshakeStatus); } int produced = 0; int consumed = 0; try { int orig = sink.position(); int[] inout = null; if (sysMessage != null) { if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "encrypt system message {0} to {1}", sysMessage, sink); inout = outsec.encrypt(new ByteBuffer[] { sysMessage }, 0, 1, type, sink); produced = inout[1]; } else { inout = outsec.encrypt(sources, offset, length, ContentType.APPLICATION_DATA, sink); consumed = inout[0]; produced = inout[1]; } if (Debug.DEBUG) logger.logv(Component.SSL_RECORD_LAYER, "emitting record:\n{0}", new Record((ByteBuffer) sink.duplicate().position(orig).limit(produced))); } catch (ShortBufferException sbe) { // We don't expect this to happen, except for bugs; signal an // internal error. lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.INTERNAL_ERROR); return new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, 0, 0); } catch (IllegalBlockSizeException ibse) { // We don't expect this to happen, except for bugs; signal an // internal error. lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.INTERNAL_ERROR); return new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, 0, 0); } catch (DataFormatException dfe) { // We don't expect this to happen; signal an internal error. lastAlert = new Alert(Alert.Level.FATAL, Alert.Description.INTERNAL_ERROR); return new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, 0, 0); } if (lastAlert != null && lastAlert.level() == Alert.Level.FATAL) { AlertException ae = new AlertException(lastAlert); lastAlert = null; throw ae; } if (changeCipherSpec) { outsec = handshake.getOutputParams(); changeCipherSpec = false; } SSLEngineResult result = new SSLEngineResult(outClosed ? SSLEngineResult.Status.CLOSED : SSLEngineResult.Status.OK, handshakeStatus, consumed, produced); if (handshakeStatus == HandshakeStatus.FINISHED) { handshake = null; // done with it. handshakeStatus = HandshakeStatus.NOT_HANDSHAKING; } return result; } // Package-private methods. SessionImpl session () { return session; } void setSession(SessionImpl session) { this.session = session; } void changeCipherSpec() { changeCipherSpec = true; } }