WIP: Need to fix ed25519 implementation

master
Christophe Mehay 5 years ago committed by Christophe Mehay
parent 68a4ebb08b
commit 1ed76e069e

@ -14,6 +14,7 @@ autopep8 = "*"
[packages]
pycryptodome = "*"
numpy = "*"
[requires]
python_version = "3"

31
Pipfile.lock generated

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "352e3d3e8d72c1b9ced4c95a6c324e3f2a1255a21d5cd6bc8fdabc37ea85d0fc"
"sha256": "ff39f92bdef2a510dcd0a910df87b514de70dfaf7f3e7f044eb67810f5dc4852"
},
"pipfile-spec": 6,
"requires": {
@ -16,6 +16,35 @@
]
},
"default": {
"numpy": {
"hashes": [
"sha256:1980f8d84548d74921685f68096911585fee393975f53797614b34d4f409b6da",
"sha256:22752cd809272671b273bb86df0f505f505a12368a3a5fc0aa811c7ece4dfd5c",
"sha256:23cc40313036cffd5d1873ef3ce2e949bdee0646c5d6f375bf7ee4f368db2511",
"sha256:2b0b118ff547fecabc247a2668f48f48b3b1f7d63676ebc5be7352a5fd9e85a5",
"sha256:3a0bd1edf64f6a911427b608a894111f9fcdb25284f724016f34a84c9a3a6ea9",
"sha256:3f25f6c7b0d000017e5ac55977a3999b0b1a74491eacb3c1aa716f0e01f6dcd1",
"sha256:4061c79ac2230594a7419151028e808239450e676c39e58302ad296232e3c2e8",
"sha256:560ceaa24f971ab37dede7ba030fc5d8fa173305d94365f814d9523ffd5d5916",
"sha256:62be044cd58da2a947b7e7b2252a10b42920df9520fc3d39f5c4c70d5460b8ba",
"sha256:6c692e3879dde0b67a9dc78f9bfb6f61c666b4562fd8619632d7043fb5b691b0",
"sha256:6f65e37b5a331df950ef6ff03bd4136b3c0bbcf44d4b8e99135d68a537711b5a",
"sha256:7a78cc4ddb253a55971115f8320a7ce28fd23a065fc33166d601f51760eecfa9",
"sha256:80a41edf64a3626e729a62df7dd278474fc1726836552b67a8c6396fd7e86760",
"sha256:893f4d75255f25a7b8516feb5766c6b63c54780323b9bd4bc51cdd7efc943c73",
"sha256:972ea92f9c1b54cc1c1a3d8508e326c0114aaf0f34996772a30f3f52b73b942f",
"sha256:9f1d4865436f794accdabadc57a8395bd3faa755449b4f65b88b7df65ae05f89",
"sha256:9f4cd7832b35e736b739be03b55875706c8c3e5fe334a06210f1a61e5c2c8ca5",
"sha256:adab43bf657488300d3aeeb8030d7f024fcc86e3a9b8848741ea2ea903e56610",
"sha256:bd2834d496ba9b1bdda3a6cf3de4dc0d4a0e7be306335940402ec95132ad063d",
"sha256:d20c0360940f30003a23c0adae2fe50a0a04f3e48dc05c298493b51fd6280197",
"sha256:d3b3ed87061d2314ff3659bb73896e622252da52558f2380f12c421fbdee3d89",
"sha256:dc235bf29a406dfda5790d01b998a1c01d7d37f449128c0b1b7d1c89a84fae8b",
"sha256:fb3c83554f39f48f3fa3123b9c24aecf681b1c289f9334f8215c1d3c8e2f6e5b"
],
"index": "pypi",
"version": "==1.16.2"
},
"pycryptodome": {
"hashes": [
"sha256:02838a7184803c05710ccb4f3c9129fe26b2634f04daf017a3490d8be5f55b62",

@ -0,0 +1,145 @@
"""
this code is a cleaned version of http://ed25519.cr.yp.to/python/ed25519.py
for python3
code released under the terms of the GNU Public License v3, copyleft 2015
yoochan
"""
import collections
import hashlib
import os
Point = collections.namedtuple('Point', ['x', 'y'])
key_mask = int.from_bytes(b'\x3F' + b'\xFF' * 30 +
b'\xF8', 'big', signed=False)
class Ed25519():
length = 256
def __init__(self):
self.q = 2**255 - 19
self.l = 2**252 + 27742317777372353535851937790883648493
self.d = -121665 * self.inverse(121666)
self.i = pow(2, (self.q - 1) // 4, self.q)
self.B = self.point(4 * self.inverse(5))
def to_hash(self, m):
return hashlib.sha512(m).digest()
def from_bytes(self, h):
""" pick 32 bytes, return a 256 bit int """
return int.from_bytes(h[0:self.length // 8], 'little', signed=False)
def to_bytes(self, k):
return k.to_bytes(self.length // 8, 'little', signed=False)
def as_key(self, h):
return 2**(self.length - 2) + (self.from_bytes(h) & key_mask)
def to_num(self, h):
return 2**(self.length - 2) + (self.from_bytes(h))
def secret_key(self):
""" pick a random secret key """
m = os.urandom(1024)
h = self.to_hash(m)
k = self.as_key(h)
return self.to_bytes(k)
def public_key(self, sk):
""" compute the public key from the secret one """
return self.public_key_from_hash(self.as_key(self.to_hash(sk)))
def public_key_from_hash(self, hash):
c = self.outer(self.B, hash)
return self.point_to_bytes(c)
def public_key_from_byte_key(self, hash):
c = self.outer(self.B, self.to_num(hash))
return self.point_to_bytes(c)
def inverse(self, x):
return pow(x, self.q - 2, self.q)
def sign(self, message, secret_key, public_key):
s_h = self.to_hash(secret_key)
return self.sign_from_hash(message, s_h, public_key)
def sign_from_hash(self, message, hash, public_key):
s_h = hash
s_d = self.as_key(s_h)
m_h = self.to_hash(s_h[self.length // 8:self.length // 4] + message)
m_d = self.from_bytes(m_h)
R = self.outer(self.B, m_d)
r_h = self.to_hash(self.point_to_bytes(R) + public_key + message)
r_d = m_d + self.from_bytes(r_h) * s_d
return self.point_to_bytes(R) + self.to_bytes(r_d % self.l)
def verify(self, message, signature, public_key):
r_b = signature[0:self.length // 8]
r_h = self.to_hash(r_b + public_key + message)
r_d = self.from_bytes(r_h)
s_d = self.from_bytes(signature[self.length // 8:self.length // 4])
b_j = self.outer(self.B, s_d)
P = self.bytes_to_point(public_key)
p_j = self.outer(P, r_d)
R = self.bytes_to_point(r_b)
return b_j == self.inner(R, p_j)
def recover(self, y):
""" given a value y, recover the preimage x """
p = (y * y - 1) * self.inverse(self.d * y * y + 1)
x = pow(p, (self.q + 3) // 8, self.q)
if (x * x - p) % self.q != 0:
x = (x * self.i) % self.q
if x % 2 != 0:
x = self.q - x
return x
def point(self, y):
""" given a value y, recover x and return the corresponding P(x, y) """
return Point(self.recover(y) % self.q, y % self.q)
def is_on_curve(self, P):
return (
P.y * P.y - P.x * P.x - 1 - self.d * P.x * P.x * P.y * P.y) % self.q == 0
def inner(self, P, Q):
""" inner product on the curve, between two points """
x = (P.x * Q.y + Q.x * P.y) * self.inverse(1 + self.d * P.x * Q.x * P.y * Q.y)
y = (P.y * Q.y + P.x * Q.x) * self.inverse(1 - self.d * P.x * Q.x * P.y * Q.y)
return Point(x % self.q, y % self.q)
def outer(self, P, n):
""" outer product on the curve, between a point and a scalar """
if n == 0:
return Point(0, 1)
Q = self.outer(P, n // 2)
Q = self.inner(Q, Q)
if n & 1:
Q = self.inner(Q, P)
return Q
def point_to_bytes(self, P):
return (P.y + ((P.x & 1) << 255)).to_bytes(self.length // 8, 'little')
def bytes_to_point(self, b):
i = self.from_bytes(b)
y = i % 2**(self.length - 1)
x = self.recover(y)
if (x & 1) != ((i >> (self.length - 1)) & 1):
x = self.q - x
return Point(x, y)

@ -4,9 +4,14 @@ from abc import ABC
from abc import abstractmethod
from base64 import b32encode
from hashlib import sha1
from hashlib import sha3_256
from hashlib import sha512
from typing import BinaryIO
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from .ed25519 import Ed25519
__all__ = [
@ -196,4 +201,96 @@ class OnionV3(Onion):
Tor onion address v3 implement
'''
_header_priv = b'== ed25519v1-secret: type0 ==\x00\x00\x00'
_header_pub = b'== ed25519v1-public: type0 ==\x00\x00\x00'
_test = False
_version = 3
def _save_keypair(self, key: bytes) -> None:
self._priv = key
self._pub = Ed25519().public_key_from_byte_key(key)
def gen_new_private_key(self) -> None:
'Generate new tor ed25519 512 bits key'
random = get_random_bytes(32)
key = bytearray(sha512(random).digest())
key[0] &= 248
key[31] &= 63
key[31] |= 64
self._save_keypair(bytes(key))
def set_private_key(self, key: bytes) -> None:
'Add private key'
if not key.startswith(self._header_priv):
raise Exception(
'Private key does not seems to be a valid ed25519 tor key'
)
parsed_key = key[32:]
if len(parsed_key) != 64:
raise Exception(
'Private key does not seem to have the good lenght'
)
self._save_keypair(parsed_key)
def set_private_key_from_file(self, file: BinaryIO):
'Load private key from file'
self.set_private_key(file.read())
def _get_private_key_has_native(self) -> bytes:
'Get RSA private key like in PEM'
return self._header_priv + self._priv
def get_public_key(self) -> bytes:
'Compute public key'
super().get_public_key()
return self._header_pub + self._pub
def load_hidden_service(self, path: str) -> None:
if not os.path.isdir(path):
raise Exception(
'{path} should be an existing directory'.format(path=path)
)
if 'hs_ed25519_secret_key' not in os.listdir(path):
raise EmptyDirException(
'hs_ed25519_secret_key file not found in {path}'.format(
path=path
)
)
with open(os.path.join(path, 'hs_ed25519_secret_key'), 'rb') as f:
self.set_private_key_from_file(f)
def write_hidden_service(self, path: str = None,
force: bool = False) -> None:
path = path or self._hidden_service_path
if not path:
raise Exception('Missing hidden service path')
if not os.path.exists(path):
raise Exception(
'{path} should be an existing directory'.format(path=path)
)
if os.path.exists(os.path.join(path, 'private_key')) and not force:
raise Exception(
'Use force=True for non empty hidden service directory'
)
with open(os.path.join(path, 'hs_ed25519_secret_key'), 'wb') as f:
f.write(self._get_private_key_has_native())
with open(os.path.join(path, 'hs_ed25519_public_key'), 'wb') as f:
f.write(self.get_public_key())
with open(os.path.join(path, 'hostname'), 'w') as f:
f.write(self.onion_address)
def get_onion_str(self) -> str:
'Compute onion address string'
version_byte = b'\x03'
def checksum(pubkey):
checksum_str = '.onion checksum'.encode('ascii')
return sha3_256(
checksum_str + self._pub + version_byte
).digest()[:2]
return b32encode(
self._pub + checksum(self._pub) + version_byte
).decode().lower()

@ -21,7 +21,7 @@ class _testOnion:
o = self.onion_cls(private_key=self.private_key)
assert o.get_public_key() == self.public_key
assert o.onion_address == self.onion_address
assert o.get_private_key() == self.private_key.encode()
assert o.get_private_key() == self.private_key
def test_generate_key(self):
o = self.onion_cls()
@ -39,7 +39,7 @@ class _testOnion:
def test_import_hidden_directory(self, tmpdir):
d = tmpdir.mkdir("hidden_directory")
f = d.join('private_key')
f.write(self.private_key)
f.write_binary(self.private_key)
o = self.onion_cls(hidden_service_path=d)
assert o.onion_address == self.onion_address
@ -47,7 +47,7 @@ class _testOnion:
d = tmpdir.mkdir("hidden_directory")
o = self.onion_cls(private_key=self.private_key)
o.write_hidden_service(path=str(d))
assert d.join('private_key').read() == self.private_key
assert d.join('private_key').read() == self.private_key.decode()
assert d.join('hostname').read() == o.onion_address
def test_import_empty_hidden_directory(self, tmpdir):
@ -60,7 +60,7 @@ class _testOnion:
def test_import_hidden_directory_with_new_key(self, tmpdir):
d = tmpdir.mkdir("hidden_directory")
f = d.join('private_key')
f.write(self.generate_new_key())
f.write_binary(self.generate_new_key())
o = self.onion_cls(hidden_service_path=d,
private_key=self.private_key)
with pytest.raises(Exception):
@ -91,7 +91,7 @@ l5MQPWBkRKK2pc2Hfj8cdIMi8kJ/1CyCwE6c5l8etR3sbIMRTtZ76nAbXRFkmsRv
La/7Syrnobngsh/vX90CQB+PSSBqiPSsK2yPz6Gsd6OLCQ9sdy2oRwFTasH8sZyl
bhJ3M9WzP/EMkAzyW8mVs1moFp3hRcfQlZHl6g1U9D8=
-----END RSA PRIVATE KEY-----
'''.strip()
'''.strip().encode()
public_key = b64decode('''
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsMP4gl6g1Q313miPhb1GnDr56
@ -101,6 +101,11 @@ Ch5OTBuvMLzQ8W0yDwIDAQAB
''')
onion_address = 'wcet3bgkj4purdfx.onion'
onion_cls = OnionV2
files_in_hidden_dir = {
'priv': 'private_key',
'pub': None,
'onion': 'hostname',
}
version = 2
@ -121,4 +126,9 @@ m9/hW13isA==
)
onion_cls = OnionV3
files_in_hidden_dir = {
'priv': 'hs_ed25519_secret_key',
'pub': 'hs_ed25519_public_key',
'onion': 'hostname',
}
version = 3

Loading…
Cancel
Save