From b1ed925e408fc2d4fba4472bd677885bab3c53e5 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Thu, 15 Oct 2026 18:03:42 +0200 Subject: [PATCH] Fall back to libcrypt when no crypt module imports Python 3.13 dropped crypt from the stdlib, and openSUSE Leap 16 packages neither legacycrypt nor crypt_r, so the server did not start there without a pip install. Both of those only call crypt(3) from libcrypt, which every distribution ships, so do that through ctypes as the last resort, serialized because crypt(3) returns a static buffer. Its hashes match the stdlib module byte for byte. Move the import chain both callers carried into confluent.pwcrypt. The legacycrypt Recommends is no longer needed. --- .../confluent/config/configmanager.py | 10 +-- confluent_server/confluent/pwcrypt.py | 66 +++++++++++++++++++ confluent_server/confluent/selfservice.py | 10 +-- confluent_server/confluent_server.spec.tmpl | 6 -- 4 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 confluent_server/confluent/pwcrypt.py diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 71ff5cef..1957c303 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -69,15 +69,7 @@ import confluent.netutil as netutil import confluent.exceptions as exc import confluent.log import copy -try: - import crypt -except ImportError: - # crypt left the stdlib in 3.13. Both of these reach the same libcrypt - # call and give the same hash; distros package one, the other, or neither. - try: - import legacycrypt as crypt - except ImportError: - import crypt_r as crypt +import confluent.pwcrypt as crypt try: import cPickle except ModuleNotFoundError: diff --git a/confluent_server/confluent/pwcrypt.py b/confluent_server/confluent/pwcrypt.py new file mode 100644 index 00000000..18560c9d --- /dev/null +++ b/confluent_server/confluent/pwcrypt.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2026 MEGWARE Computer Vertrieb und Service GmbH +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# crypt(3) for hashing and checking the crypted.* attributes. crypt left the +# stdlib in 3.13, so prefer it, then the packaged replacements (legacycrypt, +# crypt_r), and finally call libcrypt directly, which is all the others do. +# Every Linux distribution ships libcrypt, while some package none of the +# Python modules (openSUSE Leap 16). + +try: + from crypt import crypt +except ImportError: + try: + from legacycrypt import crypt + except ImportError: + try: + from crypt_r import crypt + except ImportError: + crypt = None + +if crypt is None: + import ctypes + import ctypes.util + import threading + + def _load_libcrypt(): + # libxcrypt is libcrypt.so.2 where it is built without the glibc + # compatible API, libcrypt.so.1 otherwise + names = [ctypes.util.find_library('crypt'), 'libcrypt.so.2', + 'libcrypt.so.1'] + for name in names: + if not name: + continue + try: + lib = ctypes.CDLL(name, use_errno=True) + except OSError: + continue + lib.crypt.argtypes = (ctypes.c_char_p, ctypes.c_char_p) + lib.crypt.restype = ctypes.c_char_p + return lib + raise ImportError('no crypt module and no libcrypt found') + + _libcrypt = _load_libcrypt() + # crypt(3) returns a pointer to a static buffer + _lock = threading.Lock() + + def crypt(word, salt): + with _lock: + ret = _libcrypt.crypt(word.encode('utf8'), salt.encode('utf8')) + if ret is None: + errno = ctypes.get_errno() + raise OSError(errno, 'crypt failed') + return ret.decode('utf8') diff --git a/confluent_server/confluent/selfservice.py b/confluent_server/confluent/selfservice.py index e65d91a4..bd57b29b 100644 --- a/confluent_server/confluent/selfservice.py +++ b/confluent_server/confluent/selfservice.py @@ -15,15 +15,7 @@ import confluent.discovery.core as disco import base64 import hmac import hashlib -try: - import crypt -except ImportError: - # crypt left the stdlib in 3.13. Both of these reach the same libcrypt - # call and give the same hash; distros package one, the other, or neither. - try: - import legacycrypt as crypt - except ImportError: - import crypt_r as crypt +import confluent.pwcrypt as crypt import json import os import tempfile diff --git a/confluent_server/confluent_server.spec.tmpl b/confluent_server/confluent_server.spec.tmpl index 056be11e..42c723fb 100644 --- a/confluent_server/confluent_server.spec.tmpl +++ b/confluent_server/confluent_server.spec.tmpl @@ -17,12 +17,6 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot Prefix: %{_prefix} BuildArch: noarch Requires: confluent_vtbufferd -%if 0%{?python3_version_nodots} >= 313 -# 3.13 removed crypt from the stdlib, and stored credentials are hashed with -# it. Recommends rather than Requires: some 3.13 distros package no candidate -# at all, where a hard dependency would make the rpm uninstallable. -Recommends: python3-legacycrypt -%endif %if "%{dist}" == ".el9" Requires: python3-asyncssh, python3-cryptography, confluent_client == %{version}, python3-pyparsing, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL python3-msgpack python3-libarchive-c python3-yaml python3-yarl python3-aiohttp openssl iproute # httpapi drops passkey support without webauthn, and only BIOS settings on