Project

General

Profile

[Solved] Client Authentication with Certificate not fully working

Added by Roko about 1 month ago

Hi,

we are planning to use lighttpd in an embedded linux environment on ARMv7 as a proxy.
One requirement we have is the certificate-based client authentication where we tried
the planned functionality on a PC with gentoo linux first.

We discovered a problem where certificate-based client authentication only works partially.

Tested on System:

Linux 5.3.12-gentoo x86_64 Intel(R) Core(TM) i5-6600 CPU @ 3.30GHz GenuineIntel GNU/Linux

Affected versions of lighttpd:

All versions beginning with 1.4.56-rc1 up to current repository state (tested with 1.4.55, 1.4.56-rc1, 1.4.59, latest repository 2021-06-21)
NOTE:
1.4.55 doesn't have the problem

Configuration

config {
    var.CWD                           = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd" 
    var.PID                           = 32483
    var.base_dir                      = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd" 
    var.log_root                      = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/log" 
    var.server_root                   = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/srv" 
    var.state_dir                     = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/run" 
    var.conf_dir                      = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/conf" 
    var.cache_dir                     = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/cache" 
    var.socket_dir                    = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/sockets" 
    var.vhosts_dir                    = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/srv/vhosts" 
    server.port                       = 8001
    server.document-root              = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/srv/htdocs" 
    server.pid-file                   = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/run/lighttpd.pid" 
    server.errorlog                   = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/log/error.log" 
    server.network-backend            = "sendfile" 
    server.max-fds                    = 2048
    server.stat-cache-engine          = "simple" 
    server.max-connections            = 1024
    server.feature-flags              = (
        "server.h2proto" => "disable",
        "server.h2c"     => "disable",
        # 2
    )
    server.modules                    = ("mod_proxy", "mod_openssl")
    debug.log-request-handling        = "enable" 
    debug.log-request-header          = "enable" 
    debug.log-request-header-on-error = "enable" 
    debug.log-response-header         = "enable" 
    debug.log-file-not-found          = "enable" 
    debug.log-state-handling          = "enable" 
    debug.log-condition-handling      = "enable" 
    ssl.engine                        = "enable" 
    ssl.pemfile                       = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/conf/https.pem" 
    ssl.privkey                       = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/conf/https.key" 
    ssl.ca-file                       = "/home/rko/test/20210618_Reverse_Proxy_lighttpd/lighttpd/conf/ca-root.pem" 
    ssl.verifyclient.activate         = "enable" 
    ssl.verifyclient.enforce          = "disable" 
    ssl.verifyclient.depth            = 2

    $HTTP["url"] =~ "(^/http)" {
        # block 1
        proxy.forwarded = (
            "for"         => 1,
            "proto"       => 1,
            "remote_user" => 1,
            # 3
        )
        proxy.server    = (
            "" => (
                "HTTP" => (
                    "host" => "192.168.1.5",
                    "port" => 8888,
                    # 2
                ),
            ),
        )

    } # end of $HTTP["url"] =~ "(^/http)" 

    $HTTP["url"] =~ "(^/api)" {
        # block 2
        proxy.server = (
            "" => (
                "API" => (
                    "host" => "192.168.1.5",
                    "port" => 8889,
                    # 2
                ),
            ),
        )

    } # end of $HTTP["url"] =~ "(^/api)" 

    $HTTP["url"] =~ "(^/ws)" {
        # block 3
        proxy.server = (
            "" => (
                "Websocket" => (
                    "host" => "192.168.1.5",
                    "port" => 8887,
                    # 2
                ),
            ),
        )
        proxy.header = (
            "upgrade" => "enable",
        )

    } # end of $HTTP["url"] =~ "(^/ws)" 
}

Clients used

Firefox 88.0.1 (64-Bit)
Chrome 91.0.4472.101 (Official Build, ungoogled-chromium) (64-bit)

Explanation of problem:
Given is a server certificate and private key (https.pem + https.key - see config above) for server authentication by the client - certificate is signed by an internal CA (not configured or used on server side).

For certificate-based client authentication an appropriate CA is configured on server side (ca-root.pem - see config above).
It is quite common to use different CAs for these two purposes (although one could use a single one).

If we try this usual use case that the client's certificate is signed by a different CA than the server certificate, we get the following error in the error.log on the server side:

2021-06-21 11:06:11: connections.c.1414) state at enter 7 read
2021-06-21 11:06:11: connections.c.1060) state for fd:7 id:0 read
2021-06-21 11:06:11: mod_openssl.c.1095) SSL: building cert chain for TLS server name localhost: error:00000000:lib(0):func(0):reason(0)
2021-06-21 11:06:11: mod_openssl.c.3095) SSL: 1 error:1417A179:SSL routines:tls_post_process_client_hello:cert cb error
2021-06-21 11:06:11: connections.c.1060) state for fd:7 id:0 error
2021-06-21 11:06:11: connections.c.191) shutdown for fd 7
2021-06-21 11:06:11: connections.c.1060) state for fd:7 id:0 close
2021-06-21 11:06:11: connections.c.1421) state at exit: 7 close

In this case both browsers show SSL_ERROR_INTERNAL_ERROR_ALERT.

If we use a single CA for both authentication sides then it works!

We think that this might be a bug.

Many greetings
Roman


Replies (4)

RE: Client Authentication with Certificate not fully working - Added by gstrauss about 1 month ago

We think that this might be a bug.

It does not sound like you read the lighttpd TLS docs. Instead, it sounds like you should have been using ssl.ca-dn-file since lighttpd 1.4.46, and your misconfiguration may be sending larger-than-necessary ServerHello responses.

Please carefully read the lighttpd TLS docs. Specifically, it is strongly recommended that the certificate chain of intermediates be included in ssl.pemfile, ordered from the leaf cert to the last cert prior to the root cert. This has been supported in ssl.pemfile in $SERVER["socket"] for a very long time, and is supported in ssl.pemfile in $HTTP["host"] (and everywhere else that is valid) since lighttpd 1.4.56.

In your case, I think that ssl.ca-file should contain only the root cert for the CA which signs the client certificates, and the CA cert for the client certificate signer. If there is an intermediate which signs the certificate you expect from the client, include that cert in both ssl.ca-file and ssl.ca-dn-file so that lighttpd will send the DN for the intermediate in the ServerHello when the server sends the certificate request to the client.

BTW, in lighttpd 1.4.60 (not yet released), ssl.ca-file has been renamed ssl.verifyclient.ca-file, and ssl.ca-dn-file has been renamed to ssl.verifyclient.ca-dn-file to better indicate when these directives are intended to be used. (ssl.ca-file and ssl.ca-dn-file are still supported, and are remapped to the new names)

The full certificate chain for ssl.pemfile should be in ssl.pemfile and should not rely on ssl.ca-file at all, as the server cert can be issued by an entirely separate chain of CAs than what is used for the client certificates, and you do not want someone to generate a certificate on the (external) CA, different from your internal CA used to sign client certs, and then accidentally allow access to your server via a certificate issued from the external CA. If you have not been using ssl.ca-dn-file, then you might be exposed due to your misconfiguration. It would be much better to separate these, putting the full chain in ssl.pemfile, and leaving ssl.ca-file (ssl.verifyclient.ca-file) for your client certificate trusted CA signers and root.

RE: Client Authentication with Certificate not fully working - Added by Roko about 1 month ago

Thank you for your prompt answer. Well we read the docs, not only once.

We have a rather simple setup here:
Server CA signs https.pem
Clientauth CA signs Client cert(s)
No intermediate certificates at all here.

Nevertheless the trigger in the right direction was

...it is strongly recommended that the certificate chain of intermediates be included in ssl.pemfile, ordered from the leaf cert to the last cert prior to the root cert...

The lighttpd TLS docs state for ssl.pemfile: path to the PEM file certificate chain
So the Server CA was missing...

After creation of a new file with https.pem + https.key + ca.pem for ssl.pemfile and deletion of ssl.privkey from config it works now.

Questions:
  1. Until 1.4.55 it works without the CA so it was not clear to us that there is a change in the configuration needed. We used a solution with libmicrohttpd in the past where we also didn't need to use the Server CA in the configuration.
  2. :

If there is an intermediate which signs the certificate you expect from the client, include that cert in both ssl.ca-file and ssl.ca-dn-file so that lighttpd will send the DN for the intermediate in the ServerHello when the server sends the certificate request to the client.

Hm sounds strange.

If you have not been using ssl.ca-dn-file, then you might be exposed due to your misconfiguration. It would be much better to separate these, putting the full chain in ssl.pemfile, and leaving ssl.ca-file (ssl.verifyclient.ca-file) for your client certificate trusted CA signers and root.

For what is ssl.ca-dn-file needed/useful? The explanation path to file for certificate authorities (CA) from which client should select client certs (if needed) rather confuses us. Even after reading your answer it's not clear to us.
Do we need ssl.ca-dn-file even without any intermediate certs? What about larger-than-necessary ServerHello responses?

Thanks very much
Roman

RE: Client Authentication with Certificate not fully working - Added by gstrauss about 1 month ago

The lighttpd TLS docs state for ssl.pemfile: path to the PEM file certificate chain

Yes, there is an important difference between "certificate" and "certificate chain"

There is also (potentially) a difference between ssl.pemfile certificate chain and the client certificate chains.

For what is ssl.ca-dn-file needed/useful?

ssl.ca-dn-file is needed when the CA which signs the client certificate certs is not a root CA, e.g. if a distinct root CA signs the CA cert which signs the client certs.

Do we need ssl.ca-dn-file even without any intermediate certs? What about larger-than-necessary ServerHello responses?

No, not needed if there are no intermediate certs used to sign client certificates. The ssl.ca-dn-file is to specify to the client that the client certificate selection should use a certificate signed by (issued by) the specified DN (distinguished name of the subject in the cert in ssl.ca-dn-file), and that any other issuer (including trusted parent chain of the cert of the client certificate signing CA in ssl.ca-dn-file) will not be accepted as a client cert. I think I misunderstood what you had written earlier, and thought that you might have an intermediate CA signing client certificates. If that is not the case, then you do not need ssl.ca-dn-file.

Larger than necessary ServerHello might occur if ssl.ca-dn-file is not specified and ssl.ca-file contains many trusted certs, e.g. including different CAs for ssl.pemfile, which are not what you desire for client certificate. If you have excess trusted CA certs in ssl.ca-file and you do not specify ssl.ca-dn-file, then you might be vulnerable to accepting client certificates signed by those other, different CAs. After all, ssl.ca-file contains certs for CAs that you are telling lighttpd to trust.

If there is an intermediate which signs the certificate you expect from the client, include that cert in both ssl.ca-file and ssl.ca-dn-file so that lighttpd will send the DN for the intermediate in the ServerHello when the server sends the certificate request to the client.

Hm sounds strange.

ssl.ca-file (ssl.verifyclient.ca-file) should contain the certificate chain (including root CA) of the CA used to sign (issue) client certificates.
ssl.ca-dn-file (ssl.verifyclient.ca-dn-file) should contain only the certificate of the CA used to sign (issue) client certificates.

ssl.ca-dn-file (ssl.verifyclient.ca-dn-file) is used in ServerHello when sending a client certificate request, telling the client to select a certificate signed by (issued by) the DN of the subject of the cert in ssl.ca-dn-file
ssl.ca-file (ssl.verifyclient.ca-file) is used to validate the client certificate presented by the client in response to ServerHello containing a client certificate request.

After creation of a new file with https.pem + https.key + ca.pem for ssl.pemfile and deletion of ssl.privkey from config it works now.

As an aside, ssl.privkey is separate from the discussion above, and since lighttpd 1.4.53 may be separate from ssl.pemfile. ssl.privkey is not relevant to your issue with client certificate verification.

RE: Client Authentication with Certificate not fully working - Added by Roko about 1 month ago

Thank you for this great explanation, this clarifies it!

    (1-4/4)