mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
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:
parent
55f18e8154
commit
119a70e1e6
6 changed files with 145 additions and 46 deletions
|
|
@ -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: ???
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ six==1.12.0
|
|||
stem==1.8.0
|
||||
embit==0.1.2
|
||||
psutil==5.7.3
|
||||
pyopenssl==20.0.1
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue