diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cea7ec..e5a37c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.8.4 + +- Support loading client certificates from directories + ## 1.8.3 - reload tls server key pair if the file changes diff --git a/exthttp/listener.go b/exthttp/listener.go index 826fad7..581abc2 100644 --- a/exthttp/listener.go +++ b/exthttp/listener.go @@ -173,11 +173,19 @@ func prepareHttpsServer(port int, spec ListenSpecification) (*http.Server, func( func loadCertPool(filePaths []string) (*x509.CertPool, error) { pool := x509.NewCertPool() for _, filePath := range filePaths { - caCert, err := os.ReadFile(filePath) - if err != nil { - return nil, err - } - pool.AppendCertsFromPEM(caCert) + _ = filepath.Walk(filePath, func(path string, info os.FileInfo, _ error) error { + if info.IsDir() { + return nil + } + caCert, err := os.ReadFile(path) + if err == nil { + log.Debug().Msgf("Loading CA certificate from %s", path) + pool.AppendCertsFromPEM(caCert) + } else { + log.Error().Err(err).Msgf("Failed to read CA certificate from %s", path) + } + return nil + }) } return pool, nil } diff --git a/exthttp/listener_test.go b/exthttp/listener_test.go index bd3b453..6e9b5ac 100644 --- a/exthttp/listener_test.go +++ b/exthttp/listener_test.go @@ -8,6 +8,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "github.com/madflojo/testcerts" "github.com/phayes/freeport" "github.com/stretchr/testify/require" "net" @@ -76,52 +77,83 @@ func TestStartHttpServer(t *testing.T) { } func TestStartHttpsServer(t *testing.T) { + certs, err := testcerts.NewCA().NewKeyPair("localhost") + require.NoError(t, err) + + cert, key, err := certs.ToTempFile(t.TempDir()) + require.NoError(t, err) + port, err := freeport.GetFreePort() require.NoError(t, err) server, start, err := prepareHttpsServer(port, ListenSpecification{ - TlsServerCert: "testdata/cert.pem", - TlsServerKey: "testdata/key.pem", + TlsServerCert: cert.Name(), + TlsServerKey: key.Name(), }) require.NoError(t, err) go start() defer server.Close() - _, err = http.Get(fmt.Sprintf("https://localhost:%d", port)) - require.ErrorContains(t, err, "certificate") + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(certs.PublicKey()) + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + }, + }, + } + _, err = client.Get(fmt.Sprintf("https://localhost:%d", port)) + require.NoError(t, err) } func TestStartHttpsServerMustFailWhenCertificateCannotBeFound(t *testing.T) { + _, key, err := testcerts.GenerateCertsToTempFile(t.TempDir()) + require.NoError(t, err) + port, err := freeport.GetFreePort() require.NoError(t, err) _, _, err = prepareHttpsServer(port, ListenSpecification{ - TlsServerCert: "testdata/unknown.pem", - TlsServerKey: "testdata/key.pem", + TlsServerCert: filepath.Join(t.TempDir(), "unknown.pem"), + TlsServerKey: key, }) require.ErrorContains(t, err, "no such file or directory") } func TestStartHttpsServerMustFailWhenKeyCannotBeFound(t *testing.T) { + _, key, err := testcerts.GenerateCertsToTempFile(t.TempDir()) + require.NoError(t, err) + port, err := freeport.GetFreePort() require.NoError(t, err) _, _, err = prepareHttpsServer(port, ListenSpecification{ - TlsServerCert: "testdata/cert.pem", - TlsServerKey: "testdata/unknown.pem", + TlsServerCert: key, + TlsServerKey: filepath.Join(t.TempDir(), "unknown.pem"), }) require.ErrorContains(t, err, "no such file or directory") } func TestStartHttpsServerWithMutualTlsMustRefuseConnectionsWithoutMutualTls(t *testing.T) { + ca := testcerts.NewCA() + caCerts, _, err := ca.ToTempFile(t.TempDir()) + require.NoError(t, err) + + serverPair, err := ca.NewKeyPair("localhost") + require.NoError(t, err) + serverCert, serverKey, err := serverPair.ToTempFile(t.TempDir()) + require.NoError(t, err) + port, err := freeport.GetFreePort() require.NoError(t, err) server, start, err := prepareHttpsServer(port, ListenSpecification{ - TlsServerCert: "testdata/cert.pem", - TlsServerKey: "testdata/key.pem", - TlsClientCas: []string{"testdata/cert.pem"}, + TlsServerCert: serverCert.Name(), + TlsServerKey: serverKey.Name(), + TlsClientCas: []string{caCerts.Name()}, }) require.NoError(t, err) @@ -129,37 +161,47 @@ func TestStartHttpsServerWithMutualTlsMustRefuseConnectionsWithoutMutualTls(t *t defer server.Close() _, err = http.Get(fmt.Sprintf("https://localhost:%d", port)) - require.ErrorContains(t, err, "failed to verify certificate") } -func TestStartHttpsServerWithMutualTlsMustSuccessfullyAllowMutualTlsConnections(t *testing.T) { +func TestStartHttpsServerEnforcingMutualTls(t *testing.T) { + ca := testcerts.NewCA() + + clientPair, err := ca.NewKeyPair() + require.NoError(t, err) + clientCertDir := t.TempDir() + err = os.WriteFile(filepath.Join(clientCertDir, "client.crt"), clientPair.PublicKey(), 0644) + require.NoError(t, err) + + serverPair, err := ca.NewKeyPair("localhost") + require.NoError(t, err) + serverCert, serverKey, err := serverPair.ToTempFile(t.TempDir()) + require.NoError(t, err) + port, err := freeport.GetFreePort() require.NoError(t, err) server, start, err := prepareHttpsServer(port, ListenSpecification{ - TlsServerCert: "testdata/cert.pem", - TlsServerKey: "testdata/key.pem", - TlsClientCas: []string{"testdata/cert.pem"}, + TlsServerCert: serverCert.Name(), + TlsServerKey: serverKey.Name(), + TlsClientCas: []string{clientCertDir}, }) require.NoError(t, err) go start() defer server.Close() - cert, err := tls.LoadX509KeyPair("testdata/cert.pem", "testdata/key.pem") + clientCertificate, err := tls.X509KeyPair(clientPair.PublicKey(), clientPair.PrivateKey()) require.NoError(t, err) - caCert, err := os.ReadFile("testdata/cert.pem") - require.NoError(t, err) - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(caCert) + clientPool := x509.NewCertPool() + clientPool.AppendCertsFromPEM(serverPair.PublicKey()) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - RootCAs: caCertPool, - Certificates: []tls.Certificate{cert}, + RootCAs: clientPool, + Certificates: []tls.Certificate{clientCertificate}, }, }, } diff --git a/exthttp/testdata/cert.pem b/exthttp/testdata/cert.pem deleted file mode 100644 index 6dc9075..0000000 --- a/exthttp/testdata/cert.pem +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID8zCCAtugAwIBAgIUB6LwyfohLxc7uNrjX1e9Jh33yvgwDQYJKoZIhvcNAQEL -BQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM -DU1vdW50YWluIFZpZXcxGjAYBgNVBAoMEVlvdXIgT3JnYW5pemF0aW9uMRIwEAYD -VQQLDAlZb3VyIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzAxMDkwODUw -MjFaFw0zMzAxMDYwODUwMjFaMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp -Zm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRowGAYDVQQKDBFZb3VyIE9y -Z2FuaXphdGlvbjESMBAGA1UECwwJWW91ciBVbml0MRIwEAYDVQQDDAlsb2NhbGhv -c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4zr05DurDQ2z9KEXL -41rxRJMhbH3tQvA7Z3kEOrU+372l6A5emxwUKmFAqPKQp1+05XdbOskXdWoqr3qL -yxBxhBLisu6RRWmhttoCxqjerUeLyoOl0jXJJUx5ckW9GgD9cfGcJktkItG2Bo4B -U9hosuTy8n8+bb5enjfzi6Rzsn/Qsr2U3jSMwgnPJGkYQhINBuPUT7jWerP5EOgB -36rCHfKseqc0zsI7VdXZO1UIjurOGwcKIO3PHEIBo50Xg34fm7nx4LBdLS+rXB2h -pi3dpT4grMGZACpSL3sD3OSvCu5AOEgjlKHJ101TdSt8jfu7jCEOrRpdpikZNBZn -6i1jAgMBAAGjaTBnMB0GA1UdDgQWBBQeur0c9yLLWFSJfgPGj6sL+SsMZzAfBgNV -HSMEGDAWgBQeur0c9yLLWFSJfgPGj6sL+SsMZzAPBgNVHRMBAf8EBTADAQH/MBQG -A1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAk3L6JpQN4cMc -6DRKPGsQf5B+WWSyxsz8H7S2mL1KkRpHzcjZn3a4nUMjR6dzBxu+/N63oFmQy8Jw -SR0h5guSx1MUXXYSVfuhwBXyaBnSbY3oJw/9ean7ETgj0Wuv6GtiCfD9XD6rLL0e -NN0sitdX+Pkc0dT+i/ZehMVijNGiTx6KvPhglJKkjgpGInwPlzGfZ0i7iY7DwEJ/ -GkUxdGITtHbHnlETec6tni7j1EprzkP1GMi2JAXYX7kokVgTTYYuiKcCJ+BGlpzw -XNQgTCIsk6j3VxYLXh0hs83HTjVbU58LpoLhSMtOkqp0Rn96H5CZTSx2qzKPh8bp -1HTj/0lTvA== ------END CERTIFICATE----- diff --git a/exthttp/testdata/key.pem b/exthttp/testdata/key.pem deleted file mode 100644 index a731137..0000000 --- a/exthttp/testdata/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC4zr05DurDQ2z9 -KEXL41rxRJMhbH3tQvA7Z3kEOrU+372l6A5emxwUKmFAqPKQp1+05XdbOskXdWoq -r3qLyxBxhBLisu6RRWmhttoCxqjerUeLyoOl0jXJJUx5ckW9GgD9cfGcJktkItG2 -Bo4BU9hosuTy8n8+bb5enjfzi6Rzsn/Qsr2U3jSMwgnPJGkYQhINBuPUT7jWerP5 -EOgB36rCHfKseqc0zsI7VdXZO1UIjurOGwcKIO3PHEIBo50Xg34fm7nx4LBdLS+r -XB2hpi3dpT4grMGZACpSL3sD3OSvCu5AOEgjlKHJ101TdSt8jfu7jCEOrRpdpikZ -NBZn6i1jAgMBAAECggEATEvp2fMbH9cn2VI1konAA2h0t7FTQc9HX7cFwqW8KwNz -B9oIiK1Px9GBShEV53t6KzQq9QqNd7ZdSNceaDVDCiJlK5uEm4wFIqLbWZcLo7b1 -GTTX6e8hjnPsIR14xivEqd3PSlCTAnnPi28kVE75wqvMkrJjrvHezLBUWCNYFv0w -Zu4BvoAoPa+lY14SXJcS516NbnakRTyudRP0i71DhUw5Zna8gmaPcqXkNp6VLS7P -MgePKDANSrMaX355G8sPuJ02XIRojZpO06++p9gj6CCM2is7OoILtMxdO0ca+zCu -Bue5+crdUZGamER3InMPNHfQBQssOvydKRnfHsJLMQKBgQDYfS9gopoTcG1lFFAZ -SmX4clrFx8Qg//4JqpEFUGcTGo200Q1vE6QF8E7bsT0D2uBb+aKBW+7eJD0I7/Se -N8pE40zQryFfwXPKQAohQR0wOcFn2/2fGCp3KrfGyfTozWidG7W8/4qBYeeI7Gxs -q7fgCpWj831t0shHBeXPRfwtsQKBgQDaiVUBAJxoxmG83Jf+/r+3DA/BaNaskJbZ -P5HhZl7qaRrEq43QpCAbVDdQQLC6f6c9j3RyecL8aRrUsaO2KcE+AeDVjfq1vUgI -HOayVEF7U59JjB+utXyGsy7PmPW0pD7BHbgCBGV/O0rZOWBRo2KlU0rCYGRSKIEq -rEDpQKNtUwKBgQCFA623d1CpvvtIDsoEMAUlOMXzHYGxMPiaYdWG6VbbkwYcYhIZ -/HxcNcGOFIFDvBj8Cg7B4oWKscNamWy4RdkeqHYLBn/AAPGvA9f1hLd1aRcfRDi5 -prR40aNnHbE/1O2BEoSAopYsVsZXB8S6pGtu2bIFsVaQwuDRWptP1lVSkQKBgQCb -dO3/FIwvDFAipVmKj3WZpP7gOs/bWc+1Iz+G8+e5IKNmHBN5xAcC6dmfQSV2xbAW -XqIbfPpzy+DGRMeMog9RKMzjnWgnOEqxWr3RRZZ/QHEjRIaVJY071OML0meW5O+v -OJDY/n+lDmykMeiOqodVy/Z2Z1N2DlI0JOzYAJ9A+QKBgCl92AxWKPu6dKC3Ei6F -OmQSs9Hp+uiT1d9oFxqlctJKbwnFhWmfVWVgjbX+g7W7s4k72UVm04TsMZBeyidN -odDK7tGwGIaX1xwP6FDtqMjKEabLNdcBZN9oYVv9tWAtqFRbfDTEQo++fysOGUlk -PIVkQCmubR++rENqrI1MiRgM ------END PRIVATE KEY-----