Score:0

Unable to verify TLS cert with only CommonName in NGINX reverse proxy

se flag

I'm looking to create an NGINX reverse proxy to my WiFi router, and I'm looking to verify the connection. My router uses a self-signed certificate which lists the tplinkwifi.net domain as the Subject Common Name (CN) but doesn't include any Subject Alternative Names (SAN).

I've already tried this with Caddy as well, but support documentation is clear that Caddy will not consider the CommonName. This is probably reasonable since it's been deprecated since around 2000, but since I'm stuck working with a legacy certificate I'm looking for a work-around.

I can't tell if NGINX will support this. The code has some legacy paths that mention falling back to commonname, but more recent releases rely on OpenSSL. I haven't done a code dive to confirm.

The problematic certificate:

-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIJAO2y2LjMMgMJMA0GCSqGSIb3DQEBCwUAMCYxCzAJBgNV
BAYTAkNOMRcwFQYDVQQDDA50cGxpbmt3aWZpLm5ldDAeFw0xMDAxMDEwMDAwMDBa
Fw0zMDEyMzEwMDAwMDBaMCYxCzAJBgNVBAYTAkNOMRcwFQYDVQQDDA50cGxpbmt3
aWZpLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxYm7686e9c
Dc9R37jKfJe5MRbexde4bgANyIUwGd2ZyXQhTYcAWUNnj+AA5JDfDUUwhqO0GJWE
wj77xplSjS6iiKoT0pMffyPOdZtM6vHm+pKSC3trpRaw7HUlhDOJC+8Vw+NWOz5i
6R8dWihd9+atKpuiPcU0zEn7JFXgCtXKksyiA73FXZa7td2N/laBUtww7zn0d7QJ
ahYi0AJMFvvqtmUU6lCAs4DVaLbvMt9NQtkGHnk9PdVJDUMkPZ9LhGy9LT4GDSlG
4n0dFyKB+1fcsAHDoil9zo5D6ObkZudvhZvl0HLm+81MKTmZJs/0/pc4ZajkWS2/
ZB8GBFBQtF8CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl
blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFI4D3y7RVWbZUAeT
J0ejh3/dz7iTMB8GA1UdIwQYMBaAFF0ec0ZdWbKTStafLg1Ufi01LbEkMA0GCSqG
SIb3DQEBCwUAA4IBAQBb2TIgM5f4F0MxrY8/GXVrOkz50g5qHb0lOBvQigHIx20I
KVeJ47t0bjbffaspUS9CV2a1gbmf0cbNmk+KenUY4eW6HJ9ZOy8kHVGm1NtnLEAq
/Sarb4OWxfq45PNpcZbR7CU3+SnueV+b3NZ8CpIifl3RtTsuYNsGQKsnPtEp1SaA
HuZZNznNWxVKU7yyoQIFDXFBwHVDJoke00x/gxBJgHXBqPEHtcXa9HrYeGKkuHfH
FvnnUtD1VIOQT3R9oAWMgkYQenox/zmBshpiGSXLQaGOVtM9UeXHSCjDceOZ5VNq
auGL6Br1aRq9/rBUb0V5Z4RE7Ey659XRqjW/uxEw
-----END CERTIFICATE-----
$ openssl x509 -in cert.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            [...]
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = CN, CN = tplinkwifi.net
        Validity
            Not Before: Jan  1 00:00:00 2010 GMT
            Not After : Dec 31 00:00:00 2030 GMT
        Subject: C = CN, CN = tplinkwifi.net
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                [...]
        X509v3 extensions:
            X509v3 Basic Constraints: 
                CA:FALSE
            Netscape Comment: 
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier: 
                [...]
            X509v3 Authority Key Identifier: 
                [...]

    Signature Algorithm: sha256WithRSAEncryption
         [...]

I'm able to get curl to authenticate and connect if I use the server's certificate in --cacert:

$ curl --cacert cert.pem https://tplinkwifi.net/
<?xml version="1.0" encoding="utf-8"?>
[...]

But not OpenSSL command line:

$ openssl s_client -servername tplinkwifi.net -connect tplinkwifi.net:443 -CAfile cert.pem 
CONNECTED(00000003)
depth=0 C = CN, CN = tplinkwifi.net
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = CN, CN = tplinkwifi.net
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 C = CN, CN = tplinkwifi.net
verify return:1
---
Certificate chain
 0 s:C = CN, CN = tplinkwifi.net
   i:C = CN, CN = tplinkwifi.net
-----BEGIN CERTIFICATE-----
MIIDSjCCAjKgAwIBAgIJAO2y2LjMMgMJMA0GCSqGSIb3DQEBCwUAMCYxCzAJBgNV
BAYTAkNOMRcwFQYDVQQDDA50cGxpbmt3aWZpLm5ldDAeFw0xMDAxMDEwMDAwMDBa
Fw0zMDEyMzEwMDAwMDBaMCYxCzAJBgNVBAYTAkNOMRcwFQYDVQQDDA50cGxpbmt3
aWZpLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKxYm7686e9c
Dc9R37jKfJe5MRbexde4bgANyIUwGd2ZyXQhTYcAWUNnj+AA5JDfDUUwhqO0GJWE
wj77xplSjS6iiKoT0pMffyPOdZtM6vHm+pKSC3trpRaw7HUlhDOJC+8Vw+NWOz5i
6R8dWihd9+atKpuiPcU0zEn7JFXgCtXKksyiA73FXZa7td2N/laBUtww7zn0d7QJ
ahYi0AJMFvvqtmUU6lCAs4DVaLbvMt9NQtkGHnk9PdVJDUMkPZ9LhGy9LT4GDSlG
4n0dFyKB+1fcsAHDoil9zo5D6ObkZudvhZvl0HLm+81MKTmZJs/0/pc4ZajkWS2/
ZB8GBFBQtF8CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3Bl
blNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFI4D3y7RVWbZUAeT
J0ejh3/dz7iTMB8GA1UdIwQYMBaAFF0ec0ZdWbKTStafLg1Ufi01LbEkMA0GCSqG
SIb3DQEBCwUAA4IBAQBb2TIgM5f4F0MxrY8/GXVrOkz50g5qHb0lOBvQigHIx20I
KVeJ47t0bjbffaspUS9CV2a1gbmf0cbNmk+KenUY4eW6HJ9ZOy8kHVGm1NtnLEAq
/Sarb4OWxfq45PNpcZbR7CU3+SnueV+b3NZ8CpIifl3RtTsuYNsGQKsnPtEp1SaA
HuZZNznNWxVKU7yyoQIFDXFBwHVDJoke00x/gxBJgHXBqPEHtcXa9HrYeGKkuHfH
FvnnUtD1VIOQT3R9oAWMgkYQenox/zmBshpiGSXLQaGOVtM9UeXHSCjDceOZ5VNq
auGL6Br1aRq9/rBUb0V5Z4RE7Ey659XRqjW/uxEw
-----END CERTIFICATE-----
---
Server certificate
subject=C = CN, CN = tplinkwifi.net

issuer=C = CN, CN = tplinkwifi.net

[...]
    Verify return code: 21 (unable to verify the first certificate)
[...]

I suspect the issue is that I'm using -CAfile and the certificate has CA:FALSE.

In NGINX, I've tried the following:

events {
}

error_log /dev/stdout debug;

http {
  upstream tplinkwifi.net {
    server tplinkwifi.net:443;
  }

  server {
    access_log /dev/stdout,severity=debug;
    listen 80;
    server_name router;

    location / {
      proxy_pass https://tplinkwifi.net;
      proxy_ssl_trusted_certificate /cert.pem;
      proxy_ssl_verify on;
      proxy_ssl_server_name on;
      proxy_ssl_name tplinkwifi.net;
      proxy_ssl_verify_depth 10;
    }
  }
}

When I connect I get a 502 Bad Gateway and log output:

2023/04/27 07:16:51 [error] 29#29: *1 upstream SSL certificate verify error: (21:unable to verify the first certificate) while SSL handshaking to upstream, client: 172.17.0.1, server: router, request: "GET / HTTP/1.1", upstream: "https://192.168.0.1:443/", host: "router"

This looks consistent with the OpenSSL command line behavior I'm seeing (both fail to verify the certificate).

I'm aware I could just ignore the certificate with curl -k and NGINX's proxy_ssl_verify off;, but I'm looking to actually authenticate the connection and I'd prefer to pin the certificate my router uses. I'm aware I could change to a different router, but the router is already new. I'd prefer not to alter the router either.

I'm open to using a different reverse proxy software, if there's one that can support the legacy CommonName-only certificate my router uses. While the certificate is problematic, I think I'm able to get an authenticated connection if I can pin my reverse proxy to the certificate.

Score:0
se flag

I ended up getting this mostly working.

I traced the NGINX error to X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE. Turns out, the certificate wasn't actually self signed. It was signed by a private CA, but that root wasn't included in the chain provided by the router. I didn't see a way to download the root either.

I made progress with OpenSSL next. Partial chain verification allows me to verify the leaf cert against itself, instead of needing to find the full chain and trust from the root:

openssl verify -partial_chain -CAfile tplink.pem tplink.pem
tplink.pem: OK

I traced -partial_chain through the OpenSSL code and found OPT_V_PARTIAL_CHAIN and X509_V_FLAG_PARTIAL_CHAIN. Interestingly, Curl uses partial chain verification by default. Envoy supports this as a flag, but I wanted to keep using NGINX.

I patched NGINX and compiled it:

diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c
index 9cc202c9..8fc78ed0 100644
--- a/src/http/modules/ngx_http_proxy_module.c
+++ b/src/http/modules/ngx_http_proxy_module.c
@@ -4993,6 +4993,12 @@ ngx_http_proxy_set_ssl(ngx_conf_t *cf, ngx_http_proxy_loc_conf_t *plcf)
         return NGX_ERROR;
     }

+    {
+        // Patch: Permit partial chain verification
+        X509_STORE *store = SSL_CTX_get_cert_store(plcf->upstream.ssl->ctx);
+        X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN);
+    }
+
     cln = ngx_pool_cleanup_add(cf->pool, 0);
     if (cln == NULL) {
         ngx_ssl_cleanup_ctx(plcf->upstream.ssl);

This allowed NGINX to validate the leaf certificate against itself and it worked!

Unfortunately, my tplink router generates a new TLS certificate on every reboot, so trust broke at that point. That's where I gave up, but hopefully these notes help someone else.

I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.