From 1ee4490e8e2bf750e275bc096dd80357d7747c57 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 29 Mar 2022 12:27:14 +0200 Subject: [PATCH 1/2] bpo-47102: Linux Kernel CryptoAPI bindings (WIP) --- Lib/hashlib.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++- Lib/hmac.py | 8 +++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/Lib/hashlib.py b/Lib/hashlib.py index b546a3fd795311..14bed4a0bd9e8d 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -94,7 +94,8 @@ def __get_builtin_constructor(name): elif name in {'SHA256', 'sha256', 'SHA224', 'sha224'}: import _sha256 cache['SHA224'] = cache['sha224'] = _sha256.sha224 - cache['SHA256'] = cache['sha256'] = _sha256.sha256 + # cache['SHA256'] = cache['sha256'] = _sha256.sha256 + cache['SHA256'] = cache['sha256'] = linux_sha256 elif name in {'SHA512', 'sha512', 'SHA384', 'sha384'}: import _sha512 cache['SHA384'] = cache['sha384'] = _sha512.sha384 @@ -254,6 +255,122 @@ def prf(msg, inner=inner, outer=outer): pass +import socket as _socket +import binascii as _binascii + +class _LinuxKCAPI: + """Linux Kernel Crypto API (AF_ALG socket) + """ + + # TODO: retrieve info from AF_NETLINK, NETLINK_CRYPTO + _digests = { + 'md5': (16, 64), + 'sha1': (20, 64), + 'sha224': (28, 64), + 'sha256': (32, 64), + 'sha384': (48, 128), + 'sha512': (64, 128), + } + __slots__ = ("_digest", "_opsock") + + def __init__(self, digest): + if digest not in self._digests: + raise ValueError(digest) + self._digest = digest + + def _get_opsock(self, digest, key=None): + """Create KCAPI client socket + + Algorithm and MAC key are configured on the server socket. accept() + creates a new client socket that consumes data. accept() on a client + socket creates an independent copy. + """ + with _socket.socket(_socket.AF_ALG, _socket.SOCK_SEQPACKET, 0) as cfg: + binding = ("hash", digest if key is None else f"hmac({digest})") + try: + cfg.bind(binding) + except FileNotFoundError: + raise ValueError(binding) + if key is not None: + cfg.setsockopt(_socket.SOL_ALG, _socket.ALG_SET_KEY, key) + return cfg.accept()[0] + + def __del__(self): + if getattr(self, "_opsock", None): + self._opsock.close() + self._opsock = None + + def __repr__(self): + return f"" + + @property + def digest_size(self): + return self._digests[self._digest][0] + + @property + def block_size(self): + return self._digests[self._digest][1] + + def update(self, data): + self._opsock.sendall(data, _socket.MSG_MORE) + + def copy(self): + new = self.__new__(type(self)) + new._digest = self._digest + new._opsock = self._opsock.accept()[0] + return new + + def digest(self): + copysock = self._opsock.accept()[0] + with copysock: + copysock.send(b'') + return copysock.recv(64) + + def hexdigest(self): + return _binascii.hexlify(self.digest()).decode("ascii") + + +class _LinuxKCAPIHash(_LinuxKCAPI): + def __init__(self, name, data=None, usedforsecurity=True): + super().__init__(name) + # ignores usedforsecurity + self._opsock = self._get_opsock(name) + if data is not None: + self.update(data) + + @property + def name(self): + return self._digest + + +class _LinuxKCAPIHMAC(_LinuxKCAPI): + def __init__(self, key, msg=None, digestmod=''): + if not isinstance(key, (bytes, bytearray)): + raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__) + + if not digestmod: + raise TypeError("Missing required parameter 'digestmod'.") + elif isinstance(digestmod, str): + digest = digestmod + elif callable(digestmod): + digest = digestmod().name + else: + digest = digestmod.new().name + + super().__init__(digest) + self._opsock = self._get_opsock(digest, key) + if msg is not None: + self.update(msg) + + @property + def name(self): + return f"hmac-{self._digest}" + + +def linux_sha256(data=None, usedforsecurity=True): + return _LinuxKCAPIHash("sha256", data, usedforsecurity=usedforsecurity) + + def file_digest(fileobj, digest, /, *, _bufsize=2**18): """Hash the contents of a file-like object. Returns a digest object. diff --git a/Lib/hmac.py b/Lib/hmac.py index 8b4f920db954ca..d0ebf5ccb0a46a 100644 --- a/Lib/hmac.py +++ b/Lib/hmac.py @@ -16,6 +16,8 @@ import hashlib as _hashlib +import sys as _sys + trans_5C = bytes((x ^ 0x5C) for x in range(256)) trans_36 = bytes((x ^ 0x36) for x in range(256)) @@ -55,7 +57,11 @@ def __init__(self, key, msg=None, digestmod=''): if not digestmod: raise TypeError("Missing required parameter 'digestmod'.") - if _hashopenssl and isinstance(digestmod, (str, _functype)): + if _sys.platform == "linux": + self._hmac = _hashlib._LinuxKCAPIHMAC(key, msg, digestmod) + self.digest_size = self._hmac.digest_size + self.block_size = self._hmac.block_size + elif _hashopenssl and isinstance(digestmod, (str, _functype)): try: self._init_hmac(key, msg, digestmod) except _hashopenssl.UnsupportedDigestmodError: From 8e8dcb71c1ff24e323a90a2ac120e540d6bbf839 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 30 Mar 2022 17:58:07 +0200 Subject: [PATCH 2/2] Address review comments, refactor code --- Lib/hashlib.py | 88 +++++++++++++++++++++++++++++----------- Lib/test/test_hashlib.py | 6 +++ Lib/test/test_hmac.py | 1 + 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/Lib/hashlib.py b/Lib/hashlib.py index 14bed4a0bd9e8d..64c322322146fc 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -257,6 +257,7 @@ def prf(msg, inner=inner, outer=outer): import socket as _socket import binascii as _binascii +import warnings as _warnings class _LinuxKCAPI: """Linux Kernel Crypto API (AF_ALG socket) @@ -271,34 +272,66 @@ class _LinuxKCAPI: 'sha384': (48, 128), 'sha512': (64, 128), } - __slots__ = ("_digest", "_opsock") + __slots__ = ("_digest", "_kcapi_sock") def __init__(self, digest): + digest = digest.lower() # SHA256 -> sha256 if digest not in self._digests: - raise ValueError(digest) + raise ValueError(f"Kernel Crypto API does not support {digest}.") self._digest = digest - def _get_opsock(self, digest, key=None): + def __getstate__(self): + raise TypeError(f"cannot pickle {self.__class__.__name__!r} object") + + def __del__(self): + if getattr(self, "_kcapi_sock", None): + self._kcapi_sock.close() + self._kcapi_sock = None + + def _get_kcapi_sock(self, digest, mac_key=None): """Create KCAPI client socket Algorithm and MAC key are configured on the server socket. accept() creates a new client socket that consumes data. accept() on a client socket creates an independent copy. + + https://www.kernel.org/doc/html/v5.17/crypto/userspace-if.html """ with _socket.socket(_socket.AF_ALG, _socket.SOCK_SEQPACKET, 0) as cfg: - binding = ("hash", digest if key is None else f"hmac({digest})") + algo = digest if mac_key is None else f"hmac({digest})" + binding = ("hash", algo) try: cfg.bind(binding) except FileNotFoundError: - raise ValueError(binding) - if key is not None: - cfg.setsockopt(_socket.SOL_ALG, _socket.ALG_SET_KEY, key) + raise ValueError( + f"Kernel Crypto API does not support {algo}." + ) + if mac_key is not None: + # Linux Kernel 5.17 docs are incorrect. AF_ALG setsockopt() + # requires a non-connected 'server' socket. + # if (sock->state == SS_CONNECTED) return ENOPROTOOPT; + cfg.setsockopt(_socket.SOL_ALG, _socket.ALG_SET_KEY, mac_key) return cfg.accept()[0] - def __del__(self): - if getattr(self, "_opsock", None): - self._opsock.close() - self._opsock = None + def _digest_by_digestmod(self, digestmod): + """Get digest object and name + """ + if isinstance(digestmod, str): + return None, digestmod + elif callable(digestmod): + if _hashlib is not None: + # _hashopenssl.c constructor? + try: + return None, _hashlib._constructors[digestmod] + except KeyError: + pass + # callable + digestobj = digestmod() + return digestobj, digestobj.name + else: + # digest module with new() function + digestobj = digestmod.new() + return digestobj, digestobj.name def __repr__(self): return f"" @@ -312,16 +345,16 @@ def block_size(self): return self._digests[self._digest][1] def update(self, data): - self._opsock.sendall(data, _socket.MSG_MORE) + self._kcapi_sock.sendall(data, _socket.MSG_MORE) def copy(self): new = self.__new__(type(self)) new._digest = self._digest - new._opsock = self._opsock.accept()[0] + new._kcapi_sock = self._kcapi_sock.accept()[0] return new def digest(self): - copysock = self._opsock.accept()[0] + copysock = self._kcapi_sock.accept()[0] with copysock: copysock.send(b'') return copysock.recv(64) @@ -334,7 +367,7 @@ class _LinuxKCAPIHash(_LinuxKCAPI): def __init__(self, name, data=None, usedforsecurity=True): super().__init__(name) # ignores usedforsecurity - self._opsock = self._get_opsock(name) + self._kcapi_sock = self._get_kcapi_sock(name) if data is not None: self.update(data) @@ -350,15 +383,22 @@ def __init__(self, key, msg=None, digestmod=''): if not digestmod: raise TypeError("Missing required parameter 'digestmod'.") - elif isinstance(digestmod, str): - digest = digestmod - elif callable(digestmod): - digest = digestmod().name - else: - digest = digestmod.new().name - - super().__init__(digest) - self._opsock = self._get_opsock(digest, key) + digestobj, name = self._digest_by_digestmod(digestmod) + super().__init__(name) + if digestobj: + if not hasattr(digestobj, "block_size"): + _warnings.warn( + "No block_size attribute on given digest object.", + RuntimeWarning, 2 + ) + elif digestobj.block_size != self.block_size: + _warnings.warn( + f"digest object block_size {digestobj.block_size} does " + f"not match KCAPI block_size {self.block_size}.", + RuntimeWarning, 2 + ) + + self._kcapi_sock = self._get_kcapi_sock(name, key) if msg is not None: self.update(msg) diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index d2a92147d5f024..5906c930579e6b 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -968,6 +968,9 @@ def test_disallow_instantiation(self): h = constructor() except ValueError: continue + if isinstance(h, hashlib._LinuxKCAPI): + # Linux KCAPI classes are pure Python + continue with self.subTest(constructor=constructor): support.check_disallow_instantiation(self, type(h)) @@ -986,6 +989,9 @@ def test_readonly_types(self): hash_type = type(constructor()) except ValueError: continue + if issubclass(hash_type, hashlib._LinuxKCAPI): + # Linux KCAPI classes are pure Python + continue with self.subTest(hash_type=hash_type): with self.assertRaisesRegex(TypeError, "immutable type"): hash_type.value = False diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 7cf99735ca39f0..af973c6b989730 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -343,6 +343,7 @@ def test_sha512_rfc4231(self): def test_legacy_block_size_warnings(self): class MockCrazyHash(object): """Ain't no block_size attribute here.""" + name = "sha256" def __init__(self, *args): self._x = hashlib.sha256(*args) self.digest_size = self._x.digest_size