Merge pull request #60 from orbitalturtle/client-appointment-sign

Client appointment sign
This commit is contained in:
orbitalturtle
2019-11-28 09:07:20 -05:00
committed by GitHub
9 changed files with 231 additions and 57 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -32,7 +32,9 @@ def add_appointment():
# Check content type once if properly defined
request_data = json.loads(request.get_json())
inspector = Inspector()
appointment = inspector.inspect(request_data)
appointment = inspector.inspect(
request_data.get("appointment"), request_data.get("signature"), request_data.get("public_key")
)
error = None
response = None

View File

@@ -8,6 +8,7 @@ APPOINTMENT_FIELD_TOO_BIG = -6
APPOINTMENT_WRONG_FIELD = -7
APPOINTMENT_CIPHER_NOT_SUPPORTED = -8
APPOINTMENT_HASH_FUNCTION_NOT_SUPPORTED = -9
APPOINTMENT_INVALID_SIGNATURE = -10
# Custom RPC errors
RPC_TX_REORGED_AFTER_BROADCAST = -98

View File

@@ -1,4 +1,12 @@
import json
import re
from binascii import unhexlify
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.exceptions import InvalidSignature
from pisa import errors
import pisa.conf as conf
@@ -15,14 +23,14 @@ logger = Logger("Inspector")
class Inspector:
def inspect(self, data):
locator = data.get("locator")
start_time = data.get("start_time")
end_time = data.get("end_time")
dispute_delta = data.get("dispute_delta")
encrypted_blob = data.get("encrypted_blob")
cipher = data.get("cipher")
hash_function = data.get("hash_function")
def inspect(self, appt, signature, public_key):
locator = appt.get("locator")
start_time = appt.get("start_time")
end_time = appt.get("end_time")
dispute_delta = appt.get("dispute_delta")
encrypted_blob = appt.get("encrypted_blob")
cipher = appt.get("cipher")
hash_function = appt.get("hash_function")
block_height = BlockProcessor.get_block_count()
@@ -41,6 +49,8 @@ class Inspector:
rcode, message = self.check_cipher(cipher)
if rcode == 0:
rcode, message = self.check_hash_function(hash_function)
if rcode == 0:
rcode, message = self.check_appointment_signature(appt, signature, public_key)
if rcode == 0:
r = Appointment(locator, start_time, end_time, dispute_delta, encrypted_blob, cipher, hash_function)
@@ -245,3 +255,24 @@ class Inspector:
logger.error(message)
return rcode, message
@staticmethod
# Verifies that the appointment signature is a valid signature with public key
def check_appointment_signature(appointment, signature, pk_pem):
message = None
rcode = 0
if signature is None:
rcode = errors.APPOINTMENT_EMPTY_FIELD
message = "empty signature received"
try:
sig_bytes = unhexlify(signature.encode("utf-8"))
client_pk = load_pem_public_key(pk_pem.encode("utf-8"), backend=default_backend())
data = json.dumps(appointment, sort_keys=True, separators=(",", ":")).encode("utf-8")
client_pk.verify(sig_bytes, data, ec.ECDSA(hashes.SHA256()))
except InvalidSignature:
rcode = errors.APPOINTMENT_INVALID_SIGNATURE
return rcode, message

View File

@@ -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"],

View File

@@ -1,3 +1,4 @@
import json
import pytest
import random
import requests
@@ -5,7 +6,12 @@ from time import sleep
from shutil import rmtree
from threading import Thread
from hashlib import sha256
from binascii import unhexlify
from binascii import hexlify, unhexlify
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
from pisa.conf import DB_PATH
from apps.cli.blob import Blob
@@ -48,6 +54,18 @@ def prng_seed():
random.seed(0)
@pytest.fixture(scope="module")
def generate_keypair():
client_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
client_pk = (
client_sk.public_key()
.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)
.decode("utf-8")
)
return client_sk, client_pk
@pytest.fixture(scope="module")
def db_manager():
manager = DBManager("test_db")
@@ -73,6 +91,11 @@ def generate_blocks(n):
generate_block()
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")
def generate_dummy_appointment_data(start_time_offset=5, end_time_offset=30):
current_height = bitcoin_cli().getblockcount()
@@ -91,6 +114,14 @@ def generate_dummy_appointment_data(start_time_offset=5, end_time_offset=30):
cipher = "AES-GCM-128"
hash_function = "SHA256"
# dummy keys for this test
client_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
client_pk = (
client_sk.public_key()
.public_bytes(encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo)
.decode("utf-8")
)
locator = sha256(unhexlify(dispute_txid)).hexdigest()
blob = Blob(dummy_appointment_data.get("tx"), cipher, hash_function)
@@ -107,7 +138,11 @@ def generate_dummy_appointment_data(start_time_offset=5, end_time_offset=30):
"triggered": False,
}
return appointment_data, dispute_tx
signature = sign_appointment(client_sk, appointment_data)
data = {"appointment": appointment_data, "signature": signature, "public_key": client_pk}
return data, dispute_tx
def generate_dummy_appointment(start_time_offset=5, end_time_offset=30):
@@ -115,7 +150,7 @@ def generate_dummy_appointment(start_time_offset=5, end_time_offset=30):
start_time_offset=start_time_offset, end_time_offset=end_time_offset
)
return Appointment.from_dict(appointment_data), dispute_tx
return Appointment.from_dict(appointment_data["appointment"]), dispute_tx
def generate_dummy_job():

View File

@@ -17,40 +17,40 @@ locator_dispute_tx_map = {}
@pytest.fixture
def new_appointment():
appointment, dispute_tx = generate_dummy_appointment_data()
locator_dispute_tx_map[appointment["locator"]] = dispute_tx
def new_appt_data():
appt_data, dispute_tx = generate_dummy_appointment_data()
locator_dispute_tx_map[appt_data["appointment"]["locator"]] = dispute_tx
return appointment
return appt_data
def add_appointment(appointment):
r = requests.post(url=PISA_API, json=json.dumps(appointment), timeout=5)
def add_appointment(new_appt_data):
r = requests.post(url=PISA_API, json=json.dumps(new_appt_data), timeout=5)
if r.status_code == 200:
appointments.append(appointment)
appointments.append(new_appt_data["appointment"])
return r
def test_add_appointment(run_api, run_bitcoind, new_appointment):
def test_add_appointment(run_api, run_bitcoind, new_appt_data):
# Properly formatted appointment
r = add_appointment(new_appointment)
r = add_appointment(new_appt_data)
assert r.status_code == 200
# Incorrect appointment
new_appointment["dispute_delta"] = 0
r = add_appointment(new_appointment)
new_appt_data["appointment"]["dispute_delta"] = 0
r = add_appointment(new_appt_data)
assert r.status_code == 400
def test_request_appointment(new_appointment):
def test_request_appointment(new_appt_data):
# First we need to add an appointment
r = add_appointment(new_appointment)
r = add_appointment(new_appt_data)
assert r.status_code == 200
# Next we can request it
r = requests.get(url=PISA_API + "/get_appointment?locator=" + new_appointment["locator"])
r = requests.get(url=PISA_API + "/get_appointment?locator=" + new_appt_data["appointment"]["locator"])
assert r.status_code == 200
# Each locator may point to multiple appointments, check them all
@@ -60,7 +60,7 @@ def test_request_appointment(new_appointment):
appointment_status = [appointment.pop("status") for appointment in received_appointments]
# Check that the appointment is within the received appoints
assert new_appointment in received_appointments
assert new_appt_data["appointment"] in received_appointments
# Check that all the appointments are being watched
assert all([status == "being_watched" for status in appointment_status])
@@ -76,28 +76,28 @@ def test_request_random_appointment():
assert all([status == "not_found" for status in appointment_status])
def test_add_appointment_multiple_times(new_appointment, n=MULTIPLE_APPOINTMENTS):
def test_add_appointment_multiple_times(new_appt_data, n=MULTIPLE_APPOINTMENTS):
# Multiple appointments with the same locator should be valid
# DISCUSS: #34-store-identical-appointments
for _ in range(n):
r = add_appointment(new_appointment)
r = add_appointment(new_appt_data)
assert r.status_code == 200
def test_request_multiple_appointments_same_locator(new_appointment, n=MULTIPLE_APPOINTMENTS):
def test_request_multiple_appointments_same_locator(new_appt_data, n=MULTIPLE_APPOINTMENTS):
for _ in range(n):
r = add_appointment(new_appointment)
r = add_appointment(new_appt_data)
assert r.status_code == 200
test_request_appointment(new_appointment)
test_request_appointment(new_appt_data)
def test_add_too_many_appointment(new_appointment):
def test_add_too_many_appointment(new_appt_data):
for _ in range(MAX_APPOINTMENTS - len(appointments)):
r = add_appointment(new_appointment)
r = add_appointment(new_appt_data)
assert r.status_code == 200
r = add_appointment(new_appointment)
r = add_appointment(new_appt_data)
assert r.status_code == 503

View File

@@ -1,11 +1,18 @@
from binascii import unhexlify
import json
from binascii import hexlify, unhexlify
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from apps.cli.pisa_cli import build_appointment
from pisa import c_logger
from pisa.errors import *
from pisa.inspector import Inspector
from pisa.appointment import Appointment
from pisa.block_processor import BlockProcessor
from test.unit.conftest import get_random_value_hex
from pisa.conf import MIN_DISPUTE_DELTA, SUPPORTED_CIPHERS, SUPPORTED_HASH_FUNCTIONS
c_logger.disabled = True
@@ -18,6 +25,11 @@ WRONG_TYPES = [[], "", get_random_value_hex(32), 3.2, 2.0, (), object, {}, " " *
WRONG_TYPES_NO_STR = [[], unhexlify(get_random_value_hex(32)), 3.2, 2.0, (), object, {}, object()]
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")
def test_check_locator():
# Right appointment type, size and format
locator = get_random_value_hex(32)
@@ -189,13 +201,42 @@ def test_check_hash_function():
assert Inspector.check_hash_function(hash_function)[0] == APPOINTMENT_EMPTY_FIELD
def test_inspect(run_bitcoind):
def test_check_appointment_signature(generate_keypair):
client_sk, client_pk = generate_keypair
dummy_appointment_request = {
"tx": get_random_value_hex(192),
"tx_id": get_random_value_hex(32),
"start_time": 1500,
"end_time": 50000,
"dispute_delta": 200,
}
dummy_appointment = build_appointment(**dummy_appointment_request)
# Verify that an appointment signed by the client is valid
signature = sign_appointment(client_sk, dummy_appointment)
assert Inspector.check_appointment_signature(dummy_appointment, signature, client_pk)
fake_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
# Create a bad signature to make sure inspector rejects it
bad_signature = sign_appointment(fake_sk, dummy_appointment)
assert (
Inspector.check_appointment_signature(dummy_appointment, bad_signature, client_pk)[0]
== APPOINTMENT_INVALID_SIGNATURE
)
def test_inspect(run_bitcoind, generate_keypair):
# At this point every single check function has been already tested, let's test inspect with an invalid and a valid
# appointments.
client_sk, client_pk = generate_keypair
# Invalid appointment, every field is empty
appointment_data = dict()
appointment = inspector.inspect(appointment_data)
signature = sign_appointment(client_sk, appointment_data)
appointment = inspector.inspect(appointment_data, signature, client_pk)
assert type(appointment) == tuple and appointment[0] != 0
# Valid appointment
@@ -217,7 +258,9 @@ def test_inspect(run_bitcoind):
"hash_function": hash_function,
}
appointment = inspector.inspect(appointment_data)
signature = sign_appointment(client_sk, appointment_data)
appointment = inspector.inspect(appointment_data, signature, client_pk)
assert (
type(appointment) == Appointment