[wip] more secio implementation

This commit is contained in:
Alex Stokes 2019-08-22 17:55:36 +02:00
parent 0cc3fc24a7
commit 91e11f3ec0
No known key found for this signature in database
GPG Key ID: 51CE1721B245C086

View File

@ -1,50 +1,203 @@
from dataclasses import dataclass 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.network.connection.raw_connection_interface import IRawConnection
from libp2p.peer.id import ID as PeerID from libp2p.peer.id import ID as PeerID
from libp2p.security.base_session import BaseSession from libp2p.security.base_session import BaseSession
from libp2p.security.base_transport import BaseSecureTransport from libp2p.security.base_transport import BaseSecureTransport
from libp2p.security.secure_conn_interface import ISecureConn from libp2p.security.secure_conn_interface import ISecureConn
from .pb.spipe_pb2 import Exchange, Propose
ID = "/secio/1.0.0" ID = "/secio/1.0.0"
NONCE_SIZE = 16 # bytes
@dataclass # NOTE: the following is only a subset of allowable parameters according to the
class NegotiationContext(frozen=True): # `secio` specification.
local_peer: PeerID DEFAULT_SUPPORTED_EXCHANGES = "P-256"
remote_peer: Optional[PeerID] DEFAULT_SUPPORTED_CIPHERS = "AES-128"
DEFAULT_SUPPORTED_HASHES = "SHA256"
local_private_key: PrivateKey
conn: IRawConnection
class SecureSession(BaseSession): class SecureSession(BaseSession):
local_peer: PeerID
remote_peer: PeerID
# specialize read and write
pass 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 pass
async def _response_to_msg(msg) -> bytes: def _mk_score(public_key: PublicKey, nonce: bytes) -> bytes:
return bytes() return _mk_multihash_sha256(public_key.serialize() + nonce)
async def _establish_session_parameters(): def _select_parameter_from_order(
# propose parameters order: int, supported_parameters: str, available_parameters: str
local_proposal = _mk_local_proposal(negotiation_context) ) -> str:
serialized_local_proposal = _mk_serialized_proposal(local_proposal) if order < 0:
serialized_remote_proposal = await _response_to_msg(serialized_local_proposal) 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 def _select_encryption_parameters(
encryption_parameters = _select_encryption_parameters(remote_proposal) 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 # exchange ephemeral pub keys
local_ephemeral_key_pair, shared_key_generator = create_elliptic_key_pair( local_ephemeral_key_pair, shared_key_generator = create_elliptic_key_pair(
@ -86,14 +239,25 @@ async def _close_handshake(session):
pass 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 try:
of this session is now ready for secure communication. session_parameters = await _establish_session_parameters(
""" local_peer, local_private_key, remote_peer, conn, nonce
session_parameters = await _establish_session_parameters() )
except PeerMismatchException as e:
conn.close()
raise e
session = _mk_session_from(session_parameters) session = _mk_session_from(session_parameters)
@ -102,22 +266,15 @@ async def _run_handshake(negotiation_context: NegotiationContext):
return 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): class SecIOTransport(BaseSecureTransport):
""" """
``SecIOTransport`` provides a security upgrader for a ``IRawConnection``, ``SecIOTransport`` provides a security upgrader for a ``IRawConnection``,
following the `secio` protocol defined in the libp2p specs. 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: async def secure_inbound(self, conn: IRawConnection) -> ISecureConn:
""" """
Secure the connection, either locally or by communicating with opposing node via conn, Secure the connection, either locally or by communicating with opposing node via conn,