diff --git a/Makefile b/Makefile index fcebe0d..28a4582 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,5 @@ lintroll: flake8 $(FILES_TO_LINT) protobufs: + cd libp2p/crypto/pb && protoc --python_out=. crypto.proto cd libp2p/pubsub/pb && protoc --python_out=. rpc.proto diff --git a/libp2p/__init__.py b/libp2p/__init__.py index 6dd42cd..af72cb8 100644 --- a/libp2p/__init__.py +++ b/libp2p/__init__.py @@ -1,8 +1,7 @@ import asyncio from typing import Mapping, Sequence -from Crypto.PublicKey import RSA - +from libp2p.crypto.rsa import create_new_key_pair from libp2p.host.basic_host import BasicHost from libp2p.kademlia.network import KademliaServer from libp2p.kademlia.storage import IStorage @@ -34,9 +33,10 @@ async def cleanup_done_tasks() -> None: await asyncio.sleep(3) -def generate_id() -> ID: - new_key = RSA.generate(2048, e=65537).publickey().export_key("DER") - new_id = ID.from_pubkey(new_key) +def generate_peer_id_from_rsa_identity() -> ID: + new_key_pair = create_new_key_pair() + new_public_key = new_key_pair.public_key + new_id = ID.from_pubkey(new_public_key) return new_id @@ -53,7 +53,7 @@ def initialize_default_kademlia_router( :return: return a default kademlia instance """ if not id_opt: - id_opt = generate_id() + id_opt = generate_peer_id_from_rsa_identity() node_id = id_opt.to_bytes() # ignore type for Kademlia module @@ -83,7 +83,7 @@ def initialize_default_swarm( """ if not id_opt: - id_opt = generate_id() + id_opt = generate_peer_id_from_rsa_identity() # TODO parse transport_opt to determine transport transport_opt = transport_opt or ["/ip4/127.0.0.1/tcp/8001"] @@ -124,7 +124,7 @@ async def new_node( """ if not id_opt: - id_opt = generate_id() + id_opt = generate_peer_id_from_rsa_identity() if not swarm_opt: swarm_opt = initialize_default_swarm( diff --git a/libp2p/crypto/__init__.py b/libp2p/crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/libp2p/crypto/keys.py b/libp2p/crypto/keys.py new file mode 100644 index 0000000..9751705 --- /dev/null +++ b/libp2p/crypto/keys.py @@ -0,0 +1,82 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum, unique + +from .pb import crypto_pb2 as protobuf + + +@unique +class KeyType(Enum): + RSA = 0 + Ed25519 = 1 + Secp256k1 = 2 + ECDSA = 3 + + +class Key: + """ + A ``Key`` represents a cryptographic key. + """ + + @abstractmethod + def to_bytes(self) -> bytes: + """ + Returns the byte representation of this key. + """ + ... + + @abstractmethod + def get_type(self) -> KeyType: + """ + Returns the ``KeyType`` for ``self``. + """ + ... + + +class PublicKey(ABC, Key): + """ + A ``PublicKey`` represents a cryptographic public key. + """ + + @abstractmethod + def verify(self, data: bytes, signature: bytes) -> bool: + """ + Verify that ``signature`` is the cryptographic signature of the hash of ``data``. + """ + ... + + def serialize_to_protobuf(self) -> protobuf.PublicKey: + _type = self.get_type() + data = self.to_bytes() + protobuf_key = protobuf.PublicKey() + protobuf_key.key_type = _type.value + protobuf_key.data = data + return protobuf_key + + +class PrivateKey(ABC, Key): + """ + A ``PrivateKey`` represents a cryptographic private key. + """ + + @abstractmethod + def sign(self, data: bytes) -> bytes: + ... + + @abstractmethod + def get_public_key(self) -> PublicKey: + ... + + def serialize_to_protobuf(self) -> protobuf.PrivateKey: + _type = self.get_type() + data = self.to_bytes() + protobuf_key = protobuf.PrivateKey() + protobuf_key.key_type = _type.value + protobuf_key.data = data + return protobuf_key + + +@dataclass(frozen=True) +class KeyPair: + private_key: PrivateKey + public_key: PublicKey diff --git a/libp2p/crypto/pb/crypto.proto b/libp2p/crypto/pb/crypto.proto new file mode 100644 index 0000000..a12f7b6 --- /dev/null +++ b/libp2p/crypto/pb/crypto.proto @@ -0,0 +1,20 @@ +syntax = "proto2"; + +package crypto.pb; + +enum KeyType { + RSA = 0; + Ed25519 = 1; + Secp256k1 = 2; + ECDSA = 3; +} + +message PublicKey { + required KeyType key_type = 1; + required bytes data = 2; +} + +message PrivateKey { + required KeyType key_type = 1; + required bytes data = 2; +} \ No newline at end of file diff --git a/libp2p/crypto/pb/crypto_pb2.py b/libp2p/crypto/pb/crypto_pb2.py new file mode 100644 index 0000000..0e4aa1f --- /dev/null +++ b/libp2p/crypto/pb/crypto_pb2.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: crypto.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='crypto.proto', + package='crypto.pb', + syntax='proto2', + serialized_options=None, + serialized_pb=_b('\n\x0c\x63rypto.proto\x12\tcrypto.pb\"?\n\tPublicKey\x12$\n\x08key_type\x18\x01 \x02(\x0e\x32\x12.crypto.pb.KeyType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x02(\x0c\"@\n\nPrivateKey\x12$\n\x08key_type\x18\x01 \x02(\x0e\x32\x12.crypto.pb.KeyType\x12\x0c\n\x04\x64\x61ta\x18\x02 \x02(\x0c*9\n\x07KeyType\x12\x07\n\x03RSA\x10\x00\x12\x0b\n\x07\x45\x64\x32\x35\x35\x31\x39\x10\x01\x12\r\n\tSecp256k1\x10\x02\x12\t\n\x05\x45\x43\x44SA\x10\x03') +) + +_KEYTYPE = _descriptor.EnumDescriptor( + name='KeyType', + full_name='crypto.pb.KeyType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='RSA', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Ed25519', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='Secp256k1', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ECDSA', index=3, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=158, + serialized_end=215, +) +_sym_db.RegisterEnumDescriptor(_KEYTYPE) + +KeyType = enum_type_wrapper.EnumTypeWrapper(_KEYTYPE) +RSA = 0 +Ed25519 = 1 +Secp256k1 = 2 +ECDSA = 3 + + + +_PUBLICKEY = _descriptor.Descriptor( + name='PublicKey', + full_name='crypto.pb.PublicKey', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key_type', full_name='crypto.pb.PublicKey.key_type', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='data', full_name='crypto.pb.PublicKey.data', index=1, + number=2, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=27, + serialized_end=90, +) + + +_PRIVATEKEY = _descriptor.Descriptor( + name='PrivateKey', + full_name='crypto.pb.PrivateKey', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='key_type', full_name='crypto.pb.PrivateKey.key_type', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='data', full_name='crypto.pb.PrivateKey.data', index=1, + number=2, type=12, cpp_type=9, label=2, + has_default_value=False, default_value=_b(""), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=92, + serialized_end=156, +) + +_PUBLICKEY.fields_by_name['key_type'].enum_type = _KEYTYPE +_PRIVATEKEY.fields_by_name['key_type'].enum_type = _KEYTYPE +DESCRIPTOR.message_types_by_name['PublicKey'] = _PUBLICKEY +DESCRIPTOR.message_types_by_name['PrivateKey'] = _PRIVATEKEY +DESCRIPTOR.enum_types_by_name['KeyType'] = _KEYTYPE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +PublicKey = _reflection.GeneratedProtocolMessageType('PublicKey', (_message.Message,), dict( + DESCRIPTOR = _PUBLICKEY, + __module__ = 'crypto_pb2' + # @@protoc_insertion_point(class_scope:crypto.pb.PublicKey) + )) +_sym_db.RegisterMessage(PublicKey) + +PrivateKey = _reflection.GeneratedProtocolMessageType('PrivateKey', (_message.Message,), dict( + DESCRIPTOR = _PRIVATEKEY, + __module__ = 'crypto_pb2' + # @@protoc_insertion_point(class_scope:crypto.pb.PrivateKey) + )) +_sym_db.RegisterMessage(PrivateKey) + + +# @@protoc_insertion_point(module_scope) diff --git a/libp2p/crypto/rsa.py b/libp2p/crypto/rsa.py new file mode 100644 index 0000000..ceea008 --- /dev/null +++ b/libp2p/crypto/rsa.py @@ -0,0 +1,50 @@ +import Crypto.PublicKey.RSA as RSA +from Crypto.PublicKey.RSA import RsaKey + +from libp2p.crypto.keys import KeyPair, KeyType, PrivateKey, PublicKey + + +class RSAPublicKey(PublicKey): + def __init__(self, impl: RsaKey) -> None: + self.impl = impl + + def to_bytes(self) -> bytes: + return self.impl.export_key("DER") + + def get_type(self) -> KeyType: + return KeyType.RSA + + def verify(self, data: bytes, signature: bytes) -> bool: + raise NotImplementedError + + +class RSAPrivateKey(PrivateKey): + def __init__(self, impl: RsaKey) -> None: + self.impl = impl + + @classmethod + def new(cls, bits: int = 2048, e: int = 65537) -> "RSAPrivateKey": + private_key_impl = RSA.generate(bits, e=e) + return cls(private_key_impl) + + def to_bytes(self) -> bytes: + return self.impl.export_key("DER") + + def get_type(self) -> KeyType: + return KeyType.RSA + + def sign(self, data: bytes) -> bytes: + raise NotImplementedError + + def get_public_key(self) -> PublicKey: + return RSAPublicKey(self.impl.publickey()) + + +def create_new_key_pair(bits: int = 2048, e: int = 65537) -> KeyPair: + """ + Returns a new RSA keypair with the requested key size (``bits``) and the given public + exponent ``e``. Sane defaults are provided for both values. + """ + private_key = RSAPrivateKey.new(bits, e) + public_key = private_key.get_public_key() + return KeyPair(private_key, public_key) diff --git a/libp2p/crypto/secp256k1.py b/libp2p/crypto/secp256k1.py new file mode 100644 index 0000000..79ffc9d --- /dev/null +++ b/libp2p/crypto/secp256k1.py @@ -0,0 +1,52 @@ +import coincurve + +from libp2p.crypto.keys import KeyPair, KeyType, PrivateKey, PublicKey + + +class Secp256k1PublicKey(PublicKey): + def __init__(self, impl: coincurve.PublicKey) -> None: + self.impl = impl + + def to_bytes(self) -> bytes: + return self.impl.format() + + def get_type(self) -> KeyType: + return KeyType.Secp256k1 + + def verify(self, data: bytes, signature: bytes) -> bool: + raise NotImplementedError + + +class Secp256k1PrivateKey(PrivateKey): + def __init__(self, impl: coincurve.PrivateKey) -> None: + self.impl = impl + + @classmethod + def new(cls, secret: bytes = None) -> "Secp256k1PrivateKey": + private_key_impl = coincurve.PrivateKey(secret) + return cls(private_key_impl) + + def to_bytes(self) -> bytes: + return self.impl.secret + + def get_type(self) -> KeyType: + return KeyType.Secp256k1 + + def sign(self, data: bytes) -> bytes: + raise NotImplementedError + + def get_public_key(self) -> PublicKey: + public_key_impl = coincurve.PublicKey.from_secret(self.impl.secret) + return Secp256k1PublicKey(public_key_impl) + + +def create_new_key_pair(secret: bytes = None) -> KeyPair: + """ + Returns a new Secp256k1 keypair derived from the provided ``secret``, + a sequence of bytes corresponding to some integer between 0 and the group order. + + A valid secret is created if ``None`` is passed. + """ + private_key = Secp256k1PrivateKey.new(secret) + public_key = private_key.get_public_key() + return KeyPair(private_key, public_key) diff --git a/libp2p/peer/id.py b/libp2p/peer/id.py index 9e39cf0..bfb78d0 100644 --- a/libp2p/peer/id.py +++ b/libp2p/peer/id.py @@ -4,6 +4,15 @@ from typing import Union import base58 import multihash +from libp2p.crypto.keys import PublicKey + + +def _serialize_public_key(key: PublicKey) -> bytes: + """ + Serializes ``key`` in the way expected to form valid peer ids. + """ + return key.serialize_to_protobuf().SerializeToString() + class ID: @@ -28,10 +37,10 @@ class ID: self._b58_str = base58.b58encode(self._bytes).decode() return self._b58_str - def __bytes__(self) -> bytes: - return self._bytes + def __repr__(self) -> str: + return "" - __repr__ = __str__ = pretty = to_string = to_base58 + __str__ = pretty = to_string = to_base58 def __eq__(self, other: object) -> bool: if isinstance(other, str): @@ -53,9 +62,10 @@ class ID: return pid @classmethod - def from_pubkey(cls, key: bytes) -> "ID": + def from_pubkey(cls, key: PublicKey) -> "ID": + serialized_key = _serialize_public_key(key) algo = multihash.Func.sha2_256 - mh_digest = multihash.digest(key, algo) + mh_digest = multihash.digest(serialized_key, algo) return cls(mh_digest.encode()) diff --git a/setup.py b/setup.py index 779a704..f91ec65 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ setuptools.setup( "rpcudp>=3.0.0,<4.0.0", "lru-dict>=1.1.6", "protobuf==3.9.0", + "coincurve==12.0.0", ], extras_require=extras_require, packages=setuptools.find_packages(exclude=["tests", "tests.*"]), diff --git a/tests/peer/test_interop.py b/tests/peer/test_interop.py new file mode 100644 index 0000000..17dcb4c --- /dev/null +++ b/tests/peer/test_interop.py @@ -0,0 +1,43 @@ +import base64 + +import Crypto.PublicKey.RSA as RSA + +from libp2p.crypto.pb import crypto_pb2 as pb +from libp2p.crypto.rsa import RSAPrivateKey +from libp2p.peer.id import ID + +# ``PRIVATE_KEY_PROTOBUF_SERIALIZATION`` is a protobuf holding an RSA private key. +PRIVATE_KEY_PROTOBUF_SERIALIZATION = """ +CAAS4AQwggJcAgEAAoGBAL7w+Wc4VhZhCdM/+Hccg5Nrf4q9NXWwJylbSrXz/unFS24wyk6pEk0zi3W +7li+vSNVO+NtJQw9qGNAMtQKjVTP+3Vt/jfQRnQM3s6awojtjueEWuLYVt62z7mofOhCtj+VwIdZNBo +/EkLZ0ETfcvN5LVtLYa8JkXybnOPsLvK+PAgMBAAECgYBdk09HDM7zzL657uHfzfOVrdslrTCj6p5mo +DzvCxLkkjIzYGnlPuqfNyGjozkpSWgSUc+X+EGLLl3WqEOVdWJtbM61fewEHlRTM5JzScvwrJ39t7o6 +CCAjKA0cBWBd6UWgbN/t53RoWvh9HrA2AW5YrT0ZiAgKe9y7EMUaENVJ8QJBAPhpdmb4ZL4Fkm4OKia +NEcjzn6mGTlZtef7K/0oRC9+2JkQnCuf6HBpaRhJoCJYg7DW8ZY+AV6xClKrgjBOfERMCQQDExhnzu2 +dsQ9k8QChBlpHO0TRbZBiQfC70oU31kM1AeLseZRmrxv9Yxzdl8D693NNWS2JbKOXl0kMHHcuGQLMVA +kBZ7WvkmPV3aPL6jnwp2pXepntdVnaTiSxJ1dkXShZ/VSSDNZMYKY306EtHrIu3NZHtXhdyHKcggDXr +qkBrdgErAkAlpGPojUwemOggr4FD8sLX1ot2hDJyyV7OK2FXfajWEYJyMRL1Gm9Uk1+Un53RAkJneqp +JGAzKpyttXBTIDO51AkEA98KTiROMnnU8Y6Mgcvr68/SMIsvCYMt9/mtwSBGgl80VaTQ5Hpaktl6Xbh +VUt5Wv0tRxlXZiViCGCD1EtrrwTw== +""".replace( + "\n", "" +) + +EXPECTED_PEER_ID = "QmRK3JgmVEGiewxWbhpXLJyjWuGuLeSTMTndA1coMHEy5o" + + +# NOTE: this test checks that we can recreate the expected peer id given a private key +# serialization, taken from the Go implementation of libp2p. +def test_peer_id_interop(): + private_key_protobuf_bytes = base64.b64decode(PRIVATE_KEY_PROTOBUF_SERIALIZATION) + private_key_protobuf = pb.PrivateKey() + private_key_protobuf.ParseFromString(private_key_protobuf_bytes) + + private_key_data = private_key_protobuf.data + + private_key_impl = RSA.import_key(private_key_data) + private_key = RSAPrivateKey(private_key_impl) + public_key = private_key.get_public_key() + + peer_id = ID.from_pubkey(public_key) + assert peer_id == EXPECTED_PEER_ID diff --git a/tests/peer/test_peerid.py b/tests/peer/test_peerid.py index 65b3c06..30b74c1 100644 --- a/tests/peer/test_peerid.py +++ b/tests/peer/test_peerid.py @@ -1,32 +1,26 @@ import random -from Crypto.PublicKey import RSA import base58 import multihash -import pytest +from libp2p.crypto.rsa import create_new_key_pair from libp2p.peer.id import ID ALPHABETS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -def test_init(): +def test_eq_impl_for_bytes(): random_id_string = "" for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) peer_id = ID(random_id_string.encode()) assert peer_id == random_id_string.encode() -def test_no_init_value(): - with pytest.raises(Exception): - ID() - - def test_pretty(): random_id_string = "" for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) peer_id = ID(random_id_string.encode()) actual = peer_id.pretty() expected = base58.b58encode(random_id_string).decode() @@ -37,7 +31,7 @@ def test_pretty(): def test_str_less_than_10(): random_id_string = "" for _ in range(5): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) peer_id = base58.b58encode(random_id_string).decode() expected = peer_id actual = ID(random_id_string.encode()).__str__() @@ -48,7 +42,7 @@ def test_str_less_than_10(): def test_str_more_than_10(): random_id_string = "" for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) peer_id = base58.b58encode(random_id_string).decode() expected = peer_id actual = ID(random_id_string.encode()).__str__() @@ -59,7 +53,7 @@ def test_str_more_than_10(): def test_eq_true(): random_id_string = "" for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) peer_id = ID(random_id_string.encode()) assert peer_id == base58.b58encode(random_id_string).decode() @@ -74,21 +68,10 @@ def test_eq_false(): assert peer_id != other -def test_hash(): - random_id_string = "" - for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) - - expected = hash(random_id_string.encode()) - actual = ID(random_id_string.encode()).__hash__() - - assert actual == expected - - def test_id_to_base58(): random_id_string = "" for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) expected = base58.b58encode(random_id_string).decode() actual = ID(random_id_string.encode()).to_base58() @@ -98,7 +81,7 @@ def test_id_to_base58(): def test_id_from_base58(): random_id_string = "" for _ in range(10): - random_id_string += random.SystemRandom().choice(ALPHABETS) + random_id_string += random.choice(ALPHABETS) expected = ID(base58.b58decode(random_id_string)) actual = ID.from_base58(random_id_string.encode()) @@ -106,12 +89,14 @@ def test_id_from_base58(): def test_id_from_public_key(): - bits_list = [1024, 1280, 1536, 1536, 2048] - key = RSA.generate(random.choice(bits_list)) - key_bin = key.exportKey("DER") + key_pair = create_new_key_pair() + public_key = key_pair.public_key + + key_bin = public_key.serialize_to_protobuf().SerializeToString() algo = multihash.Func.sha2_256 mh_digest = multihash.digest(key_bin, algo) expected = ID(mh_digest.encode()) - actual = ID.from_pubkey(key_bin) + + actual = ID.from_pubkey(public_key) assert actual == expected