SSL connections to IRC servers
This document describes how EPIC6 establishes a TLS connection to an IRC server, collects certificate verification results, and offers scripts a chance to choose the certificate policy.
Step one: Connect to a TLS server
Request a TLS connection with a server description such as:
/SERVER irc.example.com:6697:type=irc-ssl
Replace the example hostname with the server you intend to use. EPIC first establishes the TCP connection, then starts a nonblocking SSL_connect() handshake. Selecting port 6697 alone does not request TLS; the IRC-SSL connection type does. EPIC sends SNI for hostnames, but not IP addresses. By default, the handshake requires TLS 1.2 or newer; see ssl_min_version to require TLS 1.3. See server for server-description syntax.
Step two: Complete the handshake
A successful TLS handshake establishes encryption, but EPIC still needs to apply its certificate policy before IRC registration. A failed handshake or missing peer certificate prevents this path from proceeding.
Step three: Collect certificate verification results
OpenSSL calls EPIC's verification callback when it finds a certificate error or finishes checking a certificate without further errors. EPIC records the errors and always returns success from this callback so verification can continue and collect additional errors. The callback gathers information; the server connection code makes the policy decision afterward.
EPIC records three error categories:
- A self-signed server certificate:
X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT(18). - A hostname mismatch:
X509_V_ERR_HOSTNAME_MISMATCH(62). - Any other verification error.
Only the specific depth-zero self-signed error belongs to the first category; other chain errors belong to the third category. EPIC chooses a representative most_serious_error with other errors taking precedence over hostname mismatch, and hostname mismatch taking precedence over the depth-zero self-signed error. With no error, this value is X509_V_OK (0).
Step four: Offer the SSL_SERVER_CERT hook
Before calling ssl_connected(), the server connection code initializes its SSL_ACCEPT_CERT field to -1, meaning that no script decision has been recorded. After collecting certificate metadata, EPIC invokes on ssl_server_cert.
| Argument | Meaning |
|---|---|
$0 |
The connection's virtual file descriptor; use $serverctl(FROM_SERVER) for the server reference number in this hook. |
$1 |
Certificate subject, URL-encoded. |
$2 |
Certificate issuer, URL-encoded. |
$3 |
Public key length in bits. |
$4 |
Representative OpenSSL error code: 0 for no error, 18 for the depth-zero self-signed error, 62 for hostname mismatch, or another verification error code. |
$5 |
Negotiated TLS protocol version, such as TLSv1.2 or TLSv1.3. |
$6 |
Legacy SHA-1 certificate digest as lowercase hexadecimal bytes separated by colons. |
$7 |
Hostname mismatch flag: 0 for no error, 1 for an error. |
$8 |
Depth-zero self-signed flag: 0 for no error, 1 for an error. |
$9 |
Other verification error flag: 0 for no error, 1 for an error. |
$10 |
Any verification error flag: 0 for no errors, 1 for one or more errors. |
A script can change the field using $serverctl(SET <refnum> SSL_ACCEPT_CERT <value>). In the current implementation, any value other than -1 set during this early hook skips both the provisional policy and SERVER_SSL_EVAL.
Current implementation caveat: the rejection check is inside the skipped policy block. Setting SSL_ACCEPT_CERT to 0 in SSL_SERVER_CERT therefore does not reject the connection; it also proceeds to IRC registration. To reject through the normal certificate policy path, leave the field at -1 in this early hook and make the decision in SERVER_SSL_EVAL instead.
Step five: Make a provisional decision
If the early hook left SSL_ACCEPT_CERT at -1, EPIC chooses a provisional value:
| Verification result | Provisional decision |
|---|---|
| No errors | Accept (1). |
Only depth-zero self-signed and/or hostname mismatch errors, with ACCEPT_INVALID_SSL_CERT on |
Accept (1). |
Only those errors, with ACCEPT_INVALID_SSL_CERT off |
Reject (0). |
| Any other verification error | Reject (0). |
EPIC stores this result in SSL_ACCEPT_CERT. The compiled default for accept_invalid_ssl_cert is on; a user's settings may change it. That setting excuses only the two specific categories above, not all invalid certificates.
Step six: Offer the SERVER_SSL_EVAL hook
After making its provisional decision, EPIC invokes on server_ssl_eval. Scripts can inspect the metadata and change the decision before it is enforced.
| Argument | Meaning |
|---|---|
$0 |
Server reference number. |
$1 |
Configured server hostname, as returned by get_server_name(). |
$2 |
Any verification error: 0 for no errors, 1 for an error. |
$3 |
Hostname mismatch: 0 for no error, 1 for an error. |
$4 |
Depth-zero self-signed error: 0 for no error, 1 for an error. |
$5 |
Other verification error: 0 for no error, 1 for an error. |
$6 |
EPIC's provisional decision: 0 to reject, 1 to accept. |
Use $serverctl(GET <refnum> <item>) to read certificate and connection metadata:
| Item | Meaning |
|---|---|
SSL_CIPHER |
Negotiated cipher name. |
SSL_PEM |
Peer certificate in PEM format. |
SSL_CERT_HASH |
Legacy SHA-1 certificate digest with colon-separated hexadecimal bytes. |
SSL_CERT_SHA256 |
SHA-256 certificate digest with colon-separated hexadecimal bytes; prefer this for certificate pinning. |
SSL_PKEY_BITS |
Public key length in bits. |
SSL_SUBJECT |
Certificate subject. |
SSL_SUBJECT_URL |
URL-encoded certificate subject. |
SSL_ISSUER |
Certificate issuer. |
SSL_ISSUER_URL |
URL-encoded certificate issuer. |
SSL_VERSION |
Negotiated TLS protocol version. |
SSL_SANS |
Subject alternative names from the certificate. |
SSL_CHECKHOST_ERROR |
Hostname mismatch flag: 0 or 1. |
SSL_SELF_SIGNED_ERROR |
Depth-zero self-signed flag: 0 or 1. |
SSL_OTHER_ERROR |
Other verification error flag: 0 or 1. |
SSL_MOST_SERIOUS_ERROR |
Representative OpenSSL verification error code, or 0 for no error. |
SSL_MOST_SERIOUS_ERROR_STR |
Human-readable description of the representative verification error. |
SSL_VERIFY_ERROR |
Any verification error flag: 0 or 1. |
SSL_ACCEPT_CERT |
Current decision field: initially -1 in the early hook, then normally 0 or 1. |
These getters require an existing TLS-enabled server connection; they return an empty string when that prerequisite is absent. See serverctl.
To reject the certificate during SERVER_SSL_EVAL, evaluate:
$serverctl(SET <refnum> SSL_ACCEPT_CERT 0)
To accept it, evaluate:
$serverctl(SET <refnum> SSL_ACCEPT_CERT 1)
Replace <refnum> with the server reference number, available as $0 in this hook. These are function expressions to use in script context, not standalone IRC commands. Leaving the field unchanged retains EPIC's provisional decision.
Step seven: Continue or reject
After SERVER_SSL_EVAL returns, EPIC rejects the connection when SSL_ACCEPT_CERT is exactly 0. Otherwise, it returns to the connection path and starts IRC registration. Scripts should use the documented 0 and 1 values for decisions. The early-hook bypass described in step four is an exception to enforcement at this stage.
SASL PLAIN authentication
The supplied sasl script checks certificate verification separately before sending a PLAIN password. Accepting a TLS connection through accept_invalid_ssl_cert or a certificate hook does not by itself authorize PLAIN authentication. The script requires TLS and either SSL_VERIFY_ERROR equal to 0 or a matching optional certificate fingerprint in the rule. Use the SHA-256 fingerprint; legacy SHA-1 fingerprints are still accepted:
/sasl <server-pattern> PLAIN <username> <password> [<certificate-hash>]
Read the connected server's SHA-256 fingerprint with /eval echo $serverctl(GET <refnum> SSL_CERT_SHA256). A supplied fingerprint permits a certificate with recorded verification errors when it matches that value. A cleanly verified certificate is accepted even when a supplied fingerprint differs. This rule does not override the earlier connection-level certificate acceptance policy.
The script checks before requesting the SASL capability and again before sending credentials. If the check fails, it reports the refusal and continues the connection without SASL. These checks apply to the script's PLAIN mechanism; other mechanisms have their own requirements.
Source references
source/server.c:server_io()handles the handshake result, certificate policy, both hooks, rejection, and IRC registration;serverctl()implements the metadata interface.source/ssl.c:ssl_startup(),ssl_connect(),verify_callback(), andssl_connected()implement negotiation, verification collection, metadata, andSSL_SERVER_CERTarguments.source/vars.candinclude/config.h: registration and compiled default ofACCEPT_INVALID_SSL_CERT.script/sasl: PLAIN rules,sasl_auth.plain_ok, certificate-hash exceptions, and refusal handling.