This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch ssh-audit-677
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git

commit a281800dd7dddd3af03f93034561b5543b5610ad
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Feb 17 15:17:56 2026 +0000

    Use the intersection of algorithms from asyncssh and ssh-audit
---
 atr/ssh.py                     | 18 ++++++++++++++++++
 atr/util.py                    | 39 +++++++++++++++++++++++----------------
 pyproject.toml                 |  1 +
 requirements-for-pip-audit.txt |  2 ++
 uv.lock                        | 13 ++++++++++++-
 5 files changed, 56 insertions(+), 17 deletions(-)

diff --git a/atr/ssh.py b/atr/ssh.py
index b04031a7..673d2203 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -30,6 +30,10 @@ from typing import Any, Final
 import aiofiles
 import aiofiles.os
 import asyncssh
+import asyncssh.encryption as encryption
+import asyncssh.kex as kex
+import asyncssh.mac as mac
+import ssh_audit.builtin_policies as builtin_policies
 
 import atr.attestable as attestable
 import atr.config as config
@@ -43,6 +47,17 @@ import atr.util as util
 
 _CONFIG: Final = config.get()
 
+_SSH_AUDIT_POLICY: Final = builtin_policies.BUILTIN_POLICIES["Hardened OpenSSH 
Server v9.9 (version 1)"]
+
+_ASYNCSSH_SUPPORTED_ENC: Final = {bytes(a) for a in 
encryption.get_encryption_algs()}
+_ASYNCSSH_SUPPORTED_KEX: Final = {bytes(a) for a in kex.get_kex_algs()}
+_ASYNCSSH_SUPPORTED_MAC: Final = {bytes(a) for a in mac.get_mac_algs()}
+
+
+_APPROVED_CIPHERS: Final = util.intersect_algs(_SSH_AUDIT_POLICY, "ciphers", 
_ASYNCSSH_SUPPORTED_ENC)
+_APPROVED_KEX: Final = util.intersect_algs(_SSH_AUDIT_POLICY, "kex", 
_ASYNCSSH_SUPPORTED_KEX)
+_APPROVED_MACS: Final = util.intersect_algs(_SSH_AUDIT_POLICY, "macs", 
_ASYNCSSH_SUPPORTED_MAC)
+
 
 class RsyncArgsError(Exception):
     """Exception raised when the rsync arguments are invalid."""
@@ -178,6 +193,9 @@ async def server_start() -> asyncssh.SSHAcceptor:
         host=_CONFIG.SSH_HOST,
         port=_CONFIG.SSH_PORT,
         encoding=None,
+        encryption_algs=_APPROVED_CIPHERS,
+        kex_algs=_APPROVED_KEX,
+        mac_algs=_APPROVED_MACS,
     )
 
     log.info(f"SSH server started on {_CONFIG.SSH_HOST}:{_CONFIG.SSH_PORT}")
diff --git a/atr/util.py b/atr/util.py
index e03ef41e..3104fefa 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -190,22 +190,6 @@ async def async_temporary_directory(
             log.exception(f"Failed to remove temporary directory 
{temp_dir_path}")
 
 
-async def atomic_write_file(file_path: pathlib.Path, content: str, encoding: 
str = "utf-8") -> None:
-    """Atomically write content to a file using a temporary file."""
-    await aiofiles.os.makedirs(file_path.parent, exist_ok=True)
-    temp_path = file_path.parent / f".{file_path.name}.{uuid.uuid4()}.tmp"
-    try:
-        async with aiofiles.open(temp_path, "w", encoding=encoding) as f:
-            await f.write(content)
-            await f.flush()
-            await asyncio.to_thread(os.fsync, f.fileno())
-        await aiofiles.os.rename(temp_path, file_path)
-    except Exception:
-        with contextlib.suppress(FileNotFoundError):
-            await aiofiles.os.remove(temp_path)
-        raise
-
-
 async def atomic_modify_file(
     file_path: pathlib.Path,
     modify: Callable[[str], str],
@@ -227,6 +211,22 @@ async def atomic_modify_file(
         await asyncio.to_thread(os.close, lock_fd)
 
 
+async def atomic_write_file(file_path: pathlib.Path, content: str, encoding: 
str = "utf-8") -> None:
+    """Atomically write content to a file using a temporary file."""
+    await aiofiles.os.makedirs(file_path.parent, exist_ok=True)
+    temp_path = file_path.parent / f".{file_path.name}.{uuid.uuid4()}.tmp"
+    try:
+        async with aiofiles.open(temp_path, "w", encoding=encoding) as f:
+            await f.write(content)
+            await f.flush()
+            await asyncio.to_thread(os.fsync, f.fileno())
+        await aiofiles.os.rename(temp_path, file_path)
+    except Exception:
+        with contextlib.suppress(FileNotFoundError):
+            await aiofiles.os.remove(temp_path)
+        raise
+
+
 def chmod_directories(path: pathlib.Path, permissions: int = 
DIRECTORY_PERMISSIONS) -> None:
     # codeql[py/overly-permissive-file]
     os.chmod(path, permissions)
@@ -621,6 +621,13 @@ async def has_files(release: sql.Release) -> bool:
     return False
 
 
+def intersect_algs(policy: dict[str, Any], policy_key: str, supported: 
set[bytes]) -> list[str]:
+    algs = policy[policy_key]
+    if not isinstance(algs, list):
+        raise TypeError(f"ssh-audit policy '{policy_key}' is not a list")
+    return [a for a in algs if isinstance(a, str) and (a.encode("ascii") in 
supported)]
+
+
 def is_dev_environment() -> bool:
     conf = config.get()
     for development_host in ("127.0.0.1", "atr", "atr-dev", 
"localhost.apache.org"):
diff --git a/pyproject.toml b/pyproject.toml
index e9baf395..776ffe55 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,7 @@ dependencies = [
   "rich~=14.0.0",
   "semver>=3.0.4",
   "sqlmodel~=0.0.24",
+  "ssh-audit>=3.3.0",
   "standard-imghdr>=3.13.0",
   "strictyaml>=1.7.3",
   "structlog>=25.5.0",
diff --git a/requirements-for-pip-audit.txt b/requirements-for-pip-audit.txt
index b48a0135..13bb9880 100644
--- a/requirements-for-pip-audit.txt
+++ b/requirements-for-pip-audit.txt
@@ -326,6 +326,8 @@ sqlalchemy==2.0.46
     #   sqlmodel
 sqlmodel==0.0.34
     # via tooling-trusted-releases
+ssh-audit==3.3.0
+    # via tooling-trusted-releases
 standard-imghdr==3.13.0
     # via tooling-trusted-releases
 strictyaml==1.7.3
diff --git a/uv.lock b/uv.lock
index efa461cb..45991c4e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,7 +3,7 @@ revision = 3
 requires-python = "==3.13.*"
 
 [options]
-exclude-newer = "2026-02-17T14:20:34Z"
+exclude-newer = "2026-02-17T14:22:10Z"
 
 [[package]]
 name = "aiofiles"
@@ -1837,6 +1837,15 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/eb/ee/1910f4eee41af4268b0d8cd688a05fb8ea23e9e6c64b8710592df24a8c66/sqlmodel-0.0.34-py3-none-any.whl";,
 hash = 
"sha256:aeabc8f0de32076a0ed9216e88568459d737fca1e7133bfc6d1c657920789a2d", size 
= 27445, upload-time = "2026-02-16T19:06:35.709Z" },
 ]
 
+[[package]]
+name = "ssh-audit"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/3b/ec/e89fdfaaa6f08813e1a5cf926bc0dc155761144ebcac57191b4c8001aae3/ssh_audit-3.3.0.tar.gz";,
 hash = 
"sha256:b76e36ac9844f45d64986c9f293a4b46766a10412dc29fb43bd52d0f6661a5b0", size 
= 116958, upload-time = "2024-10-15T21:08:28.632Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/4f/21/2b3cd275bc3c09fc765691f6c005a6683edf47f47c18d2f9eb78c39ca7e6/ssh_audit-3.3.0-py3-none-any.whl";,
 hash = 
"sha256:8d2b22b7bb7c20c9d84452c9f13408015ec76eb025379cc70335e5315e4378d6", size 
= 121789, upload-time = "2024-10-15T21:08:27.182Z" },
+]
+
 [[package]]
 name = "standard-imghdr"
 version = "3.13.0"
@@ -1920,6 +1929,7 @@ dependencies = [
     { name = "rich" },
     { name = "semver" },
     { name = "sqlmodel" },
+    { name = "ssh-audit" },
     { name = "standard-imghdr" },
     { name = "strictyaml" },
     { name = "structlog" },
@@ -1984,6 +1994,7 @@ requires-dist = [
     { name = "rich", specifier = "~=14.0.0" },
     { name = "semver", specifier = ">=3.0.4" },
     { name = "sqlmodel", specifier = "~=0.0.24" },
+    { name = "ssh-audit", specifier = ">=3.3.0" },
     { name = "standard-imghdr", specifier = ">=3.13.0" },
     { name = "strictyaml", specifier = ">=1.7.3" },
     { name = "structlog", specifier = ">=25.5.0" },


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to