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 b81c9b40c..766b0bede 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -1,177 +1,153 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="interfaces"> <children> <tagNode name="macsec" owner="${vyos_conf_scripts_dir}/interfaces-macsec.py"> <properties> <help>MACsec Interface (802.1ae)</help> <priority>461</priority> <constraint> <regex>macsec[0-9]+</regex> </constraint> <constraintErrorMessage>MACsec interface must be named macsecN</constraintErrorMessage> <valueHelp> <format>macsecN</format> <description>MACsec interface name</description> </valueHelp> </properties> <children> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mirror.xml.i> <node name="security"> <properties> <help>Security/Encryption Settings</help> </properties> <children> <leafNode name="cipher"> <properties> <help>Cipher suite used</help> <completionHelp> <list>gcm-aes-128 gcm-aes-256</list> </completionHelp> <valueHelp> <format>gcm-aes-128</format> <description>Galois/Counter Mode of AES cipher with 128-bit key</description> </valueHelp> <valueHelp> <format>gcm-aes-256</format> <description>Galois/Counter Mode of AES cipher with 256-bit key</description> </valueHelp> <constraint> <regex>(gcm-aes-128|gcm-aes-256)</regex> </constraint> </properties> </leafNode> <leafNode name="encrypt"> <properties> <help>Enable optional MACsec encryption</help> <valueless/> </properties> </leafNode> <node name="static"> <properties> - <help>Assign static MACSec keys instead of using MKA</help> + <help>Use static keys for MACsec [static Secure Authentication Key (SAK) mode]</help> </properties> <children> - <leafNode name="tx-key"> - <properties> - <help>Set the static transmit 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 <include/interface/macsec-key.xml.i> <tagNode name="peer"> <properties> - <help>peer alias</help> + <help>MACsec peer name</help> <constraint> <regex>[^ ]{1,100}</regex> </constraint> - <constraintErrorMessage>peer alias too long (limit 100 characters)</constraintErrorMessage> + <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> - <leafNode name="rx-key"> - <properties> - <help>Set the static receive key for peer</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 <include/interface/macsec-key.xml.i> </children> </tagNode> </children> </node> <node name="mka"> <properties> <help>MACsec Key Agreement protocol (MKA)</help> </properties> <children> <leafNode name="cak"> <properties> <help>Secure Connectivity Association 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> <leafNode name="ckn"> <properties> <help>Secure Connectivity Association Key Name</help> <valueHelp> <format>txt</format> <description>1..32-bytes (8..256 bit) hex-string (2..64 hex-digits)</description> </valueHelp> <constraint> <regex>[A-Fa-f0-9]{2,64}</regex> </constraint> </properties> </leafNode> <leafNode name="priority"> <properties> <help>Priority of MACsec Key Agreement protocol (MKA) actor</help> <valueHelp> <format>u32:0-255</format> <description>MACsec Key Agreement protocol (MKA) priority</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-255" /> </constraint> </properties> <defaultValue>255</defaultValue> </leafNode> </children> </node> <leafNode name="replay-window"> <properties> <help>IEEE 802.1X/MACsec replay protection window</help> <valueHelp> <format>u32:0</format> <description>No replay window, strict check</description> </valueHelp> <valueHelp> <format>u32:1-4294967295</format> <description>Number of packets that could be misordered</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295" /> </constraint> </properties> </leafNode> </children> </node> #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/mtu-68-16000.xml.i> <leafNode name="mtu"> <defaultValue>1460</defaultValue> </leafNode> #include <include/source-interface-ethernet.xml.i> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> </children> </node> </interfaceDefinition> diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py index 6318a1688..e14f22c58 100644 --- a/python/vyos/ifconfig/macsec.py +++ b/python/vyos/ifconfig/macsec.py @@ -1,74 +1,74 @@ # Copyright 2020-2021 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 # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. from vyos.ifconfig.interface import Interface @Interface.register class MACsecIf(Interface): """ MACsec is an IEEE standard (IEEE 802.1AE) for MAC security, introduced in 2006. It defines a way to establish a protocol independent connection between two hosts with data confidentiality, authenticity and/or integrity, using GCM-AES-128. MACsec operates on the Ethernet layer and as such is a layer 2 protocol, which means it's designed to secure traffic within a layer 2 network, including DHCP or ARP requests. It does not compete with other security solutions such as IPsec (layer 3) or TLS (layer 4), as all those solutions are used for their own specific use cases. """ iftype = 'macsec' definition = { **Interface.definition, **{ 'section': 'macsec', 'prefixes': ['macsec', ], }, } def _create(self): """ Create MACsec interface in OS kernel. Interface is administrative down by default when not using static keys. """ # 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"]["tx_key"]}' + 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["rx_key"]}' + cmd += f' sa 0 pn 1 on key 01 {peer_config["key"]}' self._cmd(cmd) # Set admin state to up - self.set_admin_state('up') + self.set_admin_state('down') else: # 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 8c9867500..30d1ad659 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -1,285 +1,284 @@ #!/usr/bin/env python3 # # 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 # 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 <http://www.gnu.org/licenses/>. import os import re import unittest from base_interfaces_test import BasicInterfaceTest from netifaces import interfaces from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.utils.process import cmd from vyos.utils.file import read_file from vyos.utils.network import get_interface_config from vyos.utils.process import process_named_running PROCESS_NAME = 'wpa_supplicant' def get_config_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_cipher(interface): tmp = get_interface_config(interface) return tmp['linkinfo']['info_data']['cipher_suite'].lower() class MACsecInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'macsec'] cls._options = { 'macsec0': ['source-interface eth0', 'security cipher gcm-aes-128'] } # if we have a physical eth1 interface, add a second macsec instance if 'eth1' in Section.interfaces('ethernet'): macsec = { 'macsec1': [f'source-interface eth1', 'security cipher gcm-aes-128'] } cls._options.update(macsec) cls._interfaces = list(cls._options) # call base-classes classmethod super(MACsecInterfaceTest, cls).setUpClass() def tearDown(self): super().tearDown() self.assertFalse(process_named_running(PROCESS_NAME)) def test_macsec_encryption(self): # MACsec can be operating in authentication and encryption mode - both # using different mandatory settings, lets test encryption as the basic # authentication test has been performed using the base class tests mak_cak = '232e44b7fda6f8e2d88a07bf78a7aff4' mak_ckn = '40916f4b23e3d548ad27eedd2d10c6f98c2d21684699647d63d41b500dfe8836' replay_window = '64' for interface, option_value in self._options.items(): for option in option_value: if option.split()[0] == 'source-interface': src_interface = option.split()[1] self.cli_set(self._base_path + [interface] + option.split()) # Encrypt link self.cli_set(self._base_path + [interface, 'security', 'encrypt']) # check validate() - Physical source interface MTU must be higher then our MTU self.cli_set(self._base_path + [interface, 'mtu', '1500']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(self._base_path + [interface, 'mtu']) # check validate() - MACsec security keys mandartory when encryption is enabled with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(self._base_path + [interface, 'security', 'mka', 'cak', mak_cak]) # check validate() - MACsec security keys mandartory when encryption is enabled with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(self._base_path + [interface, 'security', 'mka', 'ckn', mak_ckn]) self.cli_set(self._base_path + [interface, 'security', 'replay-window', replay_window]) # final commit of settings self.cli_commit() tmp = get_config_value(src_interface, 'macsec_integ_only') self.assertIn("0", tmp) tmp = get_config_value(src_interface, 'mka_cak') self.assertIn(mak_cak, tmp) tmp = get_config_value(src_interface, 'mka_ckn') self.assertIn(mak_ckn, tmp) # check that the default priority of 255 is programmed tmp = get_config_value(src_interface, 'mka_priority') self.assertIn("255", tmp) tmp = get_config_value(src_interface, 'macsec_replay_window') self.assertIn(replay_window, tmp) tmp = read_file(f'/sys/class/net/{interface}/mtu') self.assertEqual(tmp, '1460') # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_macsec_gcm_aes_128(self): src_interface = 'eth0' interface = 'macsec1' cipher = 'gcm-aes-128' self.cli_set(self._base_path + [interface]) # 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', cipher]) # final commit and verify self.cli_commit() self.assertIn(interface, interfaces()) self.assertIn(interface, interfaces()) self.assertEqual(cipher, get_cipher(interface)) # check that we use the new macsec_csindex option (T4537) tmp = get_config_value(src_interface, 'macsec_csindex') self.assertIn("0", tmp) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_macsec_gcm_aes_256(self): src_interface = 'eth0' interface = 'macsec4' cipher = 'gcm-aes-256' self.cli_set(self._base_path + [interface]) # 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', cipher]) # final commit and verify self.cli_commit() self.assertIn(interface, interfaces()) self.assertEqual(cipher, get_cipher(interface)) # check that we use the new macsec_csindex option (T4537) tmp = get_config_value(src_interface, 'macsec_csindex') self.assertIn("1", tmp) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_macsec_source_interface(self): # Ensure source-interface can bot be part of any other bond or bridge base_bridge = ['interfaces', 'bridge', 'br200'] base_bond = ['interfaces', 'bonding', 'bond200'] for interface, option_value in self._options.items(): for option in option_value: self.cli_set(self._base_path + [interface] + option.split()) if option.split()[0] == 'source-interface': src_interface = option.split()[1] self.cli_set(base_bridge + ['member', 'interface', src_interface]) # check validate() - Source interface must not already be a member of a bridge with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(base_bridge) self.cli_set(base_bond + ['member', 'interface', src_interface]) # check validate() - Source interface must not already be a member of a bridge with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(base_bond) # final commit and verify self.cli_commit() self.assertIn(interface, interfaces()) # 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', 'tx-key', tx_key_2]) + 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', 'tx-key', tx_key_1]) + 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', 'rx-key', rx_key_1]) + 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', 'tx-key', tx_key_2]) + 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', 'rx-key', rx_key_2]) + 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)) - # Add checks for keys? 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 023fcdffc..0cd733294 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -1,205 +1,207 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2022 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 <http://www.gnu.org/licenses/>. import os from netifaces import interfaces from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configdict import is_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_source_interface from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import MACsecIf from vyos.ifconfig import Interface from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.utils.process import is_systemd_service_running from vyos import ConfigError from vyos import airbag 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 interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'macsec'] ifname, macsec = get_interface_dict(conf, base) # Check if interface has been removed if 'deleted' in macsec: source_interface = conf.return_effective_value(base + [ifname, 'source-interface']) macsec.update({'source_interface': source_interface}) if is_node_changed(conf, base + [ifname, 'security']): macsec.update({'shutdown_required': {}}) if is_node_changed(conf, base + [ifname, 'source_interface']): macsec.update({'shutdown_required': {}}) if 'source_interface' in macsec: tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet']) if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp}) return macsec def verify(macsec): if 'deleted' in macsec: verify_bridge_delete(macsec) return None verify_source_interface(macsec) verify_vrf(macsec) verify_mtu_ipv6(macsec) verify_address(macsec) verify_bond_bridge_member(macsec) verify_mirror_redirect(macsec) if dict_search('security.cipher', macsec) == None: raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) if dict_search('security.encrypt', macsec) != None: # 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.tx_key', macsec) == None: + if dict_search('security.static.key', macsec) == None: raise ConfigError('Static MACsec tx-key must be defined.') - tx_len = len(dict_search('security.static.tx_key', macsec)) + tx_len = len(dict_search('security.static.key', macsec)) - if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_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 tx_len != GCM_AES_128_LEN: + raise ConfigError(GCM_128_KEY_ERROR) - if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != 64: - # gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit - raise ConfigError('gcm-aes-256 requires a 256bit long key!') + 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 'rx_key' not in peer_config): + 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['rx_key']) + rx_len = len(peer_config['key']) - if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_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 rx_len != GCM_AES_128_LEN: + raise ConfigError(GCM_128_KEY_ERROR) - if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != 64: - # gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit - raise ConfigError('gcm-aes-256 requires a 256bit long key!') + 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)) - 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-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit - raise ConfigError('gcm-aes-256 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 # and 802.1q) - we need to check the underlaying MTU if our configured # MTU is at least 40 bytes less then the MTU of our physical interface. lower_mtu = Interface(macsec['source_interface']).get_mtu() if lower_mtu < (int(macsec['mtu']) + 40): raise ConfigError('MACsec overhead does not fit into underlaying device MTU,\n' \ f'{lower_mtu} bytes is too small!') return None def generate(macsec): # Only generate wpa_supplicant config if using MKA if dict_search('security.static', macsec) == None: render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec) return None def apply(macsec): systemd_service = 'wpa_supplicant-macsec@{source_interface}'.format(**macsec) # Remove macsec interface on deletion or mandatory parameter change if 'deleted' in macsec or 'shutdown_required' in macsec: call(f'systemctl stop {systemd_service}') if macsec['ifname'] in interfaces(): tmp = MACsecIf(macsec['ifname']) tmp.remove() if 'deleted' in macsec: # delete configuration on interface removal if os.path.isfile(wpa_suppl_conf.format(**macsec)): os.unlink(wpa_suppl_conf.format(**macsec)) return None # It is safe to "re-create" the interface always, there is a sanity # check that the interface will only be create if its non existent i = MACsecIf(**macsec) i.update(macsec) - #Only reload/restart if not using static keys - if dict_search('security.static', macsec) == None: + # 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 if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)