openvpn: ECC: Setup and Migration

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Joseph Nuthalapati 2020-10-30 17:38:39 +05:30 committed by James Valleroy
parent eecd4b4d5f
commit 2b33a752d0
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
8 changed files with 148 additions and 16 deletions

View File

@ -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
<ca>
@ -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)

View File

@ -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:

View File

@ -0,0 +1,37 @@
{% load i18n %}
<hr/>
<h3>{% trans "Migrate to ECC" %}</h3>
<p>
{% 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 %}
</p>
<p>
{% 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 %}
</p>
<p>
{% blocktrans trimmed %}
All new installations of OpenVPN on {{ box_name }} will
use ECC by default. We recommend migrating as soon as possible.
{% endblocktrans %}
</p>
<form class="form form-ecc" method="post"
action="{% url 'openvpn:ecc' %}">
{% csrf_token %}
<input type="submit" class="btn btn-primary"
value="{% trans "Migrate" %}"/>
</form>

View File

@ -85,6 +85,10 @@
value="{% trans "Download my profile" %}"/>
</form>
{% if not using_ecc %}
{% include "migrate_to_ecc.inc" %}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -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

View File

@ -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)

View File

@ -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'),
]

View File

@ -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: