Skip to content

Commit 3565f34

Browse files
merlokktomato42
authored andcommitted
add ecdh class (#149)
Add support for ECDH operations.
1 parent 79d7d90 commit 3565f34

File tree

4 files changed

+675
-1
lines changed

4 files changed

+675
-1
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,3 +466,18 @@ comp_str = '022799c0d0ee09772fdd337d4f28dc155581951d07082fb19a38aa396b67e77759'
466466
vk = VerifyingKey.from_string(bytearray.fromhex(comp_str), curve=NIST256p)
467467
print(vk.to_string("uncompressed").hex())
468468
```
469+
470+
ECDH key exchange with remote party
471+
472+
```
473+
from ecdsa import ECDH, NIST256p
474+
475+
ecdh = ECDH(curve=NIST256p)
476+
ecdh.generate_private_key()
477+
local_public_key = ecdh.get_public_key()
478+
#send `local_public_key` to remote party and receive `remote_public_key` from remote party
479+
with open("remote_public_key.pem") as e:
480+
remote_public_key = e.read()
481+
ecdh.load_received_public_key_pem(remote_public_key)
482+
secret = ecdh.generate_sharedsecret_bytes()
483+
```

src/ecdsa/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from .curves import NIST192p, NIST224p, NIST256p, NIST384p, NIST521p,\
44
SECP256k1, BRAINPOOLP160r1, BRAINPOOLP192r1, BRAINPOOLP224r1,\
55
BRAINPOOLP256r1, BRAINPOOLP320r1, BRAINPOOLP384r1, BRAINPOOLP512r1
6+
from .ecdh import ECDH, NoKeyError, NoCurveError, InvalidCurveError, \
7+
InvalidSharedSecretError
68
from .der import UnexpectedDER
79

810
# This code comes from http://github.com/warner/python-ecdsa
@@ -14,7 +16,8 @@
1416
"test_pyecdsa", "util", "six"]
1517

1618
_hush_pyflakes = [SigningKey, VerifyingKey, BadSignatureError, BadDigestError,
17-
MalformedPointError, UnexpectedDER,
19+
MalformedPointError, UnexpectedDER, InvalidCurveError,
20+
NoKeyError, InvalidSharedSecretError, ECDH, NoCurveError,
1821
NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1,
1922
BRAINPOOLP160r1, BRAINPOOLP192r1, BRAINPOOLP224r1,
2023
BRAINPOOLP256r1, BRAINPOOLP320r1, BRAINPOOLP384r1,

src/ecdsa/ecdh.py

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
"""
2+
Class for performing Elliptic-curve Diffie-Hellman (ECDH) operations.
3+
"""
4+
5+
from .util import number_to_string
6+
from .ellipticcurve import INFINITY
7+
from .keys import SigningKey, VerifyingKey
8+
9+
10+
__all__ = ["ECDH", "NoKeyError", "NoCurveError", "InvalidCurveError",
11+
"InvalidSharedSecretError"]
12+
13+
14+
class NoKeyError(Exception):
15+
"""ECDH. Key not found but it is needed for operation."""
16+
17+
pass
18+
19+
20+
class NoCurveError(Exception):
21+
"""ECDH. Curve not set but it is needed for operation."""
22+
23+
pass
24+
25+
26+
class InvalidCurveError(Exception):
27+
"""ECDH. Raised in case the public and private keys use different curves."""
28+
29+
pass
30+
31+
32+
class InvalidSharedSecretError(Exception):
33+
"""ECDH. Raised in case the shared secret we obtained is an INFINITY."""
34+
35+
pass
36+
37+
38+
class ECDH(object):
39+
"""
40+
Elliptic-curve Diffie-Hellman (ECDH). A key agreement protocol.
41+
42+
Allows two parties, each having an elliptic-curve public-private key
43+
pair, to establish a shared secret over an insecure channel
44+
"""""
45+
46+
def __init__(self, curve=None, private_key=None, public_key=None):
47+
"""
48+
ECDH init.
49+
50+
Call can be initialised without parameters, then the first operation
51+
(loading either key) will set the used curve.
52+
All parameters must be ultimately set before shared secret
53+
calculation will be allowed.
54+
55+
:param curve: curve for operations
56+
:type curve: Curve
57+
:param private_key: `my` private key for ECDH
58+
:type private_key: SigningKey
59+
:param public_key: `their` public key for ECDH
60+
:type public_key: VerifyingKey
61+
"""
62+
self.curve = curve
63+
self.private_key = None
64+
self.public_key = None
65+
if private_key:
66+
self.load_private_key(private_key)
67+
if public_key:
68+
self.load_received_public_key(public_key)
69+
70+
def _get_shared_secret(self, remote_public_key):
71+
if not self.private_key:
72+
raise NoKeyError(
73+
"Private key needs to be set to create shared secret")
74+
if not self.public_key:
75+
raise NoKeyError(
76+
"Public key needs to be set to create shared secret")
77+
if not (self.private_key.curve == self.curve == remote_public_key.curve):
78+
raise InvalidCurveError(
79+
"Curves for public key and private key is not equal.")
80+
81+
# shared secret = PUBKEYtheirs * PRIVATEKEYours
82+
result = remote_public_key.pubkey.point * self.private_key.privkey.secret_multiplier
83+
if result == INFINITY:
84+
raise InvalidSharedSecretError(
85+
"Invalid shared secret (INFINITY).")
86+
87+
return result.x()
88+
89+
def set_curve(self, key_curve):
90+
"""
91+
Set the working curve for ecdh operations.
92+
93+
:param key_curve: curve from `curves` module
94+
:type key_curve: Curve
95+
"""
96+
self.curve = key_curve
97+
98+
def generate_private_key(self):
99+
"""
100+
Generate local private key for ecdh operation with curve that was set.
101+
102+
:raises NoCurveError: Curve must be set before key generation.
103+
104+
:return: public (verifying) key from this private key.
105+
:rtype: VerifyingKey object
106+
"""
107+
if not self.curve:
108+
raise NoCurveError("Curve must be set prior to key generation.")
109+
return self.load_private_key(SigningKey.generate(curve=self.curve))
110+
111+
def load_private_key(self, private_key):
112+
"""
113+
Load private key from SigningKey (keys.py) object.
114+
115+
Needs to have the same curve as was set with set_curve method.
116+
If curve is not set - it sets from this SigningKey
117+
118+
:param private_key: Initialised SigningKey class
119+
:type private_key: SigningKey
120+
121+
:raises InvalidCurveError: private_key curve not the same as self.curve
122+
123+
:return: public (verifying) key from this private key.
124+
:rtype: VerifyingKey object
125+
"""
126+
if not self.curve:
127+
self.curve = private_key.curve
128+
if self.curve != private_key.curve:
129+
raise InvalidCurveError("Curve mismatch.")
130+
self.private_key = private_key
131+
return self.private_key.get_verifying_key()
132+
133+
def load_private_key_bytes(self, private_key):
134+
"""
135+
Load private key from byte string.
136+
137+
Uses current curve and checks if the provided key matches
138+
the curve of ECDH key agreement.
139+
Key loads via from_string method of SigningKey class
140+
141+
:param private_key: private key in bytes string format
142+
:type private_key: :term:`bytes-like object`
143+
144+
:raises NoCurveError: Curve must be set before loading.
145+
146+
:return: public (verifying) key from this private key.
147+
:rtype: VerifyingKey object
148+
"""
149+
if not self.curve:
150+
raise NoCurveError("Curve must be set prior to key load.")
151+
return self.load_private_key(
152+
SigningKey.from_string(private_key, curve=self.curve))
153+
154+
def load_private_key_der(self, private_key_der):
155+
"""
156+
Load private key from DER byte string.
157+
158+
Compares the curve of the DER-encoded key with the ECDH set curve,
159+
uses the former if unset.
160+
161+
Note, the only DER format supported is the RFC5915
162+
Look at keys.py:SigningKey.from_der()
163+
164+
:param private_key_der: string with the DER encoding of private ECDSA key
165+
:type private_key_der: string
166+
167+
:raises InvalidCurveError: private_key curve not the same as self.curve
168+
169+
:return: public (verifying) key from this private key.
170+
:rtype: VerifyingKey object
171+
"""
172+
return self.load_private_key(SigningKey.from_der(private_key_der))
173+
174+
def load_private_key_pem(self, private_key_pem):
175+
"""
176+
Load private key from PEM string.
177+
178+
Compares the curve of the DER-encoded key with the ECDH set curve,
179+
uses the former if unset.
180+
181+
Note, the only PEM format supported is the RFC5915
182+
Look at keys.py:SigningKey.from_pem()
183+
it needs to have `EC PRIVATE KEY` section
184+
185+
:param private_key_pem: string with PEM-encoded private ECDSA key
186+
:type private_key_pem: string
187+
188+
:raises InvalidCurveError: private_key curve not the same as self.curve
189+
190+
:return: public (verifying) key from this private key.
191+
:rtype: VerifyingKey object
192+
"""
193+
return self.load_private_key(SigningKey.from_pem(private_key_pem))
194+
195+
def get_public_key(self):
196+
"""
197+
Provides a public key that matches the local private key.
198+
199+
Needs to be sent to the remote party.
200+
201+
:return: public (verifying) key from local private key.
202+
:rtype: VerifyingKey object
203+
"""
204+
return self.private_key.get_verifying_key()
205+
206+
def load_received_public_key(self, public_key):
207+
"""
208+
Load public key from VerifyingKey (keys.py) object.
209+
210+
Needs to have the same curve as set as current for ecdh operation.
211+
If curve is not set - it sets it from VerifyingKey.
212+
213+
:param public_key: Initialised VerifyingKey class
214+
:type public_key: VerifyingKey
215+
216+
:raises InvalidCurveError: public_key curve not the same as self.curve
217+
"""
218+
if not self.curve:
219+
self.curve = public_key.curve
220+
if self.curve != public_key.curve:
221+
raise InvalidCurveError("Curve mismatch.")
222+
self.public_key = public_key
223+
224+
def load_received_public_key_bytes(self, public_key_str):
225+
"""
226+
Load public key from byte string.
227+
228+
Uses current curve and checks if key length corresponds to
229+
the current curve.
230+
Key loads via from_string method of VerifyingKey class
231+
232+
:param public_key_str: public key in bytes string format
233+
:type public_key_str: :term:`bytes-like object`
234+
"""
235+
return self.load_received_public_key(
236+
VerifyingKey.from_string(public_key_str, self.curve))
237+
238+
def load_received_public_key_der(self, public_key_der):
239+
"""
240+
Load public key from DER byte string.
241+
242+
Compares the curve of the DER-encoded key with the ECDH set curve,
243+
uses the former if unset.
244+
245+
Note, the only DER format supported is the RFC5912
246+
Look at keys.py:VerifyingKey.from_der()
247+
248+
:param public_key_der: string with the DER encoding of public ECDSA key
249+
:type public_key_der: string
250+
251+
:raises InvalidCurveError: public_key curve not the same as self.curve
252+
"""
253+
return self.load_received_public_key(VerifyingKey.from_der(public_key_der))
254+
255+
def load_received_public_key_pem(self, public_key_pem):
256+
"""
257+
Load public key from PEM string.
258+
259+
Compares the curve of the PEM-encoded key with the ECDH set curve,
260+
uses the former if unset.
261+
262+
Note, the only PEM format supported is the RFC5912
263+
Look at keys.py:VerifyingKey.from_pem()
264+
265+
:param public_key_pem: string with PEM-encoded public ECDSA key
266+
:type public_key_pem: string
267+
268+
:raises InvalidCurveError: public_key curve not the same as self.curve
269+
"""
270+
return self.load_received_public_key(VerifyingKey.from_pem(public_key_pem))
271+
272+
def generate_sharedsecret_bytes(self):
273+
"""
274+
Generate shared secret from local private key and remote public key.
275+
276+
The objects needs to have both private key and received public key
277+
before generation is allowed.
278+
279+
:raises InvalidCurveError: public_key curve not the same as self.curve
280+
:raises NoKeyError: public_key or private_key is not set
281+
282+
:return: shared secret
283+
:rtype: byte string
284+
"""
285+
return number_to_string(
286+
self.generate_sharedsecret(),
287+
self.private_key.curve.order)
288+
289+
def generate_sharedsecret(self):
290+
"""
291+
Generate shared secret from local private key and remote public key.
292+
293+
The objects needs to have both private key and received public key
294+
before generation is allowed.
295+
296+
It's the same for local and remote party.
297+
shared secret(local private key, remote public key ) ==
298+
shared secret (local public key, remote private key)
299+
300+
:raises InvalidCurveError: public_key curve not the same as self.curve
301+
:raises NoKeyError: public_key or private_key is not set
302+
303+
:return: shared secret
304+
:rtype: int
305+
"""
306+
return self._get_shared_secret(self.public_key)

0 commit comments

Comments
 (0)