1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
Derived from: https://github.com/jtesta/ssh-audit/pull/379
commit ae16f4e55128d3dfaa11bcc231bf93cca9181a0f Mon Sep 17 00:00:00 2001
Author: =?UTF-8?q?Nicol=C3=B2=20Sala?= <me@niceo.org>
Date: Wed, 7 Oct 2026 11:17:41 +0200
Subject: [PATCH] Add ssh-mldsa44-ed25519 host key (OpenSSH 10.6)
OpenSSH 10.6 enables the hybrid post-quantum ssh-mldsa44-ed25519
signature algorithm without the "@openssh.com" suffix used by the
previous experimental implementation, which was removed.
- Add ssh-mldsa44-ed25519 (available since OpenSSH 10.6).
- Mark ssh-mldsa44-ed25519@openssh.com as available in OpenSSH
10.4-10.5 only, with a note that it was removed in 10.6.
- Don't recommend adding algorithms to software newer than the last
version that supports them, so OpenSSH 10.6+ no longer gets the old
name suggested.
- Add a test covering the recommendations for OpenSSH 10.3 to 10.7.
Fixes #378
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--- a/README.md
+++ b/README.md
@@ -257,6 +257,7 @@
- Added support for Python 3.15.
- Fixed a perhaps rare crash when performing connection rate tests during standard audits.
- Added `-V`/`--version` flag to print version and terminate; credit [Christian Krause](https://github.com/wookietreiber).
+ - Added 1 new host key: `ssh-mldsa44-ed25519` (the final name used since OpenSSH 10.6). The experimental `ssh-mldsa44-ed25519@openssh.com` was removed in OpenSSH 10.6, so it is now marked as such and is no longer recommended for OpenSSH 10.6 and later.
### v3.9.0 (2026-07-04)
- BIG THANKS to [realmiwi](https://github.com/realmiwi) for being the project's *very first sponsor!!*
--- a/src/ssh_audit/algorithms.py
+++ b/src/ssh_audit/algorithms.py
@@ -141,6 +141,17 @@
break
if not matches:
continue
+ removed = False
+ if (software is not None) and (len(versions) > 1):
+ # The last version supporting this algorithm is the second entry for servers, and the last entry for clients.
+ till_versions = versions[1] if for_server else versions[-1]
+ for v in (till_versions or '').split(','):
+ ssh_prefix, ssh_version, is_cli = Algorithm.get_ssh_version(v)
+ if (not ssh_version) or (ssh_prefix != software.product) or (is_cli and for_server):
+ continue
+ if software.compare_version(ssh_version) > 0:
+ removed = True
+ break
adl, faults = len(alg_desc), 0
for i in range(1, 3):
if not adl > i:
@@ -153,7 +164,7 @@
if faults > 0 or \
(alg_type == 'key' and (('-cert-' in n) or (n.startswith('sk-')))) or \
(alg_type == 'kex' and (n.startswith('ext-info-') or n.startswith('kex-strict-'))) or \
- empty_version:
+ empty_version or removed:
continue
rec[sshv][alg_type]['add'][n] = 0
else:
--- a/src/ssh_audit/ssh2_kexdb.py
+++ b/src/ssh_audit/ssh2_kexdb.py
@@ -77,6 +77,7 @@
INFO_REMOVED_IN_OPENSSH61 = 'removed since OpenSSH 6.1, removed from specification'
INFO_REMOVED_IN_OPENSSH69 = 'removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9'
INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
+ INFO_REMOVED_IN_OPENSSH106 = 'removed in OpenSSH 10.6 (replaced by ssh-mldsa44-ed25519): https://www.openssh.com/txt/release-10.6'
INFO_WITHDRAWN_PQ_ALG = 'the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security'
INFO_EXTENSION_NEGOTIATION = 'pseudo-algorithm that denotes the peer supports RFC8308 extensions'
INFO_STRICT_KEX = 'pseudo-algorithm that denotes the peer supports a stricter key exchange method as a counter-measure to the Terrapin attack (CVE-2023-48795)'
@@ -272,7 +273,8 @@
'ssh-mldsa-65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
'ssh-mldsa-87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
'ssh-mldsa44': [[], [], [], [INFO_NIST_PQC_LEVEL_2]],
- 'ssh-mldsa44-ed25519@openssh.com': [['10.4'], [], [], [INFO_NIST_PQC_LEVEL_2]],
+ 'ssh-mldsa44-ed25519': [['10.6'], [], [], [INFO_NIST_PQC_LEVEL_2]],
+ 'ssh-mldsa44-ed25519@openssh.com': [['10.4', '10.5'], [], [], [INFO_NIST_PQC_LEVEL_2, INFO_REMOVED_IN_OPENSSH106]],
'ssh-mldsa65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
'ssh-mldsa87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
'ssh-rsa1': [[], [FAIL_SHA1]],
--- a/test/test_algorithms.py
+++ b/test/test_algorithms.py
@@ -0,0 +1,41 @@
+import os
+import pytest
+
+from ssh_audit.algorithms import Algorithms
+from ssh_audit.outputbuffer import OutputBuffer
+from ssh_audit.product import Product
+from ssh_audit.software import Software
+from ssh_audit.ssh2_kex import SSH2_Kex
+from ssh_audit.ssh2_kexparty import SSH2_KexParty
+
+
+# pylint: disable=attribute-defined-outside-init
+class TestAlgorithms:
+ @pytest.fixture(autouse=True)
+ def init(self, ssh_audit):
+ cli = SSH2_KexParty([], [], ['none'], [])
+ srv = SSH2_KexParty([], [], ['none'], [])
+ kex = SSH2_Kex(OutputBuffer(), os.urandom(16), ['mlkem768x25519-sha256'], ['ssh-ed25519'], cli, srv, False)
+ self.algs = Algorithms(kex)
+
+ def _key_additions(self, openssh_version):
+ software = Software(None, Product.OpenSSH, openssh_version, None, None)
+ _, rec = self.algs.get_recommendations(software)
+ return rec[2]['key']['add']
+
+ def test_mldsa44_ed25519_recommendations(self):
+ '''Ensures that the experimental "@openssh.com" name is only recommended for OpenSSH 10.4 & 10.5, and the final name from 10.6 onwards.'''
+
+ add = self._key_additions('10.3')
+ assert 'ssh-mldsa44-ed25519@openssh.com' not in add
+ assert 'ssh-mldsa44-ed25519' not in add
+
+ for version in ['10.4', '10.5']:
+ add = self._key_additions(version)
+ assert 'ssh-mldsa44-ed25519@openssh.com' in add
+ assert 'ssh-mldsa44-ed25519' not in add
+
+ for version in ['10.6', '10.7']:
+ add = self._key_additions(version)
+ assert 'ssh-mldsa44-ed25519@openssh.com' not in add
+ assert 'ssh-mldsa44-ed25519' in add
|