diff options
author | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2021-07-20 17:33:00 +0200 |
---|---|---|
committer | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2021-07-20 20:14:12 +0200 |
commit | bfadd6dfb5969f231097353a76ada3b839964a19 (patch) | |
tree | 5c9cae9c04121dd082c0a7a3e6d262df27c86489 | |
parent | 1554d3316eb74971d2ac7e3608173f6f113684e0 (diff) | |
download | vyos-1x-bfadd6dfb5969f231097353a76ada3b839964a19.tar.gz vyos-1x-bfadd6dfb5969f231097353a76ada3b839964a19.zip |
pki: eapol: T3642: Migrate EAPoL to use PKI configuration
-rw-r--r-- | data/templates/ethernet/wpa_supplicant.conf.tmpl | 8 | ||||
-rw-r--r-- | interface-definitions/include/interface/interface-eapol.xml.i | 5 | ||||
-rw-r--r-- | python/vyos/configverify.py | 35 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 41 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 31 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/22-to-23 | 130 |
6 files changed, 195 insertions, 55 deletions
diff --git a/data/templates/ethernet/wpa_supplicant.conf.tmpl b/data/templates/ethernet/wpa_supplicant.conf.tmpl index fe518ad45..308d777f1 100644 --- a/data/templates/ethernet/wpa_supplicant.conf.tmpl +++ b/data/templates/ethernet/wpa_supplicant.conf.tmpl @@ -32,11 +32,11 @@ fast_reauth=1 network={ {% if eapol is defined and eapol is not none %} -{% if eapol.ca_cert_file is defined and eapol.ca_cert_file is not none %} - ca_cert="{{ eapol.ca_cert_file }}" +{% if eapol.ca_certificate is defined and eapol.ca_certificate is not none %} + ca_cert="/run/wpa_supplicant/{{ ifname }}_ca.pem" {% endif %} - client_cert="{{ eapol.cert_file }}" - private_key="{{ eapol.key_file }}" + client_cert="/run/wpa_supplicant/{{ ifname }}_cert.pem" + private_key="/run/wpa_supplicant/{{ ifname }}_cert.key" {% endif %} # list of accepted authenticated key management protocols diff --git a/interface-definitions/include/interface/interface-eapol.xml.i b/interface-definitions/include/interface/interface-eapol.xml.i index 92b7a3f35..270ec5b13 100644 --- a/interface-definitions/include/interface/interface-eapol.xml.i +++ b/interface-definitions/include/interface/interface-eapol.xml.i @@ -4,9 +4,8 @@ <help>Extensible Authentication Protocol over Local Area Network</help> </properties> <children> - #include <include/certificate.xml.i> - #include <include/certificate-ca.xml.i> - #include <include/certificate-key.xml.i> + #include <include/pki/ca-certificate.xml.i> + #include <include/pki/certificate-key.xml.i> </children> </node> <!-- include end --> diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 979e28b11..58028b604 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -149,9 +149,38 @@ def verify_eapol(config): recurring validation of EAPoL configuration. """ if 'eapol' in config: - if not {'cert_file', 'key_file'} <= set(config['eapol']): - raise ConfigError('Both cert and key-file must be specified '\ - 'when using EAPoL!') + if 'certificate' not in config['eapol']: + raise ConfigError('Certificate must be specified when using EAPoL!') + + if 'certificate' not in config['pki']: + raise ConfigError('Invalid certificate specified for EAPoL') + + cert_name = config['eapol']['certificate'] + + if cert_name not in config['pki']['certificate']: + raise ConfigError('Invalid certificate specified for EAPoL') + + cert = config['pki']['certificate'][cert_name] + + if 'certificate' not in cert or 'private' not in cert or 'key' not in cert['private']: + raise ConfigError('Invalid certificate/private key specified for EAPoL') + + if 'password_protected' in cert['private']: + raise ConfigError('Encrypted private key cannot be used for EAPoL') + + if 'ca_certificate' in config['eapol']: + if 'ca' not in config['pki']: + raise ConfigError('Invalid CA certificate specified for EAPoL') + + ca_cert_name = config['eapol']['ca_certificate'] + + if ca_cert_name not in config['pki']['ca']: + raise ConfigError('Invalid CA certificate specified for EAPoL') + + ca_cert = config['pki']['ca'][cert_name] + + if 'certificate' not in ca_cert: + raise ConfigError('Invalid CA certificate specified for EAPoL') def verify_mirror(config): """ diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index a31d75423..a9cdab16a 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -25,9 +25,9 @@ from vyos.util import cmd from vyos.util import process_named_running from vyos.util import read_file -ca_cert = '/config/auth/eapol_test_ca.pem' -ssl_cert = '/config/auth/eapol_test_server.pem' -ssl_key = '/config/auth/eapol_test_server.key' +pki_path = ['pki'] +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' def get_wpa_supplicant_value(interface, key): tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') @@ -66,6 +66,8 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): def tearDown(self): + self.cli_delete(pki_path) + for interface in self._interfaces: # when using a dedicated interface to test via TEST_ETH environment # variable only this one will be cleared in the end - usable to test @@ -149,11 +151,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() def test_eapol_support(self): + self.cli_set(pki_path + ['ca', 'eapol', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'eapol', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'eapol', 'private', 'key', key_data]) + for interface in self._interfaces: # Enable EAPoL - self.cli_set(self._base_path + [interface, 'eapol', 'ca-cert-file', ca_cert]) - self.cli_set(self._base_path + [interface, 'eapol', 'cert-file', ssl_cert]) - self.cli_set(self._base_path + [interface, 'eapol', 'key-file', ssl_key]) + self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol']) + self.cli_set(self._base_path + [interface, 'eapol', 'certificate', 'eapol']) self.cli_commit() @@ -172,35 +177,17 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual('0', tmp) tmp = get_wpa_supplicant_value(interface, 'ca_cert') - self.assertEqual(f'"{ca_cert}"', tmp) + self.assertEqual(f'"/run/wpa_supplicant/{interface}_ca.pem"', tmp) tmp = get_wpa_supplicant_value(interface, 'client_cert') - self.assertEqual(f'"{ssl_cert}"', tmp) + self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.pem"', tmp) tmp = get_wpa_supplicant_value(interface, 'private_key') - self.assertEqual(f'"{ssl_key}"', tmp) + self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.key"', tmp) mac = read_file(f'/sys/class/net/{interface}/address') tmp = get_wpa_supplicant_value(interface, 'identity') self.assertEqual(f'"{mac}"', tmp) if __name__ == '__main__': - # Our SSL certificates need a subject ... - subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \ - 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/' - - if not (os.path.isfile(ssl_key) and os.path.isfile(ssl_cert)): - # Generate mandatory SSL certificate - tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\ - f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}' - cmd(tmp) - - if not os.path.isfile(ca_cert): - # Generate "CA" - tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} -subj {subject}' - cmd(tmp) - - for file in [ca_cert, ssl_cert, ssl_key]: - cmd(f'sudo chown radius_priv_user:vyattacfg {file}') - unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 378f400b8..78c24952b 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -32,6 +32,8 @@ from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call from vyos.util import dict_search @@ -40,6 +42,7 @@ from vyos import airbag airbag.enable() # XXX: wpa_supplicant works on the source interface +cfg_dir = '/run/wpa_supplicant' wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' def get_config(config=None): @@ -52,8 +55,15 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'ethernet'] + + tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + ethernet = get_interface_dict(conf, base) + if 'deleted' not in ethernet: + ethernet['pki'] = tmp_pki + return ethernet def verify(ethernet): @@ -126,6 +136,27 @@ def generate(ethernet): if 'eapol' in ethernet: render(wpa_suppl_conf.format(**ethernet), 'ethernet/wpa_supplicant.conf.tmpl', ethernet) + + ifname = ethernet['ifname'] + cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem') + cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key') + + cert_name = ethernet['eapol']['certificate'] + pki_cert = ethernet['pki']['certificate'][cert_name] + + with open(cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + with open(cert_key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + if 'ca_certificate' in ethernet['eapol']: + ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem') + ca_cert_name = ethernet['eapol']['ca_certificate'] + pki_ca_cert = ethernet['pki']['ca'][cert_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca_cert['certificate'])) else: # delete configuration on interface removal if os.path.isfile(wpa_suppl_conf.format(**ethernet)): diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23 index c6dc32baf..3fd5998a0 100755 --- a/src/migration-scripts/interfaces/22-to-23 +++ b/src/migration-scripts/interfaces/22-to-23 @@ -15,27 +15,34 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrate Wireguard to store keys in CLI +# Migrate EAPoL to PKI configuration import os import sys from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key -if __name__ == '__main__': - if (len(sys.argv) < 1): - print("Must specify file name!") - sys.exit(1) +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) - file_name = sys.argv[1] +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) - with open(file_name, 'r') as f: - config_file = f.read() +file_name = sys.argv[1] - config = ConfigTree(config_file) - base = ['interfaces', 'wireguard'] - if not config.exists(base): - # Nothing to do - sys.exit(0) +with open(file_name, 'r') as f: + config_file = f.read() +config = ConfigTree(config_file) + +# Wireguard +base = ['interfaces', 'wireguard'] + +if config.exists(base): for interface in config.list_nodes(base): private_key_path = base + [interface, 'private-key'] @@ -56,9 +63,96 @@ if __name__ == '__main__': for peer in config.list_nodes(base + [interface, 'peer']): config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') - 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) +# Ethernet EAPoL +base = ['interfaces', 'ethernet'] + +if config.exists(base): + AUTH_DIR = '/config/auth' + pki_base = ['pki'] + + for interface in config.list_nodes(base): + if not config.exists(base + [interface, 'eapol']): + continue + + x509_base = base + [interface, 'eapol'] + pki_name = f'eapol_{interface}' + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on eapol config for interface {interface}') + + config.delete(x509_base + ['key-file']) + +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) |