Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/python-nbxmpp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Hörist <philipp@hoerist.com>2023-10-22 22:31:06 +0300
committerPhilipp Hörist <philipp@hoerist.com>2023-10-24 21:41:31 +0300
commit8a87dc77b3f8ef56aa5deea5a06d1782d7bc237d (patch)
treee81f7e0b9da7b86f24ff0e2f88d13f2fa4c92012
parentc7e4a9a5e0600914c7871e9aa80fb9c73ac2a901 (diff)
refactor: Rewrite channel binding code
- Fix some bugs related to XEP-0388 (SASL2)
-rw-r--r--nbxmpp/client.py7
-rw-r--r--nbxmpp/connection.py6
-rw-r--r--nbxmpp/namespaces.py3
-rw-r--r--nbxmpp/protocol.py31
-rw-r--r--nbxmpp/sasl.py67
-rw-r--r--nbxmpp/structs.py6
-rw-r--r--nbxmpp/tcp.py19
7 files changed, 108 insertions, 31 deletions
diff --git a/nbxmpp/client.py b/nbxmpp/client.py
index c777017..5a8c02e 100644
--- a/nbxmpp/client.py
+++ b/nbxmpp/client.py
@@ -20,6 +20,7 @@ from typing import Any
from typing import Optional
from gi.repository import GLib
+from gi.repository import Gio
from nbxmpp.namespaces import Namespace
from nbxmpp.http import HTTPSession
@@ -222,6 +223,12 @@ class Client(Observable):
def ciphersuite(self):
return self._con.ciphersuite
+ def get_channel_binding_data(
+ self,
+ type_: Gio.TlsChannelBindingType
+ ) -> Optional[bytes]:
+ return self._con.get_channel_binding_data(type_)
+
def set_ignore_tls_errors(self, ignore):
self._ignore_tls_errors = ignore
diff --git a/nbxmpp/connection.py b/nbxmpp/connection.py
index 3eb54cf..e4f1d82 100644
--- a/nbxmpp/connection.py
+++ b/nbxmpp/connection.py
@@ -74,6 +74,12 @@ class Connection(Observable):
def ciphersuite(self) -> Optional[int]:
return None
+ def get_channel_binding_data(
+ self,
+ type_: Gio.TlsChannelBindingType # pylint: disable=unused-argument
+ ) -> Optional[bytes]:
+ return None
+
@property
def local_address(self):
return self._local_address
diff --git a/nbxmpp/namespaces.py b/nbxmpp/namespaces.py
index dc8a661..5a1310e 100644
--- a/nbxmpp/namespaces.py
+++ b/nbxmpp/namespaces.py
@@ -43,6 +43,7 @@ class _Namespaces:
CAPS: str = 'http://jabber.org/protocol/caps'
CAPTCHA: str = 'urn:xmpp:captcha'
CARBONS: str = 'urn:xmpp:carbons:2'
+ CHANNEL_BINDING: str = 'urn:xmpp:sasl-cb:0'
CHATMARKERS: str = 'urn:xmpp:chat-markers:0'
CHATSTATES: str = 'http://jabber.org/protocol/chatstates'
CLIENT: str = 'jabber:client'
@@ -147,7 +148,7 @@ class _Namespaces:
ROSTER_VER: str = 'urn:xmpp:features:rosterver'
RSM: str = 'http://jabber.org/protocol/rsm'
SASL: str = 'urn:ietf:params:xml:ns:xmpp-sasl'
- SASL2: str = 'urn:xmpp:sasl:1'
+ SASL2: str = 'urn:xmpp:sasl:2'
SEARCH: str = 'jabber:iq:search'
SECLABEL: str = 'urn:xmpp:sec-label:0'
SECLABEL_CATALOG: str = 'urn:xmpp:sec-label:catalog:2'
diff --git a/nbxmpp/protocol.py b/nbxmpp/protocol.py
index 0bcc0e9..42f3a9c 100644
--- a/nbxmpp/protocol.py
+++ b/nbxmpp/protocol.py
@@ -37,6 +37,7 @@ from dataclasses import dataclass
from dataclasses import asdict
from gi.repository import GLib
+from gi.repository import Gio
import idna
from nbxmpp.xmppiri import clean_iri
@@ -60,10 +61,10 @@ def ascii_upper(s):
SASL_AUTH_MECHS = [
'SCRAM-SHA-512-PLUS',
- 'SCRAM-SHA-512',
'SCRAM-SHA-256-PLUS',
- 'SCRAM-SHA-256',
'SCRAM-SHA-1-PLUS',
+ 'SCRAM-SHA-512',
+ 'SCRAM-SHA-256',
'SCRAM-SHA-1',
'GSSAPI',
'PLAIN',
@@ -1866,14 +1867,15 @@ class Features(Node):
return self.getTag('mechanisms', namespace=Namespace.SASL) is not None
def has_sasl_2(self):
- return self.getTag('mechanisms', namespace=Namespace.SASL2) is not None
+ return self.getTag('authentication', namespace=Namespace.SASL2) is not None
def get_mechs(self) -> set[str]:
- mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL2)
+ mechanisms = self.getTag('authentication', namespace=Namespace.SASL2)
if mechanisms is None:
mechanisms = self.getTag('mechanisms', namespace=Namespace.SASL)
- if mechanisms is None:
- return set()
+
+ if mechanisms is None:
+ return set()
mechanisms = mechanisms.getTags('mechanism')
return set(mech.getData() for mech in mechanisms)
@@ -1907,6 +1909,23 @@ class Features(Node):
def has_anonymous(self):
return 'ANONYMOUS' in self.get_mechs()
+ def get_channel_binding_type(self) -> Optional[Gio.TlsChannelBindingType]:
+ sasl_cb = self.getTag('sasl-channel-binding',
+ namespace=Namespace.CHANNEL_BINDING)
+ if sasl_cb is None:
+ return None
+
+ exporter = sasl_cb.getTag(
+ 'channel-binding', attrs={'type': 'tls-exporter'})
+ if exporter is not None:
+ return Gio.TlsChannelBindingType.EXPORTER
+
+ server_end_point = sasl_cb.getTag(
+ 'channel-binding', attrs={'type': 'tls-server-end-point'})
+ if server_end_point is not None:
+ return Gio.TlsChannelBindingType.SERVER_END_POINT
+ return None
+
class ErrorNode(Node):
"""
diff --git a/nbxmpp/sasl.py b/nbxmpp/sasl.py
index 3fbd935..1470ec3 100644
--- a/nbxmpp/sasl.py
+++ b/nbxmpp/sasl.py
@@ -25,10 +25,13 @@ import logging
import hashlib
from hashlib import pbkdf2_hmac
+from gi.repository import Gio
+
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Node
from nbxmpp.protocol import SASL_ERROR_CONDITIONS
from nbxmpp.protocol import SASL_AUTH_MECHS
+from nbxmpp.structs import ChannelBindingData
from nbxmpp.util import b64decode
from nbxmpp.util import b64encode
from nbxmpp.util import LogAdapter
@@ -100,23 +103,41 @@ class SASL:
elif stanza.getName() == 'success':
self._on_success(stanza)
+ def _get_channel_binding_data(self, features) -> Optional[ChannelBindingData]:
+ if self._client.tls_version != Gio.TlsProtocolVersion.TLS_1_3:
+ return None
+
+ binding_type = features.get_channel_binding_type()
+ if binding_type is None:
+ return None
+
+ channel_binding_data = self._client.get_channel_binding_data(binding_type)
+ if channel_binding_data is None:
+ return None
+
+ return ChannelBindingData(binding_type, channel_binding_data)
+
def start_auth(self, features):
+ self._mechanism = None
self._allowed_mechs = self._client.mechs
self._enabled_mechs = self._allowed_mechs
- self._mechanism = None
self._sasl_ns = Namespace.SASL
if features.has_sasl_2():
self._sasl_ns = Namespace.SASL2
+ self._log.info('Using %s', self._sasl_ns)
+
self._error = None
- # -PLUS variants need TLS channel binding data
- # This is currently not supported via GLib
- self._enabled_mechs.discard('SCRAM-SHA-1-PLUS')
- self._enabled_mechs.discard('SCRAM-SHA-256-PLUS')
- self._enabled_mechs.discard('SCRAM-SHA-512-PLUS')
- # channel_binding_data = None
+ channel_binding_data = None
+ # Segfaults see https://gitlab.gnome.org/GNOME/pygobject/-/issues/603
+ # So for now channel binding is deactivated
+ # channel_binding_data = self._get_channel_binding_data(features)
+ if channel_binding_data is None:
+ self._enabled_mechs.discard('SCRAM-SHA-1-PLUS')
+ self._enabled_mechs.discard('SCRAM-SHA-256-PLUS')
+ self._enabled_mechs.discard('SCRAM-SHA-512-PLUS')
if not GSSAPI_AVAILABLE:
self._enabled_mechs.discard('GSSAPI')
@@ -146,10 +167,7 @@ class SASL:
self._log.info('Chosen auth mechanism: %s', chosen_mechanism)
- if chosen_mechanism in ('SCRAM-SHA-512',
- 'SCRAM-SHA-256',
- 'SCRAM-SHA-1',
- 'PLAIN'):
+ if chosen_mechanism.startswith(('SCRAM', 'PLAIN')):
if not self._password:
self._on_sasl_finished(False, 'no-password')
return
@@ -159,6 +177,10 @@ class SASL:
self._password,
domain_based_name or self._client.domain)
+ if (isinstance(self._mechanism, SCRAM) and
+ channel_binding_data is not None):
+ self._mechanism.set_channel_binding_data(channel_binding_data)
+
try:
self._send_initiate()
except AuthFail as error:
@@ -340,18 +362,19 @@ class GSSAPI(BaseMechanism):
class SCRAM(BaseMechanism):
name = ''
- _channel_binding = ''
_hash_method = ''
def __init__(self, *args, **kwargs) -> None:
BaseMechanism.__init__(self, *args, **kwargs)
- self._channel_binding_data = None
+ self._channel_binding_data: ChannelBindingData | None = None
+ self._gs2_header = 'n,,'
self._client_nonce = '%x' % int(binascii.hexlify(os.urandom(24)), 16)
self._client_first_message_bare = None
self._server_signature = None
- def set_channel_binding_data(self, data: bytes) -> None:
+ def set_channel_binding_data(self, data: ChannelBindingData) -> None:
self._channel_binding_data = data
+ self._gs2_header = f'p={data.type},,'
@property
def nonce_length(self) -> int:
@@ -360,9 +383,10 @@ class SCRAM(BaseMechanism):
@property
def _b64_channel_binding_data(self) -> str:
if self.name.endswith('PLUS'):
- return b64encode(b'%s%s' % (self._channel_binding.encode(),
- self._channel_binding_data))
- return b64encode(self._channel_binding)
+ assert self._channel_binding_data is not None
+ return b64encode(b'%s%s' % (self._gs2_header.encode(),
+ self._channel_binding_data.data))
+ return b64encode(self._gs2_header)
@staticmethod
def _scram_parse(scram_data: str) -> dict[str, str]:
@@ -371,7 +395,8 @@ class SCRAM(BaseMechanism):
def get_initiate_data(self) -> str:
self._client_first_message_bare = 'n=%s,r=%s' % (self._username,
self._client_nonce)
- client_first_message = '%s%s' % (self._channel_binding,
+
+ client_first_message = '%s%s' % (self._gs2_header,
self._client_first_message_bare)
return b64encode(client_first_message)
@@ -442,40 +467,34 @@ class SCRAM(BaseMechanism):
class SCRAM_SHA_1(SCRAM):
name = 'SCRAM-SHA-1'
- _channel_binding = 'n,,'
_hash_method = 'sha1'
class SCRAM_SHA_1_PLUS(SCRAM_SHA_1):
name = 'SCRAM-SHA-1-PLUS'
- _channel_binding = 'p=tls-unique,,'
class SCRAM_SHA_256(SCRAM):
name = 'SCRAM-SHA-256'
- _channel_binding = 'n,,'
_hash_method = 'sha256'
class SCRAM_SHA_256_PLUS(SCRAM_SHA_256):
name = 'SCRAM-SHA-256-PLUS'
- _channel_binding = 'p=tls-unique,,'
class SCRAM_SHA_512(SCRAM):
name = 'SCRAM-SHA-512'
- _channel_binding = 'n,,'
_hash_method = 'sha512'
class SCRAM_SHA_512_PLUS(SCRAM_SHA_512):
name = 'SCRAM-SHA-512-PLUS'
- _channel_binding = 'p=tls-unique,,'
class AuthFail(Exception):
diff --git a/nbxmpp/structs.py b/nbxmpp/structs.py
index fa6d00d..452a017 100644
--- a/nbxmpp/structs.py
+++ b/nbxmpp/structs.py
@@ -1346,3 +1346,9 @@ class XHTMLData:
if body is not None:
return str(body)
return str(self._bodys.popitem()[1])
+
+
+@dataclass
+class ChannelBindingData:
+ type: str
+ data: bytes
diff --git a/nbxmpp/tcp.py b/nbxmpp/tcp.py
index 1a48b7e..4913a32 100644
--- a/nbxmpp/tcp.py
+++ b/nbxmpp/tcp.py
@@ -86,6 +86,25 @@ class TCPConnection(Connection):
tls_con = self._con.get_base_io_stream()
return tls_con.get_ciphersuite_name()
+ def get_channel_binding_data(
+ self,
+ type_: Gio.TlsChannelBindingType
+ ) -> Optional[bytes]:
+ if self._con is None:
+ return None
+
+ tls_con = self._con.get_base_io_stream()
+
+ try:
+ success, data = tls_con.get_channel_binding_data(type_)
+ except Exception as error:
+ self._log.warning('Unable to get channel binding data: %s', error)
+ return None
+
+ if not success:
+ return None
+ return data
+
def connect(self):
self.state = TCPState.CONNECTING