]> arthur.barton.de Git - ngircd-alex.git/blobdiff - src/ngircd/conn-ssl.c
Optionally validate certificates on TLS server links
[ngircd-alex.git] / src / ngircd / conn-ssl.c
index c9bbdd2497ee3f7f9742415ab5888fad4b1c7186..3e21ad268f1d4abf34cd3ee9d14a788875a012ee 100644 (file)
@@ -47,8 +47,11 @@ static SSL_CTX * ssl_ctx;
 static DH *dh_params;
 
 static bool ConnSSL_LoadServerKey_openssl PARAMS(( SSL_CTX *c ));
+static bool ConnSSL_SetVerifyProperties_openssl PARAMS(( SSL_CTX *c ));
 #endif
 
+#define MAX_CERT_CHAIN_LENGTH  10 /* XXX: do not hardcode */
+
 #ifdef HAVE_LIBGNUTLS
 #include <sys/types.h>
 #include <sys/stat.h>
@@ -61,10 +64,16 @@ static bool ConnSSL_LoadServerKey_openssl PARAMS(( SSL_CTX *c ));
 
 #define MAX_HASH_SIZE  64      /* from gnutls-int.h */
 
+gnutls_x509_crl_t *crl_list;
+gnutls_x509_crt_t *ca_list;
+size_t crl_list_size;
+size_t ca_list_size;
+
 static gnutls_certificate_credentials_t x509_cred;
 static gnutls_dh_params_t dh_params;
 static gnutls_priority_t priorities_cache;
 static bool ConnSSL_LoadServerKey_gnutls PARAMS(( void ));
+static bool ConnSSL_SetVerifyProperties_gnutls PARAMS(( void ));
 #endif
 
 #define SHA256_STRING_LEN      (32 * 2 + 1)
@@ -125,6 +134,34 @@ out:
  * @param msg The error message.
  * @param info Additional information text or NULL.
  */
+static void
+LogOpenSSL_CertInfo(X509 *cert, const char *msg)
+{
+       BIO *mem;
+       char *memptr;
+       long len;
+
+       assert(cert);
+       assert(msg);
+
+       if (!cert) return;
+
+       mem = BIO_new(BIO_s_mem());
+       if (!mem) return;
+
+       X509_NAME_print_ex(mem, X509_get_subject_name (cert), 4, XN_FLAG_ONELINE);
+       X509_NAME_print_ex(mem, X509_get_issuer_name (cert), 4, XN_FLAG_ONELINE);
+       if (BIO_write(mem, "", 1) == 1) {
+               len = BIO_get_mem_data(mem, &memptr);
+               assert(memptr);
+               assert(len>0);
+               Log(LOG_INFO, "%s: \"%s\"", msg, memptr);
+       }
+
+       (void)BIO_set_close(mem, BIO_CLOSE);
+       BIO_free(mem);
+}
+
 static void
 LogOpenSSLError(const char *error, const char *info)
 {
@@ -167,9 +204,33 @@ pem_passwd_cb(char *buf, int size, int rwflag, void *password)
 
 
 static int
-Verify_openssl(UNUSED int preverify_ok, UNUSED X509_STORE_CTX *x509_ctx)
+Verify_openssl(int preverify_ok, X509_STORE_CTX *ctx)
 {
-       return 1;
+       X509 *err_cert;
+       int err, depth;
+
+       err_cert = X509_STORE_CTX_get_current_cert(ctx);
+       err = X509_STORE_CTX_get_error(ctx);
+       depth = X509_STORE_CTX_get_error_depth(ctx);
+
+       LogDebug("preverify_ok %d error:num=%d:%s:depth=%d",
+                       preverify_ok, err, X509_verify_cert_error_string(err), depth);
+
+       if (preverify_ok != 1) {
+               /*
+                * if certificates are not being enforced, ignore any errors.
+                * its possible to check if a connection has a valid certificate
+                * by testing the CONN_SSL_PEERCERT_OK flag.
+                * This can be used to enforce certificates for incoming servers,
+                * but not irc clients.
+                */
+               if (!Conf_SSLOptions.RequireClientCert)
+                       preverify_ok = 1;
+               else
+                       Log(LOG_ERR, "verify error:num=%d:%s:depth=%d", err,
+                               X509_verify_cert_error_string(err), depth);
+       }
+       return preverify_ok;
 }
 #endif
 
@@ -317,6 +378,9 @@ ConnSSL_InitLibrary( void )
                goto out;
        }
 
+       if (!ConnSSL_SetVerifyProperties_openssl(newctx))
+                goto out;
+
        SSL_CTX_set_options(newctx, SSL_OP_SINGLE_DH_USE|SSL_OP_NO_SSLv2);
        SSL_CTX_set_mode(newctx, SSL_MODE_ENABLE_PARTIAL_WRITE);
        SSL_CTX_set_verify(newctx, SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE,
@@ -358,6 +422,9 @@ out:
                goto out;
        }
 
+       if (!ConnSSL_SetVerifyProperties_gnutls())
+               goto out;
+
        Log(LOG_INFO, "GnuTLS %s initialized.", gnutls_check_version(NULL));
        initialized = true;
        return true;
@@ -369,6 +436,30 @@ out:
 
 
 #ifdef HAVE_LIBGNUTLS
+static bool
+ConnSSL_SetVerifyProperties_gnutls(void)
+{
+       int err;
+
+       if (Conf_SSLOptions.CAFile) {
+               err = gnutls_certificate_set_x509_trust_file(x509_cred, Conf_SSLOptions.CAFile, GNUTLS_X509_FMT_PEM);
+               if (err < 0) {
+                       Log(LOG_ERR, "Failed to load x509 trust file %s: %s", Conf_SSLOptions.CAFile, gnutls_strerror(err));
+                       return false;
+               }
+       }
+       if (Conf_SSLOptions.CRLFile) {
+               err = gnutls_certificate_set_x509_crl_file(x509_cred, Conf_SSLOptions.CRLFile, GNUTLS_X509_FMT_PEM);
+               if (err < 0) {
+                       Log(LOG_ERR, "Failed to load x509 crl file %s: %s", Conf_SSLOptions.CRLFile, gnutls_strerror(err));
+                       return false;
+               }
+       }
+
+       return true;
+}
+
+
 static bool
 ConnSSL_LoadServerKey_gnutls(void)
 {
@@ -455,6 +546,48 @@ ConnSSL_LoadServerKey_openssl(SSL_CTX *ctx)
 }
 
 
+static bool
+ConnSSL_SetVerifyProperties_openssl(SSL_CTX * ctx)
+{
+       X509_STORE *store = NULL;
+       X509_LOOKUP *lookup;
+       int verify_flags = SSL_VERIFY_PEER;
+       bool ret = false;
+
+       if (!Conf_SSLOptions.CAFile)
+               return true;
+
+       if (SSL_CTX_load_verify_locations(ctx, Conf_SSLOptions.CAFile, NULL) != 1) {
+               LogOpenSSLError("SSL_CTX_load_verify_locations", NULL);
+               goto out;
+       }
+
+       store = SSL_CTX_get_cert_store(ctx);
+       assert(store);
+       lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
+       if (!lookup) {
+               LogOpenSSLError("X509_STORE_add_lookup", Conf_SSLOptions.CRLFile);
+               goto out;
+       }
+
+       if (1 != X509_load_crl_file(lookup, Conf_SSLOptions.CRLFile, X509_FILETYPE_PEM)) {
+               LogOpenSSLError("X509_load_crl_file", Conf_SSLOptions.CRLFile);
+               goto out;
+       }
+
+       if (Conf_SSLOptions.RequireClientCert)
+               verify_flags |= SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+
+       SSL_CTX_set_verify(ctx, verify_flags, Verify_openssl);
+       SSL_CTX_set_verify_depth(ctx, MAX_CERT_CHAIN_LENGTH);
+       ret = true;
+ out:
+       free(Conf_SSLOptions.CRLFile);
+       Conf_SSLOptions.CRLFile = NULL;
+       return ret;
+}
+
+
 #endif
 static bool
 ConnSSL_Init_SSL(CONNECTION *c)
@@ -540,7 +673,11 @@ ConnSSL_PrepareConnect(CONNECTION *c, UNUSED CONF_SERVER *s)
        Conn_OPTION_ADD(c, CONN_SSL_CONNECT);
 #ifdef HAVE_LIBSSL
        assert(c->ssl_state.ssl);
-       SSL_set_verify(c->ssl_state.ssl, SSL_VERIFY_NONE, NULL);
+
+       if (s->SSLVerify)
+               SSL_set_verify(c->ssl_state.ssl, SSL_VERIFY_PEER, Verify_openssl);
+       else
+               SSL_set_verify(c->ssl_state.ssl, SSL_VERIFY_NONE, NULL);
 #endif
        return true;
 }
@@ -639,27 +776,191 @@ ConnSSL_HandleError(CONNECTION * c, const int code, const char *fname)
 }
 
 
+#ifdef HAVE_LIBGNUTLS
+static bool check_verification(unsigned output)
+{
+       char errmsg[256] = "";
+
+       if (output & GNUTLS_CERT_SIGNER_NOT_FOUND)
+               strlcpy(errmsg, "No Issuer found", sizeof errmsg);
+       if (output & GNUTLS_CERT_SIGNER_NOT_CA) {
+               if (errmsg[0])
+                       strlcat(errmsg, " ,", sizeof errmsg);
+               strlcat(errmsg, "Issuer is not a CA", sizeof errmsg);
+       }
+       if (output & GNUTLS_CERT_INSECURE_ALGORITHM) {
+               if (errmsg[0])
+                       strlcat(errmsg, " ,", sizeof errmsg);
+               strlcat(errmsg, "Insecure Algorithm", sizeof errmsg);
+       }
+       if (output & GNUTLS_CERT_REVOKED) {
+               if (errmsg[0])
+                       strlcat(errmsg, " ,", sizeof errmsg);
+               strlcat(errmsg, "Certificate Revoked", sizeof errmsg);
+       }
+#ifdef DEBUG
+       if (output & GNUTLS_CERT_INVALID)
+               assert(errmsg[0]); /* check for unhandled error */
+#endif
+       if (!(output & GNUTLS_CERT_INVALID) && !errmsg[0]) {
+               LogDebug("Certificate verified.");
+               return true;
+       }
+       Log(LOG_ERR, "Certificate Validation failed: %s", errmsg);
+       return false;
+}
+
+static void *LogMalloc(size_t s)
+{
+       void *mem = malloc(s);
+       if (!mem)
+               Log(LOG_ERR, "Out of memory: Could not allocate %lu byte", (unsigned long) s);
+       return mem;
+}
+
+static void
+LogGnuTLS_CertInfo(gnutls_x509_crt_t cert, const char *msg)
+{
+       char *dn, *issuer_dn;
+       size_t size = 0;
+       int err = gnutls_x509_crt_get_dn(cert, NULL, &size);
+       if (size == 0 || (err && err != GNUTLS_E_SHORT_MEMORY_BUFFER))
+               goto err_crt_get;
+       dn = LogMalloc(size);
+       if (!dn)
+               return;
+       err = gnutls_x509_crt_get_dn(cert, dn, &size);
+       if (err) {
+ err_crt_get:
+               Log(LOG_ERR, "gnutls_x509_crt_get_dn: %s", err ? gnutls_strerror(err) : "size == 0");
+               return;
+       }
+       gnutls_x509_crt_get_issuer_dn(cert, NULL, &size);
+       assert(size);
+       issuer_dn = LogMalloc(size);
+       if (!issuer_dn) {
+               Log(LOG_INFO, "%s: Distinguished Name: %s", msg, dn);
+               free(dn);
+               return;
+       }
+       gnutls_x509_crt_get_issuer_dn(cert, issuer_dn, &size);
+       Log(LOG_INFO, "%s: Distinguished Name: \"%s\", Issuer \"%s\"", msg, dn, issuer_dn);
+       free(dn);
+       free(issuer_dn);
+}
+#endif
+
 static void
 ConnSSL_LogCertInfo( CONNECTION *c )
 {
+       const char *comp_alg = "no compression";
+       bool cert_seen = false, cert_ok = false, cn_match = false;
+       char msg[128];
 #ifdef HAVE_LIBSSL
+       const void *comp;
+       X509 *client_cert = NULL;
        SSL *ssl = c->ssl_state.ssl;
 
        assert(ssl);
-
-       Log(LOG_INFO, "Connection %d: initialized %s using cipher %s.",
-               c->sock, SSL_get_version(ssl), SSL_get_cipher(ssl));
+       comp=SSL_get_current_compression(ssl);
+       if (comp) {
+               Conn_OPTION_ADD(c, CONN_SSL_COMPRESSION);
+               comp_alg = SSL_COMP_get_name(comp);
+       }
+       Log(LOG_INFO, "Connection %d: initialized %s using cipher %s, %s.",
+               c->sock, SSL_get_version(ssl), SSL_get_cipher(ssl), comp_alg);
+       client_cert = SSL_get_peer_certificate(ssl);
+       if (client_cert) {
+               int err = SSL_get_verify_result(ssl);
+               if (err == X509_V_OK)
+                       cert_ok = true;
+               else
+                       Log(LOG_ERR, "Certificate Validation failed: %s", X509_verify_cert_error_string(err));
+               X509_NAME *subjectName;
+               char cn[256];
+               subjectName = X509_get_subject_name(client_cert);
+               X509_NAME_get_text_by_NID(
+                           subjectName, NID_commonName, cn, sizeof(cn));
+               if (strcmp (cn, c->host) == 0)
+                       cn_match = true;
+               else
+                       Log(LOG_ERR, "CN mismatch! Got \"%s\", expected \"%s\"", cn, c->host);
+
+               snprintf(msg, sizeof(msg), "%svalid peer certificate", cert_ok ? "":"in");
+               LogOpenSSL_CertInfo(client_cert, msg);
+               X509_free(client_cert);
+               cert_seen = true;
+       }
 #endif
 #ifdef HAVE_LIBGNUTLS
+       unsigned int status;
+       int ret;
+       gnutls_credentials_type_t cred;
        gnutls_session_t sess = c->ssl_state.gnutls_session;
        gnutls_cipher_algorithm_t cipher = gnutls_cipher_get(sess);
+       gnutls_compression_method_t comp = gnutls_compression_get(sess);
 
-       Log(LOG_INFO, "Connection %d: initialized %s using cipher %s-%s.",
+       if (comp != GNUTLS_COMP_NULL)
+               comp_alg = gnutls_compression_get_name(comp);
+       Log(LOG_INFO, "Connection %d: initialized %s using cipher %s-%s, %s.",
            c->sock,
            gnutls_protocol_get_name(gnutls_protocol_get_version(sess)),
            gnutls_cipher_get_name(cipher),
-           gnutls_mac_get_name(gnutls_mac_get(sess)));
+           gnutls_mac_get_name(gnutls_mac_get(sess)),
+            comp_alg);
+       ret = gnutls_certificate_verify_peers2(c->ssl_state.gnutls_session, &status);
+       if (ret < 0)
+               Log(LOG_ERR, "gnutls_certificate_verify_peers2 failed: %s", gnutls_strerror(ret));
+       if (ret == 0 && check_verification(status))
+               cert_ok = true;
+
+       cred = gnutls_auth_get_type (c->ssl_state.gnutls_session);
+       if (cred == GNUTLS_CRD_CERTIFICATE) {
+               gnutls_x509_crt_t cert;
+               unsigned cert_list_size;
+               const gnutls_datum_t *cert_list = gnutls_certificate_get_peers(sess, &cert_list_size);
+               if (!cert_list) {
+                       Log(LOG_ERR, "gnutls_certificate_get_peers() failed");
+                       return;
+               }
+               assert(cert_list_size > 0);
+               gnutls_x509_crt_init(&cert);
+               gnutls_x509_crt_import(cert, cert_list, GNUTLS_X509_FMT_DER);
+
+               char *cn;
+               size_t size = 0;
+               int err = gnutls_x509_crt_get_dn_by_oid(cert,
+                       GNUTLS_OID_X520_COMMON_NAME,
+                       0, 0, NULL, &size);
+               if (size == 0 || (err && err != GNUTLS_E_SHORT_MEMORY_BUFFER))
+                       goto done_cn_validation;
+               cn = LogMalloc(size);
+               if (!cn)
+                       goto done_cn_validation;
+               gnutls_x509_crt_get_dn_by_oid (cert,
+                       GNUTLS_OID_X520_COMMON_NAME,
+                       0, 0, cn, &size);
+               if (strcmp (cn, c->host) == 0)
+                       cn_match = true;
+               else
+                       Log(LOG_ERR, "CN mismatch! Got \"%s\", expected \"%s\"", cn, c->host);
+               free (cn);
+done_cn_validation:
+
+               snprintf(msg, sizeof(msg), "%svalid peer certificate", cert_ok ? "":"in");
+               LogGnuTLS_CertInfo(cert, msg);
+               gnutls_x509_crt_deinit(cert);
+               cert_seen = true;
+       }
 #endif
+       /*
+        * can be used later to check if connection was authenticated, e.g. if inbound connection
+        * tries to register itself as server. could also restrict /OPER to authenticated connections, etc.
+        */
+       if (cert_ok && cn_match)
+               Conn_OPTION_ADD(c, CONN_SSL_PEERCERT_OK);
+       if (!cert_seen)
+               Log(LOG_INFO, "Peer did not present a certificate");
 }
 
 
@@ -676,12 +977,15 @@ ConnSSL_Accept( CONNECTION *c )
        assert(c != NULL);
        if (!Conn_OPTION_ISSET(c, CONN_SSL)) {
 #ifdef HAVE_LIBGNUTLS
+               gnutls_certificate_request_t req;
                int err = gnutls_init(&c->ssl_state.gnutls_session, GNUTLS_SERVER);
                if (err) {
                        Log(LOG_ERR, "Failed to initialize new SSL session: %s",
                            gnutls_strerror(err));
                        return false;
                }
+               req = Conf_SSLOptions.RequireClientCert ? GNUTLS_CERT_REQUIRE : GNUTLS_CERT_REQUEST;
+               gnutls_certificate_server_set_request(c->ssl_state.gnutls_session, req);
 #endif
                if (!ConnSSL_Init_SSL(c))
                        return -1;
@@ -744,7 +1048,7 @@ ConnSSL_InitCertFp( CONNECTION *c )
                gnutls_x509_crt_deinit(cert);
                return 0;
        }
-       
+
        if (gnutls_x509_crt_import(cert, &cert_list[0],
                                   GNUTLS_X509_FMT_DER) != GNUTLS_E_SUCCESS) {
                gnutls_x509_crt_deinit(cert);