From 2b33a752d0af18758656fed09b1451bd66170178 Mon Sep 17 00:00:00 2001 From: Joseph Nuthalapati Date: Fri, 30 Oct 2020 17:38:39 +0530 Subject: [PATCH] openvpn: ECC: Setup and Migration Signed-off-by: Joseph Nuthalapati Reviewed-by: James Valleroy --- actions/openvpn | 42 +++++++++++------ plinth/modules/openvpn/__init__.py | 2 +- .../openvpn/templates/migrate_to_ecc.inc | 37 +++++++++++++++ plinth/modules/openvpn/templates/openvpn.html | 4 ++ plinth/modules/openvpn/tests/conftest.py | 45 +++++++++++++++++++ .../openvpn/tests/test_configuration.py | 21 +++++++++ plinth/modules/openvpn/urls.py | 1 + plinth/modules/openvpn/views.py | 12 ++++- 8 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 plinth/modules/openvpn/templates/migrate_to_ecc.inc create mode 100644 plinth/modules/openvpn/tests/conftest.py diff --git a/actions/openvpn b/actions/openvpn index 2aa17950e..1245a68c5 100755 --- a/actions/openvpn +++ b/actions/openvpn @@ -14,7 +14,11 @@ from plinth import action_utils, utils KEYS_DIRECTORY = '/etc/openvpn/freedombox-keys' -DH_KEY = '/etc/openvpn/freedombox-keys/pki/dh.pem' +DH_PARAMS = f'{KEYS_DIRECTORY}/pki/dh.pem' + +CURVE = 'secp521r1' + +EC_PARAMS = f'{KEYS_DIRECTORY}/pki/ecparams/{CURVE}.pem' SERVER_CONFIGURATION_PATH = '/etc/openvpn/server/freedombox.conf' @@ -32,16 +36,24 @@ port 1194 proto udp proto udp6 dev tun + client-to-client + ca /etc/openvpn/freedombox-keys/pki/ca.crt cert /etc/openvpn/freedombox-keys/pki/issued/server.crt key /etc/openvpn/freedombox-keys/pki/private/server.key -dh /etc/openvpn/freedombox-keys/pki/dh.pem + +dh none + server 10.91.0.0 255.255.255.0 keepalive 10 120 -cipher AES-256-CBC -comp-lzo verb 3 + +tls-server +tls-version-min 1.2 +tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384 +cipher AES-256-GCM +script-security 2 ''' CLIENT_CONFIGURATION = ''' @@ -52,8 +64,7 @@ proto udp6 dev tun nobind remote-cert-tls server -cipher AES-256-CBC -comp-lzo +cipher AES-256-GCM redirect-gateway verb 3 @@ -65,7 +76,9 @@ verb 3 CERTIFICATE_CONFIGURATION = { 'EASYRSA_BATCH': '1', - 'EASYRSA_KEY_SIZE': '4096', + 'EASYRSA_ALGO': 'ec', + 'EASYRSA_CURVE': CURVE, + 'EASYRSA_DIGEST': 'sha512', 'KEY_CONFIG': '/usr/share/easy-rsa/openssl-easyrsa.cnf', 'KEY_DIR': KEYS_DIRECTORY, 'EASYRSA_OPENSSL': 'openssl', @@ -103,7 +116,7 @@ def parse_arguments(): def _is_setup(): """Return whether setup is complete.""" - return utils.is_non_empty_file(DH_KEY) + return any(utils.is_non_empty_file(fil) for fil in [DH_PARAMS, EC_PARAMS]) def subcommand_is_setup(_): @@ -178,12 +191,13 @@ def _create_certificates(): pass _init_pki() - subprocess.check_call( - ['/usr/share/easy-rsa/easyrsa', 'build-ca', 'nopass'], **COMMON_ARGS) - subprocess.check_call([ - '/usr/share/easy-rsa/easyrsa', 'build-server-full', 'server', 'nopass' - ], **COMMON_ARGS) - subprocess.check_call(['/usr/share/easy-rsa/easyrsa', 'gen-dh'], + easy_rsa = '/usr/share/easy-rsa/easyrsa' + subprocess.check_call([easy_rsa, 'build-ca', 'nopass'], **COMMON_ARGS) + subprocess.check_call([easy_rsa, 'build-server-full', 'server', 'nopass'], + **COMMON_ARGS) + subprocess.check_call([easy_rsa, 'gen-req', 'server', 'nopass'], + **COMMON_ARGS) + subprocess.check_call([easy_rsa, 'sign-req', 'server', 'server'], **COMMON_ARGS) diff --git a/plinth/modules/openvpn/__init__.py b/plinth/modules/openvpn/__init__.py index 4cd43edfa..446a33954 100644 --- a/plinth/modules/openvpn/__init__.py +++ b/plinth/modules/openvpn/__init__.py @@ -107,7 +107,7 @@ def is_setup(): def is_using_ecc(): - """Return whether the service is using RSA.""" + """Return whether the service is using ECC.""" if os.path.exists(SERVER_CONFIGURATION_FILE): with open(SERVER_CONFIGURATION_FILE, 'r') as file_handle: for line in file_handle: diff --git a/plinth/modules/openvpn/templates/migrate_to_ecc.inc b/plinth/modules/openvpn/templates/migrate_to_ecc.inc new file mode 100644 index 000000000..18bf65fb3 --- /dev/null +++ b/plinth/modules/openvpn/templates/migrate_to_ecc.inc @@ -0,0 +1,37 @@ +{% load i18n %} + +
+ +

{% trans "Migrate to ECC" %}

+ +

+ {% blocktrans trimmed %} + Your OpenVPN installation is currently using RSA. Switching to the + modern Elliptic Curve Cryptography improves speed of establishing a + connection and security. This operation is irreversible. It should only take + a few minutes on most single board computers. + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + Existing client profiles will be invalidated by this operation. All + OpenVPN users on {{ box_name }} should download their new profiles. OpenVPN + clients compatible with ECC should be used to connect to this server. + {% endblocktrans %} +

+ +

+ {% blocktrans trimmed %} + All new installations of OpenVPN on {{ box_name }} will + use ECC by default. We recommend migrating as soon as possible. + {% endblocktrans %} +

+ +
+ {% csrf_token %} + + +
diff --git a/plinth/modules/openvpn/templates/openvpn.html b/plinth/modules/openvpn/templates/openvpn.html index af1a8ff4b..2757468bd 100644 --- a/plinth/modules/openvpn/templates/openvpn.html +++ b/plinth/modules/openvpn/templates/openvpn.html @@ -85,6 +85,10 @@ value="{% trans "Download my profile" %}"/> + {% if not using_ecc %} + {% include "migrate_to_ecc.inc" %} + {% endif %} + {% endif %} {% endblock %} diff --git a/plinth/modules/openvpn/tests/conftest.py b/plinth/modules/openvpn/tests/conftest.py new file mode 100644 index 000000000..832cbfdca --- /dev/null +++ b/plinth/modules/openvpn/tests/conftest.py @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Common test fixtures for OpenVPN. +""" + +import importlib +import pathlib +import types +from unittest.mock import patch + +import pytest + +current_directory = pathlib.Path(__file__).parent + + +def _load_actions_module(): + actions_file_path = str(current_directory / '..' / '..' / '..' / '..' / + 'actions' / 'openvpn') + loader = importlib.machinery.SourceFileLoader('openvpn', actions_file_path) + module = types.ModuleType(loader.name) + loader.exec_module(module) + return module + + +actions = _load_actions_module() + + +@pytest.fixture(name='keys_directory') +def fixture_keys_directory(tmp_path): + return tmp_path + + +@pytest.fixture(name='call_action') +def fixture_call_action(capsys, keys_directory): + """Run actions with overridden directory paths.""" + + def _call_action(module_name, args, **kwargs): + actions.DH_PARAMS = f'{keys_directory}/pki/dh.pem' + actions.EC_PARAMS = f'{keys_directory}/pki/ecparams/secp521r1.pem' + with patch('argparse._sys.argv', [module_name] + args): + actions.main() + captured = capsys.readouterr() + return captured.out + + return _call_action diff --git a/plinth/modules/openvpn/tests/test_configuration.py b/plinth/modules/openvpn/tests/test_configuration.py index fdeed6185..fdfe87e95 100644 --- a/plinth/modules/openvpn/tests/test_configuration.py +++ b/plinth/modules/openvpn/tests/test_configuration.py @@ -3,6 +3,7 @@ Test module for OpenVPN configuration. """ +import os from unittest.mock import patch import pytest @@ -30,3 +31,23 @@ def test_identify_ecc_configuration(conf_file): with open(conf_file, 'w') as file_handle: file_handle.write('dh none') assert openvpn.is_using_ecc() + + +def test_is_setup_with_rsa(keys_directory, call_action): + """is_setup should work with RSA configuration.""" + with patch('plinth.actions.superuser_run', call_action): + (keys_directory / 'pki').mkdir() + dh_params_file = keys_directory / 'pki' / 'dh.pem' + dh_params_file.write_text('some content') + assert openvpn.is_setup() + os.remove(dh_params_file) + + +def test_is_setup_with_ecc(keys_directory, call_action): + """is_setup should work with RSA configuration.""" + with patch('plinth.actions.superuser_run', call_action): + (keys_directory / 'pki' / 'ecparams').mkdir(parents=True) + ec_params_file = keys_directory / 'pki' / 'ecparams' / 'secp521r1.pem' + ec_params_file.write_text('some content') + assert openvpn.is_setup() + os.remove(ec_params_file) diff --git a/plinth/modules/openvpn/urls.py b/plinth/modules/openvpn/urls.py index 3bd56c108..6045e4607 100644 --- a/plinth/modules/openvpn/urls.py +++ b/plinth/modules/openvpn/urls.py @@ -12,6 +12,7 @@ from . import views urlpatterns = [ url(r'^apps/openvpn/$', views.OpenVPNAppView.as_view(), name='index'), url(r'^apps/openvpn/setup/$', views.setup, name='setup'), + url(r'^apps/openvpn/ecc/$', views.ecc, name='ecc'), url(r'^apps/openvpn/profile/$', non_admin_view(views.profile), name='profile'), ] diff --git a/plinth/modules/openvpn/views.py b/plinth/modules/openvpn/views.py index 8884bfbbc..a57bee893 100644 --- a/plinth/modules/openvpn/views.py +++ b/plinth/modules/openvpn/views.py @@ -39,11 +39,12 @@ class OpenVPNAppView(AppView): } context['refresh_page_sec'] = 3 if context['status'][ 'setup_running'] else None + context['using_ecc'] = openvpn.is_using_ecc() return context @require_POST -def setup(request): +def setup(_): """Start the setup process.""" if not openvpn.is_setup() and not openvpn.setup_process: openvpn.setup_process = actions.superuser_run('openvpn', ['setup'], @@ -73,6 +74,15 @@ def profile(request): return response +@require_POST +def ecc(_): + """Migrate from RSA to ECC.""" + if openvpn.is_setup(): + openvpn.setup_process = actions.superuser_run('openvpn', ['setup'], + run_in_background=True) + return redirect('openvpn:index') + + def _collect_setup_result(request): """Handle setup process is completion.""" if not openvpn.setup_process: