diff options
| author | Christian Breunig <christian@breunig.cc> | 2023-08-23 07:35:28 +0200 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-08-23 07:35:28 +0200 | 
| commit | 8c7fbec24f8bfe064d8ad804951f5ae59b54748f (patch) | |
| tree | 6bbf7902d77f0761fe95719f2492efad3a486ede | |
| parent | a090dd71231167cda95baaf7284e031230b5ee7a (diff) | |
| parent | 007942130b3e9b86391fd08c3e372002afc6025e (diff) | |
| download | vyos-1x-8c7fbec24f8bfe064d8ad804951f5ae59b54748f.tar.gz vyos-1x-8c7fbec24f8bfe064d8ad804951f5ae59b54748f.zip | |
Merge pull request #2156 from giga1699/T5447
T5447: Initial support for MACsec static keys
| -rw-r--r-- | interface-definitions/include/interface/macsec-key.xml.i | 15 | ||||
| -rw-r--r-- | interface-definitions/interfaces-macsec.xml.in | 22 | ||||
| -rw-r--r-- | python/vyos/ifconfig/macsec.py | 22 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_interfaces_macsec.py | 72 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-macsec.py | 74 | 
5 files changed, 191 insertions, 14 deletions
| diff --git a/interface-definitions/include/interface/macsec-key.xml.i b/interface-definitions/include/interface/macsec-key.xml.i new file mode 100644 index 000000000..5a857a612 --- /dev/null +++ b/interface-definitions/include/interface/macsec-key.xml.i @@ -0,0 +1,15 @@ +<!-- include start from interface/macsec-key.xml.i --> +<leafNode name="key"> +  <properties> +    <help>MACsec static key</help> +    <valueHelp> +      <format>txt</format> +      <description>16-byte (128-bit) hex-string (32 hex-digits) for gcm-aes-128 or 32-byte (256-bit) hex-string (64 hex-digits) for gcm-aes-256</description> +    </valueHelp> +    <constraint> +      <regex>[A-Fa-f0-9]{32}</regex> +      <regex>[A-Fa-f0-9]{64}</regex> +    </constraint> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index 6bc28e44b..766b0bede 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -52,6 +52,28 @@                    <valueless/>                  </properties>                </leafNode> +              <node name="static"> +                <properties> +                  <help>Use static keys for MACsec [static Secure Authentication Key (SAK) mode]</help> +                </properties> +                <children> +                  #include <include/interface/macsec-key.xml.i> +                  <tagNode name="peer"> +                    <properties> +                      <help>MACsec peer name</help> +                      <constraint> +                        <regex>[^ ]{1,100}</regex> +                      </constraint> +                      <constraintErrorMessage>MACsec peer name exceeds limit of 100 characters</constraintErrorMessage> +                    </properties> +                    <children> +                      #include <include/generic-disable-node.xml.i> +                      #include <include/interface/mac.xml.i> +                      #include <include/interface/macsec-key.xml.i> +                    </children> +                  </tagNode> +                </children> +              </node>                <node name="mka">                  <properties>                    <help>MACsec Key Agreement protocol (MKA)</help> diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index 1a78d18d8..9329c5ee7 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -1,4 +1,4 @@ -# Copyright 2020-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io>  #  # This library is free software; you can redistribute it and/or  # modify it under the terms of the GNU Lesser General Public @@ -41,10 +41,30 @@ class MACsecIf(Interface):          Create MACsec interface in OS kernel. Interface is administrative          down by default.          """ +          # create tunnel interface          cmd  = 'ip link add link {source_interface} {ifname} type {type}'.format(**self.config)          cmd += f' cipher {self.config["security"]["cipher"]}'          self._cmd(cmd) +        # Check if using static keys +        if 'static' in self.config["security"]: +            # Set static TX key +            cmd = 'ip macsec add {ifname} tx sa 0 pn 1 on key 00'.format(**self.config) +            cmd += f' {self.config["security"]["static"]["key"]}' +            self._cmd(cmd) + +            for peer, peer_config in self.config["security"]["static"]["peer"].items(): +                if 'disable' in peer_config: +                    continue + +                # Create the address +                cmd = 'ip macsec add {ifname} rx port 1 address'.format(**self.config) +                cmd += f' {peer_config["mac"]}' +                self._cmd(cmd) +                # Add the rx-key to the address +                cmd += f' sa 0 pn 1 on key 01 {peer_config["key"]}' +                self._cmd(cmd) +          # interface is always A/D down. It needs to be enabled explicitly          self.set_admin_state('down') diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index b32a6f524..30d1ad659 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -208,5 +208,77 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase):          # Check for running process          self.assertTrue(process_named_running(PROCESS_NAME)) +    def test_macsec_static_keys(self): +        src_interface = 'eth0' +        interface = 'macsec5' +        cipher1 = 'gcm-aes-128' +        cipher2 = 'gcm-aes-256' +        tx_key_1 = '71a82a48eddfa12c08a19792ca20c4bb' +        tx_key_2 = 'dd487b2958e855ea35a5d43a5ecb3dcfbe7889ffcb877770252feb13b734478d' +        rx_key_1 = '0022d00f57e75241a230cdf7118dfcc5' +        rx_key_2 = 'b7d6d7ad075e02323fdeb845217b884d3f93ff36b2cdaf6b07eeb189b877245f' +        peer_mac = '00:11:22:33:44:55' +        self.cli_set(self._base_path + [interface]) + +         # Encrypt link +        self.cli_set(self._base_path + [interface, 'security', 'encrypt']) + +        # check validate() - source interface is mandatory +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'source-interface', src_interface]) + +        # check validate() - cipher is mandatory +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher1]) + +        # check validate() - only static or mka config is allowed +        self.cli_set(self._base_path + [interface, 'security', 'static']) +        self.cli_set(self._base_path + [interface, 'security', 'mka', 'cak', tx_key_1]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_delete(self._base_path + [interface, 'security', 'mka']) + +        # check validate() - tx-key required +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        # check validate() - tx-key length must match cipher +        self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_2]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_1]) + +        # check validate() - at least one peer must be defined +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() + +        # check validate() - enabled peer must have both rx-key and MAC defined +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER']) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac', peer_mac]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_delete(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac']) +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'key', rx_key_1]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'mac', peer_mac]) + +        # check validate() - peer rx-key length must match cipher +        self.cli_set(self._base_path + [interface, 'security', 'cipher', cipher2]) +        self.cli_set(self._base_path + [interface, 'security', 'static', 'key', tx_key_2]) +        with self.assertRaises(ConfigSessionError): +            self.cli_commit() +        self.cli_set(self._base_path + [interface, 'security', 'static', 'peer', 'TESTPEER', 'key', rx_key_2]) + +        # final commit and verify +        self.cli_commit() +        self.assertIn(interface, interfaces()) +        self.assertEqual(cipher2, get_cipher(interface)) +        self.assertTrue(os.path.isdir(f'/sys/class/net/{interface}')) +  if __name__ == '__main__':      unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 3f86e2638..0a927ac88 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -43,6 +43,14 @@ airbag.enable()  # XXX: wpa_supplicant works on the source interface  wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' +# Constants +## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit +GCM_AES_128_LEN: int = 32 +GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!' +## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit +GCM_AES_256_LEN: int = 64 +GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!' +  def get_config(config=None):      """      Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -89,18 +97,54 @@ def verify(macsec):          raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))      if dict_search('security.encrypt', macsec) != None: -        if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: -            raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') +        # Check that only static or MKA config is present +        if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None): +            raise ConfigError('Only static or MKA can be used!') + +        # Logic to check static configuration +        if dict_search('security.static', macsec) != None: +            # tx-key must be defined +            if dict_search('security.static.key', macsec) == None: +                raise ConfigError('Static MACsec tx-key must be defined.') + +            tx_len = len(dict_search('security.static.key', macsec)) + +            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN: +                raise ConfigError(GCM_128_KEY_ERROR) + +            if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN: +                raise ConfigError(GCM_256_KEY_ERROR) + +            # Make sure at least one peer is defined +            if 'peer' not in macsec['security']['static']: +                raise ConfigError('Must have at least one peer defined for static MACsec') + +            # For every enabled peer, make sure a MAC and rx-key is defined +            for peer, peer_config in macsec['security']['static']['peer'].items(): +                if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config): +                    raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.') + +                # check rx-key length against cipher suite +                rx_len = len(peer_config['key']) + +                if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN: +                    raise ConfigError(GCM_128_KEY_ERROR) + +                if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN: +                    raise ConfigError(GCM_256_KEY_ERROR) + +        # Logic to check MKA configuration +        else: +            if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None: +                raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!') -        cak_len = len(dict_search('security.mka.cak', macsec)) +            cak_len = len(dict_search('security.mka.cak', macsec)) -        if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32: -            # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit -            raise ConfigError('gcm-aes-128 requires a 128bit long key!') +            if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN: +                raise ConfigError(GCM_128_KEY_ERROR) -        elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64: -            # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit -            raise ConfigError('gcm-aes-128 requires a 256bit long key!') +            elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN: +                raise ConfigError(GCM_256_KEY_ERROR)      if 'source_interface' in macsec:          # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad @@ -115,7 +159,9 @@ def verify(macsec):  def generate(macsec): -    render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) +    # Only generate wpa_supplicant config if using MKA +    if dict_search('security.mka.cak', macsec): +        render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)      return None @@ -142,8 +188,10 @@ def apply(macsec):      i = MACsecIf(**macsec)      i.update(macsec) -    if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: -        call(f'systemctl reload-or-restart {systemd_service}') +    # Only reload/restart if using MKA +    if dict_search('security.mka.cak', macsec): +        if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec: +            call(f'systemctl reload-or-restart {systemd_service}')      return None | 
