From 787f8c44327fc0adc38af51d034e178b32424fea Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Tue, 17 Sep 2024 20:32:02 +0300
Subject: bond: T6709: add EAPoL support (backport #4069) (#4076)

* ethernet: T6709: move EAPoL support to common framework

Instead of having EAPoL (Extensible Authentication Protocol over Local Area
Network) support only available for ethernet interfaces, move this to common
ground at vyos.ifconfig.interface making it available for all sorts of
interfaces by simply including the XML portion

  #include <include/interface/eapol.xml.i>

(cherry picked from commit 0ee8d5e35044e7480dac6a23e92d43744b8c5d36)

* bond: T6709: add EAPoL support

(cherry picked from commit 8eeb1bdcdfc104ffa77531f270a38cda2aee7f82)

---------

Co-authored-by: Christian Breunig <christian@breunig.cc>
---
 interface-definitions/interfaces_bonding.xml.in   |   1 +
 python/vyos/configverify.py                       |  17 +++
 python/vyos/ifconfig/interface.py                 |  72 +++++++++-
 smoketest/scripts/cli/base_interfaces_test.py     | 162 +++++++++++++++++++++-
 smoketest/scripts/cli/test_interfaces_ethernet.py | 142 -------------------
 src/conf_mode/interfaces_bonding.py               |   4 +-
 src/conf_mode/interfaces_ethernet.py              |  78 +----------
 7 files changed, 251 insertions(+), 225 deletions(-)

diff --git a/interface-definitions/interfaces_bonding.xml.in b/interface-definitions/interfaces_bonding.xml.in
index cc0327f3d..b17cad478 100644
--- a/interface-definitions/interfaces_bonding.xml.in
+++ b/interface-definitions/interfaces_bonding.xml.in
@@ -56,6 +56,7 @@
           #include <include/interface/disable.xml.i>
           #include <include/interface/vrf.xml.i>
           #include <include/interface/mirror.xml.i>
+          #include <include/interface/eapol.xml.i>
           <node name="evpn">
             <properties>
               <help>EVPN Multihoming</help>
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 59b67300d..92996f2ee 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -520,3 +520,20 @@ def verify_pki_dh_parameters(config: dict, dh_name: str, min_key_size: int=0):
         dh_bits = dh_numbers.p.bit_length()
         if dh_bits < min_key_size:
             raise ConfigError(f'Minimum DH key-size is {min_key_size} bits!')
+
+def verify_eapol(config: dict):
+    """
+    Common helper function used by interface implementations to perform
+    recurring validation of EAPoL configuration.
+    """
+    if 'eapol' not in config:
+        return
+
+    if 'certificate' not in config['eapol']:
+        raise ConfigError('Certificate must be specified when using EAPoL!')
+
+    verify_pki_certificate(config, config['eapol']['certificate'], no_password_protected=True)
+
+    if 'ca_certificate' in config['eapol']:
+        for ca_cert in config['eapol']['ca_certificate']:
+            verify_pki_ca_certificate(config, ca_cert)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 72d3d3afe..31fcf6ca6 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -32,6 +32,12 @@ from vyos.configdict import list_diff
 from vyos.configdict import dict_merge
 from vyos.configdict import get_vlan_ids
 from vyos.defaults import directories
+from vyos.pki import find_chain
+from vyos.pki import encode_certificate
+from vyos.pki import load_certificate
+from vyos.pki import wrap_private_key
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
 from vyos.template import render
 from vyos.utils.network import mac2eui64
 from vyos.utils.dict import dict_search
@@ -41,9 +47,8 @@ from vyos.utils.network import get_vrf_tableid
 from vyos.utils.network import is_netns_interface
 from vyos.utils.process import is_systemd_service_active
 from vyos.utils.process import run
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
 from vyos.utils.file import read_file
+from vyos.utils.file import write_file
 from vyos.utils.network import is_intf_addr_assigned
 from vyos.utils.network import is_ipv6_link_local
 from vyos.utils.assertion import assert_boolean
@@ -52,7 +57,6 @@ from vyos.utils.assertion import assert_mac
 from vyos.utils.assertion import assert_mtu
 from vyos.utils.assertion import assert_positive
 from vyos.utils.assertion import assert_range
-
 from vyos.ifconfig.control import Control
 from vyos.ifconfig.vrrp import VRRP
 from vyos.ifconfig.operational import Operational
@@ -377,6 +381,9 @@ class Interface(Control):
         >>> i = Interface('eth0')
         >>> i.remove()
         """
+        # Stop WPA supplicant if EAPoL was in use
+        if is_systemd_service_active(f'wpa_supplicant-wired@{self.ifname}'):
+            self._cmd(f'systemctl stop wpa_supplicant-wired@{self.ifname}')
 
         # remove all assigned IP addresses from interface - this is a bit redundant
         # as the kernel will remove all addresses on interface deletion, but we
@@ -1522,6 +1529,61 @@ class Interface(Control):
             return None
         self.set_interface('per_client_thread', enable)
 
+    def set_eapol(self) -> None:
+        """ Take care about EAPoL supplicant daemon """
+
+        # XXX: wpa_supplicant works on the source interface
+        cfg_dir = '/run/wpa_supplicant'
+        wpa_supplicant_conf = f'{cfg_dir}/{self.ifname}.conf'
+        eapol_action='stop'
+
+        if 'eapol' in self.config:
+            # The default is a fallback to hw_id which is not present for any interface
+            # other then an ethernet interface. Thus we emulate hw_id by reading back the
+            # Kernel assigned MAC address
+            if 'hw_id' not in self.config:
+                self.config['hw_id'] = read_file(f'/sys/class/net/{self.ifname}/address')
+            render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', self.config)
+
+            cert_file_path = os.path.join(cfg_dir, f'{self.ifname}_cert.pem')
+            cert_key_path = os.path.join(cfg_dir, f'{self.ifname}_cert.key')
+
+            cert_name = self.config['eapol']['certificate']
+            pki_cert = self.config['pki']['certificate'][cert_name]
+
+            loaded_pki_cert = load_certificate(pki_cert['certificate'])
+            loaded_ca_certs = {load_certificate(c['certificate'])
+                for c in self.config['pki']['ca'].values()} if 'ca' in self.config['pki'] else {}
+
+            cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
+
+            write_file(cert_file_path,
+                    '\n'.join(encode_certificate(c) for c in cert_full_chain))
+            write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
+
+            if 'ca_certificate' in self.config['eapol']:
+                ca_cert_file_path = os.path.join(cfg_dir, f'{self.ifname}_ca.pem')
+                ca_chains = []
+
+                for ca_cert_name in self.config['eapol']['ca_certificate']:
+                    pki_ca_cert = self.config['pki']['ca'][ca_cert_name]
+                    loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+                    ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+                    ca_chains.append(
+                        '\n'.join(encode_certificate(c) for c in ca_full_chain))
+
+                write_file(ca_cert_file_path, '\n'.join(ca_chains))
+
+            eapol_action='reload-or-restart'
+
+        # start/stop WPA supplicant service
+        self._cmd(f'systemctl {eapol_action} wpa_supplicant-wired@{self.ifname}')
+
+        if 'eapol' not in self.config:
+            # delete configuration on interface removal
+            if os.path.isfile(wpa_supplicant_conf):
+                os.unlink(wpa_supplicant_conf)
+
     def update(self, config):
         """ General helper function which works on a dictionary retrived by
         get_config_dict(). It's main intention is to consolidate the scattered
@@ -1609,7 +1671,6 @@ class Interface(Control):
             tmp = get_interface_config(config['ifname'])
             if 'master' in tmp and tmp['master'] != bridge_if:
                 self.set_vrf('')
-
         else:
             self.set_vrf(config.get('vrf', ''))
 
@@ -1752,6 +1813,9 @@ class Interface(Control):
         value = '1' if (tmp != None) else '0'
         self.set_per_client_thread(value)
 
+        # enable/disable EAPoL (Extensible Authentication Protocol over Local Area Network)
+        self.set_eapol()
+
         # Enable/Disable of an interface must always be done at the end of the
         # derived class to make use of the ref-counting set_admin_state()
         # function. We will only enable the interface if 'up' was called as
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index e7e29387f..593b4b415 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -12,6 +12,8 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import re
+
 from netifaces import AF_INET
 from netifaces import AF_INET6
 from netifaces import ifaddresses
@@ -22,6 +24,7 @@ from vyos.configsession import ConfigSessionError
 from vyos.defaults import directories
 from vyos.ifconfig import Interface
 from vyos.ifconfig import Section
+from vyos.pki import CERT_BEGIN
 from vyos.utils.file import read_file
 from vyos.utils.dict import dict_search
 from vyos.utils.process import cmd
@@ -40,6 +43,79 @@ dhclient_process_name = 'dhclient'
 dhcp6c_base_dir = directories['dhcp6_client_dir']
 dhcp6c_process_name = 'dhcp6c'
 
+server_ca_root_cert_data = """
+MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
+Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj
+ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK
+BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW
+a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ==
+"""
+
+server_ca_intermediate_cert_data = """
+MIIBmTCCAT+gAwIBAgIUNzrtHzLmi3QpPK57tUgCnJZhXXQwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
+Fw0zMjAyMTUxOTQxMjFaMCYxJDAiBgNVBAMMG1Z5T1Mgc2VydmVyIGludGVybWVk
+aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEl2nJ1CzoqPV6hWII2m
+eGN/uieU6wDMECTk/LgG8CCCSYb488dibUiFN/1UFsmoLIdIhkx/6MUCYh62m8U2
+WNujUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMV3YwH88I5gFsFUibbQ
+kMR0ECPsMB8GA1UdIwQYMBaAFHAL9hqJ4wSeOFhrsrcWXD+jRIW7MAoGCCqGSM49
+BAMCA0gAMEUCIQC/ahujD9dp5pMMCd3SZddqGC9cXtOwMN0JR3e5CxP13AIgIMQm
+jMYrinFoInxmX64HfshYqnUY8608nK9D2BNPOHo=
+"""
+
+client_ca_root_cert_data = """
+MIIBcDCCARagAwIBAgIUZmoW2xVdwkZSvglnkCq0AHKa6zIwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
+Fw0zMjAyMTUxOTQxMjFaMB4xHDAaBgNVBAMME1Z5T1MgY2xpZW50IHJvb3QgQ0Ew
+WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATUpKXzQk2NOVKDN4VULk2yw4mOKPvn
+mg947+VY7lbpfOfAUD0QRg95qZWCw899eKnXp/U4TkAVrmEKhUb6OJTFozIwMDAP
+BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTXu6xGWUl25X3sBtrhm3BJSICIATAK
+BggqhkjOPQQDAgNIADBFAiEAnTzEwuTI9bz2Oae3LZbjP6f/f50KFJtjLZFDbQz7
+DpYCIDNRHV8zBUibC+zg5PqMpQBKd/oPfNU76nEv6xkp/ijO
+"""
+
+client_ca_intermediate_cert_data = """
+MIIBmDCCAT+gAwIBAgIUJEMdotgqA7wU4XXJvEzDulUAGqgwCgYIKoZIzj0EAwIw
+HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjJa
+Fw0zMjAyMTUxOTQxMjJaMCYxJDAiBgNVBAMMG1Z5T1MgY2xpZW50IGludGVybWVk
+aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGyIVIi217s9j3O+WQ2b
+6R65/Z0ZjQpELxPjBRc0CA0GFCo+pI5EvwI+jNFArvTAJ5+ZdEWUJ1DQhBKDDQdI
+avCjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOUS8oNJjChB1Rb9Blcl
+ETvziHJ9MB8GA1UdIwQYMBaAFNe7rEZZSXblfewG2uGbcElIgIgBMAoGCCqGSM49
+BAMCA0cAMEQCIArhaxWgRsAUbEeNHD/ULtstLHxw/P97qPUSROLQld53AiBjgiiz
+9pDfISmpekZYz6bIDWRIR0cXUToZEMFNzNMrQg==
+"""
+
+client_cert_data = """
+MIIBmTCCAUCgAwIBAgIUV5T77XdE/tV82Tk4Vzhp5BIFFm0wCgYIKoZIzj0EAwIw
+JjEkMCIGA1UEAwwbVnlPUyBjbGllbnQgaW50ZXJtZWRpYXRlIENBMB4XDTIyMDIx
+NzE5NDEyMloXDTMyMDIxNTE5NDEyMlowIjEgMB4GA1UEAwwXVnlPUyBjbGllbnQg
+Y2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuyynqfc/qJj5e
+KJ03oOH8X4Z8spDeAPO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAh
+CIhytmJao1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTIFKrxZ+PqOhYSUqnl
+TGCUmM7wTjAfBgNVHSMEGDAWgBTlEvKDSYwoQdUW/QZXJRE784hyfTAKBggqhkjO
+PQQDAgNHADBEAiAvO8/jvz05xqmP3OXD53XhfxDLMIxzN4KPoCkFqvjlhQIgIHq2
+/geVx3rAOtSps56q/jiDouN/aw01TdpmGKVAa9U=
+"""
+
+client_key_data = """
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxaxAQsJwjoOCByQE
++qSYKtKtJzbdbOnTsKNSrfgkFH6hRANCAARuyynqfc/qJj5eKJ03oOH8X4Z8spDe
+APO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAhCIhytmJa
+"""
+
+def get_wpa_supplicant_value(interface, key):
+    tmp = read_file(f'/run/wpa_supplicant/{interface}.conf')
+    tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp)
+    return tmp[0]
+
+def get_certificate_count(interface, cert_type):
+    tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem')
+    return tmp.count(CERT_BEGIN)
+
 def is_mirrored_to(interface, mirror_if, qdisc):
     """
     Ask TC if we are mirroring traffic to a discrete interface.
@@ -57,10 +133,10 @@ def is_mirrored_to(interface, mirror_if, qdisc):
     if mirror_if in tmp:
         ret_val = True
     return ret_val
-
 class BasicInterfaceTest:
     class TestCase(VyOSUnitTestSHIM.TestCase):
         _test_dhcp = False
+        _test_eapol = False
         _test_ip = False
         _test_mtu = False
         _test_vlan = False
@@ -92,6 +168,7 @@ class BasicInterfaceTest:
             cls._test_vlan = cli_defined(cls._base_path, 'vif')
             cls._test_qinq = cli_defined(cls._base_path, 'vif-s')
             cls._test_dhcp = cli_defined(cls._base_path, 'dhcp-options')
+            cls._test_eapol = cli_defined(cls._base_path, 'eapol')
             cls._test_ip = cli_defined(cls._base_path, 'ip')
             cls._test_ipv6 = cli_defined(cls._base_path, 'ipv6')
             cls._test_ipv6_dhcpc6 = cli_defined(cls._base_path, 'dhcpv6-options')
@@ -1158,3 +1235,86 @@ class BasicInterfaceTest:
                 # as until commit() is called, nothing happens
                 section = Section.section(delegatee)
                 self.cli_delete(['interfaces', section, delegatee])
+
+        def test_eapol(self):
+            if not self._test_eapol:
+                self.skipTest('not supported')
+
+            cfg_dir = '/run/wpa_supplicant'
+
+            ca_certs = {
+                'eapol-server-ca-root': server_ca_root_cert_data,
+                'eapol-server-ca-intermediate': server_ca_intermediate_cert_data,
+                'eapol-client-ca-root': client_ca_root_cert_data,
+                'eapol-client-ca-intermediate': client_ca_intermediate_cert_data,
+            }
+            cert_name = 'eapol-client'
+
+            for name, data in ca_certs.items():
+                self.cli_set(['pki', 'ca', name, 'certificate', data.replace('\n','')])
+
+            self.cli_set(['pki', 'certificate', cert_name, 'certificate', client_cert_data.replace('\n','')])
+            self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', client_key_data.replace('\n','')])
+
+            for interface in self._interfaces:
+                path = self._base_path + [interface]
+                for option in self._options.get(interface, []):
+                    self.cli_set(path + option.split())
+
+                # Enable EAPoL
+                self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-server-ca-intermediate'])
+                self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate'])
+                self.cli_set(self._base_path + [interface, 'eapol', 'certificate', cert_name])
+
+            self.cli_commit()
+
+            # Test multiple CA chains
+            self.assertEqual(get_certificate_count(interface, 'ca'), 4)
+
+            for interface in self._interfaces:
+                self.cli_delete(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate'])
+
+            self.cli_commit()
+
+            # Validate interface config
+            for interface in self._interfaces:
+                tmp = get_wpa_supplicant_value(interface, 'key_mgmt')
+                self.assertEqual('IEEE8021X', tmp)
+
+                tmp = get_wpa_supplicant_value(interface, 'eap')
+                self.assertEqual('TLS', tmp)
+
+                tmp = get_wpa_supplicant_value(interface, 'eapol_flags')
+                self.assertEqual('0', tmp)
+
+                tmp = get_wpa_supplicant_value(interface, 'ca_cert')
+                self.assertEqual(f'"{cfg_dir}/{interface}_ca.pem"', tmp)
+
+                tmp = get_wpa_supplicant_value(interface, 'client_cert')
+                self.assertEqual(f'"{cfg_dir}/{interface}_cert.pem"', tmp)
+
+                tmp = get_wpa_supplicant_value(interface, 'private_key')
+                self.assertEqual(f'"{cfg_dir}/{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)
+
+                # Check certificate files have the full chain
+                self.assertEqual(get_certificate_count(interface, 'ca'), 2)
+                self.assertEqual(get_certificate_count(interface, 'cert'), 3)
+
+                # Check for running process
+                self.assertTrue(process_named_running('wpa_supplicant', cmdline=f'-i{interface}'))
+
+            # Remove EAPoL configuration
+            for interface in self._interfaces:
+                self.cli_delete(self._base_path + [interface, 'eapol'])
+
+            # Commit and check that process is no longer running
+            self.cli_commit()
+            self.assertFalse(process_named_running('wpa_supplicant'))
+
+            for name in ca_certs:
+                self.cli_delete(['pki', 'ca', name])
+            self.cli_delete(['pki', 'certificate', cert_name])
diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
index 4843a40da..3d12364f7 100755
--- a/smoketest/scripts/cli/test_interfaces_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
@@ -15,7 +15,6 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import os
-import re
 import unittest
 
 from glob import glob
@@ -28,86 +27,11 @@ from netifaces import ifaddresses
 from base_interfaces_test import BasicInterfaceTest
 from vyos.configsession import ConfigSessionError
 from vyos.ifconfig import Section
-from vyos.pki import CERT_BEGIN
 from vyos.utils.process import cmd
-from vyos.utils.process import process_named_running
 from vyos.utils.process import popen
 from vyos.utils.file import read_file
 from vyos.utils.network import is_ipv6_link_local
 
-server_ca_root_cert_data = """
-MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
-HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
-Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew
-WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj
-ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP
-BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK
-BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW
-a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ==
-"""
-
-server_ca_intermediate_cert_data = """
-MIIBmTCCAT+gAwIBAgIUNzrtHzLmi3QpPK57tUgCnJZhXXQwCgYIKoZIzj0EAwIw
-HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
-Fw0zMjAyMTUxOTQxMjFaMCYxJDAiBgNVBAMMG1Z5T1Mgc2VydmVyIGludGVybWVk
-aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEl2nJ1CzoqPV6hWII2m
-eGN/uieU6wDMECTk/LgG8CCCSYb488dibUiFN/1UFsmoLIdIhkx/6MUCYh62m8U2
-WNujUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMV3YwH88I5gFsFUibbQ
-kMR0ECPsMB8GA1UdIwQYMBaAFHAL9hqJ4wSeOFhrsrcWXD+jRIW7MAoGCCqGSM49
-BAMCA0gAMEUCIQC/ahujD9dp5pMMCd3SZddqGC9cXtOwMN0JR3e5CxP13AIgIMQm
-jMYrinFoInxmX64HfshYqnUY8608nK9D2BNPOHo=
-"""
-
-client_ca_root_cert_data = """
-MIIBcDCCARagAwIBAgIUZmoW2xVdwkZSvglnkCq0AHKa6zIwCgYIKoZIzj0EAwIw
-HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa
-Fw0zMjAyMTUxOTQxMjFaMB4xHDAaBgNVBAMME1Z5T1MgY2xpZW50IHJvb3QgQ0Ew
-WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATUpKXzQk2NOVKDN4VULk2yw4mOKPvn
-mg947+VY7lbpfOfAUD0QRg95qZWCw899eKnXp/U4TkAVrmEKhUb6OJTFozIwMDAP
-BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTXu6xGWUl25X3sBtrhm3BJSICIATAK
-BggqhkjOPQQDAgNIADBFAiEAnTzEwuTI9bz2Oae3LZbjP6f/f50KFJtjLZFDbQz7
-DpYCIDNRHV8zBUibC+zg5PqMpQBKd/oPfNU76nEv6xkp/ijO
-"""
-
-client_ca_intermediate_cert_data = """
-MIIBmDCCAT+gAwIBAgIUJEMdotgqA7wU4XXJvEzDulUAGqgwCgYIKoZIzj0EAwIw
-HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjJa
-Fw0zMjAyMTUxOTQxMjJaMCYxJDAiBgNVBAMMG1Z5T1MgY2xpZW50IGludGVybWVk
-aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGyIVIi217s9j3O+WQ2b
-6R65/Z0ZjQpELxPjBRc0CA0GFCo+pI5EvwI+jNFArvTAJ5+ZdEWUJ1DQhBKDDQdI
-avCjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOUS8oNJjChB1Rb9Blcl
-ETvziHJ9MB8GA1UdIwQYMBaAFNe7rEZZSXblfewG2uGbcElIgIgBMAoGCCqGSM49
-BAMCA0cAMEQCIArhaxWgRsAUbEeNHD/ULtstLHxw/P97qPUSROLQld53AiBjgiiz
-9pDfISmpekZYz6bIDWRIR0cXUToZEMFNzNMrQg==
-"""
-
-client_cert_data = """
-MIIBmTCCAUCgAwIBAgIUV5T77XdE/tV82Tk4Vzhp5BIFFm0wCgYIKoZIzj0EAwIw
-JjEkMCIGA1UEAwwbVnlPUyBjbGllbnQgaW50ZXJtZWRpYXRlIENBMB4XDTIyMDIx
-NzE5NDEyMloXDTMyMDIxNTE5NDEyMlowIjEgMB4GA1UEAwwXVnlPUyBjbGllbnQg
-Y2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuyynqfc/qJj5e
-KJ03oOH8X4Z8spDeAPO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAh
-CIhytmJao1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTIFKrxZ+PqOhYSUqnl
-TGCUmM7wTjAfBgNVHSMEGDAWgBTlEvKDSYwoQdUW/QZXJRE784hyfTAKBggqhkjO
-PQQDAgNHADBEAiAvO8/jvz05xqmP3OXD53XhfxDLMIxzN4KPoCkFqvjlhQIgIHq2
-/geVx3rAOtSps56q/jiDouN/aw01TdpmGKVAa9U=
-"""
-
-client_key_data = """
-MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxaxAQsJwjoOCByQE
-+qSYKtKtJzbdbOnTsKNSrfgkFH6hRANCAARuyynqfc/qJj5eKJ03oOH8X4Z8spDe
-APO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAhCIhytmJa
-"""
-
-def get_wpa_supplicant_value(interface, key):
-    tmp = read_file(f'/run/wpa_supplicant/{interface}.conf')
-    tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp)
-    return tmp[0]
-
-def get_certificate_count(interface, cert_type):
-    tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem')
-    return tmp.count(CERT_BEGIN)
-
 class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
     @classmethod
     def setUpClass(cls):
@@ -237,72 +161,6 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
             self.cli_set(self._base_path + [interface, 'speed', 'auto'])
             self.cli_commit()
 
-    def test_eapol_support(self):
-        ca_certs = {
-            'eapol-server-ca-root': server_ca_root_cert_data,
-            'eapol-server-ca-intermediate': server_ca_intermediate_cert_data,
-            'eapol-client-ca-root': client_ca_root_cert_data,
-            'eapol-client-ca-intermediate': client_ca_intermediate_cert_data,
-        }
-        cert_name = 'eapol-client'
-
-        for name, data in ca_certs.items():
-            self.cli_set(['pki', 'ca', name, 'certificate', data.replace('\n','')])
-
-        self.cli_set(['pki', 'certificate', cert_name, 'certificate', client_cert_data.replace('\n','')])
-        self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', client_key_data.replace('\n','')])
-
-        for interface in self._interfaces:
-            # Enable EAPoL
-            self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-server-ca-intermediate'])
-            self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate'])
-            self.cli_set(self._base_path + [interface, 'eapol', 'certificate', cert_name])
-
-        self.cli_commit()
-
-        # Test multiple CA chains
-        self.assertEqual(get_certificate_count(interface, 'ca'), 4)
-
-        for interface in self._interfaces:
-            self.cli_delete(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate'])
-
-        self.cli_commit()
-
-        # Check for running process
-        self.assertTrue(process_named_running('wpa_supplicant'))
-
-        # Validate interface config
-        for interface in self._interfaces:
-            tmp = get_wpa_supplicant_value(interface, 'key_mgmt')
-            self.assertEqual('IEEE8021X', tmp)
-
-            tmp = get_wpa_supplicant_value(interface, 'eap')
-            self.assertEqual('TLS', tmp)
-
-            tmp = get_wpa_supplicant_value(interface, 'eapol_flags')
-            self.assertEqual('0', tmp)
-
-            tmp = get_wpa_supplicant_value(interface, 'ca_cert')
-            self.assertEqual(f'"/run/wpa_supplicant/{interface}_ca.pem"', tmp)
-
-            tmp = get_wpa_supplicant_value(interface, 'client_cert')
-            self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.pem"', tmp)
-
-            tmp = get_wpa_supplicant_value(interface, 'private_key')
-            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)
-
-        # Check certificate files have the full chain
-        self.assertEqual(get_certificate_count(interface, 'ca'), 2)
-        self.assertEqual(get_certificate_count(interface, 'cert'), 3)
-
-        for name in ca_certs:
-            self.cli_delete(['pki', 'ca', name])
-        self.cli_delete(['pki', 'certificate', cert_name])
-
     def test_ethtool_ring_buffer(self):
         for interface in self._interfaces:
             # We do not use vyos.ethtool here to not have any chance
diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py
index 5e5d5fba1..bbbfb0385 100755
--- a/src/conf_mode/interfaces_bonding.py
+++ b/src/conf_mode/interfaces_bonding.py
@@ -25,6 +25,7 @@ from vyos.configdict import is_source_interface
 from vyos.configverify import verify_address
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_eapol
 from vyos.configverify import verify_mirror_redirect
 from vyos.configverify import verify_mtu_ipv6
 from vyos.configverify import verify_vlan_config
@@ -73,7 +74,7 @@ def get_config(config=None):
     else:
         conf = Config()
     base = ['interfaces', 'bonding']
-    ifname, bond = get_interface_dict(conf, base)
+    ifname, bond = get_interface_dict(conf, base, with_pki=True)
 
     # To make our own life easier transfor the list of member interfaces
     # into a dictionary - we will use this to add additional information
@@ -196,6 +197,7 @@ def verify(bond):
     verify_dhcpv6(bond)
     verify_vrf(bond)
     verify_mirror_redirect(bond)
+    verify_eapol(bond)
 
     # use common function to verify VLAN configuration
     verify_vlan_config(bond)
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
index afc48ead8..34ce7bc47 100755
--- a/src/conf_mode/interfaces_ethernet.py
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -31,32 +31,20 @@ from vyos.configverify import verify_mtu_ipv6
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
 from vyos.configverify import verify_bond_bridge_member
-from vyos.configverify import verify_pki_certificate
-from vyos.configverify import verify_pki_ca_certificate
+from vyos.configverify import verify_eapol
 from vyos.ethtool import Ethtool
 from vyos.ifconfig import EthernetIf
 from vyos.ifconfig import BondIf
-from vyos.pki import find_chain
-from vyos.pki import encode_certificate
-from vyos.pki import load_certificate
-from vyos.pki import wrap_private_key
-from vyos.template import render
 from vyos.template import render_to_string
-from vyos.utils.process import call
 from vyos.utils.dict import dict_search
 from vyos.utils.dict import dict_to_paths_values
 from vyos.utils.dict import dict_set
 from vyos.utils.dict import dict_delete
-from vyos.utils.file import write_file
 from vyos import ConfigError
 from vyos import frr
 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 update_bond_options(conf: Config, eth_conf: dict) -> list:
     """
     Return list of blocked options if interface is a bond member
@@ -277,23 +265,6 @@ def verify_allowedbond_changes(ethernet: dict):
                               f' on interface "{ethernet["ifname"]}".' \
                               f' Interface is a bond member')
 
-def verify_eapol(ethernet: dict):
-    """
-    Common helper function used by interface implementations to perform
-    recurring validation of EAPoL configuration.
-    """
-    if 'eapol' not in ethernet:
-        return
-
-    if 'certificate' not in ethernet['eapol']:
-        raise ConfigError('Certificate must be specified when using EAPoL!')
-
-    verify_pki_certificate(ethernet, ethernet['eapol']['certificate'], no_password_protected=True)
-
-    if 'ca_certificate' in ethernet['eapol']:
-        for ca_cert in ethernet['eapol']['ca_certificate']:
-            verify_pki_ca_certificate(ethernet, ca_cert)
-
 def verify(ethernet):
     if 'deleted' in ethernet:
         return None
@@ -346,51 +317,10 @@ def verify_ethernet(ethernet):
     verify_vlan_config(ethernet)
     return None
 
-
 def generate(ethernet):
-    # render real configuration file once
-    wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
-
     if 'deleted' in ethernet:
-        # delete configuration on interface removal
-        if os.path.isfile(wpa_supplicant_conf):
-            os.unlink(wpa_supplicant_conf)
         return None
 
-    if 'eapol' in ethernet:
-        ifname = ethernet['ifname']
-
-        render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet)
-
-        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]
-
-        loaded_pki_cert = load_certificate(pki_cert['certificate'])
-        loaded_ca_certs = {load_certificate(c['certificate'])
-            for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {}
-
-        cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
-
-        write_file(cert_file_path,
-                   '\n'.join(encode_certificate(c) for c in cert_full_chain))
-        write_file(cert_key_path, 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_chains = []
-
-            for ca_cert_name in ethernet['eapol']['ca_certificate']:
-                pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
-                loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
-                ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
-                ca_chains.append(
-                    '\n'.join(encode_certificate(c) for c in ca_full_chain))
-
-            write_file(ca_cert_file_path, '\n'.join(ca_chains))
-
     ethernet['frr_zebra_config'] = ''
     if 'deleted' not in ethernet:
         ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet)
@@ -399,8 +329,6 @@ def generate(ethernet):
 
 def apply(ethernet):
     ifname = ethernet['ifname']
-    # take care about EAPoL supplicant daemon
-    eapol_action='stop'
 
     e = EthernetIf(ifname)
     if 'deleted' in ethernet:
@@ -408,10 +336,6 @@ def apply(ethernet):
         e.remove()
     else:
         e.update(ethernet)
-        if 'eapol' in ethernet:
-            eapol_action='reload-or-restart'
-
-    call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
 
     zebra_daemon = 'zebra'
     # Save original configuration prior to starting any commit actions
-- 
cgit v1.2.3