adding automatic ssl-creation (#789)

* adding automatic ssl-creation

* fix tests

* Update docs/self-signed-certificates.md

Co-authored-by: benk10 <ben.kaufman10@gmail.com>

* disabling hwibridge with ssl

Co-authored-by: benk10 <ben.kaufman10@gmail.com>
This commit is contained in:
Kim Neunert 2020-12-20 15:03:23 +01:00 committed by GitHub
parent 55f18e8154
commit 119a70e1e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 145 additions and 46 deletions

View file

@ -1,12 +1,22 @@
# Issuing self-signed certificates
# Why a certificate is important
Browsers require secure communication with the server to use camera API. Without it we can't use QR code scanning.
If you are running a VPS it's easy - you just [issue a new certificate](./reverse-proxy#adding-https) with Letsencrypt.
If you are only using the node at home and want to use it from your local network you need to issue a certificate yourself.
If you are only using the node at home and want to use it from your local network and via camera, you need to run it via ssl.
The easiest way is to run the [`gen-certificate.sh`](gen-certificate.sh) script in this folder with your node's ip address as an argument:
# Easy solution
The easiest solution is to simply add `--ssl` to the serve-command and the certificate will get created automatically in the specter-home-folder.
```
python3 -m cryptoadance.specter server --ssl
```
# Manual creation
A second way, which provides more customisation, is to run the [`gen-certificate.sh`](gen-certificate.sh) script in this folder with your node's ip address as an argument:
```sh
gen-certificate.sh <your-node-local-ip-address>
@ -69,4 +79,4 @@ On **Mac**: copy `cert.pem` to your computer, add it to your keychain and set `T
- Right-click on your certificate and unfold the `Trust` list
- In row `When using this certificate`, choose `Always Trust`
Other platforms: ???
Other platforms: ???

View file

@ -16,3 +16,4 @@ six==1.12.0
stem==1.8.0
embit==0.1.2
psutil==5.7.3
pyopenssl==20.0.1

View file

@ -85,7 +85,7 @@ cryptography==3.2 \
--hash=sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae \
--hash=sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f \
--hash=sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef \
# via noiseprotocol
# via noiseprotocol, pyopenssl
daemonize==2.5.0 \
--hash=sha256:9b6b91311a9d934ff3f5f766666635ca280d3de8e7137e4cd7d3f052543b989f \
--hash=sha256:dd026e4ff8d22cb016ed2130bc738b7d4b1da597ef93c074d2adb9e4dea08bc3 \
@ -229,6 +229,10 @@ pycparser==2.20 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705 \
# via cffi
pyopenssl==20.0.1 \
--hash=sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51 \
--hash=sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b \
# via -r requirements.in
pyserial==3.4 \
--hash=sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627 \
--hash=sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8 \
@ -253,7 +257,7 @@ semver==2.10.2 \
six==1.12.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \
# via -r requirements.in, cryptography, flask-cors, protobuf
# via -r requirements.in, cryptography, flask-cors, protobuf, pyopenssl
stem==1.8.0 \
--hash=sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2 \
# via -r requirements.in

View file

@ -4,8 +4,10 @@ import signal
import sys
import time
from os import path
from socket import gethostname
import click
from OpenSSL import SSL, crypto
from stem.control import Controller
from ..server import create_app, init_app
@ -55,7 +57,15 @@ def cli():
@click.option(
"--cert", help="--cert and --key are for using a self-signed-cert/ssl-encryption"
)
@click.option("--key")
@click.option(
"--key", help="--cert and --key are for using a self-signed-cert/ssl-encryption"
)
@click.option(
"--ssl/--no-ssl",
is_flag=True,
default=False,
help="By default will run unencrypted. Use -ssl to create a self-signed certificate or you can also specify it via --cert and --key.",
)
@click.option("--debug/--no-debug", default=None)
@click.option("--tor", is_flag=True)
@click.option(
@ -82,19 +92,21 @@ def server(
host,
cert,
key,
ssl,
debug,
tor,
hwibridge,
specter_data_folder,
config,
):
# create an app to get Specter instance
# and it's data folder
logger.info("Logging is hopefully configured")
# logging
if debug:
ca_logger = logging.getLogger("cryptoadvance")
ca_logger.setLevel(logging.DEBUG)
logger.debug("We're now on level DEBUG on logger cryptoadvance")
# create an app to get Specter instance
# and it's data folder
if config is None:
app = create_app()
else:
@ -114,7 +126,7 @@ def server(
logger.info("CERT:" + str(cert))
app.config["CERT"] = cert
if key:
key = app.config["KEY"] = key
app.config["KEY"] = key
app.app_context().push()
init_app(app, hwibridge=hwibridge)
@ -123,6 +135,7 @@ def server(
# When we remove it, we should imho keep the pid_file thing which can be very useful!
# we will store our daemon PID here
pid_file = path.join(app.specter.data_folder, "daemon.pid")
toraddr_file = path.join(app.specter.data_folder, "onion.txt")
# check if pid file exists
if path.isfile(pid_file):
@ -164,19 +177,19 @@ def server(
if os.path.isfile(filename):
extra_files.append(filename)
protocol = "http"
kwargs = {"host": host, "port": app.config["PORT"], "extra_files": extra_files}
if cert is not None and key is not None:
cert = os.path.abspath(app.config["CERT"])
key = os.path.abspath(app.config["KEY"])
kwargs["ssl_context"] = (cert, key)
protocol = "https"
kwargs = configure_ssl(kwargs, app.config, ssl)
if hwibridge:
if kwargs.get("ssl_context"):
logger.error(
"Running the hwibridge is not supported via ssl. Remove --ssl or make sure to not pass --cert or --key."
)
exit(1)
print(
" * Running HWI Bridge mode.\n"
" * You can configure access to the API "
"at: %s://%s:%d/hwi/settings" % (protocol, host, app.config["PORT"])
"at: %s://%s:%d/hwi/settings" % ("http", host, app.config["PORT"])
)
# debug is false by default
@ -221,15 +234,15 @@ def server(
# check if we should run a daemon or not
if daemon or restart:
print("Starting server in background...")
print(" * Hopefully running on %s://%s:%d/" % (protocol, host, port))
print(" * Running on %s://%s:%d/" % (protocol, host, port))
# macOS + python3.7 is buggy
if sys.platform == "darwin" and (
sys.version_info.major == 3 and sys.version_info.minor < 8
):
print(
" * WARNING: --daemon mode might not \
work properly in python 3.7 and lower \
on MacOS. Upgrade to python 3.8+"
raise Exception(
" ERROR: --daemon mode is no longer \
supported in python 3.7 and lower \
on MacOS. Upgrade to python 3.8+. (Might not work anyway)"
)
from daemonize import Daemonize
@ -240,3 +253,48 @@ def server(
if debug is None:
debug = app.config["DEBUG"]
run(debug=debug)
def configure_ssl(kwargs, app_config, ssl):
""" accepts kwargs and adjust them based on the config and ssl """
# If we should create a cert but it's not specified where, let's specify the location
if not ssl and app_config["CERT"] is None:
return kwargs
if app_config["CERT"] is None:
app_config["CERT"] = app_config["SPECTER_DATA_FOLDER"] + "/cert.pem"
if app_config["KEY"] is None:
app_config["KEY"] = app_config["SPECTER_DATA_FOLDER"] + "/key.pem"
if not os.path.exists(app_config["CERT"]):
logger.info("Creating SSL-cert " + app_config["CERT"])
# create a key pair
k = crypto.PKey()
k.generate_key(crypto.TYPE_RSA, 2048)
# create a self-signed cert
cert = crypto.X509()
cert.get_subject().C = app_config["SPECTER_SSL_CERT_SUBJECT_C"]
cert.get_subject().ST = app_config["SPECTER_SSL_CERT_SUBJECT_ST"]
cert.get_subject().L = app_config["SPECTER_SSL_CERT_SUBJECT_L"]
cert.get_subject().O = app_config["SPECTER_SSL_CERT_SUBJECT_O"]
cert.get_subject().OU = app_config["SPECTER_SSL_CERT_SUBJECT_OU"]
cert.get_subject().CN = app_config["SPECTER_SSL_CERT_SUBJECT_CN"]
cert.set_serial_number(app_config["SPECTER_SSL_CERT_SERIAL_NUMBER"])
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
cert.set_issuer(cert.get_subject())
cert.set_pubkey(k)
cert.sign(k, "sha1")
open(app_config["CERT"], "wt").write(
crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")
)
open(app_config["KEY"], "wt").write(
crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8")
)
logger.info("Configuring SSL-cert " + app_config["CERT"])
kwargs["ssl_context"] = (app_config["CERT"], app_config["KEY"])
return kwargs

View file

@ -1,12 +1,12 @@
""" A config module contains static configuration """
import configparser
import datetime
import os
import configparser
import random
from pathlib import Path
from dotenv import load_dotenv
# BASEDIR = os.path.abspath(os.path.dirname(__file__))
# Loading env-vars from .flaskenv (4 levels above this file)
@ -48,6 +48,24 @@ class BaseConfig(object):
"BTCD_REGTEST_DATA_DIR", "/tmp/specter_btc_regtest_plain_datadir"
)
# The self-signed ssl-certificate which is lazily created is configurable to a certain extent
SPECTER_SSL_CERT_SUBJECT_C = os.getenv("SPECTER_SSL_CERT_SUBJECT_C", "DE")
SPECTER_SSL_CERT_SUBJECT_ST = os.getenv("SPECTER_SSL_CERT_SUBJECT_ST", "BDW")
SPECTER_SSL_CERT_SUBJECT_L = os.getenv("SPECTER_SSL_CERT_SUBJECT_L", "Freiburg")
SPECTER_SSL_CERT_SUBJECT_O = os.getenv(
"SPECTER_SSL_CERT_SUBJECT_O", "Specter Citadel Cert"
)
SPECTER_SSL_CERT_SUBJECT_OU = os.getenv(
"SPECTER_SSL_CERT_SUBJECT_OU", "Specter Citadel Cert"
)
SPECTER_SSL_CERT_SUBJECT_CN = os.getenv(
"SPECTER_SSL_CERT_SUBJECT_CN", "Specter Citadel Cert"
)
# For self-signed certs, serial-number collision is a risk, so let's do a random one by default
SPECTER_SSL_CERT_SERIAL_NUMBER = int(
os.getenv("SPECTER_SSL_CERT_SERIAL_NUMBER", random.randrange(1, 100000))
)
class DevelopmentConfig(BaseConfig):
# https://stackoverflow.com/questions/22463939/demystify-flask-app-secret-key

View file

@ -8,17 +8,30 @@ import mock
from mock import patch, MagicMock, call
mock_config_dict = {
"PORT": "123",
"DEBUG": "WURSTBROT",
"SPECTER_SSL_CERT_SUBJECT_C": "AT",
"SPECTER_SSL_CERT_SUBJECT_ST": "Blub",
"SPECTER_SSL_CERT_SUBJECT_L": "Blub",
"SPECTER_SSL_CERT_SUBJECT_O": "Blub",
"SPECTER_SSL_CERT_SUBJECT_OU": "Blub",
"SPECTER_SSL_CERT_SUBJECT_CN": "Blub",
"SPECTER_SSL_CERT_SERIAL_NUMBER": 123,
# We don't want to make a more sophisticated mock, so we simply set here
# the same value we set via CMD-Line
"CERT": "bla",
"KEY": "blub",
}
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@patch("cryptoadvance.specter.cli.cli_server.init_app")
def test_server_host_and_port(init_app, create_app, caplog):
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()
d = {
"SPECTER_DATA_FOLDER": "someValueWillGetChanged",
"PORT": "123",
"DEBUG": "WURSTBROT",
}
d = mock_config_dict
mock_app.config.__getitem__.side_effect = d.__getitem__
create_app.return_value = mock_app
runner = CliRunner()
@ -42,14 +55,7 @@ def test_server_host_and_port(init_app, create_app, caplog):
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()
d = {
"PORT": "123",
"DEBUG": "WURSTBROT",
# We don't want to make a more sophisticated mock, so we simply set here
# the same value we set via CMD-Line
"CERT": "bla",
"KEY": "blub",
}
d = mock_config_dict
mock_app.config.__getitem__.side_effect = d.__getitem__
create_app.return_value = mock_app
runner = CliRunner()
@ -72,8 +78,8 @@ def test_server_host_and_port(init_app, create_app, caplog):
print(mock_app.run.call_args.kwargs)
# results in something like:
# {'debug': 'WURSTBROT', 'host': '127.0.0.1', 'port': '123', 'extra_files': ['templates'], 'ssl_context': ('/tmp/tmpnzivft_y/bla', '/tmp/tmpnzivft_y/blub')}
assert mock_app.run.call_args.kwargs["ssl_context"][0].endswith("/bla")
assert mock_app.run.call_args.kwargs["ssl_context"][1].endswith("/blub")
assert mock_app.run.call_args.kwargs["ssl_context"][0].endswith("bla")
assert mock_app.run.call_args.kwargs["ssl_context"][1].endswith("blub")
@patch("cryptoadvance.specter.cli.cli_server.create_app")
@ -88,7 +94,6 @@ def test_server_debug(init_app, create_app, caplog):
traceback.print_tb(result.exception.__traceback__)
print(result.exception)
assert result.exit_code == 0
assert "Logging is hopefully configured" in caplog.text
assert "We're now on level DEBUG on logger cryptoadvance" in caplog.text
@ -98,11 +103,7 @@ def test_server_datafolder(init_app, create_app, caplog):
caplog.set_level(logging.DEBUG)
mock_app = MagicMock()
mock_app.config = MagicMock()
d = {
"SPECTER_DATA_FOLDER": "someValueWillGetChanged",
"PORT": "123",
"DEBUG": "WURSTBROT",
}
d = mock_config_dict
mock_app.config.__getitem__.side_effect = d.__getitem__
create_app.return_value = mock_app
runner = CliRunner()
@ -128,6 +129,13 @@ def test_server_config(init_app, create_app, caplog):
d = {
"PORT": "123",
"DEBUG": "WURSTBROT",
"SPECTER_SSL_CERT_SUBJECT_C": "AT",
"SPECTER_SSL_CERT_SUBJECT_ST": "Blub",
"SPECTER_SSL_CERT_SUBJECT_L": "Blub",
"SPECTER_SSL_CERT_SUBJECT_O": "Blub",
"SPECTER_SSL_CERT_SUBJECT_OU": "Blub",
"SPECTER_SSL_CERT_SUBJECT_CN": "Blub",
"SPECTER_SSL_CERT_SERIAL_NUMBER": 123,
# We don't want to make a more sophisticated mock, so we simply set here
# the same value we set via CMD-Line
"CERT": "bla",