[FIXED] Better support for distinguishedNameMatch in TLS Auth

Signed-off-by: Waldemar Quevedo <wally@synadia.com>
This commit is contained in:
Waldemar Quevedo
2020-09-01 12:08:58 -07:00
committed by Ivan Kozlovic
parent 12ae32a477
commit 7a88eee090
6 changed files with 720 additions and 39 deletions

234
internal/ldap/dn.go Normal file
View File

@@ -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
}

212
internal/ldap/dn_test.go Normal file
View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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, `