SSL.java
/*
** Module : SSL.java
** Abstract : Implements abstract SSL FSM on the top of SSLEngine.
**
** Copyright (c) 2016-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 IAS 20160805 Initial version
** 002 IAS 20200729 Process large messages
** 003 IAS 20210310 Added incomingBuffersGuard to prevent race conditions
** IAS 20210323 Re-worked synchronization logic and added logging.
** IAS 20210608 Provide more details on the unwrap() failure.
** IAS 20210827 Fixed sporadic SSL failures
** IAS 20220624 Added support for the Conscrypt JCE/JSSE provider
** IAS 20220626 Added SSL JMX counters.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 EVL 20231124 All byte buffers are now direct ones to improve performance.
** 006 HC 20240222 Enabled JMX on FWD Client.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.net;
import java.io.*;
import java.nio.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;
import java.util.logging.*;
import javax.net.ssl.*;
import javax.net.ssl.SSLEngineResult.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.util.*;
/**
* Implements abstract SSL FSM.
*/
public abstract class SSL
implements Runnable
{
/** Logger */
protected static final CentralLogger LOG = CentralLogger.get(SSL.class.getName(), true, true);
/** Set of HandshakeStatus values after the end of SSL handshake */
public static final Set<HandshakeStatus> AFTER_HANDSHAKE = EnumSet.of(HandshakeStatus.NOT_HANDSHAKING,
HandshakeStatus.FINISHED);
/** timer for measuring send time */
private static final NanoTimer SEND_TIMER = NanoTimer.getInstance(
FwdServerJMX.TimeStat.sslSend
);
/** timer for measuring wrap time */
private static final NanoTimer WRAP_TIMER = NanoTimer.getInstance(
FwdServerJMX.TimeStat.sslWrap
);
/** timer for measuring wrap time */
private static final NanoTimer UNWRAP_TIMER = NanoTimer.getInstance(
FwdServerJMX.TimeStat.sslUnwrap
);
/** SSLEngine instance */
protected final SSLEngine engine;
/** ExecutorService for internal tasks' execution */
protected final ExecutorService fsmWorkers;
/** Application buffer size */
protected final int appBufferMax;
/** Packet buffer size */
protected final int netBufferMax;
/** Incoming buffers */
protected final ByteBuffer outWrap, outUnwrap;
/** Outgoing buffers */
protected final ByteBuffer inpWrap, inpUnwrap;
/** Handshake done flag */
protected volatile AtomicBoolean done = new AtomicBoolean(false);
/** Input chunks' queue */
protected final java.util.Queue<ByteBuffer> incomingBuffers = new ConcurrentLinkedQueue <>();
/** Input stream guard */
protected final ReentrantLock inpGuard = new ReentrantLock(true);
/** Output stream guard */
protected final ReentrantLock outGuard = new ReentrantLock(true);
/** Max message size */
private final int maxMessageSize;
/** Dump SSL packets flasg */
private final boolean dumpSSL;
/** Use packet seqNo instrumentation */
private final boolean trackSeqNo;
/** Input packet seqNo holder */
protected final AtomicLong inpSeqNo = new AtomicLong(0);
/** Output packet seqNo generator */
protected final AtomicLong outSeqNo = new AtomicLong(0);
/** Current packet seqNo*/
protected long cSeqNo = -1;
/** Use packet seqNo instrumentation */
protected boolean checkSeqNo = true;
/** dump halper */
protected final HexDump hex = new HexDump(s -> LOG.log(Level.WARNING, s), 256);
/**
* Constructor
*
* @param engine
* the SSLEngine instance
* @param fsmWorkers
* ExecutorService for internal tasks' execution
*/
public SSL(SSLEngine engine, ExecutorService fsmWorkers)
{
if (LOG.isLoggable(Level.FINER))
{
LOG.log(Level.FINER, "SSLEngine class: " + engine.getClass().getName());
}
int delta = engine.getClass().getName().startsWith("org.conscrypt") ? 0 : 50;
// the wrap buffers' size adjustment and the value of delta where added
// based on multiple experiments.
SSLSession session = engine.getSession();
this.appBufferMax = session.getApplicationBufferSize();
this.netBufferMax = session.getPacketBufferSize();
LOG.log(Level.FINE, String.format("appBufferMax: %d, netBufferMax: %d", appBufferMax, netBufferMax));
this.outWrap = ByteBuffer.allocateDirect(appBufferMax + delta);
this.inpWrap = ByteBuffer.allocateDirect(appBufferMax + 50);
this.outUnwrap = ByteBuffer.allocateDirect(2 * netBufferMax);
this.inpUnwrap = ByteBuffer.allocateDirect(2 * netBufferMax);
// EVL: this is strange call this.outUnwrap.limit(0);
// made twice this.outUnwrap.limit(0);
this.maxMessageSize = appBufferMax - 50 + delta;
this.engine = engine;
this.fsmWorkers = fsmWorkers;
BootstrapConfig bc = SessionManager.get().config();
this.dumpSSL = bc.getBoolean("net", "ssl", "dump", false);
this.trackSeqNo = bc.getBoolean("net", "ssl", "trackSeqNo", false);
this.checkSeqNo = trackSeqNo;
}
/**
* Check if handshake is finished
* @return <code>true</code> if handshake is finished
*/
public boolean handshakeDone()
{
return done.get();
}
/**
* Accept the next decrypted portion of the input
*
* @param decrypted
* the next decrypted portion of the input
*/
public abstract void nextChunk(ByteBuffer decrypted);
/**
* Check if more input is available for processing
* @throws IOException
* on network failure
*/
public abstract void checkInput() throws IOException;
/**
* Process the next portion of the encrypted output
*
* @param encrypted
* the next portion of the encrypted output
*/
public abstract void onOutput(ByteBuffer encrypted);
/**
* Called on the SSL session close
*/
public abstract void onClosed();
/**
* Process the next portion of the decrypted input
*
* @param decrypted
* the next portion of the decrypted input
*/
public void onInput(ByteBuffer decrypted)
{
log("onInput(%s)", decrypted.remaining());
nextChunk(decrypted);
}
/**
* Process the next portion of the output
*
* @param data
* the next portion of the output
*/
public void send(final ByteBuffer data)
{
tryLock(outGuard);
try
{
SEND_TIMER.start();
outWrap.put(data);
run();
}
finally
{
SEND_TIMER.stop();
outGuard.unlock();
}
}
/**
* Notify about new portion of the input
*
* @param data
* a new portion of the input
*/
public void notify(final ByteBuffer data)
{
if (dumpSSL)
{
hex.dump("notify", data, false);
}
log("notify(%s)", data.remaining());
incomingBuffers.add(data);
tryLock(inpGuard);
try
{
while (appendIncoming())
;
}
finally
{
inpGuard.unlock();
}
run();
}
/**
* Perform next FSM step
*/
@Override
public void run()
{
while (this.handshake())
{
continue;
}
}
/**
* Report the handshake failure
*
* @param ex
* The exception which caused the failure
*/
protected abstract void onFailure(Exception ex);
/**
* Report the handshake success
*/
protected void onSuccess()
{
if (done.compareAndSet(false, true))
{
LOG.log(Level.FINE, "done");
}
}
/**
* Get maximal message size
*
* @return maximal message size;
*/
protected int getMaxMessageSize()
{
return this.maxMessageSize;
}
/**
* Write message to stderr
*
* @param message
* message to be written
*/
protected void log(String message)
{
if (LOG.isLoggable(Level.FINER) && !done.get())
{
LOG.log(Level.FINER, message);
}
}
/**
* Write a message to stdout using the specified format string and
* arguments.
*
* @param fmt
* A format string
*
* @param args
* Arguments referenced by the format specifiers in the format
* string.
*/
protected void log(String fmt, Object... args)
{
if (LOG.isLoggable(Level.FINER) && !done.get())
{
log(String.format(fmt, args));
}
}
/**
* Timed lock acquisition
* @param lock
* lock to be acquired
*/
protected void tryLock(Lock lock)
{
try
{
if (!lock.tryLock(100, TimeUnit.MILLISECONDS))
{
LOG.log(Level.WARNING, "Faild to acquire lock", new Exception());
throw new RuntimeException("Faild to acquire lock");
}
}
catch (InterruptedException e)
{
LOG.log(Level.WARNING, "InterruptedException", e);
throw new RuntimeException("Faild to acquire lock");
}
}
/**
* Append input data to the processing buffer if possible. This is a precaution
* against possible BufferOverflow which where experienced in some rare situations
*
* @return <code>true</code> if data was appended to the incoming buffer
*/
private boolean appendIncoming()
{
ByteBuffer bb = incomingBuffers.peek();
if (bb == null)
{
return false;
}
if (dumpSSL)
{
hex.dump("append", bb, false);
}
int remaining = outUnwrap.remaining();
int pended = bb.remaining();
if (pended <= remaining)
{
try
{
outUnwrap.put(bb);
incomingBuffers.remove();
}
catch (BufferOverflowException e)
{
LOG.log(Level.WARNING,
String.format("appendIncoming() failed: "+
"remaining: %d, pended: %d, "+
"outUnwrap.position(: %d), outUnwrap.remaining(): %d",
remaining, pended, outUnwrap.position(), outUnwrap.remaining()), e);
return false;
}
log("appendIncoming(): remaining: %d, pended: %d", remaining, pended);
if (dumpSSL)
{
hex.dump("after append", outUnwrap, true);
}
return true;
}
else if (remaining > 0)
{
byte[] b = new byte[remaining];
bb.get(b);
bb.compact().flip();
try
{
outUnwrap.put(b);
}
catch (BufferOverflowException e)
{
LOG.log(Level.WARNING,
String.format("appendIncoming() failed: "+
"remaining: %d, pended: %d, "+
"outUnwrap.position(: %d), outUnwrap.remaining(): %d",
remaining, b.length, outUnwrap.position(), outUnwrap.remaining()), e);
return false;
}
log("appendIncoming(): remaining: %d, written: %d, bb.remaining(): %d",
remaining, pended, bb.remaining());
return false;
}
// OM : got these messages: cause under investigation
System.out.println(
"Potential buffer overflow prevented (bytes pended:" + pended + ", remaining:" + remaining + ")");
return false;
}
/**
* Make a next step of handshake or process input/output data
*
* @return <code>true</code> if the step was successful and the engine is ready for
* the next step
*/
private boolean handshake()
{
HandshakeStatus handshakeStatus = engine.getHandshakeStatus();
log("engine.getHandshakeStatus(): %s", handshakeStatus);
switch (handshakeStatus)
{
case NOT_HANDSHAKING:
boolean havePendingData = false;
{
if (outWrap.position() > 0)
havePendingData |= this.wrap();
if (outUnwrap.position() > 0)
havePendingData |= this.unwrap();
}
if (done.compareAndSet(false, true))
{
log("done");
}
return havePendingData;
case NEED_WRAP:
if (!this.wrap())
return false;
break;
case NEED_UNWRAP:
if (!this.unwrap())
return false;
break;
case NEED_TASK:
final Runnable sslTask = engine.getDelegatedTask();
if (sslTask != null)
{
fsmWorkers.execute(() -> {
sslTask.run();
run();
});
}
return false;
case FINISHED:
throw new IllegalStateException("FINISHED");
}
return true;
}
/**
* Encrypt the next portion of output (guarded).
*
* @return <code>true</code> if the operation was successful
*/
private boolean wrap()
{
tryLock(outGuard);
try
{
WRAP_TIMER.start();
return doWrap();
}
finally
{
WRAP_TIMER.stop();
outGuard.unlock();
}
}
/**
* Encrypt the next portion of output.
*
* @return <code>true</code> if the operation was successful
*/
private boolean doWrap()
{
log("wrap()");
if (trackSeqNo && checkSeqNo && engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING)
{
inpWrap.putLong(outSeqNo.incrementAndGet());
}
SSLEngineResult wrapResult;
try
{
if (dumpSSL)
{
hex.dump("wrapping", outWrap,true);
hex.dump("before wrap", inpWrap, true);
}
outWrap.flip();
wrapResult = engine.wrap(outWrap, inpWrap);
outWrap.compact();
if (dumpSSL)
{
hex.dump("after wrap", inpWrap, true);
}
}
catch (SSLException exc)
{
SSLSession session = engine.getSession();
LOG.log(Level.WARNING,
String.format("wrap() failed for %s; %s; [%d, %d] ",
session.getProtocol(), session.toString(),
outWrap.position(), outWrap.limit()
),
exc);
this.onFailure(exc);
return false;
}
Status status = wrapResult.getStatus();
log("wrapResult.getStatus(): %s", status);
switch (status)
{
case OK:
if (inpWrap.position() > 0)
{
inpWrap.flip();
this.onOutput(inpWrap);
inpWrap.compact();
}
break;
case BUFFER_UNDERFLOW:
// try again later
break;
case BUFFER_OVERFLOW:
throw new IllegalStateException("failed to wrap - buffer overflow");
case CLOSED:
this.onClosed();
return false;
}
return true;
}
/**
* Decrypt the next portion of input (guarded).
*
* @return <code>true</code> if the operation was successful
*/
private boolean unwrap()
{
tryLock(inpGuard);
try
{
UNWRAP_TIMER.start();
return doUnwrap();
}
finally
{
UNWRAP_TIMER.stop();
inpGuard.unlock();
}
}
/**
* Decrypt the next portion of input.
*
* @return <code>true</code> if the operation was successful
*/
private boolean doUnwrap()
{
SSLEngineResult unwrapResult = null;
try
{
outUnwrap.flip();
if (dumpSSL)
{
hex.dump("before unwrap", outUnwrap, false);
}
if (trackSeqNo && engine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING &&
outUnwrap.remaining() >= 8)
{
if (cSeqNo < 0)
{
outUnwrap.mark();
cSeqNo = outUnwrap.getLong();
if (cSeqNo >> 48 != 0)
{
outUnwrap.reset();
checkSeqNo = false;
}
// LOG.warning(
// String.format("cSeqNo:%x; inpSeqNo:%x; %s", cSeqNo, inpSeqNo.get(),
// engine.getHandshakeStatus())
// );
if (checkSeqNo && !inpSeqNo.compareAndSet(cSeqNo-1, cSeqNo))
{
throw new SSLException(
String.format("Out ot order: prev = %x, current = %x; %s",
inpSeqNo.get(), cSeqNo, engine.getHandshakeStatus())
);
}
}
}
unwrapResult = engine.unwrap(outUnwrap, inpUnwrap);
outUnwrap.compact();
ByteBuffer decoded = inpUnwrap.duplicate();
decoded.flip();
if (decoded.limit() > 0)
{
cSeqNo = -1;
}
if (dumpSSL)
{
hex.dump("after unwrap", decoded, false);
}
}
catch (SSLException ex)
{
SSLSession session = engine.getSession();
LOG.log(Level.WARNING,
String.format("%s: unwrap() failed for %s; %s; [%d, %d] ",
Thread.currentThread().getName(),
session.getProtocol(), session.toString(),
outUnwrap.position(), outUnwrap.limit()
),
ex);
this.onFailure(ex);
return false;
}
Status status = unwrapResult.getStatus();
log("unwrapResult.getStatus(): %s, got: %d", status, inpUnwrap.position());
switch (status)
{
case OK:
if (inpUnwrap.position() > 0)
{
inpUnwrap.flip();
this.onInput(inpUnwrap);
inpUnwrap.compact();
}
break;
case CLOSED:
this.onClosed();
return false;
case BUFFER_OVERFLOW:
throw new IllegalStateException("failed to unwrap - buffer overflow");
case BUFFER_UNDERFLOW:
return false;
}
HandshakeStatus handshakeStatus = unwrapResult.getHandshakeStatus();
log("unwrapResult.getHandshakeStatus()): %s", handshakeStatus);
if (handshakeStatus == HandshakeStatus.FINISHED)
{
this.onSuccess();
return false;
}
return true;
}
}