-
Notifications
You must be signed in to change notification settings - Fork 15
Add teos cli auth #262
base: master
Are you sure you want to change the base?
Add teos cli auth #262
Changes from all commits
6d3da13
a7130bd
1bcebb1
8e5fbfb
9606211
d260c05
127c4ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import datetime | ||
import os.path | ||
import pyzbase32 | ||
from pathlib import Path | ||
|
@@ -6,6 +7,12 @@ | |
from coincurve import PrivateKey, PublicKey | ||
from cryptography.exceptions import InvalidTag | ||
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 | ||
from cryptography.hazmat.primitives import hashes | ||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.hazmat.primitives.asymmetric import rsa | ||
from cryptography.hazmat.primitives.serialization import load_pem_private_key | ||
from cryptography import x509 | ||
from cryptography.x509.oid import NameOID | ||
|
||
from common.tools import is_256b_hex_str | ||
from common.exceptions import InvalidKey, InvalidParameter, SignatureError, EncryptionError | ||
|
@@ -192,44 +199,44 @@ def generate_key(): | |
return PrivateKey() | ||
|
||
@staticmethod | ||
def save_key_file(key, name, data_dir): | ||
def save_crypto_file(crypto_data, name, data_dir): | ||
""" | ||
Saves a key to disk in DER format. | ||
Saves cryptographic data, like a key or certificate, to disk in format. | ||
|
||
Args: | ||
key (:obj:`bytes`): the key to be saved to disk. | ||
crypto_data (:obj:`bytes`): the key to be saved to disk. | ||
name (:obj:`str`): the name of the key file to be generated. | ||
data_dir (:obj:`str`): the data directory where the file will be saved. | ||
|
||
Raises: | ||
:obj:`InvalidParameter`: If the given key is not bytes or the name or data_dir are not strings. | ||
:obj:`InvalidParameter`: If the given crypto data is not bytes or the name or data_dir are not strings. | ||
""" | ||
|
||
if not isinstance(key, bytes): | ||
raise InvalidParameter("Key must be bytes, {} received".format(type(key))) | ||
if not isinstance(crypto_data, bytes): | ||
raise InvalidParameter("Crypto data must be bytes, {} received".format(type(crypto_data))) | ||
|
||
if not isinstance(name, str): | ||
raise InvalidParameter("Key name must be str, {} received".format(type(name))) | ||
raise InvalidParameter("Crypto data name must be str, {} received".format(type(name))) | ||
|
||
if not isinstance(data_dir, str): | ||
raise InvalidParameter("Data dir must be str, {} received".format(type(data_dir))) | ||
|
||
# Create the output folder it it does not exist (and all the parents if they don't either) | ||
Path(data_dir).mkdir(parents=True, exist_ok=True) | ||
|
||
with open(os.path.join(data_dir, "{}.der".format(name)), "wb") as der_out: | ||
der_out.write(key) | ||
with open(os.path.join(data_dir, "{}".format(name)), "wb") as crypto_out: | ||
crypto_out.write(crypto_data) | ||
|
||
@staticmethod | ||
def load_key_file(file_path): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same applies to I think generalizing the messages and updating the name would do. e.g. "Key file path was expected, {} received" -> File path was expected, {} received" |
||
""" | ||
Loads a key from a key file. | ||
Loads a key or certificate from a disk file. | ||
|
||
Args: | ||
file_path (:obj:`str`): the path to the key file to be loaded. | ||
file_path (:obj:`str`): the path to the key or certificate file to be loaded. | ||
|
||
Returns: | ||
:obj:`bytes`: The key file data if the file can be found and read. | ||
:obj:`bytes`: The key or certificate file data if the file can be found and read. | ||
|
||
Raises: | ||
:obj:`InvalidParameter`: if the file_path has wrong format or cannot be found. | ||
|
@@ -375,3 +382,77 @@ def get_compressed_pk(pk): | |
|
||
except TypeError as e: | ||
raise InvalidKey("PublicKey has invalid initializer", error=str(e)) | ||
|
||
|
||
@staticmethod | ||
def generate_cert_key(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm really debating between using openssl to generate the cert or do it with code. Given the code addition is rather small, I don't think it a big deal though. However, I'd greatly encourage to use ECDSA instead of RSA. Furthermore, we are currently using DER for the rest of the keys, so I think we should also use DER for consistency here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, sounds good, I'll change to ECDSA. The reason I used PEM is because that's the format GRPC accepts. Sound okay for me to save the key/certificate as DER, and then just convert to PEM when we need to use them in GRPC? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I guess you can actually create Key and Cert objects with If it ends up being too complicated it won't be worth it, so feel free to weight it and decide whatever you see fits best. |
||
""" | ||
Generates an RSA key with which to self-sign our TSL certificate and converts it to PEM format. | ||
|
||
Returns: | ||
:obj:`bytes`: An RSA key in PEM format. | ||
""" | ||
sk = rsa.generate_private_key( | ||
public_exponent=65537, | ||
key_size=2048, | ||
) | ||
sk_pem = sk.private_bytes( | ||
encoding=serialization.Encoding.PEM, | ||
format=serialization.PrivateFormat.TraditionalOpenSSL, | ||
encryption_algorithm=serialization.NoEncryption(), | ||
) | ||
|
||
return sk_pem | ||
|
||
|
||
@staticmethod | ||
def generate_self_signed_cert(cert_key_path): | ||
""" | ||
Generates a self-signed TLS certificate for securing the connection between the CLI and the server. | ||
|
||
Args: | ||
cert_key_path(:obj:`str`): Path to RSA key. | ||
|
||
Returns: | ||
:obj:`Certificate`: A x509 certificate. | ||
|
||
Raises: | ||
:obj:`InvalidKey`: if the RSA key file is invalid or could not be found. | ||
|
||
""" | ||
try: | ||
sk_pem = Cryptographer.load_key_file(cert_key_path) | ||
|
||
except (InvalidParameter, InvalidKey): | ||
raise InvalidKey("Failed to load RSA key needed for TLS certificate") | ||
|
||
|
||
sk = load_pem_private_key(sk_pem, None) | ||
|
||
# get public key from private key | ||
pk = sk.public_key() | ||
|
||
subject = issuer = x509.Name([ | ||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Teos watchtower"), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Eye of Satoshi maybe? I've never liked |
||
x509.NameAttribute(NameOID.COMMON_NAME, u"localhost"), | ||
]) | ||
|
||
cert = x509.CertificateBuilder().subject_name( | ||
subject | ||
).issuer_name( | ||
issuer | ||
).public_key( | ||
pk | ||
).serial_number( | ||
x509.random_serial_number() | ||
).not_valid_before(datetime.datetime.utcnow()).not_valid_after( | ||
# Our certificate will be valid for 365 days | ||
datetime.datetime.utcnow() + datetime.timedelta(days=365) | ||
).add_extension( | ||
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]), | ||
critical=False, | ||
).sign(sk, hashes.SHA256()) | ||
|
||
cert = cert.public_bytes(serialization.Encoding.PEM) | ||
|
||
return cert |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -119,8 +119,10 @@ def __init__(self, data_dir, command_line_conf): | |
|
||
teos_rpc_host = config.get("RPC_BIND") | ||
teos_rpc_port = config.get("RPC_PORT") | ||
|
||
self.rpc_client = RPCClient(teos_rpc_host, teos_rpc_port) | ||
rpc_cert = config.get("RPC_CERT") | ||
rpc_user = config.get("RPC_USER") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
rpc_pass = config.get("RPC_PASS") | ||
self.rpc_client = RPCClient(teos_rpc_host, teos_rpc_port, rpc_cert, rpc_user, rpc_pass) | ||
|
||
@classmethod | ||
def command(cls, command_cls): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,6 +142,17 @@ def __init__(self, config, sk, logger, logging_port): | |
bitcoind_feed_params, | ||
) | ||
|
||
if not os.path.exists(self.config.get("RPC_CERT_KEY")): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing this is here for the first bootstrap, but I think there's an uncovered edge case. If the sk does not exists and the cert does exists, you will generate a new sk and load the old cert. That will not work. I think you need to check that they both exist at the same time. There are basically 4 cases here:
** For the new pair generation, I think the best approach is not doing it straight-away. This is the same case as for when the CERT expires. A new RPC command (only available from localhost) needs to be added to re-create the CERT. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So basically here we should return an error regarding the data being invalid / corrupted / not found (whatever sounds good) and ask the user to regenerate the CERT. |
||
sk_pem = Cryptographer.generate_cert_key() | ||
Cryptographer.save_crypto_file(sk_pem, "teos_cert.key", self.config.get("DATA_DIR")) | ||
|
||
if not os.path.exists(self.config.get("RPC_CERT")): | ||
cert = Cryptographer.generate_self_signed_cert(self.config.get("RPC_CERT_KEY")) | ||
Cryptographer.save_crypto_file(cert, "teos_server.crt", self.config.get("DATA_DIR")) | ||
|
||
sk_pem = Cryptographer.load_key_file(self.config.get("RPC_CERT_KEY")) | ||
certificate = Cryptographer.load_key_file(self.config.get("RPC_CERT")) | ||
|
||
# Set up the internal API | ||
self.internal_api_endpoint = f'{self.config.get("INTERNAL_API_HOST")}:{self.config.get("INTERNAL_API_PORT")}' | ||
self.internal_api = InternalAPI( | ||
|
@@ -157,6 +168,10 @@ def __init__(self, config, sk, logger, logging_port): | |
self.internal_api_endpoint, | ||
self.logging_port, | ||
self.stop_event, | ||
sk_pem, | ||
certificate, | ||
self.config.get("RPC_USER"), | ||
self.config.get("RPC_PASS"), | ||
), | ||
daemon=True, | ||
) | ||
|
@@ -366,7 +381,7 @@ def main(config): | |
if not os.path.exists(config.get("TEOS_SECRET_KEY")) or config.get("OVERWRITE_KEY"): | ||
logger.info("Generating a new key pair") | ||
sk = Cryptographer.generate_key() | ||
Cryptographer.save_key_file(sk.to_der(), "teos_sk", config.get("DATA_DIR")) | ||
Cryptographer.save_crypto_file(sk.to_der(), "teos_sk.der", config.get("DATA_DIR")) | ||
|
||
else: | ||
logger.info("Tower identity found. Loading keys") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could call this
save_bytes_file
orsave_file_bytes
, given it has no actually cryptographic restriction.