mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 22:24:23 +01:00
Merge pull request #51 from sr-gi/13-appointment-signature
Add Pisa's signature to appointments
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,3 +12,5 @@ appointments/
|
||||
test.py
|
||||
*.pyc
|
||||
.cache
|
||||
.pytest_cache/
|
||||
*.pem
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import logging
|
||||
from apps.cli.logger import Logger
|
||||
|
||||
# PISA-SERVER
|
||||
DEFAULT_PISA_API_SERVER = 'btc.pisa.watch'
|
||||
DEFAULT_PISA_API_PORT = 9814
|
||||
|
||||
# PISA-CLI
|
||||
CLIENT_LOG_FILE = 'pisa.log'
|
||||
CLIENT_LOG_FILE = 'pisa-cli.log'
|
||||
APPOINTMENTS_FOLDER_NAME = 'appointments'
|
||||
|
||||
# CRYPTO
|
||||
SUPPORTED_HASH_FUNCTIONS = ["SHA256"]
|
||||
SUPPORTED_CIPHERS = ["AES-GCM-128"]
|
||||
|
||||
PISA_PUBLIC_KEY = "pisa_pk.pem"
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(format='%(message)s', level=logging.INFO, handlers=[
|
||||
logging.FileHandler(CLIENT_LOG_FILE),
|
||||
logging.StreamHandler()
|
||||
])
|
||||
|
||||
logger = Logger("Client")
|
||||
|
||||
@@ -4,9 +4,7 @@ from binascii import hexlify, unhexlify
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
|
||||
from apps.cli import SUPPORTED_HASH_FUNCTIONS, SUPPORTED_CIPHERS
|
||||
from pisa.logger import Logger
|
||||
|
||||
logger = Logger("Client")
|
||||
from apps.cli import logger
|
||||
|
||||
|
||||
class Blob:
|
||||
|
||||
33
apps/cli/logger.py
Normal file
33
apps/cli/logger.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class StructuredMessage(object):
|
||||
def __init__(self, message, **kwargs):
|
||||
self.message = message
|
||||
self.time = time.asctime()
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps({**self.kwargs, "message": self.message, "time": self.time})
|
||||
|
||||
|
||||
class Logger(object):
|
||||
def __init__(self, actor=None):
|
||||
self.actor = actor
|
||||
|
||||
def _add_prefix(self, msg):
|
||||
return msg if self.actor is None else "[{}] {}".format(self.actor, msg)
|
||||
|
||||
def info(self, msg, **kwargs):
|
||||
logging.info(StructuredMessage(self._add_prefix(msg), actor=self.actor, **kwargs))
|
||||
|
||||
def debug(self, msg, **kwargs):
|
||||
logging.debug(StructuredMessage(self._add_prefix(msg), actor=self.actor, **kwargs))
|
||||
|
||||
def error(self, msg, **kwargs):
|
||||
logging.error(StructuredMessage(self._add_prefix(msg), actor=self.actor, **kwargs))
|
||||
|
||||
def warning(self, msg, **kwargs):
|
||||
logging.warning(StructuredMessage(self._add_prefix(msg), actor=self.actor, **kwargs))
|
||||
@@ -3,19 +3,28 @@ import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
from sys import argv
|
||||
from hashlib import sha256
|
||||
from binascii import 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.asymmetric import ec
|
||||
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
|
||||
|
||||
from pisa.logger import Logger
|
||||
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
|
||||
from apps.cli import DEFAULT_PISA_API_SERVER, DEFAULT_PISA_API_PORT, PISA_PUBLIC_KEY, APPOINTMENTS_FOLDER_NAME
|
||||
from apps.cli import logger
|
||||
|
||||
|
||||
logger = Logger("Client")
|
||||
HTTP_OK = 200
|
||||
|
||||
|
||||
# FIXME: TESTING ENDPOINT, WON'T BE THERE IN PRODUCTION
|
||||
@@ -35,6 +44,47 @@ def generate_dummy_appointment():
|
||||
print('\nData stored in dummy_appointment_data.json')
|
||||
|
||||
|
||||
# Loads Pisa's public key from disk and verifies that the appointment signature is a valid signature from Pisa,
|
||||
# returning True or False accordingly.
|
||||
# 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 is_appointment_signature_valid(appointment, signature) -> bool:
|
||||
# Load the key from disk
|
||||
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())
|
||||
except UnsupportedAlgorithm:
|
||||
raise ValueError("Could not deserialize the public key (unsupported algorithm).")
|
||||
|
||||
try:
|
||||
sig_bytes = unhexlify(signature.encode('utf-8'))
|
||||
data = json.dumps(appointment, sort_keys=True, separators=(',', ':')).encode("utf-8")
|
||||
pisa_public_key.verify(sig_bytes, data, ec.ECDSA(hashes.SHA256()))
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
|
||||
# Makes sure that the folder APPOINTMENTS_FOLDER_NAME exists, then saves the appointment and signature in it.
|
||||
def save_signed_appointment(appointment, signature):
|
||||
# Create the appointments directory if it doesn't already exist
|
||||
try:
|
||||
os.makedirs(APPOINTMENTS_FOLDER_NAME)
|
||||
except FileExistsError:
|
||||
# directory already exists, this is fine
|
||||
pass
|
||||
|
||||
timestamp = int(time.time()*1000)
|
||||
locator = appointment['locator']
|
||||
uuid = uuid4() # prevent filename collisions
|
||||
filename = "{}/appointment-{}-{}-{}.json".format(APPOINTMENTS_FOLDER_NAME, timestamp, locator, uuid)
|
||||
data = {"appointment": appointment, "signature": signature}
|
||||
|
||||
with open(filename, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
|
||||
def add_appointment(args):
|
||||
appointment_data = None
|
||||
use_help = "Use 'help add_appointment' for help of how to use the command."
|
||||
@@ -69,19 +119,64 @@ def add_appointment(args):
|
||||
appointment = build_appointment(appointment_data.get('tx'), appointment_data.get('tx_id'),
|
||||
appointment_data.get('start_time'), appointment_data.get('end_time'),
|
||||
appointment_data.get('dispute_delta'))
|
||||
appointment_json = json.dumps(appointment, sort_keys=True, separators=(',', ':'))
|
||||
|
||||
logger.info("Sending appointment to PISA")
|
||||
|
||||
try:
|
||||
r = requests.post(url=add_appointment_endpoint, json=json.dumps(appointment), timeout=5)
|
||||
r = requests.post(url=add_appointment_endpoint, json=appointment_json, timeout=5)
|
||||
|
||||
logger.info("{} (code: {}).".format(r.text, r.status_code))
|
||||
response_json = r.json()
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error("The response was not valid JSON.")
|
||||
return
|
||||
|
||||
except ConnectTimeout:
|
||||
logger.error("Can't connect to pisa API. Connection timeout.")
|
||||
return
|
||||
|
||||
except ConnectionError:
|
||||
logger.error("Can't connect to pisa API. Server cannot be reached.")
|
||||
return
|
||||
|
||||
if r.status_code == HTTP_OK:
|
||||
if 'signature' not in response_json:
|
||||
logger.error("The response does not contain the signature of the appointment.")
|
||||
else:
|
||||
signature = response_json['signature']
|
||||
# verify that the returned signature is valid
|
||||
try:
|
||||
is_sig_valid = is_appointment_signature_valid(appointment, signature)
|
||||
except ValueError:
|
||||
logger.error("Failed to deserialize the public key. It might be in an unsupported format.")
|
||||
return
|
||||
except FileNotFoundError:
|
||||
logger.error("Pisa's public key file not found. Please check your settings.")
|
||||
return
|
||||
except IOError as e:
|
||||
logger.error("I/O error({}): {}".format(e.errno, e.strerror))
|
||||
return
|
||||
|
||||
if is_sig_valid:
|
||||
logger.info("Appointment accepted and signed by Pisa.")
|
||||
# all good, store appointment and signature
|
||||
try:
|
||||
save_signed_appointment(appointment, signature)
|
||||
except OSError as e:
|
||||
logger.error("There was an error while saving the appointment: {}".format(e))
|
||||
else:
|
||||
logger.error("The returned appointment's signature is invalid.")
|
||||
|
||||
else:
|
||||
if 'error' not in response_json:
|
||||
logger.error("The server returned status code {}, but no error description."
|
||||
.format(r.status_code))
|
||||
else:
|
||||
error = r.json()['error']
|
||||
logger.error("The server returned status code {}, and the following error: {}."
|
||||
.format(r.status_code, error))
|
||||
|
||||
else:
|
||||
logger.error("The provided locator is not valid.")
|
||||
else:
|
||||
@@ -119,7 +214,7 @@ def get_appointment(args):
|
||||
logger.error("The provided locator is not valid.")
|
||||
|
||||
|
||||
def build_appointment(tx, tx_id, start_block, end_block, dispute_delta):
|
||||
def build_appointment(tx, tx_id, start_time, end_time, dispute_delta):
|
||||
locator = sha256(unhexlify(tx_id)).hexdigest()
|
||||
|
||||
cipher = "AES-GCM-128"
|
||||
@@ -129,9 +224,9 @@ def build_appointment(tx, tx_id, start_block, end_block, dispute_delta):
|
||||
blob = Blob(tx, cipher, hash_function)
|
||||
encrypted_blob = blob.encrypt(tx_id)
|
||||
|
||||
appointment = {"locator": locator, "start_time": start_block, "end_time": end_block,
|
||||
"dispute_delta": dispute_delta, "encrypted_blob": encrypted_blob, "cipher": cipher, "hash_function":
|
||||
hash_function}
|
||||
appointment = {
|
||||
'locator': locator, 'start_time': start_time, 'end_time': end_time, 'dispute_delta': dispute_delta,
|
||||
'encrypted_blob': encrypted_blob, 'cipher': cipher, 'hash_function': hash_function}
|
||||
|
||||
return appointment
|
||||
|
||||
@@ -223,4 +318,3 @@ if __name__ == '__main__':
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error("Non-JSON encoded appointment passed as parameter.")
|
||||
|
||||
|
||||
47
apps/generate_key.py
Normal file
47
apps/generate_key.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import os.path
|
||||
from sys import exit
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
|
||||
# Simple tool to generate an ECDSA private key using the secp256k1 curve and save private and public keys
|
||||
# as 'pisa_sk.pem' 'and pisa_pk.pem', respectively.
|
||||
|
||||
SK_FILE_NAME = 'pisa_sk.pem'
|
||||
PK_FILE_NAME = 'pisa_pk.pem'
|
||||
|
||||
|
||||
def save_sk(sk, filename):
|
||||
pem = sk.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
with open(filename, 'wb') as pem_out:
|
||||
pem_out.write(pem)
|
||||
|
||||
|
||||
def save_pk(pk, filename):
|
||||
pem = pk.public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
with open(filename, 'wb') as pem_out:
|
||||
pem_out.write(pem)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if os.path.exists(SK_FILE_NAME):
|
||||
print("A key with name \"{}\" already exists. Aborting.".format(SK_FILE_NAME))
|
||||
exit(1)
|
||||
|
||||
sk = ec.generate_private_key(
|
||||
ec.SECP256K1, default_backend()
|
||||
)
|
||||
pk = sk.public_key()
|
||||
|
||||
save_sk(sk, SK_FILE_NAME)
|
||||
save_pk(pk, PK_FILE_NAME)
|
||||
print("Saved private key \"{}\" and public key \"{}\".".format(SK_FILE_NAME, PK_FILE_NAME))
|
||||
32
pisa/api.py
32
pisa/api.py
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import json
|
||||
from flask import Flask, request, Response, abort, jsonify
|
||||
from flask import Flask, request, abort, jsonify
|
||||
from binascii import hexlify
|
||||
|
||||
from pisa import HOST, PORT, logging
|
||||
from pisa.logger import Logger
|
||||
@@ -31,30 +32,35 @@ def add_appointment():
|
||||
request_data = json.loads(request.get_json())
|
||||
appointment = inspector.inspect(request_data)
|
||||
|
||||
if type(appointment) == Appointment:
|
||||
appointment_added = watcher.add_appointment(appointment)
|
||||
error = None
|
||||
response = None
|
||||
|
||||
if type(appointment) == Appointment:
|
||||
appointment_added, signature = watcher.add_appointment(appointment)
|
||||
|
||||
# ToDo: #13-create-server-side-signature-receipt
|
||||
if appointment_added:
|
||||
rcode = HTTP_OK
|
||||
response = "appointment accepted. locator: {}".format(appointment.locator)
|
||||
response = {"locator": appointment.locator, "signature": hexlify(signature).decode('utf-8')}
|
||||
else:
|
||||
rcode = HTTP_SERVICE_UNAVAILABLE
|
||||
response = "appointment rejected"
|
||||
error = "appointment rejected"
|
||||
|
||||
elif type(appointment) == tuple:
|
||||
rcode = HTTP_BAD_REQUEST
|
||||
response = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1])
|
||||
error = "appointment rejected. Error {}: {}".format(appointment[0], appointment[1])
|
||||
|
||||
else:
|
||||
# We should never end up here, since inspect only returns appointments or tuples. Just in case.
|
||||
rcode = HTTP_BAD_REQUEST
|
||||
response = "appointment rejected. Request does not match the standard"
|
||||
error = "appointment rejected. Request does not match the standard"
|
||||
|
||||
logger.info('Sending response and disconnecting',
|
||||
from_addr_port='{}:{}'.format(remote_addr, remote_port), response=response)
|
||||
from_addr_port='{}:{}'.format(remote_addr, remote_port), response=response, error=error)
|
||||
|
||||
return Response(response, status=rcode, mimetype='text/plain')
|
||||
if error is None:
|
||||
return jsonify(response), rcode
|
||||
else:
|
||||
return jsonify({"error": error}), rcode
|
||||
|
||||
|
||||
# FIXME: THE NEXT THREE API ENDPOINTS ARE FOR TESTING AND SHOULD BE REMOVED / PROPERLY MANAGED BEFORE PRODUCTION!
|
||||
@@ -70,7 +76,7 @@ def get_appointment():
|
||||
|
||||
if appointment_in_watcher:
|
||||
for uuid in appointment_in_watcher:
|
||||
appointment_data = watcher.appointments[uuid].to_json()
|
||||
appointment_data = watcher.appointments[uuid].to_dict()
|
||||
appointment_data['status'] = "being_watched"
|
||||
response.append(appointment_data)
|
||||
|
||||
@@ -79,7 +85,7 @@ def get_appointment():
|
||||
|
||||
for job in responder_jobs.values():
|
||||
if job.locator == locator:
|
||||
job_data = job.to_json()
|
||||
job_data = job.to_dict()
|
||||
job_data['status'] = "dispute_responded"
|
||||
response.append(job_data)
|
||||
|
||||
@@ -100,7 +106,7 @@ def get_all_appointments():
|
||||
|
||||
if request.remote_addr in request.host or request.remote_addr == '127.0.0.1':
|
||||
for uuid, appointment in watcher.appointments.items():
|
||||
watcher_appointments[uuid] = appointment.to_json()
|
||||
watcher_appointments[uuid] = appointment.to_dict()
|
||||
|
||||
if watcher.responder:
|
||||
for uuid, job in watcher.responder.jobs.items():
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from pisa.encrypted_blob import EncryptedBlob
|
||||
|
||||
|
||||
@@ -13,7 +15,7 @@ class Appointment:
|
||||
self.cipher = cipher
|
||||
self.hash_function = hash_function
|
||||
|
||||
def to_json(self):
|
||||
def to_dict(self):
|
||||
appointment = {"locator": self.locator, "start_time": self.start_time, "end_time": self.end_time,
|
||||
"dispute_delta": self.dispute_delta, "encrypted_blob": self.encrypted_blob.data,
|
||||
"cipher": self.cipher, "hash_function": self.hash_function}
|
||||
@@ -22,3 +24,5 @@ class Appointment:
|
||||
|
||||
# ToDO: #3-improve-appointment-structure
|
||||
|
||||
def to_json(self):
|
||||
return json.dumps(self.to_dict(), sort_keys=True, separators=(',', ':'))
|
||||
|
||||
@@ -36,6 +36,9 @@ if __name__ == '__main__':
|
||||
logger.error("bitcoind is running on a different network, check conf.py and bitcoin.conf. Shutting down")
|
||||
|
||||
else:
|
||||
# Fire the api
|
||||
start_api()
|
||||
|
||||
try:
|
||||
# Fire the api
|
||||
start_api()
|
||||
except Exception as e:
|
||||
logger.error("An error occurred: {}. Shutting down".format(e))
|
||||
exit(1)
|
||||
|
||||
@@ -17,6 +17,7 @@ EXPIRY_DELTA = 6
|
||||
MIN_DISPUTE_DELTA = 20
|
||||
SERVER_LOG_FILE = 'pisa.log'
|
||||
DB_PATH = 'appointments/'
|
||||
PISA_SECRET_KEY = 'pisa_sk.pem'
|
||||
|
||||
# PISA-CLI
|
||||
CLIENT_LOG_FILE = 'pisa.log'
|
||||
|
||||
@@ -2,11 +2,15 @@ from uuid import uuid4
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
|
||||
from pisa.logger import Logger
|
||||
from pisa.cleaner import Cleaner
|
||||
from pisa.conf import EXPIRY_DELTA
|
||||
from pisa.conf import EXPIRY_DELTA, MAX_APPOINTMENTS, PISA_SECRET_KEY
|
||||
from pisa.responder import Responder
|
||||
from pisa.conf import MAX_APPOINTMENTS
|
||||
from pisa.block_processor import BlockProcessor
|
||||
from pisa.utils.zmq_subscriber import ZMQHandler
|
||||
|
||||
@@ -23,6 +27,17 @@ class Watcher:
|
||||
self.zmq_subscriber = None
|
||||
self.responder = Responder()
|
||||
|
||||
if PISA_SECRET_KEY is None:
|
||||
raise ValueError("No signing key provided. Please fix your pisa.conf")
|
||||
else:
|
||||
with open(PISA_SECRET_KEY, "r") as key_file:
|
||||
secret_key_pem = key_file.read().encode("utf-8")
|
||||
self.signing_key = load_pem_private_key(secret_key_pem, password=None, backend=default_backend())
|
||||
|
||||
def sign_appointment(self, appointment):
|
||||
data = appointment.to_json().encode("utf-8")
|
||||
return self.signing_key.sign(data, ec.ECDSA(hashes.SHA256()))
|
||||
|
||||
def add_appointment(self, appointment):
|
||||
# Rationale:
|
||||
# The Watcher will analyze every received block looking for appointment matches. If there is no work
|
||||
@@ -60,12 +75,14 @@ class Watcher:
|
||||
|
||||
logger.info("New appointment accepted.", locator=appointment.locator)
|
||||
|
||||
signature = self.sign_appointment(appointment)
|
||||
else:
|
||||
appointment_added = False
|
||||
signature = None
|
||||
|
||||
logger.info("Maximum appointments reached, appointment rejected.", locator=appointment.locator)
|
||||
|
||||
return appointment_added
|
||||
return appointment_added, signature
|
||||
|
||||
def do_subscribe(self):
|
||||
self.zmq_subscriber = ZMQHandler(parent="Watcher")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from pytest import fixture
|
||||
|
||||
from pisa.appointment import Appointment
|
||||
@@ -35,13 +36,25 @@ def test_init_appointment(appointment_data):
|
||||
and dispute_delta == appointment.dispute_delta and hash_function == appointment.hash_function)
|
||||
|
||||
|
||||
def test_to_dict(appointment_data):
|
||||
locator, start_time, end_time, dispute_delta, encrypted_blob_data, cipher, hash_function = appointment_data
|
||||
appointment = Appointment(locator, start_time, end_time, dispute_delta, encrypted_blob_data, cipher, hash_function)
|
||||
|
||||
dict_appointment = appointment.to_dict()
|
||||
|
||||
assert (locator == dict_appointment.get("locator") and start_time == dict_appointment.get("start_time")
|
||||
and end_time == dict_appointment.get("end_time") and dispute_delta == dict_appointment.get("dispute_delta")
|
||||
and cipher == dict_appointment.get("cipher") and hash_function == dict_appointment.get("hash_function")
|
||||
and encrypted_blob_data == dict_appointment.get("encrypted_blob"))
|
||||
|
||||
|
||||
def test_to_json(appointment_data):
|
||||
locator, start_time, end_time, dispute_delta, encrypted_blob_data, cipher, hash_function = appointment_data
|
||||
appointment = Appointment(locator, start_time, end_time, dispute_delta, encrypted_blob_data, cipher, hash_function)
|
||||
|
||||
json_appointment = appointment.to_json()
|
||||
dict_appointment = json.loads(appointment.to_json())
|
||||
|
||||
assert (locator == json_appointment.get("locator") and start_time == json_appointment.get("start_time")
|
||||
and end_time == json_appointment.get("end_time") and dispute_delta == json_appointment.get("dispute_delta")
|
||||
and cipher == json_appointment.get("cipher") and hash_function == json_appointment.get("hash_function")
|
||||
and encrypted_blob_data == json_appointment.get("encrypted_blob"))
|
||||
assert (locator == dict_appointment.get("locator") and start_time == dict_appointment.get("start_time")
|
||||
and end_time == dict_appointment.get("end_time") and dispute_delta == dict_appointment.get("dispute_delta")
|
||||
and cipher == dict_appointment.get("cipher") and hash_function == dict_appointment.get("hash_function")
|
||||
and encrypted_blob_data == dict_appointment.get("encrypted_blob"))
|
||||
|
||||
@@ -6,6 +6,12 @@ from threading import Thread
|
||||
from binascii import unhexlify
|
||||
from queue import Queue, Empty
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
from apps.cli.blob import Blob
|
||||
from pisa.watcher import Watcher
|
||||
from pisa.responder import Responder
|
||||
@@ -16,7 +22,7 @@ from test.simulator.utils import sha256d
|
||||
from test.simulator.transaction import TX
|
||||
from pisa.utils.auth_proxy import AuthServiceProxy
|
||||
from test.unit.conftest import generate_block, generate_blocks
|
||||
from pisa.conf import EXPIRY_DELTA, BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT
|
||||
from pisa.conf import EXPIRY_DELTA, BTC_RPC_USER, BTC_RPC_PASSWD, BTC_RPC_HOST, BTC_RPC_PORT, PISA_SECRET_KEY
|
||||
|
||||
logging.getLogger().disabled = True
|
||||
|
||||
@@ -24,6 +30,12 @@ APPOINTMENTS = 5
|
||||
START_TIME_OFFSET = 1
|
||||
END_TIME_OFFSET = 1
|
||||
|
||||
with open(PISA_SECRET_KEY, "r") as key_file:
|
||||
pubkey_pem = key_file.read().encode("utf-8")
|
||||
# TODO: should use the public key file instead, but it is not currently exported in the configuration
|
||||
signing_key = load_pem_private_key(pubkey_pem, password=None, backend=default_backend())
|
||||
public_key = signing_key.public_key()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def watcher():
|
||||
@@ -70,6 +82,16 @@ def create_appointments(n):
|
||||
return appointments, locator_uuid_map, dispute_txs
|
||||
|
||||
|
||||
def is_signature_valid(appointment, signature, pk):
|
||||
# verify the signature
|
||||
try:
|
||||
data = appointment.to_json().encode('utf-8')
|
||||
pk.verify(signature, data, ec.ECDSA(hashes.SHA256()))
|
||||
except InvalidSignature:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def test_init(watcher):
|
||||
assert type(watcher.appointments) is dict and len(watcher.appointments) == 0
|
||||
assert type(watcher.locator_uuid_map) is dict and len(watcher.locator_uuid_map) == 0
|
||||
@@ -88,9 +110,16 @@ def test_add_appointment(run_bitcoind, watcher):
|
||||
# We should be able to add appointments up to the limit
|
||||
for _ in range(10):
|
||||
appointment, dispute_tx = generate_dummy_appointment()
|
||||
added_appointment = watcher.add_appointment(appointment)
|
||||
added_appointment, sig = watcher.add_appointment(appointment)
|
||||
|
||||
assert added_appointment is True
|
||||
assert is_signature_valid(appointment, sig, public_key)
|
||||
|
||||
|
||||
def test_sign_appointment(watcher):
|
||||
appointment, _ = generate_dummy_appointment()
|
||||
signature = watcher.sign_appointment(appointment)
|
||||
assert is_signature_valid(appointment, signature, public_key)
|
||||
|
||||
|
||||
def test_add_too_many_appointments(watcher):
|
||||
@@ -99,14 +128,16 @@ def test_add_too_many_appointments(watcher):
|
||||
|
||||
for _ in range(MAX_APPOINTMENTS):
|
||||
appointment, dispute_tx = generate_dummy_appointment()
|
||||
added_appointment = watcher.add_appointment(appointment)
|
||||
added_appointment, sig = watcher.add_appointment(appointment)
|
||||
|
||||
assert added_appointment is True
|
||||
assert is_signature_valid(appointment, sig, public_key)
|
||||
|
||||
appointment, dispute_tx = generate_dummy_appointment()
|
||||
added_appointment = watcher.add_appointment(appointment)
|
||||
added_appointment, sig = watcher.add_appointment(appointment)
|
||||
|
||||
assert added_appointment is False
|
||||
assert sig is None
|
||||
|
||||
|
||||
def test_do_subscribe(watcher):
|
||||
|
||||
Reference in New Issue
Block a user