Skip to content

Commit a448945

Browse files
committed
Remove SHA1 support from RSA key handling
1 parent 3d63eb6 commit a448945

12 files changed

Lines changed: 143 additions & 136 deletions

File tree

paramiko/auth_handler.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,13 @@ def _parse_userauth_request(self, m):
614614
# NOTE: server never wants to guess a client's algo, they're
615615
# telling us directly. No need for _finalize_pubkey_algorithm
616616
# anywhere in this flow.
617+
# TODO: ok is this a spot where it can say a SHA2 dealie in some
618+
# fields but still ssh-rsa within the pubkey blob part?
619+
# TODO: ok so this would be rsa-sha2-256 or w/e, if this field says
620+
# ssh-rsa the request can get stuffed.
617621
algorithm = m.get_text()
622+
# TODO: This part would, if deconstructed, still be allowed to have
623+
# "ssh-rsa" in its first field.
618624
keyblob = m.get_binary()
619625
try:
620626
key = self._generate_key_from_request(algorithm, keyblob)

paramiko/client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,10 +439,32 @@ def connect(
439439

440440
our_server_keys = self._system_host_keys.get(server_hostkey_name)
441441
if our_server_keys is None:
442+
# TODO: this is getting us the test suite _test_connection host key
443+
# setup by virtue of that code doing tc.get_host_keys().add() (that
444+
# method wraps self._host_keys)
445+
# TODO: it should be analogous to running paramiko w/ a
446+
# ~/.ssh/known_hosts file
442447
our_server_keys = self._host_keys.get(server_hostkey_name)
443448
if our_server_keys is not None:
449+
# TODO: keytype needs to turn into one of the rsa-sha2 keys if it's
450+
# an RSAKey.
451+
# TODO: where is the equivalent on our server-side? hopefully the
452+
# tests exercise that lol
453+
# TODO: also, is it just me or does this [0] mean we literally do
454+
# not actually implement real HostKeyAlgorithms agreement like
455+
# ssh.c does?! eesh
444456
keytype = our_server_keys.keys()[0]
445457
sec_opts = t.get_security_options()
458+
# TODO: clean this up a bit, but it does help some tests pass!
459+
if keytype == "ssh-rsa":
460+
if "rsa-sha2-512" in sec_opts.key_types:
461+
keytype = "rsa-sha2-512"
462+
elif "rsa-sha2-256" in sec_opts.key_types:
463+
keytype = "rsa-sha2-256"
464+
else:
465+
raise Exception(
466+
"TODO: REPLACEME with appropriate exception for 'what even is this key type in your known_hosts files?'"
467+
)
446468
other_types = [x for x in sec_opts.key_types if x != keytype]
447469
sec_opts.key_types = [keytype] + other_types
448470

@@ -459,6 +481,10 @@ def connect(
459481
self, server_hostkey_name, server_key
460482
)
461483
else:
484+
# TODO: this should 'just work' but dblcheck (HostKeys will be
485+
# offering an ssh-rsa-via-sha2 key as "ssh-rsa" still, and the
486+
# key would be RSAKey whose .get_name would still say
487+
# "ssh-rsa")
462488
our_key = our_server_keys.get(server_key.get_name())
463489
if our_key != server_key:
464490
if our_key is None:

paramiko/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,7 @@ def _tokenize(self, config, target_hostname, key, value):
445445
# The actual tokens!
446446
replacements = {
447447
# TODO: %%???
448+
# TODO: sha1 bad / this is offspec from rfc/openssh
448449
"%C": sha1(tohash.encode()).hexdigest(),
449450
"%d": homedir,
450451
"%h": configured_hostname,

paramiko/rsakey.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ class RSAKey(PKey):
3838

3939
name = "ssh-rsa"
4040
HASHES = {
41-
"ssh-rsa": hashes.SHA1,
42-
"ssh-rsa-cert-v01@openssh.com": hashes.SHA1,
4341
"rsa-sha2-256": hashes.SHA256,
4442
"rsa-sha2-256-cert-v01@openssh.com": hashes.SHA256,
4543
"rsa-sha2-512": hashes.SHA512,
@@ -81,7 +79,14 @@ def __init__(
8179

8280
@classmethod
8381
def identifiers(cls):
84-
return list(cls.HASHES.keys())
82+
# NOTE: we no longer want to have ssh-rsa+SHA1 in HASHES but we still
83+
# need to advertise we can be used to read ssh-rsa keys (w/ assumption
84+
# other parts of system will enforce the use of SHA2 signing algos).
85+
# Thus, just say so here.
86+
return list(cls.HASHES.keys()) + [
87+
"ssh-rsa",
88+
"ssh-rsa-cert-v01@openssh.com",
89+
]
8590

8691
@property
8792
def size(self):
@@ -126,8 +131,8 @@ def sign_ssh_data(self, data, algorithm=None):
126131
sig = self.key.sign(
127132
data,
128133
padding=padding.PKCS1v15(),
129-
# HASHES being just a map from long identifier to either SHA1 or
130-
# SHA256 - cert'ness is not truly relevant.
134+
# HASHES being just a map from long identifier to algo; cert'ness
135+
# is not truly relevant.
131136
algorithm=self.HASHES[algorithm](),
132137
)
133138
m = Message()

paramiko/sftp_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
from paramiko.sftp_si import SFTPServerInterface
8080
from paramiko.util import b
8181

82+
# TODO: nuke or update w/ newer algs, see below
8283
_hash_class = {"sha1": sha1, "md5": md5}
8384

8485

@@ -302,6 +303,9 @@ def _check_file(self, request_number, msg):
302303
return
303304
f = self.file_table[handle]
304305
for x in alg_list:
306+
# TODO: this only contains sha1 and md5 so uh, is this extension
307+
# actually supported anymore? do we need to update the map for
308+
# newer algos instead? other?
305309
if x in _hash_class:
306310
algname = x
307311
alg = _hash_class[x]

paramiko/transport.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ class Transport(threading.Thread, ClosingContextManager):
204204
"ecdsa-sha2-nistp521",
205205
"rsa-sha2-512",
206206
"rsa-sha2-256",
207-
"ssh-rsa",
208207
)
209208
# ~= PubkeyAcceptedAlgorithms
210209
_preferred_pubkeys = (
@@ -214,7 +213,6 @@ class Transport(threading.Thread, ClosingContextManager):
214213
"ecdsa-sha2-nistp521",
215214
"rsa-sha2-512",
216215
"rsa-sha2-256",
217-
"ssh-rsa",
218216
)
219217
_preferred_kex = (
220218
"ecdh-sha2-nistp256",
@@ -307,12 +305,18 @@ class Transport(threading.Thread, ClosingContextManager):
307305
}
308306

309307
_key_info = {
310-
# TODO: at some point we will want to drop this as it's no longer
311-
# considered secure due to using SHA-1 for signatures. OpenSSH 8.8 no
312-
# longer supports it. Question becomes at what point do we want to
313-
# prevent users with older setups from using this?
314-
"ssh-rsa": RSAKey,
315-
"ssh-rsa-cert-v01@openssh.com": RSAKey,
308+
# TODO: do some downstream uses of this need to be able to 'see'
309+
# ssh-rsa in not-using-SHA1 contexts?
310+
# TODO: NO!!! good.
311+
# TODO: it's used in:
312+
# - Transport._verify_key - verification - do not want ssh-rsa
313+
# - SecurityOptions - only really uses this as a filter for what's
314+
# allowed to be overwritten into its .key_types (which ==
315+
# transport._preferred_keys), and since the latter doesn't want ssh-rsa
316+
# in it, this use case doesn't require that string in here either.
317+
# - AuthHandler._generate_key_from_request - server-side auth
318+
# support - is looking at the 'algorithm' field in the request when it
319+
# references this structure, so yup, do not want ssh-rsa
316320
"rsa-sha2-256": RSAKey,
317321
"rsa-sha2-256-cert-v01@openssh.com": RSAKey,
318322
"rsa-sha2-512": RSAKey,
@@ -1396,11 +1400,13 @@ def connect(
13961400
# TODO: a more robust implementation would be to ask each key class
13971401
# for its nameS plural, and just use that.
13981402
# TODO: that could be used in a bunch of other spots too
1403+
# TODO: don't we have that now, lol
1404+
# TODO: either way this is ~= like using SecurityOptions.key_types
1405+
# = xxx, but different, which sucks sigh
13991406
if isinstance(hostkey, RSAKey):
14001407
self._preferred_keys = [
14011408
"rsa-sha2-512",
14021409
"rsa-sha2-256",
1403-
"ssh-rsa",
14041410
]
14051411
else:
14061412
self._preferred_keys = [hostkey.get_name()]
@@ -2002,6 +2008,8 @@ def _verify_key(self, host_key, sig):
20022008
key: PKey = self._key_info[self.host_key_type](Message(host_key))
20032009
if key is None:
20042010
raise SSHException("Unknown host key type")
2011+
# TODO: like, here, can a host offer "ssh-rsa" but request SHA2, or are
2012+
# those baked in?
20052013
if not key.verify_ssh_sig(self.H, Message(sig)):
20062014
raise SSHException(
20072015
"Signature verification ({}) failed.".format(
@@ -3232,6 +3240,14 @@ def key_types(self):
32323240

32333241
@key_types.setter
32343242
def key_types(self, x):
3243+
# TODO: so this reads Transport._key_info.keys(), yells if any values
3244+
# in `x` /aren't/ in that list, then overwrites
3245+
# Transport._preferred_keys with `x`...
3246+
# TODO: so you can read this pretty simply as "replace
3247+
# transport._preferred_keys with x".
3248+
# TODO: which is...bad...in cases where SSHClient is trying to simply
3249+
# load up known_hosts or system known hosts, and use those to determine
3250+
# which hostkey /algorithms/ it is willing to accept
32353251
self._set("_preferred_keys", "_key_info", x)
32363252

32373253
@property

sites/www/changelog.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22
Changelog
33
=========
44

5+
- :support:`-` Removed support for verifying/signing with RSA keys using SHA-1
6+
hashing. Generally, this means most cases where ``"ssh-rsa"`` was used as an
7+
algorithm identifier (as opposed to a key material identifier) will no longer
8+
accept that string as valid, and the relevant code that actually used eg
9+
`hashes.SHA1` no longer does.
10+
11+
.. warning::
12+
This change is backwards incompatible if you are stuck supporting legacy
13+
systems with Paramiko that are unable to use SHA2-based signatures with RSA
14+
keys (or other workarounds, such as switching from RSA keys to Ed25519
15+
ones).
16+
517
- :bug:`- major` Added a ``password`` kwarg to `PKey.from_type_string
618
<paramiko.pkey.PKey.from_type_string>` so it can handle encrypted keys like
719
most other PKey constructors already could.

tests/_util.py

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -167,42 +167,9 @@ def is_low_entropy():
167167
return is_32bit and os.environ.get("PYTHONHASHSEED", None) == "0"
168168

169169

170-
def sha1_signing_unsupported():
171-
"""
172-
This is used to skip tests in environments where SHA-1 signing is
173-
not supported by the backend.
174-
"""
175-
private_key = rsa.generate_private_key(
176-
public_exponent=65537, key_size=2048, backend=default_backend()
177-
)
178-
message = b"Some dummy text"
179-
try:
180-
private_key.sign(
181-
message,
182-
padding.PSS(
183-
mgf=padding.MGF1(hashes.SHA1()),
184-
salt_length=padding.PSS.MAX_LENGTH,
185-
),
186-
hashes.SHA1(),
187-
)
188-
return False
189-
except UnsupportedAlgorithm as e:
190-
return e._reason == _Reasons.UNSUPPORTED_HASH
191-
192-
193-
requires_sha1_signing = unittest.skipIf(
194-
sha1_signing_unsupported(), "SHA-1 signing not supported"
195-
)
196-
197-
_disable_sha2 = dict(
198-
disabled_algorithms=dict(keys=["rsa-sha2-256", "rsa-sha2-512"])
199-
)
200-
_disable_sha1 = dict(disabled_algorithms=dict(keys=["ssh-rsa"]))
201-
_disable_sha2_pubkey = dict(
202-
disabled_algorithms=dict(pubkeys=["rsa-sha2-256", "rsa-sha2-512"])
203-
)
204-
_disable_sha1_pubkey = dict(disabled_algorithms=dict(pubkeys=["ssh-rsa"]))
205-
170+
# TODO: doublecheck where _disable_xxx helpers were in use; disabling sha1 no
171+
# longer makes any sense, and presumably neither does en/dis abling sha2 but
172+
# that's the question innit
206173

207174
unicodey = "\u2022"
208175

0 commit comments

Comments
 (0)