From 79d986677d238aaf4dd4f5331c791aa360139d8a Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 7 Nov 2019 17:50:58 -0500 Subject: [PATCH] Client signs appointment before sending it to server --- apps/cli/__init__.py | 2 + apps/cli/pisa_cli.py | 90 ++++++++++++++++++++++++++++------ test/apps/cli/test_pisa_cli.py | 6 +-- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/apps/cli/__init__.py b/apps/cli/__init__.py index f6d347c..58be430 100644 --- a/apps/cli/__init__.py +++ b/apps/cli/__init__.py @@ -13,6 +13,8 @@ APPOINTMENTS_FOLDER_NAME = "appointments" SUPPORTED_HASH_FUNCTIONS = ["SHA256"] SUPPORTED_CIPHERS = ["AES-GCM-128"] +CLI_PUBLIC_KEY = "cli_pk.pem" +CLI_PRIVATE_KEY = "cli_sk.pem" PISA_PUBLIC_KEY = "pisa_pk.pem" # Configure logging diff --git a/apps/cli/pisa_cli.py b/apps/cli/pisa_cli.py index 2d1da6c..12213dd 100644 --- a/apps/cli/pisa_cli.py +++ b/apps/cli/pisa_cli.py @@ -6,21 +6,28 @@ import requests import time from sys import argv from hashlib import sha256 -from binascii import unhexlify +from binascii import hexlify, unhexlify from getopt import getopt, GetoptError from requests import ConnectTimeout, ConnectionError from uuid import uuid4 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.serialization import load_pem_public_key +from cryptography.hazmat.primitives.serialization import load_pem_public_key, load_pem_private_key from cryptography.hazmat.primitives.asymmetric import ec from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm from apps.cli.blob import Blob from apps.cli.help import help_add_appointment, help_get_appointment -from apps.cli import DEFAULT_PISA_API_SERVER, DEFAULT_PISA_API_PORT, PISA_PUBLIC_KEY, APPOINTMENTS_FOLDER_NAME -from apps.cli import logger +from apps.cli import ( + DEFAULT_PISA_API_SERVER, + DEFAULT_PISA_API_PORT, + CLI_PUBLIC_KEY, + CLI_PRIVATE_KEY, + PISA_PUBLIC_KEY, + APPOINTMENTS_FOLDER_NAME, + logger, +) HTTP_OK = 200 @@ -48,21 +55,42 @@ def generate_dummy_appointment(): print("\nData stored in dummy_appointment_data.json") -# Loads and returns Pisa's public key from disk. -# Will raise NotFoundError or IOError if the attempts to open and read the public key file fail. -# Will raise ValueError if it the public key file was present but it failed to be deserialized. -def load_pisa_public_key(): +def sign_appointment(sk, appointment): + data = json.dumps(appointment, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hexlify(sk.sign(data, ec.ECDSA(hashes.SHA256()))).decode("utf-8") + + +# Loads and returns Pisa keys from disk +def load_key_file_data(file_name): try: - with open(PISA_PUBLIC_KEY, "r") as key_file: - pubkey_pem = key_file.read().encode("utf-8") - pisa_public_key = load_pem_public_key(pubkey_pem, backend=default_backend()) - return pisa_public_key + with open(file_name, "r") as key_file: + key_pem = key_file.read().encode("utf-8") + return key_pem + + except FileNotFoundError: + raise FileNotFoundError("File not found.") + + +# Deserialize public key from pem data. +def load_public_key(pk_pem): + try: + pisa_pk = load_pem_public_key(pk_pem, backend=default_backend()) + return pisa_pk except UnsupportedAlgorithm: raise ValueError("Could not deserialize the public key (unsupported algorithm).") -# Verifies that the appointment signature is a valid signature with public key `pk`, +# Deserialize private key from pem data. +def load_private_key(sk_pem): + try: + cli_sk = load_pem_private_key(sk_pem, None, backend=default_backend()) + return cli_sk + + except UnsupportedAlgorithm: + raise ValueError("Could not deserialize the private key (unsupported algorithm).") + + # returning True or False accordingly. def is_appointment_signature_valid(appointment, signature, pk): try: @@ -143,7 +171,38 @@ def add_appointment(args): appointment_data.get("end_time"), appointment_data.get("dispute_delta"), ) - appointment_json = json.dumps(appointment, sort_keys=True, separators=(",", ":")) + + try: + sk_pem = load_key_file_data(CLI_PRIVATE_KEY) + cli_sk = load_private_key(sk_pem) + + except ValueError: + logger.error("Failed to deserialize the public key. It might be in an unsupported format.") + return False + + except FileNotFoundError: + logger.error("Client's private key file not found. Please check your settings.") + return False + + except IOError as e: + logger.error("I/O error({}): {}".format(e.errno, e.strerror)) + return False + + signature = sign_appointment(cli_sk, appointment) + try: + cli_pk_pem = load_key_file_data(CLI_PUBLIC_KEY) + + except FileNotFoundError: + logger.error("Client's private key file not found. Please check your settings.") + return False + + except IOError as e: + logger.error("I/O error({}): {}".format(e.errno, e.strerror)) + return False + + data = {"appointment": appointment, "signature": signature, "public_key": cli_pk_pem.decode("utf-8")} + + appointment_json = json.dumps(data, sort_keys=True, separators=(",", ":")) logger.info("Sending appointment to PISA") @@ -181,7 +240,8 @@ def add_appointment(args): signature = response_json["signature"] # verify that the returned signature is valid try: - pk = load_pisa_public_key() + pk_pem = load_key_file_data(PISA_PUBLIC_KEY) + pk = load_public_key(pk_pem) is_sig_valid = is_appointment_signature_valid(appointment, signature, pk) except ValueError: diff --git a/test/apps/cli/test_pisa_cli.py b/test/apps/cli/test_pisa_cli.py index caf2e91..8390b25 100644 --- a/test/apps/cli/test_pisa_cli.py +++ b/test/apps/cli/test_pisa_cli.py @@ -50,7 +50,7 @@ def test_is_appointment_signature_valid(): assert not pisa_cli.is_appointment_signature_valid(dummy_appointment, other_signature, pisa_pk) -def get_dummy_pisa_pk(): +def get_dummy_pisa_pk(pem_data): return pisa_pk @@ -60,7 +60,7 @@ def test_add_appointment(monkeypatch): # and the return value is True # make sure the test uses the right dummy key instead of loading it from disk - monkeypatch.setattr(pisa_cli, "load_pisa_public_key", get_dummy_pisa_pk) + monkeypatch.setattr(pisa_cli, "load_public_key", get_dummy_pisa_pk) response = {"locator": dummy_appointment["locator"], "signature": sign_appointment(pisa_sk, dummy_appointment)} @@ -81,7 +81,7 @@ def test_add_appointment_with_invalid_signature(monkeypatch): # make sure that the right endpoint is requested, but the return value is False # make sure the test uses the right dummy key instead of loading it from disk - monkeypatch.setattr(pisa_cli, "load_pisa_public_key", get_dummy_pisa_pk) + monkeypatch.setattr(pisa_cli, "load_public_key", get_dummy_pisa_pk) response = { "locator": dummy_appointment["locator"],