1- // SCRAM-SHA-256 authentication
1+ // SCRAM-SHA-256 and SCRAM-SHA-256-PLUS authentication
22//
33// Resources:
44// https://tools.ietf.org/html/rfc5802
5+ // https://tools.ietf.org/html/rfc5929
56// https://tools.ietf.org/html/rfc8265
67// https://www.postgresql.org/docs/current/sasl-authentication.html
78//
@@ -18,17 +19,25 @@ import (
1819 "crypto/pbkdf2"
1920 "crypto/rand"
2021 "crypto/sha256"
22+ "crypto/sha512"
23+ "crypto/tls"
24+ "crypto/x509"
2125 "encoding/base64"
2226 "errors"
2327 "fmt"
28+ "hash"
2429 "slices"
2530 "strconv"
2631
2732 "github.com/jackc/pgx/v5/pgproto3"
2833 "golang.org/x/text/secure/precis"
2934)
3035
31- const clientNonceLen = 18
36+ const (
37+ clientNonceLen = 18
38+ scramSHA256Name = "SCRAM-SHA-256"
39+ scramSHA256PlusName = "SCRAM-SHA-256-PLUS"
40+ )
3241
3342// Perform SCRAM authentication.
3443func (c * PgConn ) scramAuth (serverAuthMechanisms []string ) error {
@@ -37,9 +46,34 @@ func (c *PgConn) scramAuth(serverAuthMechanisms []string) error {
3746 return err
3847 }
3948
49+ serverHasPlus := slices .Contains (sc .serverAuthMechanisms , scramSHA256PlusName )
50+ if c .config .ChannelBinding == "require" && ! serverHasPlus {
51+ return errors .New ("channel binding required but server does not support SCRAM-SHA-256-PLUS" )
52+ }
53+
54+ // If we have a TLS connection and channel binding is not disabled, attempt to
55+ // extract the server certificate hash for tls-server-end-point channel binding.
56+ if tlsConn , ok := c .conn .(* tls.Conn ); ok && c .config .ChannelBinding != "disable" {
57+ certHash , err := getTLSCertificateHash (tlsConn )
58+ if err != nil && c .config .ChannelBinding == "require" {
59+ return fmt .Errorf ("channel binding required but failed to get server certificate hash: %w" , err )
60+ }
61+
62+ // Upgrade to SCRAM-SHA-256-PLUS if we have binding data and the server supports it.
63+ if certHash != nil && serverHasPlus {
64+ sc .authMechanism = scramSHA256PlusName
65+ }
66+
67+ sc .channelBindingData = certHash
68+ }
69+
70+ if c .config .ChannelBinding == "require" && sc .channelBindingData == nil {
71+ return errors .New ("channel binding required but channel binding data is not available" )
72+ }
73+
4074 // Send client-first-message in a SASLInitialResponse
4175 saslInitialResponse := & pgproto3.SASLInitialResponse {
42- AuthMechanism : "SCRAM-SHA-256" ,
76+ AuthMechanism : sc . authMechanism ,
4377 Data : sc .clientFirstMessage (),
4478 }
4579 c .frontend .Send (saslInitialResponse )
@@ -111,7 +145,22 @@ type scramClient struct {
111145 password string
112146 clientNonce []byte
113147
148+ // authMechanism is the clients selected SASL mechanism. Must be either
149+ // SCRAM-SHA-256 (default) or SCRAM-SHA-256-PLUS.
150+ //
151+ // Upgraded to SCRAM-SHA-256-PLUS during authentication when channel binding
152+ // is not disabled, channel binding data is available (TLS connection
153+ // with an obtainable server certificate hash) and the server advertises
154+ // SCRAM-SHA-256-PLUS.
155+ authMechanism string
156+
157+ // channelBindingData is the hash of the server's TLS certificate, computed
158+ // per the tls-server-end-point channel binding type (RFC 5929). Used as
159+ // the binding input in SCRAM-SHA-256-PLUS. nil when not in use.
160+ channelBindingData []byte
161+
114162 clientFirstMessageBare []byte
163+ clientGS2Header []byte
115164
116165 serverFirstMessage []byte
117166 clientAndServerNonce []byte
@@ -125,11 +174,14 @@ type scramClient struct {
125174func newScramClient (serverAuthMechanisms []string , password string ) (* scramClient , error ) {
126175 sc := & scramClient {
127176 serverAuthMechanisms : serverAuthMechanisms ,
177+ authMechanism : scramSHA256Name ,
128178 }
129179
130- // Ensure server supports SCRAM-SHA-256
131- hasScramSHA256 := slices .Contains (sc .serverAuthMechanisms , "SCRAM-SHA-256" )
132- if ! hasScramSHA256 {
180+ // Ensure the server supports SCRAM-SHA-256. SCRAM-SHA-256-PLUS is the
181+ // channel binding variant and is only advertised when the server supports
182+ // SSL. PostgreSQL always advertises the base SCRAM-SHA-256 mechanism
183+ // regardless of SSL.
184+ if ! slices .Contains (sc .serverAuthMechanisms , scramSHA256Name ) {
133185 return nil , errors .New ("server does not support SCRAM-SHA-256" )
134186 }
135187
@@ -153,8 +205,31 @@ func newScramClient(serverAuthMechanisms []string, password string) (*scramClien
153205}
154206
155207func (sc * scramClient ) clientFirstMessage () []byte {
208+ // The client-first-message is the GS2 header concatenated with the bare
209+ // message (username + client nonce). The GS2 header communicates the
210+ // client's channel binding capability to the server:
211+ //
212+ // "n,," - client does not support channel binding
213+ // "y,," - client supports channel binding, but the
214+ // server did not advertise SCRAM-SHA-256-PLUS
215+ // "p=tls-server-end-point,," - channel binding is active via SCRAM-SHA-256-PLUS
216+ //
217+ // See:
218+ // https://www.rfc-editor.org/rfc/rfc5802#section-6
219+ // https://www.rfc-editor.org/rfc/rfc5929#section-4
220+ // https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256
221+
156222 sc .clientFirstMessageBare = fmt .Appendf (nil , "n=,r=%s" , sc .clientNonce )
157- return fmt .Appendf (nil , "n,,%s" , sc .clientFirstMessageBare )
223+
224+ if sc .authMechanism == scramSHA256PlusName {
225+ sc .clientGS2Header = []byte ("p=tls-server-end-point,," )
226+ } else if sc .channelBindingData != nil {
227+ sc .clientGS2Header = []byte ("y,," )
228+ } else {
229+ sc .clientGS2Header = []byte ("n,," )
230+ }
231+
232+ return append (sc .clientGS2Header , sc .clientFirstMessageBare ... )
158233}
159234
160235func (sc * scramClient ) recvServerFirstMessage (serverFirstMessage []byte ) error {
@@ -213,7 +288,19 @@ func (sc *scramClient) recvServerFirstMessage(serverFirstMessage []byte) error {
213288}
214289
215290func (sc * scramClient ) clientFinalMessage () string {
216- clientFinalMessageWithoutProof := fmt .Appendf (nil , "c=biws,r=%s" , sc .clientAndServerNonce )
291+ // The c= attribute carries the base64-encoded channel binding input.
292+ //
293+ // Without channel binding this is just the GS2 header alone ("biws" for
294+ // "n,," or "eSws" for "y,,").
295+ //
296+ // With channel binding, this is the GS2 header with the channel binding data
297+ // (certificate hash) appended.
298+ channelBindInput := sc .clientGS2Header
299+ if sc .authMechanism == scramSHA256PlusName {
300+ channelBindInput = slices .Concat (sc .clientGS2Header , sc .channelBindingData )
301+ }
302+ channelBindingEncoded := base64 .StdEncoding .EncodeToString (channelBindInput )
303+ clientFinalMessageWithoutProof := fmt .Appendf (nil , "c=%s,r=%s" , channelBindingEncoded , sc .clientAndServerNonce )
217304
218305 var err error
219306 sc .saltedPassword , err = pbkdf2 .Key (sha256 .New , sc .password , sc .salt , sc .iterations , 32 )
@@ -269,3 +356,36 @@ func computeServerSignature(saltedPassword, authMessage []byte) []byte {
269356 base64 .StdEncoding .Encode (buf , serverSignature )
270357 return buf
271358}
359+
360+ // Get the server certificate hash for SCRAM channel binding type
361+ // tls-server-end-point.
362+ func getTLSCertificateHash (conn * tls.Conn ) ([]byte , error ) {
363+ state := conn .ConnectionState ()
364+ if len (state .PeerCertificates ) == 0 {
365+ return nil , errors .New ("no peer certificates for channel binding" )
366+ }
367+
368+ cert := state .PeerCertificates [0 ]
369+
370+ // Per RFC 5929 section 4.1: If the certificate's signatureAlgorithm uses
371+ // MD5 or SHA-1, use SHA-256. Otherwise use the hash from the signature
372+ // algorithm.
373+ //
374+ // See: https://www.rfc-editor.org/rfc/rfc5929.html#section-4.1
375+ var h hash.Hash
376+ switch cert .SignatureAlgorithm {
377+ case x509 .MD5WithRSA , x509 .SHA1WithRSA , x509 .ECDSAWithSHA1 :
378+ h = sha256 .New ()
379+ case x509 .SHA256WithRSA , x509 .SHA256WithRSAPSS , x509 .ECDSAWithSHA256 :
380+ h = sha256 .New ()
381+ case x509 .SHA384WithRSA , x509 .SHA384WithRSAPSS , x509 .ECDSAWithSHA384 :
382+ h = sha512 .New384 ()
383+ case x509 .SHA512WithRSA , x509 .SHA512WithRSAPSS , x509 .ECDSAWithSHA512 :
384+ h = sha512 .New ()
385+ default :
386+ return nil , fmt .Errorf ("tls-server-end-point channel binding is undefined for certificate signature algorithm %v" , cert .SignatureAlgorithm )
387+ }
388+
389+ h .Write (cert .Raw )
390+ return h .Sum (nil ), nil
391+ }
0 commit comments