def test_that_serializing_and_deserializing_message_with_wrong_payload_type_should_raise_exception( self): middleman_message = GolemMessageFrame(Ping(), self.request_id) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) payload_type_position = FRAME_SIGNATURE_BYTES_LENGTH + FRAME_REQUEST_ID_BYTES_LENGTH invalid_payload_type = 100 # Sanity check for payload type in this case to be between expected bytes assert raw_message[payload_type_position:payload_type_position + 1] == b'\x00' assert invalid_payload_type not in PAYLOAD_TYPE_TO_MIDDLEMAN_MESSAGE_CLASS # Replace bytes with payload length raw_message = ( raw_message[:payload_type_position] + bytes(bytearray([invalid_payload_type])) + raw_message[payload_type_position + FRAME_PAYLOAD_TYPE_LENGTH:]) # Replace message signature new_signature = ecdsa_sign(CONCENT_PRIVATE_KEY, raw_message[FRAME_SIGNATURE_BYTES_LENGTH:]) raw_message_with_new_signature = new_signature + raw_message[ FRAME_SIGNATURE_BYTES_LENGTH:] with pytest.raises(PayloadTypeInvalidMiddlemanProtocolError): AbstractFrame.deserialize(raw_message_with_new_signature, CONCENT_PUBLIC_KEY)
def test_that__handle_connection_should_send_error_frame_if_payload_golem_message_type_is_unexpected( self): # Prepare frame payload which is Golem message other than TransactionSigningRequest. middleman_message = GolemMessageFrame( payload=Ping(), request_id=99, ) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) raw_message_received = self._prepare_and_execute_handle_connection( raw_message) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that( deserialized_message.payload).is_instance_of(tuple) assertpy.assert_that(deserialized_message.payload).is_length(2) assertpy.assert_that(deserialized_message.payload[0]).is_equal_to( ErrorCode.UnexpectedMessage) assertpy.assert_that(deserialized_message.request_id).is_equal_to( REQUEST_ID_FOR_RESPONSE_FOR_INVALID_FRAME)
def test_that__handle_connection_should_send_error_frame_if_frame_signature_is_wrong( self): # Prepare message with wrong signature. middleman_message = GolemMessageFrame( payload=self._get_deserialized_transaction_signing_request(), request_id=99, ) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) first_byte = 2 if raw_message[0] == 0 else raw_message[0] malformed_raw_message = bytes(bytearray([first_byte - 1 ])) + raw_message[1:] raw_message_received = self._prepare_and_execute_handle_connection( malformed_raw_message) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that( deserialized_message.payload).is_instance_of(tuple) assertpy.assert_that(deserialized_message.payload).is_length(2) assertpy.assert_that(deserialized_message.payload[0]).is_equal_to( ErrorCode.InvalidFrameSignature) assertpy.assert_that(deserialized_message.request_id).is_equal_to( REQUEST_ID_FOR_RESPONSE_FOR_INVALID_FRAME)
def test_that__handle_connection_should_send_golem_message_signed_transaction_if_warning_daily_threshold_exceeded( self): middleman_message = GolemMessageFrame( payload=self._get_deserialized_transaction_signing_request(), request_id=99, ) middleman_message.payload.value = WARNING_DAILY_THRESHOLD + 1 raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) def handle_connection_wrapper(signing_service, connection, receive_frame_generator): with mock.patch( 'signing_service.signing_service.SigningService._get_signed_transaction', return_value=self._get_deserialized_signed_transaction(), ): with mock.patch( 'signing_service.signing_service.SigningService._add_payload_value_to_daily_transactions_sum' ): signing_service._handle_connection(receive_frame_generator, connection) raw_message_received = self._prepare_and_execute_handle_connection( raw_message, handle_connection_wrapper, ) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that( deserialized_message.payload).is_instance_of(SignedTransaction)
def test_that_authenticate_should_send_authentication_response_if_authentication_challenge_is_correct( self): middleman_message = AuthenticationChallengeFrame( payload=self.authentication_challenge, request_id=99, ) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) raw_message_received = self._prepare_and_execute_handle_connection( raw_message, ) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that(deserialized_message).is_instance_of( AuthenticationResponseFrame) assertpy.assert_that( deserialized_message.payload).is_instance_of(bytes) assertpy.assert_that( ecdsa_verify( SIGNING_SERVICE_PUBLIC_KEY, deserialized_message.payload, self.authentication_challenge, )).is_true()
def test_that_abstract_middleman_message_instantiation_should_raise_exception( self): with pytest.raises(TypeError): AbstractFrame( # pylint: disable=abstract-class-instantiated Ping(), self.request_id, )
def test_that_serializing_and_deserializing_message_too_short_should_raise_exception( self, middleman_message_type, payload, ): middleman_message = middleman_message_type(payload, self.request_id) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) malformed_raw_message = raw_message[:-1] with pytest.raises(FrameInvalidMiddlemanProtocolError): AbstractFrame.deserialize( malformed_raw_message, CONCENT_PUBLIC_KEY, )
def test_that__handle_connection_should_send_error_frame_if_payload_golem_message_type_cannot_be_deserialized( self): # Prepare frame payload which is Golem message that cannot be deserialized. middleman_message = GolemMessageFrame( payload=self._get_deserialized_transaction_signing_request( nonce= 'not_int_nonce_which_will_fail_on_deserialization_causing_message_error' ), request_id=99, ) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) raw_message_received = self._prepare_and_execute_handle_connection( raw_message) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that( deserialized_message.payload).is_instance_of(tuple) assertpy.assert_that(deserialized_message.payload).is_length(2) assertpy.assert_that(deserialized_message.payload[0]).is_equal_to( ErrorCode.InvalidPayload) assertpy.assert_that(deserialized_message.request_id).is_equal_to( REQUEST_ID_FOR_RESPONSE_FOR_INVALID_FRAME)
async def handle_frame_receive_async(reader: asyncio.StreamReader, public_key: bytes) -> AbstractFrame: raw_data = await reader.readuntil(FRAME_SEPARATOR) index = raw_data.index(FRAME_SEPARATOR) raw_data_without_separator = escape_decode_raw_message(raw_data[:index]) deserialized_data = AbstractFrame.deserialize(raw_data_without_separator, public_key) return deserialized_data
def test_that_serializing_and_deserializing_message_with_wrong_signature_should_raise_exception( self, middleman_message_type, payload, ): middleman_message = middleman_message_type(payload, self.request_id) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) first_byte = 2 if raw_message[0] == 0 else raw_message[0] malformed_raw_message = bytes(bytearray([first_byte - 1 ])) + raw_message[1:] with pytest.raises(SignatureInvalidMiddlemanProtocolError): AbstractFrame.deserialize( malformed_raw_message, CONCENT_PUBLIC_KEY, )
def _authenticate( self, receive_frame_generator: Iterator[Optional[bytes]], tcp_socket: socket.socket ) -> None: """ Handles authentication challenge. """ # Set timeout on socket, after which, if AuthenticationChallengeFrame is not received, # SigningService will have to reconnect. tcp_socket.settimeout(RECEIVE_AUTHENTICATION_CHALLENGE_TIMEOUT) # After establishing a TCP connection start listening for the AuthenticationChallengeFrame. try: raw_message_received = next(receive_frame_generator) authentication_challenge_frame = AbstractFrame.deserialize( raw_message_received, public_key=self.concent_public_key, ) # If SigningService receive any other message that AuthenticationChallengeFrame # disconnect, log the incident and treat it as a failure to connect. if not isinstance(authentication_challenge_frame, AuthenticationChallengeFrame): logger.info( f'SigningService received {type(authentication_challenge_frame)} instead of AuthenticationChallengeFrame.' ) raise socket.error() # If received message is invalid or # if nothing was received in a predefined time, # disconnect, log the incident and treat it as a failure to connect. except (MiddlemanProtocolError, socket.timeout) as exception: logger.info(f'SigningService failed to receive AuthenticationChallengeFrame with exception: {exception}.') raise socket.error() # If you receive a valid challenge, sign it with the private key of the service and # send AuthenticationResponseFrame with signature as payload. authentication_response_frame = AuthenticationResponseFrame( payload=self._get_authentication_challenge_signature( authentication_challenge_frame.payload, ), request_id=authentication_challenge_frame.request_id, ) try: send_over_stream( connection=tcp_socket, raw_message=authentication_response_frame, private_key=self.signing_service_private_key, ) # If the server disconnects, log the incident and treat it as a failure to connect. except socket.error as exception: logger.info( f'MiddleMan server disconnects after receiving AuthenticationResponseFrame with exception: {exception}.' ) raise socket.error() logger.info('Authentication successful. ')
def test_that__handle_connection_should_send_error_frame_if_payload_is_invalid( self): # Prepare frame payload which is not Golem message. middleman_message = GolemMessageFrame( payload=Ping(), request_id=99, ) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) malformed_raw_message = ( raw_message[:FRAME_PAYLOAD_STARTING_BYTE] + AbstractFrame.get_frame_format( ).signed_part_of_the_frame.payload.build(b'\x00' * 100)) # Replace message signature new_signature = ecdsa_sign( CONCENT_PRIVATE_KEY, malformed_raw_message[FRAME_SIGNATURE_BYTES_LENGTH:]) malformed_raw_message_with_new_signature = new_signature + malformed_raw_message[ FRAME_SIGNATURE_BYTES_LENGTH:] raw_message_received = self._prepare_and_execute_handle_connection( malformed_raw_message_with_new_signature) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that( deserialized_message.payload).is_instance_of(tuple) assertpy.assert_that(deserialized_message.payload).is_length(2) assertpy.assert_that(deserialized_message.payload[0]).is_equal_to( ErrorCode.InvalidPayload) assertpy.assert_that(deserialized_message.request_id).is_equal_to( REQUEST_ID_FOR_RESPONSE_FOR_INVALID_FRAME)
def test_that_exceeding_maximum_frame_length_should_treat_exceeded_frame_as_invalid( self, unused_tcp_port): first_middleman_message = GolemMessageFrame(Ping(), self.request_id) first_raw_message = append_frame_separator( escape_encode_raw_message( first_middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY))) second_middleman_message = AuthenticationChallengeFrame( payload=b'', request_id=100, ) second_raw_message = append_frame_separator( escape_encode_raw_message( second_middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY))) assert len(first_raw_message) > len(second_raw_message) + 10 with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as server_socket: server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as client_socket: client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) server_socket.bind(('127.0.0.1', unused_tcp_port)) server_socket.listen(1) client_socket.connect(('127.0.0.1', unused_tcp_port)) (connection, _address) = server_socket.accept() client_socket.send(first_raw_message) client_socket.send(second_raw_message) with mock.patch( 'middleman_protocol.stream.MAXIMUM_FRAME_LENGTH', len(first_raw_message) - 10): raw_message_received = next( unescape_stream(connection=connection)) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=CONCENT_PUBLIC_KEY, ) assertpy.assert_that(deserialized_message.request_id).is_equal_to(100)
def test_that_serializing_and_deserializing_message_should_preserve_original_data( self, middleman_message_type, payload, ): middleman_message = middleman_message_type(payload, self.request_id) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) deserialized_message = AbstractFrame.deserialize( raw_message, public_key=CONCENT_PUBLIC_KEY, ) assertpy.assert_that(deserialized_message).is_instance_of( middleman_message_type) assertpy.assert_that(deserialized_message.payload).is_instance_of( type(payload)) assertpy.assert_that(deserialized_message.payload).is_equal_to(payload)
def test_that_receiving_a_series_of_messages_should_be_handled_correctly( self, unused_tcp_port): payload = Ping() middleman_message = GolemMessageFrame(payload, self.request_id) with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as server_socket: server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as client_socket: client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) server_socket.bind(('127.0.0.1', unused_tcp_port)) server_socket.listen(1) client_socket.connect(('127.0.0.1', unused_tcp_port)) (connection, _address) = server_socket.accept() for _i in range(10): send_over_stream(connection=client_socket, raw_message=middleman_message, private_key=CONCENT_PRIVATE_KEY) unescape_stream_generator = unescape_stream( connection=connection) for _i in range(10): raw_message_received = next(unescape_stream_generator) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=CONCENT_PUBLIC_KEY, ) assertpy.assert_that(deserialized_message).is_instance_of( GolemMessageFrame) assertpy.assert_that( deserialized_message.payload).is_instance_of(Ping) assertpy.assert_that( deserialized_message.payload).is_equal_to(payload)
def send_over_stream(connection: socket.socket, raw_message: AbstractFrame, private_key: bytes) -> int: """ Helper for preparing AbstractFrame for sending through socket stream do actual send by doing: * serializing, * escaping, * adding frame separator, * sending over the socket. """ assert isinstance(raw_message, AbstractFrame) assert isinstance(private_key, bytes) return connection.send( append_frame_separator( escape_encode_raw_message( raw_message.serialize( private_key=private_key ) ) ) )
def test_that__handle_connection_should_send_error_frame_if_payload_type_is_invalid( self): # Prepare frame with malformed payload_type. middleman_message = GolemMessageFrame( payload=self._get_deserialized_transaction_signing_request(), request_id=99, ) raw_message = middleman_message.serialize( private_key=CONCENT_PRIVATE_KEY) payload_type_position = FRAME_SIGNATURE_BYTES_LENGTH + FRAME_REQUEST_ID_BYTES_LENGTH invalid_payload_type = 100 # Replace bytes with payload length. malformed_raw_message = ( raw_message[:payload_type_position] + bytes(bytearray([invalid_payload_type])) + raw_message[payload_type_position + FRAME_PAYLOAD_TYPE_LENGTH:]) # Replace message signature new_signature = ecdsa_sign( CONCENT_PRIVATE_KEY, malformed_raw_message[FRAME_SIGNATURE_BYTES_LENGTH:]) malformed_raw_message_with_new_signature = new_signature + malformed_raw_message[ FRAME_SIGNATURE_BYTES_LENGTH:] raw_message_received = self._prepare_and_execute_handle_connection( malformed_raw_message_with_new_signature) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=SIGNING_SERVICE_PUBLIC_KEY, ) assertpy.assert_that( deserialized_message.payload).is_instance_of(tuple) assertpy.assert_that(deserialized_message.payload).is_length(2) assertpy.assert_that(deserialized_message.payload[0]).is_equal_to( ErrorCode.InvalidFrame) assertpy.assert_that(deserialized_message.request_id).is_equal_to( REQUEST_ID_FOR_RESPONSE_FOR_INVALID_FRAME)
def test_that_sending_message_over_tcp_socket_should_preserve_original_data( self, middleman_message_type, payload, unused_tcp_port, ): middleman_message = middleman_message_type(payload, self.request_id) with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as server_socket: server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as client_socket: client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) server_socket.bind(('127.0.0.1', unused_tcp_port)) server_socket.listen(1) client_socket.connect(('127.0.0.1', unused_tcp_port)) (connection, _address) = server_socket.accept() send_over_stream(connection=client_socket, raw_message=middleman_message, private_key=CONCENT_PRIVATE_KEY) raw_message_received = next( unescape_stream(connection=connection)) deserialized_message = AbstractFrame.deserialize( raw_message=raw_message_received, public_key=CONCENT_PUBLIC_KEY, ) assertpy.assert_that(deserialized_message).is_instance_of( middleman_message_type) assertpy.assert_that(deserialized_message.payload).is_instance_of( type(payload)) assertpy.assert_that(deserialized_message.payload).is_equal_to(payload)
async def send_over_stream_async(frame: AbstractFrame, writer: asyncio.StreamWriter, private_key: bytes) -> None: data = frame.serialize(private_key) writer.write(append_frame_separator(escape_encode_raw_message(data))) await writer.drain()
def _prepare_mocked_reader(frame: AbstractFrame, private_key: bytes = SIGNING_SERVICE_PRIVATE_KEY): serialized = frame.serialize(private_key) data_to_send = append_frame_separator(escape_encode_raw_message(serialized)) return prepare_mocked_reader(data_to_send)
def _handle_connection( self, receive_frame_generator: Iterator[Optional[bytes]], tcp_socket: socket.socket ) -> None: """ Inner loop that handles data exchange over socket. """ # Set socket back blocking mode. tcp_socket.setblocking(True) for raw_message_received in receive_frame_generator: try: middleman_message = AbstractFrame.deserialize( raw_message_received, public_key=self.concent_public_key, ) # Heartbeat is received: connection is still active and Signing Service doesn't have to respond. if middleman_message.payload_type == PayloadType.HEARTBEAT: continue if ( not middleman_message.payload_type == PayloadType.GOLEM_MESSAGE or not isinstance(middleman_message.payload, TransactionSigningRequest) ): raise SigningServiceUnexpectedMessageError # Is the frame correct according to the protocol? If not, error code is InvalidFrame. except ( FrameInvalidMiddlemanProtocolError, PayloadTypeInvalidMiddlemanProtocolError, RequestIdInvalidTypeMiddlemanProtocolError, ) as exception: middleman_message_response = self._prepare_error_response(ErrorCode.InvalidFrame, exception) # Is frame signature correct? If not, error code is InvalidFrameSignature. except SignatureInvalidMiddlemanProtocolError as exception: middleman_message_response = self._prepare_error_response(ErrorCode.InvalidFrameSignature, exception) # Is the content of the message valid? Do types match the schema and all values are within allowed ranges? # If not, error code is InvalidPayload. # Can the payload be decoded as a Golem message? If not, error code is InvalidPayload. # Is payload message signature correct? If not, error code is InvalidPayload. except (MessageError, PayloadInvalidMiddlemanProtocolError) as exception: middleman_message_response = self._prepare_error_response(ErrorCode.InvalidPayload, exception) # Is frame type GOLEM_MESSAGE? If not, error code is UnexpectedMessage. # Is Golem message type TransactionSigningRequest? If not, error code is UnexpectedMessage. except SigningServiceUnexpectedMessageError as exception: middleman_message_response = self._prepare_error_response(ErrorCode.UnexpectedMessage, exception) # If received frame is correct, validate transaction. else: self._update_daily_transactions_limit_file_name() golem_message_response = self._get_signed_transaction(middleman_message.payload) if isinstance(golem_message_response, SignedTransaction): transaction_sum_combined = self.signing_service_daily_transaction_sum_so_far + middleman_message.payload.value if transaction_sum_combined > MAXIMUM_DAILY_THRESHOLD: logger.warning( f'Signing Service is unable to transact more then {MAXIMUM_DAILY_THRESHOLD} GNTB today.' f'Transaction from {middleman_message.payload.from_address} rejected.' ) self.notifier.send( f'Signing Service is unable to transact more then {MAXIMUM_DAILY_THRESHOLD} GNTB today.' ) golem_message_response = TransactionRejected( reason=TransactionRejected.REASON.DailyLimitExceeded, nonce=middleman_message.payload.nonce, ) elif transaction_sum_combined > WARNING_DAILY_THRESHOLD: logger.warning(f'Signing Service has signed transactions worth {transaction_sum_combined} GNTB today.') self.notifier.send( f'Signing Service has signed transactions worth {transaction_sum_combined} GNTB today.' ) self._add_payload_value_to_daily_transactions_sum(transaction_sum_combined) else: self._add_payload_value_to_daily_transactions_sum(transaction_sum_combined) golem_message_response.sign_message(private_key=self.signing_service_private_key) middleman_message_response = GolemMessageFrame( payload=golem_message_response, request_id=middleman_message.request_id, ) logger.info( f'Sending Middleman protocol message with request_id: {middleman_message_response.request_id}.' ) send_over_stream( connection=tcp_socket, raw_message=middleman_message_response, private_key=self.signing_service_private_key, )