Merge pull request #240 from ralexstokes/allow-multiple-identity-types

Allow multiple peer identity types (via different cryptosystems)
This commit is contained in:
Alex Stokes 2019-08-14 09:13:02 -07:00 committed by GitHub
commit 9977933fd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 449 additions and 43 deletions

View File

@ -11,4 +11,5 @@ lintroll:
flake8 $(FILES_TO_LINT) flake8 $(FILES_TO_LINT)
protobufs: protobufs:
cd libp2p/crypto/pb && protoc --python_out=. crypto.proto
cd libp2p/pubsub/pb && protoc --python_out=. rpc.proto cd libp2p/pubsub/pb && protoc --python_out=. rpc.proto

View File

@ -1,8 +1,7 @@
import asyncio import asyncio
from typing import Mapping, Sequence 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.host.basic_host import BasicHost
from libp2p.kademlia.network import KademliaServer from libp2p.kademlia.network import KademliaServer
from libp2p.kademlia.storage import IStorage from libp2p.kademlia.storage import IStorage
@ -34,9 +33,10 @@ async def cleanup_done_tasks() -> None:
await asyncio.sleep(3) await asyncio.sleep(3)
def generate_id() -> ID: def generate_peer_id_from_rsa_identity() -> ID:
new_key = RSA.generate(2048, e=65537).publickey().export_key("DER") new_key_pair = create_new_key_pair()
new_id = ID.from_pubkey(new_key) new_public_key = new_key_pair.public_key
new_id = ID.from_pubkey(new_public_key)
return new_id return new_id
@ -53,7 +53,7 @@ def initialize_default_kademlia_router(
:return: return a default kademlia instance :return: return a default kademlia instance
""" """
if not id_opt: if not id_opt:
id_opt = generate_id() id_opt = generate_peer_id_from_rsa_identity()
node_id = id_opt.to_bytes() node_id = id_opt.to_bytes()
# ignore type for Kademlia module # ignore type for Kademlia module
@ -83,7 +83,7 @@ def initialize_default_swarm(
""" """
if not id_opt: if not id_opt:
id_opt = generate_id() id_opt = generate_peer_id_from_rsa_identity()
# TODO parse transport_opt to determine transport # TODO parse transport_opt to determine transport
transport_opt = transport_opt or ["/ip4/127.0.0.1/tcp/8001"] transport_opt = transport_opt or ["/ip4/127.0.0.1/tcp/8001"]
@ -124,7 +124,7 @@ async def new_node(
""" """
if not id_opt: if not id_opt:
id_opt = generate_id() id_opt = generate_peer_id_from_rsa_identity()
if not swarm_opt: if not swarm_opt:
swarm_opt = initialize_default_swarm( swarm_opt = initialize_default_swarm(

View File

82
libp2p/crypto/keys.py Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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)

50
libp2p/crypto/rsa.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -4,6 +4,15 @@ from typing import Union
import base58 import base58
import multihash 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: class ID:
@ -28,10 +37,10 @@ class ID:
self._b58_str = base58.b58encode(self._bytes).decode() self._b58_str = base58.b58encode(self._bytes).decode()
return self._b58_str return self._b58_str
def __bytes__(self) -> bytes: def __repr__(self) -> str:
return self._bytes return "<libp2p.peer.id.ID 0x" + self._bytes.hex() + ">"
__repr__ = __str__ = pretty = to_string = to_base58 __str__ = pretty = to_string = to_base58
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if isinstance(other, str): if isinstance(other, str):
@ -53,9 +62,10 @@ class ID:
return pid return pid
@classmethod @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 algo = multihash.Func.sha2_256
mh_digest = multihash.digest(key, algo) mh_digest = multihash.digest(serialized_key, algo)
return cls(mh_digest.encode()) return cls(mh_digest.encode())

View File

@ -38,6 +38,7 @@ setuptools.setup(
"rpcudp>=3.0.0,<4.0.0", "rpcudp>=3.0.0,<4.0.0",
"lru-dict>=1.1.6", "lru-dict>=1.1.6",
"protobuf==3.9.0", "protobuf==3.9.0",
"coincurve==12.0.0",
], ],
extras_require=extras_require, extras_require=extras_require,
packages=setuptools.find_packages(exclude=["tests", "tests.*"]), packages=setuptools.find_packages(exclude=["tests", "tests.*"]),

View File

@ -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

View File

@ -1,32 +1,26 @@
import random import random
from Crypto.PublicKey import RSA
import base58 import base58
import multihash import multihash
import pytest
from libp2p.crypto.rsa import create_new_key_pair
from libp2p.peer.id import ID from libp2p.peer.id import ID
ALPHABETS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" ALPHABETS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def test_init(): def test_eq_impl_for_bytes():
random_id_string = "" random_id_string = ""
for _ in range(10): for _ in range(10):
random_id_string += random.SystemRandom().choice(ALPHABETS) random_id_string += random.choice(ALPHABETS)
peer_id = ID(random_id_string.encode()) peer_id = ID(random_id_string.encode())
assert peer_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(): def test_pretty():
random_id_string = "" random_id_string = ""
for _ in range(10): for _ in range(10):
random_id_string += random.SystemRandom().choice(ALPHABETS) random_id_string += random.choice(ALPHABETS)
peer_id = ID(random_id_string.encode()) peer_id = ID(random_id_string.encode())
actual = peer_id.pretty() actual = peer_id.pretty()
expected = base58.b58encode(random_id_string).decode() expected = base58.b58encode(random_id_string).decode()
@ -37,7 +31,7 @@ def test_pretty():
def test_str_less_than_10(): def test_str_less_than_10():
random_id_string = "" random_id_string = ""
for _ in range(5): 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() peer_id = base58.b58encode(random_id_string).decode()
expected = peer_id expected = peer_id
actual = ID(random_id_string.encode()).__str__() actual = ID(random_id_string.encode()).__str__()
@ -48,7 +42,7 @@ def test_str_less_than_10():
def test_str_more_than_10(): def test_str_more_than_10():
random_id_string = "" random_id_string = ""
for _ in range(10): 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() peer_id = base58.b58encode(random_id_string).decode()
expected = peer_id expected = peer_id
actual = ID(random_id_string.encode()).__str__() actual = ID(random_id_string.encode()).__str__()
@ -59,7 +53,7 @@ def test_str_more_than_10():
def test_eq_true(): def test_eq_true():
random_id_string = "" random_id_string = ""
for _ in range(10): for _ in range(10):
random_id_string += random.SystemRandom().choice(ALPHABETS) random_id_string += random.choice(ALPHABETS)
peer_id = ID(random_id_string.encode()) peer_id = ID(random_id_string.encode())
assert peer_id == base58.b58encode(random_id_string).decode() assert peer_id == base58.b58encode(random_id_string).decode()
@ -74,21 +68,10 @@ def test_eq_false():
assert peer_id != other 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(): def test_id_to_base58():
random_id_string = "" random_id_string = ""
for _ in range(10): for _ in range(10):
random_id_string += random.SystemRandom().choice(ALPHABETS) random_id_string += random.choice(ALPHABETS)
expected = base58.b58encode(random_id_string).decode() expected = base58.b58encode(random_id_string).decode()
actual = ID(random_id_string.encode()).to_base58() actual = ID(random_id_string.encode()).to_base58()
@ -98,7 +81,7 @@ def test_id_to_base58():
def test_id_from_base58(): def test_id_from_base58():
random_id_string = "" random_id_string = ""
for _ in range(10): for _ in range(10):
random_id_string += random.SystemRandom().choice(ALPHABETS) random_id_string += random.choice(ALPHABETS)
expected = ID(base58.b58decode(random_id_string)) expected = ID(base58.b58decode(random_id_string))
actual = ID.from_base58(random_id_string.encode()) actual = ID.from_base58(random_id_string.encode())
@ -106,12 +89,14 @@ def test_id_from_base58():
def test_id_from_public_key(): def test_id_from_public_key():
bits_list = [1024, 1280, 1536, 1536, 2048] key_pair = create_new_key_pair()
key = RSA.generate(random.choice(bits_list)) public_key = key_pair.public_key
key_bin = key.exportKey("DER")
key_bin = public_key.serialize_to_protobuf().SerializeToString()
algo = multihash.Func.sha2_256 algo = multihash.Func.sha2_256
mh_digest = multihash.digest(key_bin, algo) mh_digest = multihash.digest(key_bin, algo)
expected = ID(mh_digest.encode()) expected = ID(mh_digest.encode())
actual = ID.from_pubkey(key_bin)
actual = ID.from_pubkey(public_key)
assert actual == expected assert actual == expected