============

Make self-signed cert:
$ openssl req -x509 -newkey ec:<(openssl ecparam -name secp384r1) -keyout key.pem -out cert.pem -nodes -sha256 -days 3650 -subj '/CN=server'

Record our fingerprint in an environment variable for the client to use later:
$ server_fingerprint="$(openssl x509 -in cert.pem -noout -sha256 -fingerprint | sed 's/.*=//;s/\(.*\)/\L\1/')"

Start openvpn with tls verify script:
$ sudo openvpn --server 10.66.0.0 255.255.255.0 --dev tun --dh none --ca none --cert cert.pem --key key.pem --tls-verify $(readlink -f tls-verify.sh) --script-security 2

TLS Verify Script:
==================

#!/bin/sh
[ -n "$tls_digest_sha256_0" -a -e "/tmp/allowed-openvpn-fingerprints/$tls_digest_sha256_0" ]

Client side:
============

Make self-signed cert:
$ openssl req -x509 -newkey ec:<(openssl ecparam -name secp384r1) -keyout key.pem -out cert.pem -nodes -sha256 -days 3650 -subj '/CN=client'

"Tell" the server about our fingerprint:
$ mkdir -p /tmp/allowed-openvpn-fingerprints; touch "/tmp/allowed-openvpn-fingerprints/$(openssl x509 -in cert.pem -noout -sha256 -fingerprint | sed 's/.*=//;s/\(.*\)/\L\1/')"

Start openvpn with server fingerprint verification:
$ sudo openvpn --client --remote 127.0.0.1 --dev tun --ca none --cert cert.pem --key key.pem --verify-hash "$server_fingerprint" SHA256 --nobind

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
---
Sorry for the double post. Somebody suggested I submit this as an actual
git patch instead of as just part of the goofy email I sent earlier. So
here's that email turned into a proper patch that you can apply. Feel
free to hack the patch to pieces or discard it and start over.

 src/openvpn/init.c               | 1 +
 src/openvpn/options.c            | 9 ++++++++-
 src/openvpn/options.h            | 1 +
 src/openvpn/ssl.c                | 2 +-
 src/openvpn/ssl_common.h         | 1 +
 src/openvpn/ssl_verify.c         | 2 +-
 src/openvpn/ssl_verify_mbedtls.c | 2 +-
 src/openvpn/ssl_verify_openssl.c | 2 +-
 8 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/openvpn/init.c b/src/openvpn/init.c
index 36c1a4c4..4da994d8 100644
--- a/src/openvpn/init.c
+++ b/src/openvpn/init.c
@@ -2722,6 +2722,7 @@ do_init_crypto_tls(struct context *c, const unsigned int flags)
     to.remote_cert_eku = options->remote_cert_eku;
     to.verify_hash = options->verify_hash;
     to.verify_hash_algo = options->verify_hash_algo;
+    to.ca_file_none = options->ca_file_none;
 #ifdef ENABLE_X509ALTUSERNAME
     to.x509_username_field = (char *) options->x509_username_field;
 #else
diff --git a/src/openvpn/options.c b/src/openvpn/options.c
index 426057ab..dad706d9 100644
--- a/src/openvpn/options.c
+++ b/src/openvpn/options.c
@@ -3259,7 +3259,10 @@ options_postprocess_filechecks(struct options *options)
 
     /* ** SSL/TLS/crypto related files ** */
     errs |= check_file_access(CHKACC_FILE|CHKACC_INLINE, options->dh_file, R_OK, "--dh");
-    errs |= check_file_access(CHKACC_FILE|CHKACC_INLINE, options->ca_file, R_OK, "--ca");
+    if (!options->ca_file_none)
+    {
+        errs |= check_file_access(CHKACC_FILE|CHKACC_INLINE, options->ca_file, R_OK, "--ca");
+    }
     errs |= check_file_access_chroot(options->chroot_dir, CHKACC_FILE, options->ca_path, R_OK, "--capath");
     errs |= check_file_access(CHKACC_FILE|CHKACC_INLINE, options->cert_file, R_OK, "--cert");
     errs |= check_file_access(CHKACC_FILE|CHKACC_INLINE, options->extra_certs_file, R_OK,
@@ -7638,6 +7641,10 @@ add_option(struct options *options,
         {
             options->ca_file_inline = p[2];
         }
+        else if (streq(p[1], "none"))
+        {
+            options->ca_file_none = true;
+        }
     }
 #ifndef ENABLE_CRYPTO_MBEDTLS
     else if (streq(p[0], "capath") && p[1] && !p[2])
diff --git a/src/openvpn/options.h b/src/openvpn/options.h
index f7d0145a..6ccdca49 100644
--- a/src/openvpn/options.h
+++ b/src/openvpn/options.h
@@ -489,6 +489,7 @@ struct options
     /* TLS (control channel) parms */
     bool tls_server;
     bool tls_client;
+    bool ca_file_none;
     const char *ca_file;
     const char *ca_path;
     const char *dh_file;
diff --git a/src/openvpn/ssl.c b/src/openvpn/ssl.c
index 669f941b..0ef0f31f 100644
--- a/src/openvpn/ssl.c
+++ b/src/openvpn/ssl.c
@@ -693,7 +693,7 @@ init_ssl(const struct options *options, struct tls_root_ctx *new_ctx)
         }
     }
 
-    if (options->ca_file || options->ca_path)
+    if ((!options->ca_file_none && options->ca_file) || options->ca_path)
     {
         tls_ctx_load_ca(new_ctx, options->ca_file, options->ca_file_inline,
                         options->ca_path, options->tls_server);
diff --git a/src/openvpn/ssl_common.h b/src/openvpn/ssl_common.h
index 08ef6ffa..e9ac5271 100644
--- a/src/openvpn/ssl_common.h
+++ b/src/openvpn/ssl_common.h
@@ -270,6 +270,7 @@ struct tls_options
     uint8_t *verify_hash;
     hash_algo_type verify_hash_algo;
     char *x509_username_field;
+    bool ca_file_none;
 
     /* allow openvpn config info to be
      * passed over control channel */
diff --git a/src/openvpn/ssl_verify.c b/src/openvpn/ssl_verify.c
index 25395b27..61873e6f 100644
--- a/src/openvpn/ssl_verify.c
+++ b/src/openvpn/ssl_verify.c
@@ -720,7 +720,7 @@ verify_cert(struct tls_session *session, openvpn_x509_cert_t *cert, int cert_dep
     }
 
     /* verify level 1 cert, i.e. the CA that signed our leaf cert */
-    if (cert_depth == 1 && opt->verify_hash)
+    if ((opt->ca_file_none || cert_depth == 1) && opt->verify_hash)
     {
         struct buffer ca_hash = {0};
 
diff --git a/src/openvpn/ssl_verify_mbedtls.c b/src/openvpn/ssl_verify_mbedtls.c
index fd31bbbd..147fb983 100644
--- a/src/openvpn/ssl_verify_mbedtls.c
+++ b/src/openvpn/ssl_verify_mbedtls.c
@@ -63,7 +63,7 @@ verify_callback(void *session_obj, mbedtls_x509_crt *cert, int cert_depth,
     cert_hash_remember(session, cert_depth, &cert_fingerprint);
 
     /* did peer present cert which was signed by our root cert? */
-    if (*flags != 0)
+    if (*flags != 0 && !session->opt->ca_file_none)
     {
         int ret = 0;
         char errstr[512] = { 0 };
diff --git a/src/openvpn/ssl_verify_openssl.c b/src/openvpn/ssl_verify_openssl.c
index 9b984751..10d351f4 100644
--- a/src/openvpn/ssl_verify_openssl.c
+++ b/src/openvpn/ssl_verify_openssl.c
@@ -66,7 +66,7 @@ verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
     cert_hash_remember(session, X509_STORE_CTX_get_error_depth(ctx), &cert_hash);
 
     /* did peer present cert which was signed by our root cert? */
-    if (!preverify_ok)
+    if (!preverify_ok && !session->opt->ca_file_none)
     {
         /* get the X509 name */
         char *subject = x509_get_subject(current_cert, &gc);
