From 5a7c46016a23387312b2c9e18528ad7bb20e8366 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 6 Jul 2021 23:19:48 +0200 Subject: pki: T3642: Migrate rsa-keys to PKI configuration --- data/configd-include.json | 1 - data/templates/ipsec/swanctl.conf.tmpl | 11 +- data/templates/ipsec/swanctl/peer.tmpl | 4 +- debian/control | 1 - .../include/ipsec/authentication-rsa.xml.i | 30 +++++ interface-definitions/vpn_ipsec.xml.in | 6 +- interface-definitions/vpn_rsa-keys.xml.in | 47 -------- op-mode-definitions/vpn-ipsec.xml.in | 50 --------- smoketest/configs/pki-ipsec | 26 +++++ src/conf_mode/vpn_ipsec.py | 122 +++++++++++--------- src/conf_mode/vpn_rsa-keys.py | 113 ------------------- src/migration-scripts/ipsec/7-to-8 | 125 +++++++++++++++++++++ src/op_mode/vpn_ipsec.py | 94 +--------------- 13 files changed, 260 insertions(+), 370 deletions(-) create mode 100644 interface-definitions/include/ipsec/authentication-rsa.xml.i delete mode 100644 interface-definitions/vpn_rsa-keys.xml.in delete mode 100755 src/conf_mode/vpn_rsa-keys.py create mode 100755 src/migration-scripts/ipsec/7-to-8 diff --git a/data/configd-include.json b/data/configd-include.json index d228ac8a3..a03360bdb 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -67,7 +67,6 @@ "tftp_server.py", "vpn_l2tp.py", "vpn_pptp.py", -"vpn_rsa-keys.py", "vpn_sstp.py", "vrf.py", "vrrp.py", diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index 00251d44d..a6ab73cc2 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -48,7 +48,6 @@ secrets { {% endfor %} {% endif %} {% if site_to_site is defined and site_to_site.peer is defined %} -{% set ns = namespace(local_key_set=False) %} {% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address and peer_conf.disable is not defined %} {% set peer_name = peer.replace(".", "-").replace("@", "") %} {% if peer_conf.authentication.mode == 'pre-shared-secret' %} @@ -72,10 +71,12 @@ secrets { secret = "{{ peer_conf.authentication.x509.passphrase }}" {% endif %} } -{% elif peer_conf.authentication.mode == 'rsa' and not ns.local_key_set %} -{% set ns.local_key_set = True %} - rsa_local { - file = {{ rsa_local_key }} +{% elif peer_conf.authentication.mode == 'rsa' %} + rsa_{{ peer_name }}_local { + file = {{ peer_conf.authentication.rsa.local_key }}.pem +{% if peer_conf.authentication.rsa.passphrase is defined %} + secret = "{{ peer_conf.authentication.rsa.passphrase }}" +{% endif %} } {% endif %} {% endfor %} diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl index 4ace06701..8e46e8892 100644 --- a/data/templates/ipsec/swanctl/peer.tmpl +++ b/data/templates/ipsec/swanctl/peer.tmpl @@ -38,7 +38,7 @@ {% if peer_conf.authentication.mode == 'x509' %} certs = {{ peer_conf.authentication.x509.certificate }}.pem {% elif peer_conf.authentication.mode == 'rsa' %} - pubkeys = localhost.pub + pubkeys = {{ peer_conf.authentication.rsa.local_key }}.pem {% endif %} } remote { @@ -49,7 +49,7 @@ {% endif %} auth = {{ 'psk' if peer_conf.authentication.mode == 'pre-shared-secret' else 'pubkey' }} {% if peer_conf.authentication.mode == 'rsa' %} - pubkeys = {{ peer_conf.authentication.rsa_key_name }}.pub + pubkeys = {{ peer_conf.authentication.rsa.remote_key }}.pem {% endif %} } children { diff --git a/debian/control b/debian/control index 7ff64e5a3..9ac82cf03 100644 --- a/debian/control +++ b/debian/control @@ -111,7 +111,6 @@ Depends: procps, python3, python3-certbot-nginx, - python3-pycryptodome, python3-cryptography, python3-flask, python3-hurry.filesize, diff --git a/interface-definitions/include/ipsec/authentication-rsa.xml.i b/interface-definitions/include/ipsec/authentication-rsa.xml.i new file mode 100644 index 000000000..0a364e838 --- /dev/null +++ b/interface-definitions/include/ipsec/authentication-rsa.xml.i @@ -0,0 +1,30 @@ + + + + RSA keys + + + + + Name of PKI key-pair with local private key + + pki key-pair + + + + + + Local private key passphrase + + + + + Name of PKI key-pair with remote public key + + pki key-pair + + + + + + diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 4425ab02a..147f351f2 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -922,6 +922,7 @@ #include + #include #include @@ -964,11 +965,6 @@ - - - RSA key name - - Use certificate common name as ID diff --git a/interface-definitions/vpn_rsa-keys.xml.in b/interface-definitions/vpn_rsa-keys.xml.in deleted file mode 100644 index 2d8e97f4f..000000000 --- a/interface-definitions/vpn_rsa-keys.xml.in +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - RSA keys - 900 - - - - - Local RSA key - - - - - Local RSA key file location - - txt - File in /config/auth or /config/ipsec.d/rsa-keys - - - - - - - - Name of remote RSA key - - - - - Remote RSA key - - txt - Remote RSA key - - - - - - - - - - diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index fe0597eed..20f275e9b 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -1,49 +1,5 @@ - - - - - VPN key generation utility - - - - - Generate local RSA key (default: bits=2192) - - - - - Generate local RSA key with specified number of bits - - <16-4096> - - - sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="rsa-key" --bits="$5" - - - sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="rsa-key" --bits="2192" - - - - x509 key-pair generation tool - - - - - Generate x509 key-pair - - <common-name> - - - sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="x509" --name="$5" - - - - - - - @@ -139,12 +95,6 @@ Show Internet Key Exchange (IKE) information - - - Show VPN RSA keys - - sudo ${vyos_op_scripts_dir}/vpn_ipsec.py --action="rsa-key-show" - Show all currently active IKE Security Associations (SA) diff --git a/smoketest/configs/pki-ipsec b/smoketest/configs/pki-ipsec index 7708a3cdd..5025117f7 100644 --- a/smoketest/configs/pki-ipsec +++ b/smoketest/configs/pki-ipsec @@ -85,6 +85,32 @@ vpn { } } } + peer 192.168.150.3 { + authentication { + mode rsa + pre-shared-secret MYSECRETKEY + rsa-key-name peer2 + } + default-esp-group MyESPGroup + ike-group MyIKEGroup + local-address 192.168.150.1 + tunnel 0 { + local { + prefix 172.20.0.0/24 + } + remote { + prefix 172.22.0.0/24 + } + } + } + } + } + rsa-keys { + local-key { + file /config/auth/ovpn_test_server.key + } + rsa-key-name peer2 { + rsa-key 0sAwEAAbudt5WQZSW2plbixjpgx4yVN/WMHdYRIZhyypJWO4ujQ/UQS9j3oTBgV2+RLtQ0YQ7eocwIfkvJVUnnZVMyZ4asQMOarQgbQ5nFGliCcDOMtNXRxHlMsvmjLx4o6FWbGukwgoxsT2x915n0XMn4XJNNSIEQotxj2GWFhEfBSPHyOM++kODk0lkbE7mLeHMMFq02vQhoczzEPWxjUUoY3jywhmHMfb4PdAKLFyt9x40znmPCYh+NSMQmpBXtD3gjGtX62bgrqKuP3BJU44x1gLlv8rJAJ4SY74YKnFUZ8m5GSbnVapwPOrp65lJZFKOGs2XXjAp5leoR+wmSYyqbDJM= } } } diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 53a50fa1e..3fab8e868 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -26,6 +26,8 @@ from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists from vyos.configdict import dict_merge from vyos.ifconfig import Interface +from vyos.pki import encode_public_key +from vyos.pki import load_private_key from vyos.pki import wrap_certificate from vyos.pki import wrap_crl from vyos.pki import wrap_public_key @@ -57,6 +59,7 @@ default_install_routes = 'yes' vici_socket = '/var/run/charon.vici' CERT_PATH = f'{swanctl_dir}/x509/' +PUBKEY_PATH = f'{swanctl_dir}/pubkey/' KEY_PATH = f'{swanctl_dir}/private/' CA_PATH = f'{swanctl_dir}/x509ca/' CRL_PATH = f'{swanctl_dir}/x509crl/' @@ -64,9 +67,6 @@ CRL_PATH = f'{swanctl_dir}/x509crl/' DHCP_BASE = '/var/lib/dhcp/dhclient' DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting' -LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] -X509_PATH = '/config/auth/' - def get_config(config=None): if config: conf = config @@ -116,32 +116,9 @@ def get_config(config=None): ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - ipsec['rsa_keys'] = conf.get_config_dict(['vpn', 'rsa-keys'], - key_mangling=('-', '_'), - get_first_key=True, - no_tag_node_value_mangle=True) return ipsec -def get_rsa_local_key(ipsec): - return dict_search_args(ipsec['rsa_keys'], 'local_key', 'file') - -def verify_rsa_local_key(ipsec): - file = get_rsa_local_key(ipsec) - - if not file: - return False - - for path in LOCAL_KEY_PATHS: - full_path = os.path.join(path, file) - if os.path.exists(full_path): - return full_path - - return False - -def verify_rsa_key(ipsec, key_name): - return dict_search_args(ipsec['rsa_keys'], 'rsa_key_name', key_name, 'rsa_key') - def get_dhcp_address(iface): addresses = Interface(iface).get_addr() if not addresses: @@ -151,7 +128,7 @@ def get_dhcp_address(iface): return ip_from_cidr(address) return None -def verify_pki(pki, x509_conf): +def verify_pki_x509(pki, x509_conf): if not pki or 'ca' not in pki or 'certificate' not in pki: raise ConfigError(f'PKI is not configured') @@ -169,6 +146,21 @@ def verify_pki(pki, x509_conf): return True +def verify_pki_rsa(pki, rsa_conf): + if not pki or 'key_pair' not in pki: + raise ConfigError(f'PKI is not configured') + + local_key = rsa_conf['local_key'] + remote_key = rsa_conf['remote_key'] + + if not dict_search_args(pki, 'key_pair', local_key, 'private', 'key'): + raise ConfigError(f'Missing private key on specified local-key "{local_key}"') + + if not dict_search_args(pki, 'key_pair', remote_key, 'public', 'key'): + raise ConfigError(f'Missing public key on specified remote-key "{remote_key}"') + + return True + def verify(ipsec): if not ipsec: return None @@ -224,7 +216,7 @@ def verify(ipsec): if 'ca_certificate' not in x509 or 'certificate' not in x509: raise ConfigError(f"Missing x509 certificates on {name} remote-access config") - verify_pki(ipsec['pki'], x509) + verify_pki_x509(ipsec['pki'], x509) elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret': if 'pre_shared_secret' not in ra_conf['authentication']: raise ConfigError(f"Missing pre-shared-key on {name} remote-access config") @@ -255,17 +247,20 @@ def verify(ipsec): if 'ca_certificate' not in x509 or 'certificate' not in x509: raise ConfigError(f"Missing x509 certificates on site-to-site peer {peer}") - verify_pki(ipsec['pki'], x509) + verify_pki_x509(ipsec['pki'], x509) + elif peer_conf['authentication']['mode'] == 'rsa': + if 'rsa' not in peer_conf['authentication']: + raise ConfigError(f"Missing RSA settings on site-to-site peer {peer}") - if peer_conf['authentication']['mode'] == 'rsa': - if not verify_rsa_local_key(ipsec): - raise ConfigError(f"Invalid key on rsa-keys local-key") + rsa = peer_conf['authentication']['rsa'] - if 'rsa_key_name' not in peer_conf['authentication']: - raise ConfigError(f"Missing rsa-key-name on site-to-site peer {peer}") + if 'local_key' not in rsa: + raise ConfigError(f"Missing RSA local-key on site-to-site peer {peer}") - if not verify_rsa_key(ipsec, peer_conf['authentication']['rsa_key_name']): - raise ConfigError(f"Invalid rsa-key-name on site-to-site peer {peer}") + if 'remote_key' not in rsa: + raise ConfigError(f"Missing RSA remote-key on site-to-site peer {peer}") + + verify_pki_rsa(ipsec['pki'], rsa) if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf: raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}") @@ -322,7 +317,7 @@ def verify(ipsec): raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") def cleanup_pki_files(): - for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH]: + for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]: if not os.path.exists(path): continue for file in os.listdir(path): @@ -330,7 +325,7 @@ def cleanup_pki_files(): if os.path.isfile(file_path): os.unlink(file_path) -def generate_pki_files(pki, x509_conf): +def generate_pki_files_x509(pki, x509_conf): ca_cert_name = x509_conf['ca_certificate'] ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate') ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or [] @@ -352,9 +347,27 @@ def generate_pki_files(pki, x509_conf): with open(os.path.join(CERT_PATH, f'{cert_name}.pem'), 'w') as f: f.write(wrap_certificate(cert_data)) - with open(os.path.join(KEY_PATH, f'{cert_name}.pem'), 'w') as f: + with open(os.path.join(KEY_PATH, f'x509_{cert_name}.pem'), 'w') as f: f.write(wrap_private_key(key_data, protected)) +def generate_pki_files_rsa(pki, rsa_conf): + local_key_name = rsa_conf['local_key'] + local_key_data = dict_search_args(pki, 'key_pair', local_key_name, 'private', 'key') + protected = 'passphrase' in rsa_conf + remote_key_name = rsa_conf['remote_key'] + remote_key_data = dict_search_args(pki, 'key_pair', remote_key_name, 'public', 'key') + + local_key = load_private_key(local_key_data, rsa_conf['passphrase'] if protected else None) + + with open(os.path.join(KEY_PATH, f'rsa_{local_key_name}.pem'), 'w') as f: + f.write(wrap_private_key(local_key_data, protected)) + + with open(os.path.join(PUBKEY_PATH, f'{local_key_name}.pem'), 'w') as f: + f.write(encode_public_key(local_key.public_key())) + + with open(os.path.join(PUBKEY_PATH, f'{remote_key_name}.pem'), 'w') as f: + f.write(wrap_public_key(remote_key_data)) + def generate(ipsec): cleanup_pki_files() @@ -369,28 +382,27 @@ def generate(ipsec): with open(DHCP_HOOK_IFLIST, 'w') as f: f.write(" ".join(ipsec['dhcp_no_address'].values())) - data = ipsec - data['rsa_local_key'] = verify_rsa_local_key(ipsec) - - for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH]: + for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH, PUBKEY_PATH]: if not os.path.exists(path): os.mkdir(path, mode=0o755) if not os.path.exists(KEY_PATH): os.mkdir(KEY_PATH, mode=0o700) - if 'remote_access' in data: + if 'remote_access' in ipsec: for rw, rw_conf in ipsec['remote_access'].items(): if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']: - generate_pki_files(ipsec['pki'], rw_conf['authentication']['x509']) + generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509']) - if 'site_to_site' in data and 'peer' in data['site_to_site']: + if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): if peer in ipsec['dhcp_no_address']: continue if peer_conf['authentication']['mode'] == 'x509': - generate_pki_files(ipsec['pki'], peer_conf['authentication']['x509']) + generate_pki_files_x509(ipsec['pki'], peer_conf['authentication']['x509']) + elif peer_conf['authentication']['mode'] == 'rsa': + generate_pki_files_rsa(ipsec['pki'], peer_conf['authentication']['rsa']) local_ip = '' if 'local_address' in peer_conf: @@ -398,7 +410,7 @@ def generate(ipsec): elif 'dhcp_interface' in peer_conf: local_ip = get_dhcp_address(peer_conf['dhcp_interface']) - data['site_to_site']['peer'][peer]['local_address'] = local_ip + ipsec['site_to_site']['peer'][peer]['local_address'] = local_ip if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): @@ -417,15 +429,15 @@ def generate(ipsec): if local_net.overlaps(remote_net): passthrough.append(local_prefix) - data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough + ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough - render(ipsec_conf, 'ipsec/ipsec.conf.tmpl', data) - render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', data) - render(charon_conf, 'ipsec/charon.tmpl', data) - render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', data) - render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', data) - render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', data) + render(ipsec_conf, 'ipsec/ipsec.conf.tmpl', ipsec) + render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec) + render(charon_conf, 'ipsec/charon.tmpl', ipsec) + render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec) + render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec) + render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec) def resync_l2tp(ipsec): if ipsec and not ipsec['l2tp_exists']: diff --git a/src/conf_mode/vpn_rsa-keys.py b/src/conf_mode/vpn_rsa-keys.py deleted file mode 100755 index 83de93088..000000000 --- a/src/conf_mode/vpn_rsa-keys.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import base64 -import os -import struct - -from sys import exit - -from vyos.config import Config -from vyos.util import call -from vyos import ConfigError -from vyos import airbag -from Cryptodome.PublicKey.RSA import construct - -airbag.enable() - -LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] -LOCAL_OUTPUT = '/etc/swanctl/pubkey/localhost.pub' -LOCAL_KEY_OUTPUT = '/etc/swanctl/private/localhost.key' - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - base = ['vpn', 'rsa-keys'] - if not conf.exists(base): - return None - - return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - -def verify(conf): - if not conf: - return - - if 'local_key' in conf and 'file' in conf['local_key']: - local_key = conf['local_key']['file'] - if not local_key: - raise ConfigError(f'Invalid local-key') - - if not get_local_key(local_key): - raise ConfigError(f'File not found for local-key: {local_key}') - -def get_local_key(local_key): - for path in LOCAL_KEY_PATHS: - full_path = os.path.join(path, local_key) - if os.path.exists(full_path): - return full_path - return False - -def generate(conf): - if not conf: - return - - if 'local_key' in conf and 'file' in conf['local_key']: - local_key = conf['local_key']['file'] - local_key_path = get_local_key(local_key) - call(f'sudo cp -f {local_key_path} {LOCAL_KEY_OUTPUT}') - call(f'sudo /usr/bin/openssl rsa -in {local_key_path} -pubout -out {LOCAL_OUTPUT}') - - if 'rsa_key_name' in conf: - for key_name, key_conf in conf['rsa_key_name'].items(): - if 'rsa_key' not in key_conf: - continue - - remote_key = key_conf['rsa_key'] - - if remote_key[:2] == "0s": # Vyatta format - remote_key = migrate_from_vyatta_key(remote_key) - else: - remote_key = bytes('-----BEGIN PUBLIC KEY-----\n' + remote_key + '\n-----END PUBLIC KEY-----\n', 'utf-8') - - with open(f'/etc/swanctl/pubkey/{key_name}.pub', 'wb') as f: - f.write(remote_key) - -def migrate_from_vyatta_key(data): - data = base64.b64decode(data[2:]) - length = struct.unpack('B', data[:1])[0] - e = int.from_bytes(data[1:1+length], 'big') - n = int.from_bytes(data[1+length:], 'big') - pubkey = construct((n, e)) - return pubkey.exportKey(format='PEM') - -def apply(conf): - if not conf: - return - - call('sudo /usr/sbin/ipsec rereadall') - call('sudo /usr/sbin/ipsec reload') - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/migration-scripts/ipsec/7-to-8 b/src/migration-scripts/ipsec/7-to-8 new file mode 100755 index 000000000..5d48b2875 --- /dev/null +++ b/src/migration-scripts/ipsec/7-to-8 @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Migrate rsa keys into PKI configuration + +import base64 +import os +import struct + +from cryptography.hazmat.primitives.asymmetric import rsa + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.pki import load_public_key +from vyos.pki import load_private_key +from vyos.pki import encode_public_key +from vyos.pki import encode_private_key + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +pki_base = ['pki'] +ipsec_site_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] +rsa_keys_base = ['vpn', 'rsa-keys'] + +config = ConfigTree(config_file) + +LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] + +def migrate_from_vyatta_key(data): + data = base64.b64decode(data[2:]) + length = struct.unpack('B', data[:1])[0] + e = int.from_bytes(data[1:1+length], 'big') + n = int.from_bytes(data[1+length:], 'big') + public_numbers = rsa.RSAPublicNumbers(e, n) + return public_numbers.public_key() + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +local_key_name = 'localhost' + +if config.exists(rsa_keys_base): + if not config.exists(pki_base + ['key-pair']): + config.set(pki_base + ['key-pair']) + config.set_tag(pki_base + ['key-pair']) + + if config.exists(rsa_keys_base + ['local-key', 'file']): + local_file = config.return_value(rsa_keys_base + ['local-key', 'file']) + local_path = None + local_key = None + + for path in LOCAL_KEY_PATHS: + full_path = os.path.join(path, local_file) + if os.path.exists(full_path): + local_path = full_path + break + + if local_path: + with open(local_path, 'r') as f: + local_key_data = f.read() + local_key = load_private_key(local_key_data, wrap_tags=False) + + if local_key: + local_key_pem = encode_private_key(local_key) + config.set(pki_base + ['key-pair', local_key_name, 'private', 'key'], value=wrapped_pem_to_config_value(local_key_pem)) + else: + print('Failed to migrate local RSA key') + + if config.exists(rsa_keys_base + ['rsa-key-name']): + for rsa_name in config.list_nodes(rsa_keys_base + ['rsa-key-name']): + if not config.exists(rsa_keys_base + ['rsa-key-name', rsa_name, 'rsa-key']): + continue + + vyatta_key = config.return_value(rsa_keys_base + ['rsa-key-name', rsa_name, 'rsa-key']) + public_key = migrate_from_vyatta_key(vyatta_key) + + if public_key: + public_key_pem = encode_public_key(public_key) + config.set(pki_base + ['key-pair', rsa_name, 'public', 'key'], value=wrapped_pem_to_config_value(public_key_pem)) + else: + print(f'Failed to migrate rsa-key "{rsa_name}"') + + config.delete(rsa_keys_base) + +if config.exists(ipsec_site_base): + for peer in config.list_nodes(ipsec_site_base): + mode = config.return_value(ipsec_site_base + [peer, 'authentication', 'mode']) + + if mode != 'rsa': + continue + + config.set(ipsec_site_base + [peer, 'authentication', 'rsa', 'local-key'], value=local_key_name) + + remote_key_name = config.return_value(ipsec_site_base + [peer, 'authentication', 'rsa-key-name']) + config.set(ipsec_site_base + [peer, 'authentication', 'rsa', 'remote-key'], value=remote_key_name) + config.delete(ipsec_site_base + [peer, 'authentication', 'rsa-key-name']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index ad7efbf2d..06e227ccf 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -14,91 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import base64 -import os import re -import struct -import sys import argparse from subprocess import TimeoutExpired -from vyos.util import ask_yes_no, call, cmd, process_named_running -from Cryptodome.PublicKey.RSA import importKey +from vyos.util import call -RSA_LOCAL_KEY_PATH = '/config/ipsec.d/rsa-keys/localhost.key' -RSA_LOCAL_PUB_PATH = '/etc/ipsec.d/certs/localhost.pub' -RSA_KEY_PATHS = ['/config/auth', '/config/ipsec.d/rsa-keys'] - -X509_CONFIG_PATH = '/etc/ipsec.d/key-pair.template' -X509_PATH = '/config/auth/' - -IPSEC_CONF = '/etc/ipsec.conf' SWANCTL_CONF = '/etc/swanctl/swanctl.conf' -def migrate_to_vyatta_key(path): - with open(path, 'r') as f: - key = importKey(f.read()) - e = key.e.to_bytes((key.e.bit_length() + 7) // 8, 'big') - n = key.n.to_bytes((key.n.bit_length() + 7) // 8, 'big') - return '0s' + str(base64.b64encode(struct.pack('B', len(e)) + e + n), 'ascii') - return None - -def find_rsa_keys(): - keys = [] - for path in RSA_KEY_PATHS: - if not os.path.exists(path): - continue - for filename in os.listdir(path): - full_path = os.path.join(path, filename) - if os.path.isfile(full_path) and full_path.endswith(".key"): - keys.append(full_path) - return keys - -def show_rsa_keys(): - for key_path in find_rsa_keys(): - print('Private key: ' + os.path.basename(key_path)) - print('Public key: ' + migrate_to_vyatta_key(key_path) + '\n') - -def generate_rsa_key(bits = 2192): - if (bits < 16 or bits > 4096) or bits % 16 != 0: - print('Invalid bit length') - return - - if os.path.exists(RSA_LOCAL_KEY_PATH): - if not ask_yes_no("A local RSA key file already exists and will be overwritten. Continue?"): - return - - print(f'Generating rsa-key to {RSA_LOCAL_KEY_PATH}') - - directory = os.path.dirname(RSA_LOCAL_KEY_PATH) - call(f'sudo mkdir -p {directory}') - result = call(f'sudo /usr/bin/openssl genrsa -out {RSA_LOCAL_KEY_PATH} {bits}') - - if result != 0: - print(f'Could not generate RSA key: {result}') - return - - call(f'sudo /usr/bin/openssl rsa -inform PEM -in {RSA_LOCAL_KEY_PATH} -pubout -out {RSA_LOCAL_PUB_PATH}') - - print('Your new local RSA key has been generated') - print('The public portion of the key is:\n') - print(migrate_to_vyatta_key(RSA_LOCAL_KEY_PATH)) - -def generate_x509_pair(name): - if os.path.exists(X509_PATH + name): - if not ask_yes_no("A certificate request with this name already exists and will be overwritten. Continue?"): - return - - result = os.system(f'openssl req -new -nodes -keyout {X509_PATH}{name}.key -out {X509_PATH}{name}.csr -config {X509_CONFIG_PATH}') - - if result != 0: - print(f'Could not generate x509 key-pair: {result}') - return - - print('Private key and certificate request has been generated') - print(f'CSR: {X509_PATH}{name}.csr') - print(f'Private key: {X509_PATH}{name}.key') - def get_peer_connections(peer, tunnel, return_all = False): search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' matches = [] @@ -183,23 +106,12 @@ def debug_peer(peer, tunnel): if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--action', help='Control action', required=True) - parser.add_argument('--bits', help='Bits for rsa-key', required=False) - parser.add_argument('--name', help='Name for x509 key-pair, peer for reset', required=False) + parser.add_argument('--name', help='Name for peer reset', required=False) parser.add_argument('--tunnel', help='Specific tunnel of peer', required=False) args = parser.parse_args() - if args.action == 'rsa-key': - bits = int(args.bits) if args.bits else 2192 - generate_rsa_key(bits) - elif args.action == 'rsa-key-show': - show_rsa_keys() - elif args.action == 'x509': - if not args.name: - print('Invalid name for key-pair, aborting.') - sys.exit(0) - generate_x509_pair(args.name) - elif args.action == 'reset-peer': + if args.action == 'reset-peer': reset_peer(args.name, args.tunnel) elif args.action == "reset-profile": reset_profile(args.name, args.tunnel) -- cgit v1.2.3