Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions examples/channel_binding/tsql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package main

import (
"bufio"
"context"
"crypto/tls"
"database/sql"
"flag"
"fmt"
"io"
"log"
"os"
"time"

"github.com/google/uuid"
mssqldb "github.com/microsoft/go-mssqldb"
"github.com/microsoft/go-mssqldb/msdsn"
_ "github.com/microsoft/go-mssqldb/integratedauth/krb5"
)

func main() {
var (
userid = flag.String("U", "", "login_id")
password = flag.String("P", "", "password")
server = flag.String("S", "localhost", "server_name[\\instance_name]")
port = flag.Uint64("p", 1433, "server port")
keyLog = flag.String("K", "tlslog.log", "path to sslkeylog file")
database = flag.String("d", "", "db_name")
spn = flag.String("spn", "", "SPN")
auth = flag.String("a", "ntlm", "Authentication method: ntlm or krb5")
epa = flag.String("epa", "tls-unique", "EPA mode: tls-unique, tls-server-end-point")
)
flag.Parse()

keyLogFile, err := os.OpenFile(*keyLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err != nil {
log.Fatal("failed to open keylog file:", err)
}
defer func() {
if cerr := keyLogFile.Close(); cerr != nil {
log.Printf("warning: failed to close keylog file: %v", cerr)
}
}()


cfg := msdsn.Config{
User: *userid,
Database: *database,
Host: *server,
Port: *port,
Password: *password,
ChangePassword: "",
AppName: "go-mssqldb",
ServerSPN: *spn,
TLSConfig: &tls.Config{
InsecureSkipVerify: true, // adjust for your case
ServerName: *server,
KeyLogWriter: keyLogFile,
DynamicRecordSizingDisabled: true,
MinVersion: tls.VersionTLS11,
MaxVersion: tls.VersionTLS12,
},
Encryption: msdsn.EncryptionRequired,

Parameters: map[string]string{
"authenticator": *auth,
"krb5-credcachefile": os.Getenv("KRB5_CCNAME"),
"krb5-configfile": os.Getenv("KRB5_CONFIG"),
},
ProtocolParameters: map[string]interface{}{},
Protocols: []string{
"tcp",
},
Encoding: msdsn.EncodeParameters{
Timezone: time.UTC,
GuidConversion: false,
},
DialTimeout: time.Second * 5,
ConnTimeout: time.Second * 10,
KeepAlive: time.Second * 30,
EpaMode: msdsn.EpaMode(*epa),
}

activityid, uerr := uuid.NewRandom()
if uerr == nil {
cfg.ActivityID = activityid[:]
}

workstation, err := os.Hostname()
if err == nil {
cfg.Workstation = workstation
}

connector := mssqldb.NewConnectorConfig(cfg)

_, err = connector.Connect(context.Background())
if err != nil {
fmt.Println("connector.Connect: ", err.Error())
return
}

db := sql.OpenDB(connector)
defer db.Close()

err = db.Ping()
if err != nil {
fmt.Println("Cannot connect: ", err.Error())
return
}
r := bufio.NewReader(os.Stdin)
for {
_, err = os.Stdout.Write([]byte("> "))
if err != nil {
fmt.Println(err)
return
}
cmd, err := r.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println()
return
}
fmt.Println(err)
return
}
err = exec(db, cmd)
if err != nil {
fmt.Println(err)
}
}
}
func exec(db *sql.DB, cmd string) error {
rows, err := db.Query(cmd)
if err != nil {
return err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return err
}
if cols == nil {
return nil
}
vals := make([]interface{}, len(cols))
for i := 0; i < len(cols); i++ {
vals[i] = new(interface{})
if i != 0 {
fmt.Print("\t")
}
fmt.Print(cols[i])
}
fmt.Println()
for rows.Next() {
err = rows.Scan(vals...)
if err != nil {
fmt.Println(err)
continue
}
for i := 0; i < len(vals); i++ {
if i != 0 {
fmt.Print("\t")
}
printValue(vals[i].(*interface{}))
}
fmt.Println()

}
if rows.Err() != nil {
return rows.Err()
}
return nil
}

func printValue(pval *interface{}) {
switch v := (*pval).(type) {
case nil:
fmt.Print("NULL")
case bool:
if v {
fmt.Print("1")
} else {
fmt.Print("0")
}
case []byte:
fmt.Print(string(v))
case time.Time:
fmt.Print(v.Format("2006-01-02 15:04:05.999"))
default:
fmt.Print(v)
}
}
1 change: 1 addition & 0 deletions integratedauth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type stubAuth struct {
func (s *stubAuth) InitialBytes() ([]byte, error) { return nil, nil }
func (s *stubAuth) NextBytes([]byte) ([]byte, error) { return nil, nil }
func (s *stubAuth) Free() {}
func (s *stubAuth) SetChannelBinding(*ChannelBindings) {}

func getAuth(config msdsn.Config) (IntegratedAuthenticator, error) {
return &stubAuth{config.User}, nil
Expand Down
174 changes: 174 additions & 0 deletions integratedauth/channel_binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package integratedauth

import (
"crypto/md5"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/binary"
"hash"
)

// gss_channel_bindings_struct: https://docs.oracle.com/cd/E19683-01/816-1331/overview-52/index.html
// gss_buffer_desc: https://docs.oracle.com/cd/E19683-01/816-1331/reference-21/index.html
type ChannelBindings struct {
InitiatorAddrType uint32
InitiatorAddress []byte
AcceptorAddrType uint32
AcceptorAddress []byte
ApplicationData []byte
}

// SEC_CHANNEL_BINDINGS: https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_channel_bindings
type SEC_CHANNEL_BINDINGS struct {
DwInitiatorAddrType uint32
CbInitiatorLength uint32
DwInitiatorOffset uint32
DwAcceptorAddrType uint32
CbAcceptorLength uint32
DwAcceptorOffset uint32
CbApplicationDataLength uint32
DwApplicationDataOffset uint32
Data []byte
}

// ToBytes converts a ChannelBindings struct to a byte slice as it would be gss_channel_bindings_struct structure in GSSAPI.
// Returns:
// - a byte slice
func (cb *ChannelBindings) ToBytes() []byte {
binarylength := 4 + 4 + 4 + 4 + 4 + uint32(len(cb.InitiatorAddress)+len(cb.AcceptorAddress)+len(cb.ApplicationData))
i := 0
bytes := make([]byte, binarylength)
binary.LittleEndian.PutUint32(bytes[i:i+4], cb.InitiatorAddrType)
i += 4
binary.LittleEndian.PutUint32(bytes[i:i+4], uint32(len(cb.InitiatorAddress)))
i += 4
if len(cb.InitiatorAddress) > 0 {
copy(bytes[i:i+len(cb.InitiatorAddress)], cb.InitiatorAddress)
i += len(cb.InitiatorAddress)
}
binary.LittleEndian.PutUint32(bytes[i:i+4], cb.AcceptorAddrType)
i += 4
binary.LittleEndian.PutUint32(bytes[i:i+4], uint32(len(cb.AcceptorAddress)))
i += 4
if len(cb.AcceptorAddress) > 0 {
copy(bytes[i:i+len(cb.AcceptorAddress)], cb.AcceptorAddress)
i += len(cb.AcceptorAddress)
}
binary.LittleEndian.PutUint32(bytes[i:i+4], uint32(len(cb.ApplicationData)))
i += 4
if len(cb.ApplicationData) > 0 {
copy(bytes[i:i+len(cb.ApplicationData)], cb.ApplicationData)
i += len(cb.ApplicationData)
}
// Print bytes in hexdump -C style for debugging
return bytes
}

// Md5Hash calculates the MD5 hash of the ChannelBindings struct
// Returns:
// - a byte slice
func (cb *ChannelBindings) Md5Hash() []byte {
hash := md5.New()
hash.Write(cb.ToBytes())
return hash.Sum(nil)
}

// AsSSPI_SEC_CHANNEL_BINDINGS converts a ChannelBindings struct to a SEC_CHANNEL_BINDINGS struct
// Returns:
// - a SEC_CHANNEL_BINDINGS struct
func (cb *ChannelBindings) AsSSPI_SEC_CHANNEL_BINDINGS() *SEC_CHANNEL_BINDINGS {
initiatorOffset := uint32(32)
acceptorOffset := initiatorOffset + uint32(len(cb.InitiatorAddress))
applicationDataOffset := acceptorOffset + uint32(len(cb.AcceptorAddress))
c := &SEC_CHANNEL_BINDINGS{
DwInitiatorAddrType: cb.InitiatorAddrType,
CbInitiatorLength: uint32(len(cb.InitiatorAddress)),
DwInitiatorOffset: initiatorOffset,
DwAcceptorAddrType: cb.AcceptorAddrType,
CbAcceptorLength: uint32(len(cb.AcceptorAddress)),
DwAcceptorOffset: acceptorOffset,
CbApplicationDataLength: uint32(len(cb.ApplicationData)),
DwApplicationDataOffset: applicationDataOffset,
}
data := make([]byte, c.CbInitiatorLength+c.CbAcceptorLength+c.CbApplicationDataLength)
var i uint32 = 0
if c.CbInitiatorLength > 0 {
copy(data[i:i+c.CbInitiatorLength], cb.InitiatorAddress)
i += c.CbInitiatorLength
}
if c.CbAcceptorLength > 0 {
copy(data[i:i+c.CbAcceptorLength], cb.AcceptorAddress)
i += c.CbAcceptorLength
}
if c.CbApplicationDataLength > 0 {
copy(data[i:i+c.CbApplicationDataLength], cb.ApplicationData)
i += c.CbApplicationDataLength
}
c.Data = data
return c
}

// ToBytes converts a SEC_CHANNEL_BINDINGS struct to a byte slice, that can be use in SSPI InitializeSecurityContext function.
// Returns:
// - a byte slice
func (cb *SEC_CHANNEL_BINDINGS) ToBytes() []byte {
bytes := make([]byte, 32+len(cb.Data))
binary.LittleEndian.PutUint32(bytes[0:4], cb.DwInitiatorAddrType)
binary.LittleEndian.PutUint32(bytes[4:8], cb.CbInitiatorLength)
binary.LittleEndian.PutUint32(bytes[8:12], cb.DwInitiatorOffset)
binary.LittleEndian.PutUint32(bytes[12:16], cb.DwAcceptorAddrType)
binary.LittleEndian.PutUint32(bytes[16:20], cb.CbAcceptorLength)
binary.LittleEndian.PutUint32(bytes[20:24], cb.DwAcceptorOffset)
binary.LittleEndian.PutUint32(bytes[24:28], cb.CbApplicationDataLength)
binary.LittleEndian.PutUint32(bytes[28:32], cb.DwApplicationDataOffset)
copy(bytes[32:32+len(cb.Data)], cb.Data)

return bytes
}

// GenerateCBTFromTLSUnique generates a ChannelBindings struct from a TLS unique value
// Adds tls-unique: prefix to the TLS unique value.
// Parameters:
// - tlsUnique: the TLS unique value
// Returns:
// - a ChannelBindings struct
func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings {
return &ChannelBindings{
InitiatorAddrType: 0,
InitiatorAddress: nil,
AcceptorAddrType: 0,
AcceptorAddress: nil,
ApplicationData: append([]byte("tls-unique:"), tlsUnique...),
}
}

// GenerateCBTFromServerCert generates a ChannelBindings struct from a server certificate
// Calculates the hash of the server certificate as described in 4.2 section of RFC5056.
// Parameters:
// - cert: the server certificate
// Returns:
// - a ChannelBindings struct
func GenerateCBTFromServerCert(cert *x509.Certificate) *ChannelBindings {
var certHash []byte
var h hash.Hash
switch cert.SignatureAlgorithm {
case x509.SHA256WithRSA, x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS:
h = sha256.New()
case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
h = sha512.New384()
case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS:
h = sha512.New()
default:
h = sha256.New()
}
h.Write(cert.Raw)
certHash = h.Sum(nil)
return &ChannelBindings{
InitiatorAddrType: 0,
InitiatorAddress: nil,
AcceptorAddrType: 0,
AcceptorAddress: nil,
ApplicationData: append([]byte("tls-server-end-point:"), certHash...),
}
}
1 change: 1 addition & 0 deletions integratedauth/integratedauthenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type IntegratedAuthenticator interface {
InitialBytes() ([]byte, error)
NextBytes([]byte) ([]byte, error)
Free()
SetChannelBinding(*ChannelBindings)
}

// ProviderFunc is an adapter to convert a GetIntegratedAuthenticator func into a Provider
Expand Down
5 changes: 5 additions & 0 deletions integratedauth/krb5/krb5.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ type krbAuth struct {
krb5Config *krb5Login
spnegoClient *spnego.SPNEGO
krb5Client *client.Client
channelBinding *integratedauth.ChannelBindings
}

func (k *krbAuth) SetChannelBinding(channelBinding *integratedauth.ChannelBindings) {
k.channelBinding = channelBinding
}

func (k *krbAuth) InitialBytes() ([]byte, error) {
Expand Down
Loading