From 27c0a4f77b4a15afbd7d846ce1b1b2a21b764f89 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Aug 2019 13:04:55 -0700 Subject: [PATCH 01/31] formatting --- libp2p/security/base_session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libp2p/security/base_session.py b/libp2p/security/base_session.py index 54562b18..4d1bddf5 100644 --- a/libp2p/security/base_session.py +++ b/libp2p/security/base_session.py @@ -24,10 +24,12 @@ class BaseSession(ISecureConn): ) -> None: self.local_peer = transport.local_peer self.local_private_key = transport.local_private_key - self.conn = conn self.remote_peer_id = peer_id self.remote_permanent_pubkey = None + self.conn = conn + self.writer = self.conn.writer + self.reader = self.conn.reader self.initiator = self.conn.initiator async def write(self, data: bytes) -> None: From 23f53ef9543a32362567d883507a37d618775640 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Aug 2019 14:08:27 -0700 Subject: [PATCH 02/31] Allow optional peer ID in a security session --- libp2p/security/base_session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libp2p/security/base_session.py b/libp2p/security/base_session.py index 4d1bddf5..b3c88148 100644 --- a/libp2p/security/base_session.py +++ b/libp2p/security/base_session.py @@ -20,7 +20,10 @@ class BaseSession(ISecureConn): remote_permanent_pubkey: PublicKey def __init__( - self, transport: BaseSecureTransport, conn: IRawConnection, peer_id: ID + self, + transport: BaseSecureTransport, + conn: IRawConnection, + peer_id: Optional[ID] = None, ) -> None: self.local_peer = transport.local_peer self.local_private_key = transport.local_private_key From fd08bcf624ee9e88246a54ed93a34b531ea0e223 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Aug 2019 14:08:54 -0700 Subject: [PATCH 03/31] Add `secio` protobufs --- Makefile | 2 +- libp2p/security/secio/pb/spipe.proto | 16 +++ libp2p/security/secio/pb/spipe_pb2.py | 144 +++++++++++++++++++++++++ libp2p/security/secio/pb/spipe_pb2.pyi | 67 ++++++++++++ 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 libp2p/security/secio/pb/spipe.proto create mode 100644 libp2p/security/secio/pb/spipe_pb2.py create mode 100644 libp2p/security/secio/pb/spipe_pb2.pyi diff --git a/Makefile b/Makefile index e5b6509c..45412701 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ FILES_TO_LINT = libp2p tests examples setup.py -PB = libp2p/crypto/pb/crypto.proto libp2p/pubsub/pb/rpc.proto libp2p/security/insecure/pb/plaintext.proto +PB = libp2p/crypto/pb/crypto.proto libp2p/pubsub/pb/rpc.proto libp2p/security/insecure/pb/plaintext.proto libp2p/security/secio/pb/spipe.proto PY = $(PB:.proto=_pb2.py) PYI = $(PB:.proto=_pb2.pyi) diff --git a/libp2p/security/secio/pb/spipe.proto b/libp2p/security/secio/pb/spipe.proto new file mode 100644 index 00000000..942a9a5f --- /dev/null +++ b/libp2p/security/secio/pb/spipe.proto @@ -0,0 +1,16 @@ +syntax = "proto2"; + +package spipe.pb; + +message Propose { + optional bytes rand = 1; + optional bytes public_key = 2; + optional string exchanges = 3; + optional string ciphers = 4; + optional string hashes = 5; +} + +message Exchange { + optional bytes ephemeral_public_key = 1; + optional bytes signature = 2; +} \ No newline at end of file diff --git a/libp2p/security/secio/pb/spipe_pb2.py b/libp2p/security/secio/pb/spipe_pb2.py new file mode 100644 index 00000000..87a2f37d --- /dev/null +++ b/libp2p/security/secio/pb/spipe_pb2.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: spipe.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +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='spipe.proto', + package='spipe.pb', + syntax='proto2', + serialized_options=None, + serialized_pb=_b('\n\x0bspipe.proto\x12\x08spipe.pb\"_\n\x07Propose\x12\x0c\n\x04rand\x18\x01 \x01(\x0c\x12\x12\n\npublic_key\x18\x02 \x01(\x0c\x12\x11\n\texchanges\x18\x03 \x01(\t\x12\x0f\n\x07\x63iphers\x18\x04 \x01(\t\x12\x0e\n\x06hashes\x18\x05 \x01(\t\";\n\x08\x45xchange\x12\x1c\n\x14\x65phemeral_public_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c') +) + + + + +_PROPOSE = _descriptor.Descriptor( + name='Propose', + full_name='spipe.pb.Propose', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='rand', full_name='spipe.pb.Propose.rand', index=0, + number=1, type=12, cpp_type=9, label=1, + 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), + _descriptor.FieldDescriptor( + name='public_key', full_name='spipe.pb.Propose.public_key', index=1, + number=2, type=12, cpp_type=9, label=1, + 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), + _descriptor.FieldDescriptor( + name='exchanges', full_name='spipe.pb.Propose.exchanges', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ciphers', full_name='spipe.pb.Propose.ciphers', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='hashes', full_name='spipe.pb.Propose.hashes', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + 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=25, + serialized_end=120, +) + + +_EXCHANGE = _descriptor.Descriptor( + name='Exchange', + full_name='spipe.pb.Exchange', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='ephemeral_public_key', full_name='spipe.pb.Exchange.ephemeral_public_key', index=0, + number=1, type=12, cpp_type=9, label=1, + 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), + _descriptor.FieldDescriptor( + name='signature', full_name='spipe.pb.Exchange.signature', index=1, + number=2, type=12, cpp_type=9, label=1, + 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=122, + serialized_end=181, +) + +DESCRIPTOR.message_types_by_name['Propose'] = _PROPOSE +DESCRIPTOR.message_types_by_name['Exchange'] = _EXCHANGE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Propose = _reflection.GeneratedProtocolMessageType('Propose', (_message.Message,), dict( + DESCRIPTOR = _PROPOSE, + __module__ = 'spipe_pb2' + # @@protoc_insertion_point(class_scope:spipe.pb.Propose) + )) +_sym_db.RegisterMessage(Propose) + +Exchange = _reflection.GeneratedProtocolMessageType('Exchange', (_message.Message,), dict( + DESCRIPTOR = _EXCHANGE, + __module__ = 'spipe_pb2' + # @@protoc_insertion_point(class_scope:spipe.pb.Exchange) + )) +_sym_db.RegisterMessage(Exchange) + + +# @@protoc_insertion_point(module_scope) diff --git a/libp2p/security/secio/pb/spipe_pb2.pyi b/libp2p/security/secio/pb/spipe_pb2.pyi new file mode 100644 index 00000000..2025ff13 --- /dev/null +++ b/libp2p/security/secio/pb/spipe_pb2.pyi @@ -0,0 +1,67 @@ +# @generated by generate_proto_mypy_stubs.py. Do not edit! +import sys +from google.protobuf.descriptor import ( + Descriptor as google___protobuf___descriptor___Descriptor, +) + +from google.protobuf.message import ( + Message as google___protobuf___message___Message, +) + +from typing import ( + Optional as typing___Optional, + Text as typing___Text, +) + +from typing_extensions import ( + Literal as typing_extensions___Literal, +) + + +class Propose(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + rand = ... # type: bytes + public_key = ... # type: bytes + exchanges = ... # type: typing___Text + ciphers = ... # type: typing___Text + hashes = ... # type: typing___Text + + def __init__(self, + *, + rand : typing___Optional[bytes] = None, + public_key : typing___Optional[bytes] = None, + exchanges : typing___Optional[typing___Text] = None, + ciphers : typing___Optional[typing___Text] = None, + hashes : typing___Optional[typing___Text] = None, + ) -> None: ... + @classmethod + def FromString(cls, s: bytes) -> Propose: ... + def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... + def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... + if sys.version_info >= (3,): + def HasField(self, field_name: typing_extensions___Literal[u"ciphers",u"exchanges",u"hashes",u"public_key",u"rand"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"ciphers",u"exchanges",u"hashes",u"public_key",u"rand"]) -> None: ... + else: + def HasField(self, field_name: typing_extensions___Literal[u"ciphers",b"ciphers",u"exchanges",b"exchanges",u"hashes",b"hashes",u"public_key",b"public_key",u"rand",b"rand"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"ciphers",b"ciphers",u"exchanges",b"exchanges",u"hashes",b"hashes",u"public_key",b"public_key",u"rand",b"rand"]) -> None: ... + +class Exchange(google___protobuf___message___Message): + DESCRIPTOR: google___protobuf___descriptor___Descriptor = ... + ephemeral_public_key = ... # type: bytes + signature = ... # type: bytes + + def __init__(self, + *, + ephemeral_public_key : typing___Optional[bytes] = None, + signature : typing___Optional[bytes] = None, + ) -> None: ... + @classmethod + def FromString(cls, s: bytes) -> Exchange: ... + def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... + def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... + if sys.version_info >= (3,): + def HasField(self, field_name: typing_extensions___Literal[u"ephemeral_public_key",u"signature"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"ephemeral_public_key",u"signature"]) -> None: ... + else: + def HasField(self, field_name: typing_extensions___Literal[u"ephemeral_public_key",b"ephemeral_public_key",u"signature",b"signature"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"ephemeral_public_key",b"ephemeral_public_key",u"signature",b"signature"]) -> None: ... From 26165b0729c277371512389aae4b61e56251491d Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 16 Aug 2019 18:54:06 -0700 Subject: [PATCH 04/31] [wip] sketch of secio handshake --- libp2p/security/secio/transport.py | 137 +++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 libp2p/security/secio/transport.py diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py new file mode 100644 index 00000000..e0918d9a --- /dev/null +++ b/libp2p/security/secio/transport.py @@ -0,0 +1,137 @@ +from dataclasses import dataclass +from typing import Optional + +from libp2p.crypto.keys import PrivateKey +from libp2p.network.connection.raw_connection_interface import IRawConnection +from libp2p.peer.id import ID as PeerID +from libp2p.security.base_session import BaseSession +from libp2p.security.base_transport import BaseSecureTransport +from libp2p.security.secure_conn_interface import ISecureConn + +ID = "/secio/1.0.0" + + +@dataclass +class NegotiationContext(frozen=True): + local_peer: PeerID + remote_peer: Optional[PeerID] + + local_private_key: PrivateKey + conn: IRawConnection + + +class SecureSession(BaseSession): + pass + + +def _mk_serialized_proposal(negotiation_context: NegotiationContext) -> bytes: + pass + + +async def _response_to_msg(msg) -> bytes: + return bytes() + + +async def _establish_session_parameters(): + # propose parameters + local_proposal = _mk_local_proposal(negotiation_context) + serialized_local_proposal = _mk_serialized_proposal(local_proposal) + serialized_remote_proposal = await _response_to_msg(serialized_local_proposal) + + remote_proposal = _parse_proposal(serialized_remote_proposal) + + # identify peer + remote_peer = _peer_from_proposal(remote_proposal) + + # select enc params + encryption_parameters = _select_encryption_parameters(remote_proposal) + + # exchange ephemeral pub keys + local_ephemeral_key_pair, shared_key_generator = create_elliptic_key_pair( + encryption_parameters + ) + local_selection = _mk_serialized_selection( + local_proposal, remote_proposal, local_ephemeral_key_pair.public_key + ) + serialized_local_selection = _mk_serialized_selection(local_selection) + + local_exchange = _mk_exchange( + local_ephemeral_key_pair.public_key, serialized_local_selection + ) + serialized_local_exchange = _mk_serialized_exchange_msg(local_exchange) + serialized_remote_exchange = await _response_to_msg(serialized_local_exchange) + + remote_exchange = _parse_exchange(serialized_remote_exchange) + + remote_selection = _mk_remote_selection( + remote_exchange, local_proposal, remote_proposal + ) + verify_exchange(remote_exchange, remote_selection, remote_proposal) + + # return all the data we need + + +def _mk_session_from(session_parameters): + # use ephemeral pubkey to make a shared key + # stretch shared key to get two keys + # decide which side has which key + # set up mac and cipher, based on shared key, for each side + # make new rdr/wtr pairs using each mac/cipher gadget + pass + + +async def _close_handshake(session): + # send nonce over encrypted channel + # verify we get our nonce back + pass + + +async def _run_handshake(negotiation_context: NegotiationContext): + """ + Attempts the initial `secio` handshake with the remote peer. + + Successfully completing this routine implies ``self``'s instance + of this session is now ready for secure communication. + """ + session_parameters = await _establish_session_parameters() + + session = _mk_session_from(session_parameters) + + await _close_handshake(session) + + return session + + +async def create_secure_session( + transport: BaseSecureTransport, conn: IRawConnection, remote_peer: PeerID = None +) -> ISecureConn: + negotiation_context = NegotiationContext( + transport.local_peer, remote_peer, transport.local_private_key, conn + ) + + return await _run_handshake(negotiation_context) + + +class SecIOTransport(BaseSecureTransport): + """ + ``SecIOTransport`` provides a security upgrader for a ``IRawConnection``, + following the `secio` protocol defined in the libp2p specs. + """ + + async def secure_inbound(self, conn: IRawConnection) -> ISecureConn: + """ + Secure the connection, either locally or by communicating with opposing node via conn, + for an inbound connection (i.e. we are not the initiator) + :return: secure connection object (that implements secure_conn_interface) + """ + return await create_secure_session(self, conn) + + async def secure_outbound( + self, conn: IRawConnection, peer_id: PeerID + ) -> ISecureConn: + """ + Secure the connection, either locally or by communicating with opposing node via conn, + for an inbound connection (i.e. we are the initiator) + :return: secure connection object (that implements secure_conn_interface) + """ + return await create_secure_session(self, conn, peer_id) From b59c5d6ca11c8767a152a910666828f575644fc3 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 22 Aug 2019 17:54:49 +0200 Subject: [PATCH 05/31] Add "msgio" functions --- libp2p/io/__init__.py | 0 libp2p/io/msgio.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 libp2p/io/__init__.py create mode 100644 libp2p/io/msgio.py diff --git a/libp2p/io/__init__.py b/libp2p/io/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libp2p/io/msgio.py b/libp2p/io/msgio.py new file mode 100644 index 00000000..6ad11bcd --- /dev/null +++ b/libp2p/io/msgio.py @@ -0,0 +1,16 @@ +import asyncio + +SIZE_LEN_BYTES = 4 + +# TODO unify w/ https://github.com/libp2p/py-libp2p/blob/1aed52856f56a4b791696bbcbac31b5f9c2e88c9/libp2p/utils.py#L85-L99 + + +def encode(msg_bytes: bytes) -> bytes: + len_prefix = len(msg_bytes).to_bytes(SIZE_LEN_BYTES, "big") + return len_prefix + msg_bytes + + +async def read_next_message(reader: asyncio.StreamReader) -> bytes: + len_bytes = await reader.readexactly(SIZE_LEN_BYTES) + len_int = int.from_bytes(len_bytes, "big") + return await reader.readexactly(len_int) From 0cc3fc24a76c5080eb8149032e22a8da1cc22e5d Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 22 Aug 2019 17:55:05 +0200 Subject: [PATCH 06/31] Add source for some secure bytes, e.g. to provide a nonce --- libp2p/security/base_transport.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/libp2p/security/base_transport.py b/libp2p/security/base_transport.py index 0f096bfc..10d7b663 100644 --- a/libp2p/security/base_transport.py +++ b/libp2p/security/base_transport.py @@ -1,14 +1,30 @@ +import secrets +from typing import Callable + from libp2p.crypto.keys import KeyPair from libp2p.peer.id import ID from libp2p.security.secure_transport_interface import ISecureTransport +def default_secure_bytes_provider(n: int) -> bytes: + return secrets.token_bytes(n) + + class BaseSecureTransport(ISecureTransport): """ ``BaseSecureTransport`` is not fully instantiated from its abstract classes as it is only meant to be used in clases that derive from it. + + Clients can provide a strategy to get cryptographically secure bytes of a given length. + A default implementation is provided using the ``secrets`` module from the + standard library. """ - def __init__(self, local_key_pair: KeyPair) -> None: + def __init__( + self, + local_key_pair: KeyPair, + secure_bytes_provider: Callable[[int], bytes] = default_secure_bytes_provider, + ) -> None: self.local_private_key = local_key_pair.private_key self.local_peer = ID.from_pubkey(local_key_pair.public_key) + self.secure_bytes_provider = secure_bytes_provider From 91e11f3ec0f2bb4accf421fef6ed7f8e6ce47b94 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 22 Aug 2019 17:55:36 +0200 Subject: [PATCH 07/31] [wip] more secio implementation --- libp2p/security/secio/transport.py | 233 ++++++++++++++++++++++++----- 1 file changed, 195 insertions(+), 38 deletions(-) diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index e0918d9a..c0c7745c 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -1,50 +1,203 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Tuple -from libp2p.crypto.keys import PrivateKey +from libp2p.crypto.keys import PrivateKey, PublicKey +from libp2p.io.msgio import encode as encode_message +from libp2p.io.msgio import read_next_message from libp2p.network.connection.raw_connection_interface import IRawConnection from libp2p.peer.id import ID as PeerID from libp2p.security.base_session import BaseSession from libp2p.security.base_transport import BaseSecureTransport from libp2p.security.secure_conn_interface import ISecureConn +from .pb.spipe_pb2 import Exchange, Propose + ID = "/secio/1.0.0" +NONCE_SIZE = 16 # bytes -@dataclass -class NegotiationContext(frozen=True): - local_peer: PeerID - remote_peer: Optional[PeerID] - - local_private_key: PrivateKey - conn: IRawConnection +# NOTE: the following is only a subset of allowable parameters according to the +# `secio` specification. +DEFAULT_SUPPORTED_EXCHANGES = "P-256" +DEFAULT_SUPPORTED_CIPHERS = "AES-128" +DEFAULT_SUPPORTED_HASHES = "SHA256" class SecureSession(BaseSession): + local_peer: PeerID + remote_peer: PeerID + # specialize read and write pass -def _mk_serialized_proposal(negotiation_context: NegotiationContext) -> bytes: +@dataclass(frozen=True) +class Proposal: + """ + A ``Proposal`` represents the set of session parameters one peer in a pair of + peers attempting to negotiate a `secio` channel prefers. + """ + + nonce: bytes + public_key: PublicKey + exchanges: str = DEFAULT_SUPPORTED_EXCHANGES # comma separated list + ciphers: str = DEFAULT_SUPPORTED_CIPHERS # comma separated list + hashes: str = DEFAULT_SUPPORTED_HASHES # comma separated list + + def serialize(self) -> bytes: + protobuf = Propose( + self.nonce, + self.public_key.serialize(), + self.exchanges, + self.ciphers, + self.hashes, + ) + return protobuf.SerializeToString() + + @classmethod + def deserialize(cls, protobuf_bytes: bytes) -> "Proposal": + protobuf = Propose() + protobuf.ParseFromString(protobuf_bytes) + + nonce = protobuf.rand + public_key_protobuf_bytes = protobuf.public_key + # TODO (ralexstokes) handle genericity in the deserialization + public_key = PublicKey.deserialize(public_key_protobuf_bytes) + exchanges = protobuf.exchanges + ciphers = protobuf.ciphers + hashes = protobuf.hashes + + return cls(nonce, public_key, exchanges, ciphers, hashes) + + def calculate_peer_id(self) -> PeerID: + return PeerID.from_pubkey(self.public_key) + + +@dataclass +class EncryptionParameters: + permanent_public_key: PublicKey + + curve_type: str + cipher_type: str + hash_type: str + + ephemeral_public_key: PublicKey + keys: ... + cipher: ... + mac: ... + + +async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: + # TODO clean up ``IRawConnection`` so that we don't have to break + # the abstraction + conn.writer.write(encode_message(msg)) + await conn.writer.drain() + + return await read_next_message(conn.reader) + + +@dataclass +class SessionParameters: + local_peer: PeerID + local_encryption_parameters: EncryptionParameters + remote_peer: PeerID + remote_encryption_parameters: EncryptionParameters + + +def _mk_multihash_sha256(data: bytes) -> bytes: pass -async def _response_to_msg(msg) -> bytes: - return bytes() +def _mk_score(public_key: PublicKey, nonce: bytes) -> bytes: + return _mk_multihash_sha256(public_key.serialize() + nonce) -async def _establish_session_parameters(): - # propose parameters - local_proposal = _mk_local_proposal(negotiation_context) - serialized_local_proposal = _mk_serialized_proposal(local_proposal) - serialized_remote_proposal = await _response_to_msg(serialized_local_proposal) +def _select_parameter_from_order( + order: int, supported_parameters: str, available_parameters: str +) -> str: + if order < 0: + first_choices = available_parameters.split(",") + second_choices = supported_parameters.split(",") + elif order > 0: + first_choices = supported_parameters.split(",") + second_choices = available_parameters.split(",") + else: + return supported_parameters.split(",")[0] - remote_proposal = _parse_proposal(serialized_remote_proposal) + for first, second in zip(first_choices, second_choices): + if first == second: + return first - # identify peer - remote_peer = _peer_from_proposal(remote_proposal) - # select enc params - encryption_parameters = _select_encryption_parameters(remote_proposal) +def _select_encryption_parameters( + local_proposal: Proposal, remote_proposal: Proposal +) -> Tuple[str, str, str]: + first_score = _mk_score(remote_proposal.public_key, local_proposal.nonce) + second_score = _mk_score(local_proposal.public_key, remote_proposal.nonce) + + order = 0 + if first_score < second_score: + order = -1 + elif second_score < first_score: + order = 1 + + # NOTE: if order is 0, "talking to self" + # TODO(ralexstokes) nicer error handling here... + assert order != 0 + + return ( + _select_parameter_from_order( + order, DEFAULT_SUPPORTED_EXCHANGES, remote_proposal.exchanges + ), + _select_encryption_parameters( + order, DEFAULT_SUPPORTED_CIPHERS, remote_proposal.ciphers + ), + _select_encryption_parameters( + order, DEFAULT_SUPPORTED_HASHES, remote_proposal.hashes + ), + ) + + +async def _establish_session_parameters( + local_peer: PeerID, + local_private_key: PrivateKey, + remote_peer: Optional[PeerID], + conn: IRawConnection, + nonce: bytes, +) -> SessionParameters: + session_parameters = SessionParameters() + session_parameters.local_peer = local_peer + + local_encryption_parameters = EncryptionParameters() + session_parameters.local_encryption_parameters = local_encryption_parameters + + local_public_key = local_private_key.get_public_key() + local_encryption_parameters.permanent_public_key = local_public_key + + local_proposal = Proposal(nonce, local_public_key) + serialized_local_proposal = local_proposal.serialize() + serialized_remote_proposal = await _response_to_msg(conn, serialized_local_proposal) + + remote_encryption_parameters = EncryptionParameters() + session_parameters.remote_encryption_parameters = remote_encryption_parameters + remote_proposal = Proposal.deserialize(serialized_remote_proposal) + remote_encryption_parameters.permanent_public_key = remote_proposal.public_key + + remote_peer_from_proposal = remote_proposal.calculate_peer_id() + if not remote_peer: + remote_peer = remote_peer_from_proposal + elif remote_peer != remote_peer_from_proposal: + raise PeerMismatchException() + session_parameters.remote_peer = remote_peer + + curve_param, cipher_param, hash_param = _select_encryption_parameters( + local_proposal, remote_proposal + ) + local_encryption_parameters.curve_type = curve_param + local_encryption_parameters.cipher_type = cipher_param + local_encryption_parameters.hash_type = hash_param + remote_encryption_parameters.curve_type = curve_param + remote_encryption_parameters.cipher_type = cipher_param + remote_encryption_parameters.hash_type = hash_param # exchange ephemeral pub keys local_ephemeral_key_pair, shared_key_generator = create_elliptic_key_pair( @@ -86,14 +239,25 @@ async def _close_handshake(session): pass -async def _run_handshake(negotiation_context: NegotiationContext): +async def create_secure_session( + transport: BaseSecureTransport, conn: IRawConnection, remote_peer: PeerID = None +) -> ISecureConn: """ - Attempts the initial `secio` handshake with the remote peer. + Attempt the initial `secio` handshake with the remote peer. + If successful, return an object that provides secure communication to the + ``remote_peer``. + """ + nonce = transport.get_nonce() + local_peer = transport.local_peer + local_private_key = transport.local_private_key - Successfully completing this routine implies ``self``'s instance - of this session is now ready for secure communication. - """ - session_parameters = await _establish_session_parameters() + try: + session_parameters = await _establish_session_parameters( + local_peer, local_private_key, remote_peer, conn, nonce + ) + except PeerMismatchException as e: + conn.close() + raise e session = _mk_session_from(session_parameters) @@ -102,22 +266,15 @@ async def _run_handshake(negotiation_context: NegotiationContext): return session -async def create_secure_session( - transport: BaseSecureTransport, conn: IRawConnection, remote_peer: PeerID = None -) -> ISecureConn: - negotiation_context = NegotiationContext( - transport.local_peer, remote_peer, transport.local_private_key, conn - ) - - return await _run_handshake(negotiation_context) - - class SecIOTransport(BaseSecureTransport): """ ``SecIOTransport`` provides a security upgrader for a ``IRawConnection``, following the `secio` protocol defined in the libp2p specs. """ + def get_nonce(self) -> bytes: + return self.secure_bytes_provider(NONCE_SIZE) + async def secure_inbound(self, conn: IRawConnection) -> ISecureConn: """ Secure the connection, either locally or by communicating with opposing node via conn, From 3c97a5a0ed63660381cb87bcbe7c32dbe4dd53d3 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 16:54:16 +0200 Subject: [PATCH 08/31] Add ECC key implementation --- libp2p/crypto/ecc.py | 55 +++++++++++++++++++++++++++++++++++++++++++ libp2p/crypto/keys.py | 1 + 2 files changed, 56 insertions(+) create mode 100644 libp2p/crypto/ecc.py diff --git a/libp2p/crypto/ecc.py b/libp2p/crypto/ecc.py new file mode 100644 index 00000000..7cfb4335 --- /dev/null +++ b/libp2p/crypto/ecc.py @@ -0,0 +1,55 @@ +from Crypto.PublicKey import ECC +from Crypto.PublicKey.ECC import EccKey + +from libp2p.crypto.keys import KeyPair, KeyType, PrivateKey, PublicKey + + +class ECCPublicKey(PublicKey): + def __init__(self, impl: EccKey) -> None: + self.impl = impl + + def to_bytes(self) -> bytes: + return self.impl.export_key("DER") + + @classmethod + def from_bytes(cls, data: bytes) -> "ECCPublicKey": + public_key_impl = ECC.import_key(data) + return cls(public_key_impl) + + def get_type(self) -> KeyType: + return KeyType.ECC_P256 + + def verify(self, data: bytes, signature: bytes) -> bool: + raise NotImplementedError + + +class ECCPrivateKey(PrivateKey): + def __init__(self, impl: EccKey) -> None: + self.impl = impl + + @classmethod + def new(cls, curve: str) -> "ECCPrivateKey": + private_key_impl = ECC.generate(curve=curve) + return cls(private_key_impl) + + def to_bytes(self) -> bytes: + return self.impl.export_key("DER") + + def get_type(self) -> KeyType: + return KeyType.ECC_P256 + + def sign(self, data: bytes) -> bytes: + raise NotImplementedError + + def get_public_key(self) -> PublicKey: + return ECCPublicKey(self.impl.publickey()) + + +def create_new_key_pair(curve: str) -> 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 = ECCPrivateKey.new(curve) + public_key = private_key.get_public_key() + return KeyPair(private_key, public_key) diff --git a/libp2p/crypto/keys.py b/libp2p/crypto/keys.py index 31caca00..33cca9d5 100644 --- a/libp2p/crypto/keys.py +++ b/libp2p/crypto/keys.py @@ -11,6 +11,7 @@ class KeyType(Enum): Ed25519 = 1 Secp256k1 = 2 ECDSA = 3 + ECC_P256 = 4 class Key(ABC): From fb13dfa7b3e75c81a09063250a141b6bd8c739a3 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 16:54:31 +0200 Subject: [PATCH 09/31] Add `sign` and `verify` operations for `secp256k1` keys --- libp2p/crypto/secp256k1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libp2p/crypto/secp256k1.py b/libp2p/crypto/secp256k1.py index e2d5fb26..524877c9 100644 --- a/libp2p/crypto/secp256k1.py +++ b/libp2p/crypto/secp256k1.py @@ -19,7 +19,7 @@ class Secp256k1PublicKey(PublicKey): return KeyType.Secp256k1 def verify(self, data: bytes, signature: bytes) -> bool: - raise NotImplementedError + return self.impl.verify(signature, data) class Secp256k1PrivateKey(PrivateKey): @@ -38,7 +38,7 @@ class Secp256k1PrivateKey(PrivateKey): return KeyType.Secp256k1 def sign(self, data: bytes) -> bytes: - raise NotImplementedError + return self.impl.sign(data) def get_public_key(self) -> PublicKey: public_key_impl = coincurve.PublicKey.from_secret(self.impl.secret) From 228c17ae9e19acb5cac8e1242a41a9451b234c99 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 16:54:59 +0200 Subject: [PATCH 10/31] Add ECDH key exchange utility --- libp2p/crypto/key_exchange.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 libp2p/crypto/key_exchange.py diff --git a/libp2p/crypto/key_exchange.py b/libp2p/crypto/key_exchange.py new file mode 100644 index 00000000..a204de31 --- /dev/null +++ b/libp2p/crypto/key_exchange.py @@ -0,0 +1,27 @@ +from typing import Callable, Tuple + +import Crypto.PublicKey.ECC as ECC + +from libp2p.crypto.ecc import create_new_key_pair +from libp2p.crypto.keys import PublicKey + +SharedKeyGenerator = Callable[[bytes], bytes] + + +def create_ephemeral_key_pair(curve_type: str) -> Tuple[PublicKey, SharedKeyGenerator]: + """ + Facilitates ECDH key exchange. + """ + if curve_type != "P-256": + raise NotImplementedError() + + key_pair = create_new_key_pair(curve_type) + + def _key_exchange(serialized_remote_public_key: bytes) -> bytes: + remote_public_key = ECC.import_key(serialized_remote_public_key) + curve_point = remote_public_key.pointQ + secret_point = curve_point * key_pair.private_key.impl.d + byte_size = secret_point.size_in_bytes() + return secret_point.x.to_bytes(byte_size, byteorder="big") + + return key_pair.public_key, _key_exchange From af2e50aaf4b562b36d6d0c0b47580cfffffd2c95 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 16:55:31 +0200 Subject: [PATCH 11/31] Add facility for authenticated encryption --- libp2p/crypto/authenticated_encryption.py | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 libp2p/crypto/authenticated_encryption.py diff --git a/libp2p/crypto/authenticated_encryption.py b/libp2p/crypto/authenticated_encryption.py new file mode 100644 index 00000000..f84ecb74 --- /dev/null +++ b/libp2p/crypto/authenticated_encryption.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +import hmac +from typing import Tuple + +from Crypto.Cipher import AES + + +class InvalidMACException(Exception): + pass + + +@dataclass(frozen=True) +class EncryptionParameters: + cipher_type: str + hash_type: str + iv: bytes + mac_key: bytes + cipher_key: bytes + + +class MacAndCipher: + def __init__(self, parameters: EncryptionParameters) -> None: + self.authenticator = hmac.new( + parameters.mac_key, digestmod=parameters.hash_type + ) + cipher = AES.new( + parameters.cipher_key, AES.MODE_CTR, initial_value=parameters.iv + ) + self.cipher = cipher + + def encrypt(self, data: bytes) -> bytes: + return self.cipher.encrypt(data) + + def authenticate(self, data: bytes) -> bytes: + authenticator = self.authenticator.copy() + authenticator.update(data) + return authenticator.digest() + + def decrypt_if_valid(self, data_with_tag: bytes) -> bytes: + tag_position = len(data_with_tag) - self.authenticator.digest_size + data = data_with_tag[:tag_position] + tag = data_with_tag[tag_position:] + + authenticator = self.authenticator.copy() + authenticator.update(data) + expected_tag = authenticator.digest() + + if not hmac.compare_digest(tag, expected_tag): + raise InvalidMACException(expected_tag, tag) + + return self.cipher.decrypt(data) + + +def initialize_pair( + cipher_type: str, hash_type: str, secret: bytes +) -> Tuple[EncryptionParameters, EncryptionParameters]: + """ + Return a pair of ``Keys`` for use in securing a + communications channel with authenticated encryption + derived from the ``secret`` and using the + requested ``cipher_type`` and ``hash_type``. + """ + if cipher_type != "AES-128": + raise NotImplementedError() + if hash_type != "SHA256": + raise NotImplementedError() + + iv_size = 16 + cipher_key_size = 16 + hmac_key_size = 20 + seed = "key expansion".encode() + + result = bytearray(2 * (iv_size + cipher_key_size + hmac_key_size)) + + authenticator = hmac.new(secret, digestmod=hash_type) + authenticator.update(seed) + tag = authenticator.digest() + + i = 0 + while i < len(result): + authenticator = hmac.new(secret, digestmod=hash_type) + + authenticator.update(tag) + authenticator.update(seed) + + another_tag = authenticator.digest() + + remaining_bytes = len(another_tag) + + if i + remaining_bytes > len(result): + remaining_bytes = len(result) - i + + result[i : i + remaining_bytes] = another_tag + + i += remaining_bytes + + authenticator = hmac.new(secret, digestmod=hash_type) + authenticator.update(tag) + tag = authenticator.digest() + + half = len(result) / 2 + first_half = result[:half] + second_half = result[half:] + + return ( + EncryptionParameters( + cipher_type, + hash_type, + first_half[0:iv_size], + first_half[iv_size : iv_size + cipher_key_size], + first_half[iv_size + cipher_key_size :], + ), + EncryptionParameters( + cipher_type, + hash_type, + second_half[0:iv_size], + second_half[iv_size : iv_size + cipher_key_size], + second_half[iv_size + cipher_key_size :], + ), + ) From 4d30b31c55aeb2b42cf6a6115c89103feb996be3 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 16:55:49 +0200 Subject: [PATCH 12/31] Finish first pass at `secio` implementation --- libp2p/security/secio/exceptions.py | 14 ++ libp2p/security/secio/transport.py | 194 ++++++++++++++++++++-------- 2 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 libp2p/security/secio/exceptions.py diff --git a/libp2p/security/secio/exceptions.py b/libp2p/security/secio/exceptions.py new file mode 100644 index 00000000..a5f7464d --- /dev/null +++ b/libp2p/security/secio/exceptions.py @@ -0,0 +1,14 @@ +class SecioException(Exception): + pass + + +class PeerMismatchException(SecioException): + pass + + +class InvalidSignatureOnExchange(SecioException): + pass + + +class HandshakeFailed(SecioException): + pass diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index c0c7745c..8ea27989 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -1,6 +1,18 @@ from dataclasses import dataclass +import hashlib from typing import Optional, Tuple +import multihash + +from libp2p.crypto.authenticated_encryption import ( + EncryptionParameters as AuthenticatedEncryptionParameters, +) +from libp2p.crypto.authenticated_encryption import ( + initialize_pair as initialize_pair_for_encryption, +) +from libp2p.crypto.authenticated_encryption import MacAndCipher as Encrypter +from libp2p.crypto.ecc import ECCPublicKey +from libp2p.crypto.key_exchange import create_ephemeral_key_pair from libp2p.crypto.keys import PrivateKey, PublicKey from libp2p.io.msgio import encode as encode_message from libp2p.io.msgio import read_next_message @@ -10,6 +22,12 @@ from libp2p.security.base_session import BaseSession from libp2p.security.base_transport import BaseSecureTransport from libp2p.security.secure_conn_interface import ISecureConn +from .exceptions import ( + HandshakeFailed, + InvalidSignatureOnExchange, + PeerMismatchException, + SecioException, +) from .pb.spipe_pb2 import Exchange, Propose ID = "/secio/1.0.0" @@ -23,11 +41,45 @@ DEFAULT_SUPPORTED_CIPHERS = "AES-128" DEFAULT_SUPPORTED_HASHES = "SHA256" +@dataclass class SecureSession(BaseSession): local_peer: PeerID + local_encryption_parameters: AuthenticatedEncryptionParameters + remote_peer: PeerID - # specialize read and write - pass + remote_encryption_parameters: AuthenticatedEncryptionParameters + + conn: IRawConnection + + def __post_init__(self): + self._initialize_authenticated_encryption_for_local_peer() + self._initialize_authenticated_encryption_for_remote_peer() + + def _initialize_authenticated_encryption_for_local_peer(self) -> None: + self.local_encrypter = Encrypter(self.local_encryption_parameters) + + def _initialize_authenticated_encryption_for_remote_peer(self) -> None: + self.remote_encrypter = Encrypter(self.remote_encryption_parameters) + + async def read(self) -> bytes: + return await self._read_msg() + + async def _read_msg(self) -> bytes: + # TODO do we need to serialize reads? + msg = await read_next_message(self.conn) + return self.remote_encrypter.decrypt_if_valid(msg) + + async def write(self, data: bytes) -> None: + await self._write_msg(data) + + async def _write_msg(self, data: bytes) -> None: + # TODO do we need to serialize writes? + encrypted_data = self.local_encrypter.encrypt(data) + tag = self.local_encrypter.authenticate(encrypted_data) + msg = encode_message(encrypted_data + tag) + # TODO clean up how we write messages + self.conn.writer.write(msg) + await self.conn.writer.drain() @dataclass(frozen=True) @@ -81,9 +133,19 @@ class EncryptionParameters: hash_type: str ephemeral_public_key: PublicKey - keys: ... - cipher: ... - mac: ... + + +@dataclass +class SessionParameters: + local_peer: PeerID + local_encryption_parameters: EncryptionParameters + + remote_peer: PeerID + remote_encryption_parameters: EncryptionParameters + + # order is a comparator used to break the symmetry b/t each pair of peers + order: int + shared_key: bytes async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: @@ -95,16 +157,9 @@ async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: return await read_next_message(conn.reader) -@dataclass -class SessionParameters: - local_peer: PeerID - local_encryption_parameters: EncryptionParameters - remote_peer: PeerID - remote_encryption_parameters: EncryptionParameters - - def _mk_multihash_sha256(data: bytes) -> bytes: - pass + digest = hashlib.sha256(data).digest() + return multihash.encode(digest, "sha2-256") def _mk_score(public_key: PublicKey, nonce: bytes) -> bytes: @@ -130,7 +185,7 @@ def _select_parameter_from_order( def _select_encryption_parameters( local_proposal: Proposal, remote_proposal: Proposal -) -> Tuple[str, str, str]: +) -> Tuple[str, str, str, int]: first_score = _mk_score(remote_proposal.public_key, local_proposal.nonce) second_score = _mk_score(local_proposal.public_key, remote_proposal.nonce) @@ -148,12 +203,13 @@ def _select_encryption_parameters( _select_parameter_from_order( order, DEFAULT_SUPPORTED_EXCHANGES, remote_proposal.exchanges ), - _select_encryption_parameters( + _select_parameter_from_order( order, DEFAULT_SUPPORTED_CIPHERS, remote_proposal.ciphers ), - _select_encryption_parameters( + _select_parameter_from_order( order, DEFAULT_SUPPORTED_HASHES, remote_proposal.hashes ), + order, ) @@ -163,7 +219,8 @@ async def _establish_session_parameters( remote_peer: Optional[PeerID], conn: IRawConnection, nonce: bytes, -) -> SessionParameters: +) -> Tuple[SessionParameters, bytes]: + # establish shared encryption parameters session_parameters = SessionParameters() session_parameters.local_peer = local_peer @@ -189,7 +246,7 @@ async def _establish_session_parameters( raise PeerMismatchException() session_parameters.remote_peer = remote_peer - curve_param, cipher_param, hash_param = _select_encryption_parameters( + curve_param, cipher_param, hash_param, order = _select_encryption_parameters( local_proposal, remote_proposal ) local_encryption_parameters.curve_type = curve_param @@ -198,45 +255,77 @@ async def _establish_session_parameters( remote_encryption_parameters.curve_type = curve_param remote_encryption_parameters.cipher_type = cipher_param remote_encryption_parameters.hash_type = hash_param + session_parameters.order = order # exchange ephemeral pub keys - local_ephemeral_key_pair, shared_key_generator = create_elliptic_key_pair( - encryption_parameters + local_ephemeral_public_key, shared_key_generator = create_ephemeral_key_pair( + curve_param ) - local_selection = _mk_serialized_selection( - local_proposal, remote_proposal, local_ephemeral_key_pair.public_key + local_encryption_parameters.ephemeral_public_key = local_ephemeral_public_key + local_selection = ( + serialized_local_proposal + + serialized_remote_proposal + + local_ephemeral_public_key.to_bytes() ) - serialized_local_selection = _mk_serialized_selection(local_selection) - - local_exchange = _mk_exchange( - local_ephemeral_key_pair.public_key, serialized_local_selection + exchange_signature = local_private_key.sign(local_selection) + local_exchange = Exchange( + ephemeral_public_key=local_ephemeral_public_key.to_bytes(), + signature=exchange_signature, ) - serialized_local_exchange = _mk_serialized_exchange_msg(local_exchange) - serialized_remote_exchange = await _response_to_msg(serialized_local_exchange) - remote_exchange = _parse_exchange(serialized_remote_exchange) + serialized_local_exchange = local_exchange.SerializeToString() + serialized_remote_exchange = await _response_to_msg(conn, serialized_local_exchange) - remote_selection = _mk_remote_selection( - remote_exchange, local_proposal, remote_proposal + remote_exchange = Exchange() + remote_exchange.ParseFromString(serialized_remote_exchange) + + remote_ephemeral_public_key_bytes = remote_exchange.ephemeral_public_key + remote_ephemeral_public_key = ECCPublicKey.from_bytes( + remote_ephemeral_public_key_bytes ) - verify_exchange(remote_exchange, remote_selection, remote_proposal) + remote_encryption_parameters.ephemeral_public_key = remote_ephemeral_public_key + remote_selection = ( + serialized_remote_proposal + + serialized_local_proposal + + remote_ephemeral_public_key_bytes + ) + valid_signature = remote_encryption_parameters.permanent_public_key.verify( + remote_selection, remote_exchange.signature + ) + if not valid_signature: + raise InvalidSignatureOnExchange() - # return all the data we need + shared_key = shared_key_generator(remote_ephemeral_public_key_bytes) + session_parameters.shared_key = shared_key + + return session_parameters, remote_proposal.nonce -def _mk_session_from(session_parameters): - # use ephemeral pubkey to make a shared key - # stretch shared key to get two keys - # decide which side has which key - # set up mac and cipher, based on shared key, for each side - # make new rdr/wtr pairs using each mac/cipher gadget - pass +def _mk_session_from( + session_parameters: SessionParameters, conn: IRawConnection +) -> SecureSession: + key_set1, key_set2 = initialize_pair_for_encryption( + session_parameters.local_encryption_parameters.cipher_type, + session_parameters.local_encryption_parameters.hash_type, + session_parameters.shared_key, + ) + + if session_parameters.order < 0: + key_set1, key_set2 = key_set2, key_set1 + + session = SecureSession( + session_parameters.local_peer, + key_set1, + session_parameters.remote_peer, + key_set2, + conn, + ) + return session -async def _close_handshake(session): - # send nonce over encrypted channel - # verify we get our nonce back - pass +async def _finish_handshake(session: ISecureConn, remote_nonce: bytes) -> bytes: + await session.write(remote_nonce) + return await session.read() async def create_secure_session( @@ -247,21 +336,24 @@ async def create_secure_session( If successful, return an object that provides secure communication to the ``remote_peer``. """ - nonce = transport.get_nonce() + local_nonce = transport.get_nonce() local_peer = transport.local_peer local_private_key = transport.local_private_key try: - session_parameters = await _establish_session_parameters( - local_peer, local_private_key, remote_peer, conn, nonce + session_parameters, remote_nonce = await _establish_session_parameters( + local_peer, local_private_key, remote_peer, conn, local_nonce ) - except PeerMismatchException as e: + except SecioException as e: conn.close() raise e - session = _mk_session_from(session_parameters) + session = _mk_session_from(session_parameters, conn) - await _close_handshake(session) + received_nonce = await _finish_handshake(session, remote_nonce) + if received_nonce != local_nonce: + conn.close() + raise HandshakeFailed() return session From 8e913a3faa1401326bf32ec6eef246a96cc7c2ff Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 22:12:13 +0200 Subject: [PATCH 13/31] Dispatch serialization of keys based on key type - Add some tests to check high-level roundtrip --- libp2p/crypto/keys.py | 26 ++++++++++++++++++++++++-- libp2p/crypto/secp256k1.py | 21 ++++++++++++++++++--- libp2p/crypto/serialization.py | 22 ++++++++++++++++++++++ libp2p/security/secio/transport.py | 4 ++-- tests/crypto/secp256k1.py | 22 ++++++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 libp2p/crypto/serialization.py create mode 100644 tests/crypto/secp256k1.py diff --git a/libp2p/crypto/keys.py b/libp2p/crypto/keys.py index 33cca9d5..0647a4bf 100644 --- a/libp2p/crypto/keys.py +++ b/libp2p/crypto/keys.py @@ -33,6 +33,9 @@ class Key(ABC): """ ... + def __eq__(self, other: "Key") -> bool: + return self.impl == other.impl + class PublicKey(Key): """ @@ -61,14 +64,18 @@ class PublicKey(Key): """ return self._serialize_to_protobuf().SerializeToString() + @classmethod + def deserialize_from_protobuf(cls, protobuf_data: bytes) -> protobuf.PublicKey: + protobuf_key = protobuf.PublicKey() + protobuf_key.ParseFromString(protobuf_data) + return protobuf_key + class PrivateKey(Key): """ A ``PrivateKey`` represents a cryptographic private key. """ - protobuf_constructor = protobuf.PrivateKey - @abstractmethod def sign(self, data: bytes) -> bytes: ... @@ -92,6 +99,21 @@ class PrivateKey(Key): """ return self._serialize_to_protobuf().SerializeToString() + def _protobuf_from_serialization(self, data: bytes) -> protobuf.PrivateKey: + """ + Return the protobuf representation of this ``Key``. + """ + key_type = self.get_type().value + data = self.to_bytes() + protobuf_key = protobuf.PrivateKey(key_type=key_type, data=data) + return protobuf_key + + @classmethod + def deserialize_from_protobuf(cls, protobuf_data: bytes) -> protobuf.PrivateKey: + protobuf_key = protobuf.PrivateKey() + protobuf_key.ParseFromString(protobuf_data) + return protobuf_key + @dataclass(frozen=True) class KeyPair: diff --git a/libp2p/crypto/secp256k1.py b/libp2p/crypto/secp256k1.py index 524877c9..475c1673 100644 --- a/libp2p/crypto/secp256k1.py +++ b/libp2p/crypto/secp256k1.py @@ -11,9 +11,14 @@ class Secp256k1PublicKey(PublicKey): return self.impl.format() @classmethod - def from_bytes(cls, key_bytes: bytes) -> "Secp256k1PublicKey": - secp256k1_pubkey = coincurve.PublicKey(key_bytes) - return cls(secp256k1_pubkey) + def from_bytes(cls, data: bytes) -> "Secp256k1PublicKey": + impl = coincurve.PublicKey(data) + return cls(impl) + + @classmethod + def deserialize(cls, data: bytes) -> "Secp256k1PublicKey": + protobuf_key = cls.deserialize_from_protobuf(data) + return cls.from_bytes(protobuf_key.data) def get_type(self) -> KeyType: return KeyType.Secp256k1 @@ -34,6 +39,16 @@ class Secp256k1PrivateKey(PrivateKey): def to_bytes(self) -> bytes: return self.impl.secret + @classmethod + def from_bytes(cls, data: bytes) -> "Secp256k1PrivateKey": + impl = coincurve.PrivateKey(data) + return cls(impl) + + @classmethod + def deserialize(cls, data: bytes) -> "Secp256k1PrivateKey": + protobuf_key = cls.deserialize_from_protobuf(data) + return cls.from_bytes(protobuf_key.data) + def get_type(self) -> KeyType: return KeyType.Secp256k1 diff --git a/libp2p/crypto/serialization.py b/libp2p/crypto/serialization.py new file mode 100644 index 00000000..5b6b2764 --- /dev/null +++ b/libp2p/crypto/serialization.py @@ -0,0 +1,22 @@ +from libp2p.crypto.keys import KeyType, PrivateKey, PublicKey +from libp2p.crypto.secp256k1 import Secp256k1PrivateKey, Secp256k1PublicKey + +key_type_to_public_key_deserializer = { + KeyType.Secp256k1.value: Secp256k1PublicKey.from_bytes +} + +key_type_to_private_key_deserializer = { + KeyType.Secp256k1.value: Secp256k1PrivateKey.from_bytes +} + + +def deserialize_public_key(data: bytes) -> PublicKey: + f = PublicKey.deserialize_from_protobuf(data) + deserializer = key_type_to_public_key_deserializer[f.key_type] + return deserializer(f.data) + + +def deserialize_private_key(data: bytes) -> PrivateKey: + f = PrivateKey.deserialize_from_protobuf(data) + deserializer = key_type_to_private_key_deserializer[f.key_type] + return deserializer(f.data) diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index 8ea27989..955c9068 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -14,6 +14,7 @@ from libp2p.crypto.authenticated_encryption import MacAndCipher as Encrypter from libp2p.crypto.ecc import ECCPublicKey from libp2p.crypto.key_exchange import create_ephemeral_key_pair from libp2p.crypto.keys import PrivateKey, PublicKey +from libp2p.crypto.serialization import deserialize_public_key from libp2p.io.msgio import encode as encode_message from libp2p.io.msgio import read_next_message from libp2p.network.connection.raw_connection_interface import IRawConnection @@ -112,8 +113,7 @@ class Proposal: nonce = protobuf.rand public_key_protobuf_bytes = protobuf.public_key - # TODO (ralexstokes) handle genericity in the deserialization - public_key = PublicKey.deserialize(public_key_protobuf_bytes) + public_key = deserialize_public_key(public_key_protobuf_bytes) exchanges = protobuf.exchanges ciphers = protobuf.ciphers hashes = protobuf.hashes diff --git a/tests/crypto/secp256k1.py b/tests/crypto/secp256k1.py new file mode 100644 index 00000000..81e9eb23 --- /dev/null +++ b/tests/crypto/secp256k1.py @@ -0,0 +1,22 @@ +from libp2p.crypto.secp256k1 import create_new_key_pair +from libp2p.crypto.serialization import deserialize_private_key, deserialize_public_key + + +def test_public_key_serialize_deserialize_round_trip(): + key_pair = create_new_key_pair() + public_key = key_pair.public_key + + public_key_bytes = public_key.serialize() + another_public_key = deserialize_public_key(public_key_bytes) + + assert public_key == another_public_key + + +def test_private_key_serialize_deserialize_round_trip(): + key_pair = create_new_key_pair() + private_key = key_pair.private_key + + private_key_bytes = private_key.serialize() + another_private_key = deserialize_private_key(private_key_bytes) + + assert private_key == another_private_key From 0fa3331b8c7cb6b1bd4476f112cd5d77b9694301 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 23:02:53 +0200 Subject: [PATCH 14/31] Add clearer indication of "self encryption" --- libp2p/security/secio/exceptions.py | 9 +++++++++ libp2p/security/secio/transport.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/libp2p/security/secio/exceptions.py b/libp2p/security/secio/exceptions.py index a5f7464d..1461be9f 100644 --- a/libp2p/security/secio/exceptions.py +++ b/libp2p/security/secio/exceptions.py @@ -2,6 +2,15 @@ class SecioException(Exception): pass +class SelfEncryption(SecioException): + """ + Raised to indicate that a host is attempting to encrypt communications + with itself. + """ + + pass + + class PeerMismatchException(SecioException): pass diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index 955c9068..f77f3535 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -28,6 +28,7 @@ from .exceptions import ( InvalidSignatureOnExchange, PeerMismatchException, SecioException, + SelfEncryption, ) from .pb.spipe_pb2 import Exchange, Propose @@ -195,9 +196,8 @@ def _select_encryption_parameters( elif second_score < first_score: order = 1 - # NOTE: if order is 0, "talking to self" - # TODO(ralexstokes) nicer error handling here... - assert order != 0 + if order == 0: + raise SelfEncryption() return ( _select_parameter_from_order( From 1adef05e941644e1baa2489c957af6917afd4022 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 23:43:36 +0200 Subject: [PATCH 15/31] Typing and linting fixes --- libp2p/crypto/authenticated_encryption.py | 2 +- libp2p/crypto/ecc.py | 8 +++-- libp2p/crypto/key_exchange.py | 7 ++-- libp2p/crypto/keys.py | 14 ++++---- libp2p/io/msgio.py | 2 +- libp2p/security/base_session.py | 8 ++--- libp2p/security/secio/exceptions.py | 4 +++ libp2p/security/secio/transport.py | 41 ++++++++++++++++------- 8 files changed, 53 insertions(+), 33 deletions(-) diff --git a/libp2p/crypto/authenticated_encryption.py b/libp2p/crypto/authenticated_encryption.py index f84ecb74..733d4b5f 100644 --- a/libp2p/crypto/authenticated_encryption.py +++ b/libp2p/crypto/authenticated_encryption.py @@ -98,7 +98,7 @@ def initialize_pair( authenticator.update(tag) tag = authenticator.digest() - half = len(result) / 2 + half = int(len(result) / 2) first_half = result[:half] second_half = result[half:] diff --git a/libp2p/crypto/ecc.py b/libp2p/crypto/ecc.py index 7cfb4335..f4d4c542 100644 --- a/libp2p/crypto/ecc.py +++ b/libp2p/crypto/ecc.py @@ -1,3 +1,5 @@ +from typing import cast + from Crypto.PublicKey import ECC from Crypto.PublicKey.ECC import EccKey @@ -9,7 +11,7 @@ class ECCPublicKey(PublicKey): self.impl = impl def to_bytes(self) -> bytes: - return self.impl.export_key("DER") + return cast(bytes, self.impl.export_key(format="DER")) @classmethod def from_bytes(cls, data: bytes) -> "ECCPublicKey": @@ -33,7 +35,7 @@ class ECCPrivateKey(PrivateKey): return cls(private_key_impl) def to_bytes(self) -> bytes: - return self.impl.export_key("DER") + return cast(bytes, self.impl.export_key(format="DER")) def get_type(self) -> KeyType: return KeyType.ECC_P256 @@ -42,7 +44,7 @@ class ECCPrivateKey(PrivateKey): raise NotImplementedError def get_public_key(self) -> PublicKey: - return ECCPublicKey(self.impl.publickey()) + return ECCPublicKey(self.impl.public_key()) def create_new_key_pair(curve: str) -> KeyPair: diff --git a/libp2p/crypto/key_exchange.py b/libp2p/crypto/key_exchange.py index a204de31..5da467f0 100644 --- a/libp2p/crypto/key_exchange.py +++ b/libp2p/crypto/key_exchange.py @@ -1,8 +1,8 @@ -from typing import Callable, Tuple +from typing import Callable, Tuple, cast import Crypto.PublicKey.ECC as ECC -from libp2p.crypto.ecc import create_new_key_pair +from libp2p.crypto.ecc import ECCPrivateKey, create_new_key_pair from libp2p.crypto.keys import PublicKey SharedKeyGenerator = Callable[[bytes], bytes] @@ -20,7 +20,8 @@ def create_ephemeral_key_pair(curve_type: str) -> Tuple[PublicKey, SharedKeyGene def _key_exchange(serialized_remote_public_key: bytes) -> bytes: remote_public_key = ECC.import_key(serialized_remote_public_key) curve_point = remote_public_key.pointQ - secret_point = curve_point * key_pair.private_key.impl.d + private_key = cast(ECCPrivateKey, key_pair.private_key) + secret_point = curve_point * private_key.impl.d byte_size = secret_point.size_in_bytes() return secret_point.x.to_bytes(byte_size, byteorder="big") diff --git a/libp2p/crypto/keys.py b/libp2p/crypto/keys.py index 0647a4bf..0cbdfd0e 100644 --- a/libp2p/crypto/keys.py +++ b/libp2p/crypto/keys.py @@ -33,8 +33,10 @@ class Key(ABC): """ ... - def __eq__(self, other: "Key") -> bool: - return self.impl == other.impl + def __eq__(self, other: object) -> bool: + if not isinstance(other, Key): + return NotImplemented + return self.to_bytes() == other.to_bytes() class PublicKey(Key): @@ -66,9 +68,7 @@ class PublicKey(Key): @classmethod def deserialize_from_protobuf(cls, protobuf_data: bytes) -> protobuf.PublicKey: - protobuf_key = protobuf.PublicKey() - protobuf_key.ParseFromString(protobuf_data) - return protobuf_key + return protobuf.PublicKey.FromString(protobuf_data) class PrivateKey(Key): @@ -110,9 +110,7 @@ class PrivateKey(Key): @classmethod def deserialize_from_protobuf(cls, protobuf_data: bytes) -> protobuf.PrivateKey: - protobuf_key = protobuf.PrivateKey() - protobuf_key.ParseFromString(protobuf_data) - return protobuf_key + return protobuf.PrivateKey.FromString(protobuf_data) @dataclass(frozen=True) diff --git a/libp2p/io/msgio.py b/libp2p/io/msgio.py index 6ad11bcd..f745c180 100644 --- a/libp2p/io/msgio.py +++ b/libp2p/io/msgio.py @@ -2,7 +2,7 @@ import asyncio SIZE_LEN_BYTES = 4 -# TODO unify w/ https://github.com/libp2p/py-libp2p/blob/1aed52856f56a4b791696bbcbac31b5f9c2e88c9/libp2p/utils.py#L85-L99 +# TODO unify w/ https://github.com/libp2p/py-libp2p/blob/1aed52856f56a4b791696bbcbac31b5f9c2e88c9/libp2p/utils.py#L85-L99 # noqa: E501 def encode(msg_bytes: bytes) -> bytes: diff --git a/libp2p/security/base_session.py b/libp2p/security/base_session.py index b3c88148..6f62f9a1 100644 --- a/libp2p/security/base_session.py +++ b/libp2p/security/base_session.py @@ -3,7 +3,6 @@ from typing import Optional from libp2p.crypto.keys import PrivateKey, PublicKey from libp2p.network.connection.raw_connection_interface import IRawConnection from libp2p.peer.id import ID -from libp2p.security.base_transport import BaseSecureTransport from libp2p.security.secure_conn_interface import ISecureConn @@ -21,12 +20,13 @@ class BaseSession(ISecureConn): def __init__( self, - transport: BaseSecureTransport, + local_peer: ID, + local_private_key: PrivateKey, conn: IRawConnection, peer_id: Optional[ID] = None, ) -> None: - self.local_peer = transport.local_peer - self.local_private_key = transport.local_private_key + self.local_peer = local_peer + self.local_private_key = local_private_key self.remote_peer_id = peer_id self.remote_permanent_pubkey = None diff --git a/libp2p/security/secio/exceptions.py b/libp2p/security/secio/exceptions.py index 1461be9f..f9ea8cf5 100644 --- a/libp2p/security/secio/exceptions.py +++ b/libp2p/security/secio/exceptions.py @@ -21,3 +21,7 @@ class InvalidSignatureOnExchange(SecioException): class HandshakeFailed(SecioException): pass + + +class IncompatibleChoices(SecioException): + pass diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index f77f3535..03119feb 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -25,6 +25,7 @@ from libp2p.security.secure_conn_interface import ISecureConn from .exceptions import ( HandshakeFailed, + IncompatibleChoices, InvalidSignatureOnExchange, PeerMismatchException, SecioException, @@ -43,17 +44,20 @@ DEFAULT_SUPPORTED_CIPHERS = "AES-128" DEFAULT_SUPPORTED_HASHES = "SHA256" -@dataclass class SecureSession(BaseSession): - local_peer: PeerID - local_encryption_parameters: AuthenticatedEncryptionParameters + def __init__( + self, + local_peer: PeerID, + local_private_key: PrivateKey, + local_encryption_parameters: AuthenticatedEncryptionParameters, + remote_peer: PeerID, + remote_encryption_parameters: AuthenticatedEncryptionParameters, + conn: IRawConnection, + ) -> None: + super().__init__(local_peer, local_private_key, conn, remote_peer) - remote_peer: PeerID - remote_encryption_parameters: AuthenticatedEncryptionParameters - - conn: IRawConnection - - def __post_init__(self): + self.local_encryption_parameters = local_encryption_parameters + self.remote_encryption_parameters = remote_encryption_parameters self._initialize_authenticated_encryption_for_local_peer() self._initialize_authenticated_encryption_for_remote_peer() @@ -68,7 +72,8 @@ class SecureSession(BaseSession): async def _read_msg(self) -> bytes: # TODO do we need to serialize reads? - msg = await read_next_message(self.conn) + # TODO do not expose reader + msg = await read_next_message(self.conn.reader) return self.remote_encrypter.decrypt_if_valid(msg) async def write(self, data: bytes) -> None: @@ -135,6 +140,9 @@ class EncryptionParameters: ephemeral_public_key: PublicKey + def __init__(self) -> None: + pass + @dataclass class SessionParameters: @@ -148,6 +156,9 @@ class SessionParameters: order: int shared_key: bytes + def __init__(self) -> None: + pass + async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: # TODO clean up ``IRawConnection`` so that we don't have to break @@ -182,6 +193,7 @@ def _select_parameter_from_order( for first, second in zip(first_choices, second_choices): if first == second: return first + raise IncompatibleChoices() def _select_encryption_parameters( @@ -302,7 +314,9 @@ async def _establish_session_parameters( def _mk_session_from( - session_parameters: SessionParameters, conn: IRawConnection + local_private_key: PrivateKey, + session_parameters: SessionParameters, + conn: IRawConnection, ) -> SecureSession: key_set1, key_set2 = initialize_pair_for_encryption( session_parameters.local_encryption_parameters.cipher_type, @@ -315,6 +329,7 @@ def _mk_session_from( session = SecureSession( session_parameters.local_peer, + local_private_key, key_set1, session_parameters.remote_peer, key_set2, @@ -329,7 +344,7 @@ async def _finish_handshake(session: ISecureConn, remote_nonce: bytes) -> bytes: async def create_secure_session( - transport: BaseSecureTransport, conn: IRawConnection, remote_peer: PeerID = None + transport: "SecIOTransport", conn: IRawConnection, remote_peer: PeerID = None ) -> ISecureConn: """ Attempt the initial `secio` handshake with the remote peer. @@ -348,7 +363,7 @@ async def create_secure_session( conn.close() raise e - session = _mk_session_from(session_parameters, conn) + session = _mk_session_from(local_private_key, session_parameters, conn) received_nonce = await _finish_handshake(session, remote_nonce) if received_nonce != local_nonce: From 376a5d4fc68ce1d4f969b15bad9b50fa1a6ad0ba Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Fri, 23 Aug 2019 23:49:59 +0200 Subject: [PATCH 16/31] Adjust callsite --- libp2p/security/insecure/transport.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libp2p/security/insecure/transport.py b/libp2p/security/insecure/transport.py index 2aad45c0..8ad2e614 100644 --- a/libp2p/security/insecure/transport.py +++ b/libp2p/security/insecure/transport.py @@ -76,7 +76,7 @@ class InsecureTransport(BaseSecureTransport): for an inbound connection (i.e. we are not the initiator) :return: secure connection object (that implements secure_conn_interface) """ - session = InsecureSession(self, conn, ID(b"")) + session = InsecureSession(self.local_peer, self.local_private_key, conn) await session.run_handshake() return session @@ -86,7 +86,9 @@ class InsecureTransport(BaseSecureTransport): for an inbound connection (i.e. we are the initiator) :return: secure connection object (that implements secure_conn_interface) """ - session = InsecureSession(self, conn, peer_id) + session = InsecureSession( + self.local_peer, self.local_private_key, conn, peer_id + ) await session.run_handshake() return session From d1761159724f3da0d08202677dc52258f73411e8 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 00:00:11 +0200 Subject: [PATCH 17/31] Add `secio` to security upgrader suite --- libp2p/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libp2p/__init__.py b/libp2p/__init__.py index 10e71dba..3052ce68 100644 --- a/libp2p/__init__.py +++ b/libp2p/__init__.py @@ -14,6 +14,7 @@ from libp2p.peer.peerstore_interface import IPeerStore from libp2p.routing.interfaces import IPeerRouting from libp2p.routing.kademlia.kademlia_peer_router import KadmeliaPeerRouter from libp2p.security.insecure.transport import PLAINTEXT_PROTOCOL_ID, InsecureTransport +import libp2p.security.secio.transport as secio from libp2p.security.secure_transport_interface import ISecureTransport from libp2p.stream_muxer.mplex.mplex import MPLEX_PROTOCOL_ID, Mplex from libp2p.stream_muxer.muxer_multistream import MuxerClassType @@ -98,7 +99,8 @@ def initialize_default_swarm( muxer_transports_by_protocol = muxer_opt or {MPLEX_PROTOCOL_ID: Mplex} security_transports_by_protocol = sec_opt or { - TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair) + TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair), + TProtocol(secio.ID): secio.SecIOTransport(key_pair), } upgrader = TransportUpgrader( security_transports_by_protocol, muxer_transports_by_protocol From 3f4589d4979460b190a61b4f49c9279a0a7a8926 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 00:21:47 +0200 Subject: [PATCH 18/31] Get tests working --- libp2p/security/simple/transport.py | 79 +++++++++++++++++++ .../{secp256k1.py => test_secp256k1.py} | 0 2 files changed, 79 insertions(+) create mode 100644 libp2p/security/simple/transport.py rename tests/crypto/{secp256k1.py => test_secp256k1.py} (100%) diff --git a/libp2p/security/simple/transport.py b/libp2p/security/simple/transport.py new file mode 100644 index 00000000..28187d1d --- /dev/null +++ b/libp2p/security/simple/transport.py @@ -0,0 +1,79 @@ +import asyncio + +from libp2p.crypto.keys import KeyPair +from libp2p.network.connection.raw_connection_interface import IRawConnection +from libp2p.peer.id import ID +from libp2p.security.base_transport import BaseSecureTransport +from libp2p.security.insecure.transport import InsecureSession +from libp2p.security.secure_conn_interface import ISecureConn +from libp2p.transport.exceptions import SecurityUpgradeFailure +from libp2p.utils import encode_fixedint_prefixed, read_fixedint_prefixed + + +class SimpleSecurityTransport(BaseSecureTransport): + key_phrase: str + + def __init__(self, local_key_pair: KeyPair, key_phrase: str) -> None: + super().__init__(local_key_pair) + self.key_phrase = key_phrase + + async def secure_inbound(self, conn: IRawConnection) -> ISecureConn: + """ + Secure the connection, either locally or by communicating with opposing node via conn, + for an inbound connection (i.e. we are not the initiator) + :return: secure connection object (that implements secure_conn_interface) + """ + await conn.write(encode_fixedint_prefixed(self.key_phrase.encode())) + incoming = (await read_fixedint_prefixed(conn)).decode() + + if incoming != self.key_phrase: + raise SecurityUpgradeFailure( + "Key phrase differed between nodes. Expected " + self.key_phrase + ) + + session = InsecureSession( + self.local_peer, self.local_private_key, conn, ID(b"") + ) + # NOTE: Here we calls `run_handshake` for both sides to exchange their public keys and + # peer ids, otherwise tests fail. However, it seems pretty weird that + # `SimpleSecurityTransport` sends peer id through `Insecure`. + await session.run_handshake() + # NOTE: this is abusing the abstraction we have here + # but this code may be deprecated soon and this exists + # mainly to satisfy a test that will go along w/ it + # FIXME: Enable type check back when we can deprecate the simple transport. + session.key_phrase = self.key_phrase # type: ignore + return session + + async def secure_outbound(self, conn: IRawConnection, peer_id: ID) -> ISecureConn: + """ + Secure the connection, either locally or by communicating with opposing node via conn, + for an inbound connection (i.e. we are the initiator) + :return: secure connection object (that implements secure_conn_interface) + """ + await conn.write(encode_fixedint_prefixed(self.key_phrase.encode())) + incoming = (await read_fixedint_prefixed(conn)).decode() + + # Force context switch, as this security transport is built for testing locally + # in a single event loop + await asyncio.sleep(0) + + if incoming != self.key_phrase: + raise SecurityUpgradeFailure( + "Key phrase differed between nodes. Expected " + self.key_phrase + ) + + session = InsecureSession( + self.local_peer, self.local_private_key, conn, peer_id + ) + + # NOTE: Here we calls `run_handshake` for both sides to exchange their public keys and + # peer ids, otherwise tests fail. However, it seems pretty weird that + # `SimpleSecurityTransport` sends peer id through `Insecure`. + await session.run_handshake() + # NOTE: this is abusing the abstraction we have here + # but this code may be deprecated soon and this exists + # mainly to satisfy a test that will go along w/ it + # FIXME: Enable type check back when we can deprecate the simple transport. + session.key_phrase = self.key_phrase # type: ignore + return session diff --git a/tests/crypto/secp256k1.py b/tests/crypto/test_secp256k1.py similarity index 100% rename from tests/crypto/secp256k1.py rename to tests/crypto/test_secp256k1.py From 228032805a228d2d7401cb2b638d7dc1dd5c2e10 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 11:43:27 +0200 Subject: [PATCH 19/31] Some code cleanup --- libp2p/__init__.py | 2 +- libp2p/security/secio/transport.py | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/libp2p/__init__.py b/libp2p/__init__.py index 3052ce68..dce33b9c 100644 --- a/libp2p/__init__.py +++ b/libp2p/__init__.py @@ -100,7 +100,7 @@ def initialize_default_swarm( muxer_transports_by_protocol = muxer_opt or {MPLEX_PROTOCOL_ID: Mplex} security_transports_by_protocol = sec_opt or { TProtocol(PLAINTEXT_PROTOCOL_ID): InsecureTransport(key_pair), - TProtocol(secio.ID): secio.SecIOTransport(key_pair), + TProtocol(secio.ID): secio.Transport(key_pair), } upgrader = TransportUpgrader( security_transports_by_protocol, muxer_transports_by_protocol diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index 03119feb..e1d4530f 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -344,17 +344,17 @@ async def _finish_handshake(session: ISecureConn, remote_nonce: bytes) -> bytes: async def create_secure_session( - transport: "SecIOTransport", conn: IRawConnection, remote_peer: PeerID = None + local_nonce: bytes, + local_peer: PeerID, + local_private_key: PrivateKey, + conn: IRawConnection, + remote_peer: PeerID = None, ) -> ISecureConn: """ Attempt the initial `secio` handshake with the remote peer. If successful, return an object that provides secure communication to the ``remote_peer``. """ - local_nonce = transport.get_nonce() - local_peer = transport.local_peer - local_private_key = transport.local_private_key - try: session_parameters, remote_nonce = await _establish_session_parameters( local_peer, local_private_key, remote_peer, conn, local_nonce @@ -373,9 +373,9 @@ async def create_secure_session( return session -class SecIOTransport(BaseSecureTransport): +class Transport(BaseSecureTransport): """ - ``SecIOTransport`` provides a security upgrader for a ``IRawConnection``, + ``Transport`` provides a security upgrader for a ``IRawConnection``, following the `secio` protocol defined in the libp2p specs. """ @@ -388,7 +388,13 @@ class SecIOTransport(BaseSecureTransport): for an inbound connection (i.e. we are not the initiator) :return: secure connection object (that implements secure_conn_interface) """ - return await create_secure_session(self, conn) + local_nonce = self.get_nonce() + local_peer = self.local_peer + local_private_key = self.local_private_key + + return await create_secure_session( + local_nonce, local_peer, local_private_key, conn + ) async def secure_outbound( self, conn: IRawConnection, peer_id: PeerID @@ -398,4 +404,10 @@ class SecIOTransport(BaseSecureTransport): for an inbound connection (i.e. we are the initiator) :return: secure connection object (that implements secure_conn_interface) """ - return await create_secure_session(self, conn, peer_id) + local_nonce = self.get_nonce() + local_peer = self.local_peer + local_private_key = self.local_private_key + + return await create_secure_session( + local_nonce, local_peer, local_private_key, conn, peer_id + ) From b8c0ef9ebb8de2e6b7c60d3701467888b0ea2a8b Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 19:57:56 +0200 Subject: [PATCH 20/31] Fix bugs in `secio` implementation --- libp2p/crypto/authenticated_encryption.py | 30 ++++++++++++++--------- libp2p/crypto/key_exchange.py | 2 +- libp2p/security/secio/transport.py | 20 +++++++-------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/libp2p/crypto/authenticated_encryption.py b/libp2p/crypto/authenticated_encryption.py index 733d4b5f..6900f1f4 100644 --- a/libp2p/crypto/authenticated_encryption.py +++ b/libp2p/crypto/authenticated_encryption.py @@ -3,6 +3,7 @@ import hmac from typing import Tuple from Crypto.Cipher import AES +import Crypto.Util.Counter as Counter class InvalidMACException(Exception): @@ -23,8 +24,14 @@ class MacAndCipher: self.authenticator = hmac.new( parameters.mac_key, digestmod=parameters.hash_type ) + iv_bit_size = 8 * len(parameters.iv) cipher = AES.new( - parameters.cipher_key, AES.MODE_CTR, initial_value=parameters.iv + parameters.cipher_key, + AES.MODE_CTR, + counter=Counter.new( + iv_bit_size, + initial_value=int.from_bytes(parameters.iv, byteorder="big"), + ), ) self.cipher = cipher @@ -70,14 +77,16 @@ def initialize_pair( hmac_key_size = 20 seed = "key expansion".encode() - result = bytearray(2 * (iv_size + cipher_key_size + hmac_key_size)) + params_size = iv_size + cipher_key_size + hmac_key_size + result = bytearray(2 * params_size) authenticator = hmac.new(secret, digestmod=hash_type) authenticator.update(seed) tag = authenticator.digest() i = 0 - while i < len(result): + len_result = 2 * params_size + while i < len_result: authenticator = hmac.new(secret, digestmod=hash_type) authenticator.update(tag) @@ -87,10 +96,10 @@ def initialize_pair( remaining_bytes = len(another_tag) - if i + remaining_bytes > len(result): - remaining_bytes = len(result) - i + if i + remaining_bytes > len_result: + remaining_bytes = len_result - i - result[i : i + remaining_bytes] = another_tag + result[i : i + remaining_bytes] = another_tag[0:remaining_bytes] i += remaining_bytes @@ -98,23 +107,22 @@ def initialize_pair( authenticator.update(tag) tag = authenticator.digest() - half = int(len(result) / 2) - first_half = result[:half] - second_half = result[half:] + first_half = result[:params_size] + second_half = result[params_size:] return ( EncryptionParameters( cipher_type, hash_type, first_half[0:iv_size], - first_half[iv_size : iv_size + cipher_key_size], first_half[iv_size + cipher_key_size :], + first_half[iv_size : iv_size + cipher_key_size], ), EncryptionParameters( cipher_type, hash_type, second_half[0:iv_size], - second_half[iv_size : iv_size + cipher_key_size], second_half[iv_size + cipher_key_size :], + second_half[iv_size : iv_size + cipher_key_size], ), ) diff --git a/libp2p/crypto/key_exchange.py b/libp2p/crypto/key_exchange.py index 5da467f0..70e7944c 100644 --- a/libp2p/crypto/key_exchange.py +++ b/libp2p/crypto/key_exchange.py @@ -23,6 +23,6 @@ def create_ephemeral_key_pair(curve_type: str) -> Tuple[PublicKey, SharedKeyGene private_key = cast(ECCPrivateKey, key_pair.private_key) secret_point = curve_point * private_key.impl.d byte_size = secret_point.size_in_bytes() - return secret_point.x.to_bytes(byte_size, byteorder="big") + return secret_point.x.to_bytes(byte_size) return key_pair.public_key, _key_exchange diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index e1d4530f..272639e6 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -85,7 +85,7 @@ class SecureSession(BaseSession): tag = self.local_encrypter.authenticate(encrypted_data) msg = encode_message(encrypted_data + tag) # TODO clean up how we write messages - self.conn.writer.write(msg) + await self.conn.writer.write(msg) await self.conn.writer.drain() @@ -104,18 +104,17 @@ class Proposal: def serialize(self) -> bytes: protobuf = Propose( - self.nonce, - self.public_key.serialize(), - self.exchanges, - self.ciphers, - self.hashes, + rand=self.nonce, + public_key=self.public_key.serialize(), + exchanges=self.exchanges, + ciphers=self.ciphers, + hashes=self.hashes, ) return protobuf.SerializeToString() @classmethod def deserialize(cls, protobuf_bytes: bytes) -> "Proposal": - protobuf = Propose() - protobuf.ParseFromString(protobuf_bytes) + protobuf = Propose.FromString(protobuf_bytes) nonce = protobuf.rand public_key_protobuf_bytes = protobuf.public_key @@ -163,15 +162,14 @@ class SessionParameters: async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: # TODO clean up ``IRawConnection`` so that we don't have to break # the abstraction - conn.writer.write(encode_message(msg)) + await conn.writer.write(encode_message(msg)) await conn.writer.drain() return await read_next_message(conn.reader) def _mk_multihash_sha256(data: bytes) -> bytes: - digest = hashlib.sha256(data).digest() - return multihash.encode(digest, "sha2-256") + return multihash.digest(data, "sha2-256") def _mk_score(public_key: PublicKey, nonce: bytes) -> bytes: From 9355f33da80374957638909d8425d365dc039adf Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 19:58:56 +0200 Subject: [PATCH 21/31] Add basic test for `secio` Two peers in-memory can create a secure, bidirectional channel --- tests/security/test_secio.py | 120 +++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/security/test_secio.py diff --git a/tests/security/test_secio.py b/tests/security/test_secio.py new file mode 100644 index 00000000..27a00c5e --- /dev/null +++ b/tests/security/test_secio.py @@ -0,0 +1,120 @@ +import asyncio + +import pytest + +from libp2p.crypto.secp256k1 import create_new_key_pair +from libp2p.network.connection.raw_connection_interface import IRawConnection +from libp2p.peer.id import ID +from libp2p.security.secio.transport import NONCE_SIZE, create_secure_session + + +class InMemoryConnection(IRawConnection): + def __init__(self, peer, initiator=False): + self.peer = peer + self.recv_queue = asyncio.Queue() + self.send_queue = asyncio.Queue() + self.initiator = initiator + + self.current_msg = None + self.current_position = 0 + + self.closed = False + self.stream_counter = 0 + + @property + def writer(self): + return self + + @property + def reader(self): + return self + + async def write(self, data: bytes) -> None: + if self.closed: + raise Exception("InMemoryConnection is closed for writing") + + await self.send_queue.put(data) + + async def drain(self): + return + + async def readexactly(self, n): + return await self.read(n) + + async def read(self, n: int = -1) -> bytes: + """ + NOTE: have to buffer the current message and juggle packets + off the recv queue to satisfy the semantics of this function. + """ + if self.closed: + raise Exception("InMemoryConnection is closed for reading") + + if not self.current_msg: + self.current_msg = await self.recv_queue.get() + self.current_position = 0 + + if n < 0: + msg = self.current_msg + self.current_msg = None + return msg + + next_msg = self.current_msg[self.current_position : self.current_position + n] + self.current_position += n + if self.current_position == len(self.current_msg): + self.current_msg = None + return next_msg + + def close(self) -> None: + self.closed = True + + def next_stream_id(self) -> int: + self.stream_counter += 1 + return self.stream_counter + + +async def create_pipe(local_conn, remote_conn): + try: + while True: + next_msg = await local_conn.send_queue.get() + await remote_conn.recv_queue.put(next_msg) + except asyncio.CancelledError: + return + + +@pytest.mark.asyncio +async def test_create_secure_session(): + local_nonce = b"\x01" * NONCE_SIZE + local_key_pair = create_new_key_pair(b"a") + local_peer = ID.from_pubkey(local_key_pair.public_key) + + remote_nonce = b"\x02" * NONCE_SIZE + remote_key_pair = create_new_key_pair(b"b") + remote_peer = ID.from_pubkey(remote_key_pair.public_key) + + local_conn = InMemoryConnection(local_peer, initiator=True) + remote_conn = InMemoryConnection(remote_peer) + + local_pipe_task = asyncio.create_task(create_pipe(local_conn, remote_conn)) + remote_pipe_task = asyncio.create_task(create_pipe(remote_conn, local_conn)) + + local_session_builder = create_secure_session( + local_nonce, local_peer, local_key_pair.private_key, local_conn, remote_peer + ) + remote_session_builder = create_secure_session( + remote_nonce, remote_peer, remote_key_pair.private_key, remote_conn + ) + local_secure_conn, remote_secure_conn = await asyncio.gather( + local_session_builder, remote_session_builder + ) + + local_pipe_task.cancel() + remote_pipe_task.cancel() + await local_pipe_task + await remote_pipe_task + + assert local_secure_conn + assert remote_secure_conn + + +if __name__ == "__main__": + asyncio.run(test_create_secure_session()) From 852609c85d67f7f2ecd02d52526cb47d3558e734 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 20:33:55 +0200 Subject: [PATCH 22/31] Clean up base session type --- libp2p/security/base_session.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libp2p/security/base_session.py b/libp2p/security/base_session.py index 6f62f9a1..2d76198a 100644 --- a/libp2p/security/base_session.py +++ b/libp2p/security/base_session.py @@ -31,8 +31,6 @@ class BaseSession(ISecureConn): self.remote_permanent_pubkey = None self.conn = conn - self.writer = self.conn.writer - self.reader = self.conn.reader self.initiator = self.conn.initiator async def write(self, data: bytes) -> None: From 44e5de636ff25cadbfef782d98dc5501df7c5ea3 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 20:36:00 +0200 Subject: [PATCH 23/31] Add "friendly" peer ID string representation for debugging --- libp2p/peer/id.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libp2p/peer/id.py b/libp2p/peer/id.py index 303f519f..c1b52f02 100644 --- a/libp2p/peer/id.py +++ b/libp2p/peer/id.py @@ -6,6 +6,11 @@ import multihash from libp2p.crypto.keys import PublicKey +# NOTE: ``FRIENDLY_IDS`` renders a ``str`` representation of ``ID`` as a +# short string of a prefix of the base58 representation. This feature is primarily +# intended for debugging, logging, etc. +FRIENDLY_IDS = True + class ID: _bytes: bytes @@ -32,7 +37,13 @@ class ID: def __repr__(self) -> str: return "" - __str__ = pretty = to_string = to_base58 + pretty = to_string = to_base58 + + def __str__(self) -> str: + if FRIENDLY_IDS: + return self.to_string()[2:8] + else: + return self.to_string() def __eq__(self, other: object) -> bool: if isinstance(other, str): From 7c004a4e1443860d2ac132068de2bd9e7625db2a Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 20:36:33 +0200 Subject: [PATCH 24/31] Mypy fixes --- libp2p/crypto/key_exchange.py | 3 ++- libp2p/security/secio/transport.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/libp2p/crypto/key_exchange.py b/libp2p/crypto/key_exchange.py index 70e7944c..4e895c95 100644 --- a/libp2p/crypto/key_exchange.py +++ b/libp2p/crypto/key_exchange.py @@ -1,5 +1,6 @@ from typing import Callable, Tuple, cast +from Crypto.Math.Numbers import Integer import Crypto.PublicKey.ECC as ECC from libp2p.crypto.ecc import ECCPrivateKey, create_new_key_pair @@ -23,6 +24,6 @@ def create_ephemeral_key_pair(curve_type: str) -> Tuple[PublicKey, SharedKeyGene private_key = cast(ECCPrivateKey, key_pair.private_key) secret_point = curve_point * private_key.impl.d byte_size = secret_point.size_in_bytes() - return secret_point.x.to_bytes(byte_size) + return cast(Integer, secret_point.x).to_bytes(byte_size) return key_pair.public_key, _key_exchange diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index 272639e6..0c9f8463 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -1,6 +1,5 @@ from dataclasses import dataclass -import hashlib -from typing import Optional, Tuple +from typing import Optional, Tuple, cast import multihash @@ -17,6 +16,7 @@ from libp2p.crypto.keys import PrivateKey, PublicKey from libp2p.crypto.serialization import deserialize_public_key from libp2p.io.msgio import encode as encode_message from libp2p.io.msgio import read_next_message +from libp2p.network.connection.raw_connection import RawConnection from libp2p.network.connection.raw_connection_interface import IRawConnection from libp2p.peer.id import ID as PeerID from libp2p.security.base_session import BaseSession @@ -67,13 +67,13 @@ class SecureSession(BaseSession): def _initialize_authenticated_encryption_for_remote_peer(self) -> None: self.remote_encrypter = Encrypter(self.remote_encryption_parameters) - async def read(self) -> bytes: + async def read(self, n: int = -1) -> bytes: return await self._read_msg() async def _read_msg(self) -> bytes: # TODO do we need to serialize reads? # TODO do not expose reader - msg = await read_next_message(self.conn.reader) + msg = await read_next_message(cast(RawConnection, self.conn).reader) return self.remote_encrypter.decrypt_if_valid(msg) async def write(self, data: bytes) -> None: @@ -85,8 +85,8 @@ class SecureSession(BaseSession): tag = self.local_encrypter.authenticate(encrypted_data) msg = encode_message(encrypted_data + tag) # TODO clean up how we write messages - await self.conn.writer.write(msg) - await self.conn.writer.drain() + await cast(RawConnection, self.conn).writer.write(msg) + await cast(RawConnection, self.conn).writer.drain() @dataclass(frozen=True) @@ -162,10 +162,10 @@ class SessionParameters: async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: # TODO clean up ``IRawConnection`` so that we don't have to break # the abstraction - await conn.writer.write(encode_message(msg)) - await conn.writer.drain() + await cast(RawConnection, conn).writer.write(encode_message(msg)) + await cast(RawConnection, conn).writer.drain() - return await read_next_message(conn.reader) + return await read_next_message(cast(RawConnection, conn).reader) def _mk_multihash_sha256(data: bytes) -> bytes: From eb5ef39399b86b387ca796d2d0aa09d1d195a8df Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 22:47:56 +0200 Subject: [PATCH 25/31] Convert message IO to work w/ a `RawConnection`. --- libp2p/io/exceptions.py | 10 ++++++++++ libp2p/io/msgio.py | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 libp2p/io/exceptions.py diff --git a/libp2p/io/exceptions.py b/libp2p/io/exceptions.py new file mode 100644 index 00000000..4d06ece4 --- /dev/null +++ b/libp2p/io/exceptions.py @@ -0,0 +1,10 @@ +class MsgioException(Exception): + pass + + +class MissingLengthException(MsgioException): + pass + + +class MissingMessageException(MsgioException): + pass diff --git a/libp2p/io/msgio.py b/libp2p/io/msgio.py index f745c180..65fde685 100644 --- a/libp2p/io/msgio.py +++ b/libp2p/io/msgio.py @@ -1,4 +1,6 @@ -import asyncio +from libp2p.network.connection.raw_connection_interface import IRawConnection + +from .exceptions import MissingLengthException, MissingMessageException SIZE_LEN_BYTES = 4 @@ -10,7 +12,13 @@ def encode(msg_bytes: bytes) -> bytes: return len_prefix + msg_bytes -async def read_next_message(reader: asyncio.StreamReader) -> bytes: - len_bytes = await reader.readexactly(SIZE_LEN_BYTES) +async def read_next_message(reader: IRawConnection) -> bytes: + len_bytes = await reader.read(SIZE_LEN_BYTES) + if len(len_bytes) != SIZE_LEN_BYTES: + raise MissingLengthException() len_int = int.from_bytes(len_bytes, "big") - return await reader.readexactly(len_int) + next_msg = await reader.read(len_int) + if len(next_msg) != len_int: + # TODO makes sense to keep reading until this condition is true? + raise MissingMessageException() + return next_msg From a363ba97d181e7b47d9ed714e4087c151ada4b37 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 22:52:09 +0200 Subject: [PATCH 26/31] Work in terms of the `IRawConnection` abstraction --- libp2p/security/secio/transport.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index 0c9f8463..7bea92d8 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional, Tuple, cast +from typing import Optional, Tuple import multihash @@ -16,7 +16,6 @@ from libp2p.crypto.keys import PrivateKey, PublicKey from libp2p.crypto.serialization import deserialize_public_key from libp2p.io.msgio import encode as encode_message from libp2p.io.msgio import read_next_message -from libp2p.network.connection.raw_connection import RawConnection from libp2p.network.connection.raw_connection_interface import IRawConnection from libp2p.peer.id import ID as PeerID from libp2p.security.base_session import BaseSession @@ -72,8 +71,7 @@ class SecureSession(BaseSession): async def _read_msg(self) -> bytes: # TODO do we need to serialize reads? - # TODO do not expose reader - msg = await read_next_message(cast(RawConnection, self.conn).reader) + msg = await read_next_message(self.conn) return self.remote_encrypter.decrypt_if_valid(msg) async def write(self, data: bytes) -> None: @@ -84,9 +82,7 @@ class SecureSession(BaseSession): encrypted_data = self.local_encrypter.encrypt(data) tag = self.local_encrypter.authenticate(encrypted_data) msg = encode_message(encrypted_data + tag) - # TODO clean up how we write messages - await cast(RawConnection, self.conn).writer.write(msg) - await cast(RawConnection, self.conn).writer.drain() + await self.conn.write(msg) @dataclass(frozen=True) @@ -160,12 +156,8 @@ class SessionParameters: async def _response_to_msg(conn: IRawConnection, msg: bytes) -> bytes: - # TODO clean up ``IRawConnection`` so that we don't have to break - # the abstraction - await cast(RawConnection, conn).writer.write(encode_message(msg)) - await cast(RawConnection, conn).writer.drain() - - return await read_next_message(cast(RawConnection, conn).reader) + await conn.write(encode_message(msg)) + return await read_next_message(conn) def _mk_multihash_sha256(data: bytes) -> bytes: From 10e30beb420bdde3cddf32bd2bed080ec4e75997 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 22:57:22 +0200 Subject: [PATCH 27/31] Disable "friendly" IDs for tests that expect a full string --- tests/peer/test_peerid.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/peer/test_peerid.py b/tests/peer/test_peerid.py index ea244ce8..e808a3b6 100644 --- a/tests/peer/test_peerid.py +++ b/tests/peer/test_peerid.py @@ -4,10 +4,14 @@ import base58 import multihash from libp2p.crypto.rsa import create_new_key_pair +import libp2p.peer.id as PeerID from libp2p.peer.id import ID ALPHABETS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +# ensure we are not in "debug" mode for the following tests +PeerID.FRIENDLY_IDS = False + def test_eq_impl_for_bytes(): random_id_string = "" From 737195f4617c54be7ec2278d1ed063042b7315a6 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 23:15:31 +0200 Subject: [PATCH 28/31] Simplify testing connection w/ other simplifications --- tests/security/test_secio.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/security/test_secio.py b/tests/security/test_secio.py index 27a00c5e..da7e1f16 100644 --- a/tests/security/test_secio.py +++ b/tests/security/test_secio.py @@ -19,15 +19,6 @@ class InMemoryConnection(IRawConnection): self.current_position = 0 self.closed = False - self.stream_counter = 0 - - @property - def writer(self): - return self - - @property - def reader(self): - return self async def write(self, data: bytes) -> None: if self.closed: @@ -35,12 +26,6 @@ class InMemoryConnection(IRawConnection): await self.send_queue.put(data) - async def drain(self): - return - - async def readexactly(self, n): - return await self.read(n) - async def read(self, n: int = -1) -> bytes: """ NOTE: have to buffer the current message and juggle packets @@ -67,10 +52,6 @@ class InMemoryConnection(IRawConnection): def close(self) -> None: self.closed = True - def next_stream_id(self) -> int: - self.stream_counter += 1 - return self.stream_counter - async def create_pipe(local_conn, remote_conn): try: From f08aa339b40b8c6ed7600c52dd7c8422c34c1241 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Sat, 24 Aug 2019 23:26:26 +0200 Subject: [PATCH 29/31] Verify the channel can pass some plaintext --- tests/security/test_secio.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/security/test_secio.py b/tests/security/test_secio.py index da7e1f16..f9fdad72 100644 --- a/tests/security/test_secio.py +++ b/tests/security/test_secio.py @@ -49,7 +49,7 @@ class InMemoryConnection(IRawConnection): self.current_msg = None return next_msg - def close(self) -> None: + async def close(self) -> None: self.closed = True @@ -88,14 +88,18 @@ async def test_create_secure_session(): local_session_builder, remote_session_builder ) + msg = b"abc" + await local_secure_conn.write(msg) + received_msg = await remote_secure_conn.read() + assert received_msg == msg + + await asyncio.gather(local_secure_conn.close(), remote_secure_conn.close()) + local_pipe_task.cancel() remote_pipe_task.cancel() await local_pipe_task await remote_pipe_task - assert local_secure_conn - assert remote_secure_conn - if __name__ == "__main__": asyncio.run(test_create_secure_session()) From c1ffb03f774c88fe22a3c6b47143ca9b4a764fc2 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Mon, 26 Aug 2019 09:51:49 -0700 Subject: [PATCH 30/31] Update comment to reflect correct function --- libp2p/crypto/ecc.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libp2p/crypto/ecc.py b/libp2p/crypto/ecc.py index f4d4c542..8ede8f8c 100644 --- a/libp2p/crypto/ecc.py +++ b/libp2p/crypto/ecc.py @@ -49,8 +49,7 @@ class ECCPrivateKey(PrivateKey): def create_new_key_pair(curve: str) -> 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. + Return a new ECC keypair with the requested ``curve`` type, e.g. "P-256". """ private_key = ECCPrivateKey.new(curve) public_key = private_key.get_public_key() From fa0acd9fc56af2122740f7b8e72bdb27779455e1 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Mon, 26 Aug 2019 10:03:12 -0700 Subject: [PATCH 31/31] Apply PR feedback --- libp2p/crypto/keys.py | 9 --------- libp2p/io/exceptions.py | 5 ++++- libp2p/security/secio/transport.py | 4 ++-- tests/security/test_secio.py | 4 ---- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/libp2p/crypto/keys.py b/libp2p/crypto/keys.py index 0cbdfd0e..5bcc5a37 100644 --- a/libp2p/crypto/keys.py +++ b/libp2p/crypto/keys.py @@ -99,15 +99,6 @@ class PrivateKey(Key): """ return self._serialize_to_protobuf().SerializeToString() - def _protobuf_from_serialization(self, data: bytes) -> protobuf.PrivateKey: - """ - Return the protobuf representation of this ``Key``. - """ - key_type = self.get_type().value - data = self.to_bytes() - protobuf_key = protobuf.PrivateKey(key_type=key_type, data=data) - return protobuf_key - @classmethod def deserialize_from_protobuf(cls, protobuf_data: bytes) -> protobuf.PrivateKey: return protobuf.PrivateKey.FromString(protobuf_data) diff --git a/libp2p/io/exceptions.py b/libp2p/io/exceptions.py index 4d06ece4..6e1376fa 100644 --- a/libp2p/io/exceptions.py +++ b/libp2p/io/exceptions.py @@ -1,4 +1,7 @@ -class MsgioException(Exception): +from libp2p.exceptions import BaseLibp2pError + + +class MsgioException(BaseLibp2pError): pass diff --git a/libp2p/security/secio/transport.py b/libp2p/security/secio/transport.py index 7bea92d8..4c3dbc08 100644 --- a/libp2p/security/secio/transport.py +++ b/libp2p/security/secio/transport.py @@ -350,14 +350,14 @@ async def create_secure_session( local_peer, local_private_key, remote_peer, conn, local_nonce ) except SecioException as e: - conn.close() + await conn.close() raise e session = _mk_session_from(local_private_key, session_parameters, conn) received_nonce = await _finish_handshake(session, remote_nonce) if received_nonce != local_nonce: - conn.close() + await conn.close() raise HandshakeFailed() return session diff --git a/tests/security/test_secio.py b/tests/security/test_secio.py index f9fdad72..673cbc58 100644 --- a/tests/security/test_secio.py +++ b/tests/security/test_secio.py @@ -99,7 +99,3 @@ async def test_create_secure_session(): remote_pipe_task.cancel() await local_pipe_task await remote_pipe_task - - -if __name__ == "__main__": - asyncio.run(test_create_secure_session())