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 --- src/conf_mode/vpn_ipsec.py | 122 +++++++++++++++++++++++------------------- src/conf_mode/vpn_rsa-keys.py | 113 -------------------------------------- 2 files changed, 67 insertions(+), 168 deletions(-) delete mode 100755 src/conf_mode/vpn_rsa-keys.py (limited to 'src/conf_mode') 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) -- cgit v1.2.3