py-libp2p/libp2p/security/secio/transport.py
2019-08-24 22:37:44 +02:00

295 lines
9.5 KiB
Python

from dataclasses import dataclass
from typing import Optional, Tuple
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
# 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
@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
def _mk_score(public_key: PublicKey, nonce: bytes) -> bytes:
return _mk_multihash_sha256(public_key.serialize() + nonce)
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]
for first, second in zip(first_choices, second_choices):
if first == second:
return first
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(
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 create_secure_session(
transport: BaseSecureTransport, 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``.
"""
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
)
except PeerMismatchException as e:
conn.close()
raise e
session = _mk_session_from(session_parameters)
await _close_handshake(session)
return session
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,
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)