Basic FAST support.

This commit is contained in:
Martin Dosch 2024-04-10 15:13:20 +02:00
parent 7486b7a363
commit b0f55a8f7f

171
xmpp.go
View File

@ -52,6 +52,7 @@ const (
nsSASL2 = "urn:xmpp:sasl:2" nsSASL2 = "urn:xmpp:sasl:2"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind" nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsBind2 = "urn:xmpp:bind:0" nsBind2 = "urn:xmpp:bind:0"
nsFast = "urn:xmpp:fast:0"
nsSASLCB = "urn:xmpp:sasl-cb:0" nsSASLCB = "urn:xmpp:sasl-cb:0"
nsClient = "jabber:client" nsClient = "jabber:client"
nsSession = "urn:ietf:params:xml:ns:xmpp-session" nsSession = "urn:ietf:params:xml:ns:xmpp-session"
@ -75,6 +76,13 @@ func getCookie() Cookie {
return Cookie(binary.LittleEndian.Uint64(buf[:])) return Cookie(binary.LittleEndian.Uint64(buf[:]))
} }
// Fast holds the XEP-0484 fast token, mechanism and expiry date
type Fast struct {
Token string
Mechanism string
Expiry time.Time
}
// Client holds XMPP connection options // Client holds XMPP connection options
type Client struct { type Client struct {
conn net.Conn // connection to server conn net.Conn // connection to server
@ -87,6 +95,7 @@ type Client struct {
LimitMaxBytes int // Maximum stanza size (XEP-0478: Stream Limits Advertisement) LimitMaxBytes int // Maximum stanza size (XEP-0478: Stream Limits Advertisement)
LimitIdleSeconds int // Maximum idle seconds (XEP-0478: Stream Limits Advertisement) LimitIdleSeconds int // Maximum idle seconds (XEP-0478: Stream Limits Advertisement)
Mechanism string Mechanism string
Fast Fast // XEP-0484 FAST Token, mechanism and expiry.
} }
func (c *Client) JID() string { func (c *Client) JID() string {
@ -240,7 +249,7 @@ type Options struct {
// XEP-0474: SASL SCRAM Downgrade Protection // XEP-0474: SASL SCRAM Downgrade Protection
SSDP bool SSDP bool
// XEP-0388: XEP-0388: Extensible SASL Profile // XEP-0388: Extensible SASL Profile
// Value for software // Value for software
UserAgentSW string UserAgentSW string
@ -248,10 +257,18 @@ type Options struct {
// Value for device // Value for device
UserAgentDev string UserAgentDev string
// XEP-0388: XEP-0388: Extensible SASL Profile // XEP-0388: Extensible SASL Profile
// Unique stable identifier for the client installation // Unique stable identifier for the client installation
// MUST be a valid UUIDv4 // MUST be a valid UUIDv4
UserAgentID string UserAgentID string
// XEP-0484: Fast Authentication Streamlining Tokens
// Fast Token
FastToken string
// XEP-0484: Fast Authentication Streamlining Tokens
// Fast Mechanism
FastMechanism string
} }
// NewClient establishes a new Client connection based on a set of Options. // NewClient establishes a new Client connection based on a set of Options.
@ -431,7 +448,7 @@ func (c *Client) init(o *Options) error {
return err return err
} }
var mechanism, channelBinding, clientFirstMessage, clientFinalMessageBare, authMessage string var mechanism, channelBinding, clientFirstMessage, clientFinalMessageBare, authMessage string
var bind2Data, resource, userAgentSW, userAgentDev, userAgentID string var bind2Data, resource, userAgentSW, userAgentDev, userAgentID, fastAuth string
var serverSignature, keyingMaterial []byte var serverSignature, keyingMaterial []byte
var scramPlus, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool var scramPlus, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool
var cbsSlice, mechSlice []string var cbsSlice, mechSlice []string
@ -604,9 +621,68 @@ func (c *Client) init(o *Options) error {
if o.UserAgentID != "" { if o.UserAgentID != "" {
userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID) userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID)
} }
if f.Authentication.Inline.Fast.Mechanism != nil && o.UserAgentID != "" {
var mech string
if o.FastToken == "" {
m := f.Authentication.Inline.Fast.Mechanism
switch {
case slices.Contains(m, "HT-SHA-256-EXPR") && tls13:
mech = "HT-SHA-256-EXPR"
case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13:
mech = "HT-SHA-256-UNIQ"
case slices.Contains(m, "HT-SHA-256-ENDP"):
mech = "HT-SHA-256-ENDP"
case slices.Contains(m, "HT-SHA-256-NONE"):
mech = "HT-SHA-256-NONE"
default:
return fmt.Errorf("fast: unsupported auth mechanism %s", m)
}
fastAuth = fmt.Sprintf("<request-token xmlns='%s' mechanism='%s'/>", nsFast, mech)
} else {
fastAuth = fmt.Sprintf("<fast xmlns='%s' />", nsFast)
tlsState := tlsConn.ConnectionState()
mechanism = o.FastMechanism
switch mechanism {
case "HT-SHA-256-EXPR":
keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32)
if err != nil {
return err
}
case "HT-SHA-256-UNIQ":
keyingMaterial = tlsState.TLSUnique
case "HT-SHA-256-ENDP":
var h hash.Hash
switch tlsState.PeerCertificates[0].SignatureAlgorithm {
case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.ECDSAWithSHA1,
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()
}
h.Write(tlsState.PeerCertificates[0].Raw)
keyingMaterial = h.Sum(nil)
h.Reset()
case "HT-SHA-256-NONE":
keyingMaterial = []byte("")
default:
return fmt.Errorf("fast: unsupported auth mechanism %s", mechanism)
}
h := hmac.New(sha256.New, []byte(o.FastToken))
initiator := append([]byte("Initiator")[:], keyingMaterial[:]...)
_, err = h.Write(initiator)
if err != nil {
return err
}
initiatorHashedToken := h.Sum(nil)
user := strings.Split(o.User, "@")[0]
clientFirstMessage = user + "\x00" + string(initiatorHashedToken)
}
}
fmt.Fprintf(c.stanzaWriter, fmt.Fprintf(c.stanzaWriter,
"<authenticate xmlns='%s' mechanism='%s'><initial-response>%s</initial-response><user-agent%s>%s%s</user-agent>%s</authenticate>\n", "<authenticate xmlns='%s' mechanism='%s'><initial-response>%s</initial-response><user-agent%s>%s%s</user-agent>%s%s</authenticate>\n",
nsSASL2, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data) nsSASL2, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
} else { } else {
fmt.Fprintf(c.stanzaWriter, "<auth xmlns='%s' mechanism='%s'>%s</auth>\n", fmt.Fprintf(c.stanzaWriter, "<auth xmlns='%s' mechanism='%s'>%s</auth>\n",
nsSASL, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage))) nsSASL, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)))
@ -633,6 +709,55 @@ func (c *Client) init(o *Options) error {
errorMessage = v.Any.Local errorMessage = v.Any.Local
} }
return errors.New("auth failure: " + errorMessage) return errors.New("auth failure: " + errorMessage)
case *sasl2Success:
if strings.HasPrefix(mechanism, "SCRAM-SHA") {
successMsg, err := base64.StdEncoding.DecodeString(v.AdditionalData)
if err != nil {
return err
}
if !strings.HasPrefix(string(successMsg), "v=") {
return errors.New("server sent unexpected content in SCRAM success message")
}
c.Mechanism = mechanism
}
if strings.HasPrefix(mechanism, "HT-SHA") {
// TODO: Check whether server implementations already support
// https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html#section-3.3
h := hmac.New(sha256.New, []byte(o.FastToken))
responder := append([]byte("Responder")[:], keyingMaterial[:]...)
_, err = h.Write(responder)
if err != nil {
return err
}
responderMsgRcv, err := base64.StdEncoding.DecodeString(v.AdditionalData)
if err != nil {
return err
}
responderMsgCalc := h.Sum(nil)
if string(responderMsgCalc) != string(responderMsgRcv) {
return fmt.Errorf("server sent unexpected content in FAST success message")
}
c.Mechanism = mechanism
}
if bind2 {
c.jid = v.AuthorizationIdentifier
}
if v.Token.Token != "" {
m := f.Authentication.Inline.Fast.Mechanism
switch {
case slices.Contains(m, "HT-SHA-256-EXPR") && tls13:
c.Fast.Mechanism = "HT-SHA-256-EXPR"
case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13:
c.Fast.Mechanism = "HT-SHA-256-UNIQ"
case slices.Contains(m, "HT-SHA-256-ENDP"):
c.Fast.Mechanism = "HT-SHA-256-ENDP"
case slices.Contains(m, "HT-SHA-256-NONE"):
c.Fast.Mechanism = "HT-SHA-256-NONE"
}
c.Fast.Token = v.Token.Token
c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry)
}
return nil
case *sasl2Challenge: case *sasl2Challenge:
sfm = v.Text sfm = v.Text
case *saslChallenge: case *saslChallenge:
@ -829,6 +954,23 @@ func (c *Client) init(o *Options) error {
if bind2 { if bind2 {
c.jid = v.AuthorizationIdentifier c.jid = v.AuthorizationIdentifier
} }
if v.Token.Token != "" {
if v.Token.Token != "" {
m := f.Authentication.Inline.Fast.Mechanism
switch {
case slices.Contains(m, "HT-SHA-256-EXPR") && tls13:
c.Fast.Mechanism = "HT-SHA-256-EXPR"
case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13:
c.Fast.Mechanism = "HT-SHA-256-UNIQ"
case slices.Contains(m, "HT-SHA-256-ENDP"):
c.Fast.Mechanism = "HT-SHA-256-ENDP"
case slices.Contains(m, "HT-SHA-256-NONE"):
c.Fast.Mechanism = "HT-SHA-256-NONE"
}
c.Fast.Token = v.Token.Token
c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry)
}
}
case *saslSuccess: case *saslSuccess:
if strings.HasPrefix(mechanism, "SCRAM-SHA") { if strings.HasPrefix(mechanism, "SCRAM-SHA") {
successMsg, err := base64.StdEncoding.DecodeString(v.Text) successMsg, err := base64.StdEncoding.DecodeString(v.Text)
@ -1488,9 +1630,16 @@ type sasl2Authentication struct {
Inline struct { Inline struct {
Text string `xml:",chardata"` Text string `xml:",chardata"`
Bind struct { Bind struct {
Text string `xml:",chardata"` XMLName xml.Name `xml:"urn:xmpp:bind:0 bind"`
Xmlns string `xml:"xmlns,attr"` Xmlns string `xml:"xmlns,attr"`
Text string `xml:",chardata"`
} `xml:"bind"` } `xml:"bind"`
Fast struct {
XMLName xml.Name `xml:"urn:xmpp:fast:0 fast"`
Text string `xml:",chardata"`
Tls0rtt string `xml:"tls-0rtt,attr"`
Mechanism []string `xml:"mechanism"`
} `xml:"fast"`
} `xml:"inline"` } `xml:"inline"`
} }
@ -1521,8 +1670,14 @@ type sasl2Success struct {
AuthorizationIdentifier string `xml:"authorization-identifier"` AuthorizationIdentifier string `xml:"authorization-identifier"`
Bound struct { Bound struct {
Text string `xml:",chardata"` Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"` Xmlns string `xml:"urn:xmpp:bind:0,attr"`
} `xml:"bound"` } `xml:"bound"`
Token struct {
Text string `xml:",chardata"`
Xmlns string `xml:"urn:xmpp:fast:0,attr"`
Expiry string `xml:"expiry,attr"`
Token string `xml:"token,attr"`
} `xml:"token"`
} }
type saslSuccess struct { type saslSuccess struct {