mirror of
https://github.com/aljazceru/python-teos.git
synced 2025-12-17 22:24:23 +01:00
@@ -7,81 +7,73 @@ The PISA REST API consists, currently, of two endpoints: `/` and `/check_appoint
|
|||||||
`/` is the default endpoint, and is where the appointments should be sent to. `/` accepts `HTTP POST` requests only, with json request body, where data must match the following format:
|
`/` is the default endpoint, and is where the appointments should be sent to. `/` accepts `HTTP POST` requests only, with json request body, where data must match the following format:
|
||||||
|
|
||||||
{"locator": l, "start_time": s, "end_time": e,
|
{"locator": l, "start_time": s, "end_time": e,
|
||||||
"dispute_delta": d, "encrypted_blob": eb, "cipher":
|
"dispute_delta": d, "encrypted_blob": eb}
|
||||||
c, "hash_function": h}
|
|
||||||
|
|
||||||
We'll discuss the parameters one by one in the following:
|
We'll discuss the parameters one by one in the following:
|
||||||
|
|
||||||
The locator, `l`, is the `sha256` hex representation of the **dispute transaction id** (i.e. the sha256 of the byte representation of the dispute transaction id encoded in hex). `type(l) = hex encoded str`
|
The locator, `l`, is the first half of the **dispute transaction id** (i.e. the 16 MSB of the dispute_txid encoded in hex). `type(l) = hex encoded str`
|
||||||
|
|
||||||
The start\_time, `s`, is the time when the PISA server will start watching your transaction, and will normally match with whenever you will be offline. `s` is measured in block height, and must be **higher than the current block height** and not too close to it. `type(s) = int`
|
The start\_time, `s`, is the time when the PISA server will start watching your transaction, and will normally match with whenever you will be offline. `s` is measured in block height, and must be **higher than the current block height**. `type(s) = int`
|
||||||
|
|
||||||
The end\_time, `e`, is the time where the PISA server will stop watching your transaction, and will normally match which whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
The end\_time, `e`, is the time where the PISA server will stop watching your transaction, and will normally match with whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
||||||
|
|
||||||
The dispute\_delta, `d`, is the time PISA would have to respond with the **justice transaction** once the **dispute transaction** is seen in the blockchain. `d` must match with the `OP_CSV` specified in the dispute transaction. If the dispute_delta does not match the `OP_CSV `, PISA would try to respond with the justice transaction anyway, but success is not guaranteed. `d` is measured in blocks and should be, at least, `20`. `type(d) = int`
|
The to\_self\_delay, `d`, is the time PISA would have to respond with the **penalty transaction** once the **dispute transaction** is seen in the blockchain. `d` must match with the `OP_CSV` specified in the dispute transaction. If the dispute_delta does not match the `OP_CSV `, PISA would try to respond with the penalty transaction anyway, but success is not guaranteed. `d` is measured in blocks and should be, at least, `20`. `type(d) = int`
|
||||||
|
|
||||||
The encrypted\_blob, `eb`, is a data blob containing the `raw justice transaction` and it is encrypted using `AES-GCM-128`. The `encryption key` and `nonce` used by the cipher are **derived from the justice transaction id** as follows:
|
The encrypted\_blob, `eb`, is a data blob containing the `raw penalty transaction` and it is encrypted using `CHACHA20-POLY1305`. The `encryption key` used by the cipher is the sha256 of the **dispute transaction id**, and the `nonce` is a 12-byte long zero byte array:
|
||||||
|
|
||||||
master_key = SHA256(tx_id|tx_id)
|
sk = sk = sha256(unhexlify(secret)).digest()
|
||||||
sk = master_key[:16]
|
nonce = nonce = bytearray(12) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||||
nonce = master_key[16:]
|
|
||||||
|
|
||||||
where `| `represents concatenation, `[:16]` represent the first half (16 bytes), and `[16:]` represents the second half of the master key. Finally, the encrypted blob must be hex encoded. `type(eb) = hex encoded str`
|
Finally, the encrypted blob must be hex encoded. `type(eb) = hex encoded str`
|
||||||
|
|
||||||
The cipher, `c`, represents the cipher used to encrypt `eb`. The only cipher supported, for now, is `AES-GCM-128`. `type(c) = str`
|
The API will return a `application/json` HTTP response code `200/OK` if the appointment is accepted, with the locator encoded in the response text, or a `400/Bad Request` if the appointment is rejected, with the rejection reason encoded in the response text.
|
||||||
|
|
||||||
The hash\_function, `h`, represents the hash function used to derive the encryption key and the nonce used to create `eb`. The only hash function supported, for now, is `SHA256`. `type(h) = str`
|
|
||||||
|
|
||||||
The API will return a `text/plain` HTTP response code `200/OK` if the appointment is accepted, with the locator encoded in the response text, or a `400/Bad Request` if the appointment is rejected, with the rejection reason encoded in the response text.
|
|
||||||
|
|
||||||
#### Appointment example
|
#### Appointment example
|
||||||
|
|
||||||
{"locator": "3c3375883f01027e5ca14f9760a8b853824ca4ebc0258c00e7fae4bae2571a80",
|
{"locator": "3c3375883f01027e5ca14f9760a8b853824ca4ebc0258c00e7fae4bae2571a80",
|
||||||
"start_time": 1568118,
|
"start_time": 1568118,
|
||||||
"end_time": 1568120,
|
"end_time": 1568120,
|
||||||
"dispute_delta": 20,
|
"to_self_delay": 20,
|
||||||
"encrypted_blob": "6c7687a97e874363e1c2b9a08386125e09ea000a9b4330feb33a5c698265f3565c267554e6fdd7b0544ced026aaab73c255bcc97c18eb9fa704d9cc5f1c83adaf921de7ba62b2b6ddb1bda7775288019ec3708642e738eddc22882abf5b3f4e34ef2d4077ed23e135f7fe22caaec845982918e7df4a3f949cadd2d3e7c541b1dbf77daf64e7ed61531aaa487b468581b5aa7b1da81e2617e351c9d5cf445e3391c3fea4497aaa7ad286552759791b9caa5e4c055d1b38adfceddb1ef2b99e3b467dd0b0b13ce863c1bf6b6f24543c30d",
|
"encrypted_blob": "6c7687a97e874363e1c2b9a08386125e09ea000a9b4330feb33a5c698265f3565c267554e6fdd7b0544ced026aaab73c255bcc97c18eb9fa704d9cc5f1c83adaf921de7ba62b2b6ddb1bda7775288019ec3708642e738eddc22882abf5b3f4e34ef2d4077ed23e135f7fe22caaec845982918e7df4a3f949cadd2d3e7c541b1dbf77daf64e7ed61531aaa487b468581b5aa7b1da81e2617e351c9d5cf445e3391c3fea4497aaa7ad286552759791b9caa5e4c055d1b38adfceddb1ef2b99e3b467dd0b0b13ce863c1bf6b6f24543c30d"}
|
||||||
"cipher": "AES-GCM-128",
|
|
||||||
"hash_function": "SHA256"}
|
|
||||||
|
|
||||||
# Check appointment
|
# Check appointment
|
||||||
|
|
||||||
`/check_appointment` is a testing endpoint provided to check the status of the appointments sent to PISA. The endpoint is accessible without any type of authentication for now. `/check_appointment` accepts `HTTP GET` requests only, where the data to be provided must be the locator of an appointment. The query must match the following format:
|
`/check_appointment` is an endpoint provided to check the status of the appointments sent to PISA. The endpoint is accessible without any type of authentication for now. `/check_appointment` accepts `HTTP GET` requests only, where the data to be provided must be the locator of an appointment. The query must match the following format:
|
||||||
|
|
||||||
`http://pisa_server:pisa_port/check_appointment?locator=appointment_locator`
|
`http://pisa_server:pisa_port/check_appointment?locator=appointment_locator`
|
||||||
|
|
||||||
### Appointment can be in three states
|
### Appointment can be in three states
|
||||||
|
|
||||||
- `not_found`: meaning the locator is not recognised by the API. This could either mean the locator is wrong, or the appointment has already been fulfilled (the PISA server does not have any kind of data persistency for now).
|
- `not_found`: meaning the locator is not recognised by the API. This could either mean the locator is wrong, or the appointment has already been fulfilled.
|
||||||
- `being_watched`: the appointment has been accepted by the PISA server and it's being watched at the moment. This stage means that the dispute transaction has now been seen yet, and therefore no justice transaction has been published.
|
- `being_watched`: the appointment has been accepted by the PISA server and it's being watched at the moment. This stage means that the dispute transaction has not been seen yet, and therefore no justice transaction has been published.
|
||||||
- `dispute_responded`: the dispute was found by the watcher and the corresponding justice transaction has been broadcast by the node. In this stage PISA is actively monitoring until the justice transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
- `dispute_responded`: the dispute was found by the watcher and the corresponding justice transaction has been broadcast by the node. In this stage PISA is actively monitoring until the justice transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
||||||
|
|
||||||
### Check appointment response formats
|
### Check appointment response formats
|
||||||
|
|
||||||
`/check_appointment` will always reply with `json` containing the information about the requested appointment. The structure is as follows:
|
`/check_appointment` will always reply with `json` containing the information about the requested appointment. The structure is as follows:
|
||||||
|
|
||||||
#### not_found
|
**not_found**
|
||||||
|
|
||||||
[{"locator": appointment_locator,
|
[{"locator": appointment_locator,
|
||||||
"status":"not_found"}]
|
"status":"not_found"}]
|
||||||
|
|
||||||
#### being_watched
|
**being_watched**
|
||||||
[{"cipher": "AES-GCM-128",
|
|
||||||
"dispute_delta": d,
|
[{"encrypted_blob": eb,
|
||||||
"encrypted_blob": eb,
|
|
||||||
"end_time": e,
|
"end_time": e,
|
||||||
"hash_function": "SHA256",
|
|
||||||
"locator": appointment_locator,
|
"locator": appointment_locator,
|
||||||
"start_time": s,
|
"start_time": s,
|
||||||
"status": "being_watched"}]
|
"status": "being_watched",
|
||||||
|
"to_self_delay": d}]
|
||||||
|
|
||||||
#### dispute_responded
|
**dispute_responded**
|
||||||
|
|
||||||
[{"locator": appointment_locator,
|
[{"appointment_end": e,
|
||||||
"justice_rawtx": j,
|
"dispute_txid": dispute_txid,
|
||||||
"appointment_end": e,
|
"locator": appointment_locator,
|
||||||
"status": "dispute_responded"
|
"penalty_rawtx": penalty_rawtx,
|
||||||
"confirmations": c}]
|
"penalty_txid": penalty_txid,
|
||||||
|
"status": "dispute_responded"}]
|
||||||
|
|
||||||
Notice that the response json always contains a list. Why? It is possible for both parties to send the “same locator” to our service:
|
Notice that the response json always contains a list. Why? It is possible for both parties to send the “same locator” to our service:
|
||||||
|
|
||||||
@@ -92,6 +84,6 @@ In the above scenario, Bob can hire our service with a bad encrypted blob for th
|
|||||||
|
|
||||||
### Data persistence
|
### Data persistence
|
||||||
|
|
||||||
As mentioned earlier, our service has no data persistence. this means that fulfilled appointments cannot be queried from `/check_appointment`. On top of that, if our service is restarted, all jobs are lost. This is only temporary and we are currently working on it. Do not use this service for production-ready software yet and please consider it as an early-stage demo to better understand how our API will work.
|
PISA keeps track of the appointment while they are being monitored, but data is wiped once an appointment has been completed with enough confirmations. Notice that during the alpha there will be no authentication, so data may be wiped periodically.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# pisa_cli
|
# wt_cli
|
||||||
|
|
||||||
`pisa_cli` is a command line interface to interact with the PISA server, written in Python3.
|
`wt_cli` is a command line interface to interact with the PISA WatchTower server, written in Python3.
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
Refer to [DEPENDENCIES.md](DEPENDENCIES.md)
|
Refer to [DEPENDENCIES.md](DEPENDENCIES.md)
|
||||||
@@ -11,7 +11,7 @@ Refer to [INSTALL.md](INSTALL.md)
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
python pisa_cli.py [global options] command [command options] [arguments]
|
python wt_cli.py [global options] command [command options] [arguments]
|
||||||
|
|
||||||
#### Global options
|
#### Global options
|
||||||
|
|
||||||
@@ -39,25 +39,24 @@ This command is used to register appointments to the PISA server. Appointments *
|
|||||||
"dispute_delta": d
|
"dispute_delta": d
|
||||||
}
|
}
|
||||||
|
|
||||||
`tx` **must** be the raw justice transaction that will be encrypted before sent to the PISA server. `type(tx) = hex encoded str`
|
`tx` **must** be the raw penalty transaction that will be encrypted before sent to the PISA server. `type(tx) = hex encoded str`
|
||||||
|
|
||||||
`tx_id` **must** match the **commitment transaction id**, and will be used to encrypt the **justice transaction** and **generate the locator**. `type(tx_id) = hex encoded str`
|
`tx_id` **must** match the **commitment transaction id**, and will be used to encrypt the **penalty transaction** and **generate the locator**. `type(tx_id) = hex encoded str`
|
||||||
|
|
||||||
`s` is the time when the PISA server will start watching your transaction, and will normally match to whenever you will be offline. `s` is measured in block height, and must be **higher than the current block height** and not too close to it. `type(s) = int`
|
`s` is the time when the PISA server will start watching your transaction, and will normally match to whenever you will be offline. `s` is measured in block height, and must be **higher than the current block height**. `type(s) = int`
|
||||||
|
|
||||||
`e` is the time where the PISA server will stop watching your transaction, and will normally match which whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
`e` is the time where the PISA server will stop watching your transaction, and will normally match with whenever you should be back online. `e` is also measured in block height, and must be **higher than** `s`. `type(e) = int`
|
||||||
|
|
||||||
`d` is the time PISA would have to respond with the **justice transaction** once the **dispute transaction** is seen in the blockchain. `d` must match with the `OP_CSV` specified in the dispute transaction. If the dispute_delta does not match the `OP_CSV `, PISA would try to respond with the justice transaction anyway, but success is not guaranteed. `d` is measured in blocks and should be at least `20`. `type(d) = int`
|
`d` is the time PISA would have to respond with the **penalty transaction** once the **dispute transaction** is seen in the blockchain. `d` must match with the `OP_CSV` specified in the dispute transaction. If the to\_self\_delay does not match the `OP_CSV`, PISA will try to respond with the penalty transaction anyway, but success is not guaranteed. `d` is measured in blocks and should be at least `20`. `type(d) = int`
|
||||||
|
|
||||||
The API will return a `text/plain` HTTP response code `200/OK` if the appointment is accepted, with the locator encoded in the response text, or a `400/Bad Request` if the appointment is rejected, with the rejection reason encoded in the response text.
|
The API will return a `application/json` HTTP response code `200/OK` if the appointment is accepted, with the locator encoded in the response text, or a `400/Bad Request` if the appointment is rejected, with the rejection reason encoded in the response text.
|
||||||
|
|
||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
|
|
||||||
python pisa_cli add_appointment [command options] <appointment>/<path_to_appointment_file>
|
python wt_cli.py add_appointment [command options] <appointment>/<path_to_appointment_file>
|
||||||
|
|
||||||
if `-f, --file` **is** specified, then the command expects a path to a json file instead of a json encoded
|
if `-f, --file` **is** specified, then the command expects a path to a json file instead of a json encoded string as parameter.
|
||||||
string as parameter.
|
|
||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
- `-f, --file path_to_json_file` loads the appointment data from the specified json file instead of command line.
|
- `-f, --file path_to_json_file` loads the appointment data from the specified json file instead of command line.
|
||||||
@@ -68,9 +67,9 @@ if `-f, --file` **is** specified, then the command expects a path to a json file
|
|||||||
|
|
||||||
**Appointment can be in three states**
|
**Appointment can be in three states**
|
||||||
|
|
||||||
- `not_found`: meaning the locator is not recognised by the API. This could either mean the locator is wrong, or the appointment has already been fulfilled (the PISA server does not have any kind of data persistency for now).
|
- `not_found`: meaning the locator is not recognised by the tower. This can either mean the locator is wrong, or the appointment has already been fulfilled (the PISA server does not keep track of completed appointments for now).
|
||||||
- `being_watched`: the appointment has been accepted by the PISA server and it's being watched at the moment. This stage means that the dispute transaction has now been seen yet, and therefore no justice transaction has been published.
|
- `being_watched`: the appointment has been accepted by the PISA server and it's being watched at the moment. This stage means that the dispute transaction has not been seen yet, and therefore no penalty transaction has been broadcast.
|
||||||
- `dispute_responded`: the dispute was found by the watcher and the corresponding justice transaction has been broadcast by the node. In this stage PISA is actively monitoring until the justice transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
- `dispute_responded`: the dispute was found by the watcher and the corresponding penalty transaction has been broadcast by the node. In this stage PISA is actively monitoring until the penalty transaction reaches enough confirmations and making sure no fork occurs in the meantime.
|
||||||
|
|
||||||
**Response formats**
|
**Response formats**
|
||||||
|
|
||||||
@@ -81,26 +80,25 @@ if `-f, --file` **is** specified, then the command expects a path to a json file
|
|||||||
|
|
||||||
**being_watched**
|
**being_watched**
|
||||||
|
|
||||||
[{"cipher": "AES-GCM-128",
|
[{"encrypted_blob": eb,
|
||||||
"dispute_delta": d,
|
|
||||||
"encrypted_blob": eb,
|
|
||||||
"end_time": e,
|
"end_time": e,
|
||||||
"hash_function": "SHA256",
|
|
||||||
"locator": appointment_locator,
|
"locator": appointment_locator,
|
||||||
"start_time": s,
|
"start_time": s,
|
||||||
"status": "being_watched"}]
|
"status": "being_watched",
|
||||||
|
"to_self_delay": d}]
|
||||||
|
|
||||||
**dispute_responded**
|
**dispute_responded**
|
||||||
|
|
||||||
[{"locator": appointment_locator,
|
[{"appointment_end": e,
|
||||||
"justice_rawtx": j,
|
"dispute_txid": dispute_txid,
|
||||||
"appointment_end": e,
|
"locator": appointment_locator,
|
||||||
"status": "dispute_responded"
|
"penalty_rawtx": penalty_rawtx,
|
||||||
"confirmations": c}]
|
"penalty_txid": penalty_txid,
|
||||||
|
"status": "dispute_responded"}]
|
||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
|
|
||||||
python pisa_cli get_appointment <appointment_locator>
|
python wt_cli.py get_appointment <appointment_locator>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -109,26 +107,26 @@ if `-f, --file` **is** specified, then the command expects a path to a json file
|
|||||||
Shows the list of commands or help about how to run a specific command.
|
Shows the list of commands or help about how to run a specific command.
|
||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
python pisa_cli help
|
python wt_cli.py help
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
python pisa_cli help command
|
python wt_cli.py help command
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
1. Generate a new dummy appointment. **Note:** this appointment will never be fulfilled (it will eventually expire) since it does not corresopond to a valid transaction. However it can be used to interact with the PISA API.
|
1. Generate a new dummy appointment. **Note:** this appointment will never be fulfilled (it will eventually expire) since it does not corresopond to a valid transaction. However it can be used to interact with the PISA API.
|
||||||
|
|
||||||
```
|
```
|
||||||
python pisa_cli.py generate_dummy_appointment
|
echo '{"tx": "4615a58815475ab8145b6bb90b1268a0dbb02e344ddd483f45052bec1f15b1951c1ee7f070a0993da395a5ee92ea3a1c184b5ffdb2507164bf1f8c1364155d48bdbc882eee0868ca69864a807f213f538990ad16f56d7dfb28a18e69e3f31ae9adad229e3244073b7d643b4597ec88bf247b9f73f301b0f25ae8207b02b7709c271da98af19f1db276ac48ba64f099644af1ae2c90edb7def5e8589a1bb17cc72ac42ecf07dd29cff91823938fd0d772c2c92b7ab050f8837efd46197c9b2b3f", "tx_id": "0b9510d92a50c1d67c6f7fc5d47908d96b3eccdea093d89bcbaf05bcfebdd951", "start_time": 0, "end_time": 0, "to_self_delay": 20}' > dummy_appointment_data.json
|
||||||
```
|
```
|
||||||
|
|
||||||
That will create a json file that follows the appointment data structure filled with dummy data and store it in `dummy_appointment_data.json`.
|
That will create a json file that follows the appointment data structure filled with dummy data and store it in `dummy_appointment_data.json`. **Note**: You'll need to update the `start_time` and `end_time` to match valid block heights.
|
||||||
|
|
||||||
2. Send the appointment to the PISA API. Which will then start monitoring for matching transactions.
|
2. Send the appointment to the PISA API. Which will then start monitoring for matching transactions.
|
||||||
|
|
||||||
```
|
```
|
||||||
python pisa_cli.py add_appointment -f dummy_appointment_data.json
|
python wt_cli.py add_appointment -f dummy_appointment_data.json
|
||||||
```
|
```
|
||||||
|
|
||||||
This returns a appointment locator that can be used to get updates about this appointment from PISA.
|
This returns a appointment locator that can be used to get updates about this appointment from PISA.
|
||||||
@@ -136,7 +134,7 @@ or
|
|||||||
3. Test that PISA is still watching the appointment by replacing the appointment locator received into the following command:
|
3. Test that PISA is still watching the appointment by replacing the appointment locator received into the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
python pisa_cli.py get_appointment <appointment_locator>
|
python wt_cli.py get_appointment <appointment_locator>
|
||||||
```
|
```
|
||||||
|
|
||||||
## PISA API
|
## PISA API
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ conf_fields = {
|
|||||||
"DATA_FOLDER": {"value": conf.DATA_FOLDER, "type": str},
|
"DATA_FOLDER": {"value": conf.DATA_FOLDER, "type": str},
|
||||||
"CLIENT_LOG_FILE": {"value": conf.CLIENT_LOG_FILE, "type": str, "path": True},
|
"CLIENT_LOG_FILE": {"value": conf.CLIENT_LOG_FILE, "type": str, "path": True},
|
||||||
"APPOINTMENTS_FOLDER_NAME": {"value": conf.APPOINTMENTS_FOLDER_NAME, "type": str, "path": True},
|
"APPOINTMENTS_FOLDER_NAME": {"value": conf.APPOINTMENTS_FOLDER_NAME, "type": str, "path": True},
|
||||||
"CLI_PUBLIC_KEY": {"value": conf.CLI_PUBLIC_KEY, "type": str, "path": True},
|
# "CLI_PUBLIC_KEY": {"value": conf.CLI_PUBLIC_KEY, "type": str, "path": True},
|
||||||
"CLI_PRIVATE_KEY": {"value": conf.CLI_PRIVATE_KEY, "type": str, "path": True},
|
# "CLI_PRIVATE_KEY": {"value": conf.CLI_PRIVATE_KEY, "type": str, "path": True},
|
||||||
"PISA_PUBLIC_KEY": {"value": conf.PISA_PUBLIC_KEY, "type": str, "path": True},
|
# "PISA_PUBLIC_KEY": {"value": conf.PISA_PUBLIC_KEY, "type": str, "path": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Expand user (~) if found and check fields are correct
|
# Expand user (~) if found and check fields are correct
|
||||||
|
|||||||
@@ -1,402 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
import binascii
|
|
||||||
from sys import argv
|
|
||||||
from getopt import getopt, GetoptError
|
|
||||||
from requests import ConnectTimeout, ConnectionError
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from apps.cli import config, LOG_PREFIX
|
|
||||||
from apps.cli.help import help_add_appointment, help_get_appointment
|
|
||||||
from apps.cli.blob import Blob
|
|
||||||
|
|
||||||
from common.logger import Logger
|
|
||||||
from common.appointment import Appointment
|
|
||||||
from common.cryptographer import Cryptographer
|
|
||||||
from common.tools import check_sha256_hex_format, check_locator_format, compute_locator
|
|
||||||
|
|
||||||
|
|
||||||
HTTP_OK = 200
|
|
||||||
logger = Logger(actor="Client", log_name_prefix=LOG_PREFIX)
|
|
||||||
|
|
||||||
|
|
||||||
# FIXME: TESTING ENDPOINT, WON'T BE THERE IN PRODUCTION
|
|
||||||
def generate_dummy_appointment():
|
|
||||||
get_block_count_end_point = "http://{}:{}/get_block_count".format(pisa_api_server, pisa_api_port)
|
|
||||||
r = requests.get(url=get_block_count_end_point, timeout=5)
|
|
||||||
|
|
||||||
current_height = r.json().get("block_count")
|
|
||||||
|
|
||||||
dummy_appointment_data = {
|
|
||||||
"tx": os.urandom(192).hex(),
|
|
||||||
"tx_id": os.urandom(32).hex(),
|
|
||||||
"start_time": current_height + 5,
|
|
||||||
"end_time": current_height + 10,
|
|
||||||
"to_self_delay": 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Generating dummy appointment data:" "\n\n" + json.dumps(dummy_appointment_data, indent=4, sort_keys=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
json.dump(dummy_appointment_data, open("dummy_appointment_data.json", "w"))
|
|
||||||
|
|
||||||
logger.info("\nData stored in dummy_appointment_data.json")
|
|
||||||
|
|
||||||
|
|
||||||
# Loads and returns Pisa keys from disk
|
|
||||||
def load_key_file_data(file_name):
|
|
||||||
try:
|
|
||||||
with open(file_name, "rb") as key_file:
|
|
||||||
key = key_file.read()
|
|
||||||
return key
|
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
logger.error("Client's key file not found. Please check your settings")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
except IOError as e:
|
|
||||||
logger.error("I/O error({}): {}".format(e.errno, e.strerror))
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
os.makedirs(config.get("APPOINTMENTS_FOLDER_NAME"), exist_ok=True)
|
|
||||||
|
|
||||||
timestamp = int(time.time())
|
|
||||||
locator = appointment["locator"]
|
|
||||||
uuid = uuid4().hex # prevent filename collisions
|
|
||||||
|
|
||||||
filename = "{}/appointment-{}-{}-{}.json".format(config.get("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):
|
|
||||||
# Get appointment data from user.
|
|
||||||
appointment_data = parse_add_appointment_args(args)
|
|
||||||
|
|
||||||
if appointment_data is None:
|
|
||||||
logger.error("The provided appointment JSON is empty")
|
|
||||||
return False
|
|
||||||
|
|
||||||
valid_txid = check_sha256_hex_format(appointment_data.get("tx_id"))
|
|
||||||
|
|
||||||
if not valid_txid:
|
|
||||||
logger.error("The provided txid is not valid")
|
|
||||||
return False
|
|
||||||
|
|
||||||
tx_id = appointment_data.get("tx_id")
|
|
||||||
tx = appointment_data.get("tx")
|
|
||||||
|
|
||||||
if None not in [tx_id, tx]:
|
|
||||||
appointment_data["locator"] = compute_locator(tx_id)
|
|
||||||
appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(tx), tx_id)
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error("Appointment data is missing some fields")
|
|
||||||
return False
|
|
||||||
|
|
||||||
appointment = Appointment.from_dict(appointment_data)
|
|
||||||
|
|
||||||
signature = get_appointment_signature(appointment)
|
|
||||||
hex_pk_der = get_pk()
|
|
||||||
|
|
||||||
if not (appointment and signature and hex_pk_der):
|
|
||||||
return False
|
|
||||||
|
|
||||||
data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")}
|
|
||||||
|
|
||||||
# Send appointment to the server.
|
|
||||||
response_json = post_data_to_add_appointment_endpoint(data)
|
|
||||||
|
|
||||||
if response_json is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
signature = response_json.get("signature")
|
|
||||||
# Check that the server signed the appointment as it should.
|
|
||||||
if signature is None:
|
|
||||||
logger.error("The response does not contain the signature of the appointment")
|
|
||||||
return False
|
|
||||||
|
|
||||||
valid = check_signature(signature, appointment)
|
|
||||||
|
|
||||||
if not valid:
|
|
||||||
logger.error("The returned appointment's signature is invalid")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info("Appointment accepted and signed by Pisa")
|
|
||||||
# all good, store appointment and signature
|
|
||||||
try:
|
|
||||||
save_signed_appointment(appointment.to_dict(), signature)
|
|
||||||
|
|
||||||
except OSError as e:
|
|
||||||
logger.error("There was an error while saving the appointment", error=e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# Parse arguments passed to add_appointment and handle them accordingly.
|
|
||||||
# Returns appointment data.
|
|
||||||
def parse_add_appointment_args(args):
|
|
||||||
use_help = "Use 'help add_appointment' for help of how to use the command"
|
|
||||||
|
|
||||||
if not args:
|
|
||||||
logger.error("No appointment data provided. " + use_help)
|
|
||||||
return None
|
|
||||||
|
|
||||||
arg_opt = args.pop(0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if arg_opt in ["-h", "--help"]:
|
|
||||||
sys.exit(help_add_appointment())
|
|
||||||
|
|
||||||
if arg_opt in ["-f", "--file"]:
|
|
||||||
fin = args.pop(0)
|
|
||||||
if not os.path.isfile(fin):
|
|
||||||
logger.error("Can't find file", filename=fin)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(fin) as f:
|
|
||||||
appointment_data = json.load(f)
|
|
||||||
|
|
||||||
except IOError as e:
|
|
||||||
logger.error("I/O error", errno=e.errno, error=e.strerror)
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
appointment_data = json.loads(arg_opt)
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.error("Non-JSON encoded data provided as appointment. " + use_help)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return appointment_data
|
|
||||||
|
|
||||||
|
|
||||||
# Sends appointment data to add_appointment endpoint to be processed by the server.
|
|
||||||
def post_data_to_add_appointment_endpoint(data):
|
|
||||||
logger.info("Sending appointment to PISA")
|
|
||||||
|
|
||||||
try:
|
|
||||||
add_appointment_endpoint = "http://{}:{}".format(pisa_api_server, pisa_api_port)
|
|
||||||
r = requests.post(url=add_appointment_endpoint, json=json.dumps(data), timeout=5)
|
|
||||||
|
|
||||||
response_json = r.json()
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
logger.error("The response was not valid JSON")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except ConnectTimeout:
|
|
||||||
logger.error("Can't connect to pisa API. Connection timeout")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except ConnectionError:
|
|
||||||
logger.error("Can't connect to pisa API. Server cannot be reached")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if r.status_code != HTTP_OK:
|
|
||||||
if "error" not in response_json:
|
|
||||||
logger.error("The server returned an error status code but no error description", status_code=r.status_code)
|
|
||||||
else:
|
|
||||||
error = response_json["error"]
|
|
||||||
logger.error(
|
|
||||||
"The server returned an error status code with an error description",
|
|
||||||
status_code=r.status_code,
|
|
||||||
description=error,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if "signature" not in response_json:
|
|
||||||
logger.error("The response does not contain the signature of the appointment")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return response_json
|
|
||||||
|
|
||||||
|
|
||||||
# Verify that the signature returned from the watchtower is valid.
|
|
||||||
def check_signature(signature, appointment):
|
|
||||||
try:
|
|
||||||
pisa_pk_der = load_key_file_data(config.get("PISA_PUBLIC_KEY"))
|
|
||||||
pisa_pk = Cryptographer.load_public_key_der(pisa_pk_der)
|
|
||||||
|
|
||||||
if pisa_pk is None:
|
|
||||||
logger.error("Failed to deserialize the public key. It might be in an unsupported format")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return Cryptographer.verify(appointment.serialize(), signature, pisa_pk)
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error("Pisa's public key file not found. Please check your settings")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except IOError as e:
|
|
||||||
logger.error("I/O error", errno=e.errno, error=e.strerror)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_appointment(args):
|
|
||||||
if not args:
|
|
||||||
logger.error("No arguments were given")
|
|
||||||
return None
|
|
||||||
|
|
||||||
arg_opt = args.pop(0)
|
|
||||||
|
|
||||||
if arg_opt in ["-h", "--help"]:
|
|
||||||
sys.exit(help_get_appointment())
|
|
||||||
else:
|
|
||||||
locator = arg_opt
|
|
||||||
valid_locator = check_locator_format(locator)
|
|
||||||
|
|
||||||
if not valid_locator:
|
|
||||||
logger.error("The provided locator is not valid", locator=locator)
|
|
||||||
return None
|
|
||||||
|
|
||||||
get_appointment_endpoint = "http://{}:{}/get_appointment".format(pisa_api_server, pisa_api_port)
|
|
||||||
parameters = "?locator={}".format(locator)
|
|
||||||
|
|
||||||
try:
|
|
||||||
r = requests.get(url=get_appointment_endpoint + parameters, timeout=5)
|
|
||||||
logger.info("Appointment response returned from server: {}".format(r.json()))
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
except ConnectTimeout:
|
|
||||||
logger.error("Can't connect to pisa API. Connection timeout")
|
|
||||||
return None
|
|
||||||
|
|
||||||
except ConnectionError:
|
|
||||||
logger.error("Can't connect to pisa API. Server cannot be reached")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_appointment_signature(appointment):
|
|
||||||
try:
|
|
||||||
sk_der = load_key_file_data(config.get("CLI_PRIVATE_KEY"))
|
|
||||||
cli_sk = Cryptographer.load_private_key_der(sk_der)
|
|
||||||
|
|
||||||
signature = Cryptographer.sign(appointment.serialize(), cli_sk)
|
|
||||||
|
|
||||||
return signature
|
|
||||||
|
|
||||||
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", errno=e.errno, error=e.strerror)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_pk():
|
|
||||||
try:
|
|
||||||
cli_pk_der = load_key_file_data(config.get("CLI_PUBLIC_KEY"))
|
|
||||||
hex_pk_der = binascii.hexlify(cli_pk_der)
|
|
||||||
|
|
||||||
return hex_pk_der
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error("Client's public key file not found. Please check your settings")
|
|
||||||
return False
|
|
||||||
|
|
||||||
except IOError as e:
|
|
||||||
logger.error("I/O error", errno=e.errno, error=e.strerror)
|
|
||||||
return False
|
|
||||||
|
|
||||||
except binascii.Error as e:
|
|
||||||
logger.error("Could not successfully encode public key as hex: ", e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def show_usage():
|
|
||||||
return (
|
|
||||||
"USAGE: "
|
|
||||||
"\n\tpython pisa-cli.py [global options] command [command options] [arguments]"
|
|
||||||
"\n\nCOMMANDS:"
|
|
||||||
"\n\tadd_appointment \tRegisters a json formatted appointment to the PISA server."
|
|
||||||
"\n\tget_appointment \tGets json formatted data about an appointment from the PISA server."
|
|
||||||
"\n\thelp \t\t\tShows a list of commands or help for a specific command."
|
|
||||||
"\n\nGLOBAL OPTIONS:"
|
|
||||||
"\n\t-s, --server \tAPI server where to send the requests. Defaults to btc.pisa.watch (modifiable in "
|
|
||||||
"__init__.py)"
|
|
||||||
"\n\t-p, --port \tAPI port where to send the requests. Defaults to 9814 (modifiable in __init__.py)"
|
|
||||||
"\n\t-d, --debug \tshows debug information and stores it in pisa.log"
|
|
||||||
"\n\t-h --help \tshows this message."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pisa_api_server = config.get("DEFAULT_PISA_API_SERVER")
|
|
||||||
pisa_api_port = config.get("DEFAULT_PISA_API_PORT")
|
|
||||||
commands = ["add_appointment", "get_appointment", "help"]
|
|
||||||
testing_commands = ["generate_dummy_appointment"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
opts, args = getopt(argv[1:], "s:p:h", ["server", "port", "help"])
|
|
||||||
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt in ["-s", "server"]:
|
|
||||||
if arg:
|
|
||||||
pisa_api_server = arg
|
|
||||||
|
|
||||||
if opt in ["-p", "--port"]:
|
|
||||||
if arg:
|
|
||||||
pisa_api_port = int(arg)
|
|
||||||
|
|
||||||
if opt in ["-h", "--help"]:
|
|
||||||
sys.exit(show_usage())
|
|
||||||
|
|
||||||
if args:
|
|
||||||
command = args.pop(0)
|
|
||||||
|
|
||||||
if command in commands:
|
|
||||||
if command == "add_appointment":
|
|
||||||
add_appointment(args)
|
|
||||||
|
|
||||||
elif command == "get_appointment":
|
|
||||||
get_appointment(args)
|
|
||||||
|
|
||||||
elif command == "help":
|
|
||||||
if args:
|
|
||||||
command = args.pop(0)
|
|
||||||
|
|
||||||
if command == "add_appointment":
|
|
||||||
sys.exit(help_add_appointment())
|
|
||||||
|
|
||||||
elif command == "get_appointment":
|
|
||||||
sys.exit(help_get_appointment())
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error("Unknown command. Use help to check the list of available commands")
|
|
||||||
|
|
||||||
else:
|
|
||||||
sys.exit(show_usage())
|
|
||||||
|
|
||||||
# FIXME: testing command, not for production
|
|
||||||
elif command in testing_commands:
|
|
||||||
if command == "generate_dummy_appointment":
|
|
||||||
generate_dummy_appointment()
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error("Unknown command. Use help to check the list of available commands")
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error("No command provided. Use help to check the list of available commands")
|
|
||||||
|
|
||||||
except GetoptError as e:
|
|
||||||
logger.error("{}".format(e))
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error("Non-JSON encoded appointment passed as parameter")
|
|
||||||
@@ -5,9 +5,5 @@ DEFAULT_PISA_API_PORT = 9814
|
|||||||
# PISA-CLI
|
# PISA-CLI
|
||||||
DATA_FOLDER = "~/.pisa_btc/"
|
DATA_FOLDER = "~/.pisa_btc/"
|
||||||
|
|
||||||
CLIENT_LOG_FILE = "pisa-cli.log"
|
CLIENT_LOG_FILE = "cli.log"
|
||||||
APPOINTMENTS_FOLDER_NAME = "appointment_receipts"
|
APPOINTMENTS_FOLDER_NAME = "appointment_receipts"
|
||||||
|
|
||||||
CLI_PUBLIC_KEY = "cli_pk.der"
|
|
||||||
CLI_PRIVATE_KEY = "cli_sk.der"
|
|
||||||
PISA_PUBLIC_KEY = "pisa_pk.der"
|
|
||||||
|
|||||||
440
apps/cli/wt_cli.py
Normal file
440
apps/cli/wt_cli.py
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import binascii
|
||||||
|
from sys import argv
|
||||||
|
from getopt import getopt, GetoptError
|
||||||
|
from requests import ConnectTimeout, ConnectionError
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from apps.cli import config, LOG_PREFIX
|
||||||
|
from apps.cli.help import help_add_appointment, help_get_appointment
|
||||||
|
from apps.cli.blob import Blob
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
|
from common import constants
|
||||||
|
from common.logger import Logger
|
||||||
|
from common.appointment import Appointment
|
||||||
|
from common.cryptographer import Cryptographer
|
||||||
|
from common.tools import check_sha256_hex_format, check_locator_format, compute_locator
|
||||||
|
|
||||||
|
logger = Logger(actor="Client", log_name_prefix=LOG_PREFIX)
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
|
# FIXME: creating a simpler load_keys for the alpha. Client keys will not be necessary. PISA key is hardcoded.
|
||||||
|
# def load_keys(pisa_pk_path, cli_sk_path, cli_pk_path):
|
||||||
|
# """
|
||||||
|
# Loads all the keys required so sign, send, and verify the appointment.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# pisa_pk_path (:obj:`str`): path to the PISA public key file.
|
||||||
|
# cli_sk_path (:obj:`str`): path to the client private key file.
|
||||||
|
# cli_pk_path (:obj:`str`): path to the client public key file.
|
||||||
|
#
|
||||||
|
# Returns:
|
||||||
|
# :obj:`tuple` or ``None``: a three item tuple containing a pisa_pk object, cli_sk object and the cli_sk_der
|
||||||
|
# encoded key if all keys can be loaded. ``None`` otherwise.
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# pisa_pk_der = Cryptographer.load_key_file(pisa_pk_path)
|
||||||
|
# pisa_pk = Cryptographer.load_public_key_der(pisa_pk_der)
|
||||||
|
#
|
||||||
|
# if pisa_pk is None:
|
||||||
|
# logger.error("PISA's public key file not found. Please check your settings")
|
||||||
|
# return None
|
||||||
|
#
|
||||||
|
# cli_sk_der = Cryptographer.load_key_file(cli_sk_path)
|
||||||
|
# cli_sk = Cryptographer.load_private_key_der(cli_sk_der)
|
||||||
|
#
|
||||||
|
# if cli_sk is None:
|
||||||
|
# logger.error("Client's private key file not found. Please check your settings")
|
||||||
|
# return None
|
||||||
|
#
|
||||||
|
# cli_pk_der = Cryptographer.load_key_file(cli_pk_path)
|
||||||
|
#
|
||||||
|
# if cli_pk_der is None:
|
||||||
|
# logger.error("Client's public key file not found. Please check your settings")
|
||||||
|
# return None
|
||||||
|
#
|
||||||
|
# return pisa_pk, cli_sk, cli_pk_der
|
||||||
|
|
||||||
|
|
||||||
|
def load_keys():
|
||||||
|
PISA_PUBLIC_KEY = "3056301006072a8648ce3d020106052b8104000a0342000430053e39c53b8bcb43354a4ed886b8082af1d1e8fc14956e60ad0592bfdfab511b7e309f6ac83b7495462196692e145bf7b1a321e96ec8fc4d678719c77342da"
|
||||||
|
pisa_pk = Cryptographer.load_public_key_der(binascii.unhexlify(PISA_PUBLIC_KEY))
|
||||||
|
|
||||||
|
return pisa_pk
|
||||||
|
|
||||||
|
|
||||||
|
def add_appointment(args):
|
||||||
|
"""
|
||||||
|
Manages the add_appointment command, from argument parsing, trough sending the appointment to the tower, until
|
||||||
|
saving the appointment receipt.
|
||||||
|
|
||||||
|
The life cycle of the function is as follows:
|
||||||
|
- Load the add_appointment arguments
|
||||||
|
- Check that the given commitment_txid is correct (proper format and not missing)
|
||||||
|
- Check that the transaction is correct (not missing)
|
||||||
|
- Create the appointment locator and encrypted blob from the commitment_txid and the penalty_tx
|
||||||
|
- Load the client private key and sign the appointment
|
||||||
|
- Send the appointment to the tower
|
||||||
|
- Wait for the response
|
||||||
|
- Check the tower's response and signature
|
||||||
|
- Store the receipt (appointment + signature) on disk
|
||||||
|
|
||||||
|
If any of the above-mentioned steps fails, the method returns false, otherwise it returns true.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args (:obj:`list`): a list of arguments to pass to ``parse_add_appointment_args``. Must contain a json encoded
|
||||||
|
appointment, or the file option and the path to a file containing a json encoded appointment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`bool`: True if the appointment is accepted by the tower and the receipt is properly stored, false if any
|
||||||
|
error occurs during the process.
|
||||||
|
"""
|
||||||
|
# FIXME: creating a simpler load_keys for the alpha. Client keys will not be necessary. PISA key is hardcoded.
|
||||||
|
# pisa_pk, cli_sk, cli_pk_der = load_keys(
|
||||||
|
# config.get("PISA_PUBLIC_KEY"), config.get("CLI_PRIVATE_KEY"), config.get("CLI_PUBLIC_KEY")
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# try:
|
||||||
|
# hex_pk_der = binascii.hexlify(cli_pk_der)
|
||||||
|
#
|
||||||
|
# except binascii.Error as e:
|
||||||
|
# logger.error("Could not successfully encode public key as hex", error=str(e))
|
||||||
|
# return False
|
||||||
|
pisa_pk = load_keys()
|
||||||
|
|
||||||
|
if pisa_pk is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get appointment data from user.
|
||||||
|
appointment_data = parse_add_appointment_args(args)
|
||||||
|
|
||||||
|
if appointment_data is None:
|
||||||
|
logger.error("The provided appointment JSON is empty")
|
||||||
|
return False
|
||||||
|
|
||||||
|
valid_txid = check_sha256_hex_format(appointment_data.get("tx_id"))
|
||||||
|
|
||||||
|
if not valid_txid:
|
||||||
|
logger.error("The provided txid is not valid")
|
||||||
|
return False
|
||||||
|
|
||||||
|
tx_id = appointment_data.get("tx_id")
|
||||||
|
tx = appointment_data.get("tx")
|
||||||
|
|
||||||
|
if None not in [tx_id, tx]:
|
||||||
|
appointment_data["locator"] = compute_locator(tx_id)
|
||||||
|
appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(tx), tx_id)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error("Appointment data is missing some fields")
|
||||||
|
return False
|
||||||
|
|
||||||
|
appointment = Appointment.from_dict(appointment_data)
|
||||||
|
|
||||||
|
# FIXME: getting rid of the client-side signature for the alpha. A proper authentication is required.
|
||||||
|
# signature = Cryptographer.sign(appointment.serialize(), cli_sk)
|
||||||
|
#
|
||||||
|
# if not (appointment and signature):
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")}
|
||||||
|
data = {"appointment": appointment.to_dict()}
|
||||||
|
|
||||||
|
# Send appointment to the server.
|
||||||
|
server_response = post_appointment(data)
|
||||||
|
if server_response is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
response_json = process_post_appointment_response(server_response)
|
||||||
|
|
||||||
|
if response_json is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
signature = response_json.get("signature")
|
||||||
|
# Check that the server signed the appointment as it should.
|
||||||
|
if signature is None:
|
||||||
|
logger.error("The response does not contain the signature of the appointment")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not Cryptographer.verify(appointment.serialize(), signature, pisa_pk):
|
||||||
|
logger.error("The returned appointment's signature is invalid")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info("Appointment accepted and signed by PISA")
|
||||||
|
|
||||||
|
# All good, store appointment and signature
|
||||||
|
return save_appointment_receipt(appointment.to_dict(), signature)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_add_appointment_args(args):
|
||||||
|
"""
|
||||||
|
Parses the arguments of the add_appointment command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args (:obj:`list`): a list of arguments to pass to ``parse_add_appointment_args``. Must contain a json encoded
|
||||||
|
appointment, or the file option and the path to a file containing a json encoded appointment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`dict` or :obj:`None`: A dictionary containing the appointment data if it can be loaded. ``None``
|
||||||
|
otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_help = "Use 'help add_appointment' for help of how to use the command"
|
||||||
|
|
||||||
|
if not args:
|
||||||
|
logger.error("No appointment data provided. " + use_help)
|
||||||
|
return None
|
||||||
|
|
||||||
|
arg_opt = args.pop(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if arg_opt in ["-h", "--help"]:
|
||||||
|
sys.exit(help_add_appointment())
|
||||||
|
|
||||||
|
if arg_opt in ["-f", "--file"]:
|
||||||
|
fin = args.pop(0)
|
||||||
|
if not os.path.isfile(fin):
|
||||||
|
logger.error("Can't find file", filename=fin)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(fin) as f:
|
||||||
|
appointment_data = json.load(f)
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
logger.error("I/O error", errno=e.errno, error=e.strerror)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
appointment_data = json.loads(arg_opt)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error("Non-JSON encoded data provided as appointment. " + use_help)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return appointment_data
|
||||||
|
|
||||||
|
|
||||||
|
def post_appointment(data):
|
||||||
|
"""
|
||||||
|
Sends appointment data to add_appointment endpoint to be processed by the tower.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:obj:`dict`): a dictionary containing three fields: an appointment, the client-side signature, and the
|
||||||
|
der-encoded client public key.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`dict` or ``None``: a json-encoded dictionary with the server response if the data can be posted.
|
||||||
|
None otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info("Sending appointment to PISA")
|
||||||
|
|
||||||
|
try:
|
||||||
|
add_appointment_endpoint = "http://{}:{}".format(pisa_api_server, pisa_api_port)
|
||||||
|
return requests.post(url=add_appointment_endpoint, json=json.dumps(data), timeout=5)
|
||||||
|
|
||||||
|
except ConnectTimeout:
|
||||||
|
logger.error("Can't connect to PISA API. Connection timeout")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except ConnectionError:
|
||||||
|
logger.error("Can't connect to PISA API. Server cannot be reached")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def process_post_appointment_response(response):
|
||||||
|
"""
|
||||||
|
Processes the server response to an add_appointment request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response (:obj:`requests.models.Response`): a ``Response` object obtained from the sent request.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`dict` or :obj:`None`: a dictionary containing the tower's response data if it can be properly parsed and
|
||||||
|
the response type is ``HTTP_OK``. ``None`` otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error("The response was not valid JSON")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if response.status_code != constants.HTTP_OK:
|
||||||
|
if "error" not in response_json:
|
||||||
|
logger.error(
|
||||||
|
"The server returned an error status code but no error description", status_code=response.status_code
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error = response_json["error"]
|
||||||
|
logger.error(
|
||||||
|
"The server returned an error status code with an error description",
|
||||||
|
status_code=response.status_code,
|
||||||
|
description=error,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return response_json
|
||||||
|
|
||||||
|
|
||||||
|
def save_appointment_receipt(appointment, signature):
|
||||||
|
"""
|
||||||
|
Saves an appointment receipt to disk. A receipt consists in an appointment and a signature from the tower.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
appointment (:obj:`Appointment <common.appointment.Appointment>`): the appointment to be saved on disk.
|
||||||
|
signature (:obj:`str`): the signature of the appointment performed by the tower.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`bool`: True if the appointment if properly saved, false otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IOError: if an error occurs whilst writing the file on disk.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create the appointments directory if it doesn't already exist
|
||||||
|
os.makedirs(config.get("APPOINTMENTS_FOLDER_NAME"), exist_ok=True)
|
||||||
|
|
||||||
|
timestamp = int(time.time())
|
||||||
|
locator = appointment["locator"]
|
||||||
|
uuid = uuid4().hex # prevent filename collisions
|
||||||
|
|
||||||
|
filename = "{}/appointment-{}-{}-{}.json".format(config.get("APPOINTMENTS_FOLDER_NAME"), timestamp, locator, uuid)
|
||||||
|
data = {"appointment": appointment, "signature": signature}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
logger.info("Appointment saved at {}".format(filename))
|
||||||
|
return True
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
logger.error("There was an error while saving the appointment", error=e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_appointment(locator):
|
||||||
|
"""
|
||||||
|
Gets information about an appointment from the tower.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
locator (:obj:`str`): the appointment locator used to identify it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`dict` or :obj:`None`: a dictionary containing thew appointment data if the locator is valid and the tower
|
||||||
|
responds. ``None`` otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
valid_locator = check_locator_format(locator)
|
||||||
|
|
||||||
|
if not valid_locator:
|
||||||
|
logger.error("The provided locator is not valid", locator=locator)
|
||||||
|
return None
|
||||||
|
|
||||||
|
get_appointment_endpoint = "http://{}:{}/get_appointment".format(pisa_api_server, pisa_api_port)
|
||||||
|
parameters = "?locator={}".format(locator)
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.get(url=get_appointment_endpoint + parameters, timeout=5)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
except ConnectTimeout:
|
||||||
|
logger.error("Can't connect to PISA API. Connection timeout")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except ConnectionError:
|
||||||
|
logger.error("Can't connect to PISA API. Server cannot be reached")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def show_usage():
|
||||||
|
return (
|
||||||
|
"USAGE: "
|
||||||
|
"\n\tpython pisa-cli.py [global options] command [command options] [arguments]"
|
||||||
|
"\n\nCOMMANDS:"
|
||||||
|
"\n\tadd_appointment \tRegisters a json formatted appointment to the PISA server."
|
||||||
|
"\n\tget_appointment \tGets json formatted data about an appointment from the PISA server."
|
||||||
|
"\n\thelp \t\t\tShows a list of commands or help for a specific command."
|
||||||
|
"\n\nGLOBAL OPTIONS:"
|
||||||
|
"\n\t-s, --server \tAPI server where to send the requests. Defaults to btc.pisa.watch (modifiable in "
|
||||||
|
"__init__.py)"
|
||||||
|
"\n\t-p, --port \tAPI port where to send the requests. Defaults to 9814 (modifiable in __init__.py)"
|
||||||
|
"\n\t-d, --debug \tshows debug information and stores it in pisa_cli.log"
|
||||||
|
"\n\t-h --help \tshows this message."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pisa_api_server = config.get("DEFAULT_PISA_API_SERVER")
|
||||||
|
pisa_api_port = config.get("DEFAULT_PISA_API_PORT")
|
||||||
|
commands = ["add_appointment", "get_appointment", "help"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt(argv[1:], "s:p:h", ["server", "port", "help"])
|
||||||
|
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt in ["-s", "server"]:
|
||||||
|
if arg:
|
||||||
|
pisa_api_server = arg
|
||||||
|
|
||||||
|
if opt in ["-p", "--port"]:
|
||||||
|
if arg:
|
||||||
|
pisa_api_port = int(arg)
|
||||||
|
|
||||||
|
if opt in ["-h", "--help"]:
|
||||||
|
sys.exit(show_usage())
|
||||||
|
|
||||||
|
if args:
|
||||||
|
command = args.pop(0)
|
||||||
|
|
||||||
|
if command in commands:
|
||||||
|
if command == "add_appointment":
|
||||||
|
add_appointment(args)
|
||||||
|
|
||||||
|
elif command == "get_appointment":
|
||||||
|
if not args:
|
||||||
|
logger.error("No arguments were given")
|
||||||
|
|
||||||
|
else:
|
||||||
|
arg_opt = args.pop(0)
|
||||||
|
|
||||||
|
if arg_opt in ["-h", "--help"]:
|
||||||
|
sys.exit(help_get_appointment())
|
||||||
|
|
||||||
|
appointment_data = get_appointment(arg_opt)
|
||||||
|
if appointment_data:
|
||||||
|
print(appointment_data)
|
||||||
|
|
||||||
|
elif command == "help":
|
||||||
|
if args:
|
||||||
|
command = args.pop(0)
|
||||||
|
|
||||||
|
if command == "add_appointment":
|
||||||
|
sys.exit(help_add_appointment())
|
||||||
|
|
||||||
|
elif command == "get_appointment":
|
||||||
|
sys.exit(help_get_appointment())
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error("Unknown command. Use help to check the list of available commands")
|
||||||
|
|
||||||
|
else:
|
||||||
|
sys.exit(show_usage())
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error("Unknown command. Use help to check the list of available commands")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error("No command provided. Use help to check the list of available commands")
|
||||||
|
|
||||||
|
except GetoptError as e:
|
||||||
|
logger.error("{}".format(e))
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error("Non-JSON encoded appointment passed as parameter")
|
||||||
@@ -10,9 +10,8 @@ from cryptography.hazmat.primitives.serialization import load_der_public_key, lo
|
|||||||
from cryptography.exceptions import InvalidSignature
|
from cryptography.exceptions import InvalidSignature
|
||||||
from common.tools import check_sha256_hex_format
|
from common.tools import check_sha256_hex_format
|
||||||
|
|
||||||
from common.logger import Logger
|
# FIXME: Common has not log file, so it needs to log in the same log as the caller. This is a temporary fix.
|
||||||
|
logger = None
|
||||||
logger = Logger("Cryptographer")
|
|
||||||
|
|
||||||
|
|
||||||
class Cryptographer:
|
class Cryptographer:
|
||||||
@@ -39,12 +38,10 @@ class Cryptographer:
|
|||||||
|
|
||||||
if len(data) % 2:
|
if len(data) % 2:
|
||||||
error = "Incorrect (Odd-length) value"
|
error = "Incorrect (Odd-length) value"
|
||||||
logger.error(error, data=data)
|
|
||||||
raise ValueError(error)
|
raise ValueError(error)
|
||||||
|
|
||||||
if not check_sha256_hex_format(secret):
|
if not check_sha256_hex_format(secret):
|
||||||
error = "Secret must be a 32-byte hex value (64 hex chars)"
|
error = "Secret must be a 32-byte hex value (64 hex chars)"
|
||||||
logger.error(error, secret=secret)
|
|
||||||
raise ValueError(error)
|
raise ValueError(error)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -146,6 +143,35 @@ class Cryptographer:
|
|||||||
|
|
||||||
return blob
|
return blob
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_key_file(file_path):
|
||||||
|
"""
|
||||||
|
Loads a key from a key file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (:obj:`str`): the path to the key file to be loaded.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`bytes` or :obj:`None`: the key file data if the file can be found and read. ``None`` otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(file_path, str):
|
||||||
|
logger.error("Key file path was expected, {} received".format(type(file_path)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "rb") as key_file:
|
||||||
|
key = key_file.read()
|
||||||
|
return key
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("Key file not found. Please check your settings")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
logger.error("I/O error({}): {}".format(e.errno, e.strerror))
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_public_key_der(pk_der):
|
def load_public_key_der(pk_der):
|
||||||
"""
|
"""
|
||||||
@@ -199,7 +225,7 @@ class Cryptographer:
|
|||||||
return sk
|
return sk
|
||||||
|
|
||||||
except UnsupportedAlgorithm:
|
except UnsupportedAlgorithm:
|
||||||
raise ValueError("Could not deserialize the private key (unsupported algorithm).")
|
logger.error("Could not deserialize the private key (unsupported algorithm)")
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.error("The provided data cannot be deserialized (wrong size or format)")
|
logger.error("The provided data cannot be deserialized (wrong size or format)")
|
||||||
@@ -207,6 +233,8 @@ class Cryptographer:
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
logger.error("The provided data cannot be deserialized (wrong type)")
|
logger.error("The provided data cannot be deserialized (wrong type)")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sign(data, sk, rtype="str"):
|
def sign(data, sk, rtype="str"):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
from common.constants import LOCATOR_LEN_HEX
|
from common.constants import LOCATOR_LEN_HEX
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ from common.appointment import Appointment
|
|||||||
from pisa.block_processor import BlockProcessor
|
from pisa.block_processor import BlockProcessor
|
||||||
|
|
||||||
logger = Logger(actor="Inspector", log_name_prefix=LOG_PREFIX)
|
logger = Logger(actor="Inspector", log_name_prefix=LOG_PREFIX)
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
# FIXME: The inspector logs the wrong messages sent form the users. A possible attack surface would be to send a really
|
# FIXME: The inspector logs the wrong messages sent form the users. A possible attack surface would be to send a really
|
||||||
# long field that, even if not accepted by PISA, would be stored in the logs. This is a possible DoS surface
|
# long field that, even if not accepted by PISA, would be stored in the logs. This is a possible DoS surface
|
||||||
@@ -58,8 +60,8 @@ class Inspector:
|
|||||||
rcode, message = self.check_to_self_delay(appointment_data.get("to_self_delay"))
|
rcode, message = self.check_to_self_delay(appointment_data.get("to_self_delay"))
|
||||||
if rcode == 0:
|
if rcode == 0:
|
||||||
rcode, message = self.check_blob(appointment_data.get("encrypted_blob"))
|
rcode, message = self.check_blob(appointment_data.get("encrypted_blob"))
|
||||||
if rcode == 0:
|
# if rcode == 0:
|
||||||
rcode, message = self.check_appointment_signature(appointment_data, signature, public_key)
|
# rcode, message = self.check_appointment_signature(appointment_data, signature, public_key)
|
||||||
|
|
||||||
if rcode == 0:
|
if rcode == 0:
|
||||||
r = Appointment.from_dict(appointment_data)
|
r = Appointment.from_dict(appointment_data)
|
||||||
@@ -336,11 +338,16 @@ class Inspector:
|
|||||||
rcode = errors.APPOINTMENT_EMPTY_FIELD
|
rcode = errors.APPOINTMENT_EMPTY_FIELD
|
||||||
message = "empty signature received"
|
message = "empty signature received"
|
||||||
|
|
||||||
pk = Cryptographer.load_public_key_der(unhexlify(pk_der))
|
elif pk_der is None:
|
||||||
valid_sig = Cryptographer.verify(Appointment.from_dict(appointment_data).serialize(), signature, pk)
|
rcode = errors.APPOINTMENT_EMPTY_FIELD
|
||||||
|
message = "empty public key received"
|
||||||
|
|
||||||
if not valid_sig:
|
else:
|
||||||
rcode = errors.APPOINTMENT_INVALID_SIGNATURE
|
pk = Cryptographer.load_public_key_der(unhexlify(pk_der))
|
||||||
message = "invalid signature"
|
valid_sig = Cryptographer.verify(Appointment.from_dict(appointment_data).serialize(), signature, pk)
|
||||||
|
|
||||||
|
if not valid_sig:
|
||||||
|
rcode = errors.APPOINTMENT_INVALID_SIGNATURE
|
||||||
|
message = "invalid signature"
|
||||||
|
|
||||||
return rcode, message
|
return rcode, message
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ from getopt import getopt
|
|||||||
from sys import argv, exit
|
from sys import argv, exit
|
||||||
from signal import signal, SIGINT, SIGQUIT, SIGTERM
|
from signal import signal, SIGINT, SIGQUIT, SIGTERM
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
from common.logger import Logger
|
from common.logger import Logger
|
||||||
|
from common.cryptographer import Cryptographer
|
||||||
|
|
||||||
from pisa import config, LOG_PREFIX
|
from pisa import config, LOG_PREFIX
|
||||||
from pisa.api import API
|
from pisa.api import API
|
||||||
@@ -15,6 +17,7 @@ from pisa.block_processor import BlockProcessor
|
|||||||
from pisa.tools import can_connect_to_bitcoind, in_correct_network
|
from pisa.tools import can_connect_to_bitcoind, in_correct_network
|
||||||
|
|
||||||
logger = Logger(actor="Daemon", log_name_prefix=LOG_PREFIX)
|
logger = Logger(actor="Daemon", log_name_prefix=LOG_PREFIX)
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
def handle_signals(signal_received, frame):
|
def handle_signals(signal_received, frame):
|
||||||
@@ -44,8 +47,9 @@ def main():
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
with open(config.get("PISA_SECRET_KEY"), "rb") as key_file:
|
secret_key_der = Cryptographer.load_key_file(config.get("PISA_SECRET_KEY"))
|
||||||
secret_key_der = key_file.read()
|
if not secret_key_der:
|
||||||
|
raise IOError("PISA private key can't be loaded")
|
||||||
|
|
||||||
watcher = Watcher(db_manager, Responder(db_manager), secret_key_der, config)
|
watcher = Watcher(db_manager, Responder(db_manager), secret_key_der, config)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from uuid import uuid4
|
|||||||
from queue import Queue
|
from queue import Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
from common.appointment import Appointment
|
from common.appointment import Appointment
|
||||||
from common.tools import compute_locator
|
from common.tools import compute_locator
|
||||||
@@ -13,6 +14,7 @@ from pisa.cleaner import Cleaner
|
|||||||
from pisa.block_processor import BlockProcessor
|
from pisa.block_processor import BlockProcessor
|
||||||
|
|
||||||
logger = Logger(actor="Watcher", log_name_prefix=LOG_PREFIX)
|
logger = Logger(actor="Watcher", log_name_prefix=LOG_PREFIX)
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
class Watcher:
|
class Watcher:
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import responses
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
from binascii import hexlify
|
|
||||||
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
|
||||||
|
|
||||||
from common.appointment import Appointment
|
|
||||||
from common.cryptographer import Cryptographer
|
|
||||||
|
|
||||||
import apps.cli.pisa_cli as pisa_cli
|
|
||||||
from test.apps.cli.unit.conftest import get_random_value_hex
|
|
||||||
|
|
||||||
# dummy keys for the tests
|
|
||||||
pisa_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
|
|
||||||
pisa_pk = pisa_sk.public_key()
|
|
||||||
|
|
||||||
other_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
|
|
||||||
|
|
||||||
pisa_sk_der = pisa_sk.private_bytes(
|
|
||||||
encoding=serialization.Encoding.DER,
|
|
||||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
||||||
encryption_algorithm=serialization.NoEncryption(),
|
|
||||||
)
|
|
||||||
pisa_pk_der = pisa_pk.public_bytes(
|
|
||||||
encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
other_sk_der = other_sk.private_bytes(
|
|
||||||
encoding=serialization.Encoding.DER,
|
|
||||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
||||||
encryption_algorithm=serialization.NoEncryption(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Replace the key in the module with a key we control for the tests
|
|
||||||
pisa_cli.pisa_public_key = pisa_pk
|
|
||||||
# Replace endpoint with dummy one
|
|
||||||
pisa_cli.pisa_api_server = "dummy.com"
|
|
||||||
pisa_cli.pisa_api_port = 12345
|
|
||||||
pisa_endpoint = "http://{}:{}/".format(pisa_cli.pisa_api_server, pisa_cli.pisa_api_port)
|
|
||||||
|
|
||||||
dummy_appointment_request = {
|
|
||||||
"tx": get_random_value_hex(192),
|
|
||||||
"tx_id": get_random_value_hex(32),
|
|
||||||
"start_time": 1500,
|
|
||||||
"end_time": 50000,
|
|
||||||
"to_self_delay": 200,
|
|
||||||
}
|
|
||||||
|
|
||||||
# This is the format appointment turns into once it hits "add_appointment"
|
|
||||||
dummy_appointment_full = {
|
|
||||||
"locator": get_random_value_hex(16),
|
|
||||||
"start_time": 1500,
|
|
||||||
"end_time": 50000,
|
|
||||||
"to_self_delay": 200,
|
|
||||||
"encrypted_blob": get_random_value_hex(120),
|
|
||||||
}
|
|
||||||
|
|
||||||
dummy_appointment = Appointment.from_dict(dummy_appointment_full)
|
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_pisa_sk_der(*args):
|
|
||||||
return pisa_sk_der
|
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_pisa_pk_der(*args):
|
|
||||||
return pisa_pk_der
|
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_hex_pk_der(*args):
|
|
||||||
return hexlify(get_dummy_pisa_pk_der())
|
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_signature(*args):
|
|
||||||
sk = Cryptographer.load_private_key_der(pisa_sk_der)
|
|
||||||
return Cryptographer.sign(dummy_appointment.serialize(), sk)
|
|
||||||
|
|
||||||
|
|
||||||
def get_bad_signature(*args):
|
|
||||||
sk = Cryptographer.load_private_key_der(other_sk_der)
|
|
||||||
return Cryptographer.sign(dummy_appointment.serialize(), sk)
|
|
||||||
|
|
||||||
|
|
||||||
def valid_sig(*args):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def invalid_sig(*args):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_add_appointment(monkeypatch):
|
|
||||||
# Simulate a request to add_appointment for dummy_appointment, make sure that the right endpoint is requested
|
|
||||||
# and the return value is True
|
|
||||||
|
|
||||||
# Make sure the test uses the dummy signature
|
|
||||||
monkeypatch.setattr(pisa_cli, "get_appointment_signature", get_dummy_signature)
|
|
||||||
monkeypatch.setattr(pisa_cli, "get_pk", get_dummy_hex_pk_der)
|
|
||||||
monkeypatch.setattr(pisa_cli, "check_signature", valid_sig)
|
|
||||||
|
|
||||||
response = {"locator": dummy_appointment.to_dict()["locator"], "signature": get_dummy_signature()}
|
|
||||||
|
|
||||||
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
|
||||||
|
|
||||||
result = pisa_cli.add_appointment([json.dumps(dummy_appointment_request)])
|
|
||||||
|
|
||||||
assert len(responses.calls) == 1
|
|
||||||
assert responses.calls[0].request.url == pisa_endpoint
|
|
||||||
|
|
||||||
assert result
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_add_appointment_with_invalid_signature(monkeypatch):
|
|
||||||
# Simulate a request to add_appointment for dummy_appointment, but sign with a different key,
|
|
||||||
# make sure that the right endpoint is requested, but the return value is False
|
|
||||||
|
|
||||||
# Make sure the test uses the bad dummy signature
|
|
||||||
monkeypatch.setattr(pisa_cli, "get_appointment_signature", get_bad_signature)
|
|
||||||
monkeypatch.setattr(pisa_cli, "get_pk", get_dummy_hex_pk_der)
|
|
||||||
monkeypatch.setattr(pisa_cli, "check_signature", invalid_sig)
|
|
||||||
|
|
||||||
response = {
|
|
||||||
"locator": dummy_appointment.to_dict()["locator"],
|
|
||||||
"signature": get_bad_signature(), # Sign with a bad key
|
|
||||||
}
|
|
||||||
|
|
||||||
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
|
||||||
|
|
||||||
result = pisa_cli.add_appointment([json.dumps(dummy_appointment_request)])
|
|
||||||
|
|
||||||
assert result is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_key_file_data():
|
|
||||||
# If file exists and has data in it, function should work.
|
|
||||||
with open("key_test_file", "w+b") as f:
|
|
||||||
f.write(pisa_sk_der)
|
|
||||||
|
|
||||||
appt_data = pisa_cli.load_key_file_data("key_test_file")
|
|
||||||
assert appt_data
|
|
||||||
|
|
||||||
os.remove("key_test_file")
|
|
||||||
|
|
||||||
# If file doesn't exist, function should fail.
|
|
||||||
with pytest.raises(FileNotFoundError):
|
|
||||||
assert pisa_cli.load_key_file_data("nonexistent_file")
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_signed_appointment(monkeypatch):
|
|
||||||
appointments_folder = "test_appointments_receipts"
|
|
||||||
pisa_cli.config["APPOINTMENTS_FOLDER_NAME"] = appointments_folder
|
|
||||||
|
|
||||||
pisa_cli.save_signed_appointment(dummy_appointment.to_dict(), get_dummy_signature())
|
|
||||||
|
|
||||||
# In folder "Appointments," grab all files and print them.
|
|
||||||
files = os.listdir(appointments_folder)
|
|
||||||
|
|
||||||
found = False
|
|
||||||
for f in files:
|
|
||||||
if dummy_appointment.to_dict().get("locator") in f:
|
|
||||||
found = True
|
|
||||||
|
|
||||||
assert found
|
|
||||||
|
|
||||||
# If "appointments" directory doesn't exist, function should create it.
|
|
||||||
assert os.path.exists(appointments_folder)
|
|
||||||
|
|
||||||
# Delete test directory once we're done.
|
|
||||||
shutil.rmtree(appointments_folder)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_add_appointment_args():
|
|
||||||
# If no args are passed, function should fail.
|
|
||||||
appt_data = pisa_cli.parse_add_appointment_args(None)
|
|
||||||
assert not appt_data
|
|
||||||
|
|
||||||
# If file doesn't exist, function should fail.
|
|
||||||
appt_data = pisa_cli.parse_add_appointment_args(["-f", "nonexistent_file"])
|
|
||||||
assert not appt_data
|
|
||||||
|
|
||||||
# If file exists and has data in it, function should work.
|
|
||||||
with open("appt_test_file", "w") as f:
|
|
||||||
json.dump(dummy_appointment_request, f)
|
|
||||||
|
|
||||||
appt_data = pisa_cli.parse_add_appointment_args(["-f", "appt_test_file"])
|
|
||||||
assert appt_data
|
|
||||||
|
|
||||||
os.remove("appt_test_file")
|
|
||||||
|
|
||||||
# If appointment json is passed in, function should work.
|
|
||||||
appt_data = pisa_cli.parse_add_appointment_args([json.dumps(dummy_appointment_request)])
|
|
||||||
assert appt_data
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_post_data_to_add_appointment_endpoint():
|
|
||||||
response = {
|
|
||||||
"locator": dummy_appointment.to_dict()["locator"],
|
|
||||||
"signature": Cryptographer.sign(dummy_appointment.serialize(), pisa_sk),
|
|
||||||
}
|
|
||||||
|
|
||||||
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
|
||||||
|
|
||||||
response = pisa_cli.post_data_to_add_appointment_endpoint(json.dumps(dummy_appointment_request))
|
|
||||||
|
|
||||||
assert len(responses.calls) == 1
|
|
||||||
assert responses.calls[0].request.url == pisa_endpoint
|
|
||||||
|
|
||||||
assert response
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_signature(monkeypatch):
|
|
||||||
# Make sure the test uses the right dummy key instead of loading it from disk
|
|
||||||
monkeypatch.setattr(pisa_cli, "load_key_file_data", get_dummy_pisa_pk_der)
|
|
||||||
|
|
||||||
valid = pisa_cli.check_signature(get_dummy_signature(), dummy_appointment)
|
|
||||||
|
|
||||||
assert valid
|
|
||||||
|
|
||||||
valid = pisa_cli.check_signature(get_bad_signature(), dummy_appointment)
|
|
||||||
|
|
||||||
assert not valid
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_get_appointment():
|
|
||||||
# Response of get_appointment endpoint is an appointment with status added to it.
|
|
||||||
dummy_appointment_full["status"] = "being_watched"
|
|
||||||
response = dummy_appointment_full
|
|
||||||
|
|
||||||
request_url = "{}get_appointment?locator={}".format(pisa_endpoint, response.get("locator"))
|
|
||||||
responses.add(responses.GET, request_url, json=response, status=200)
|
|
||||||
|
|
||||||
result = pisa_cli.get_appointment([response.get("locator")])
|
|
||||||
|
|
||||||
assert len(responses.calls) == 1
|
|
||||||
assert responses.calls[0].request.url == request_url
|
|
||||||
|
|
||||||
assert result.get("locator") == response.get("locator")
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_get_appointment_err():
|
|
||||||
locator = get_random_value_hex(32)
|
|
||||||
|
|
||||||
# Test that get_appointment handles a connection error appropriately.
|
|
||||||
request_url = "{}get_appointment?locator=".format(pisa_endpoint, locator)
|
|
||||||
responses.add(responses.GET, request_url, body=ConnectionError())
|
|
||||||
|
|
||||||
assert not pisa_cli.get_appointment([locator])
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_appointment_signature(monkeypatch):
|
|
||||||
# Make sure the test uses the right dummy key instead of loading it from disk
|
|
||||||
monkeypatch.setattr(pisa_cli, "load_key_file_data", get_dummy_pisa_sk_der)
|
|
||||||
|
|
||||||
signature = pisa_cli.get_appointment_signature(dummy_appointment)
|
|
||||||
|
|
||||||
assert isinstance(signature, str)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_pk(monkeypatch):
|
|
||||||
# Make sure the test uses the right dummy key instead of loading it from disk
|
|
||||||
monkeypatch.setattr(pisa_cli, "load_key_file_data", get_dummy_pisa_pk_der)
|
|
||||||
|
|
||||||
pk = pisa_cli.get_pk()
|
|
||||||
|
|
||||||
assert isinstance(pk, bytes)
|
|
||||||
250
test/apps/cli/unit/test_wt_cli.py
Normal file
250
test/apps/cli/unit/test_wt_cli.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import responses
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from binascii import hexlify
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
|
from common.logger import Logger
|
||||||
|
from common.tools import compute_locator
|
||||||
|
from common.appointment import Appointment
|
||||||
|
from common.cryptographer import Cryptographer
|
||||||
|
|
||||||
|
from apps.cli.blob import Blob
|
||||||
|
import apps.cli.wt_cli as wt_cli
|
||||||
|
from test.apps.cli.unit.conftest import get_random_value_hex
|
||||||
|
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=wt_cli.LOG_PREFIX)
|
||||||
|
|
||||||
|
# dummy keys for the tests
|
||||||
|
dummy_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
|
||||||
|
dummy_pk = dummy_sk.public_key()
|
||||||
|
another_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
|
||||||
|
|
||||||
|
dummy_sk_der = dummy_sk.private_bytes(
|
||||||
|
encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
dummy_pk_der = dummy_pk.public_bytes(
|
||||||
|
encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Replace the key in the module with a key we control for the tests
|
||||||
|
wt_cli.pisa_public_key = dummy_pk
|
||||||
|
# Replace endpoint with dummy one
|
||||||
|
wt_cli.pisa_api_server = "dummy.com"
|
||||||
|
wt_cli.pisa_api_port = 12345
|
||||||
|
pisa_endpoint = "http://{}:{}/".format(wt_cli.pisa_api_server, wt_cli.pisa_api_port)
|
||||||
|
|
||||||
|
dummy_appointment_request = {
|
||||||
|
"tx": get_random_value_hex(192),
|
||||||
|
"tx_id": get_random_value_hex(32),
|
||||||
|
"start_time": 1500,
|
||||||
|
"end_time": 50000,
|
||||||
|
"to_self_delay": 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
# This is the format appointment turns into once it hits "add_appointment"
|
||||||
|
dummy_appointment_full = {
|
||||||
|
"locator": compute_locator(dummy_appointment_request.get("tx_id")),
|
||||||
|
"start_time": dummy_appointment_request.get("start_time"),
|
||||||
|
"end_time": dummy_appointment_request.get("end_time"),
|
||||||
|
"to_self_delay": dummy_appointment_request.get("to_self_delay"),
|
||||||
|
"encrypted_blob": Cryptographer.encrypt(
|
||||||
|
Blob(dummy_appointment_request.get("tx")), dummy_appointment_request.get("tx_id")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy_appointment = Appointment.from_dict(dummy_appointment_full)
|
||||||
|
|
||||||
|
|
||||||
|
def load_dummy_keys(*args):
|
||||||
|
# return dummy_pk, dummy_sk, dummy_pk_der
|
||||||
|
return dummy_pk
|
||||||
|
|
||||||
|
|
||||||
|
def get_dummy_pisa_pk_der(*args):
|
||||||
|
return dummy_pk_der
|
||||||
|
|
||||||
|
|
||||||
|
def get_dummy_hex_pk_der(*args):
|
||||||
|
return hexlify(get_dummy_pisa_pk_der())
|
||||||
|
|
||||||
|
|
||||||
|
def get_dummy_signature(*args):
|
||||||
|
return Cryptographer.sign(dummy_appointment.serialize(), dummy_sk)
|
||||||
|
|
||||||
|
|
||||||
|
def get_bad_signature(*args):
|
||||||
|
return Cryptographer.sign(dummy_appointment.serialize(), another_sk)
|
||||||
|
|
||||||
|
|
||||||
|
# def test_load_keys():
|
||||||
|
# # Let's first create a private key and public key files
|
||||||
|
# private_key_file_path = "sk_test_file"
|
||||||
|
# public_key_file_path = "pk_test_file"
|
||||||
|
# with open(private_key_file_path, "wb") as f:
|
||||||
|
# f.write(dummy_sk_der)
|
||||||
|
# with open(public_key_file_path, "wb") as f:
|
||||||
|
# f.write(dummy_pk_der)
|
||||||
|
#
|
||||||
|
# # Now we can test the function passing the using this files (we'll use the same pk for both)
|
||||||
|
# r = wt_cli.load_keys(public_key_file_path, private_key_file_path, public_key_file_path)
|
||||||
|
# assert isinstance(r, tuple)
|
||||||
|
# assert len(r) == 3
|
||||||
|
#
|
||||||
|
# # If any param does not match we should get None as result
|
||||||
|
# assert wt_cli.load_keys(None, private_key_file_path, public_key_file_path) is None
|
||||||
|
# assert wt_cli.load_keys(public_key_file_path, None, public_key_file_path) is None
|
||||||
|
# assert wt_cli.load_keys(public_key_file_path, private_key_file_path, None) is None
|
||||||
|
#
|
||||||
|
# # The same should happen if we pass a public key where a private should be, for instance
|
||||||
|
# assert wt_cli.load_keys(private_key_file_path, public_key_file_path, private_key_file_path) is None
|
||||||
|
#
|
||||||
|
# os.remove(private_key_file_path)
|
||||||
|
# os.remove(public_key_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: 90-add-more-add-appointment-tests
|
||||||
|
@responses.activate
|
||||||
|
def test_add_appointment(monkeypatch):
|
||||||
|
# Simulate a request to add_appointment for dummy_appointment, make sure that the right endpoint is requested
|
||||||
|
# and the return value is True
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", load_dummy_keys)
|
||||||
|
|
||||||
|
response = {"locator": dummy_appointment.locator, "signature": get_dummy_signature()}
|
||||||
|
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
||||||
|
result = wt_cli.add_appointment([json.dumps(dummy_appointment_request)])
|
||||||
|
|
||||||
|
assert len(responses.calls) == 1
|
||||||
|
assert responses.calls[0].request.url == pisa_endpoint
|
||||||
|
assert result
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_add_appointment_with_invalid_signature(monkeypatch):
|
||||||
|
# Simulate a request to add_appointment for dummy_appointment, but sign with a different key,
|
||||||
|
# make sure that the right endpoint is requested, but the return value is False
|
||||||
|
|
||||||
|
# Make sure the test uses the bad dummy signature
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", load_dummy_keys)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"locator": dummy_appointment.to_dict()["locator"],
|
||||||
|
"signature": get_bad_signature(), # Sign with a bad key
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
||||||
|
result = wt_cli.add_appointment([json.dumps(dummy_appointment_request)])
|
||||||
|
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_add_appointment_args():
|
||||||
|
# If no args are passed, function should fail.
|
||||||
|
appt_data = wt_cli.parse_add_appointment_args(None)
|
||||||
|
assert not appt_data
|
||||||
|
|
||||||
|
# If file doesn't exist, function should fail.
|
||||||
|
appt_data = wt_cli.parse_add_appointment_args(["-f", "nonexistent_file"])
|
||||||
|
assert not appt_data
|
||||||
|
|
||||||
|
# If file exists and has data in it, function should work.
|
||||||
|
with open("appt_test_file", "w") as f:
|
||||||
|
json.dump(dummy_appointment_request, f)
|
||||||
|
|
||||||
|
appt_data = wt_cli.parse_add_appointment_args(["-f", "appt_test_file"])
|
||||||
|
assert appt_data
|
||||||
|
|
||||||
|
os.remove("appt_test_file")
|
||||||
|
|
||||||
|
# If appointment json is passed in, function should work.
|
||||||
|
appt_data = wt_cli.parse_add_appointment_args([json.dumps(dummy_appointment_request)])
|
||||||
|
assert appt_data
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_post_appointment():
|
||||||
|
response = {
|
||||||
|
"locator": dummy_appointment.to_dict()["locator"],
|
||||||
|
"signature": Cryptographer.sign(dummy_appointment.serialize(), dummy_pk),
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
||||||
|
response = wt_cli.post_appointment(json.dumps(dummy_appointment_request))
|
||||||
|
|
||||||
|
assert len(responses.calls) == 1
|
||||||
|
assert responses.calls[0].request.url == pisa_endpoint
|
||||||
|
assert response
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_process_post_appointment_response():
|
||||||
|
# Let's first crete a response
|
||||||
|
response = {
|
||||||
|
"locator": dummy_appointment.to_dict()["locator"],
|
||||||
|
"signature": Cryptographer.sign(dummy_appointment.serialize(), dummy_pk),
|
||||||
|
}
|
||||||
|
|
||||||
|
# A 200 OK with a correct json response should return the json of the response
|
||||||
|
responses.add(responses.POST, pisa_endpoint, json=response, status=200)
|
||||||
|
r = wt_cli.post_appointment(json.dumps(dummy_appointment_request))
|
||||||
|
assert wt_cli.process_post_appointment_response(r) == r.json()
|
||||||
|
|
||||||
|
# If we modify the response code tor a rejection (lets say 404) we should get None
|
||||||
|
responses.replace(responses.POST, pisa_endpoint, json=response, status=404)
|
||||||
|
r = wt_cli.post_appointment(json.dumps(dummy_appointment_request))
|
||||||
|
assert wt_cli.process_post_appointment_response(r) is None
|
||||||
|
|
||||||
|
# The same should happen if the response is not in json
|
||||||
|
responses.replace(responses.POST, pisa_endpoint, status=404)
|
||||||
|
r = wt_cli.post_appointment(json.dumps(dummy_appointment_request))
|
||||||
|
assert wt_cli.process_post_appointment_response(r) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_appointment_receipt(monkeypatch):
|
||||||
|
appointments_folder = "test_appointments_receipts"
|
||||||
|
wt_cli.config["APPOINTMENTS_FOLDER_NAME"] = appointments_folder
|
||||||
|
|
||||||
|
# The functions creates a new directory if it does not exist
|
||||||
|
assert not os.path.exists(appointments_folder)
|
||||||
|
wt_cli.save_appointment_receipt(dummy_appointment.to_dict(), get_dummy_signature())
|
||||||
|
assert os.path.exists(appointments_folder)
|
||||||
|
|
||||||
|
# Check that the receipt has been saved by checking the file names
|
||||||
|
files = os.listdir(appointments_folder)
|
||||||
|
assert any([dummy_appointment.locator in f for f in files])
|
||||||
|
|
||||||
|
shutil.rmtree(appointments_folder)
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_appointment():
|
||||||
|
# Response of get_appointment endpoint is an appointment with status added to it.
|
||||||
|
dummy_appointment_full["status"] = "being_watched"
|
||||||
|
response = dummy_appointment_full
|
||||||
|
|
||||||
|
request_url = "{}get_appointment?locator={}".format(pisa_endpoint, response.get("locator"))
|
||||||
|
responses.add(responses.GET, request_url, json=response, status=200)
|
||||||
|
result = wt_cli.get_appointment(response.get("locator"))
|
||||||
|
|
||||||
|
assert len(responses.calls) == 1
|
||||||
|
assert responses.calls[0].request.url == request_url
|
||||||
|
assert result.get("locator") == response.get("locator")
|
||||||
|
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
def test_get_appointment_err():
|
||||||
|
locator = get_random_value_hex(16)
|
||||||
|
|
||||||
|
# Test that get_appointment handles a connection error appropriately.
|
||||||
|
request_url = "{}get_appointment?locator=".format(pisa_endpoint, locator)
|
||||||
|
responses.add(responses.GET, request_url, body=ConnectionError())
|
||||||
|
|
||||||
|
assert not wt_cli.get_appointment(locator)
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
|
import os
|
||||||
import binascii
|
import binascii
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
from apps.cli.blob import Blob
|
from apps.cli.blob import Blob
|
||||||
|
from common.logger import Logger
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
from pisa.encrypted_blob import EncryptedBlob
|
from pisa.encrypted_blob import EncryptedBlob
|
||||||
from test.common.unit.conftest import get_random_value_hex
|
from test.common.unit.conftest import get_random_value_hex
|
||||||
|
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix="")
|
||||||
|
|
||||||
data = "6097cdf52309b1b2124efeed36bd34f46dc1c25ad23ac86f28380f746254f777"
|
data = "6097cdf52309b1b2124efeed36bd34f46dc1c25ad23ac86f28380f746254f777"
|
||||||
key = "b2e984a570f6f49bc38ace178e09147b0aa296cbb7c92eb01412f7e2d07b5659"
|
key = "b2e984a570f6f49bc38ace178e09147b0aa296cbb7c92eb01412f7e2d07b5659"
|
||||||
encrypted_data = "8f31028097a8bf12a92e088caab5cf3fcddf0d35ed2b72c24b12269373efcdea04f9d2a820adafe830c20ff132d89810"
|
encrypted_data = "8f31028097a8bf12a92e088caab5cf3fcddf0d35ed2b72c24b12269373efcdea04f9d2a820adafe830c20ff132d89810"
|
||||||
@@ -181,6 +186,30 @@ def test_decrypt_wrong_return():
|
|||||||
assert True
|
assert True
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_key_file():
|
||||||
|
dummy_sk = ec.generate_private_key(ec.SECP256K1, default_backend())
|
||||||
|
dummy_sk_der = dummy_sk.private_bytes(
|
||||||
|
encoding=serialization.Encoding.DER,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# If file exists and has data in it, function should work.
|
||||||
|
with open("key_test_file", "wb") as f:
|
||||||
|
f.write(dummy_sk_der)
|
||||||
|
|
||||||
|
appt_data = Cryptographer.load_key_file("key_test_file")
|
||||||
|
assert appt_data
|
||||||
|
|
||||||
|
os.remove("key_test_file")
|
||||||
|
|
||||||
|
# If file doesn't exist, function should return None
|
||||||
|
assert Cryptographer.load_key_file("nonexistent_file") is None
|
||||||
|
|
||||||
|
# If something that's not a file_path is passed as parameter the method should also return None
|
||||||
|
assert Cryptographer.load_key_file(0) is None and Cryptographer.load_key_file(None) is None
|
||||||
|
|
||||||
|
|
||||||
def test_load_public_key_der():
|
def test_load_public_key_der():
|
||||||
# load_public_key_der expects a byte encoded data. Any other should fail and return None
|
# load_public_key_der expects a byte encoded data. Any other should fail and return None
|
||||||
for wtype in WRONG_TYPES:
|
for wtype in WRONG_TYPES:
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
|
import binascii
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from riemann.tx import Tx
|
from riemann.tx import Tx
|
||||||
|
|
||||||
|
from pisa import config
|
||||||
from pisa import HOST, PORT
|
from pisa import HOST, PORT
|
||||||
from apps.cli import pisa_cli
|
from apps.cli import wt_cli
|
||||||
from apps.cli.blob import Blob
|
from apps.cli.blob import Blob
|
||||||
|
from apps.cli import config as cli_conf
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
|
from common.logger import Logger
|
||||||
from common.tools import compute_locator
|
from common.tools import compute_locator
|
||||||
from common.appointment import Appointment
|
from common.appointment import Appointment
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
@@ -17,15 +23,24 @@ from test.pisa.e2e.conftest import (
|
|||||||
run_pisad,
|
run_pisad,
|
||||||
)
|
)
|
||||||
|
|
||||||
# We'll use pisa_cli to add appointments. The expected input format is a list of arguments with a json-encoded
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix="")
|
||||||
|
|
||||||
|
# We'll use wt_cli to add appointments. The expected input format is a list of arguments with a json-encoded
|
||||||
# appointment
|
# appointment
|
||||||
pisa_cli.pisa_api_server = HOST
|
wt_cli.pisa_api_server = HOST
|
||||||
pisa_cli.pisa_api_port = PORT
|
wt_cli.pisa_api_port = PORT
|
||||||
|
|
||||||
# Run pisad
|
# Run pisad
|
||||||
pisad_process = run_pisad()
|
pisad_process = run_pisad()
|
||||||
|
|
||||||
|
|
||||||
|
def get_pisa_pk():
|
||||||
|
pisa_sk = Cryptographer.load_private_key_der(Cryptographer.load_key_file(config.get("PISA_SECRET_KEY")))
|
||||||
|
pisa_pk = pisa_sk.public_key()
|
||||||
|
|
||||||
|
return pisa_pk
|
||||||
|
|
||||||
|
|
||||||
def broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, addr):
|
def broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, addr):
|
||||||
# Broadcast the commitment transaction and mine a block
|
# Broadcast the commitment transaction and mine a block
|
||||||
bitcoin_cli.sendrawtransaction(commitment_tx)
|
bitcoin_cli.sendrawtransaction(commitment_tx)
|
||||||
@@ -35,22 +50,28 @@ def broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, addr):
|
|||||||
def get_appointment_info(locator):
|
def get_appointment_info(locator):
|
||||||
# Check that the justice has been triggered (the appointment has moved from Watcher to Responder)
|
# Check that the justice has been triggered (the appointment has moved from Watcher to Responder)
|
||||||
sleep(1) # Let's add a bit of delay so the state can be updated
|
sleep(1) # Let's add a bit of delay so the state can be updated
|
||||||
return pisa_cli.get_appointment([locator])
|
return wt_cli.get_appointment(locator)
|
||||||
|
|
||||||
|
|
||||||
def test_appointment_life_cycle(bitcoin_cli, create_txs):
|
def test_appointment_life_cycle(monkeypatch, bitcoin_cli, create_txs):
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", get_pisa_pk)
|
||||||
|
|
||||||
commitment_tx, penalty_tx = create_txs
|
commitment_tx, penalty_tx = create_txs
|
||||||
commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")
|
commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")
|
||||||
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx)
|
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx)
|
||||||
locator = compute_locator(commitment_tx_id)
|
locator = compute_locator(commitment_tx_id)
|
||||||
|
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment_data)]) is True
|
||||||
|
|
||||||
|
appointment_info = get_appointment_info(locator)
|
||||||
|
assert appointment_info is not None
|
||||||
|
assert len(appointment_info) == 1
|
||||||
|
assert appointment_info[0].get("status") == "being_watched"
|
||||||
|
|
||||||
new_addr = bitcoin_cli.getnewaddress()
|
new_addr = bitcoin_cli.getnewaddress()
|
||||||
broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr)
|
broadcast_transaction_and_mine_block(bitcoin_cli, commitment_tx, new_addr)
|
||||||
|
|
||||||
appointment_info = get_appointment_info(locator)
|
appointment_info = get_appointment_info(locator)
|
||||||
|
|
||||||
assert appointment_info is not None
|
assert appointment_info is not None
|
||||||
assert len(appointment_info) == 1
|
assert len(appointment_info) == 1
|
||||||
assert appointment_info[0].get("status") == "dispute_responded"
|
assert appointment_info[0].get("status") == "dispute_responded"
|
||||||
@@ -77,7 +98,9 @@ def test_appointment_life_cycle(bitcoin_cli, create_txs):
|
|||||||
assert appointment_info[0].get("status") == "not_found"
|
assert appointment_info[0].get("status") == "not_found"
|
||||||
|
|
||||||
|
|
||||||
def test_appointment_malformed_penalty(bitcoin_cli, create_txs):
|
def test_appointment_malformed_penalty(monkeypatch, bitcoin_cli, create_txs):
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", get_pisa_pk)
|
||||||
|
|
||||||
# Lets start by creating two valid transaction
|
# Lets start by creating two valid transaction
|
||||||
commitment_tx, penalty_tx = create_txs
|
commitment_tx, penalty_tx = create_txs
|
||||||
|
|
||||||
@@ -90,7 +113,7 @@ def test_appointment_malformed_penalty(bitcoin_cli, create_txs):
|
|||||||
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, mod_penalty_tx.hex())
|
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, mod_penalty_tx.hex())
|
||||||
locator = compute_locator(commitment_tx_id)
|
locator = compute_locator(commitment_tx_id)
|
||||||
|
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment_data)]) is True
|
||||||
|
|
||||||
# Broadcast the commitment transaction and mine a block
|
# Broadcast the commitment transaction and mine a block
|
||||||
new_addr = bitcoin_cli.getnewaddress()
|
new_addr = bitcoin_cli.getnewaddress()
|
||||||
@@ -113,24 +136,32 @@ def test_appointment_wrong_key(bitcoin_cli, create_txs):
|
|||||||
# The appointment data is built using a random 32-byte value.
|
# The appointment data is built using a random 32-byte value.
|
||||||
appointment_data = build_appointment_data(bitcoin_cli, get_random_value_hex(32), penalty_tx)
|
appointment_data = build_appointment_data(bitcoin_cli, get_random_value_hex(32), penalty_tx)
|
||||||
|
|
||||||
# We can't use pisa_cli.add_appointment here since it computes the locator internally, so let's do it manually.
|
# We can't use wt_cli.add_appointment here since it computes the locator internally, so let's do it manually.
|
||||||
# We will encrypt the blob using the random value and derive the locator from the commitment tx.
|
# We will encrypt the blob using the random value and derive the locator from the commitment tx.
|
||||||
appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid"))
|
appointment_data["locator"] = compute_locator(bitcoin_cli.decoderawtransaction(commitment_tx).get("txid"))
|
||||||
appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(penalty_tx), appointment_data.get("tx_id"))
|
appointment_data["encrypted_blob"] = Cryptographer.encrypt(Blob(penalty_tx), get_random_value_hex(32))
|
||||||
appointment = Appointment.from_dict(appointment_data)
|
appointment = Appointment.from_dict(appointment_data)
|
||||||
|
|
||||||
signature = pisa_cli.get_appointment_signature(appointment)
|
# pisa_pk, cli_sk, cli_pk_der = wt_cli.load_keys(
|
||||||
hex_pk_der = pisa_cli.get_pk()
|
# cli_conf.get("PISA_PUBLIC_KEY"), cli_conf.get("CLI_PRIVATE_KEY"), cli_conf.get("CLI_PUBLIC_KEY")
|
||||||
|
# )
|
||||||
data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")}
|
# hex_pk_der = binascii.hexlify(cli_pk_der)
|
||||||
|
#
|
||||||
|
# signature = Cryptographer.sign(appointment.serialize(), cli_sk)
|
||||||
|
# data = {"appointment": appointment.to_dict(), "signature": signature, "public_key": hex_pk_der.decode("utf-8")}
|
||||||
|
# FIXME: Since the pk is now hardcoded for the alpha in the cli we cannot use load_keys here. We need to derive
|
||||||
|
# the pk from the sk on disk.
|
||||||
|
pisa_pk = get_pisa_pk()
|
||||||
|
data = {"appointment": appointment.to_dict()}
|
||||||
|
|
||||||
# Send appointment to the server.
|
# Send appointment to the server.
|
||||||
response_json = pisa_cli.post_data_to_add_appointment_endpoint(data)
|
response = wt_cli.post_appointment(data)
|
||||||
|
response_json = wt_cli.process_post_appointment_response(response)
|
||||||
|
|
||||||
# Check that the server has accepted the appointment
|
# Check that the server has accepted the appointment
|
||||||
signature = response_json.get("signature")
|
signature = response_json.get("signature")
|
||||||
assert signature is not None
|
assert signature is not None
|
||||||
assert pisa_cli.check_signature(signature, appointment) is True
|
assert Cryptographer.verify(appointment.serialize(), signature, pisa_pk) is True
|
||||||
assert response_json.get("locator") == appointment.locator
|
assert response_json.get("locator") == appointment.locator
|
||||||
|
|
||||||
# Trigger the appointment
|
# Trigger the appointment
|
||||||
@@ -146,7 +177,9 @@ def test_appointment_wrong_key(bitcoin_cli, create_txs):
|
|||||||
assert appointment_info[0].get("status") == "not_found"
|
assert appointment_info[0].get("status") == "not_found"
|
||||||
|
|
||||||
|
|
||||||
def test_two_identical_appointments(bitcoin_cli, create_txs):
|
def test_two_identical_appointments(monkeypatch, bitcoin_cli, create_txs):
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", get_pisa_pk)
|
||||||
|
|
||||||
# Tests sending two identical appointments to the tower.
|
# Tests sending two identical appointments to the tower.
|
||||||
# At the moment there are no checks for identical appointments, so both will be accepted, decrypted and kept until
|
# At the moment there are no checks for identical appointments, so both will be accepted, decrypted and kept until
|
||||||
# the end.
|
# the end.
|
||||||
@@ -159,8 +192,8 @@ def test_two_identical_appointments(bitcoin_cli, create_txs):
|
|||||||
locator = compute_locator(commitment_tx_id)
|
locator = compute_locator(commitment_tx_id)
|
||||||
|
|
||||||
# Send the appointment twice
|
# Send the appointment twice
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment_data)]) is True
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment_data)]) is True
|
||||||
|
|
||||||
# Broadcast the commitment transaction and mine a block
|
# Broadcast the commitment transaction and mine a block
|
||||||
new_addr = bitcoin_cli.getnewaddress()
|
new_addr = bitcoin_cli.getnewaddress()
|
||||||
@@ -179,7 +212,9 @@ def test_two_identical_appointments(bitcoin_cli, create_txs):
|
|||||||
assert info.get("penalty_rawtx") == penalty_tx
|
assert info.get("penalty_rawtx") == penalty_tx
|
||||||
|
|
||||||
|
|
||||||
def test_two_appointment_same_locator_different_penalty(bitcoin_cli, create_txs):
|
def test_two_appointment_same_locator_different_penalty(monkeypatch, bitcoin_cli, create_txs):
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", get_pisa_pk)
|
||||||
|
|
||||||
# This tests sending an appointment with two valid transaction with the same locator.
|
# This tests sending an appointment with two valid transaction with the same locator.
|
||||||
commitment_tx, penalty_tx1 = create_txs
|
commitment_tx, penalty_tx1 = create_txs
|
||||||
commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")
|
commitment_tx_id = bitcoin_cli.decoderawtransaction(commitment_tx).get("txid")
|
||||||
@@ -193,8 +228,8 @@ def test_two_appointment_same_locator_different_penalty(bitcoin_cli, create_txs)
|
|||||||
appointment2_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx2)
|
appointment2_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx2)
|
||||||
locator = compute_locator(commitment_tx_id)
|
locator = compute_locator(commitment_tx_id)
|
||||||
|
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment1_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment1_data)]) is True
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment2_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment2_data)]) is True
|
||||||
|
|
||||||
# Broadcast the commitment transaction and mine a block
|
# Broadcast the commitment transaction and mine a block
|
||||||
new_addr = bitcoin_cli.getnewaddress()
|
new_addr = bitcoin_cli.getnewaddress()
|
||||||
@@ -211,9 +246,11 @@ def test_two_appointment_same_locator_different_penalty(bitcoin_cli, create_txs)
|
|||||||
assert appointment_info[0].get("penalty_rawtx") == penalty_tx1
|
assert appointment_info[0].get("penalty_rawtx") == penalty_tx1
|
||||||
|
|
||||||
|
|
||||||
def test_appointment_shutdown_pisa_trigger_back_online(create_txs, bitcoin_cli):
|
def test_appointment_shutdown_pisa_trigger_back_online(monkeypatch, create_txs, bitcoin_cli):
|
||||||
global pisad_process
|
global pisad_process
|
||||||
|
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", get_pisa_pk)
|
||||||
|
|
||||||
pisa_pid = pisad_process.pid
|
pisa_pid = pisad_process.pid
|
||||||
|
|
||||||
commitment_tx, penalty_tx = create_txs
|
commitment_tx, penalty_tx = create_txs
|
||||||
@@ -221,7 +258,7 @@ def test_appointment_shutdown_pisa_trigger_back_online(create_txs, bitcoin_cli):
|
|||||||
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx)
|
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx)
|
||||||
locator = compute_locator(commitment_tx_id)
|
locator = compute_locator(commitment_tx_id)
|
||||||
|
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment_data)]) is True
|
||||||
|
|
||||||
# Restart pisa
|
# Restart pisa
|
||||||
pisad_process.terminate()
|
pisad_process.terminate()
|
||||||
@@ -249,9 +286,11 @@ def test_appointment_shutdown_pisa_trigger_back_online(create_txs, bitcoin_cli):
|
|||||||
assert appointment_info[0].get("status") == "dispute_responded"
|
assert appointment_info[0].get("status") == "dispute_responded"
|
||||||
|
|
||||||
|
|
||||||
def test_appointment_shutdown_pisa_trigger_while_offline(create_txs, bitcoin_cli):
|
def test_appointment_shutdown_pisa_trigger_while_offline(monkeypatch, create_txs, bitcoin_cli):
|
||||||
global pisad_process
|
global pisad_process
|
||||||
|
|
||||||
|
monkeypatch.setattr(wt_cli, "load_keys", get_pisa_pk)
|
||||||
|
|
||||||
pisa_pid = pisad_process.pid
|
pisa_pid = pisad_process.pid
|
||||||
|
|
||||||
commitment_tx, penalty_tx = create_txs
|
commitment_tx, penalty_tx = create_txs
|
||||||
@@ -259,7 +298,7 @@ def test_appointment_shutdown_pisa_trigger_while_offline(create_txs, bitcoin_cli
|
|||||||
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx)
|
appointment_data = build_appointment_data(bitcoin_cli, commitment_tx_id, penalty_tx)
|
||||||
locator = compute_locator(commitment_tx_id)
|
locator = compute_locator(commitment_tx_id)
|
||||||
|
|
||||||
assert pisa_cli.add_appointment([json.dumps(appointment_data)]) is True
|
assert wt_cli.add_appointment([json.dumps(appointment_data)]) is True
|
||||||
|
|
||||||
# Check that the appointment is still in the Watcher
|
# Check that the appointment is still in the Watcher
|
||||||
appointment_info = get_appointment_info(locator)
|
appointment_info = get_appointment_info(locator)
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ from bitcoind_mock.transaction import create_dummy_transaction
|
|||||||
from bitcoind_mock.bitcoind import BitcoindMock
|
from bitcoind_mock.bitcoind import BitcoindMock
|
||||||
from bitcoind_mock.conf import BTC_RPC_HOST, BTC_RPC_PORT
|
from bitcoind_mock.conf import BTC_RPC_HOST, BTC_RPC_PORT
|
||||||
|
|
||||||
|
from pisa import LOG_PREFIX
|
||||||
|
import common.cryptographer
|
||||||
|
from common.logger import Logger
|
||||||
from common.constants import LOCATOR_LEN_HEX
|
from common.constants import LOCATOR_LEN_HEX
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
|
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def run_bitcoind():
|
def run_bitcoind():
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ from test.pisa.unit.conftest import get_random_value_hex, generate_dummy_appoint
|
|||||||
|
|
||||||
from common.constants import LOCATOR_LEN_BYTES, LOCATOR_LEN_HEX
|
from common.constants import LOCATOR_LEN_BYTES, LOCATOR_LEN_HEX
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
|
from common.logger import Logger
|
||||||
|
|
||||||
|
from pisa import LOG_PREFIX
|
||||||
|
import common.cryptographer
|
||||||
|
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
inspector = Inspector(get_config())
|
inspector = Inspector(get_config())
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ from test.pisa.unit.conftest import (
|
|||||||
)
|
)
|
||||||
from pisa.conf import EXPIRY_DELTA, MAX_APPOINTMENTS
|
from pisa.conf import EXPIRY_DELTA, MAX_APPOINTMENTS
|
||||||
|
|
||||||
|
import common.cryptographer
|
||||||
|
from pisa import LOG_PREFIX
|
||||||
|
from common.logger import Logger
|
||||||
from common.tools import compute_locator
|
from common.tools import compute_locator
|
||||||
from common.cryptographer import Cryptographer
|
from common.cryptographer import Cryptographer
|
||||||
|
|
||||||
|
common.cryptographer.logger = Logger(actor="Cryptographer", log_name_prefix=LOG_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
APPOINTMENTS = 5
|
APPOINTMENTS = 5
|
||||||
START_TIME_OFFSET = 1
|
START_TIME_OFFSET = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user