# $Id: ssl.bro 5988 2008-07-19 07:02:12Z vern $

@load notice
@load conn
@load weird
@load ssl-ciphers
@load ssl-errors

global ssl_log = open_log_file("ssl") &redef;

redef enum Notice += {
	SSL_X509Violation,	# blanket X509 error
	SSL_SessConIncon,	# session data not consistent with connection
};


const SSLv2  = 0x0002;
const SSLv3  = 0x0300;
const TLSv10 = 0x0301;
const TLSv11 = 0x0302;

# If true, Bro stores the client and server cipher specs and performs
# additional tests.  This costs an extra amount of memory (normally
# only for a short time) but enables detecting of non-intersecting
# cipher sets, for example.
const ssl_compare_cipherspecs = T &redef;

# Whether to analyze certificates seen in SSL connections.
const ssl_analyze_certificates = T &redef;

# If we analyze SSL certificates, we can choose to store them.
const ssl_store_certificates = T &redef;

# Path where we dump the certificates into.  If it's empty,
# use the current directory.
const ssl_store_cert_path = "certs" &redef;

# If we analyze SSL certificates, we can choose to verify them.
const ssl_verify_certificates = T &redef;

# This is the path where OpenSSL looks after the trusted certificates.
# If empty, the default path will be used.
const x509_trusted_cert_path = "" &redef;

# Whether to store key-material exchanged in the handshaking phase.
const ssl_store_key_material = F &redef;

# Report weak/unknown ciphers in CLIENT_HELLO, SSLv2 SERVER_HELLO.
const ssl_report_client_weak = F &redef;
const ssl_report_client_unknown = F &redef;
const ssl_report_server_weak = F &redef;

# Log all ciphers.
const ssl_log_ciphers = T &redef;

# NOTE: this is a 'local' port format for your site
# --- well-known ports for ssl ---------
redef capture_filters += {
	["ssl"] = "tcp port 443",
	["nntps"] = "tcp port 563",
	["imap4-ssl"] = "tcp port 585",
	["sshell"] = "tcp port 614",
	["ldaps"] = "tcp port 636",
	["ftps-data"] = "tcp port 989",
	["ftps"] = "tcp port 990",
	["telnets"] = "tcp port 992",
	["imaps"] = "tcp port 993",
	["ircs"] = "tcp port 994",
	["pop3s"] = "tcp port 995"
};

global ssl_ports = {
	443/tcp, 563/tcp, 585/tcp, 614/tcp, 636/tcp,
	989/tcp, 990/tcp, 992/tcp, 993/tcp, 995/tcp,
} &redef;

redef dpd_config += {
	[[ANALYZER_SSL, ANALYZER_SSL_BINPAC]] = [$ports = ssl_ports]
};

# --- Weak Cipher Demo -------------

const myWeakCiphers: set[count] = {
	SSLv20_CK_RC4_128_EXPORT40_WITH_MD5,
	SSLv20_CK_RC2_128_CBC_EXPORT40_WITH_MD5,
	SSLv20_CK_DES_64_CBC_WITH_MD5,

	SSLv3x_NULL_WITH_NULL_NULL,
	SSLv3x_RSA_WITH_NULL_MD5,
	SSLv3x_RSA_WITH_NULL_SHA,
	SSLv3x_RSA_EXPORT_WITH_RC4_40_MD5,
	SSLv3x_RSA_EXPORT_WITH_RC2_CBC_40_MD5,
	SSLv3x_RSA_EXPORT_WITH_DES40_CBC_SHA,
	SSLv3x_RSA_WITH_DES_CBC_SHA,

	SSLv3x_DH_DSS_EXPORT_WITH_DES40_CBC_SHA,
	SSLv3x_DH_DSS_WITH_DES_CBC_SHA,
	SSLv3x_DH_RSA_EXPORT_WITH_DES40_CBC_SHA,
	SSLv3x_DH_RSA_WITH_DES_CBC_SHA,
	SSLv3x_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA,
	SSLv3x_DHE_DSS_WITH_DES_CBC_SHA,
	SSLv3x_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA,
	SSLv3x_DHE_RSA_WITH_DES_CBC_SHA,

	SSLv3x_DH_anon_EXPORT_WITH_RC4_40_MD5,
	SSLv3x_DH_anon_WITH_RC4_128_MD5,
	SSLv3x_DH_anon_EXPORT_WITH_DES40_CBC_SHA,
	SSLv3x_DH_anon_WITH_DES_CBC_SHA,
	SSLv3x_DH_anon_WITH_3DES_EDE_CBC_SHA,
	SSLv3x_FORTEZZA_KEA_WITH_NULL_SHA
};

const x509_ignore_errors: set[int] = {
	X509_V_OK,
	# X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE
};

const x509_hot_errors: set[int] = {
	X509_V_ERR_CRL_SIGNATURE_FAILURE,
	X509_V_ERR_CERT_NOT_YET_VALID,
	X509_V_ERR_CERT_HAS_EXPIRED,
	X509_V_ERR_CERT_REVOKED,
	X509_V_ERR_SUBJECT_ISSUER_MISMATCH,
	# X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE	# for testing
};

redef Weird::weird_action += {
	[["SSLv2: Unknown CIPHER-SPEC in CLIENT-HELLO!",
	  "SSLv2: Client has CipherSpecs > MAX_CIPHERSPEC_SIZE",
	  "unexpected_SSLv3_record",
	  "SSLv3_data_without_full_handshake"]]	= Weird::WEIRD_IGNORE
};


global SSL_cipherCount: table[count] of count &default = 0;

type ssl_connection_info: record {
	id: count;			# the log identifier number
	connection_id: conn_id;		# IP connection information
	version: string;		# version assosciated with connection
	client_cert: X509;
	server_cert: X509;
	id_index: string;		# index for associated SSL_sessionID
	handshake_cipher: string;	# agreed-upon cipher for session/conn.
};

# SSL_sessionID index - used to track version assosciated with a session id.
type SSL_sessionID_record: record {
	num_reuse: count;
	id: SSL_sessionID;	# literal session ID

	# everything below is an example of session vs connection monitoring.
	version: string;	# version assosciated with session id
	client_cert: X509;
	server_cert: X509;
	handshake_cipher: string;
};

global ssl_connections: table[conn_id] of ssl_connection_info;
global ssl_sessionIDs: table[string] of SSL_sessionID_record
						&read_expire = 2 hrs;
global ssl_connection_id = 0;

# Used when there's no issuer/subject/cipher.
const NONE = "<none>";

# --- SSL helper functions ---------
function new_ssl_connection(c: connection)
	{
	local conn = c$id;
	local new_id = ++ssl_connection_id;

	local info: ssl_connection_info;
	info$id = new_id;
	info$id_index = md5_hash(info$id);
	info$version = "";
	info$client_cert$issuer = NONE;
	info$client_cert$subject = NONE;
	info$server_cert$issuer = NONE;
	info$server_cert$subject = NONE;
	info$handshake_cipher = NONE;
	info$connection_id = conn;

	ssl_connections[conn] = info;
	append_addl(c,  fmt("#%d", new_id));

	print ssl_log, fmt("%.6f #%d %s start",
				network_time(), new_id, id_string(conn));
	}

function new_sessionID_record(session: SSL_sessionID)
	{
	local info: SSL_sessionID_record;

	info$num_reuse = 1;
	info$client_cert$issuer = NONE;
	info$client_cert$subject = NONE;
	info$server_cert$issuer = NONE;
	info$server_cert$subject = NONE;
	info$handshake_cipher = NONE;

	local index = md5_hash(session);
	ssl_sessionIDs[index] = info;
	}

function ssl_get_cipher_name(cipherSuite: count): string
	{
	return cipherSuite in ssl_cipher_desc ?
		ssl_cipher_desc[cipherSuite] : "UNKNOWN";
	}

function ssl_get_version_string(version: count): string
	{
	if ( version == SSLv2 )
		return "SSL version 2";
	else if ( version == SSLv3 )
		return "SSL version 3";
	else if ( version == TLSv10 )
		return "TLS version 1.0";
	else if ( version == TLSv11 )
		return "TLS version 1.1";
	else
		return "?.?";
	}

function ssl_con2str(c: connection): string
	{
	return fmt("%s:%s -> %s:%s",
			c$id$orig_h, c$id$orig_p, c$id$resp_h, c$id$resp_p);
	}

function lookup_ssl_conn(c: connection, func: string, log_if_new: bool)
	{
	if ( c$id !in ssl_connections )
		{
		new_ssl_connection(c);

		if ( log_if_new )
			print ssl_log,
				fmt("%.6f #%d creating new SSL connection in %s",
					network_time(), ssl_connections[c$id]$id, func);
		}
	}

event ssl_conn_weak(name: string, c: connection)
	{
	lookup_ssl_conn(c, "ssl_conn_weak", T);
	print ssl_log, fmt("%.6f #%d %s",
		network_time(), ssl_connections[c$id]$id, name);
	}

# --- SSL events -------------------

event ssl_certificate_seen(c: connection, is_server: bool)
	{
	# Called whenever there's an certificate to analyze.
	# we could do something here, like...

	# if ( c$id$orig_h in hostsToIgnore )
	#	{
	#	ssl_store_certificates = F;
	#	ssl_verify_certificates = F;
	#	}
	# else
	#	{
	#	ssl_store_certificates = T;
	#	ssl_verify_certificates = T;
	#	}
	}

event ssl_certificate(c: connection, cert: X509, is_server: bool)
	{
	local direction = is_local_addr(c$id$orig_h) ? "client" : "server";

	lookup_ssl_conn(c, "ssl_certificate", T);
	local conn = ssl_connections[c$id];

	if( direction == "client" )
		conn$client_cert = cert;
	else
		{
		conn$server_cert = cert;

		# We have not filled in the field for the master session
		# for this connection.  Do it now, but only if this is not a
		# SSLv2 connection (no session information in that case).
		if ( conn$id_index in ssl_sessionIDs &&
		     ssl_sessionIDs[conn$id_index]$server_cert$subject == NONE )
			ssl_sessionIDs[conn$id_index]$server_cert$subject =
				cert$subject;
		}

	print ssl_log, fmt("%.6f #%d X.509 %s issuer %s",
			network_time(), conn$id, direction, cert$issuer);

	print ssl_log, fmt("%.6f #%d X.509 %s subject %s",
			network_time(), conn$id, direction, cert$subject);
	}

event ssl_conn_attempt(c: connection, version: count,
			ciphers: cipher_suites_list)
	{
	lookup_ssl_conn(c, "ssl_conn_attempt", F);
	local conn = ssl_connections[c$id];
	local version_string = ssl_get_version_string(version);

	print ssl_log, fmt("%.6f #%d SSL connection attempt %s",
				network_time(), conn$id, version_string);

	conn$version = version_string;

	for ( cs in ciphers )
		{ # display a list of the cipher suites
		# Demo: report clients who support weak ciphers.
		if ( ssl_report_client_weak && cs in myWeakCiphers )
			event ssl_conn_weak(
				fmt("SSL client supports weak cipher: %s (0x%x)",
					ssl_get_cipher_name(cs), cs), c);

		# Demo: report unknown ciphers.
		if ( ssl_report_client_unknown && cs !in ssl_cipher_desc )
			event ssl_conn_weak(
				fmt("SSL: unknown cipher-spec: %s (0x%x)",
					ssl_get_cipher_name(cs), cs), c);

		if ( ssl_log_ciphers )
			print ssl_log, fmt("%.6f #%d client cipher %s (0x%x)",
					network_time(), conn$id,
					ssl_get_cipher_name(cs), cs);
		}
	}

event ssl_conn_server_reply(c: connection, version: count,
				ciphers: cipher_suites_list)
	{
	lookup_ssl_conn(c, "ssl_conn_server_reply", T);

	local conn = ssl_connections[c$id];
	local version_string = ssl_get_version_string(version);

	print ssl_log, fmt("%.6f #%d SSL connection server reply, %s",
				network_time(), conn$id, version_string);

	conn$version = version_string;

	for ( cs in ciphers )
		{
		# Demo: report servers who support weak ciphers.
		if ( ssl_report_server_weak && version == SSLv2 &&
		     cs in myWeakCiphers )
			event ssl_conn_weak(
				fmt("SSLv2 server supports weak cipher: %s (0x%x)",
					ssl_get_cipher_name(cs), cs), c);

                if ( ssl_log_ciphers )
                        print ssl_log, fmt("%.6f #%d server cipher %s (0x%x)",
					network_time(), conn$id,
                                        ssl_get_cipher_name(cs), cs);
		}
	}

event ssl_conn_established(c: connection, version: count, cipher_suite: count)
	{
	lookup_ssl_conn(c, "ssl_conn_established", T);

	local conn = ssl_connections[c$id];
	local version_string = ssl_get_version_string(version);

	print ssl_log,
		fmt("%.6f #%d handshake finished, %s",
			network_time(), conn$id, version_string);

	if ( cipher_suite in myWeakCiphers )
		event ssl_conn_weak(fmt("%.6f #%d weak cipher: %s (0x%x)",
			network_time(), conn$id,
			ssl_get_cipher_name(cipher_suite), cipher_suite), c);

	if ( ssl_log_ciphers )
		print ssl_log, fmt("%.6f #%d connection cipher %s (0x%x)",
			network_time(), conn$id,
			ssl_get_cipher_name(cipher_suite), cipher_suite);

	++SSL_cipherCount[cipher_suite];

	# This should be the version identified with the session, unless
	# there is some renegotiation.  That will be caught later.
	conn$version = version_string;
	}

event process_X509_extensions(c: connection, ex: X509_extension)
	{
	lookup_ssl_conn(c, "process_X509_extensions", T);
	local conn = ssl_connections[c$id];

	local msg = fmt("%.6f #%d X.509 extensions: ", network_time(), conn$id);
	for ( i in ex )
		msg = fmt("%s, %s", msg, ex[i]);

	print ssl_log, msg;
	}

event ssl_session_insertion(c: connection, id: SSL_sessionID)
	{
	local idd = c$id;

	if ( idd !in ssl_connections)
		{
		new_ssl_connection(c);

		print ssl_log,
			fmt("%.6f #%d creating new SSL connection in ssl_session_insertion",
				network_time(), ssl_connections[c$id]$id);

		# None of the conn$object values will exist, so we leave this
		# to prevent needless crashing.
		return;
		}

	local conn = ssl_connections[idd];
	local id_index = md5_hash(id);

	# If there is no session with thIS id we create (a typical) one,
	# otherwise we move on.
	if ( id_index !in ssl_sessionIDs )
		{
		new_sessionID_record(id);

		local session = ssl_sessionIDs[id_index];
		session$version = conn$version;
		session$client_cert$subject = conn$client_cert$subject;
		session$server_cert$subject = conn$server_cert$subject;
		session$handshake_cipher = conn$handshake_cipher;
		session$id = id;

		conn$id_index = id_index;
		}

	else
		{ # should we ever get here?
		session = ssl_sessionIDs[id_index];
		conn$id_index = id_index;
		}
	}

event ssl_conn_reused(c: connection, session_id: SSL_sessionID)
	{
	lookup_ssl_conn(c, "ssl_conn_reused", T);
	local conn = ssl_connections[c$id];
	local id_index = md5_hash(session_id);

	print ssl_log, fmt("%.6f #%d reusing former SSL session: %s",
				network_time(), conn$id, id_index);

	# We cannot track sessions with SSLv2.
	if ( conn$version == ssl_get_version_string(SSLv2) )
		return;

	if ( id_index !in ssl_sessionIDs )
		{
		new_sessionID_record(session_id);
		local session = ssl_sessionIDs[id_index];
		session$version = conn$version;
		session$client_cert$subject = conn$client_cert$subject;
		session$server_cert$subject = conn$server_cert$subject;
		session$id = session_id;
		}
	else
		session = ssl_sessionIDs[id_index];

	++session$num_reuse;

	# At this point, the connection values have been set.  We can then
	# compare session and connection values with some confidence.
	if ( session$version != conn$version ||
	     session$handshake_cipher != conn$handshake_cipher )
		{
		NOTICE([$note=SSL_SessConIncon, $conn=c,
			$msg="session violation"]);
		++c$hot;
		}
	}

event ssl_X509_error(c: connection, err: int, err_string: string)
	{
	if ( err in x509_ignore_errors )
		return;

	lookup_ssl_conn(c, "ssl_X509_error", T);
	local conn = ssl_connections[c$id];
	local error =
		err in x509_errors ?  x509_errors[err] : "unknown X.509 error";

	local severity = "warning";
	if ( err in x509_hot_errors )
		{
		NOTICE([$note=SSL_X509Violation, $conn=c, $msg=error]);
		++c$hot;
		severity = "error";
		}

	print ssl_log,
		fmt("%.6f #%d X.509 %s %s (%s)",
			network_time(), conn$id, severity, error, err_string);
	}

event connection_state_remove(c: connection)
	{
	delete ssl_connections[c$id];
	}

event bro_init()
	{
	if ( ssl_store_cert_path != "" )
		# The event engine will generate a run-time if this fails for
		# reasons other than that the directory already exists.
		mkdir(ssl_store_cert_path);
	}

event bro_done()
	{
	print ssl_log, "Cipher suite statistics: ";
	for ( i in SSL_cipherCount )
		print ssl_log, fmt("%s (0x%x): %d", ssl_get_cipher_name(i), i,
					SSL_cipherCount[i]);

	print ssl_log, ("count     session ID");
	print ssl_log, ("-----     ---------------------------------");
	for ( j in ssl_sessionIDs )
		if ( ssl_sessionIDs[j]$server_cert$subject != NONE )
			{
			print ssl_log,
				fmt("(%s)      %s   %s",
					ssl_sessionIDs[j]$num_reuse,
					ssl_sessionIDs[j]$server_cert$subject,
					j);
			}
	}
