Skip to content

Commit 2bc449d

Browse files
committed
Replace ecdsa with PyCA/cryptography
Dependency on ecdsa is a problem for all applications and users that have strict crypto requirements and do not permit custom implementations of cryptographic algorithms. The package even warns that it is potentially insecure and does not protect against side channel attacks. It's less of an issue for pubkeys, but still a compliance issue. Cryptography can handle ecdsa SSH keys without help of ecdsa Python package. The PR replaces the key verification code. I have also included a (mostly untested) re-implementation of ecdsa.key.VerifyingKey. Signed-off-by: Christian Heimes <christian@python.org>
1 parent c81fbf0 commit 2bc449d

4 files changed

Lines changed: 75 additions & 13 deletions

File tree

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
cryptography==3.2
2-
ecdsa==0.13.3
32
yapf==0.21.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
packages=["sshpubkeys"],
3434
test_suite="tests",
3535
python_requires='>=3',
36-
install_requires=['cryptography>=2.1.4', 'ecdsa>=0.13'],
36+
install_requires=['cryptography>=2.5'],
3737
extras_require={
3838
'dev': ['twine', 'wheel', 'yapf'],
3939
},

sshpubkeys/keys.py

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@
2020
from cryptography.hazmat.backends import default_backend
2121
from cryptography.hazmat.primitives.asymmetric.dsa import DSAParameterNumbers, DSAPublicNumbers
2222
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
23+
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
24+
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
25+
from cryptography.hazmat.primitives.asymmetric import ec
26+
from cryptography.hazmat.primitives import hashes
2327
from urllib.parse import urlparse
2428

2529
import base64
2630
import binascii
27-
import ecdsa
2831
import hashlib
2932
import re
3033
import struct
@@ -34,6 +37,57 @@
3437
__all__ = ["AuthorizedKeysFile", "SSHKey"]
3538

3639

40+
class _ECVerifyingKey:
41+
"""ecdsa.key.VerifyingKey reimplementation
42+
"""
43+
def __init__(self, pubkey, default_hashfunc):
44+
self.pubkey = pubkey
45+
self.default_hashfunc = default_hashfunc
46+
47+
@property
48+
def curve(self):
49+
"""Curve instance"""
50+
return self.pubkey.curve
51+
52+
def __repr__(self):
53+
pub_key = self.to_string("compressed")
54+
self.to_string("raw")
55+
return "VerifyingKey({0!r}, {1!r}, {2})".format(
56+
pub_key, self.curve.name, self.default_hashfunc.name
57+
)
58+
59+
def to_string(self, encoding="raw"):
60+
"""Pub key as bytes string"""
61+
if encoding == "raw":
62+
return self.pubkey.public_numbers().encode_point()[1:]
63+
elif encoding == "uncompressed":
64+
return self.pubkey.public_numbers().encode_point()
65+
elif encoding == "compressed":
66+
return self.pubkey.public_bytes(Encoding.X962, PublicFormat.CompressedPoint)
67+
else:
68+
raise ValueError(encoding)
69+
70+
def to_pem(self, point_encoding="uncompressed"):
71+
"""Pub key as PEM"""
72+
if point_encoding != "uncompressed":
73+
raise ValueError(point_encoding)
74+
return self.pubkey.public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
75+
76+
def to_der(self, point_encoding="uncompressed"):
77+
"""Pub key as ASN.1/DER"""
78+
if point_encoding != "uncompressed":
79+
raise ValueError(point_encoding)
80+
return self.pubkey.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
81+
82+
def verify(self, signature, data):
83+
"""Verify signature of provided data"""
84+
return self.pubkey.verify(signature, data, ec.ECDSA(self.default_hashfunc))
85+
86+
def verify_digest(self, signature, digest):
87+
"""Verify signature over prehashed digest"""
88+
return self.pubkey.verify(signature, data, ec.ECDSA(Prehashed(digest)))
89+
90+
3791
class AuthorizedKeysFile: # pylint:disable=too-few-public-methods
3892
"""Represents a full authorized_keys file.
3993
@@ -73,11 +127,11 @@ class SSHKey: # pylint:disable=too-many-instance-attributes
73127
DSA_N_LENGTH = 160
74128

75129
ECDSA_CURVE_DATA = {
76-
b"nistp256": (ecdsa.curves.NIST256p, hashlib.sha256),
77-
b"nistp192": (ecdsa.curves.NIST192p, hashlib.sha256),
78-
b"nistp224": (ecdsa.curves.NIST224p, hashlib.sha256),
79-
b"nistp384": (ecdsa.curves.NIST384p, hashlib.sha384),
80-
b"nistp521": (ecdsa.curves.NIST521p, hashlib.sha512),
130+
b"nistp256": (ec.SECP256R1(), hashes.SHA256()),
131+
b"nistp192": (ec.SECP192R1(), hashes.SHA256()),
132+
b"nistp224": (ec.SECP224R1(), hashes.SHA256()),
133+
b"nistp384": (ec.SECP384R1(), hashes.SHA384()),
134+
b"nistp521": (ec.SECP521R1(), hashes.SHA512())
81135
}
82136

83137
RSA_MIN_LENGTH_STRICT = 1024
@@ -368,12 +422,13 @@ def _process_ecdsa_sha(self, data):
368422

369423
current_position, key_data = self._unpack_by_int(data, current_position)
370424
try:
371-
# data starts with \x04, which should be discarded.
372-
ecdsa_key = ecdsa.VerifyingKey.from_string(key_data[1:], curve, hash_algorithm)
373-
except AssertionError as ex:
425+
ecdsa_pubkey = ec.EllipticCurvePublicKey.from_encoded_point(
426+
curve, key_data
427+
)
428+
except ValueError as ex:
374429
raise InvalidKeyError("Invalid ecdsa key") from ex
375-
self.bits = int(curve_information.replace(b"nistp", b""))
376-
self.ecdsa = ecdsa_key
430+
self.bits = curve.key_size
431+
self.ecdsa = _ECVerifyingKey(ecdsa_pubkey, hash_algorithm)
377432
return current_position
378433

379434
def _process_ed25516(self, data):

tests/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ def check_key(self, pubkey, bits, fingerprint_md5, fingerprint_sha256, options,
4242
self.assertEqual(ssh.comment, comment)
4343
if fingerprint_sha256 is not None:
4444
self.assertEqual(ssh.hash_sha256(), fingerprint_sha256)
45+
if ssh.ecdsa:
46+
ec = ssh.ecdsa
47+
repr(ec)
48+
ec.to_pem()
49+
ec.to_der()
50+
ec.to_string("raw")
51+
ec.to_string("uncompressed")
52+
ec.to_string("compressed")
4553

4654
def check_fail(self, pubkey, expected_error, **kwargs):
4755
""" Checks that key check raises specified exception """

0 commit comments

Comments
 (0)