package http import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "net" "net/http" "os" "testing" "time" "github.com/rqlite/rqlite/v8/rtls" "golang.org/x/net/http2" ) func Test_TLSServiceInsecure(t *testing.T) { m := &MockStore{} c := &mockClusterService{} s := New("127.0.0.1:0", m, c, nil) cert, key, err := rtls.GenerateSelfSignedCert(pkix.Name{CommonName: "rqlite"}, time.Hour, 2048) if err != nil { t.Fatalf("failed to generate self-signed cert: %s", err) } s.CertFile = mustWriteTempFile(t, cert) s.KeyFile = mustWriteTempFile(t, key) s.BuildInfo = map[string]interface{}{ "version": "the version", } if err := s.Start(); err != nil { t.Fatalf("failed to start service") } if !s.HTTPS() { t.Fatalf("expected service to report HTTPS") } defer s.Close() url := fmt.Sprintf("https://%s", s.Addr().String()) // Test connecting with an HTTP client. tn := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: tn} resp, err := client.Get(url) if err != nil { t.Fatalf("failed to make HTTP request: %s", err) } if v := resp.Header.Get("X-RQLITE-VERSION"); v != "the version" { t.Fatalf("incorrect build version present in HTTP response header, got: %s", v) } // Test connecting with an HTTP/2 client. client = &http.Client{ Transport: &http2.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } resp, err = client.Get(url) if err != nil { t.Fatalf("failed to make HTTP/2 request: %s", err) } if v := resp.Header.Get("X-RQLITE-VERSION"); v != "the version" { t.Fatalf("incorrect build version present in HTTP/2 response header, got: %s", v) } } func Test_TLSServiceSecure(t *testing.T) { m := &MockStore{} c := &mockClusterService{} s := New("127.0.0.1:0", m, c, nil) cert, key, err := rtls.GenerateSelfSignedCertIPSAN(pkix.Name{CommonName: "rqlite.io"}, time.Hour, 2048, net.ParseIP("127.0.0.1")) if err != nil { t.Fatalf("failed to generate self-signed cert: %s", err) } s.CertFile = mustWriteTempFile(t, cert) s.KeyFile = mustWriteTempFile(t, key) s.BuildInfo = map[string]interface{}{ "version": "the version", } if err := s.Start(); err != nil { t.Fatalf("failed to start service") } if !s.HTTPS() { t.Fatalf("expected service to report HTTPS") } defer s.Close() url := fmt.Sprintf("https://%s", s.Addr().String()) // Create a TLS Config which verfies server cert, and trusts the CA cert. tlsConfig := &tls.Config{InsecureSkipVerify: false} tlsConfig.RootCAs = x509.NewCertPool() ok := tlsConfig.RootCAs.AppendCertsFromPEM(cert) if !ok { t.Fatalf("failed to parse CA certificate(s) for client verification in %q", cert) } // Test connecting with an HTTP1.1 client. client := &http.Client{Transport: &http.Transport{ TLSClientConfig: tlsConfig, }} resp, err := client.Get(url) if err != nil { t.Fatalf("failed to make HTTP request: %s", err) } if v := resp.Header.Get("X-RQLITE-VERSION"); v != "the version" { t.Fatalf("incorrect build version present in HTTP response header, got: %s", v) } // Test connecting with an HTTP/2 client. client = &http.Client{ Transport: &http2.Transport{ TLSClientConfig: tlsConfig, }, } resp, err = client.Get(url) if err != nil { t.Fatalf("failed to make HTTP/2 request: %s", err) } if v := resp.Header.Get("X-RQLITE-VERSION"); v != "the version" { t.Fatalf("incorrect build version present in HTTP/2 response header, got: %s", v) } } func Test_TLSServiceSecureMutual(t *testing.T) { // Generate a CA cert and key. caCertPEM, caKeyPEM, err := rtls.GenerateCACert(pkix.Name{CommonName: "ca.rqlite.io"}, time.Hour, 2048) if err != nil { t.Fatalf("failed to generate CA cert: %s", err) } caCert, _ := pem.Decode(caCertPEM) if caCert == nil { t.Fatal("failed to decode certificate") } caKey, _ := pem.Decode(caKeyPEM) if caKey == nil { t.Fatal("failed to decode key") } // parse the certificate and private key parsedCACert, err := x509.ParseCertificate(caCert.Bytes) if err != nil { t.Fatal(err) } parsedCAKey, err := x509.ParsePKCS1PrivateKey(caKey.Bytes) if err != nil { t.Fatal(err) } // Create a cert signed by the CA for the server certServer, keyServer, err := rtls.GenerateCertIPSAN(pkix.Name{CommonName: "server.rqlite.io"}, time.Hour, 2048, parsedCACert, parsedCAKey, net.ParseIP("127.0.0.1")) if err != nil { t.Fatalf("failed to generate server cert: %s", err) } // Create a cert signed by the CA for the client certClient, keyClient, err := rtls.GenerateCertIPSAN(pkix.Name{CommonName: "client.rqlite.io"}, time.Hour, 2048, parsedCACert, parsedCAKey, net.ParseIP("127.0.0.1")) if err != nil { t.Fatalf("failed to generate client cert: %s", err) } // Create and start the HTTP service. m := &MockStore{} c := &mockClusterService{} s := New("127.0.0.1:0", m, c, nil) s.CertFile = mustWriteTempFile(t, certServer) s.KeyFile = mustWriteTempFile(t, keyServer) s.CACertFile = mustWriteTempFile(t, caCertPEM) // Enables client verification by HTTP server s.BuildInfo = map[string]interface{}{ "version": "the version", } s.ClientVerify = true if err := s.Start(); err != nil { t.Fatalf("failed to start service") } defer s.Close() url := fmt.Sprintf("https://%s", s.Addr().String()) // Create a TLS Config which wil require verification of the server cert, and trusts the CA cert. tlsConfig := &tls.Config{InsecureSkipVerify: false} tlsConfig.RootCAs = x509.NewCertPool() ok := tlsConfig.RootCAs.AppendCertsFromPEM(caCertPEM) if !ok { t.Fatalf("failed to parse CA certificate(s) for client verification in %q", caCertPEM) } client := &http.Client{Transport: &http.Transport{ TLSClientConfig: tlsConfig, }} _, err = client.Get(url) if err == nil { t.Fatalf("made successful HTTP request by untrusted client") } // Now set the client cert, which as also been signed by the CA. This should // mean the HTTP server trusts the client. tlsConfig.Certificates = make([]tls.Certificate, 1) tlsConfig.Certificates[0], err = tls.X509KeyPair(certClient, keyClient) if err != nil { t.Fatalf("failed to set X509 key pair %s", err) } client = &http.Client{Transport: &http.Transport{ TLSClientConfig: tlsConfig, }} resp, err := client.Get(url) if err != nil { t.Fatalf("trusted client failed to make HTTP request: %s", err) } if v := resp.Header.Get("X-RQLITE-VERSION"); v != "the version" { t.Fatalf("incorrect build version present in HTTP response header, got: %s", v) } } // mustWriteTempFile writes the given bytes to a temporary file, and returns the // path to the file. If there is an error, it panics. The file will be automatically // deleted when the test ends. func mustWriteTempFile(t *testing.T, b []byte) string { f, err := os.CreateTemp(t.TempDir(), "rqlite-test") if err != nil { panic("failed to create temp file") } defer f.Close() if _, err := f.Write(b); err != nil { panic("failed to write to temp file") } return f.Name() }