diff --git a/TODO.md b/TODO.md index 68c70b53..9f9f1d7a 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ # General - [ ] SSL/TLS support +- [ ] Better user/pass support using bcrypt etc. - [ ] Pedantic state - [ ] brew, apt-get, rpm, chocately (windows) - [ ] Dynamic socket buffer sizes diff --git a/server/opts.go b/server/opts.go index def9e41e..0031dd6f 100644 --- a/server/opts.go +++ b/server/opts.go @@ -63,6 +63,8 @@ type authorization struct { type tlsConfig struct { certFile string keyFile string + caFile string + verify bool } // ProcessConfigFile processes a configuration file. @@ -207,6 +209,19 @@ func parseTLS(tlsm map[string]interface{}, opts *Options) error { return fmt.Errorf("error parsing tls config, expected 'key_file' to be filename") } tc.keyFile = keyFile + case "ca_file": + caFile, ok := mv.(string) + if !ok { + return fmt.Errorf("error parsing tls config, expected 'ca_file' to be filename") + } + tc.caFile = caFile + case "verify": + verify, ok := mv.(bool) + if !ok { + return fmt.Errorf("error parsing tls config, expected 'veridy' to be a boolean") + } + tc.verify = verify + default: return fmt.Errorf("error parsing tls config, unknown field [%q]", mk) } @@ -234,6 +249,10 @@ func parseTLS(tlsm map[string]interface{}, opts *Options) error { tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, }, } + // Require client certificates as needed + if tc.verify == true { + config.ClientAuth = tls.RequireAnyClientCert + } opts.TLSConfig = &config return nil } diff --git a/server/server.go b/server/server.go index d46ab8d7..d5aa685a 100644 --- a/server/server.go +++ b/server/server.go @@ -32,7 +32,9 @@ type Info struct { Host string `json:"host"` Port int `json:"port"` AuthRequired bool `json:"auth_required"` - TLSRequired bool `json:"ssl_required"` // ssl json used for older clients + SSLRequired bool `json:"ssl_required"` // ssl json used for older clients + TLSRequired bool `json:"tls_required"` + TLSVerify bool `json:"tls_verify"` MaxPayload int `json:"max_payload"` } @@ -75,6 +77,11 @@ type stats struct { // New will setup a new server struct after parsing the options. func New(opts *Options) *Server { processOptions(opts) + + // Process TLS options, including whether we require client certificates. + tlsReq := opts.TLSConfig != nil + verify := (tlsReq == true && opts.TLSConfig.ClientAuth == tls.RequireAnyClientCert) + info := Info{ ID: genID(), Version: VERSION, @@ -82,7 +89,9 @@ func New(opts *Options) *Server { Host: opts.Host, Port: opts.Port, AuthRequired: false, - TLSRequired: opts.TLSConfig != nil, + TLSRequired: tlsReq, + SSLRequired: tlsReq, + TLSVerify: verify, MaxPayload: opts.MaxPayload, } diff --git a/test/configs/tls.conf b/test/configs/tls.conf index 029cc141..ac6a9aa2 100644 --- a/test/configs/tls.conf +++ b/test/configs/tls.conf @@ -5,9 +5,10 @@ port: 4443 net: localhost tls { -# ca_file: "./configs/certs/ca.pem" - cert_file: "./configs/certs/server-cert.pem" - key_file: "./configs/certs/server-key.pem" + # Server cert + cert_file: "./configs/certs/server-cert.pem" + # Server private key + key_file: "./configs/certs/server-key.pem" } authorization { diff --git a/test/configs/tlsverify.conf b/test/configs/tlsverify.conf new file mode 100644 index 00000000..531b477e --- /dev/null +++ b/test/configs/tlsverify.conf @@ -0,0 +1,16 @@ + +# Simple TLS config file + +port: 4443 +net: localhost + +tls { + # Server cert + cert_file: "./configs/certs/server-cert.pem" + # Server private key + key_file: "./configs/certs/server-key.pem" + # Optional certificate authority for clients + ca_file: "./configs/certs/ca.pem" + # Require a client certificate + verify: true +} diff --git a/test/tls_test.go b/test/tls_test.go index 546567cb..415e193a 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -74,3 +74,58 @@ func TestTLSConnection(t *testing.T) { nc.Flush() defer nc.Close() } + +func TestTLSClientCertificate(t *testing.T) { + srv, opts := RunServerWithConfig("./configs/tlsverify.conf") + defer srv.Shutdown() + + nurl := fmt.Sprintf("nats://%s:%d", opts.Host, opts.Port) + + _, err := nats.Connect(nurl) + if err == nil { + t.Fatalf("Expected error trying to connect to secure server without a certificate") + } + + _, err = nats.SecureConnect(nurl) + if err == nil { + t.Fatalf("Expected error trying to secure connect to secure server without a certificate") + } + + // Load client certificate to sucessfully connect. + certFile := "./configs/certs/client-cert.pem" + keyFile := "./configs/certs/client-key.pem" + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + t.Fatalf("error parsing X509 certificate/key pair: %v", err) + } + + // Load in root CA for server verification + rootPEM, err := ioutil.ReadFile("./configs/certs/ca.pem") + if err != nil || rootPEM == nil { + t.Fatalf("failed to read root certificate") + } + pool := x509.NewCertPool() + ok := pool.AppendCertsFromPEM([]byte(rootPEM)) + if !ok { + t.Fatalf("failed to parse root certificate") + } + + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: opts.Host, + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + + copts := nats.DefaultOptions + copts.Url = nurl + copts.Secure = true + copts.TLSConfig = config + + nc, err := copts.Connect() + if err != nil { + t.Fatalf("Got an error on Connect with Secure Options: %+v\n", err) + } + nc.Flush() + defer nc.Close() +}