From 6d3da139e9018cbc3787dc0bf8f2c8a5fe31a38e Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 19 Nov 2020 23:58:14 -0500 Subject: [PATCH 1/7] Generate self-signed certificate --- common/cryptographer.py | 105 +++++++++++++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/common/cryptographer.py b/common/cryptographer.py index dac64edd..b43d48ca 100644 --- a/common/cryptographer.py +++ b/common/cryptographer.py @@ -1,3 +1,4 @@ +import datetime import os.path import pyzbase32 from pathlib import Path @@ -6,6 +7,12 @@ from coincurve import PrivateKey, PublicKey from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography import x509 +from cryptography.x509.oid import NameOID from common.tools import is_256b_hex_str from common.exceptions import InvalidKey, InvalidParameter, SignatureError, EncryptionError @@ -192,24 +199,24 @@ def generate_key(): return PrivateKey() @staticmethod - def save_key_file(key, name, data_dir): + def save_crypto_file(crypto_data, name, data_dir): """ - Saves a key to disk in DER format. + Saves cryptographic data, like a key or certificate, to disk in format. Args: - key (:obj:`bytes`): the key to be saved to disk. + crypto_data (:obj:`bytes`): the key to be saved to disk. name (:obj:`str`): the name of the key file to be generated. data_dir (:obj:`str`): the data directory where the file will be saved. Raises: - :obj:`InvalidParameter`: If the given key is not bytes or the name or data_dir are not strings. + :obj:`InvalidParameter`: If the given crypto data is not bytes or the name or data_dir are not strings. """ - if not isinstance(key, bytes): - raise InvalidParameter("Key must be bytes, {} received".format(type(key))) + if not isinstance(crypto_data, bytes): + raise InvalidParameter("Crypto data must be bytes, {} received".format(type(crypto_data))) if not isinstance(name, str): - raise InvalidParameter("Key name must be str, {} received".format(type(name))) + raise InvalidParameter("Crypto data name must be str, {} received".format(type(name))) if not isinstance(data_dir, str): raise InvalidParameter("Data dir must be str, {} received".format(type(data_dir))) @@ -217,19 +224,19 @@ def save_key_file(key, name, data_dir): # Create the output folder it it does not exist (and all the parents if they don't either) Path(data_dir).mkdir(parents=True, exist_ok=True) - with open(os.path.join(data_dir, "{}.der".format(name)), "wb") as der_out: - der_out.write(key) + with open(os.path.join(data_dir, "{}".format(name)), "wb") as crypto_out: + crypto_out.write(crypto_data) @staticmethod def load_key_file(file_path): """ - Loads a key from a key file. + Loads a key or certificate from a disk file. Args: - file_path (:obj:`str`): the path to the key file to be loaded. + file_path (:obj:`str`): the path to the key or certificate file to be loaded. Returns: - :obj:`bytes`: The key file data if the file can be found and read. + :obj:`bytes`: The key or certificate file data if the file can be found and read. Raises: :obj:`InvalidParameter`: if the file_path has wrong format or cannot be found. @@ -375,3 +382,77 @@ def get_compressed_pk(pk): except TypeError as e: raise InvalidKey("PublicKey has invalid initializer", error=str(e)) + + + @staticmethod + def generate_cert_key(): + """ + Generates an RSA key with which to self-sign our TSL certificate and converts it to PEM format. + + Returns: + :obj:`bytes`: An RSA key in PEM format. + """ + sk = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + sk_pem = sk.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + return sk_pem + + + @staticmethod + def generate_self_signed_cert(cert_key_path): + """ + Generates a self-signed TLS certificate for securing the connection between the CLI and the server. + + Args: + cert_key_path(:obj:`str`): Path to RSA key. + + Returns: + :obj:`Certificate`: A x509 certificate. + + Raises: + :obj:`InvalidKey`: if the RSA key file is invalid or could not be found. + + """ + try: + sk_pem = Cryptographer.load_key_file(cert_key_path) + + except (InvalidParameter, InvalidKey): + raise InvalidKey("Failed to load RSA key needed for TLS certificate") + + + sk = load_pem_private_key(sk_pem, None) + + # get public key from private key + pk = sk.public_key() + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Teos watchtower"), + x509.NameAttribute(NameOID.COMMON_NAME, u"localhost"), + ]) + + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + pk + ).serial_number( + x509.random_serial_number() + ).not_valid_before(datetime.datetime.utcnow()).not_valid_after( + # Our certificate will be valid for 365 days + datetime.datetime.utcnow() + datetime.timedelta(days=365) + ).add_extension( + x509.SubjectAlternativeName([x509.DNSName(u"localhost")]), + critical=False, + ).sign(sk, hashes.SHA256()) + + cert = cert.public_bytes(serialization.Encoding.PEM) + + return cert From a7130bd42504ce2cb10ff5a294c9de46495e4ed0 Mon Sep 17 00:00:00 2001 From: Turtle Date: Fri, 27 Nov 2020 16:36:16 -0500 Subject: [PATCH 2/7] Add authentication to cli --- teos/cli/rpc_client.py | 33 +++++++++++++++++++++++++++++++-- teos/cli/teos_cli.py | 6 ++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/teos/cli/rpc_client.py b/teos/cli/rpc_client.py index 88e86c1f..2958133f 100644 --- a/teos/cli/rpc_client.py +++ b/teos/cli/rpc_client.py @@ -5,6 +5,7 @@ from google.protobuf import json_format from google.protobuf.empty_pb2 import Empty +from common.cryptographer import Cryptographer from common.tools import is_compressed_pk, intify from common.exceptions import InvalidParameter @@ -46,15 +47,26 @@ class RPCClient: Args: rpc_host (:obj:`str`): the IP or host where the RPC server is hosted. rpc_port (:obj:`int`): the port where the RPC server is hosted. + rpc_cert_path (:obj:`str`): path to certificate used to validate the server's TSL credentials. + rpc_user (:obj:`str`): a username that will be authenticated by the grpc server. + rpc_pass (:obj:`str`): a password that will be authenticated by the grpc server. + Attributes: stub: The rpc client stub. """ - def __init__(self, rpc_host, rpc_port): + def __init__(self, rpc_host, rpc_port, rpc_cert_path, rpc_user, rpc_pass): self.rpc_host = rpc_host self.rpc_port = rpc_port - channel = grpc.insecure_channel(f"{rpc_host}:{rpc_port}") + + cert = Cryptographer.load_key_file(rpc_cert_path) + user_creds = UserPassCallCredentials(rpc_user, rpc_pass) + call_creds = grpc.metadata_call_credentials(user_creds) + ssl_creds = grpc.ssl_channel_credentials(root_certificates=cert) + creds = grpc.composite_channel_credentials(ssl_creds, call_creds) + + channel = grpc.secure_channel(f"{rpc_host}:{rpc_port}", creds) self.stub = TowerServicesStub(channel) @formatted @@ -95,3 +107,20 @@ def stop(self): """Stops TEOS gracefully.""" self.stub.stop(Empty()) print("Closing the Eye of Satoshi") + + +class UserPassCallCredentials(grpc.AuthMetadataPlugin): + """ + Creates call credentials, which include a username and password, to be passed to grpc. + + Args: + username (:obj:`str`): a username that will be authenticated by the grpc server. + password (:obj:`str`): a password that will be authenticated by the grpc server. + """ + def __init__(self, username, password): + self._username = username + self._password = password + + def __call__(self, context, callback): + metadata = [('user', self._username), ('pass', self._password)] + callback(metadata, None) diff --git a/teos/cli/teos_cli.py b/teos/cli/teos_cli.py index 45084525..de453605 100755 --- a/teos/cli/teos_cli.py +++ b/teos/cli/teos_cli.py @@ -119,8 +119,10 @@ def __init__(self, data_dir, command_line_conf): teos_rpc_host = config.get("RPC_BIND") teos_rpc_port = config.get("RPC_PORT") - - self.rpc_client = RPCClient(teos_rpc_host, teos_rpc_port) + rpc_cert = config.get("RPC_CERT") + rpc_user = config.get("RPC_USER") + rpc_pass = config.get("RPC_PASS") + self.rpc_client = RPCClient(teos_rpc_host, teos_rpc_port, rpc_cert, rpc_user, rpc_pass) @classmethod def command(cls, command_cls): From d260c05935d78ee533967dd9a7302e2d53fb76e4 Mon Sep 17 00:00:00 2001 From: Turtle Date: Sun, 29 Nov 2020 01:55:31 -0500 Subject: [PATCH 3/7] Update cli tests with secure channel --- test/teos/unit/cli/test_rpc_client.py | 12 +++++++-- test/teos/unit/cli/test_teos_cli.py | 38 ++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/test/teos/unit/cli/test_rpc_client.py b/test/teos/unit/cli/test_rpc_client.py index 48da73d6..d431a2bc 100644 --- a/test/teos/unit/cli/test_rpc_client.py +++ b/test/teos/unit/cli/test_rpc_client.py @@ -3,13 +3,21 @@ from teos.cli.rpc_client import RPCClient from common.exceptions import InvalidParameter +from test.teos.unit.cli.test_teos_cli import monkeypatch_rpcclient + + test_host = "test" test_port = 4242 +test_cert_path = "" +test_user = "user" +test_pass = "pass" @pytest.fixture -def rpc_client(): - return RPCClient(test_host, test_port) +def rpc_client(monkeypatch): + monkeypatch, rpc_client = monkeypatch_rpcclient(monkeypatch, RPCClient, [test_host, test_port, test_cert_path, test_user, test_pass]) + + return rpc_client def test_get_user_invalid_user_id(rpc_client): diff --git a/test/teos/unit/cli/test_teos_cli.py b/test/teos/unit/cli/test_teos_cli.py index a07bbec4..577b5dc8 100644 --- a/test/teos/unit/cli/test_teos_cli.py +++ b/test/teos/unit/cli/test_teos_cli.py @@ -1,9 +1,14 @@ import os import pytest +import time import grpc +from grpc_testing import channel, strict_fake_time from unittest.mock import MagicMock +from common.cryptographer import Cryptographer from teos.cli.teos_cli import show_usage, CLI, CLICommand +from teos.cli.rpc_client import UserPassCallCredentials +from teos.protobuf import tower_services_pb2 from common.exceptions import InvalidParameter @@ -66,9 +71,34 @@ def run(rpc_client, opts_args): raise Exception("Mock Exception") +def return_none(*args, **kwargs): + return None + + +def create_test_channel(*args): + descriptors = tower_services_pb2.DESCRIPTOR.services_by_name.values() + fake_time = strict_fake_time(time.time()) + test_channel = channel(descriptors, fake_time) + + return test_channel + + +def monkeypatch_rpcclient(monkeypatch, function, func_args): + for attr in [(Cryptographer, "load_key_file"), (UserPassCallCredentials, "__init__"), (grpc, "metadata_call_credentials"), (grpc, "ssl_channel_credentials"), (grpc, "composite_channel_credentials")]: + monkeypatch.setattr(attr[0], attr[1], return_none, raising=True) + + monkeypatch.setattr(grpc, "secure_channel", create_test_channel, raising=True) + + result = function(*func_args) + + return monkeypatch, result + + @pytest.fixture -def cli(): - cli = CLI(".teos-cli-test", {}) +def cli(monkeypatch): + monkeypatch, cli = monkeypatch_rpcclient(monkeypatch, CLI, [".teos-cli-test", {}]) + + # cli = CLI(".teos-cli-test", {}) yield cli os.rmdir(".teos-cli-test") @@ -78,9 +108,9 @@ def test_show_usage_does_not_throw(): show_usage() -def test_cli_init_does_not_throw(): +def test_cli_init_does_not_throw(monkeypatch): try: - CLI(".teos-cli-test", {}) + monkeypatch, cli = monkeypatch_rpcclient(monkeypatch, CLI, [".teos-cli-test", {}]) finally: os.rmdir(".teos-cli-test") From 127c4abe06f59c66ebe85b4598c411d40de51d05 Mon Sep 17 00:00:00 2001 From: Turtle Date: Sun, 6 Dec 2020 19:00:53 -0500 Subject: [PATCH 4/7] Test generate certificate --- test/common/unit/test_cryptographer.py | 54 +++++++++++++++++++++----- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/test/common/unit/test_cryptographer.py b/test/common/unit/test_cryptographer.py index e48bf5e9..609adf34 100644 --- a/test/common/unit/test_cryptographer.py +++ b/test/common/unit/test_cryptographer.py @@ -2,6 +2,8 @@ import pytest from shutil import rmtree from coincurve import PrivateKey, PublicKey +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import load_pem_private_key from common.exceptions import InvalidKey, InvalidParameter, EncryptionError, SignatureError from common.cryptographer import Cryptographer @@ -112,35 +114,35 @@ def test_generate_key(): issued_keys.append(sk_der) -def test_save_key_file(): +def test_save_crypto_file(): # If the params are of the right type, the key is saved no matter if the dir exists or not. - key_name = "test_key" + key_name = "test_key.der" key_dir = "test_key_dir" assert not os.path.exists(key_dir) assert not os.path.exists(os.path.join(key_dir, key_name)) - Cryptographer.save_key_file(bytes(33), key_name, key_dir) + Cryptographer.save_crypto_file(bytes(33), key_name, key_dir) assert os.path.exists(key_dir) - assert os.path.exists(os.path.join(key_dir, key_name + ".der")) + assert os.path.exists(os.path.join(key_dir, key_name)) rmtree(key_dir) -def test_save_key_file_wrong_params(): +def test_save_crypto_file_wrong_params(): # Function fails if the args are not of the proper type no_str_nor_byte = [None, 1, 1.5, object, {}, Exception()] for wrong_val in no_str_nor_byte: - with pytest.raises(InvalidParameter, match="Key must be bytes"): - Cryptographer.save_key_file(wrong_val, "name", "dir") + with pytest.raises(InvalidParameter, match="Crypto data must be bytes"): + Cryptographer.save_crypto_file(wrong_val, "name", "dir") for wrong_val in no_str_nor_byte: - with pytest.raises(InvalidParameter, match="Key name must be str"): - Cryptographer.save_key_file(bytes(33), wrong_val, "dir") + with pytest.raises(InvalidParameter, match="Crypto data name must be str"): + Cryptographer.save_crypto_file(bytes(33), wrong_val, "dir") for wrong_val in no_str_nor_byte: with pytest.raises(InvalidParameter, match="Data dir must be str"): - Cryptographer.save_key_file(bytes(33), "name", wrong_val) + Cryptographer.save_crypto_file(bytes(33), "name", wrong_val) def test_load_key_file(): @@ -285,3 +287,35 @@ def test_get_compressed_pk_wrong_type(): with pytest.raises(InvalidParameter, match="Wrong value passed as pk"): Cryptographer.get_compressed_pk(pk) + + +def test_generate_cert_key(): + pem_cert_key = Cryptographer.generate_cert_key() + assert isinstance(pem_cert_key, bytes) + + # Check that the pem key is correct + cert_key = load_pem_private_key(pem_cert_key, None) + assert isinstance(cert_key, rsa.RSAPrivateKey) + + +def test_generate_self_signed_cert(): + test_dir = "test_dir" + test_key = "cert.key" + # Create test key + pem_cert_key = Cryptographer.generate_cert_key() + # save test key. + Cryptographer.save_crypto_file(pem_cert_key, test_key, test_dir) + + cert = Cryptographer.generate_self_signed_cert(os.path.join(test_dir, test_key)) + + assert isinstance(cert, bytes) + + rmtree(test_dir) + + +def test_generate_self_signed_cert_wrong_file(): + cert_key_path = "does_not_exist" + + with pytest.raises(InvalidKey, match="Failed to load RSA key needed for TLS certificate"): + Cryptographer.generate_self_signed_cert(cert_key_path) + From 1bcebb12f9ac2e2852408dbfac059bd7026ccf19 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 14 Dec 2020 20:10:15 -0500 Subject: [PATCH 5/7] Add authentication check to RPC server --- teos/rpc.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/teos/rpc.py b/teos/rpc.py index ad65b2e9..0fd20410 100644 --- a/teos/rpc.py +++ b/teos/rpc.py @@ -13,6 +13,38 @@ ) +class AuthInterceptor(grpc.ServerInterceptor): + """ + The :obj:`AuthInterceptor` looks at every call made to the RPC server to see if the correct CLI credentials are provided. + + Args: + rpc_user (:obj:`str`): the username supplied by the CLI when calling the RPC server. + rpc_pass (:obj:`str`): the password supplied by the CLI when calling the RPC server. + + Attributes: + rpc_user (:obj:`str`): the username supplied by the CLI when calling the RPC server. + rpc_pass (:obj:`str`): the password supplied by the CLI when calling the RPC server. + """ + def __init__(self, rpc_user, rpc_pass): + self.rpc_user = rpc_user + self.rpc_pass = rpc_pass + + def abort(ignored_request, context): + context.abort(grpc.StatusCode.UNAUTHENTICATED, 'Invalid user credentials') + + self._abortion = grpc.unary_unary_rpc_method_handler(abort) + + def intercept_service(self, continuation, handler_call_details): + metadata = dict(handler_call_details.invocation_metadata) + user = metadata.get("user") + password = metadata.get("pass") + + if (user == self.rpc_user) and (password == self.rpc_pass): + return continuation(handler_call_details) + else: + return self._abortion + + class RPC: """ The :obj:`RPC` is an external RPC server offered by tower to receive requests from the CLI. @@ -29,12 +61,16 @@ class RPC: rpc_server (:obj:`grpc.Server `): The non-started gRPC server instance. """ - def __init__(self, rpc_bind, rpc_port, internal_api_endpoint): + def __init__(self, rpc_bind, rpc_port, internal_api_endpoint, teos_sk, certificate, rpc_user, rpc_pass): self.logger = get_logger(component=RPC.__name__) self.endpoint = f"{rpc_bind}:{rpc_port}" - self.rpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - self.rpc_server.add_insecure_port(self.endpoint) - add_TowerServicesServicer_to_server(_RPC(internal_api_endpoint, self.logger), self.rpc_server) + + an_interceptor = AuthInterceptor(rpc_user, rpc_pass) + self.rpc_server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), interceptors=[an_interceptor]) + server_credentials = grpc.ssl_server_credentials(((teos_sk, certificate,),)) + + self.rpc_server.add_secure_port(self.endpoint, server_credentials) + add_TowerServicesServicer_to_server(_RPC(internal_api_endpoint, self.logger, rpc_user, rpc_pass), self.rpc_server) def teardown(self): self.logger.info("Stopping") @@ -72,9 +108,12 @@ class _RPC(TowerServicesServicer): stub (:obj:`TowerServicesStub`): The rpc client stub. """ - def __init__(self, internal_api_endpoint, logger): + def __init__(self, internal_api_endpoint, logger, rpc_user, rpc_pass): self.logger = logger self.internal_api_endpoint = internal_api_endpoint + self.rpc_user = rpc_user + self.rpc_pass = rpc_pass + channel = grpc.insecure_channel(self.internal_api_endpoint) self.stub = TowerServicesStub(channel) @@ -99,7 +138,7 @@ def stop(self, request, context): return self.stub.stop(request) -def serve(rpc_bind, rpc_port, internal_api_endpoint, logging_port, stop_event): +def serve(rpc_bind, rpc_port, internal_api_endpoint, logging_port, stop_event, teos_key, rpc_cert, rpc_user, rpc_pass): """ Serves the external RPC API at the given endpoint and connects it to the internal api. @@ -115,7 +154,7 @@ def serve(rpc_bind, rpc_port, internal_api_endpoint, logging_port, stop_event): """ setup_logging(logging_port) - rpc = RPC(rpc_bind, rpc_port, internal_api_endpoint) + rpc = RPC(rpc_bind, rpc_port, internal_api_endpoint, teos_key, rpc_cert, rpc_user, rpc_pass) # Ignores SIGINT so the main process can handle the teardown signal(SIGINT, ignore_signal) rpc.rpc_server.start() From 8e5fbfbfe3d46a62c926f61465190e5b161d5b54 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 14 Dec 2020 20:11:16 -0500 Subject: [PATCH 6/7] Add key and certificate creation to watchtower --- contrib/client/teos_client.py | 6 +++--- teos/__init__.py | 4 ++++ teos/teosd.py | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/contrib/client/teos_client.py b/contrib/client/teos_client.py index 446cbdfb..e0b8919e 100755 --- a/contrib/client/teos_client.py +++ b/contrib/client/teos_client.py @@ -457,7 +457,7 @@ def main(command, args, command_line_conf): else: logger.info("Client id not found. Generating new keys") user_sk = Cryptographer.generate_key() - Cryptographer.save_key_file(user_sk.to_der(), "user_sk", config.get("DATA_DIR")) + Cryptographer.save_crypto_file(user_sk.to_der(), "user_sk.der", config.get("DATA_DIR")) user_id = Cryptographer.get_compressed_pk(user_sk.public_key) if command == "register": @@ -472,8 +472,8 @@ def main(command, args, command_line_conf): logger.info("Registration succeeded. Available slots: {}".format(available_slots)) logger.info("Subscription expires at block {}".format(subscription_expiry)) - teos_id_file = os.path.join(config.get("DATA_DIR"), "teos_pk") - Cryptographer.save_key_file(bytes.fromhex(teos_id), teos_id_file, config.get("DATA_DIR")) + teos_id_file = os.path.join(config.get("DATA_DIR"), "teos_pk.der") + Cryptographer.save_crypto_file(bytes.fromhex(teos_id), teos_id_file, config.get("DATA_DIR")) if command == "add_appointment": teos_id = load_teos_id(config.get("TEOS_PUBLIC_KEY")) diff --git a/teos/__init__.py b/teos/__init__.py index 025d7426..6172a589 100644 --- a/teos/__init__.py +++ b/teos/__init__.py @@ -11,6 +11,10 @@ "API_PORT": {"value": 9814, "type": int}, "RPC_BIND": {"value": "localhost", "type": str}, "RPC_PORT": {"value": 8814, "type": int}, + "RPC_CERT": {"value": "teos_server.crt", "type": str, "path": True}, + "RPC_CERT_KEY": {"value": "teos_cert.key", "type": str, "path": True}, + "RPC_USER": {"value": "user", "type": str}, + "RPC_PASS": {"value": "pass", "type": str}, "BTC_RPC_USER": {"value": "user", "type": str}, "BTC_RPC_PASSWORD": {"value": "passwd", "type": str}, "BTC_RPC_CONNECT": {"value": "127.0.0.1", "type": str}, diff --git a/teos/teosd.py b/teos/teosd.py index ee49d933..380afb77 100755 --- a/teos/teosd.py +++ b/teos/teosd.py @@ -142,6 +142,17 @@ def __init__(self, config, sk, logger, logging_port): bitcoind_feed_params, ) + if not os.path.exists(self.config.get("RPC_CERT_KEY")): + sk_pem = Cryptographer.generate_cert_key() + Cryptographer.save_crypto_file(sk_pem, "teos_cert.key", self.config.get("DATA_DIR")) + + if not os.path.exists(self.config.get("RPC_CERT")): + cert = Cryptographer.generate_self_signed_cert(self.config.get("RPC_CERT_KEY")) + Cryptographer.save_crypto_file(cert, "teos_server.crt", self.config.get("DATA_DIR")) + + sk_pem = Cryptographer.load_key_file(self.config.get("RPC_CERT_KEY")) + certificate = Cryptographer.load_key_file(self.config.get("RPC_CERT")) + # Set up the internal API self.internal_api_endpoint = f'{self.config.get("INTERNAL_API_HOST")}:{self.config.get("INTERNAL_API_PORT")}' self.internal_api = InternalAPI( @@ -157,6 +168,10 @@ def __init__(self, config, sk, logger, logging_port): self.internal_api_endpoint, self.logging_port, self.stop_event, + sk_pem, + certificate, + self.config.get("RPC_USER"), + self.config.get("RPC_PASS"), ), daemon=True, ) @@ -366,7 +381,7 @@ def main(config): if not os.path.exists(config.get("TEOS_SECRET_KEY")) or config.get("OVERWRITE_KEY"): logger.info("Generating a new key pair") sk = Cryptographer.generate_key() - Cryptographer.save_key_file(sk.to_der(), "teos_sk", config.get("DATA_DIR")) + Cryptographer.save_crypto_file(sk.to_der(), "teos_sk.der", config.get("DATA_DIR")) else: logger.info("Tower identity found. Loading keys") From 960621140e1f74891427b923aa6a4d2ccd61e9ca Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 14 Dec 2020 20:12:57 -0500 Subject: [PATCH 7/7] Ensure that CLI user/password is checked in e2e tests --- test/teos/e2e/conftest.py | 4 ++-- test/teos/e2e/test_cli_e2e.py | 13 ++++++++++++- test/teos/e2e/test_client_e2e.py | 8 ++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/test/teos/e2e/conftest.py b/test/teos/e2e/conftest.py index 3672b2ce..83fb3cfe 100644 --- a/test/teos/e2e/conftest.py +++ b/test/teos/e2e/conftest.py @@ -25,7 +25,7 @@ def teosd(run_bitcoind): stopped = False while not stopped: try: - rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) + rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), config.get("RPC_USER"), config.get("RPC_PASS")) rpc_client.stop() stopped = True except RpcError: @@ -44,7 +44,7 @@ def run_teosd(): if not os.path.exists(sk_file_path): # Generating teos sk so we can return the teos_id teos_sk = Cryptographer.generate_key() - Cryptographer.save_key_file(teos_sk.to_der(), "teos_sk", config.get("DATA_DIR")) + Cryptographer.save_crypto_file(teos_sk.to_der(), "teos_sk.der", config.get("DATA_DIR")) else: teos_sk = Cryptographer.load_private_key_der(Cryptographer.load_key_file(sk_file_path)) diff --git a/test/teos/e2e/test_cli_e2e.py b/test/teos/e2e/test_cli_e2e.py index 76b858ac..2f240739 100644 --- a/test/teos/e2e/test_cli_e2e.py +++ b/test/teos/e2e/test_cli_e2e.py @@ -1,5 +1,6 @@ import pytest import json +import grpc from time import sleep from contrib.client import teos_client @@ -25,7 +26,12 @@ @pytest.fixture def rpc_client(): - return RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) + return RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), config.get("RPC_USER"), config.get("RPC_PASS")) + + +@pytest.fixture +def rpc_client_wrong(): + return RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), "wronguser", "wrongpass") def get_appointment_info(teos_id, locator, sk=user_sk): @@ -81,6 +87,11 @@ def test_get_all_appointments(teosd, rpc_client): assert len(watching) == 0 and len(responding) == 0 +def test_get_all_appointments_wrong_password(teosd, rpc_client_wrong): + with pytest.raises(grpc.RpcError): + result = rpc_client_wrong.get_all_appointments() + + def test_get_tower_info(teosd, rpc_client): tower_info = json.loads(rpc_client.get_tower_info()) assert set(tower_info.keys()) == set( diff --git a/test/teos/e2e/test_client_e2e.py b/test/teos/e2e/test_client_e2e.py index 36b6346c..0bb8e6e3 100644 --- a/test/teos/e2e/test_client_e2e.py +++ b/test/teos/e2e/test_client_e2e.py @@ -129,7 +129,7 @@ def test_appointment_life_cycle(teosd): assert appointment_info.get("locator") == locator assert appointment_info.get("appointment") == appointment.to_dict() - rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) + rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), config.get("RPC_USER"), config.get("RPC_PASS")) # Check also the get_all_appointments endpoint all_appointments = json.loads(rpc_client.get_all_appointments()) @@ -219,7 +219,7 @@ def test_multiple_appointments_life_cycle(teosd): sleep(1) # Test that they all show up in get_all_appointments at the correct stages. - rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) + rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), config.get("RPC_USER"), config.get("RPC_PASS")) all_appointments = json.loads(rpc_client.get_all_appointments()) watching = all_appointments.get("watcher_appointments") responding = all_appointments.get("responder_trackers") @@ -505,7 +505,7 @@ def test_appointment_shutdown_teos_trigger_back_online(teosd): add_appointment(teos_id, appointment) # Restart teos - rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) + rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), config.get("RPC_USER"), config.get("RPC_PASS")) rpc_client.stop() teosd_process.join() @@ -546,7 +546,7 @@ def test_appointment_shutdown_teos_trigger_while_offline(teosd): assert appointment_info.get("appointment") == appointment.to_dict() # Shutdown and trigger - rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT")) + rpc_client = RPCClient(config.get("RPC_BIND"), config.get("RPC_PORT"), config.get("RPC_CERT"), config.get("RPC_USER"), config.get("RPC_PASS")) rpc_client.stop() teosd_process.join()