Skip to content

Commit fbf9c7a

Browse files
authored
Merge pull request ojarva#86 from tiran/replace_ecdsa
Replace ecdsa with PyCA/cryptography
2 parents c81fbf0 + 2bc449d commit fbf9c7a

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)