Skip to content

Commit 663dfe5

Browse files
csucudomodwyer
authored andcommitted
Add proper DN construction (#55)
* Add proper DN construction * Added openssl check to test * Addressed code review comments * Changes to RDNformatting, and test * type/value fields to tests * Changes to RDN formatting * Corrected comment in getRFC2253NameString * Corrected comment in getRFC2253NameString * Changes to login and rdn formatting * Changed escaping of #
1 parent 0454966 commit 663dfe5

File tree

3 files changed

+189
-2
lines changed

3 files changed

+189
-2
lines changed

auth_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ package mgo_test
2828

2929
import (
3030
"crypto/tls"
31+
"crypto/x509"
3132
"flag"
3233
"fmt"
3334
"io/ioutil"
@@ -963,6 +964,57 @@ func (s *S) TestAuthX509Cred(c *C) {
963964
c.Assert(len(names) > 0, Equals, true)
964965
}
965966

967+
func (s *S) TestAuthX509CredRDNConstruction(c *C) {
968+
session, err := mgo.Dial("localhost:40001")
969+
c.Assert(err, IsNil)
970+
defer session.Close()
971+
binfo, err := session.BuildInfo()
972+
c.Assert(err, IsNil)
973+
if binfo.OpenSSLVersion == "" {
974+
c.Skip("server does not support SSL")
975+
}
976+
977+
clientCertPEM, err := ioutil.ReadFile("harness/certs/client.pem")
978+
c.Assert(err, IsNil)
979+
980+
clientCert, err := tls.X509KeyPair(clientCertPEM, clientCertPEM)
981+
c.Assert(err, IsNil)
982+
983+
clientCert.Leaf, err = x509.ParseCertificate(clientCert.Certificate[0])
984+
c.Assert(err, IsNil)
985+
986+
tlsConfig := &tls.Config{
987+
InsecureSkipVerify: true,
988+
Certificates: []tls.Certificate{clientCert},
989+
}
990+
991+
var host = "localhost:40003"
992+
c.Logf("Connecting to %s...", host)
993+
session, err = mgo.DialWithInfo(&mgo.DialInfo{
994+
Addrs: []string{host},
995+
DialServer: func(addr *mgo.ServerAddr) (net.Conn, error) {
996+
return tls.Dial("tcp", addr.String(), tlsConfig)
997+
},
998+
})
999+
c.Assert(err, IsNil)
1000+
defer session.Close()
1001+
1002+
cred := &mgo.Credential{
1003+
Username: "root",
1004+
Mechanism: "MONGODB-X509",
1005+
Source: "$external",
1006+
Certificate: tlsConfig.Certificates[0].Leaf,
1007+
}
1008+
err = session.Login(cred)
1009+
c.Assert(err, NotNil)
1010+
1011+
cred.Username = ""
1012+
c.Logf("Authenticating...")
1013+
err = session.Login(cred)
1014+
c.Assert(err, IsNil)
1015+
c.Logf("Authenticated!")
1016+
}
1017+
9661018
var (
9671019
plainFlag = flag.String("plain", "", "Host to test PLAIN authentication against (depends on custom environment)")
9681020
plainUser = "einstein"

session.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ package mgo
2828

2929
import (
3030
"crypto/md5"
31+
"crypto/x509"
32+
"crypto/x509/pkix"
33+
"encoding/asn1"
3134
"encoding/hex"
3235
"errors"
3336
"fmt"
@@ -825,6 +828,15 @@ type Credential struct {
825828
// Mechanism defines the protocol for credential negotiation.
826829
// Defaults to "MONGODB-CR".
827830
Mechanism string
831+
832+
// Certificate defines an x509 certificate for authentication at login,
833+
// for reference please see, https://docs.mongodb.com/manual/tutorial/configure-x509-client-authentication/
834+
// If providing a certificate:
835+
// The Username field is populated from the cert and should not be set
836+
// The Mechanism field should be MONGODB-X509 or not set.
837+
// The Source field should be $external or not set.
838+
// If not specified, the username will have to be set manually.
839+
Certificate *x509.Certificate
828840
}
829841

830842
// Login authenticates with MongoDB using the provided credential. The
@@ -847,6 +859,19 @@ func (s *Session) Login(cred *Credential) error {
847859
defer socket.Release()
848860

849861
credCopy := *cred
862+
if cred.Certificate != nil && cred.Username != "" {
863+
return errors.New("failed to login, both certificate and credentials are given")
864+
}
865+
866+
if cred.Certificate != nil {
867+
credCopy.Username, err = getRFC2253NameStringFromCert(cred.Certificate)
868+
if err != nil {
869+
return err
870+
}
871+
credCopy.Mechanism = "MONGODB-X509"
872+
credCopy.Source = "$external"
873+
}
874+
850875
if cred.Source == "" {
851876
if cred.Mechanism == "GSSAPI" {
852877
credCopy.Source = "$external"
@@ -5212,3 +5237,73 @@ func hasErrMsg(d []byte) bool {
52125237
}
52135238
return false
52145239
}
5240+
5241+
// getRFC2253NameStringFromCert converts from an ASN.1 structured representation of the certificate
5242+
// to a UTF-8 string representation(RDN) and returns it.
5243+
func getRFC2253NameStringFromCert(certificate *x509.Certificate) (string, error) {
5244+
var RDNElements = pkix.RDNSequence{}
5245+
_, err := asn1.Unmarshal(certificate.RawSubject, &RDNElements)
5246+
return getRFC2253NameString(&RDNElements), err
5247+
}
5248+
5249+
// getRFC2253NameString converts from an ASN.1 structured representation of the RDNSequence
5250+
// from the certificate to a UTF-8 string representation(RDN) and returns it.
5251+
func getRFC2253NameString(RDNElements *pkix.RDNSequence) string {
5252+
var RDNElementsString = []string{}
5253+
var replacer = strings.NewReplacer(",", "\\,", "=", "\\=", "+", "\\+", "<", "\\<", ">", "\\>", ";", "\\;")
5254+
//The elements in the sequence needs to be reversed when converting them
5255+
for i := len(*RDNElements) - 1; i >= 0; i-- {
5256+
var nameAndValueList = make([]string,len((*RDNElements)[i]))
5257+
for j, attribute := range (*RDNElements)[i] {
5258+
var shortAttributeName = rdnOIDToShortName(attribute.Type)
5259+
if len(shortAttributeName) <= 0 {
5260+
nameAndValueList[j] = fmt.Sprintf("%s=%X", attribute.Type.String(), attribute.Value.([]byte))
5261+
continue
5262+
}
5263+
var attributeValueString = attribute.Value.(string)
5264+
// escape leading space or #
5265+
if strings.HasPrefix(attributeValueString, " ") || strings.HasPrefix(attributeValueString, "#") {
5266+
attributeValueString = "\\" + attributeValueString
5267+
}
5268+
// escape trailing space, unless it's already escaped
5269+
if strings.HasSuffix(attributeValueString, " ") && !strings.HasSuffix(attributeValueString, "\\ ") {
5270+
attributeValueString = attributeValueString[:len(attributeValueString)-1] + "\\ "
5271+
}
5272+
5273+
// escape , = + < > # ;
5274+
attributeValueString = replacer.Replace(attributeValueString)
5275+
nameAndValueList[j] = fmt.Sprintf("%s=%s", shortAttributeName, attributeValueString)
5276+
}
5277+
5278+
RDNElementsString = append(RDNElementsString, strings.Join(nameAndValueList, "+"))
5279+
}
5280+
5281+
return strings.Join(RDNElementsString, ",")
5282+
}
5283+
5284+
var oidsToShortNames = []struct {
5285+
oid asn1.ObjectIdentifier
5286+
shortName string
5287+
}{
5288+
{asn1.ObjectIdentifier{2, 5, 4, 3}, "CN"},
5289+
{asn1.ObjectIdentifier{2, 5, 4, 6}, "C"},
5290+
{asn1.ObjectIdentifier{2, 5, 4, 7}, "L"},
5291+
{asn1.ObjectIdentifier{2, 5, 4, 8}, "ST"},
5292+
{asn1.ObjectIdentifier{2, 5, 4, 10}, "O"},
5293+
{asn1.ObjectIdentifier{2, 5, 4, 11}, "OU"},
5294+
{asn1.ObjectIdentifier{2, 5, 4, 9}, "STREET"},
5295+
{asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}, "DC"},
5296+
{asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1}, "UID"},
5297+
}
5298+
5299+
// rdnOIDToShortName returns an short name of the given RDN OID. If the OID does not have a short
5300+
// name, the function returns an empty string
5301+
func rdnOIDToShortName(oid asn1.ObjectIdentifier) string {
5302+
for i := range oidsToShortNames {
5303+
if oidsToShortNames[i].oid.Equal(oid) {
5304+
return oidsToShortNames[i].shortName
5305+
}
5306+
}
5307+
5308+
return ""
5309+
}

session_internal_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package mgo
22

33
import (
4-
"testing"
5-
4+
"crypto/x509/pkix"
5+
"encoding/asn1"
66
"github.com/globalsign/mgo/bson"
7+
. "gopkg.in/check.v1"
8+
"testing"
79
)
810

11+
type S struct{}
12+
13+
var _ = Suite(&S{})
14+
915
// This file is for testing functions that are not exported outside the mgo
1016
// package - avoid doing so if at all possible.
1117

@@ -22,3 +28,37 @@ func TestIndexedInt64FieldsBug(t *testing.T) {
2228

2329
_ = simpleIndexKey(input)
2430
}
31+
32+
func (s *S) TestGetRFC2253NameStringSingleValued(c *C) {
33+
var RDNElements = pkix.RDNSequence{
34+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 6}, Value: "GO"}},
35+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 8}, Value: "MGO"}},
36+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 7}, Value: "MGO"}},
37+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 10}, Value: "MGO"}},
38+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 11}, Value: "Client"}},
39+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "localhost"}},
40+
}
41+
42+
c.Assert(getRFC2253NameString(&RDNElements), Equals, "CN=localhost,OU=Client,O=MGO,L=MGO,ST=MGO,C=GO")
43+
}
44+
45+
func (s *S) TestGetRFC2253NameStringEscapeChars(c *C) {
46+
var RDNElements = pkix.RDNSequence{
47+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 6}, Value: "GB"}},
48+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 8}, Value: "MGO "}},
49+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 10}, Value: "Sue, Grabbit and Runn < > ;"}},
50+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "L. Eagle"}},
51+
}
52+
53+
c.Assert(getRFC2253NameString(&RDNElements), Equals, "CN=L. Eagle,O=Sue\\, Grabbit and Runn \\< \\> \\;,ST=MGO\\ ,C=GB")
54+
}
55+
56+
func (s *S) TestGetRFC2253NameStringMultiValued(c *C) {
57+
var RDNElements = pkix.RDNSequence{
58+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 6}, Value: "US"}},
59+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 10}, Value: "Widget Inc."}},
60+
{{Type: asn1.ObjectIdentifier{2, 5, 4, 11}, Value: "Sales"}, {Type: asn1.ObjectIdentifier{2, 5, 4, 3}, Value: "J. Smith"}},
61+
}
62+
63+
c.Assert(getRFC2253NameString(&RDNElements), Equals, "OU=Sales+CN=J. Smith,O=Widget Inc.,C=US")
64+
}

0 commit comments

Comments
 (0)