diff --git a/internal/ldap/dn.go b/internal/ldap/dn.go new file mode 100644 index 00000000..2bd46d8c --- /dev/null +++ b/internal/ldap/dn.go @@ -0,0 +1,234 @@ +// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +// Portions copyright (c) 2015-2016 go-ldap Authors +package ldap + +import ( + "bytes" + "crypto/x509/pkix" + enchex "encoding/hex" + "errors" + "fmt" + "strings" +) + +var attributeTypeNames = map[string]string{ + "2.5.4.6": "C", + "2.5.4.10": "O", + "2.5.4.11": "OU", + "2.5.4.3": "CN", + "2.5.4.5": "SERIALNUMBER", + "2.5.4.7": "L", + "2.5.4.8": "ST", + "2.5.4.9": "STREET", + "2.5.4.17": "POSTALCODE", + // FIXME: Add others. + "0.9.2342.19200300.100.1.25": "DC", +} + +// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 +type AttributeTypeAndValue struct { + // Type is the attribute type + Type string + // Value is the attribute value + Value string +} + +// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 +type RelativeDN struct { + Attributes []*AttributeTypeAndValue +} + +// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 +type DN struct { + RDNs []*RelativeDN +} + +// FromCertSubject takes a pkix.Name from a cert and returns a DN +// that uses the same set. +func FromCertSubject(subject pkix.Name) (*DN, error) { + dn := &DN{ + RDNs: make([]*RelativeDN, 0), + } + for i := len(subject.Names) - 1; i >= 0; i-- { + name := subject.Names[i] + oidString := name.Type.String() + typeName, ok := attributeTypeNames[oidString] + if !ok { + return nil, fmt.Errorf("invalid type name: %+v", name) + } + v, ok := name.Value.(string) + if !ok { + return nil, fmt.Errorf("invalid type value: %+v", v) + } + rdn := &RelativeDN{ + Attributes: []*AttributeTypeAndValue{ + { + Type: typeName, + Value: v, + }, + }, + } + dn.RDNs = append(dn.RDNs, rdn) + } + return dn, nil +} + +// ParseDN returns a distinguishedName or an error. +// The function respects https://tools.ietf.org/html/rfc4514 +func ParseDN(str string) (*DN, error) { + dn := new(DN) + dn.RDNs = make([]*RelativeDN, 0) + rdn := new(RelativeDN) + rdn.Attributes = make([]*AttributeTypeAndValue, 0) + buffer := bytes.Buffer{} + attribute := new(AttributeTypeAndValue) + escaping := false + + unescapedTrailingSpaces := 0 + stringFromBuffer := func() string { + s := buffer.String() + s = s[0 : len(s)-unescapedTrailingSpaces] + buffer.Reset() + unescapedTrailingSpaces = 0 + return s + } + + for i := 0; i < len(str); i++ { + char := str[i] + switch { + case escaping: + unescapedTrailingSpaces = 0 + escaping = false + switch char { + case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': + buffer.WriteByte(char) + continue + } + // Not a special character, assume hex encoded octet + if len(str) == i+1 { + return nil, errors.New("got corrupted escaped character") + } + + dst := []byte{0} + n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2])) + if err != nil { + return nil, fmt.Errorf("failed to decode escaped character: %s", err) + } else if n != 1 { + return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n) + } + buffer.WriteByte(dst[0]) + i++ + case char == '\\': + unescapedTrailingSpaces = 0 + escaping = true + case char == '=': + attribute.Type = stringFromBuffer() + // Special case: If the first character in the value is # the following data + // is BER encoded. Throw an error since not supported right now. + if len(str) > i+1 && str[i+1] == '#' { + return nil, errors.New("unsupported BER encoding") + } + case char == ',' || char == '+': + // We're done with this RDN or value, push it + if len(attribute.Type) == 0 { + return nil, errors.New("incomplete type, value pair") + } + attribute.Value = stringFromBuffer() + rdn.Attributes = append(rdn.Attributes, attribute) + attribute = new(AttributeTypeAndValue) + if char == ',' { + dn.RDNs = append(dn.RDNs, rdn) + rdn = new(RelativeDN) + rdn.Attributes = make([]*AttributeTypeAndValue, 0) + } + case char == ' ' && buffer.Len() == 0: + // ignore unescaped leading spaces + continue + default: + if char == ' ' { + // Track unescaped spaces in case they are trailing and we need to remove them + unescapedTrailingSpaces++ + } else { + // Reset if we see a non-space char + unescapedTrailingSpaces = 0 + } + buffer.WriteByte(char) + } + } + if buffer.Len() > 0 { + if len(attribute.Type) == 0 { + return nil, errors.New("DN ended with incomplete type, value pair") + } + attribute.Value = stringFromBuffer() + rdn.Attributes = append(rdn.Attributes, attribute) + dn.RDNs = append(dn.RDNs, rdn) + } + return dn, nil +} + +// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). +// Returns true if they have the same number of relative distinguished names +// and corresponding relative distinguished names (by position) are the same. +func (d *DN) Equal(other *DN) bool { + if len(d.RDNs) != len(other.RDNs) { + return false + } + for i := range d.RDNs { + if !d.RDNs[i].Equal(other.RDNs[i]) { + return false + } + } + return true +} + +// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. +// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com" +// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com" +// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com" +func (d *DN) AncestorOf(other *DN) bool { + if len(d.RDNs) >= len(other.RDNs) { + return false + } + // Take the last `len(d.RDNs)` RDNs from the other DN to compare against + otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] + for i := range d.RDNs { + if !d.RDNs[i].Equal(otherRDNs[i]) { + return false + } + } + return true +} + +// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). +// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues +// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. +// The order of attributes is not significant. +// Case of attribute types is not significant. +func (r *RelativeDN) Equal(other *RelativeDN) bool { + if len(r.Attributes) != len(other.Attributes) { + return false + } + return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes) +} + +func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool { + for _, attr := range attrs { + found := false + for _, myattr := range r.Attributes { + if myattr.Equal(attr) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue +// Case of the attribute type is not significant +func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { + return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value +} diff --git a/internal/ldap/dn_test.go b/internal/ldap/dn_test.go new file mode 100644 index 00000000..c9db86c0 --- /dev/null +++ b/internal/ldap/dn_test.go @@ -0,0 +1,212 @@ +// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +// Portions copyright (c) 2015-2016 go-ldap Authors +package ldap + +import ( + "reflect" + "testing" +) + +func TestSuccessfulDNParsing(t *testing.T) { + testcases := map[string]DN{ + "": {[]*RelativeDN{}}, + "cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{ + {[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}}, + {[]*AttributeTypeAndValue{{"dc", "dummy"}}}, + {[]*AttributeTypeAndValue{{"dc", "com"}}}}}, + "UID=jsmith,DC=example,DC=net": {[]*RelativeDN{ + {[]*AttributeTypeAndValue{{"UID", "jsmith"}}}, + {[]*AttributeTypeAndValue{{"DC", "example"}}}, + {[]*AttributeTypeAndValue{{"DC", "net"}}}}}, + "OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{ + {[]*AttributeTypeAndValue{ + {"OU", "Sales"}, + {"CN", "J. Smith"}}}, + {[]*AttributeTypeAndValue{{"DC", "example"}}}, + {[]*AttributeTypeAndValue{{"DC", "net"}}}}}, + // + // "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{ + // {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}}}, + // "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{ + // {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}, + // {[]*AttributeTypeAndValue{{"DC", "net"}}}}}, + "CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{ + {[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}}, + " CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{ + {[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}}, + ` A = 1 , B = 2 `: {[]*RelativeDN{ + {[]*AttributeTypeAndValue{{"A", "1"}}}, + {[]*AttributeTypeAndValue{{"B", "2"}}}}}, + ` A = 1 + B = 2 `: {[]*RelativeDN{ + {[]*AttributeTypeAndValue{ + {"A", "1"}, + {"B", "2"}}}}}, + ` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{ + {[]*AttributeTypeAndValue{{" A ", " 1 "}}}, + {[]*AttributeTypeAndValue{{" B ", " 2 "}}}}}, + ` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{ + {[]*AttributeTypeAndValue{ + {" A ", " 1 "}, + {" B ", " 2 "}}}}}, + } + + for test, answer := range testcases { + dn, err := ParseDN(test) + if err != nil { + t.Errorf(err.Error()) + continue + } + if !reflect.DeepEqual(dn, &answer) { + t.Errorf("Parsed DN %s is not equal to the expected structure", test) + t.Logf("Expected:") + for _, rdn := range answer.RDNs { + for _, attribs := range rdn.Attributes { + t.Logf("#%v\n", attribs) + } + } + t.Logf("Actual:") + for _, rdn := range dn.RDNs { + for _, attribs := range rdn.Attributes { + t.Logf("#%v\n", attribs) + } + } + } + } +} + +func TestErrorDNParsing(t *testing.T) { + testcases := map[string]string{ + "*": "DN ended with incomplete type, value pair", + "cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'", + "cn=Jim\\0": "got corrupted escaped character", + "DC=example,=net": "DN ended with incomplete type, value pair", + // "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string", + "test,DC=example,DC=com": "incomplete type, value pair", + "=test,DC=example,DC=com": "incomplete type, value pair", + } + + for test, answer := range testcases { + _, err := ParseDN(test) + if err == nil { + t.Errorf("Expected %s to fail parsing but succeeded\n", test) + } else if err.Error() != answer { + t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error()) + } + } +} + +func TestDNEqual(t *testing.T) { + testcases := []struct { + A string + B string + Equal bool + }{ + // Exact match + {"", "", true}, + {"o=A", "o=A", true}, + {"o=A", "o=B", false}, + + {"o=A,o=B", "o=A,o=B", true}, + {"o=A,o=B", "o=A,o=C", false}, + + {"o=A+o=B", "o=A+o=B", true}, + {"o=A+o=B", "o=A+o=C", false}, + + // Case mismatch in type is ignored + {"o=A", "O=A", true}, + {"o=A,o=B", "o=A,O=B", true}, + {"o=A+o=B", "o=A+O=B", true}, + + // Case mismatch in value is significant + {"o=a", "O=A", false}, + {"o=a,o=B", "o=A,O=B", false}, + {"o=a+o=B", "o=A+O=B", false}, + + // Multi-valued RDN order mismatch is ignored + {"o=A+o=B", "O=B+o=A", true}, + // Number of RDN attributes is significant + {"o=A+o=B", "O=B+o=A+O=B", false}, + + // Missing values are significant + {"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter + {"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter + + // Whitespace tests + // Matching + { + "cn=John Doe, ou=People, dc=sun.com", + "cn=John Doe, ou=People, dc=sun.com", + true, + }, + // Difference in leading/trailing chars is ignored + { + "cn=John Doe, ou=People, dc=sun.com", + "cn=John Doe,ou=People,dc=sun.com", + true, + }, + // Difference in values is significant + { + "cn=John Doe, ou=People, dc=sun.com", + "cn=John Doe, ou=People, dc=sun.com", + false, + }, + } + + for i, tc := range testcases { + a, err := ParseDN(tc.A) + if err != nil { + t.Errorf("%d: %v", i, err) + continue + } + b, err := ParseDN(tc.B) + if err != nil { + t.Errorf("%d: %v", i, err) + continue + } + if expected, actual := tc.Equal, a.Equal(b); expected != actual { + t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + continue + } + if expected, actual := tc.Equal, b.Equal(a); expected != actual { + t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + continue + } + } +} + +func TestDNAncestor(t *testing.T) { + testcases := []struct { + A string + B string + Ancestor bool + }{ + // Exact match returns false + {"", "", false}, + {"o=A", "o=A", false}, + {"o=A,o=B", "o=A,o=B", false}, + {"o=A+o=B", "o=A+o=B", false}, + + // Mismatch + {"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false}, + + // Descendant + {"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true}, + } + + for i, tc := range testcases { + a, err := ParseDN(tc.A) + if err != nil { + t.Errorf("%d: %v", i, err) + continue + } + b, err := ParseDN(tc.B) + if err != nil { + t.Errorf("%d: %v", i, err) + continue + } + if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual { + t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual) + continue + } + } +} diff --git a/server/auth.go b/server/auth.go index a5857250..519b6c35 100644 --- a/server/auth.go +++ b/server/auth.go @@ -24,6 +24,7 @@ import ( "time" "github.com/nats-io/jwt" + "github.com/nats-io/nats-server/v2/internal/ldap" "github.com/nats-io/nkeys" "golang.org/x/crypto/bcrypt" ) @@ -323,6 +324,8 @@ func (s *Server) processClientOrLeafAuthentication(c *client) bool { ) s.mu.Lock() + users := s.users + tlsMap := opts.TLSMap authRequired := s.info.AuthRequired if !authRequired { // TODO(dlc) - If they send us credentials should we fail? @@ -363,18 +366,39 @@ func (s *Server) processClientOrLeafAuthentication(c *client) bool { return false } } else if hasUsers { - // Check if we are tls verify and are mapping users from the client_certificate - if opts.TLSMap { - var euser string - authorized := checkClientTLSCertSubject(c, func(u string) bool { - var ok bool - user, ok = s.users[u] - if !ok { - c.Debugf("User in cert [%q], not found", u) - return false + // Check if we are tls verify and are mapping users from the client_certificate. + if tlsMap { + authorized := checkClientTLSCertSubject(c, func(u string, certRDN *ldap.DN) (string, bool) { + // First do literal lookup using the resulting string representation + // of RDNSequence as implemented by the pkix package from Go. + if u != "" { + usr, ok := users[u] + if !ok { + return "", ok + } + user = usr + return usr.Username, ok } - euser = u - return true + + if certRDN == nil { + return "", false + } + + // Look through the accounts for an RDN that is equal to the one + // presented by the certificate. + for _, usr := range users { + // TODO: Use this utility to make a full validation pass + // on start in case tlsmap feature is being used. + inputRDN, err := ldap.ParseDN(usr.Username) + if err != nil { + continue + } + if inputRDN.Equal(certRDN) { + user = usr + return usr.Username, true + } + } + return "", false }) if !authorized { s.mu.Unlock() @@ -385,7 +409,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client) bool { } // Already checked that the client didn't send a user in connect // but we set it here to be able to identify it in the logs. - c.opts.Username = euser + c.opts.Username = user.Username } else { if c.kind == CLIENT && c.opts.Username == "" && s.opts.NoAuthUser != "" { if u, exists := s.users[s.opts.NoAuthUser]; exists { @@ -496,7 +520,6 @@ func (s *Server) processClientOrLeafAuthentication(c *client) bool { } return true } - if user != nil { ok = comparePasswords(user.Password, c.opts.Password) // If we are authorized, register the user which will properly setup any permissions @@ -525,7 +548,6 @@ func (s *Server) processClientOrLeafAuthentication(c *client) bool { // or the one specified in config (if provided). return s.registerLeafWithAccount(c, opts.LeafNode.Account) } - return false } @@ -549,7 +571,9 @@ func getTLSAuthDCs(rdns *pkix.RDNSequence) string { return strings.Join(dcs, ",") } -func checkClientTLSCertSubject(c *client, fn func(string) bool) bool { +type tlsMapAuthFn func(string, *ldap.DN) (string, bool) + +func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool { tlsState := c.GetTLSConnectionState() if tlsState == nil { c.Debugf("User required in cert, no TLS connection state") @@ -575,41 +599,64 @@ func checkClientTLSCertSubject(c *client, fn func(string) bool) bool { switch { case hasEmailAddresses: for _, u := range cert.EmailAddresses { - if fn(u) { - c.Debugf("Using email found in cert for auth [%q]", u) + if match, ok := fn(u, nil); ok { + c.Debugf("Using email found in cert for auth [%q]", match) return true } } fallthrough case hasSANs: for _, u := range cert.DNSNames { - if fn(u) { - c.Debugf("Using SAN found in cert for auth [%q]", u) + if match, ok := fn(u, nil); ok { + c.Debugf("Using SAN found in cert for auth [%q]", match) return true } } } - // Try to get the full RDN Sequence that includes the domain components. + // Use the string representation of the full RDN Sequence including + // the domain components in case there are any. + rdn := cert.Subject.ToRDNSequence().String() + + // Match that follows original order from the subject takes precedence. + dn, err := ldap.FromCertSubject(cert.Subject) + if err == nil { + if match, ok := fn("", dn); ok { + c.Debugf("Using DistinguishedNameMatch for auth [%q]", match) + return true + } + c.Debugf("DistinguishedNameMatch could not be used for auth [%q]", rdn) + } + var rdns pkix.RDNSequence if _, err := asn1.Unmarshal(cert.RawSubject, &rdns); err == nil { // If found domain components then include roughly following // the order from https://tools.ietf.org/html/rfc2253 - rdn := cert.Subject.ToRDNSequence().String() + // + // NOTE: The original sequence from string representation by ToRDNSequence does not follow + // the correct ordering, so this addition ofdomainComponents would likely be deprecated in + // another release in favor of using the correct ordered as parsed by the go-ldap library. + // dcs := getTLSAuthDCs(&rdns) if len(dcs) > 0 { u := strings.Join([]string{rdn, dcs}, ",") - if fn(u) { - c.Debugf("Using RDNSequence for auth [%q]", u) + if match, ok := fn(u, nil); ok { + c.Debugf("Using RDNSequence for auth [%q]", match) return true } + c.Debugf("RDNSequence could not be used for auth [%q]", u) } } - // Use the subject of the certificate. - u := cert.Subject.String() - c.Debugf("Using certificate subject for auth [%q]", u) - return fn(u) + // If no match, then use the string representation of the RDNSequence + // from the subject without the domainComponents. + if match, ok := fn(rdn, nil); ok { + c.Debugf("Using certificate subject for auth [%q]", match) + return true + } + + c.Debugf("User in cert [%q], not found", rdn) + return false } // checkRouterAuth checks optional router authorization which can be nil or username/password. @@ -628,8 +675,8 @@ func (s *Server) isRouterAuthorized(c *client) bool { } if opts.Cluster.TLSMap { - return checkClientTLSCertSubject(c, func(user string) bool { - return opts.Cluster.Username == user + return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN) (string, bool) { + return "", opts.Cluster.Username == user }) } @@ -652,8 +699,8 @@ func (s *Server) isGatewayAuthorized(c *client) bool { // Check whether TLS map is enabled, otherwise use single user/pass. if opts.Gateway.TLSMap { - return checkClientTLSCertSubject(c, func(user string) bool { - return opts.Gateway.Username == user + return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN) (string, bool) { + return "", opts.Gateway.Username == user }) } @@ -700,6 +747,30 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool { if opts.LeafNode.Username != _EMPTY_ { return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account) } else if len(opts.LeafNode.Users) > 0 { + if opts.LeafNode.TLSMap { + var user *User + found := checkClientTLSCertSubject(c, func(u string, _ *ldap.DN) (string, bool) { + // This is expected to be a very small array. + for _, usr := range opts.LeafNode.Users { + if u == usr.Username { + user = usr + return u, true + } + } + return "", false + }) + if !found { + return false + } + if c.opts.Username != "" { + s.Warnf("User %q found in connect proto, but user required from cert", c.opts.Username) + } + c.opts.Username = user.Username + // This will authorize since are using an existing user, + // but it will also register with proper account. + return isAuthorized(user.Username, user.Password, user.Account.Name) + } + // This is expected to be a very small array. for _, u := range opts.LeafNode.Users { if u.Username == c.opts.Username { diff --git a/test/configs/certs/rdns/client-d.key b/test/configs/certs/rdns/client-d.key new file mode 100644 index 00000000..0872b6f6 --- /dev/null +++ b/test/configs/certs/rdns/client-d.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDay//0l8jlnU/v +DtoyNKmxIC+goq/ooiBnOCkuRyGus1yA70V4MWYEzi/N76sVw3MHXMpWxcYvxNO7 ++8szsgXVQF6G1pxnm2e3/Xp+lZWVHkjR/6h6dQpAwgyIkXCsIN/4+TYIn1T3/hdx +x4rYgeK1YO47lUSLskGNcU76nmqhooboWmWW8NYFJszvS0VUMXvMpaOmrFaIwx4e +9nDt8n2H7lyAq7cqo7sHv9ytT6xSPV2ok8swrbD9Ez/lyT+F/WbHkmP1HCK5365d +FmH3STH1E2l2lasTC4p92RK7NiWzzyIgOelcT6TblZxxJSyH8KfjaHcxFcJhkHjc +zY3h/K0fAgMBAAECggEBAIcSx6othlXSn0VbKvMxtczmrOCDbwu0A0MV1b5/JVkf +26yxinagMHYpADQnkLw31CyoaTXWlPpqjbiQwqrgbV9whKrDlP0VYJuivdul5xmO +/6+9IDqxRKoj4e7xsthg10RyPZxnGOKcl8ajRKFS1i3ZcFmSViXT30o9uF9aK0Qq +2G3EjPQCA7Ivwz2abBWSV0absnNvx4JBaquKC/8+0241wy2md39rJAOVYzdFc6I8 +330CQvaBrlpQaNVJhZSsUa2XJb4E2kLPM9pDY4SlOewRf/hz+S/cageimpEIQJDe +u8C9xqgVrS2xT0ny7iFPTpHwyFdxywOQ4GIka7/q26ECgYEA+eqnQAs5nw/s+qEd +nVarDsGcfFwUp7SDnwWvlBvtlt9OTXcqGe929WlKVDaiumOyUKacL8PPa3ru6ZDu ++LXA9QikQldjVWCQHtKzDYmDiju5XuxJj+4rOKG2ctmpbjM2SlOoyibqRlU/k9GI +htOAqCBWE8Ul6ovsUXUZC8ByFhECgYEA4B9syA+y8HG1Rccqo9iOyq65wCqrWuo8 +d1Q34Dw+mkFUTgLVSWTX0u7926bnsgvMPmGOhZ9ekq1ihZyVhIX0AY/MbfR98wSa +YMC9ISn+otZhDtl3SvLUP28pL0zQEw8Wifz2Nk+OwUsDf9E5PlHuDD6Ps1qpiVqG +BFHQRgtAoC8CgYAd/fwuYmp63VVqSpWcQT9sGO4nuoE8ExzMo8kLdEKSHaBvCYMC +88sJ7qXd72SeC8LljOknjk9BLdKoMx2KuX07qtrTn1srbtg86rpUQJGJsFsxuhel +70Y+mKGlrNt5fynfx6R1BjCNWkO0AKxqyc0h4CeUXc+ME1i7+dqUn3bRkQKBgQDZ +LjqIp4XbsCRb6MMuIMVGLQi86dxA7mkHrWmz9k0nx5S9P8uVIo5tzb/b4SH2i64w +6PJmE+heNHwbQ4Az+mZYORN9nYWLP/OlPEBJ6drhyuIktKD/1M3OZpa/Siz7uww5 +TRL90BxivKE4c/OHq3cFEH7J61oMStdBSlKL/Y1zawKBgAUOe/bE28jglBMGlhUt +J3YTiwSMgu2JaOaa4IZwu5MKjI7sUob98ztdnYK7o5E1ingk6Bb+Kct0P3dU29mC +MqtrTbrU2RwyCC8p2tj6PatL6okhCleqRqTHFqtX8SfgqKA5sOozBQd9hDVqoDBS +ogq0KJfKew73/8Ms0Ji8M2xK +-----END PRIVATE KEY----- diff --git a/test/configs/certs/rdns/client-d.pem b/test/configs/certs/rdns/client-d.pem new file mode 100644 index 00000000..6e39eeaf --- /dev/null +++ b/test/configs/certs/rdns/client-d.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIUbcKrNnZy7OU0rvNXp+A4KhDg3bIwDQYJKoZIhvcNAQEL +BQAwajELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAcM +C0xvcyBBbmdlbGVzMQ0wCwYDVQQKDAROQVRTMQ0wCwYDVQQLDAROQVRTMRIwEAYD +VQQDDAlsb2NhbGhvc3QwHhcNMjAwOTAxMTkwMTQ5WhcNMjUwODMxMTkwMTQ5WjCB +lDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtMb3MgQW5nZWxl +czENMAsGA1UECwwETkFUUzENMAsGA1UECgwETkFUUzEWMBQGA1UEAwwNKi5leGFt +cGxlLmNvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxEzARBgoJkiaJk/IsZAEZ +FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDay//0l8jlnU/v +DtoyNKmxIC+goq/ooiBnOCkuRyGus1yA70V4MWYEzi/N76sVw3MHXMpWxcYvxNO7 ++8szsgXVQF6G1pxnm2e3/Xp+lZWVHkjR/6h6dQpAwgyIkXCsIN/4+TYIn1T3/hdx +x4rYgeK1YO47lUSLskGNcU76nmqhooboWmWW8NYFJszvS0VUMXvMpaOmrFaIwx4e +9nDt8n2H7lyAq7cqo7sHv9ytT6xSPV2ok8swrbD9Ez/lyT+F/WbHkmP1HCK5365d +FmH3STH1E2l2lasTC4p92RK7NiWzzyIgOelcT6TblZxxJSyH8KfjaHcxFcJhkHjc +zY3h/K0fAgMBAAGjNjA0MDIGA1UdEQQrMCmCCWxvY2FsaG9zdIILZXhhbXBsZS5j +b22CD3d3dy5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAhXTt3ByBsVZz +zwTm7rJEl4oOXuIjzX4aeCBboN3FzSFYY9MYD9NNIKZdhSxPpSIV7p9DHfUX2Wlp +o1kKntjHSdDSOpIihGds1Oi5kYbrKpB7uj81KFjmn52as7yf5prX2CCN9RImrNqP +NkJiCR+B6KELeqvRMoHWg11kGZnhJ1/oAKholHgaILRNTHd3QXGvFBdm6JEYIQkt +3Jk8SZIhDM/jxEEOeEK5Mz4BjAaNIbiNVY1yUDF/+OymXfgILq1+dtmaoaxVMMRX +68E538NyCS7Bg56AhxwlhhFNVk4r96JRLgZTxb+CbkkZ32DNkkp0k5h/JIpkKQRH +OwJkyC9OZQ== +-----END CERTIFICATE----- diff --git a/test/tls_test.go b/test/tls_test.go index 7a0ed1bc..a9b9f450 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -1138,19 +1138,49 @@ func TestTLSClientAuthWithRDNSequence(t *testing.T) { err error rerr error }{ + // To generate certs for these tests: + // + // ``` + // openssl req -newkey rsa:2048 -nodes -keyout client-$CLIENT_ID.key -subj "/C=US/ST=CA/L=Los Angeles/OU=NATS/O=NATS/CN=*.example.com/DC=example/DC=com" -addext extendedKeyUsage=clientAuth -out client-$CLIENT_ID.csr + // openssl x509 -req -extfile <(printf "subjectAltName=DNS:localhost,DNS:example.com,DNS:www.example.com") -days 1825 -in client-$CLIENT_ID.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client-$CLIENT_ID.pem + // ``` + // + // To confirm subject from cert: + // + // ``` + // openssl x509 -in client-$CLIENT_ID.pem -text | grep Subject: + // ``` + // { "connect with tls using full RDN sequence", + ` + port: -1 + %s + + authorization { + users = [ + { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US,DC=foo1,DC=foo2" } + ] + } + `, + // C = US, ST = California, L = Los Angeles, O = NATS, OU = NATS, CN = localhost, DC = foo1, DC = foo2 + nats.ClientCert("./configs/certs/rdns/client-a.pem", "./configs/certs/rdns/client-a.key"), + nil, + nil, + }, + { + "connect with tls using full RDN sequence in original order", ` port: -1 %s authorization { users = [ - { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US,DC=foo1,DC=foo2" } + { user = "DC=foo2,DC=foo1,CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US" } ] } `, - // C=US/ST=California/L=Los Angeles/O=NATS/OU=NATS/CN=localhost/DC=foo1/DC=foo2 + // C = US, ST = California, L = Los Angeles, O = NATS, OU = NATS, CN = localhost, DC = foo1, DC = foo2 nats.ClientCert("./configs/certs/rdns/client-a.pem", "./configs/certs/rdns/client-a.key"), nil, nil, @@ -1163,13 +1193,14 @@ func TestTLSClientAuthWithRDNSequence(t *testing.T) { authorization { users = [ + { user = "DC=foo2,DC=foo1,CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US" }, { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US,DC=foo1,DC=foo2" }, { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US", permissions = { subscribe = { deny = ">" }} } ] } `, - // C=US/ST=California/L=Los Angeles/O=NATS/OU=NATS/CN=localhost + // C = US, ST = California, L = Los Angeles, O = NATS, OU = NATS, CN = localhost nats.ClientCert("./configs/certs/rdns/client-b.pem", "./configs/certs/rdns/client-b.key"), nil, errors.New("nats: timeout"), @@ -1182,17 +1213,19 @@ func TestTLSClientAuthWithRDNSequence(t *testing.T) { authorization { users = [ + { user = "DC=foo2,DC=foo1,CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US" } { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US,DC=foo1,DC=foo2" } { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US"}, ] } `, + // Cert is: // - // C=US/ST=California/L=Los Angeles/O=NATS/OU=NATS/CN=localhost/DC=foo3/DC=foo4 + // C = US, ST = California, L = Los Angeles, O = NATS, OU = NATS, CN = localhost, DC = foo3, DC = foo4 // - // but it will actually match the 2nd user so will not get an error (backwards compatible behavior) + // but it will actually match the user without DCs so will not get an error: // - // CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US + // C = US, ST = California, L = Los Angeles, O = NATS, OU = NATS, CN = localhost // nats.ClientCert("./configs/certs/rdns/client-c.pem", "./configs/certs/rdns/client-c.key"), nil, @@ -1206,16 +1239,96 @@ func TestTLSClientAuthWithRDNSequence(t *testing.T) { authorization { users = [ - { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US,DC=foo1,DC=foo2" } + { user = "CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US,DC=foo1,DC=foo2" }, + { user = "DC=foo2,DC=foo1,CN=localhost,OU=NATS,O=NATS,L=Los Angeles,ST=California,C=US" } ] } `, - // C=US/ST=California/L=Los Angeles/O=NATS/OU=NATS/CN=localhost/DC=foo3/DC=foo4 + // C = US, ST = California, L = Los Angeles, O = NATS, OU = NATS, CN = localhost, DC = foo3, DC = foo4 // nats.ClientCert("./configs/certs/rdns/client-c.pem", "./configs/certs/rdns/client-c.key"), errors.New("nats: Authorization Violation"), nil, }, + { + "connect with tls and RDN sequence with space after comma not should matter", + ` + port: -1 + %s + + authorization { + users = [ + { user = "DC=foo2, DC=foo1, CN=localhost, OU=NATS, O=NATS, L=Los Angeles, ST=California, C=US" } + ] + } + `, + // C=US/ST=California/L=Los Angeles/O=NATS/OU=NATS/CN=localhost/DC=foo1/DC=foo2 + // + nats.ClientCert("./configs/certs/rdns/client-a.pem", "./configs/certs/rdns/client-a.key"), + nil, + nil, + }, + { + "connect with tls and full RDN sequence respects order", + ` + port: -1 + %s + + authorization { + users = [ + { user = "DC=com, DC=example, CN=*.example.com, O=NATS, OU=NATS, L=Los Angeles, ST=CA, C=US" } + ] + } + `, + // + // C = US, ST = CA, L = Los Angeles, OU = NATS, O = NATS, CN = *.example.com, DC = example, DC = com + // + nats.ClientCert("./configs/certs/rdns/client-d.pem", "./configs/certs/rdns/client-d.key"), + nil, + nil, + }, + { + "connect with tls and full RDN sequence with added domainComponents and spaces also matches", + ` + port: -1 + %s + + authorization { + users = [ + { user = "CN=*.example.com,OU=NATS,O=NATS,L=Los Angeles,ST=CA,C=US,DC=example,DC=com" } + ] + } + `, + // + // C = US, ST = CA, L = Los Angeles, OU = NATS, O = NATS, CN = *.example.com, DC = example, DC = com + // + nats.ClientCert("./configs/certs/rdns/client-d.pem", "./configs/certs/rdns/client-d.key"), + nil, + nil, + }, + { + "connect with tls and full RDN sequence with correct order takes precedence over others matches", + ` + port: -1 + %s + + authorization { + users = [ + { user = "CN=*.example.com,OU=NATS,O=NATS,L=Los Angeles,ST=CA,C=US,DC=example,DC=com", + permissions = { subscribe = { deny = ">" }} } + { user = "DC=com,DC=example,CN=*.example.com,O=NATS,OU=NATS,L=Los Angeles,ST=CA,C=US" } + { user = "CN=*.example.com,OU=NATS,O=NATS,L=Los Angeles,ST=CA,C=US", + permissions = { subscribe = { deny = ">" }} } + ] + } + `, + // + // C = US, ST = CA, L = Los Angeles, OU = NATS, O = NATS, CN = *.example.com, DC = example, DC = com + // + nats.ClientCert("./configs/certs/rdns/client-d.pem", "./configs/certs/rdns/client-d.key"), + nil, + nil, + }, } { t.Run(test.name, func(t *testing.T) { content := fmt.Sprintf(test.config, `