connect fix 4
This commit is contained in:
167
tests/test_phase11.py
Normal file
167
tests/test_phase11.py
Normal file
@ -0,0 +1,167 @@
|
||||
"""
|
||||
Tests fuer Phase 11: fehlendes bcrypt-Modul im Virtualenv.
|
||||
|
||||
Symptom im Betrieb: "Der private Schluessel konnte nicht gelesen werden:
|
||||
OpenSSH private key encryption requires bcrypt with KDF support". Der
|
||||
Schluessel und die (seit Phase 10 speicherbare) Passphrase waren dabei
|
||||
korrekt -- es fehlte schlicht das Modul 'bcrypt', ohne das asyncssh die
|
||||
bcrypt_pbkdf-Ableitung von OpenSSH nicht durchfuehren kann. 'bcrypt' stand
|
||||
nicht in requirements.txt und war deshalb im ausgerollten Virtualenv nicht
|
||||
vorhanden.
|
||||
|
||||
Geprueft wird hier:
|
||||
* die Abhaengigkeit ist deklariert (sonst kommt der Fehler nach dem
|
||||
naechsten Redeploy wieder),
|
||||
* die englische asyncssh-Meldung wird in einen deutschen Klartext
|
||||
uebersetzt, der die Ursache (Serverumgebung, nicht Schluessel/Passphrase)
|
||||
benennt,
|
||||
* beim Anwendungsstart wird gewarnt, statt den Fehler bis zur ersten
|
||||
Benutzersitzung zu verschleppen,
|
||||
* mit installiertem bcrypt laesst sich ein echter, OpenSSH-verschluesselter
|
||||
Schluessel tatsaechlich importieren (End-to-End-Gegenprobe).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from app.ssh_proxy.proxy import (
|
||||
BCRYPT_MISSING_MESSAGE,
|
||||
PrivateKeyUnusableError,
|
||||
bcrypt_kdf_available,
|
||||
import_private_key_material,
|
||||
)
|
||||
|
||||
# Wortlaut von asyncssh (asyncssh/public_key.py) bzw. cryptography.
|
||||
ASYNCSSH_MESSAGE = "OpenSSH private key encryption requires bcrypt with KDF support"
|
||||
CRYPTOGRAPHY_MESSAGE = "Need bcrypt module"
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def test_bcrypt_ist_in_requirements_deklariert():
|
||||
"""Kernursache: ohne diesen Eintrag fehlt bcrypt nach jedem Redeploy."""
|
||||
requirements = (REPO_ROOT / "requirements.txt").read_text(encoding="utf-8")
|
||||
zeilen = [
|
||||
z.strip() for z in requirements.splitlines()
|
||||
if z.strip() and not z.strip().startswith("#")
|
||||
]
|
||||
assert any(z.split("==")[0].split("[")[0].strip() == "bcrypt" for z in zeilen), (
|
||||
"bcrypt muss in requirements.txt stehen -- asyncssh kann sonst keinen "
|
||||
"passphrasegeschuetzten OpenSSH-Schluessel lesen."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("meldung", [ASYNCSSH_MESSAGE, CRYPTOGRAPHY_MESSAGE])
|
||||
@pytest.mark.parametrize("passphrase", [None, "Testpassphrase-2026"])
|
||||
def test_fehlendes_bcrypt_wird_uebersetzt(monkeypatch, meldung, passphrase):
|
||||
"""Die englische Bibliotheksmeldung darf nicht mehr durchschlagen."""
|
||||
import asyncssh
|
||||
|
||||
aufrufe = []
|
||||
|
||||
def _fake_import(pem, pw=None):
|
||||
aufrufe.append(pw)
|
||||
raise ValueError(meldung)
|
||||
|
||||
monkeypatch.setattr(asyncssh, "import_private_key", _fake_import)
|
||||
|
||||
with pytest.raises(PrivateKeyUnusableError) as excinfo:
|
||||
import_private_key_material("egal", passphrase)
|
||||
|
||||
text = str(excinfo.value)
|
||||
assert text == BCRYPT_MISSING_MESSAGE
|
||||
assert "bcrypt" in text
|
||||
assert "requirements.txt" in text
|
||||
# Der Benutzer soll NICHT bei Schluessel/Passphrase suchen:
|
||||
assert "Passphrase" in text and "nicht" in text
|
||||
assert isinstance(excinfo.value.__cause__, ValueError)
|
||||
# Kein zweiter Importversuch ohne Passphrase: die Ursache liegt in der
|
||||
# Umgebung, ein Retry wuerde nur dieselbe Meldung erzeugen.
|
||||
assert len(aufrufe) == 1
|
||||
|
||||
|
||||
def test_andere_importfehler_bleiben_unveraendert(monkeypatch):
|
||||
"""Gegenprobe: die Phase-10-Uebersetzungen duerfen nicht kapern."""
|
||||
import asyncssh
|
||||
|
||||
def _fake_import(pem, pw=None):
|
||||
raise ValueError("Passphrase must be specified to import encrypted private keys")
|
||||
|
||||
monkeypatch.setattr(asyncssh, "import_private_key", _fake_import)
|
||||
|
||||
with pytest.raises(PrivateKeyUnusableError) as excinfo:
|
||||
import_private_key_material("egal", None)
|
||||
assert "keine Passphrase hinterlegt" in str(excinfo.value)
|
||||
assert str(excinfo.value) != BCRYPT_MISSING_MESSAGE
|
||||
|
||||
|
||||
def test_bcrypt_kdf_available_erkennt_modul_ohne_kdf(monkeypatch):
|
||||
"""bcrypt < 3.1.3 bringt kein kdf() mit -- das zaehlt als 'nicht da'."""
|
||||
import sys
|
||||
import types
|
||||
|
||||
attrappe = types.ModuleType("bcrypt")
|
||||
monkeypatch.setitem(sys.modules, "bcrypt", attrappe)
|
||||
assert bcrypt_kdf_available() is False
|
||||
|
||||
attrappe.kdf = lambda **_: b""
|
||||
assert bcrypt_kdf_available() is True
|
||||
|
||||
|
||||
def test_bcrypt_kdf_available_bei_fehlendem_modul(monkeypatch):
|
||||
import builtins
|
||||
|
||||
original = builtins.__import__
|
||||
|
||||
def _import(name, *args, **kwargs):
|
||||
if name == "bcrypt":
|
||||
raise ImportError("No module named 'bcrypt'")
|
||||
return original(name, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(builtins, "__import__", _import)
|
||||
assert bcrypt_kdf_available() is False
|
||||
|
||||
|
||||
def test_startup_warnt_bei_fehlendem_bcrypt(monkeypatch, caplog):
|
||||
"""Der Fehler soll beim Start auffallen, nicht erst in der Sitzung."""
|
||||
import app.main as main
|
||||
|
||||
monkeypatch.setattr(main, "bcrypt_kdf_available", lambda: False)
|
||||
with caplog.at_level(logging.ERROR, logger="jumphost.main"):
|
||||
main._check_optional_dependencies()
|
||||
assert any("bcrypt" in r.message for r in caplog.records)
|
||||
|
||||
caplog.clear()
|
||||
monkeypatch.setattr(main, "bcrypt_kdf_available", lambda: True)
|
||||
with caplog.at_level(logging.ERROR, logger="jumphost.main"):
|
||||
main._check_optional_dependencies()
|
||||
assert caplog.records == []
|
||||
|
||||
|
||||
def test_echter_openssh_schluessel_mit_passphrase_ist_importierbar():
|
||||
"""End-to-End-Gegenprobe mit installiertem bcrypt.
|
||||
|
||||
Erzeugt genau das Format, an dem der Import gescheitert ist (OpenSSH,
|
||||
bcrypt_pbkdf-verschluesselt), und laedt es ueber den regulaeren Codepfad.
|
||||
"""
|
||||
pytest.importorskip("bcrypt", reason="ohne bcrypt ist genau dieser Pfad kaputt")
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
|
||||
passphrase = "Testpassphrase-2026"
|
||||
pem = ed25519.Ed25519PrivateKey.generate().private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.OpenSSH,
|
||||
serialization.BestAvailableEncryption(passphrase.encode()),
|
||||
).decode()
|
||||
assert "OPENSSH PRIVATE KEY" in pem
|
||||
|
||||
schluessel = import_private_key_material(pem, passphrase)
|
||||
assert schluessel is not None
|
||||
|
||||
with pytest.raises(PrivateKeyUnusableError):
|
||||
import_private_key_material(pem, None)
|
||||
Reference in New Issue
Block a user