gov.nist.javax.sip.stack.SSLStateMachine Maven / Gradle / Ivy
/*
* Conditions Of Use
*
* This software was developed by employees of the National Institute of
* Standards and Technology (NIST), an agency of the Federal Government.
* Pursuant to title 15 Untied States Code Section 105, works of NIST
* employees are not subject to copyright protection in the United States
* and are considered to be in the public domain. As a result, a formal
* license is not needed to use the software.
*
* This software is provided by NIST as a service and is expressly
* provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
* OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
* AND DATA ACCURACY. NIST does not warrant or make any representations
* regarding the use of the software or the results thereof, including but
* not limited to the correctness, accuracy, reliability or usefulness of
* the software.
*
* Permission to use this software is contingent upon your acceptance
* of the terms of this agreement
*
* .
*
*/
package gov.nist.javax.sip.stack;
import gov.nist.core.CommonLogger;
import gov.nist.core.LogWriter;
import gov.nist.core.StackLogger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Queue;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLPeerUnverifiedException;
/**
* This is a helper state machine that negotiates the SSL connection automatically
* without ever blocking any threads. It is important not to block here because
* the TLS may occur in the selector thread which is one per all sockets.
*
* Other than that the state machine is able to handle partial chunks of SIP messages
* and only supply them when they are ready to the original TCP channel once they are
* decrypted.
*
* @author vladimirralev
*
*/
public class SSLStateMachine {
private static StackLogger logger = CommonLogger.getLogger(SSLStateMachine.class);
public final static ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(new byte[] {});
protected SSLEngine sslEngine;
protected Queue pendingOutboundBuffers =
new LinkedList();
protected NioTlsChannelInterface channel;
protected ByteBuffer tlsRecordBuffer;
private Object unwrapLock = new Object();
private Object wrapLock = new Object();
public SSLStateMachine(SSLEngine sslEngine, NioTlsChannelInterface channel) {
this.sslEngine = sslEngine;
this.channel = channel;
}
public void wrapRemaining() throws IOException {
wrap(null, channel.prepareEncryptedDataBuffer(), null);
}
public void wrap(ByteBuffer src, ByteBuffer dst,
MessageSendCallback callback) throws IOException {
synchronized (wrapLock) {
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Wrapping " + src + ", buffers size " + pendingOutboundBuffers.size());
}
// Null src means we just have no ne data but still want to push any previously queued data
if(src != null) {
pendingOutboundBuffers.offer(new MessageSendItem(src, callback));
}
int iter = 0;
loop:while(true) {
iter ++;
MessageSendItem currentBuffer = pendingOutboundBuffers.peek();
// If there is no queued operations break out of the loop
if(currentBuffer == null) break;
SSLEngineResult result;
try {
result = sslEngine.wrap(currentBuffer.message, dst);
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Wrap result " + result + " buffers size " + pendingOutboundBuffers.size());
}
} finally {
if(!currentBuffer.message.hasRemaining()) {
pendingOutboundBuffers.remove();
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("REMOVED item from encryption queue because it has no more data, all is done, buffers size now is "
+ pendingOutboundBuffers.size() + " current buffer is " + currentBuffer);
}
}
}
int remaining = currentBuffer.message.remaining();
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Remaining " + remaining + " queue size is " + pendingOutboundBuffers.size());
}
if(result.bytesProduced() > 0) {
// produced > 0 means encryption was successful and we have something to send over the wire
dst.flip();
byte[] bytes = new byte[dst.remaining()];
dst.get(bytes);
if(currentBuffer.getCallBack() != null) {
// Send using message channel (it discriminates between client/server and new/old connecitons)
currentBuffer.getCallBack().doSend(bytes);
} else {
// Send using the existing connection without attempting to guess client or server etc
sendSSLMetadata(bytes);
}
dst.clear();
} else {
switch (result.getHandshakeStatus()) {
case NEED_WRAP:
if (currentBuffer.message.hasRemaining()) {
break;
} else {
break loop;
}
case NEED_UNWRAP:
break loop;
case NEED_TASK:
runDelegatedTasks(result);
break;
case FINISHED:
// Added for https://java.net/jira/browse/JSIP-483
if(channel instanceof NioTlsMessageChannel) {
((NioTlsMessageChannel)channel).setHandshakeCompleted(true);
if(sslEngine.getSession() != null) {
if(!ClientAuthType.Disabled.equals(channel.getSIPStack().getClientAuth()) && !ClientAuthType.DisabledAll.equals(channel.getSIPStack().getClientAuth())) {
// https://java.net/jira/browse/JSIP-483 Don't try to get the PeerCertificates if the client auth is Disabled or DisabledAll as they won't be available
try {
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setPeerCertificates(sslEngine.getSession().getPeerCertificates());
} catch (SSLPeerUnverifiedException e) {
// no op if -Dgov.nist.javax.sip.TLS_CLIENT_AUTH_TYPE=Disabled is used, no peer certificates will be available
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("sslEngine.getSession().getPeerCertificates() are not available, which is normal if running with gov.nist.javax.sip.TLS_CLIENT_AUTH_TYPE=Disabled");
}
}
}
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setLocalCertificates(sslEngine.getSession().getLocalCertificates());
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setCipherSuite(sslEngine.getSession().getCipherSuite());
}
}
break;
case NOT_HANDSHAKING:
break loop;
default:
break;
}
}
}
}
}
private void wrapNonAppData() throws Exception {
ByteBuffer encryptedDataBuffer = channel.prepareEncryptedDataBuffer();
SSLEngineResult result;
try {
loop:while(true) {
result = sslEngine.wrap(EMPTY_BUFFER, encryptedDataBuffer);
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("NonAppWrap result " + result + " buffers size "
+ pendingOutboundBuffers.size());
}
if (result.bytesProduced() > 0) {
// any output here is internal TLS metadata such as handshakes
encryptedDataBuffer.flip();
byte[] msg = new byte[encryptedDataBuffer.remaining()];
encryptedDataBuffer.get(msg);
// send it directly over the wire without further processing or parsing
sendSSLMetadata(msg);
encryptedDataBuffer.clear();
}
switch (result.getHandshakeStatus()) {
case FINISHED:
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Handshake complete!");
}
// Added for https://java.net/jira/browse/JSIP-483
if(channel instanceof NioTlsMessageChannel) {
((NioTlsMessageChannel)channel).setHandshakeCompleted(true);
if(sslEngine.getSession() != null) {
if(!ClientAuthType.Disabled.equals(channel.getSIPStack().getClientAuth()) && !ClientAuthType.DisabledAll.equals(channel.getSIPStack().getClientAuth())) {
// https://java.net/jira/browse/JSIP-483 Don't try to get the PeerCertificates if the client auth is Disabled or DisabledAll as they won't be available
try {
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setPeerCertificates(sslEngine.getSession().getPeerCertificates());
} catch (SSLPeerUnverifiedException e) {
// no op if -Dgov.nist.javax.sip.TLS_CLIENT_AUTH_TYPE=Disabled is used, no peer certificates will be available
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("sslEngine.getSession().getPeerCertificates() are not available, which is normal if running with gov.nist.javax.sip.TLS_CLIENT_AUTH_TYPE=Disabled");
}
}
}
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setLocalCertificates(sslEngine.getSession().getLocalCertificates());
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setCipherSuite(sslEngine.getSession().getCipherSuite());
}
}
break;
case NEED_TASK:
runDelegatedTasks(result);
break;
}
if (result.bytesProduced() == 0) {
break;
}
}
} catch (SSLException e) {
throw e;
} finally {
}
}
public void unwrap(ByteBuffer src) throws Exception {
ByteBuffer outputBuffer = channel.prepareAppDataBuffer();
unwrap(src, outputBuffer);
}
private void startBuffer(ByteBuffer src) {
if(tlsRecordBuffer == null) {
// Begin buffering, if there is already a buffer the normalization will take of adding the bytes
tlsRecordBuffer = ByteBufferFactory.getInstance().allocateDirect(33270); // max record size in other implementations
// Append the current buffer
tlsRecordBuffer.put(src);
// Prepare the buffer for reading
tlsRecordBuffer.flip();
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Allocated record buffer for reading " + tlsRecordBuffer + " for src = " + src);
}
}
}
private void clearBuffer() {
tlsRecordBuffer = null;
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Buffer cleared");
}
}
private ByteBuffer normalizeTlsRecordBuffer(ByteBuffer src) {
if(tlsRecordBuffer == null) {
return src;
} else {
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Normalize buffer " + src + " into record buffer "
+ tlsRecordBuffer);
}
// Reverse flip() to prepare the buffer to writing in append mode
tlsRecordBuffer.position(tlsRecordBuffer.limit());
tlsRecordBuffer.limit(tlsRecordBuffer.capacity());
// Append data
tlsRecordBuffer.put(src);
// And prepare it for reading again as if it came from the network
tlsRecordBuffer.flip();
return tlsRecordBuffer;
}
}
private void unwrap(ByteBuffer src, ByteBuffer dst) throws Exception {
synchronized (unwrapLock) {
loop:while(true) {
src = normalizeTlsRecordBuffer(src);
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Unwrap src " + src + " dst "
+ dst);
}
SSLEngineResult result = null;
try {
result = sslEngine.unwrap(src, dst);
} catch (Exception e) {
// https://java.net/jira/browse/JSIP-464
// Make sure to throw the exception so the result variable is not null below which makes the stack hang
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("An Exception occured while trying to unwrap the message " + e);
}
throw e;
}
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Unwrap result " + result + " buffers size "
+ pendingOutboundBuffers.size() + " src=" + src + " dst=" + dst);
}
if(result.getStatus().equals(Status.BUFFER_UNDERFLOW)) {
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Buffer underflow, wait for the next inbound chunk of data to feed the SSL engine");
}
startBuffer(src);
break;
} else {
clearBuffer();
}
if(result.getStatus().equals(Status.BUFFER_OVERFLOW)) {
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Buffer overflow , must prepare the buffer again."
+ " outNetBuffer remaining: " + dst.remaining()
+ " outNetBuffer postion: " + dst.position()
+ " Packet buffer size: " + sslEngine.getSession().getPacketBufferSize()
+ " new buffer size: " + sslEngine.getSession().getPacketBufferSize() + dst.position());
}
ByteBuffer newBuf = channel.prepareAppDataBuffer(sslEngine.getSession().getPacketBufferSize());
dst = newBuf;
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug(" new outNetBuffer remaining: " + dst.remaining()
+ " new outNetBuffer postion: " + dst.position());
}
continue;
}
if(result.bytesProduced()>0) {
// There is actual application data in this chunk
dst.flip();
byte[] a = new byte[dst.remaining()];
dst.get(a);
// take it and feed the plain text to out chunk-by-chunk parser
channel.addPlaintextBytes(a);
}
switch(result.getHandshakeStatus()) {
case NEED_UNWRAP:
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Unwrap has remaining: " + src.hasRemaining() + " buffer " + src);
}
if(src.hasRemaining()) {
break;
} else {
break loop;
}
case NEED_WRAP:
wrapNonAppData();
break;
case NEED_TASK:
runDelegatedTasks(result);
break;
case FINISHED:
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Handshaking just finnished, but has remaining. Will try to wrap the queues app items.");
}
wrapRemaining();
if(src.hasRemaining()) {
break;
} else {
if (logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
this.logger.logDebug(
"Handshake passed");
}
// Added for https://java.net/jira/browse/JSIP-483
// allow application to enforce policy by validating the
// certificate
if(channel instanceof NioTlsMessageChannel) {
((NioTlsMessageChannel)channel).setHandshakeCompleted(true);
if(sslEngine.getSession() != null) {
if(!ClientAuthType.Disabled.equals(channel.getSIPStack().getClientAuth()) && !ClientAuthType.DisabledAll.equals(channel.getSIPStack().getClientAuth())) {
// https://java.net/jira/browse/JSIP-483 Don't try to get the PeerCertificates if the client auth is Disabled or DisabledAll as they won't be available
try {
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setPeerCertificates(sslEngine.getSession().getPeerCertificates());
} catch (SSLPeerUnverifiedException e) {
// no op if -Dgov.nist.javax.sip.TLS_CLIENT_AUTH_TYPE=Disabled is used, no peer certificates will be available
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("sslEngine.getSession().getPeerCertificates() are not available, which is normal if running with gov.nist.javax.sip.TLS_CLIENT_AUTH_TYPE=Disabled");
}
}
}
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setLocalCertificates(sslEngine.getSession().getLocalCertificates());
((NioTlsMessageChannel)channel).getHandshakeCompletedListener().setCipherSuite(sslEngine.getSession().getCipherSuite());
}
try {
channel.getSIPStack()
.getTlsSecurityPolicy()
.enforceTlsPolicy(
channel
.getEncapsulatedClientTransaction());
} catch (SecurityException ex) {
throw new IOException(ex.getMessage());
}
if (logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
this.logger.logDebug(
"TLS Security policy passed");
}
}
break loop;
}
case NOT_HANDSHAKING:
wrapRemaining();
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Not handshaking, but has remaining: " + src.hasRemaining() + " buffer " + src);
}
if(src.hasRemaining()) {
break;
} else {
break loop;
}
default:
break;
}
}
}
}
private void runDelegatedTasks(SSLEngineResult result) throws IOException {
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Running delegated task for " + result);
}
/*
* Delegated tasks are just invisible steps inside the sslEngine state machine.
* Call them every time they have NEED_TASK otherwise the sslEngine won't make progress
*/
if (result.getHandshakeStatus() == HandshakeStatus.NEED_TASK) {
Runnable runnable;
while ((runnable = sslEngine.getDelegatedTask()) != null) {
runnable.run();
}
HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
if(logger.isLoggingEnabled(LogWriter.TRACE_DEBUG)) {
logger.logDebug("Handshake status after delegated tasks " + hsStatus);
}
if (hsStatus == HandshakeStatus.NEED_TASK) {
throw new IOException(
"handshake shouldn't need additional tasks");
}
}
}
public void sendSSLMetadata(byte[] msg) throws IOException {
channel.sendEncryptedData(msg);
}
public static interface MessageSendCallback {
public void doSend(byte[] bytes) throws IOException;
}
/**
* Each time we send a SIP message it will be associated with the originating channel.send() method
* which keep additional information about the transport in its context. That method will be called
* using the callback provided here.
*
* @author vladimirralev
*
*/
public static class MessageSendItem {
private ByteBuffer message;
private MessageSendCallback callback;
public MessageSendItem(ByteBuffer buffer, MessageSendCallback callback) {
this.message = buffer;
this.callback = callback;
}
public MessageSendCallback getCallBack() {
return callback;
}
public String toString() {
return MessageSendItem.class.getSimpleName() + " ["
+ message + ", " + callback + "]";
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy