From 6550654043aea2826a3950db7a19a9c21af38fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=BCger?= Date: Wed, 2 Sep 2026 14:26:55 +0200 Subject: [PATCH] web: reject a client CA that holds no certificates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x509.CertPool.AppendCertsFromPEM reports whether it parsed any certificate at all, and both call sites discarded it. A client_ca_file that exists but holds no usable PEM - a truncated copy, a DER encoded file, a path pointing at the wrong artifact - was accepted, and produced a pool with no subjects. The same applied to an inline client_ca. Under RequireAndVerifyClientCert that fails closed, so it is not an authentication bypass, but the operator gets a server that rejects every client with an opaque handshake error while Validate reports the configuration as good. Reject it at configuration time instead, naming the file so the cause is obvious. The emptiness check runs after the client auth policy has been validated. Two existing fixtures pin the ordering from opposite directions: one configures a readable but empty CA with no policy and expects the policy error, the other configures a missing CA file with no policy and expects the read error. Reporting emptiness last keeps both reporting what they did before, and reports the more fundamental fault first when a configuration carries several. The existing web_config_auth_clientCAs_invalid.bad.yml fixture only covers a missing file, which is why this went unnoticed, so add fixtures for a file that parses to nothing and for the inline equivalent. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Manuel RĂ¼ger --- web/testdata/tls-ca-chain-invalid.pem | 2 ++ ...web_config_auth_clientCAsText_nocerts.bad.yml | 5 +++++ .../web_config_auth_clientCAs_nocerts.bad.yml | 5 +++++ web/tls_config.go | 16 ++++++++++++++-- web/tls_config_test.go | 11 +++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 web/testdata/tls-ca-chain-invalid.pem create mode 100644 web/testdata/web_config_auth_clientCAsText_nocerts.bad.yml create mode 100644 web/testdata/web_config_auth_clientCAs_nocerts.bad.yml diff --git a/web/testdata/tls-ca-chain-invalid.pem b/web/testdata/tls-ca-chain-invalid.pem new file mode 100644 index 00000000..fb1a75b9 --- /dev/null +++ b/web/testdata/tls-ca-chain-invalid.pem @@ -0,0 +1,2 @@ +This file is intentionally not a PEM encoded certificate. It stands in for a +truncated, DER encoded or otherwise unusable CA bundle. diff --git a/web/testdata/web_config_auth_clientCAsText_nocerts.bad.yml b/web/testdata/web_config_auth_clientCAsText_nocerts.bad.yml new file mode 100644 index 00000000..b003b94b --- /dev/null +++ b/web/testdata/web_config_auth_clientCAsText_nocerts.bad.yml @@ -0,0 +1,5 @@ +tls_server_config: + cert_file: "server.crt" + key_file: "server.key" + client_auth_type: "RequireAndVerifyClientCert" + client_ca: "this is not a PEM encoded certificate" diff --git a/web/testdata/web_config_auth_clientCAs_nocerts.bad.yml b/web/testdata/web_config_auth_clientCAs_nocerts.bad.yml new file mode 100644 index 00000000..e7b76b7f --- /dev/null +++ b/web/testdata/web_config_auth_clientCAs_nocerts.bad.yml @@ -0,0 +1,5 @@ +tls_server_config: + cert_file: "server.crt" + key_file: "server.key" + client_auth_type: "RequireAndVerifyClientCert" + client_ca_file: "tls-ca-chain-invalid.pem" diff --git a/web/tls_config.go b/web/tls_config.go index b40be6bb..4aed6881 100644 --- a/web/tls_config.go +++ b/web/tls_config.go @@ -258,17 +258,22 @@ func ConfigToTLSConfig(c *TLSConfig) (*tls.Config, error) { cfg.CurvePreferences = cp } + // clientCAsEmpty records that a client CA was configured but held no + // usable certificate. It is reported after the client auth policy has been + // validated, so that a configuration carrying both faults keeps reporting + // the policy one. + clientCAsEmpty := false if c.ClientCAs != "" { clientCAPool := x509.NewCertPool() clientCAFile, err := os.ReadFile(c.ClientCAs) if err != nil { return nil, err } - clientCAPool.AppendCertsFromPEM(clientCAFile) + clientCAsEmpty = !clientCAPool.AppendCertsFromPEM(clientCAFile) cfg.ClientCAs = clientCAPool } else if c.ClientCAsText != "" { clientCAPool := x509.NewCertPool() - clientCAPool.AppendCertsFromPEM([]byte(c.ClientCAsText)) + clientCAsEmpty = !clientCAPool.AppendCertsFromPEM([]byte(c.ClientCAsText)) cfg.ClientCAs = clientCAPool } @@ -296,6 +301,13 @@ func ConfigToTLSConfig(c *TLSConfig) (*tls.Config, error) { return nil, errors.New("client CA's have been configured without a Client Auth Policy") } + if clientCAsEmpty { + if c.ClientCAs != "" { + return nil, fmt.Errorf("no client CA certificates found in client_ca_file (%s)", c.ClientCAs) + } + return nil, errors.New("no client CA certificates found in client_ca") + } + return cfg, nil } diff --git a/web/tls_config_test.go b/web/tls_config_test.go index b8b3a966..6498e14c 100644 --- a/web/tls_config_test.go +++ b/web/tls_config_test.go @@ -52,6 +52,7 @@ var ( "Invalid Cert or CertPath": regexp.MustCompile(`missing one of cert or cert_file`), "Invalid Key or KeyPath": regexp.MustCompile(`missing one of key or key_file`), "ClientCA set without policy": regexp.MustCompile(`client CA's have been configured without a Client Auth Policy`), + "No client CA certificates": regexp.MustCompile(`no client CA certificates found in client_ca`), "Bad password": regexp.MustCompile(`hashedSecret too short to be a bcrypted password`), "Unauthorized": regexp.MustCompile(`Unauthorized`), "Forbidden": regexp.MustCompile(`Forbidden`), @@ -171,6 +172,16 @@ func TestYAMLFiles(t *testing.T) { YAMLConfigPath: "testdata/web_config_auth_clientCAs_invalid.bad.yml", ExpectedError: ErrorMap["No such file"], }, + { + Name: `invalid config yml (client CA file without certificates)`, + YAMLConfigPath: "testdata/web_config_auth_clientCAs_nocerts.bad.yml", + ExpectedError: ErrorMap["No client CA certificates"], + }, + { + Name: `invalid config yml (inline client CA without certificates)`, + YAMLConfigPath: "testdata/web_config_auth_clientCAsText_nocerts.bad.yml", + ExpectedError: ErrorMap["No client CA certificates"], + }, { Name: `invalid config yml (invalid user list)`, YAMLConfigPath: "testdata/web_config_auth_user_list_invalid.bad.yml",