/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2009 Sun Microsystems, Inc. * Portions copyright 2012 ForgeRock AS. */ package org.opends.server.extensions; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.ClosedChannelException; import java.security.cert.Certificate; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import javax.net.ssl.*; import org.opends.server.admin.std.server.LDAPConnectionHandlerCfg; import org.opends.server.api.ClientConnection; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.DebugLogLevel; /** * A class that provides a TLS byte channel implementation. */ public class TLSByteChannel implements ByteChannel, ConnectionSecurityProvider { private static final DebugTracer TRACER = getTracer(); private final ByteChannel socketChannel; private final SSLEngine sslEngine; // read copy to buffer private final ByteBuffer appData; // read encrypted private final ByteBuffer appNetData; // Write encrypted private final ByteBuffer netData; private final ByteBuffer tempData; private final int sslBufferSize; private final int appBufSize; private boolean reading = false; // Map of cipher phrases to effective key size (bits). Taken from the // following RFCs: 5289, 4346, 3268,4132 and 4162. private static final Map cipherMap; static { cipherMap = new LinkedHashMap(); cipherMap.put("_WITH_AES_256_CBC_", new Integer(256)); cipherMap.put("_WITH_CAMELLIA_256_CBC_", new Integer(256)); cipherMap.put("_WITH_AES_256_GCM_", new Integer(256)); cipherMap.put("_WITH_3DES_EDE_CBC_", new Integer(112)); cipherMap.put("_WITH_AES_128_GCM_", new Integer(128)); cipherMap.put("_WITH_SEED_CBC_", new Integer(128)); cipherMap.put("_WITH_CAMELLIA_128_CBC_", new Integer(128)); cipherMap.put("_WITH_AES_128_CBC_", new Integer(128)); cipherMap.put("_WITH_IDEA_CBC_", new Integer(128)); cipherMap.put("_WITH_DES_CBC_", new Integer(56)); cipherMap.put("_WITH_RC2_CBC_40_", new Integer(40)); cipherMap.put("_WITH_RC4_40_", new Integer(40)); cipherMap.put("_WITH_DES40_CBC_", new Integer(40)); cipherMap.put("_WITH_NULL_", new Integer(0)); }; /** * Create an TLS byte channel instance using the specified LDAP connection * configuration, client connection, SSL context and socket channel * parameters. * * @param config * The LDAP connection configuration. * @param c * The client connection. * @param sslContext * The SSL context. * @param socketChannel * The socket channel. * @return A TLS capable byte channel. */ public static TLSByteChannel getTLSByteChannel( final LDAPConnectionHandlerCfg config, final ClientConnection c, final SSLContext sslContext, final ByteChannel socketChannel) { return new TLSByteChannel(config, c, socketChannel, sslContext); } private TLSByteChannel(final LDAPConnectionHandlerCfg config, final ClientConnection c, final ByteChannel socketChannel, final SSLContext sslContext) { this.socketChannel = socketChannel; // getHostName could potentially be very expensive and could block // the connection handler for several minutes. (See issue 4229) // Accepting new connections should be done in a seperate thread to // avoid blocking new connections. Just remove for now to prevent // potential DoS attacks. SSL sessions will not be reused and some // cipher suites (such as Kerberos) will not work. // String hostName = socketChannel.socket().getInetAddress().getHostName(); // int port = socketChannel.socket().getPort(); // sslEngine = sslContext.createSSLEngine(hostName, port); sslEngine = sslContext.createSSLEngine(); sslEngine.setUseClientMode(false); final Set protocols = config.getSSLProtocol(); if (!protocols.isEmpty()) { sslEngine.setEnabledProtocols(protocols.toArray(new String[0])); } final Set ciphers = config.getSSLCipherSuite(); if (!ciphers.isEmpty()) { sslEngine.setEnabledCipherSuites(ciphers.toArray(new String[0])); } switch (config.getSSLClientAuthPolicy()) { case DISABLED: sslEngine.setNeedClientAuth(false); sslEngine.setWantClientAuth(false); break; case REQUIRED: sslEngine.setWantClientAuth(true); sslEngine.setNeedClientAuth(true); break; case OPTIONAL: default: sslEngine.setNeedClientAuth(false); sslEngine.setWantClientAuth(true); break; } final SSLSession sslSession = sslEngine.getSession(); sslBufferSize = sslSession.getPacketBufferSize(); appBufSize = sslSession.getApplicationBufferSize(); appNetData = ByteBuffer.allocate(sslBufferSize); netData = ByteBuffer.allocate(sslBufferSize); appData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); tempData = ByteBuffer.allocate(sslSession.getApplicationBufferSize()); } /** * {@inheritDoc} */ public synchronized void close() throws IOException { sslEngine.closeInbound(); sslEngine.closeOutbound(); final SSLEngineResult.HandshakeStatus hsStatus = sslEngine .getHandshakeStatus(); if (hsStatus != SSLEngineResult.HandshakeStatus.FINISHED && hsStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { doHandshakeWrite(hsStatus); } } /** * {@inheritDoc} */ public int getAppBufSize() { return appBufSize; } /** * {@inheritDoc} */ public Certificate[] getClientCertificateChain() { try { return sslEngine.getSession().getPeerCertificates(); } catch (final SSLPeerUnverifiedException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } return new Certificate[0]; } } /** * {@inheritDoc} */ public String getName() { return "TLS"; } /** * {@inheritDoc} */ public int getSSF() { int cipherKeySSF = 0; final String cipherString = sslEngine.getSession().getCipherSuite(); for (final Map.Entry mapEntry : cipherMap.entrySet()) { if (cipherString.indexOf(mapEntry.getKey()) >= 0) { cipherKeySSF = mapEntry.getValue().intValue(); break; } } return cipherKeySSF; } /** * {@inheritDoc} */ public boolean isOpen() { if (sslEngine.isInboundDone() || sslEngine.isOutboundDone()) { return false; } return true; } /** * {@inheritDoc} */ public boolean isSecure() { return true; } /** * {@inheritDoc} */ public synchronized int read(final ByteBuffer clearBuffer) throws IOException { SSLEngineResult.HandshakeStatus hsStatus; if (!reading) { appNetData.clear(); } else { reading = false; } if (!socketChannel.isOpen()) { return -1; } if (sslEngine.isInboundDone()) { return -1; } do { final int wrappedBytes = socketChannel.read(appNetData); appNetData.flip(); if (wrappedBytes == -1) { return -1; } hsStatus = sslEngine.getHandshakeStatus(); if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) { doHandshakeRead(hsStatus); } if (wrappedBytes == 0) { return 0; } while (appNetData.hasRemaining()) { appData.clear(); final SSLEngineResult res = sslEngine.unwrap(appNetData, appData); appData.flip(); if (res.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) { appNetData.compact(); reading = true; break; } else if (res.getStatus() != SSLEngineResult.Status.OK) { return -1; } hsStatus = sslEngine.getHandshakeStatus(); if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) { doHandshakeOp(hsStatus); } clearBuffer.put(appData); } hsStatus = sslEngine.getHandshakeStatus(); } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP); return clearBuffer.position(); } /** * {@inheritDoc} */ public ByteChannel wrapChannel(final ByteChannel channel) { return this; } /** * {@inheritDoc} */ public synchronized int write(final ByteBuffer clearData) throws IOException { if (!socketChannel.isOpen() || sslEngine.isOutboundDone()) { throw new ClosedChannelException(); } final int originalPosition = clearData.position(); final int originalLimit = clearData.limit(); final int length = originalLimit - originalPosition; if (length > sslBufferSize) { int pos = originalPosition; int lim = originalPosition + sslBufferSize; while (pos < originalLimit) { clearData.position(pos); clearData.limit(lim); writeInternal(clearData); pos = lim; lim = Math.min(originalLimit, pos + sslBufferSize); } return length; } else { return writeInternal(clearData); } } private void doHandshakeOp(SSLEngineResult.HandshakeStatus hsStatus) throws IOException { SSLEngineResult res; switch (hsStatus) { case NEED_TASK: hsStatus = doTasks(); break; case NEED_WRAP: tempData.clear(); netData.clear(); res = sslEngine.wrap(tempData, netData); hsStatus = res.getHandshakeStatus(); netData.flip(); while (netData.hasRemaining()) { socketChannel.write(netData); } hsStatus = sslEngine.getHandshakeStatus(); return; default: return; } } private void doHandshakeRead(SSLEngineResult.HandshakeStatus hsStatus) throws IOException { do { doHandshakeOp(hsStatus); hsStatus = sslEngine.getHandshakeStatus(); } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP || hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK); } private void doHandshakeUnwrap() throws IOException { netData.clear(); tempData.clear(); final int bytesRead = socketChannel.read(netData); if (bytesRead <= 0) { throw new ClosedChannelException(); } else { sslEngine.unwrap(netData, tempData); } } private void doHandshakeWrite(SSLEngineResult.HandshakeStatus hsStatus) throws IOException { do { if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { doHandshakeUnwrap(); } else { doHandshakeOp(hsStatus); } hsStatus = sslEngine.getHandshakeStatus(); } while (hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP || hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP); } private SSLEngineResult.HandshakeStatus doTasks() { Runnable task; while ((task = sslEngine.getDelegatedTask()) != null) { task.run(); } return sslEngine.getHandshakeStatus(); } private int writeInternal(final ByteBuffer clearData) throws IOException { int totBytesSent = 0; SSLEngineResult.HandshakeStatus hsStatus; hsStatus = sslEngine.getHandshakeStatus(); if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP || hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { doHandshakeWrite(hsStatus); } while (clearData.hasRemaining()) { netData.clear(); final SSLEngineResult res = sslEngine.wrap(clearData, netData); netData.flip(); if (netData.remaining() == 0) { // wrap didn't produce any data from our clear buffer. // Throw exception to prevent looping. throw new SSLException("SSLEngine.wrap produced 0 bytes"); } if (res.getStatus() != SSLEngineResult.Status.OK) { throw new ClosedChannelException(); } if (hsStatus == SSLEngineResult.HandshakeStatus.NEED_TASK || hsStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP || hsStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { doHandshakeWrite(hsStatus); } totBytesSent += socketChannel.write(netData); } return totBytesSent; } }