Skip to content

Commit 07d3f72

Browse files
authored
Merge pull request #970 from mjibson/gss-merged
Add Kerberos (GSSAPI) authentication support
2 parents 79ca807 + 41af58a commit 07d3f72

File tree

5 files changed

+294
-1
lines changed

5 files changed

+294
-1
lines changed

conn.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ type conn struct {
155155

156156
// If not nil, notifications will be synchronously sent here
157157
notificationHandler func(*Notification)
158+
159+
// GSSAPI context
160+
gss Gss
158161
}
159162

160163
// Handle driver-side settings in parsed connection string.
@@ -1071,7 +1074,10 @@ func isDriverSetting(key string) bool {
10711074
return true
10721075
case "binary_parameters":
10731076
return true
1074-
1077+
case "service":
1078+
return true
1079+
case "spn":
1080+
return true
10751081
default:
10761082
return false
10771083
}
@@ -1151,6 +1157,56 @@ func (cn *conn) auth(r *readBuf, o values) {
11511157
if r.int32() != 0 {
11521158
errorf("unexpected authentication response: %q", t)
11531159
}
1160+
case 7: // GSSAPI, startup
1161+
cli, err := NewGSS()
1162+
if err != nil {
1163+
errorf("kerberos error: %s", err.Error())
1164+
}
1165+
1166+
var token []byte
1167+
1168+
if spn, ok := o["spn"]; ok {
1169+
// Use the supplied SPN if provided..
1170+
token, err = cli.GetInitTokenFromSpn(spn)
1171+
} else {
1172+
// Allow the kerberos service name to be overridden
1173+
service := "postgres"
1174+
if val, ok := o["service"]; ok {
1175+
service = val
1176+
}
1177+
1178+
token, err = cli.GetInitToken(o["host"], service)
1179+
}
1180+
1181+
if err != nil {
1182+
errorf("failed to get Kerberos ticket: %q", err)
1183+
}
1184+
1185+
w := cn.writeBuf('p')
1186+
w.bytes(token)
1187+
cn.send(w)
1188+
1189+
// Store for GSSAPI continue message
1190+
cn.gss = cli
1191+
1192+
case 8: // GSSAPI continue
1193+
1194+
if cn.gss == nil {
1195+
errorf("GSSAPI protocol error")
1196+
}
1197+
1198+
b := []byte(*r)
1199+
1200+
done, tokOut, err := cn.gss.Continue(b)
1201+
if err == nil && !done {
1202+
w := cn.writeBuf('p')
1203+
w.bytes(tokOut)
1204+
cn.send(w)
1205+
}
1206+
1207+
// Errors fall through and read the more detailed message
1208+
// from the server..
1209+
11541210
case 10:
11551211
sc := scram.NewClient(sha256.New, o["user"], o["password"])
11561212
sc.Step(nil)

go.mod

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
11
module github.com/lib/pq
2+
3+
go 1.13
4+
5+
require (
6+
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
7+
github.com/jcmturner/gokrb5/v8 v8.2.0
8+
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 // indirect
9+
gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
10+
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 // indirect
11+
gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect
12+
gopkg.in/jcmturner/gokrb5.v7 v7.5.0
13+
gopkg.in/jcmturner/rpc.v1 v1.1.0 // indirect
14+
)

krb.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package pq
2+
3+
import (
4+
"net"
5+
"strings"
6+
)
7+
8+
/*
9+
* Basic GSSAPI interface to abstract Windows (SSPI) from Unix
10+
* APIs within the driver
11+
*/
12+
13+
type Gss interface {
14+
GetInitToken(host string, service string) ([]byte, error)
15+
GetInitTokenFromSpn(spn string) ([]byte, error)
16+
Continue(inToken []byte) (done bool, outToken []byte, err error)
17+
}
18+
19+
/*
20+
* Find the A record associated with a hostname
21+
* In general, hostnames supplied to the driver should be
22+
* canonicalized because the KDC usually only has one
23+
* principal and not one per potential alias of a host.
24+
*/
25+
func canonicalizeHostname(host string) (string, error) {
26+
canon := host
27+
28+
name, err := net.LookupCNAME(host)
29+
if err != nil {
30+
return "", err
31+
}
32+
33+
name = strings.TrimSuffix(name, ".")
34+
35+
if name != "" {
36+
canon = name
37+
}
38+
39+
return canon, nil
40+
}

krb_unix.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// +build !windows
2+
3+
package pq
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"os/user"
9+
"strings"
10+
11+
"github.com/jcmturner/gokrb5/v8/client"
12+
"github.com/jcmturner/gokrb5/v8/config"
13+
"github.com/jcmturner/gokrb5/v8/credentials"
14+
"github.com/jcmturner/gokrb5/v8/spnego"
15+
)
16+
17+
/*
18+
* UNIX Kerberos support, using jcmturner's pure-go
19+
* implementation
20+
*/
21+
22+
// Implements the Gss interface
23+
type gss struct {
24+
cli *client.Client
25+
}
26+
27+
func NewGSS() (Gss, error) {
28+
g := &gss{}
29+
err := g.init()
30+
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
return g, nil
36+
}
37+
38+
func (g *gss) init() error {
39+
cfgPath, ok := os.LookupEnv("KRB5_CONFIG")
40+
if !ok {
41+
cfgPath = "/etc/krb5.conf"
42+
}
43+
44+
cfg, err := config.Load(cfgPath)
45+
if err != nil {
46+
return err
47+
}
48+
49+
u, err := user.Current()
50+
if err != nil {
51+
return err
52+
}
53+
54+
ccpath := "/tmp/krb5cc_" + u.Uid
55+
56+
ccname := os.Getenv("KRB5CCNAME")
57+
if strings.HasPrefix(ccname, "FILE:") {
58+
ccpath = strings.SplitN(ccname, ":", 2)[1]
59+
}
60+
61+
ccache, err := credentials.LoadCCache(ccpath)
62+
if err != nil {
63+
return err
64+
}
65+
66+
cl, err := client.NewFromCCache(ccache, cfg, client.DisablePAFXFAST(true))
67+
if err != nil {
68+
return err
69+
}
70+
71+
cl.Login()
72+
73+
g.cli = cl
74+
75+
return nil
76+
}
77+
78+
func (g *gss) GetInitToken(host string, service string) ([]byte, error) {
79+
80+
// Resolve the hostname down to an 'A' record, if required (usually, it is)
81+
if g.cli.Config.LibDefaults.DNSCanonicalizeHostname {
82+
var err error
83+
host, err = canonicalizeHostname(host)
84+
if err != nil {
85+
return nil, err
86+
}
87+
}
88+
89+
spn := service + "/" + host
90+
91+
return g.GetInitTokenFromSpn(spn)
92+
}
93+
94+
func (g *gss) GetInitTokenFromSpn(spn string) ([]byte, error) {
95+
s := spnego.SPNEGOClient(g.cli, spn)
96+
97+
st, err := s.InitSecContext()
98+
if err != nil {
99+
return nil, fmt.Errorf("kerberos error (InitSecContext): %s", err.Error())
100+
}
101+
102+
b, err := st.Marshal()
103+
if err != nil {
104+
return nil, fmt.Errorf("kerberos error (Marshaling token): %s", err.Error())
105+
}
106+
107+
return b, nil
108+
}
109+
110+
func (g *gss) Continue(inToken []byte) (done bool, outToken []byte, err error) {
111+
t := &spnego.SPNEGOToken{}
112+
err = t.Unmarshal(inToken)
113+
if err != nil {
114+
return true, nil, fmt.Errorf("kerberos error (Unmarshaling token): %s", err.Error())
115+
}
116+
117+
state := t.NegTokenResp.State()
118+
if state != spnego.NegStateAcceptCompleted {
119+
return true, nil, fmt.Errorf("kerberos: expected state 'Completed' - got %d", state)
120+
}
121+
122+
return true, nil, nil
123+
}

krb_windows.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// +build windows
2+
3+
package pq
4+
5+
import (
6+
"github.com/alexbrainman/sspi"
7+
"github.com/alexbrainman/sspi/negotiate"
8+
)
9+
10+
type gss struct {
11+
creds *sspi.Credentials
12+
ctx *negotiate.ClientContext
13+
}
14+
15+
func NewGSS() (Gss, error) {
16+
g := &gss{}
17+
err := g.init()
18+
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
return g, nil
24+
}
25+
26+
func (g *gss) init() error {
27+
creds, err := negotiate.AcquireCurrentUserCredentials()
28+
if err != nil {
29+
return err
30+
}
31+
32+
g.creds = creds
33+
return nil
34+
}
35+
36+
func (g *gss) GetInitToken(host string, service string) ([]byte, error) {
37+
38+
host, err := canonicalizeHostname(host)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
spn := service + "/" + host
44+
45+
return g.GetInitTokenFromSpn(spn)
46+
}
47+
48+
func (g *gss) GetInitTokenFromSpn(spn string) ([]byte, error) {
49+
ctx, token, err := negotiate.NewClientContext(g.creds, spn)
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
g.ctx = ctx
55+
56+
return token, nil
57+
}
58+
59+
func (g *gss) Continue(inToken []byte) (done bool, outToken []byte, err error) {
60+
return g.ctx.Update(inToken)
61+
}

0 commit comments

Comments
 (0)