From 7978d8e6676882296e8539ad7665e623f3739d5f Mon Sep 17 00:00:00 2001 From: Derek Collison Date: Thu, 20 Dec 2018 07:42:49 -0800 Subject: [PATCH] Support for mapping user from TLS client certificate Signed-off-by: Derek Collison --- server/auth.go | 48 +++++++++++++++++++--- server/config_check_test.go | 27 ------------ server/opts.go | 25 +++++++---- test/configs/certs/client-id-auth-cert.pem | 29 +++++++++++++ test/configs/certs/client-id-auth-key.pem | 28 +++++++++++++ test/configs/tls_cert_id.conf | 24 +++++++++++ test/tls_test.go | 13 ++++++ 7 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 test/configs/certs/client-id-auth-cert.pem create mode 100644 test/configs/certs/client-id-auth-key.pem create mode 100644 test/configs/tls_cert_id.conf diff --git a/server/auth.go b/server/auth.go index 787b415a4a..6f28e58dcd 100644 --- a/server/auth.go +++ b/server/auth.go @@ -261,6 +261,7 @@ func (s *Server) isClientAuthorized(c *client) bool { authorization := s.opts.Authorization username := s.opts.Username password := s.opts.Password + tlsMap := s.opts.TLSMap s.optsMu.RUnlock() // Check custom auth first, then jwts, then nkeys, then multiple users, then token, then single user/pass. @@ -318,11 +319,48 @@ func (s *Server) isClientAuthorized(c *client) bool { s.mu.Unlock() return false } - } else if hasUsers && c.opts.Username != "" { - user, ok = s.users[c.opts.Username] - if !ok { - s.mu.Unlock() - return false + } else if hasUsers { + // Check if we are tls verify and are mapping users from the client_certificate + if tlsMap { + tlsState := c.GetTLSConnectionState() + if tlsState == nil { + c.Debugf("User required in cert, no TLS connection state") + s.mu.Unlock() + return false + } + if len(tlsState.PeerCertificates) == 0 { + c.Debugf("User required in cert, no peer certificates found") + s.mu.Unlock() + return false + } + cert := tlsState.PeerCertificates[0] + if len(tlsState.PeerCertificates) > 1 { + c.Debugf("Multiple peer certificates found, selecting first") + } + if len(cert.EmailAddresses) == 0 { + c.Debugf("User required in cert, none found") + s.mu.Unlock() + return false + } + euser := cert.EmailAddresses[0] + user, ok = s.users[euser] + if !ok { + c.Debugf("User in cert [%q], not found", euser) + s.mu.Unlock() + return false + } + if len(cert.EmailAddresses) > 1 { + c.Debugf("Multiple users found in cert, selecting first [%q]", euser) + } + if c.opts.Username != "" { + s.Warnf("User found in connect proto, but user required from cert - %v", c) + } + } else if c.opts.Username != "" { + user, ok = s.users[c.opts.Username] + if !ok { + s.mu.Unlock() + return false + } } } s.mu.Unlock() diff --git a/server/config_check_test.go b/server/config_check_test.go index 1fbf1cb644..3b36cc397b 100644 --- a/server/config_check_test.go +++ b/server/config_check_test.go @@ -612,33 +612,6 @@ func TestConfigCheck(t *testing.T) { errorLine: 14, errorPos: 11, }, - { - name: "when account user within accounts block has no user, pass", - config: ` - accounts { - - # - # synadia > nats.io, cncf - # - synadia { - # SAADJL5XAEM6BDYSWDTGVILJVY54CQXZM5ZLG4FRUAKB62HWRTPNSGXOHA - nkey = "AC5GRL36RQV7MJ2GT6WQSCKDKJKYTK4T2LGLWJ2SEJKRDHFOQQWGGFQL" - - users [ - { - } - ] - - exports = [ - { service: "synadia.requests", accounts: [nats, cncf] } - ] - } - } - `, - err: errors.New(`User entry requires a user and a password`), - errorLine: 13, - errorPos: 10, - }, { name: "when accounts block has unknown fields", config: ` diff --git a/server/opts.go b/server/opts.go index 66bdc6dbfd..6007a91831 100644 --- a/server/opts.go +++ b/server/opts.go @@ -123,6 +123,7 @@ type Options struct { TLSTimeout float64 `json:"tls_timeout"` TLS bool `json:"-"` TLSVerify bool `json:"-"` + TLSMap bool `json:"-"` TLSCert string `json:"-"` TLSKey string `json:"-"` TLSCaCert string `json:"-"` @@ -227,6 +228,7 @@ type TLSConfigOpts struct { KeyFile string CaFile string Verify bool + Map bool Timeout float64 Ciphers []uint16 CurvePreferences []tls.CurveID @@ -238,10 +240,11 @@ TLS configuration is specified in the tls section of a configuration file: e.g. tls { - cert_file: "./certs/server-cert.pem" - key_file: "./certs/server-key.pem" - ca_file: "./certs/ca.pem" - verify: true + cert_file: "./certs/server-cert.pem" + key_file: "./certs/server-key.pem" + ca_file: "./certs/ca.pem" + verify: true + verify_and_map: true cipher_suites: [ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", @@ -457,6 +460,7 @@ func (o *Options) ProcessConfigFile(configFile string) error { continue } o.TLSTimeout = tc.Timeout + o.TLSMap = tc.Map case "write_deadline": wd, ok := v.(string) if ok { @@ -1610,9 +1614,9 @@ func parseUsers(mv interface{}, opts *Options, errors *[]error, warnings *[]erro } } - // Check to make sure we have at least username and password if defined. - if nkey.Nkey == "" && (user.Username == "" || user.Password == "") { - return nil, nil, &configErr{tk, fmt.Sprintf("User entry requires a user and a password")} + // Check to make sure we have at least an nkey or username defined. + if nkey.Nkey == "" && user.Username == "" { + return nil, nil, &configErr{tk, fmt.Sprintf("User entry requires a user")} } else if nkey.Nkey != "" { // Make sure the nkey a proper public nkey for a user.. if !nkeys.IsValidPublicUserKey(nkey.Nkey) { @@ -1871,6 +1875,13 @@ func parseTLS(v interface{}) (*TLSConfigOpts, error) { return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, expected 'verify' to be a boolean")} } tc.Verify = verify + case "verify_and_map": + verify, ok := mv.(bool) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, expected 'verify_and_map' to be a boolean")} + } + tc.Verify = verify + tc.Map = verify case "cipher_suites": ra := mv.([]interface{}) if len(ra) == 0 { diff --git a/test/configs/certs/client-id-auth-cert.pem b/test/configs/certs/client-id-auth-cert.pem new file mode 100644 index 0000000000..3809ec40e3 --- /dev/null +++ b/test/configs/certs/client-id-auth-cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE7DCCAtSgAwIBAgIJAO+k4G7bNTy5MA0GCSqGSIb3DQEBBQUAMIGLMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzAR +BgNVBAoTCkFwY2VyYSBJbmMxEDAOBgNVBAsTB25hdHMuaW8xEjAQBgNVBAMTCWxv +Y2FsaG9zdDEcMBoGCSqGSIb3DQEJARYNZGVyZWtAbmF0cy5pbzAeFw0xODEyMjAw +MDQwMjZaFw0xOTAxMTkwMDQwMjZaMIGaMQswCQYDVQQGEwJVUzELMAkGA1UECAwC +Q0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMSQwIgYDVQQKDBtTeW5hZGlhIENvbW11 +bmljYXRpb25zIEluYy4xEDAOBgNVBAsMB05BVFMuaW8xEjAQBgNVBAMMCWxvY2Fs +aG9zdDEcMBoGCSqGSIb3DQEJARYNZGVyZWtAbmF0cy5pbzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAJzsochRXgyF2qQsVNfL/H84pvhDYTqMarYSijUe +CHUxVi4nNxQsr9Z0SrtzjaeiJ9uOH1vDpzTevsI1Akv6tUiZxgFhxUv79cbO1WNF +fzwXANzDHwOxyEnSSl3X5HN+L67cgc+chof7tc+c1KqVYXPpJy0hSvCY6o1W0IbE +4KeR7m6kNh6jPvQAPz00jZeUVIenE3zuMrLeLnTqdhWrZwjme8FybH1sxMQu1T3v +SI0mhl7Xmkct0UTH11dJ4azrgmddxI0ZBIXAlQA3PTY4QyKiqR4V95wkEwn5hq3a +XCB5YT9XHYGI/An8Dkotqn/7fjQ2h7BqW6qevQOu8ce52B0CAwEAAaNCMEAwKQYD +VR0RBCIwIIIJbG9jYWxob3N0hwR/AAABgQ1kZXJla0BuYXRzLmlvMBMGA1UdJQQM +MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4ICAQBbBvhUFoVOgP+1qTUMIYCQ +hjtLOogW/tOXO6FIxFr3WEtf5WOc+Ko/FiwfsLyS0HTXvz7C7S4KRtgXLBVRjSy+ +FJdUKiCVeuZ3ZjUl2b7rLj/36N+cClz0Ipy7FKyN0ww1sscWDL1F1E65tv+LnnQ1 +p0dDIsHEbz968t0hVoUzPs1DJNKdU1K482mTPjjTbwEvt59keITRy2rV+vKxt1n2 +szfovqllHDGrvlZnT7kGYTvWHa3NkL5AdLmVb51PFcN+LpWykyQsZN1Zr5x5GbHj +UQVhrnJUJOWtI5HmXfhlwUZZtUezgAvpTEkjqT+yrL1PbN61uvBXHI8piZG3Vnu1 +QS/KT2aptmb/W5KH2EkrldvVKYOSyo0lDxBu8ZGYQCJnaI+vtpCIVZSv9LkMJNDf +NQSCjwoVfqSlaOg/b+HMaDmOaGtpYyHEJ45WgtZxU+GYdJX9XLgef/1WyG9wP9fg +/Uki2D5QqRhWZ1SA3C8zItcfLhfVbOj3RX69FRO8RcxEjLOJzQztbOdpesbEMFmV +rBYnOP4N4lBtou0f8m5TZu8tbTk1UypdS/TqZNPk/XC/xU/oj+XRcOxg7fK5MJLw +xKF/EjGP5K81KnE140uvaeoVkD5MbxFygUuvArXzuiLEf5Kydf9zT/AymNc6ySEi +kt4g8+vpo1BX4/lTyI6dKQ== +-----END CERTIFICATE----- diff --git a/test/configs/certs/client-id-auth-key.pem b/test/configs/certs/client-id-auth-key.pem new file mode 100644 index 0000000000..dbafb8606b --- /dev/null +++ b/test/configs/certs/client-id-auth-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCc7KHIUV4Mhdqk +LFTXy/x/OKb4Q2E6jGq2Eoo1Hgh1MVYuJzcULK/WdEq7c42noifbjh9bw6c03r7C +NQJL+rVImcYBYcVL+/XGztVjRX88FwDcwx8DschJ0kpd1+Rzfi+u3IHPnIaH+7XP +nNSqlWFz6SctIUrwmOqNVtCGxOCnke5upDYeoz70AD89NI2XlFSHpxN87jKy3i50 +6nYVq2cI5nvBcmx9bMTELtU970iNJoZe15pHLdFEx9dXSeGs64JnXcSNGQSFwJUA +Nz02OEMioqkeFfecJBMJ+Yat2lwgeWE/Vx2BiPwJ/A5KLap/+340Noewaluqnr0D +rvHHudgdAgMBAAECggEAU7Rtf7rXXfqwa84x24I1x27isZ7PaDmqWkB9dGP2wvx0 +Kd5FJP/JM62Ih4DV2MtIU3b2By7QLAV8338DIKA0vus3kVkjsqpNqaR+cpuJiPYE +Qb3a90+HtMj1XiVg+LIgoTeCDspBgJulmX7gebWA7CE8Ani1zqziwE3EoX63DuqU +uViC4bXKNHV3f7+rtImyHR6i7LVwS6waaOXP9I79vBncNnUGe0+JDtnFDu8wNO0k +domaHrf2lRgSHgsIppeNZ5zOEJ3YB2BORxbI2+kDZtYB8cavH5YLJw/mdha1okuI +J4JLd3F7WQz1hX6/noMZxRquWKL7NlTcz8mCyL31yQKBgQDP/NlMPVd+s9Vxrf44 +6KBV2xYRPY3O2eBgVE6EbKbYFhg3cBKb96MB98AGQ41gYmbgvI7dv2jT3PT7EJM2 +xJpP4WuJYaQ8fCL1onyk4WgGbOLdGnVHPmFi2Swjt9UJay64yDknVPpqSnBvGLmw +tl3XsfWrTpInjtXeDVwk9h0JUwKBgQDBJipgIaVqUeQ1dwXbJe2atUpAv1oVt4ll +omqUiY0LLwidZLZ4CvNlsK/eCzWFTf30yLcTYTDg5Xuav5+rBwOWYwN8lWv1rkAn +Qew3cSPXpxP97gjgAhirnOmVecTaTzqPBG82F30idoC4jpX9lhmdBD/PdGQ4ySLK +N++ReIO6zwKBgHavnPiKkKE20fhbB5VF+ijEKqWP8Jo3bnjJ4zxiHBt3ED6ib5wd +BiIbVLK+XbDAtmBMeWJE1fcAQbP7U2aPbldjFVCCLYxuciylmmckUY5JGHR/oqkT +CdO0hiGjx6fmR/UeHK87KOL6s4pSG7ShfI+Xd89XuMNmGNjr2sckwpENAoGBAJy5 +lUTvyENfM6fWbmAGhKg2Vov3OOfKR6i6g3UHr/TVM05TfGQnrpxjJDEuMz15rYnE +nBkTkg/K5eMJfkvOozCSIzAiJrnxrIiuSzgpjAXewrAXSAhMayxFZJwvdHYYN9H4 +rSzdHmKqeYRH3pkoBJyN6CEztmcFfj9L6A7IFUutAoGAJwR9nQAbRnxVKYZgXef+ +QD7/1QIhB97CWag4Gc2VyPdEoqM6z2FzOFuSjMUYRbR4W/ksNdns5Mb1MIQYLmqv +dzAjfZeaGril0QQeaYcWct41MKjKzyroOSRKNOs1Ae/Dd0KsUbWjjMoQHmBffcjS +40p5iL1eAK5f4haKBVo+uKw= +-----END PRIVATE KEY----- diff --git a/test/configs/tls_cert_id.conf b/test/configs/tls_cert_id.conf new file mode 100644 index 0000000000..62beeb6b10 --- /dev/null +++ b/test/configs/tls_cert_id.conf @@ -0,0 +1,24 @@ +# TLS config file +# We require client certs and pull the user from the cert itself. + +listen: 127.0.0.1:9333 + +tls { + # Server cert + cert_file: "./configs/certs/server-cert.pem" + # Server private key + key_file: "./configs/certs/server-key.pem" + # Specified time for handshake to complete + timeout: 2 + # Optional certificate authority for clients + ca_file: "./configs/certs/ca.pem" + # Require a client certificate and map user id from certificate + verify_and_map: true +} + +# User authenticated from above in certificate. +authorization { + users = [ + {user: derek@nats.io, permissions: { publish:"foo" }} + ] +} diff --git a/test/tls_test.go b/test/tls_test.go index 5a61d0fce4..06af1ed1ff 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -117,6 +117,19 @@ func TestTLSClientCertificate(t *testing.T) { defer nc.Close() } +func TestTLSClientCertificateHasUserID(t *testing.T) { + srv, opts := RunServerWithConfig("./configs/tls_cert_id.conf") + defer srv.Shutdown() + nurl := fmt.Sprintf("tls://%s:%d", opts.Host, opts.Port) + nc, err := nats.Connect(nurl, + nats.ClientCert("./configs/certs/client-id-auth-cert.pem", "./configs/certs/client-id-auth-key.pem"), + nats.RootCAs("./configs/certs/ca.pem")) + if err != nil { + t.Fatalf("Expected to connect, got %v", err) + } + defer nc.Close() +} + func TestTLSVerifyClientCertificate(t *testing.T) { srv, opts := RunServerWithConfig("./configs/tlsverify_noca.conf") defer srv.Shutdown()