diff --git a/README.md b/README.md index 9a2a9bb3..28769f14 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,16 @@ Other supported formats are listed below. * `Workstation ID` - The workstation name (default is the host name) * `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener. The `database` must be specified when connecting with `Application Intent` set to `ReadOnly`. +### Kerberos Parameters + +* `krb5conffile` - File path for kerberos configuration file. +* `realm` - Domain name for kerberos authentication. +* `keytabfile` - Keytab file path. +* `krbcache` - Credential cache path. +* For further information on usage: + * + * + ### The connection string can be specified in one of three formats 1. URL: with `sqlserver` scheme. username and password appears before the host. Any instance appears as @@ -86,12 +96,16 @@ Other supported formats are listed below. db, err := sql.Open("sqlserver", u.String()) ``` - + * `sqlserver://username@host/instance?krb5conffile=path/to/file&krbcache=/path/to/cache` + * `sqlserver://username@host/instance?krb5conffile=path/to/file&realm=domain.com&keytabfile=/path/to/keytabfile` + 2. ADO: `key=value` pairs separated by `;`. Values may not contain `;`, leading and trailing whitespace is ignored. Examples: * `server=localhost\\SQLExpress;user id=sa;database=master;app name=MyAppName` * `server=localhost;user id=sa;database=master;app name=MyAppName` + * `server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;krbcache=path/to/cache` + * `server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;realm=domain.com;keytabfile=path/to/keytabfile` ADO strings support synonyms for database, app name, user id, and server * server <= addr, address, network address, data source @@ -111,6 +125,8 @@ Other supported formats are listed below. * `odbc:server=localhost;user id=sa;password=foo}bar` // Literal `}`, password is "foo}bar" * `odbc:server=localhost;user id=sa;password={foo{bar}` // Literal `{`, password is "foo{bar" * `odbc:server=localhost;user id=sa;password={foo}}bar}` // Escaped `} with`}}`, password is "foo}bar" + * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;krbcache=path/to/cache` + * `odbc:server=localhost;user id=sa;database=master;app name=MyAppName;krb5conffile=path/to/file;realm=domain.com;keytabfile=path/to/keytabfile` ### Azure Active Directory authentication @@ -307,6 +323,7 @@ are supported: * Supports Single-Sign-On on Windows * Supports connections to AlwaysOn Availability Group listeners, including re-direction to read-only replicas. * Supports query notifications +* Supports Kerberos Authentication ## Tests diff --git a/go.mod b/go.mod index 13c4d25b..1f029cd1 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe + github.com/jcmturner/gokrb5/v8 v8.4.2 github.com/golang-sql/sqlexp v0.1.0 golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 ) diff --git a/go.sum b/go.sum index f4f14f65..a5ffd579 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,24 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 h1:+eHOFJl1BaXrQxKX+T06f78590z4qA2ZzBTqahsKSE4= github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188/go.mod h1:vXjM/+wXQnTPR4KqTKDgJukSZ6amVRtWMPEjE6sQoK8= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -20,12 +38,16 @@ github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -39,6 +61,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kerbauth.go b/kerbauth.go new file mode 100644 index 00000000..f0276ebf --- /dev/null +++ b/kerbauth.go @@ -0,0 +1,123 @@ +package mssql + +import ( + "fmt" + "strconv" + "strings" + + "github.com/jcmturner/gokrb5/v8/client" + "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/credentials" + "github.com/jcmturner/gokrb5/v8/keytab" + "github.com/jcmturner/gokrb5/v8/spnego" +) + +type krb5Auth struct { + username string + realm string + serverSPN string + port uint64 + krb5Config *config.Config + krbKeytab *keytab.Keytab + krbCache *credentials.CCache + krb5Client *client.Client + state krb5ClientState +} + +func getKRB5Auth(user, serverSPN string, krb5Conf *config.Config, keytabContent *keytab.Keytab, cacheContent *credentials.CCache) (auth, bool) { + var port uint64 + var realm, serviceStr string + var err error + + params1 := strings.Split(serverSPN, ":") + if len(params1) != 2 { + return nil, false + } + + params2 := strings.Split(params1[1], "@") + switch len(params2) { + case 1: + port, err = strconv.ParseUint(params1[1], 10, 16) + if err != nil { + return nil, false + } + case 2: + port, err = strconv.ParseUint(params2[0], 10, 16) + if err != nil { + return nil, false + } + default: + return nil, false + } + + params3 := strings.Split(serverSPN, "@") + switch len(params3) { + case 1: + serviceStr = params3[0] + params3 = strings.Split(params1[0], "/") + params3 = strings.Split(params3[1], ".") + realm = params3[1] + "." + params3[2] + case 2: + realm = params3[1] + serviceStr = params3[0] + default: + return nil, false + } + + return &krb5Auth{ + username: user, + serverSPN: serviceStr, + port: port, + realm: realm, + krb5Config: krb5Conf, + krbKeytab: keytabContent, + krbCache: cacheContent, + }, true +} + +func (auth *krb5Auth) InitialBytes() ([]byte, error) { + var cl *client.Client + var err error + // Init keytab from conf + if auth.krbKeytab != nil { + // Init krb5 client and login + cl = client.NewWithKeytab(auth.username, auth.realm, auth.krbKeytab, auth.krb5Config, client.DisablePAFXFAST(true)) + } else { + cl, err = client.NewFromCCache(auth.krbCache, auth.krb5Config) + if err != nil { + return []byte{}, err + } + } + auth.krb5Client = cl + auth.state = initiatorStart + tkt, sessionKey, err := cl.GetServiceTicket(auth.serverSPN) + if err != nil { + return []byte{}, err + } + + negTok, err := spnego.NewNegTokenInitKRB5(auth.krb5Client, tkt, sessionKey) + if err != nil { + return []byte{}, err + } + + outToken, err := negTok.Marshal() + if err != nil { + return []byte{}, err + } + auth.state = initiatorWaitForMutal + return outToken, nil +} + +func (auth *krb5Auth) Free() { + auth.krb5Client.Destroy() +} + +func (auth *krb5Auth) NextBytes(token []byte) ([]byte, error) { + var spnegoToken spnego.SPNEGOToken + if err := spnegoToken.Unmarshal(token); err != nil { + err := fmt.Errorf("unmarshal APRep token failed: %w", err) + return []byte{}, err + } + auth.state = initiatorReady + return []byte{}, nil +} diff --git a/kerbauth_test.go b/kerbauth_test.go new file mode 100644 index 00000000..7d7f59e1 --- /dev/null +++ b/kerbauth_test.go @@ -0,0 +1,149 @@ +package mssql + +import ( + "reflect" + "testing" + + "github.com/jcmturner/gokrb5/v8/client" + "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/credentials" + "github.com/jcmturner/gokrb5/v8/keytab" +) + +func TestGetKRB5Auth(t *testing.T) { + krbConf := &config.Config{} + krbKeytab := &keytab.Keytab{} + krbCache := &credentials.CCache{} + + got, _ := getKRB5Auth("", "MSSQLSvc/mssql.domain.com:1433", krbConf, krbKeytab, krbCache) + keytab := &krb5Auth{username: "", + realm: "domain.com", + serverSPN: "MSSQLSvc/mssql.domain.com:1433", + port: 1433, + krb5Config: krbConf, + krbKeytab: krbKeytab, + krbCache: krbCache, + state: 0} + + res := reflect.DeepEqual(got, keytab) + if !res { + t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", keytab, got) + } + + got, _ = getKRB5Auth("", "MSSQLSvc/mssql.domain.com:1433", krbConf, krbKeytab, krbCache) + keytab = &krb5Auth{username: "", + realm: "domain.com", + serverSPN: "MSSQLSvc/mssql.domain.com:1433", + port: 1433, + krb5Config: krbConf, + krbKeytab: krbKeytab, + krbCache: krbCache, + state: 0} + + res = reflect.DeepEqual(got, keytab) + if !res { + t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", keytab, got) + } + + _, val := getKRB5Auth("", "MSSQLSvc/mssql.domain.com", krbConf, krbKeytab, krbCache) + if val { + t.Errorf("Failed to get correct krb5Auth object: no port defined") + } + + got, _ = getKRB5Auth("", "MSSQLSvc/mssql.domain.com:1433@DOMAIN.COM", krbConf, krbKeytab, krbCache) + keytab = &krb5Auth{username: "", + realm: "DOMAIN.COM", + serverSPN: "MSSQLSvc/mssql.domain.com:1433", + port: 1433, + krb5Config: krbConf, + krbKeytab: krbKeytab, + krbCache: krbCache, + state: 0} + + res = reflect.DeepEqual(got, keytab) + if !res { + t.Errorf("Failed to get correct krb5Auth object\nExpected:%v\nRecieved:%v", keytab, got) + } + + _, val = getKRB5Auth("", "MSSQLSvc/mssql.domain.com:1433@domain.com@test", krbConf, krbKeytab, krbCache) + if val { + t.Errorf("Failed to get correct krb5Auth object due to incorrect serverSPN name") + } + + _, val = getKRB5Auth("", "MSSQLSvc/mssql.domain.com:port@domain.com", krbConf, krbKeytab, krbCache) + if val { + t.Errorf("Failed to get correct krb5Auth object due to incorrect port") + } + + _, val = getKRB5Auth("", "MSSQLSvc/mssql.domain.com:port", krbConf, krbKeytab, krbCache) + if val { + t.Errorf("Failed to get correct krb5Auth object due to incorrect port") + } +} + +func TestInitialBytes(t *testing.T) { + krbConf := &config.Config{} + krbKeytab := &keytab.Keytab{} + krbCache := &credentials.CCache{} + krbObj := &krb5Auth{username: "", + realm: "domain.com", + serverSPN: "MSSQLSvc/mssql.domain.com:1433", + port: 1433, + krb5Config: krbConf, + krbKeytab: krbKeytab, + krbCache: krbCache, + state: 0, + } + + _, err := krbObj.InitialBytes() + if err == nil { + t.Errorf("Failed to get Initial bytes") + } + + _, err = krbObj.InitialBytes() + if err == nil { + t.Errorf("Failed to get Initial bytes") + } +} + +func TestNextBytes(t *testing.T) { + ans := []byte{} + krbConf := &config.Config{} + krbKeytab := &keytab.Keytab{} + krbCache := &credentials.CCache{} + + var krbObj auth = &krb5Auth{username: "", + realm: "domain.com", + serverSPN: "MSSQLSvc/mssql.domain.com:1433", + port: 1433, + krb5Config: krbConf, + krbKeytab: krbKeytab, + krbCache: krbCache, + state: 0} + + _, err := krbObj.NextBytes(ans) + if err == nil { + t.Errorf("Error getting next byte") + } +} + +func TestFree(t *testing.T) { + krbConf := &config.Config{} + krbKeytab := &keytab.Keytab{} + krbCache := &credentials.CCache{} + kt := &keytab.Keytab{} + c := &config.Config{} + cl := client.NewWithKeytab("Administrator", "DOMAIN.COM", kt, c, client.DisablePAFXFAST(true)) + + var krbObj auth = &krb5Auth{username: "", + realm: "domain.com", + serverSPN: "MSSQLSvc/mssql.domain.com:1433", + port: 1433, + krb5Config: krbConf, + krbKeytab: krbKeytab, + krbCache: krbCache, + state: 0, + krb5Client: cl, + } + krbObj.Free() +} diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index 40bedb08..07e36634 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -13,8 +13,14 @@ import ( "strings" "time" "unicode" + + "github.com/jcmturner/gokrb5/v8/config" + "github.com/jcmturner/gokrb5/v8/credentials" + "github.com/jcmturner/gokrb5/v8/keytab" ) +const defaultServerPort = 1433 + type ( Encryption int Log uint64 @@ -37,6 +43,21 @@ const ( LogRetries Log = 128 ) +type Kerberos struct { + // Kerberos configuration details + Config *config.Config + + // Credential cache + Cache *credentials.CCache + + // A Kerberos realm is the domain over which a Kerberos authentication server has the authority + // to authenticate a user, host or service. + Realm string + + // Kerberos keytab that stores long-term keys for one or more principals + Keytab *keytab.Keytab +} + type Config struct { Port uint64 Host string @@ -73,6 +94,8 @@ type Config struct { ConnTimeout time.Duration // Use context for timeouts. KeepAlive time.Duration // Leave at default. PacketSize uint16 + + Kerberos *Kerberos } func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string) (*tls.Config, error) { @@ -109,6 +132,7 @@ var skipSetup = errors.New("skip setting up TLS") func Parse(dsn string) (Config, map[string]string, error) { p := Config{} + p.Kerberos = &Kerberos{} var params map[string]string if strings.HasPrefix(dsn, "odbc:") { @@ -181,8 +205,43 @@ func Parse(dsn string) (Config, map[string]string, error) { } } + krb5ConfFile, ok := params["krb5conffile"] + if ok { + var err error + p.Kerberos.Config, err = setupKerbConfig(krb5ConfFile) + if err != nil { + return p, params, fmt.Errorf("cannot read kerberos configuration file: %w", err) + } + + missingParam := checkMissingKRBConfig(params) + if missingParam != "" { + return p, params, fmt.Errorf("missing parameter:%s", missingParam) + } + + realm, ok := params["realm"] + if ok { + p.Kerberos.Realm = realm + } + + krbCache, ok := params["krbcache"] + if ok { + var err error + p.Kerberos.Cache, err = setupKerbCache(krbCache) + if err != nil { + return p, params, fmt.Errorf("cannot read kerberos cache file: %w", err) + } + } + + keytabfile, ok := params["keytabfile"] + if ok { + var err error + p.Kerberos.Keytab, err = setupKerbKeytab(keytabfile) + if err != nil { + return p, params, fmt.Errorf("cannot read kerberos keytab file: %w", err) + } + } + } // https://msdn.microsoft.com/en-us/library/dd341108.aspx - // // Do not set a connection timeout. Use Context to manage such things. // Default to zero, but still allow it to be set. if strconntimeout, ok := params["connection timeout"]; ok { @@ -267,7 +326,7 @@ func Parse(dsn string) (Config, map[string]string, error) { if ok { p.ServerSPN = serverSPN } else { - p.ServerSPN = generateSpn(p.Host, p.Port) + p.ServerSPN = generateSpn(p.Host, resolveServerPort(p.Port), p.Kerberos.Realm) } workstation, ok := params["workstation id"] @@ -322,10 +381,23 @@ func Parse(dsn string) (Config, map[string]string, error) { } else { p.DisableRetry = disableRetryDefault } - return p, params, nil } +func checkMissingKRBConfig(c map[string]string) (missingParam string) { + if c["keytabfile"] != "" { + if c["realm"] == "" { + missingParam = "realm" + return + } + } + if c["krbcache"] == "" && c["keytabfile"] == "" { + missingParam = "atleast krbcache or keytab is required" + return + } + return +} + // convert connectionParams to url style connection string // used mostly for testing func (p Config) URL() *url.URL { @@ -605,6 +677,48 @@ func normalizeOdbcKey(s string) string { return strings.ToLower(strings.TrimRightFunc(s, unicode.IsSpace)) } -func generateSpn(host string, port uint64) string { - return fmt.Sprintf("MSSQLSvc/%s:%d", host, port) +func resolveServerPort(port uint64) uint64 { + if port == 0 { + return defaultServerPort + } + return port +} + +func generateSpn(host string, port uint64, realm string) string { + if realm == "" { + return fmt.Sprintf("MSSQLSvc/%s:%d", host, port) + } + return fmt.Sprintf("MSSQLSvc/%s:%d@%s", host, port, realm) +} + +func setupKerbConfig(krb5configPath string) (*config.Config, error) { + krb5CnfFile, err := os.Open(krb5configPath) + if err != nil { + return nil, err + } + c, err := config.NewFromReader(krb5CnfFile) + if err != nil { + return nil, err + } + return c, nil +} + +func setupKerbCache(kerbCCahePath string) (*credentials.CCache, error) { + cache, err := credentials.LoadCCache(kerbCCahePath) + if err != nil { + return nil, err + } + return cache, nil +} + +func setupKerbKeytab(keytabFilePath string) (*keytab.Keytab, error) { + var kt = &keytab.Keytab{} + keytabConf, err := ioutil.ReadFile(keytabFilePath) + if err != nil { + return nil, err + } + if err = kt.Unmarshal([]byte(keytabConf)); err != nil { + return nil, err + } + return kt, nil } diff --git a/msdsn/conn_str_test.go b/msdsn/conn_str_test.go index 594b5b3d..c3040d0a 100644 --- a/msdsn/conn_str_test.go +++ b/msdsn/conn_str_test.go @@ -1,6 +1,8 @@ package msdsn import ( + "io/ioutil" + "os" "reflect" "testing" "time" @@ -196,3 +198,43 @@ func TestConnParseRoundTripFixed(t *testing.T) { t.Fatal("Parameters do not match after roundtrip", params, rtParams) } } + +func TestInvalidConnectionStringKerberos(t *testing.T) { + connStrings := []string{ + "server=server;port=1345;realm=domain;trustservercertificate=true;krb5conffile=/path/krb5.conf;", + "server=server;user id=user;password=pwd;port=1345;realm=domain;trustservercertificate=true;krb5conffile=/path/krb5.conf;", + "server=server;user id=user;password=pwd;port=1345;trustservercertificate=true;krb5conffile=/path/krb5.conf;keytabfile=/path/to/administrator2.keytab;", + } + for _, connStr := range connStrings { + _, _, err := Parse(connStr) + if err == nil { + t.Errorf("Connection expected to fail for connection string %s but it didn't", connStr) + } + } +} + +func TestValidConnectionStringKerberos(t *testing.T) { + kerberosTestFile := createKrbFile(t) + defer os.Remove(kerberosTestFile) + connStrings := []string{ + "server=server;user id=user;port=1345;realm=domain;trustservercertificate=true;krb5conffile=" + kerberosTestFile + ";keytabfile=" + kerberosTestFile, + "server=server;port=1345;realm=domain;trustservercertificate=true;krb5conffile=" + kerberosTestFile + ";krbcache=" + kerberosTestFile, + } + for _, connStr := range connStrings { + _, _, err := Parse(connStr) + if err == nil { + t.Errorf("Connection string %s should fail to parse with error %s", connStrings, err) + } + } +} + +func createKrbFile(t *testing.T) string { + file, err := ioutil.TempFile("", "test-*.txt") + if err != nil { + t.Fatalf("Failed to create a temp file:%v",err) + } + if _, err := file.Write([]byte("This is a test file\n")); err != nil { + t.Fatalf("Failed to write file:%v",err) + } + return file.Name() +} diff --git a/tds.go b/tds.go index dbe95272..e28485ef 100644 --- a/tds.go +++ b/tds.go @@ -1172,8 +1172,13 @@ initiate_connection: } } } - - auth, authOk := getAuth(p.User, p.Password, p.ServerSPN, p.Workstation) + var auth auth + var authOk bool + if p.Kerberos != nil && p.Kerberos.Config != nil { + auth, authOk = getKRB5Auth(p.User, p.ServerSPN, p.Kerberos.Config, p.Kerberos.Keytab, p.Kerberos.Cache) + } else { + auth, authOk = getAuth(p.User, p.Password, p.ServerSPN, p.Workstation) + } if authOk { defer auth.Free() } else { diff --git a/types.go b/types.go index 822a7d86..155efffd 100644 --- a/types.go +++ b/types.go @@ -113,6 +113,17 @@ type xmlInfo struct { XmlSchemaCollection string } +// Kerberos Client State +type krb5ClientState int + +const ( + // Initiator states + initiatorStart krb5ClientState = iota + initiatorRestart + initiatorWaitForMutal + initiatorReady +) + func readTypeInfo(r *tdsBuffer) (res typeInfo) { res.TypeId = r.byte() switch res.TypeId {