From 20c4d06c717cd34e099cef942f86776b9b838e58 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 5 Jul 2021 14:12:35 +0200 Subject: pki: T3642: Support for adding SANs on certificate requests --- python/vyos/pki.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) (limited to 'python') diff --git a/python/vyos/pki.py b/python/vyos/pki.py index a575ac16a..1c6282d84 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -15,6 +15,7 @@ # along with this program. If not, see . import datetime +import ipaddress from cryptography import x509 from cryptography.exceptions import InvalidSignature @@ -112,7 +113,7 @@ def create_private_key(key_type, key_size=None): private_key = ec.generate_private_key(curve) return private_key -def create_certificate_request(subject, private_key): +def create_certificate_request(subject, private_key, subject_alt_names=[]): subject_obj = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, subject['country']), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, subject['state']), @@ -120,9 +121,20 @@ def create_certificate_request(subject, private_key): x509.NameAttribute(NameOID.ORGANIZATION_NAME, subject['organization']), x509.NameAttribute(NameOID.COMMON_NAME, subject['common_name'])]) - return x509.CertificateSigningRequestBuilder() \ - .subject_name(subject_obj) \ - .sign(private_key, hashes.SHA256()) + builder = x509.CertificateSigningRequestBuilder() \ + .subject_name(subject_obj) + + if subject_alt_names: + alt_names = [] + for obj in subject_alt_names: + if isinstance(obj, ipaddress.IPv4Address) or isinstance(obj, ipaddress.IPv6Address): + alt_names.append(x509.IPAddress(obj)) + elif isinstance(obj, str): + alt_names.append(x509.DNSName(obj)) + if alt_names: + builder = builder.add_extension(x509.SubjectAlternativeName(alt_names), critical=False) + + return builder.sign(private_key, hashes.SHA256()) def add_key_identifier(ca_cert): try: @@ -166,7 +178,7 @@ def create_certificate(cert_req, ca_cert, ca_private_key, valid_days=365, cert_t builder = builder.add_extension(add_key_identifier(ca_cert), critical=False) for ext in cert_req.extensions: - builder = builder.add_extension(ext, critical=False) + builder = builder.add_extension(ext.value, critical=False) return builder.sign(ca_private_key, hashes.SHA256()) -- cgit v1.2.3 From da02980779821862eed8966fd9e9258b807eb03d Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 5 Jul 2021 14:13:57 +0200 Subject: pki: ipsec: T3642: Fix issue with '.' being present in tag nodes, adds new vyos.util method `dict_search_args` to allow for dot characters in keys. --- python/vyos/util.py | 13 +++++++++++++ src/conf_mode/vpn_ipsec.py | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) (limited to 'python') diff --git a/python/vyos/util.py b/python/vyos/util.py index 8247ccb2d..c64b477ef 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -705,6 +705,19 @@ def dict_search(path, my_dict): c = c.get(p, {}) return c.get(parts[-1], None) +def dict_search_args(dict_object, *path): + # Traverse dictionary using variable arguments + # Added due to above function not allowing for '.' in the key names + # Example: dict_search_args(some_dict, 'key', 'subkey', 'subsubkey', ...) + if not isinstance(dict_object, dict) or not path: + return None + + for item in path: + if item not in dict_object: + return None + dict_object = dict_object[item] + return dict_object + def get_interface_config(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 50223320d..76ee64a20 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -33,7 +33,7 @@ from vyos.template import ip_from_cidr from vyos.template import render from vyos.validate import is_ipv6_link_local from vyos.util import call -from vyos.util import dict_search +from vyos.util import dict_search_args from vyos.util import run from vyos.xml import defaults from vyos import ConfigError @@ -116,7 +116,7 @@ def get_config(config=None): return ipsec def get_rsa_local_key(ipsec): - return dict_search('local_key.file', ipsec['rsa_keys']) + return dict_search_args(ipsec['rsa_keys'], 'local_key', 'file') def verify_rsa_local_key(ipsec): file = get_rsa_local_key(ipsec) @@ -132,7 +132,7 @@ def verify_rsa_local_key(ipsec): return False def verify_rsa_key(ipsec, key_name): - return dict_search(f'rsa_key_name.{key_name}.rsa_key', ipsec['rsa_keys']) + return dict_search_args(ipsec['rsa_keys'], 'rsa_key_name', key_name, 'rsa_key') def get_dhcp_address(iface): addresses = Interface(iface).get_addr() @@ -150,13 +150,13 @@ def verify_pki(pki, x509_conf): ca_cert_name = x509_conf['ca_certificate'] cert_name = x509_conf['certificate'] - if not dict_search(f'ca.{ca_cert_name}.certificate', ipsec['pki']): + if not dict_search_args(ipsec['pki'], 'ca', ca_cert_name, 'certificate'): raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"') - if not dict_search(f'certificate.{cert_name}.certificate', ipsec['pki']): + if not dict_search_args(ipsec['pki'], 'certificate', cert_name, 'certificate'): raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"') - if not dict_search(f'certificate.{cert_name}.private.key', ipsec['pki']): + if not dict_search_args(ipsec['pki'], 'certificate', cert_name, 'private', 'key'): raise ConfigError(f'Missing private key on specified PKI certificate "{cert_name}"') return True @@ -284,13 +284,13 @@ def verify(ipsec): def generate_pki_files(pki, x509_conf): ca_cert_name = x509_conf['ca_certificate'] - ca_cert_data = dict_search(f'ca.{ca_cert_name}.certificate', pki) - ca_cert_crls = dict_search(f'ca.{ca_cert_name}.crl', pki) or [] + 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 [] crl_index = 1 cert_name = x509_conf['certificate'] - cert_data = dict_search(f'certificate.{cert_name}.certificate', pki) - key_data = dict_search(f'certificate.{cert_name}.private.key', pki) + cert_data = dict_search_args(pki, 'certificate', cert_name, 'certificate') + key_data = dict_search_args(pki, 'certificate', cert_name, 'private', 'key') protected = 'passphrase' in x509_conf with open(os.path.join(CA_PATH, f'{ca_cert_name}.pem'), 'w') as f: @@ -351,8 +351,8 @@ def generate(ipsec): if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): - local_prefixes = dict_search('local.prefix', tunnel_conf) - remote_prefixes = dict_search('remote.prefix', tunnel_conf) + local_prefixes = dict_search_args(tunnel_conf, 'local', 'prefix') + remote_prefixes = dict_search_args(tunnel_conf, 'remote', 'prefix') if not local_prefixes or not remote_prefixes: continue -- cgit v1.2.3 From a5cd877a0a4a43644a6d91e6b95fe938b9b2726b Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 5 Jul 2021 21:58:43 +0200 Subject: ipsec: T2816: Migrate ipsec-settings.xml.in and charon.conf to vpn_ipsec.py Also adds check for the charon socket instead of an arbitrary sleep() --- data/configd-include.json | 1 - interface-definitions/ipsec-settings.xml.in | 25 ------------------------- python/vyos/util.py | 2 -- src/conf_mode/ipsec-settings.py | 7 ------- src/conf_mode/vpn_ipsec.py | 28 ++++++++++++++++++++++++---- 5 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 interface-definitions/ipsec-settings.xml.in (limited to 'python') diff --git a/data/configd-include.json b/data/configd-include.json index 2e6226097..d228ac8a3 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -27,7 +27,6 @@ "interfaces-wireguard.py", "interfaces-wireless.py", "interfaces-wwan.py", -"ipsec-settings.py", "lldp.py", "nat.py", "nat66.py", diff --git a/interface-definitions/ipsec-settings.xml.in b/interface-definitions/ipsec-settings.xml.in deleted file mode 100644 index 0bcba9a84..000000000 --- a/interface-definitions/ipsec-settings.xml.in +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - Global IPsec settings - 902 - - - - - - Do not automatically install routes to remote networks - - - - - - - - - diff --git a/python/vyos/util.py b/python/vyos/util.py index c64b477ef..171ab397f 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -440,7 +440,6 @@ def process_running(pid_file): pid = f.read().strip() return pid_exists(int(pid)) - def process_named_running(name): """ Checks if process with given name is running and returns its PID. If Process is not running, return None @@ -451,7 +450,6 @@ def process_named_running(name): return p.pid return None - def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable interval such as 1w4d18h35m59s diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index a65e8b567..a373f821f 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -29,7 +29,6 @@ from vyos import airbag airbag.enable() ra_conn_name = "remote-access" -charon_conf_file = "/etc/strongswan.d/charon.conf" ipsec_secrets_file = "/etc/ipsec.secrets" ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/" ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name @@ -46,10 +45,6 @@ def get_config(config=None): config = config else: config = Config() - data = {"install_routes": "yes"} - - if config.exists("vpn ipsec options disable-route-autoinstall"): - data["install_routes"] = "no" if config.exists("vpn ipsec ipsec-interfaces interface"): data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface") @@ -170,8 +165,6 @@ def verify(data): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): - render(charon_conf_file, 'ipsec/charon.tmpl', data) - if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) # old_umask = os.umask(0o077) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index cf23a89c6..53a50fa1e 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -19,6 +19,7 @@ import os from sys import exit from time import sleep +from time import time from vyos.config import Config from vyos.configdict import leaf_node_changed @@ -46,10 +47,15 @@ dhcp_wait_sleep = 1 swanctl_dir = '/etc/swanctl' ipsec_conf = '/etc/ipsec.conf' ipsec_secrets = '/etc/ipsec.secrets' +charon_conf = '/etc/strongswan.d/charon.conf' charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' interface_conf = '/etc/strongswan.d/interfaces_use.conf' swanctl_conf = f'{swanctl_dir}/swanctl.conf' +default_install_routes = 'yes' + +vici_socket = '/var/run/charon.vici' + CERT_PATH = f'{swanctl_dir}/x509/' KEY_PATH = f'{swanctl_dir}/private/' CA_PATH = f'{swanctl_dir}/x509ca/' @@ -101,6 +107,7 @@ def get_config(config=None): ipsec['remote_access'][rw]) ipsec['dhcp_no_address'] = {} + ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface']) ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access', @@ -352,9 +359,10 @@ def generate(ipsec): cleanup_pki_files() if not ipsec: - for config_file in [ipsec_conf, ipsec_secrets, interface_conf, swanctl_conf]: + for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) + render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes}) return if ipsec['dhcp_no_address']: @@ -371,7 +379,7 @@ def generate(ipsec): if not os.path.exists(KEY_PATH): os.mkdir(KEY_PATH, mode=0o700) - if 'remote_access' in ipsec: + if 'remote_access' in data: 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']) @@ -414,6 +422,7 @@ def generate(ipsec): 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) @@ -434,6 +443,17 @@ def resync_nhrp(ipsec): if tmp > 0: print('ERROR: failed to reapply NHRP settings!') +def wait_for_vici_socket(timeout=5, sleep_interval=0.1): + start_time = time() + test_command = f'sudo socat -u OPEN:/dev/null UNIX-CONNECT:{vici_socket}' + while True: + if (start_time + timeout) < time(): + return None + result = run(test_command) + if result == 0: + return True + sleep(sleep_interval) + def apply(ipsec): if not ipsec: call('sudo ipsec stop') @@ -445,8 +465,8 @@ def apply(ipsec): call('sudo ipsec rereadall') call('sudo ipsec reload') - sleep(5) # Give charon enough time to start - call('sudo swanctl -q') + if wait_for_vici_socket(): + call('sudo swanctl -q') resync_l2tp(ipsec) resync_nhrp(ipsec) -- cgit v1.2.3