From 2ddf7364e14c657dbce2b736d1870b98e15dd4d1 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:22:09 +0300 Subject: [PATCH 1/8] Implemented CBT for NTLM Signed-off-by: Bulat <67165594+paraddise@users.noreply.github.com> --- examples/channel_binding/tsql.go | 206 ++++++++++++++++++++++ integratedauth/auth_test.go | 1 + integratedauth/channel_binding.go | 28 +++ integratedauth/integratedauthenticator.go | 1 + integratedauth/krb5/krb5.go | 5 + integratedauth/ntlm/ntlm.go | 26 ++- msdsn/conn_str.go | 18 +- tds.go | 15 +- 8 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 examples/channel_binding/tsql.go create mode 100644 integratedauth/channel_binding.go diff --git a/examples/channel_binding/tsql.go b/examples/channel_binding/tsql.go new file mode 100644 index 00000000..df10735d --- /dev/null +++ b/examples/channel_binding/tsql.go @@ -0,0 +1,206 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "database/sql" + "flag" + "fmt" + "io" + "log" + "os" + "time" + + // mssqldb "github.com/denisenkom/go-mssqldb" + // "github.com/denisenkom/go-mssqldb/msdsn" + "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") + ) + 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 keyLogFile.Close() + + + 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": "/tmp/krb5cc_719880", + "krb5-configfile": "/etc/krb5.conf", + }, + 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, + + } + + // if *spn != "" { + // cfg.Parameters["authenticator"] = "krb5" + // // cfg.Parameters["krb5-credcachefile"] = "/tmp/krb5cc_719880" + // } + + activityid, uerr := uuid.NewRandom() + if uerr == nil { + cfg.ActivityID = activityid[:] + } + + workstation, err := os.Hostname() + if err == nil { + cfg.Workstation = workstation + } + + connector := mssqldb.NewConnectorConfig(cfg) + // dsn := "server=" + *server + ";user id=" + *userid + ";password=" + *password + ";database=" + *database + // connector,err = mssqldb.NewConnector(dsn) + // if err != nil { + // fmt.Println("failed to create connector: ", err.Error()) + // return + // } + + _, err = connector.Connect(context.Background()) + if err != nil { + fmt.Println("connector.Connect: ", err.Error()) + return + } + + db := sql.OpenDB(connector) + defer db.Close() + + // if err != nil { + // fmt.Println("Cannot connect: ", err.Error()) + // return + // } + 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) + } +} \ No newline at end of file diff --git a/integratedauth/auth_test.go b/integratedauth/auth_test.go index a3d25e30..1d5650b7 100644 --- a/integratedauth/auth_test.go +++ b/integratedauth/auth_test.go @@ -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([]byte) {} func getAuth(config msdsn.Config) (IntegratedAuthenticator, error) { return &stubAuth{config.User}, nil diff --git a/integratedauth/channel_binding.go b/integratedauth/channel_binding.go new file mode 100644 index 00000000..6b14ce7d --- /dev/null +++ b/integratedauth/channel_binding.go @@ -0,0 +1,28 @@ +package integratedauth + +import ( + "crypto/md5" + "encoding/binary" +) + +func GenerateCBTFromTLSUnique(tlsUnique []byte) []byte { + // Initialize the channel binding structure with empty addresses + // These fields are defined in the RFC but not used for TLS bindings + initiatorAddress := make([]byte, 8) + acceptorAddress := make([]byte, 8) + + // Create the application data with the "tls-unique:" prefix + applicationDataRaw := append([]byte("tls-unique:"), tlsUnique...) + + // Add the length prefix to the application data (little-endian 32-bit integer) + lenApplicationData := make([]byte, 4) + binary.LittleEndian.PutUint32(lenApplicationData, uint32(len(applicationDataRaw))) + applicationData := append(lenApplicationData, applicationDataRaw...) + + // Assemble the complete channel binding structure + channelBindingStruct := append(append(initiatorAddress, acceptorAddress...), applicationData...) + + // Return the MD5 hash of the structure + hash := md5.Sum(channelBindingStruct) + return hash[:] +} diff --git a/integratedauth/integratedauthenticator.go b/integratedauth/integratedauthenticator.go index ce8240d7..b9264b6d 100644 --- a/integratedauth/integratedauthenticator.go +++ b/integratedauth/integratedauthenticator.go @@ -15,6 +15,7 @@ type IntegratedAuthenticator interface { InitialBytes() ([]byte, error) NextBytes([]byte) ([]byte, error) Free() + SetChannelBinding([]byte) } // ProviderFunc is an adapter to convert a GetIntegratedAuthenticator func into a Provider diff --git a/integratedauth/krb5/krb5.go b/integratedauth/krb5/krb5.go index 2dbbd41c..b252a167 100644 --- a/integratedauth/krb5/krb5.go +++ b/integratedauth/krb5/krb5.go @@ -252,6 +252,11 @@ type krbAuth struct { krb5Config *krb5Login spnegoClient *spnego.SPNEGO krb5Client *client.Client + channelBinding []byte +} + +func (k *krbAuth) SetChannelBinding(channelBinding []byte) { + k.channelBinding = channelBinding } func (k *krbAuth) InitialBytes() ([]byte, error) { diff --git a/integratedauth/ntlm/ntlm.go b/integratedauth/ntlm/ntlm.go index d95032f2..42ea5338 100644 --- a/integratedauth/ntlm/ntlm.go +++ b/integratedauth/ntlm/ntlm.go @@ -57,11 +57,20 @@ const _NEGOTIATE_FLAGS = _NEGOTIATE_UNICODE | _NEGOTIATE_ALWAYS_SIGN | _NEGOTIATE_EXTENDED_SESSIONSECURITY +const ( + AV_PAIR_MsvAvChannelBindings = 0x000A +) + type Auth struct { Domain string UserName string Password string Workstation string + ChannelBinding []byte +} + +func (auth *Auth) SetChannelBinding(channelBinding []byte) { + auth.ChannelBinding = channelBinding } // getAuth returns an authentication handle Auth to provide authentication content @@ -76,6 +85,7 @@ func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error UserName: domainUser[1], Password: config.Password, Workstation: config.Workstation, + ChannelBinding: []byte{}, }, nil } @@ -243,7 +253,7 @@ func getNTLMv2AndLMv2ResponsePayloads(userDomain, username, password string, cha return } -func negotiateExtendedSessionSecurity(flags uint32, message []byte, challenge [8]byte, username, password, userDom string) (lm, nt []byte, err error) { +func negotiateExtendedSessionSecurity(flags uint32, message []byte, challenge [8]byte, username, password, userDom string, channelBinding []byte) (lm, nt []byte, err error) { nonce := clientChallenge() // Official specification: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 @@ -254,6 +264,18 @@ func negotiateExtendedSessionSecurity(flags uint32, message []byte, challenge [8 return lm, nt, err } + if len(channelBinding) > 0 { + av_pair_cb := make([]byte, 4) + // AvId + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/83f5e789-660d-4781-8491-5f8c6641f75e + binary.LittleEndian.PutUint16(av_pair_cb[0:2], AV_PAIR_MsvAvChannelBindings) + binary.LittleEndian.PutUint16(av_pair_cb[2:4], uint16(len(channelBinding))) + av_pair_cb = append(av_pair_cb, channelBinding...) + + targetInfoFields = append(targetInfoFields[:len(targetInfoFields)-4], av_pair_cb...) + targetInfoFields = append(targetInfoFields, 0,0,0,0) + } + nt, lm = getNTLMv2AndLMv2ResponsePayloads(userDom, username, password, challenge, nonce, targetInfoFields, time.Now()) return lm, nt, nil @@ -376,7 +398,7 @@ func (auth *Auth) NextBytes(bytes []byte) ([]byte, error) { copy(challenge[:], bytes[24:32]) flags := binary.LittleEndian.Uint32(bytes[20:24]) if (flags & _NEGOTIATE_EXTENDED_SESSIONSECURITY) != 0 { - lm, nt, err := negotiateExtendedSessionSecurity(flags, bytes, challenge, auth.UserName, auth.Password, auth.Domain) + lm, nt, err := negotiateExtendedSessionSecurity(flags, bytes, challenge, auth.UserName, auth.Password, auth.Domain, auth.ChannelBinding) if err != nil { return nil, err } diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index 467e279d..20a0f7ba 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -86,6 +86,7 @@ const ( NoTraceID = "notraceid" GuidConversion = "guid conversion" Timezone = "timezone" + DisableEPA = "disableepa" ) type EncodeParameters struct { @@ -103,6 +104,7 @@ func (e EncodeParameters) GetTimezone() *time.Location { } type Config struct { + DisableEPA bool Port uint64 Host string Instance string @@ -569,6 +571,14 @@ func Parse(dsn string) (Config, error) { p.Encoding.GuidConversion = false } + disableEPA, ok := params[DisableEPA] + if ok { + p.DisableEPA, err = strconv.ParseBool(disableEPA) + if err != nil { + return p, fmt.Errorf("invalid disableEPA '%s': %s", disableEPA, err.Error()) + } + } + return p, nil } @@ -711,11 +721,11 @@ func splitAdoConnectionStringParts(dsn string) []string { var parts []string var current strings.Builder inQuotes := false - + runes := []rune(dsn) for i := 0; i < len(runes); i++ { char := runes[i] - + if char == '"' { if inQuotes && i+1 < len(runes) && runes[i+1] == '"' { // Double quote escape sequence - add both quotes to current part @@ -735,12 +745,12 @@ func splitAdoConnectionStringParts(dsn string) []string { current.WriteRune(char) } } - + // Add the last part if it's not empty if current.Len() > 0 { parts = append(parts, current.String()) } - + return parts } diff --git a/tds.go b/tds.go index aaedaf71..00835942 100644 --- a/tds.go +++ b/tds.go @@ -1129,6 +1129,7 @@ func getTLSConn(conn *timeoutConn, p msdsn.Config, alpnSeq string) (tlsConn *tls } func connect(ctx context.Context, c *Connector, logger ContextLogger, p msdsn.Config) (res *tdsSession, err error) { + cbt := []byte{} isTransportEncrypted := false // if instance is specified use instance resolution service if len(p.Instance) > 0 && p.Port != 0 && uint64(p.LogFlags)&logDebug != 0 { @@ -1253,8 +1254,16 @@ initiate_connection: outbuf.transport = toconn } } - } + if !p.DisableEPA && outbuf.transport != nil { + if tlsConn, ok := outbuf.transport.(*tls.Conn); ok { + state := tlsConn.ConnectionState() + if len(state.TLSUnique) > 0 { + cbt = integratedauth.GenerateCBTFromTLSUnique(state.TLSUnique) + } + } + } + } } auth, err := integratedauth.GetIntegratedAuthenticator(p) @@ -1268,8 +1277,12 @@ initiate_connection: if auth != nil { defer auth.Free() + if len(cbt) > 0 { + auth.SetChannelBinding(cbt) + } } + login, err := prepareLogin(ctx, c, p, logger, auth, fedAuth, uint32(outbuf.PackageSize())) if err != nil { return nil, err From 1cc4d425ae244bd60ae0157d704921db89b8c5e1 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:05:27 +0300 Subject: [PATCH 2/8] add method to winsspi Signed-off-by: Bulat <67165594+paraddise@users.noreply.github.com> --- integratedauth/winsspi/winsspi.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integratedauth/winsspi/winsspi.go b/integratedauth/winsspi/winsspi.go index 195d2288..2ec4988a 100644 --- a/integratedauth/winsspi/winsspi.go +++ b/integratedauth/winsspi/winsspi.go @@ -118,6 +118,8 @@ type Auth struct { ctxt SecHandle } +func (auth *Auth) SetChannelBinding(channelBinding []byte) {} + // getAuth returns an authentication handle Auth to provide authentication content // to mssql.connect func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error) { From 5ac4273da67deb8ecb0e295456255e995c2ff515 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:21:22 +0300 Subject: [PATCH 3/8] Add implementation for SSPI --- examples/channel_binding/tsql.go | 3 +- integratedauth/auth_test.go | 2 +- integratedauth/channel_binding.go | 154 +++++++++++++++++++--- integratedauth/integratedauthenticator.go | 2 +- integratedauth/krb5/krb5.go | 4 +- integratedauth/ntlm/ntlm.go | 4 +- integratedauth/winsspi/winsspi.go | 48 +++++-- msdsn/conn_str.go | 29 +++- tds.go | 19 ++- 9 files changed, 219 insertions(+), 46 deletions(-) diff --git a/examples/channel_binding/tsql.go b/examples/channel_binding/tsql.go index df10735d..95cfd22f 100644 --- a/examples/channel_binding/tsql.go +++ b/examples/channel_binding/tsql.go @@ -30,6 +30,7 @@ func main() { 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() @@ -77,7 +78,7 @@ func main() { DialTimeout: time.Second * 5, ConnTimeout: time.Second * 10, KeepAlive: time.Second * 30, - + EpaMode: msdsn.EpaMode(*epa), } // if *spn != "" { diff --git a/integratedauth/auth_test.go b/integratedauth/auth_test.go index 1d5650b7..265628c4 100644 --- a/integratedauth/auth_test.go +++ b/integratedauth/auth_test.go @@ -17,7 +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([]byte) {} +func (s *stubAuth) SetChannelBinding(*ChannelBindings) {} func getAuth(config msdsn.Config) (IntegratedAuthenticator, error) { return &stubAuth{config.User}, nil diff --git a/integratedauth/channel_binding.go b/integratedauth/channel_binding.go index 6b14ce7d..384145a0 100644 --- a/integratedauth/channel_binding.go +++ b/integratedauth/channel_binding.go @@ -2,27 +2,149 @@ package integratedauth import ( "crypto/md5" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" "encoding/binary" + "hash" ) -func GenerateCBTFromTLSUnique(tlsUnique []byte) []byte { - // Initialize the channel binding structure with empty addresses - // These fields are defined in the RFC but not used for TLS bindings - initiatorAddress := make([]byte, 8) - acceptorAddress := make([]byte, 8) +// 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 +} - // Create the application data with the "tls-unique:" prefix - applicationDataRaw := append([]byte("tls-unique:"), tlsUnique...) +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 +} - // Add the length prefix to the application data (little-endian 32-bit integer) - lenApplicationData := make([]byte, 4) - binary.LittleEndian.PutUint32(lenApplicationData, uint32(len(applicationDataRaw))) - applicationData := append(lenApplicationData, applicationDataRaw...) +func (cb *ChannelBindings) Md5Hash() []byte { + hash := md5.New() + hash.Write(cb.ToBytes()) + return hash.Sum(nil) +} - // Assemble the complete channel binding structure - channelBindingStruct := append(append(initiatorAddress, acceptorAddress...), applicationData...) +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 +} + +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 +} + +func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { + return &ChannelBindings{ + InitiatorAddrType: 0, + InitiatorAddress: nil, + AcceptorAddrType: 0, + AcceptorAddress: nil, + ApplicationData: append([]byte("tls-unique:"), tlsUnique...), + } +} - // Return the MD5 hash of the structure - hash := md5.Sum(channelBindingStruct) - return hash[:] +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...), + } } diff --git a/integratedauth/integratedauthenticator.go b/integratedauth/integratedauthenticator.go index b9264b6d..4f3f38ad 100644 --- a/integratedauth/integratedauthenticator.go +++ b/integratedauth/integratedauthenticator.go @@ -15,7 +15,7 @@ type IntegratedAuthenticator interface { InitialBytes() ([]byte, error) NextBytes([]byte) ([]byte, error) Free() - SetChannelBinding([]byte) + SetChannelBinding(*ChannelBindings) } // ProviderFunc is an adapter to convert a GetIntegratedAuthenticator func into a Provider diff --git a/integratedauth/krb5/krb5.go b/integratedauth/krb5/krb5.go index b252a167..3cf65b6e 100644 --- a/integratedauth/krb5/krb5.go +++ b/integratedauth/krb5/krb5.go @@ -252,10 +252,10 @@ type krbAuth struct { krb5Config *krb5Login spnegoClient *spnego.SPNEGO krb5Client *client.Client - channelBinding []byte + channelBinding *integratedauth.ChannelBindings } -func (k *krbAuth) SetChannelBinding(channelBinding []byte) { +func (k *krbAuth) SetChannelBinding(channelBinding *integratedauth.ChannelBindings) { k.channelBinding = channelBinding } diff --git a/integratedauth/ntlm/ntlm.go b/integratedauth/ntlm/ntlm.go index 42ea5338..a407ee3c 100644 --- a/integratedauth/ntlm/ntlm.go +++ b/integratedauth/ntlm/ntlm.go @@ -69,8 +69,8 @@ type Auth struct { ChannelBinding []byte } -func (auth *Auth) SetChannelBinding(channelBinding []byte) { - auth.ChannelBinding = channelBinding +func (auth *Auth) SetChannelBinding(channelBinding *integratedauth.ChannelBindings) { + auth.ChannelBinding = channelBinding.Md5Hash() } // getAuth returns an authentication handle Auth to provide authentication content diff --git a/integratedauth/winsspi/winsspi.go b/integratedauth/winsspi/winsspi.go index 2ec4988a..0e49efac 100644 --- a/integratedauth/winsspi/winsspi.go +++ b/integratedauth/winsspi/winsspi.go @@ -26,6 +26,7 @@ func init() { const ( SEC_E_OK = 0 SECPKG_CRED_OUTBOUND = 2 + SECPKG_ATTR_UNIQUE_BINDINGS = 25 SEC_WINNT_AUTH_IDENTITY_UNICODE = 2 ISC_REQ_DELEGATE = 0x00000001 ISC_REQ_REPLAY_DETECT = 0x00000004 @@ -38,6 +39,7 @@ const ( SEC_I_COMPLETE_AND_CONTINUE = 0x00090314 SECBUFFER_VERSION = 0 SECBUFFER_TOKEN = 2 + SECBUFFER_CHANNEL_BINDINGS = 14 NTLMBUF_LEN = 12000 ) @@ -110,15 +112,23 @@ type SecBufferDesc struct { } type Auth struct { - Domain string - UserName string - Password string - Service string - cred SecHandle - ctxt SecHandle + Domain string + UserName string + Password string + Service string + cred SecHandle + ctxt SecHandle + channelBinding *integratedauth.SEC_CHANNEL_BINDINGS } -func (auth *Auth) SetChannelBinding(channelBinding []byte) {} +type SecPkgContext_Bindings struct { + BindingsLength uint64 + Bindings *byte +} + +func (auth *Auth) SetChannelBinding(channelBinding *integratedauth.ChannelBindings) { + auth.channelBinding = channelBinding.AsSSPI_SEC_CHANNEL_BINDINGS() +} // getAuth returns an authentication handle Auth to provide authentication content // to mssql.connect @@ -135,6 +145,7 @@ func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error UserName: domainUser[1], Password: config.Password, Service: config.ServerSPN, + channelBinding: nil, }, nil } @@ -214,18 +225,33 @@ func (auth *Auth) InitialBytes() ([]byte, error) { func (auth *Auth) NextBytes(bytes []byte) ([]byte, error) { var in_buf, out_buf SecBuffer var in_desc, out_desc SecBufferDesc - - in_desc.ulVersion = SECBUFFER_VERSION - in_desc.cBuffers = 1 - in_desc.pBuffers = &in_buf + // Use fixed-size array instead of slice to ensure memory stability + var in_desc_buffers [2]SecBuffer + bufferCount := 0 out_desc.ulVersion = SECBUFFER_VERSION out_desc.cBuffers = 1 out_desc.pBuffers = &out_buf + // First buffer: input token in_buf.BufferType = SECBUFFER_TOKEN in_buf.pvBuffer = &bytes[0] in_buf.cbBuffer = uint32(len(bytes)) + in_desc_buffers[bufferCount] = in_buf + bufferCount++ + + // Second buffer: channel bindings (if present) + if auth.channelBinding != nil { + channelBindingBytes := auth.channelBinding.ToBytes() + in_desc_buffers[bufferCount].BufferType = SECBUFFER_CHANNEL_BINDINGS + in_desc_buffers[bufferCount].pvBuffer = &channelBindingBytes[0] + in_desc_buffers[bufferCount].cbBuffer = uint32(len(channelBindingBytes)) + bufferCount++ + } + + in_desc.ulVersion = SECBUFFER_VERSION + in_desc.cBuffers = uint32(bufferCount) + in_desc.pBuffers = &in_desc_buffers[0] outbuf := make([]byte, NTLMBUF_LEN) out_buf.BufferType = SECBUFFER_TOKEN diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index 20a0f7ba..fa3dcdcf 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -22,6 +22,7 @@ type ( Encryption int Log uint64 BrowserMsg byte + EpaMode string ) const ( @@ -56,6 +57,12 @@ const ( BrowserDAC BrowserMsg = 0x0f ) +const ( + EpaOff EpaMode = "off" + EpaTlsUnique EpaMode = "tls-unique" + EpaTlsServerEndPoint EpaMode = "tls-server-end-point" +) + const ( Database = "database" Encrypt = "encrypt" @@ -86,7 +93,7 @@ const ( NoTraceID = "notraceid" GuidConversion = "guid conversion" Timezone = "timezone" - DisableEPA = "disableepa" + EPA = "epa" ) type EncodeParameters struct { @@ -104,7 +111,7 @@ func (e EncodeParameters) GetTimezone() *time.Location { } type Config struct { - DisableEPA bool + EpaMode EpaMode Port uint64 Host string Instance string @@ -571,11 +578,19 @@ func Parse(dsn string) (Config, error) { p.Encoding.GuidConversion = false } - disableEPA, ok := params[DisableEPA] - if ok { - p.DisableEPA, err = strconv.ParseBool(disableEPA) - if err != nil { - return p, fmt.Errorf("invalid disableEPA '%s': %s", disableEPA, err.Error()) + epa, ok := params[EPA] + if !ok { + p.EpaMode = EpaOff + } else { + switch EpaMode(strings.ToLower(epa)) { + case EpaOff: + p.EpaMode = EpaOff + case EpaTlsUnique: + p.EpaMode = EpaTlsUnique + case EpaTlsServerEndPoint: + p.EpaMode = EpaTlsServerEndPoint + default: + return p, fmt.Errorf("invalid EPA mode '%s'", epa) } } diff --git a/tds.go b/tds.go index 00835942..3d3ee60d 100644 --- a/tds.go +++ b/tds.go @@ -1129,7 +1129,7 @@ func getTLSConn(conn *timeoutConn, p msdsn.Config, alpnSeq string) (tlsConn *tls } func connect(ctx context.Context, c *Connector, logger ContextLogger, p msdsn.Config) (res *tdsSession, err error) { - cbt := []byte{} + var cbt *integratedauth.ChannelBindings isTransportEncrypted := false // if instance is specified use instance resolution service if len(p.Instance) > 0 && p.Port != 0 && uint64(p.LogFlags)&logDebug != 0 { @@ -1255,11 +1255,20 @@ initiate_connection: } } - if !p.DisableEPA && outbuf.transport != nil { + if outbuf.transport != nil && p.EpaMode != msdsn.EpaOff { if tlsConn, ok := outbuf.transport.(*tls.Conn); ok { state := tlsConn.ConnectionState() - if len(state.TLSUnique) > 0 { - cbt = integratedauth.GenerateCBTFromTLSUnique(state.TLSUnique) + switch p.EpaMode { + case msdsn.EpaTlsUnique: + if len(state.TLSUnique) > 0 { + cbt = integratedauth.GenerateCBTFromTLSUnique(state.TLSUnique) + } + case msdsn.EpaTlsServerEndPoint: + if len(state.PeerCertificates) > 0 { + cbt = integratedauth.GenerateCBTFromServerCert(state.PeerCertificates[0]) + } + default: + break } } } @@ -1277,7 +1286,7 @@ initiate_connection: if auth != nil { defer auth.Free() - if len(cbt) > 0 { + if cbt != nil { auth.SetChannelBinding(cbt) } } From 645d60e318f072f510dcc610630a7b2ed3274347 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:35:23 +0300 Subject: [PATCH 4/8] Fix issues --- examples/channel_binding/tsql.go | 31 ++++++++----------------------- integratedauth/channel_binding.go | 24 ++++++++++++++++++++++++ integratedauth/ntlm/ntlm.go | 18 +++++++++--------- msdsn/conn_str.go | 5 +++-- tds.go | 27 ++++++++++++--------------- 5 files changed, 56 insertions(+), 49 deletions(-) diff --git a/examples/channel_binding/tsql.go b/examples/channel_binding/tsql.go index 95cfd22f..d1809ac6 100644 --- a/examples/channel_binding/tsql.go +++ b/examples/channel_binding/tsql.go @@ -12,8 +12,6 @@ import ( "os" "time" - // mssqldb "github.com/denisenkom/go-mssqldb" - // "github.com/denisenkom/go-mssqldb/msdsn" "github.com/google/uuid" mssqldb "github.com/microsoft/go-mssqldb" "github.com/microsoft/go-mssqldb/msdsn" @@ -38,7 +36,11 @@ func main() { if err != nil { log.Fatal("failed to open keylog file:", err) } - defer keyLogFile.Close() + defer func() { + if cerr := keyLogFile.Close(); cerr != nil { + log.Printf("warning: failed to close keylog file: %v", cerr) + } + }() cfg := msdsn.Config{ @@ -62,12 +64,10 @@ func main() { Parameters: map[string]string{ "authenticator": *auth, - "krb5-credcachefile": "/tmp/krb5cc_719880", - "krb5-configfile": "/etc/krb5.conf", - }, - ProtocolParameters: map[string]interface{}{ - + "krb5-credcachefile": os.Getenv("KRB5_CCNAME"), + "krb5-configfile": os.Getenv("KRB5_CONFIG"), }, + ProtocolParameters: map[string]interface{}{}, Protocols: []string{ "tcp", }, @@ -81,11 +81,6 @@ func main() { EpaMode: msdsn.EpaMode(*epa), } - // if *spn != "" { - // cfg.Parameters["authenticator"] = "krb5" - // // cfg.Parameters["krb5-credcachefile"] = "/tmp/krb5cc_719880" - // } - activityid, uerr := uuid.NewRandom() if uerr == nil { cfg.ActivityID = activityid[:] @@ -97,12 +92,6 @@ func main() { } connector := mssqldb.NewConnectorConfig(cfg) - // dsn := "server=" + *server + ";user id=" + *userid + ";password=" + *password + ";database=" + *database - // connector,err = mssqldb.NewConnector(dsn) - // if err != nil { - // fmt.Println("failed to create connector: ", err.Error()) - // return - // } _, err = connector.Connect(context.Background()) if err != nil { @@ -113,10 +102,6 @@ func main() { db := sql.OpenDB(connector) defer db.Close() - // if err != nil { - // fmt.Println("Cannot connect: ", err.Error()) - // return - // } err = db.Ping() if err != nil { fmt.Println("Cannot connect: ", err.Error()) diff --git a/integratedauth/channel_binding.go b/integratedauth/channel_binding.go index 384145a0..cd52890f 100644 --- a/integratedauth/channel_binding.go +++ b/integratedauth/channel_binding.go @@ -32,6 +32,9 @@ type SEC_CHANNEL_BINDINGS struct { 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 @@ -62,12 +65,18 @@ func (cb *ChannelBindings) ToBytes() []byte { 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)) @@ -100,6 +109,9 @@ func (cb *ChannelBindings) AsSSPI_SEC_CHANNEL_BINDINGS() *SEC_CHANNEL_BINDINGS { 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) @@ -115,6 +127,12 @@ func (cb *SEC_CHANNEL_BINDINGS) ToBytes() []byte { 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, @@ -125,6 +143,12 @@ func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { } } +// 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 diff --git a/integratedauth/ntlm/ntlm.go b/integratedauth/ntlm/ntlm.go index a407ee3c..2c7a35ec 100644 --- a/integratedauth/ntlm/ntlm.go +++ b/integratedauth/ntlm/ntlm.go @@ -62,10 +62,10 @@ const ( ) type Auth struct { - Domain string - UserName string - Password string - Workstation string + Domain string + UserName string + Password string + Workstation string ChannelBinding []byte } @@ -81,10 +81,10 @@ func getAuth(config msdsn.Config) (integratedauth.IntegratedAuthenticator, error } domainUser := strings.SplitN(config.User, "\\", 2) return &Auth{ - Domain: domainUser[0], - UserName: domainUser[1], - Password: config.Password, - Workstation: config.Workstation, + Domain: domainUser[0], + UserName: domainUser[1], + Password: config.Password, + Workstation: config.Workstation, ChannelBinding: []byte{}, }, nil } @@ -273,7 +273,7 @@ func negotiateExtendedSessionSecurity(flags uint32, message []byte, challenge [8 av_pair_cb = append(av_pair_cb, channelBinding...) targetInfoFields = append(targetInfoFields[:len(targetInfoFields)-4], av_pair_cb...) - targetInfoFields = append(targetInfoFields, 0,0,0,0) + targetInfoFields = append(targetInfoFields, 0, 0, 0, 0) } nt, lm = getNTLMv2AndLMv2ResponsePayloads(userDom, username, password, challenge, nonce, targetInfoFields, time.Now()) diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index fa3dcdcf..6ef78f93 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -93,7 +93,7 @@ const ( NoTraceID = "notraceid" GuidConversion = "guid conversion" Timezone = "timezone" - EPA = "epa" + EPA = "epa" ) type EncodeParameters struct { @@ -111,7 +111,6 @@ func (e EncodeParameters) GetTimezone() *time.Location { } type Config struct { - EpaMode EpaMode Port uint64 Host string Instance string @@ -168,6 +167,8 @@ type Config struct { NoTraceID bool // Parameters related to type encoding Encoding EncodeParameters + // EPA mode determines how the Channel Bindings are calculated. + EpaMode EpaMode } func readDERFile(filename string) ([]byte, error) { diff --git a/tds.go b/tds.go index 3d3ee60d..fb35e3be 100644 --- a/tds.go +++ b/tds.go @@ -1255,21 +1255,19 @@ initiate_connection: } } - if outbuf.transport != nil && p.EpaMode != msdsn.EpaOff { - if tlsConn, ok := outbuf.transport.(*tls.Conn); ok { - state := tlsConn.ConnectionState() - switch p.EpaMode { - case msdsn.EpaTlsUnique: - if len(state.TLSUnique) > 0 { - cbt = integratedauth.GenerateCBTFromTLSUnique(state.TLSUnique) - } - case msdsn.EpaTlsServerEndPoint: - if len(state.PeerCertificates) > 0 { - cbt = integratedauth.GenerateCBTFromServerCert(state.PeerCertificates[0]) - } - default: - break + if p.EpaMode != msdsn.EpaOff { + state := tlsConn.ConnectionState() + switch p.EpaMode { + case msdsn.EpaTlsUnique: + if len(state.TLSUnique) > 0 { + cbt = integratedauth.GenerateCBTFromTLSUnique(state.TLSUnique) } + case msdsn.EpaTlsServerEndPoint: + if len(state.PeerCertificates) > 0 { + cbt = integratedauth.GenerateCBTFromServerCert(state.PeerCertificates[0]) + } + default: + break } } } @@ -1291,7 +1289,6 @@ initiate_connection: } } - login, err := prepareLogin(ctx, c, p, logger, auth, fedAuth, uint32(outbuf.PackageSize())) if err != nil { return nil, err From 0e339c3016535d632f0163499701b67ace2f6427 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:38:57 +0300 Subject: [PATCH 5/8] Add ability use EPA from Env --- msdsn/conn_str.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index 6ef78f93..25a8ae7b 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -110,6 +110,21 @@ func (e EncodeParameters) GetTimezone() *time.Location { return e.Timezone } +// EpaModeFromString creates an EpaMode from a string value, case-insensitive. +// Returns EpaOff if the value does not match a known mode. +func EpaModeFromString(s string) EpaMode { + switch strings.ToLower(s) { + case string(EpaOff): + return EpaOff + case string(EpaTlsUnique): + return EpaTlsUnique + case string(EpaTlsServerEndPoint): + return EpaTlsServerEndPoint + default: + return EpaOff + } +} + type Config struct { Port uint64 Host string @@ -581,18 +596,14 @@ func Parse(dsn string) (Config, error) { epa, ok := params[EPA] if !ok { - p.EpaMode = EpaOff - } else { - switch EpaMode(strings.ToLower(epa)) { - case EpaOff: + epa = os.Getenv("MSSQL_USE_EPA") + if epa != "" { + p.EpaMode = EpaModeFromString(epa) + } else { p.EpaMode = EpaOff - case EpaTlsUnique: - p.EpaMode = EpaTlsUnique - case EpaTlsServerEndPoint: - p.EpaMode = EpaTlsServerEndPoint - default: - return p, fmt.Errorf("invalid EPA mode '%s'", epa) } + } else { + p.EpaMode = EpaModeFromString(epa) } return p, nil From 8699ca8556497b495afca7908af796c97673ae98 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:05:26 +0300 Subject: [PATCH 6/8] Add channel binding for TLS 1.3 --- examples/channel_binding/tsql.go | 47 +++++++++++++++--- integratedauth/channel_binding.go | 80 ++++++++++++++++++++++++++----- msdsn/conn_str.go | 44 +++++------------ tds.go | 22 +++------ 4 files changed, 128 insertions(+), 65 deletions(-) diff --git a/examples/channel_binding/tsql.go b/examples/channel_binding/tsql.go index d1809ac6..639c7c48 100644 --- a/examples/channel_binding/tsql.go +++ b/examples/channel_binding/tsql.go @@ -27,8 +27,12 @@ func main() { 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") + auth = flag.String("a", "ntlm", "Authentication method: ntlm, krb5 or winsspi") + epa = flag.Bool("epa", true, "EPA enabled: true, false") + encrypt = flag.String("e", "required", "encrypt mode: required, disabled, strict, optional") + query = flag.String("q", "", "query to execute") + tlsMinVersion = flag.String("tlsmin", "1.1", "TLS minimum version: 1.0, 1.1, 1.2, 1.3") + tlsMaxVersion = flag.String("tlsmax", "1.3", "TLS maximum version: 1.0, 1.1, 1.2, 1.3") ) flag.Parse() @@ -42,7 +46,13 @@ func main() { } }() + encryption, err := parseEncrypt(*encrypt) + if err != nil { + log.Fatal("failed to parse encrypt: ", err) + } + tlsMinVersionNum := msdsn.TLSVersionFromString(*tlsMinVersion) + tlsMaxVersionNum := msdsn.TLSVersionFromString(*tlsMaxVersion) cfg := msdsn.Config{ User: *userid, Database: *database, @@ -57,11 +67,10 @@ func main() { ServerName: *server, KeyLogWriter: keyLogFile, DynamicRecordSizingDisabled: true, - MinVersion: tls.VersionTLS11, - MaxVersion: tls.VersionTLS12, + MinVersion: tlsMinVersionNum, + MaxVersion: tlsMaxVersionNum, }, - Encryption: msdsn.EncryptionRequired, - + Encryption: encryption, Parameters: map[string]string{ "authenticator": *auth, "krb5-credcachefile": os.Getenv("KRB5_CCNAME"), @@ -78,7 +87,7 @@ func main() { DialTimeout: time.Second * 5, ConnTimeout: time.Second * 10, KeepAlive: time.Second * 30, - EpaMode: msdsn.EpaMode(*epa), + EpaEnabled: *epa, } activityid, uerr := uuid.NewRandom() @@ -107,6 +116,15 @@ func main() { fmt.Println("Cannot connect: ", err.Error()) return } + + if *query != "" { + err = exec(db, *query) + if err != nil { + fmt.Println(err) + } + return + } + r := bufio.NewReader(os.Stdin) for { _, err = os.Stdout.Write([]byte("> ")) @@ -189,4 +207,19 @@ func printValue(pval *interface{}) { default: fmt.Print(v) } +} + +func parseEncrypt(encrypt string) (msdsn.Encryption, error) { + switch encrypt { + case "required", "yes", "1", "t", "true", "": + return msdsn.EncryptionRequired, nil + case "disabled": + return msdsn.EncryptionDisabled, nil + case "strict": + return msdsn.EncryptionStrict, nil + case "optional", "no", "0", "f", "false": + return msdsn.EncryptionOff, nil + default: + return msdsn.EncryptionOff, fmt.Errorf("invalid encrypt '%s'", encrypt) + } } \ No newline at end of file diff --git a/integratedauth/channel_binding.go b/integratedauth/channel_binding.go index cd52890f..6151c834 100644 --- a/integratedauth/channel_binding.go +++ b/integratedauth/channel_binding.go @@ -1,12 +1,22 @@ package integratedauth import ( + "crypto" "crypto/md5" - "crypto/sha256" - "crypto/sha512" + "crypto/tls" "crypto/x509" "encoding/binary" - "hash" + "fmt" +) + +const ( + // https://datatracker.ietf.org/doc/rfc9266/ + TLS_EXPORTER_PREFIX = "tls-exporter:" + TLS_EXPORTER_EKM_LABEL = "EXPORTER-Channel-Binding" + TLS_EXPORTER_EKM_LENGTH = 32 + // https://www.rfc-editor.org/rfc/rfc5801.html#section-5.2 + TLS_UNIQUE_PREFIX = "tls-unique:" + TLS_SERVER_END_POINT_PREFIX = "tls-server-end-point:" ) // gss_channel_bindings_struct: https://docs.oracle.com/cd/E19683-01/816-1331/overview-52/index.html @@ -134,12 +144,56 @@ func (cb *SEC_CHANNEL_BINDINGS) ToBytes() []byte { // Returns: // - a ChannelBindings struct func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { + if len(tlsUnique) == 0 { + return nil + } + return &ChannelBindings{ + InitiatorAddrType: 0, + InitiatorAddress: nil, + AcceptorAddrType: 0, + AcceptorAddress: nil, + ApplicationData: append([]byte(TLS_UNIQUE_PREFIX), tlsUnique...), + } +} + +// GenerateCBTFromTLSConnState generates a ChannelBindings struct from a TLS connection state +// If the TLS version is TLS 1.3, it generates a ChannelBindings struct from the TLS exporter key. +// If the TLS version is not TLS 1.3, it generates a ChannelBindings struct from the TLS unique value. +// Parameters: +// - state: the TLS connection state +// Returns: +// - a ChannelBindings struct +func GenerateCBTFromTLSConnState(state tls.ConnectionState) *ChannelBindings { + switch state.Version { + case tls.VersionTLS13: + exporterKey, err := state.ExportKeyingMaterial(TLS_EXPORTER_EKM_LABEL, nil, TLS_EXPORTER_EKM_LENGTH) + if err != nil { + fmt.Println("GenerateCBTFromTLSExporter: error: ", err) + return nil + } + return GenerateCBTFromTLSExporter(exporterKey) + default: + return GenerateCBTFromTLSUnique(state.TLSUnique) + } +} + +// GenerateCBTFromTLSExporter generates a ChannelBindings struct from a TLS exporter key +// Parameters: +// - exporterKey: the TLS exporter key +// Returns: +// - a ChannelBindings struct +func GenerateCBTFromTLSExporter(exporterKey []byte) *ChannelBindings { + fmt.Println("GenerateCBTFromTLSExporter: exporterKey: ", exporterKey) + if len(exporterKey) == 0 { + return nil + } + return &ChannelBindings{ InitiatorAddrType: 0, InitiatorAddress: nil, AcceptorAddrType: 0, AcceptorAddress: nil, - ApplicationData: append([]byte("tls-unique:"), tlsUnique...), + ApplicationData: append([]byte(TLS_EXPORTER_PREFIX), exporterKey...), } } @@ -150,25 +204,29 @@ func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { // Returns: // - a ChannelBindings struct func GenerateCBTFromServerCert(cert *x509.Certificate) *ChannelBindings { + if cert == nil { + return nil + } var certHash []byte - var h hash.Hash + var hashType crypto.Hash switch cert.SignatureAlgorithm { case x509.SHA256WithRSA, x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS: - h = sha256.New() + hashType = crypto.SHA256 case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS: - h = sha512.New384() + hashType = crypto.SHA384 case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS: - h = sha512.New() + hashType = crypto.SHA512 default: - h = sha256.New() + hashType = crypto.SHA256 } - h.Write(cert.Raw) + h := hashType.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...), + ApplicationData: append([]byte(TLS_SERVER_END_POINT_PREFIX), certHash...), } } diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index 25a8ae7b..f5570d16 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -57,12 +57,6 @@ const ( BrowserDAC BrowserMsg = 0x0f ) -const ( - EpaOff EpaMode = "off" - EpaTlsUnique EpaMode = "tls-unique" - EpaTlsServerEndPoint EpaMode = "tls-server-end-point" -) - const ( Database = "database" Encrypt = "encrypt" @@ -93,7 +87,7 @@ const ( NoTraceID = "notraceid" GuidConversion = "guid conversion" Timezone = "timezone" - EPA = "epa" + EpaEnabled = "epa enabled" ) type EncodeParameters struct { @@ -110,21 +104,6 @@ func (e EncodeParameters) GetTimezone() *time.Location { return e.Timezone } -// EpaModeFromString creates an EpaMode from a string value, case-insensitive. -// Returns EpaOff if the value does not match a known mode. -func EpaModeFromString(s string) EpaMode { - switch strings.ToLower(s) { - case string(EpaOff): - return EpaOff - case string(EpaTlsUnique): - return EpaTlsUnique - case string(EpaTlsServerEndPoint): - return EpaTlsServerEndPoint - default: - return EpaOff - } -} - type Config struct { Port uint64 Host string @@ -183,7 +162,7 @@ type Config struct { // Parameters related to type encoding Encoding EncodeParameters // EPA mode determines how the Channel Bindings are calculated. - EpaMode EpaMode + EpaEnabled bool } func readDERFile(filename string) ([]byte, error) { @@ -594,16 +573,17 @@ func Parse(dsn string) (Config, error) { p.Encoding.GuidConversion = false } - epa, ok := params[EPA] + epaString, ok := params[EpaEnabled] if !ok { - epa = os.Getenv("MSSQL_USE_EPA") - if epa != "" { - p.EpaMode = EpaModeFromString(epa) - } else { - p.EpaMode = EpaOff - } - } else { - p.EpaMode = EpaModeFromString(epa) + epaString = os.Getenv("MSSQL_USE_EPA") + } + switch strings.ToLower(epaString) { + case "true", "1", "enabled", "yes", "y": + p.EpaEnabled = true + case "false", "0", "disabled", "no", "n": + p.EpaEnabled = false + default: + return p, fmt.Errorf("invalid epa enabled value '%s'", epaString) } return p, nil diff --git a/tds.go b/tds.go index fb35e3be..2e923ad9 100644 --- a/tds.go +++ b/tds.go @@ -1173,11 +1173,15 @@ initiate_connection: outbuf := newTdsBuffer(packetSize, toconn) if p.Encryption == msdsn.EncryptionStrict { - outbuf.transport, err = getTLSConn(toconn, p, "tds/8.0") + tlsConn, err := getTLSConn(toconn, p, "tds/8.0") if err != nil { return nil, err } isTransportEncrypted = true + outbuf.transport = tlsConn + if p.EpaEnabled { + cbt = integratedauth.GenerateCBTFromTLSConnState(tlsConn.ConnectionState()) + } } sess := newSession(outbuf, logger, p) @@ -1255,20 +1259,8 @@ initiate_connection: } } - if p.EpaMode != msdsn.EpaOff { - state := tlsConn.ConnectionState() - switch p.EpaMode { - case msdsn.EpaTlsUnique: - if len(state.TLSUnique) > 0 { - cbt = integratedauth.GenerateCBTFromTLSUnique(state.TLSUnique) - } - case msdsn.EpaTlsServerEndPoint: - if len(state.PeerCertificates) > 0 { - cbt = integratedauth.GenerateCBTFromServerCert(state.PeerCertificates[0]) - } - default: - break - } + if p.EpaEnabled { + cbt = integratedauth.GenerateCBTFromTLSConnState(tlsConn.ConnectionState()) } } } From d3db044380db7e807f6132ef13dfb321f7c5bfe8 Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:15:04 +0300 Subject: [PATCH 7/8] log error --- integratedauth/channel_binding.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/integratedauth/channel_binding.go b/integratedauth/channel_binding.go index 6151c834..41234b1d 100644 --- a/integratedauth/channel_binding.go +++ b/integratedauth/channel_binding.go @@ -11,7 +11,7 @@ import ( const ( // https://datatracker.ietf.org/doc/rfc9266/ - TLS_EXPORTER_PREFIX = "tls-exporter:" + TLS_EXPORTER_PREFIX = "tls-exporter:" TLS_EXPORTER_EKM_LABEL = "EXPORTER-Channel-Binding" TLS_EXPORTER_EKM_LENGTH = 32 // https://www.rfc-editor.org/rfc/rfc5801.html#section-5.2 @@ -143,9 +143,9 @@ func (cb *SEC_CHANNEL_BINDINGS) ToBytes() []byte { // - tlsUnique: the TLS unique value // Returns: // - a ChannelBindings struct -func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { +func GenerateCBTFromTLSUnique(tlsUnique []byte) (*ChannelBindings, error) { if len(tlsUnique) == 0 { - return nil + return nil, fmt.Errorf("tlsUnique is empty") } return &ChannelBindings{ InitiatorAddrType: 0, @@ -153,7 +153,7 @@ func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { AcceptorAddrType: 0, AcceptorAddress: nil, ApplicationData: append([]byte(TLS_UNIQUE_PREFIX), tlsUnique...), - } + }, nil } // GenerateCBTFromTLSConnState generates a ChannelBindings struct from a TLS connection state @@ -163,13 +163,12 @@ func GenerateCBTFromTLSUnique(tlsUnique []byte) *ChannelBindings { // - state: the TLS connection state // Returns: // - a ChannelBindings struct -func GenerateCBTFromTLSConnState(state tls.ConnectionState) *ChannelBindings { +func GenerateCBTFromTLSConnState(state tls.ConnectionState) (*ChannelBindings, error) { switch state.Version { case tls.VersionTLS13: exporterKey, err := state.ExportKeyingMaterial(TLS_EXPORTER_EKM_LABEL, nil, TLS_EXPORTER_EKM_LENGTH) if err != nil { - fmt.Println("GenerateCBTFromTLSExporter: error: ", err) - return nil + return nil, fmt.Errorf("error exporting keying material: %w", err) } return GenerateCBTFromTLSExporter(exporterKey) default: @@ -182,10 +181,9 @@ func GenerateCBTFromTLSConnState(state tls.ConnectionState) *ChannelBindings { // - exporterKey: the TLS exporter key // Returns: // - a ChannelBindings struct -func GenerateCBTFromTLSExporter(exporterKey []byte) *ChannelBindings { - fmt.Println("GenerateCBTFromTLSExporter: exporterKey: ", exporterKey) +func GenerateCBTFromTLSExporter(exporterKey []byte) (*ChannelBindings, error) { if len(exporterKey) == 0 { - return nil + return nil, fmt.Errorf("exporterKey is empty") } return &ChannelBindings{ @@ -194,7 +192,7 @@ func GenerateCBTFromTLSExporter(exporterKey []byte) *ChannelBindings { AcceptorAddrType: 0, AcceptorAddress: nil, ApplicationData: append([]byte(TLS_EXPORTER_PREFIX), exporterKey...), - } + }, nil } // GenerateCBTFromServerCert generates a ChannelBindings struct from a server certificate From 4a26b6c84617b919e0b2e503e69b17ed265bbb8d Mon Sep 17 00:00:00 2001 From: Bulat <67165594+paraddise@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:16:43 +0300 Subject: [PATCH 8/8] f --- tds.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tds.go b/tds.go index 2e923ad9..d06ca4bf 100644 --- a/tds.go +++ b/tds.go @@ -1180,7 +1180,10 @@ initiate_connection: isTransportEncrypted = true outbuf.transport = tlsConn if p.EpaEnabled { - cbt = integratedauth.GenerateCBTFromTLSConnState(tlsConn.ConnectionState()) + cbt, err = integratedauth.GenerateCBTFromTLSConnState(tlsConn.ConnectionState()) + if err != nil { + logger.Log(ctx, msdsn.LogErrors, fmt.Sprintf("Error while generating Channel Bindings from TLS connection state: %v", err)) + } } } sess := newSession(outbuf, logger, p) @@ -1260,7 +1263,10 @@ initiate_connection: } if p.EpaEnabled { - cbt = integratedauth.GenerateCBTFromTLSConnState(tlsConn.ConnectionState()) + cbt, err = integratedauth.GenerateCBTFromTLSConnState(tlsConn.ConnectionState()) + if err != nil { + logger.Log(ctx, msdsn.LogErrors, fmt.Sprintf("Error while generating Channel Bindings from TLS connection state: %v", err)) + } } } }