diff --git a/.gitignore b/.gitignore index 68203577f..92779dc04 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ build/ # Ignore generated files from unit tests testing/unit_test/temp/ +testing/unit/conn/output/ testing/unit/dns/output/ testing/unit/nmap/output/ testing/unit/ntp/output/ diff --git a/modules/test/base/bin/setup b/modules/test/base/bin/setup new file mode 100644 index 000000000..23c96c513 --- /dev/null +++ b/modules/test/base/bin/setup @@ -0,0 +1,71 @@ +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Define the local mount point to store local files to +export OUTPUT_DIR="/runtime/output" + +# Directory where all binaries will be loaded +export BIN_DIR="/testrun/bin" + +# Default interface should be veth0 for all containers +export IFACE=veth0 + +# Create a local user that matches the same as the host +# to be used for correct file ownership for various logs +# HOST_USER mapped in via docker container environemnt variables +useradd $HOST_USER + +# Set permissions on the output files +chown -R $HOST_USER $OUTPUT_DIR + +# Enable IPv6 for all containers +sysctl net.ipv6.conf.all.disable_ipv6=0 +sysctl -p + +# Read in the config file +CONF_FILE="/testrun/conf/module_config.json" +CONF=`cat $CONF_FILE` + +if [[ -z $CONF ]] +then + echo "No config file present at $CONF_FILE. Exiting startup." + exit 1 +fi + +# Extract the necessary config parameters +export MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') +export NETWORK_REQUIRED=$(echo "$CONF" | jq -r '.config.network') +export GRPC=$(echo "$CONF" | jq -r '.config.grpc') + +# Validate the module name is present +if [[ -z "$MODULE_NAME" || "$MODULE_NAME" == "null" ]] +then + echo "No module name present in $CONF_FILE. Exiting startup." + exit 1 +fi + +# Setup the PYTHONPATH so all imports work as expected +echo "Setting up PYTHONPATH..." +export PYTHONPATH=$($BIN_DIR/setup_python_path) +echo "PYTHONPATH: $PYTHONPATH" + +echo "Configuring binary files..." +$BIN_DIR/setup_binaries $BIN_DIR + +# Build all gRPC files from the proto for use in +# gRPC clients for communications to network modules +echo "Building gRPC files from available proto files..." +$BIN_DIR/setup_grpc_clients \ No newline at end of file diff --git a/modules/test/base/bin/start b/modules/test/base/bin/start index 37902b868..d1f29989f 100755 --- a/modules/test/base/bin/start +++ b/modules/test/base/bin/start @@ -14,4 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -/testrun/bin/start_module \ No newline at end of file +# Allow one argument which is the unit test file to run +# instead of running the test module +UNIT_TEST_FILE=$1 + +source /testrun/bin/setup + +# Conditionally run start_module based on RUN +if [[ -z "$UNIT_TEST_FILE" ]];then + /testrun/bin/start_module +else + python3 $UNIT_TEST_FILE +fi diff --git a/modules/test/base/bin/start_module b/modules/test/base/bin/start_module index 0ee68fa6a..fb79cc018 100644 --- a/modules/test/base/bin/start_module +++ b/modules/test/base/bin/start_module @@ -1,102 +1,46 @@ -#!/bin/bash - -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Define the local mount point to store local files to -OUTPUT_DIR="/runtime/output" - -# Directory where all binaries will be loaded -BIN_DIR="/testrun/bin" - -# Default interface should be veth0 for all containers -IFACE=veth0 - -# Create a local user that matches the same as the host -# to be used for correct file ownership for various logs -# HOST_USER mapped in via docker container environemnt variables -useradd $HOST_USER - -# Set permissions on the output files -chown -R $HOST_USER $OUTPUT_DIR - -# Enable IPv6 for all containers -sysctl net.ipv6.conf.all.disable_ipv6=0 -sysctl -p - -# Read in the config file -CONF_FILE="/testrun/conf/module_config.json" -CONF=`cat $CONF_FILE` - -if [[ -z $CONF ]] -then - echo "No config file present at $CONF_FILE. Exiting startup." - exit 1 -fi - -# Extract the necessary config parameters -MODULE_NAME=$(echo "$CONF" | jq -r '.config.meta.name') -NETWORK_REQUIRED=$(echo "$CONF" | jq -r '.config.network') -GRPC=$(echo "$CONF" | jq -r '.config.grpc') - -# Validate the module name is present -if [[ -z "$MODULE_NAME" || "$MODULE_NAME" == "null" ]] -then - echo "No module name present in $CONF_FILE. Exiting startup." - exit 1 -fi - -# Setup the PYTHONPATH so all imports work as expected -echo "Setting up PYTHONPATH..." -export PYTHONPATH=$($BIN_DIR/setup_python_path) -echo "PYTHONPATH: $PYTHONPATH" - -# Build all gRPC files from the proto for use in -# gRPC clients for communications to network modules -echo "Building gRPC files from available proto files..." -$BIN_DIR/setup_grpc_clients - -echo "Configuring binary files..." -$BIN_DIR/setup_binaries $BIN_DIR - -echo "Starting module $MODULE_NAME..." - -# Only start network services if the test container needs -# a network connection to run its tests -if [ $NETWORK_REQUIRED == "true" ];then - # Wait for interface to become ready - $BIN_DIR/wait_for_interface $IFACE - - # Start network capture - $BIN_DIR/capture $MODULE_NAME $IFACE -fi - -# Start the grpc server -if [[ ! -z $GRPC && ! $GRPC == "null" ]] -then - GRPC_PORT=$(echo "$GRPC" | jq -r '.port') - if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] - then - echo "gRPC port resolved from config: $GRPC_PORT" - $BIN_DIR/start_grpc "-p $GRPC_PORT" - else - $BIN_DIR/start_grpc - fi -fi - -# Small pause to let all core services stabalize -sleep 3 - -# Start the test module +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +echo "Starting module $MODULE_NAME..." + +# Only start network services if the test container needs +# a network connection to run its tests +if [ $NETWORK_REQUIRED == "true" ];then + # Wait for interface to become ready + $BIN_DIR/wait_for_interface $IFACE + + # Start network capture + $BIN_DIR/capture $MODULE_NAME $IFACE +fi + +# Start the grpc server +if [[ ! -z $GRPC && ! $GRPC == "null" ]] +then + GRPC_PORT=$(echo "$GRPC" | jq -r '.port') + if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] + then + echo "gRPC port resolved from config: $GRPC_PORT" + $BIN_DIR/start_grpc "-p $GRPC_PORT" + else + $BIN_DIR/start_grpc + fi +fi + +# Small pause to let all core services stabalize +sleep 3 + +# Start the test module $BIN_DIR/start_test_module $MODULE_NAME $IFACE \ No newline at end of file diff --git a/modules/test/baseline/baseline.Dockerfile b/modules/test/baseline/baseline.Dockerfile index 5e0288018..f7d21f8c8 100644 --- a/modules/test/baseline/baseline.Dockerfile +++ b/modules/test/baseline/baseline.Dockerfile @@ -24,11 +24,5 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin -# Remove incorrect line endings -RUN dos2unix /testrun/bin/* - -# Make sure all the bin files are executable -RUN chmod u+x /testrun/bin/* - # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/conn/conn.Dockerfile b/modules/test/conn/conn.Dockerfile index 97c411e63..a9f523e44 100644 --- a/modules/test/conn/conn.Dockerfile +++ b/modules/test/conn/conn.Dockerfile @@ -35,11 +35,5 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin -# Remove incorrect line endings -RUN dos2unix /testrun/bin/* - -# Make sure all the bin files are executable -RUN chmod u+x /testrun/bin/* - # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index aaaf51638..88dd40393 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -15,7 +15,7 @@ import util import time import traceback -from scapy.all import rdpcap, DHCP, ARP, Ether, IPv6, ICMPv6ND_NS +from scapy.all import rdpcap, DHCP, ARP, Ether, ICMP, IPv6, ICMPv6ND_NS from test_module import TestModule from dhcp1.client import Client as DHCPClient1 from dhcp2.client import Client as DHCPClient2 @@ -39,7 +39,14 @@ class ConnectionModule(TestModule): """Connection Test module""" - def __init__(self, module, log_dir=None, conf_file=None, results_dir=None): + def __init__(self, + module, + log_dir=None, + conf_file=None, + results_dir=None, + startup_capture_file=STARTUP_CAPTURE_FILE, + monitor_capture_file=MONITOR_CAPTURE_FILE): + super().__init__(module_name=module, log_name=LOG_NAME, log_dir=log_dir, @@ -47,6 +54,8 @@ def __init__(self, module, log_dir=None, conf_file=None, results_dir=None): results_dir=results_dir) global LOGGER LOGGER = self._get_logger() + self.startup_capture_file = startup_capture_file + self.monitor_capture_file = monitor_capture_file self._port_stats = PortStatsUtil(logger=LOGGER) self.dhcp1_client = DHCPClient1() self.dhcp2_client = DHCPClient2() @@ -106,7 +115,8 @@ def _connection_switch_arp_inspection(self): no_arp = True # Read all the pcap files - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) for packet in packets: # We are not interested in packets unless they are ARP packets @@ -123,12 +133,8 @@ def _connection_switch_arp_inspection(self): # Check MAC address matches IP address if (arp_packet.hwsrc == self._device_mac - and (arp_packet.psrc not in ( - self._device_ipv4_addr, - '0.0.0.0' - )) and not arp_packet.psrc.startswith( - '169.254' - )): + and (arp_packet.psrc not in (self._device_ipv4_addr, '0.0.0.0')) + and not arp_packet.psrc.startswith('169.254')): LOGGER.info(f'Bad ARP packet detected for MAC: {self._device_mac}') LOGGER.info(f'''ARP packet from IP {arp_packet.psrc} does not match {self._device_ipv4_addr}''') @@ -145,7 +151,8 @@ def _connection_switch_dhcp_snooping(self): disallowed_dhcp_types = [2, 4, 5, 6, 9, 10, 12, 13, 15, 17] # Read all the pcap files - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) for packet in packets: # We are not interested in packets unless they are DHCP packets @@ -158,6 +165,11 @@ def _connection_switch_dhcp_snooping(self): dhcp_type = self._get_dhcp_type(packet) if dhcp_type in disallowed_dhcp_types: + + # Check if packet is responding with port unreachable + if ICMP in packet and packet[ICMP].type == 3: + continue + return False, 'Device has sent disallowed DHCP message' return True, 'Device does not act as a DHCP server' @@ -220,7 +232,8 @@ def _connection_single_ip(self): return result, 'No MAC address found.' # Read all the pcap files containing DHCP packet information - packets = rdpcap(STARTUP_CAPTURE_FILE) + rdpcap(MONITOR_CAPTURE_FILE) + packets = rdpcap(self.startup_capture_file) + rdpcap( + self.monitor_capture_file) # Extract MAC addresses from DHCP packets mac_addresses = set() @@ -394,8 +407,9 @@ def _connection_ipv6_slaac(self): return result def _has_slaac_addres(self): - packet_capture = (rdpcap(STARTUP_CAPTURE_FILE) + - rdpcap(MONITOR_CAPTURE_FILE) + rdpcap(DHCP_CAPTURE_FILE)) + packet_capture = (rdpcap(self.startup_capture_file) + + rdpcap(self.monitor_capture_file) + + rdpcap(DHCP_CAPTURE_FILE)) sends_ipv6 = False for packet_number, packet in enumerate(packet_capture, start=1): if IPv6 in packet and packet.src == self._device_mac: @@ -432,7 +446,7 @@ def _ping(self, host, ipv6=False): cmd += ' -6 ' if ipv6 else '' cmd += str(host) #cmd = 'ping -c 1 ' + str(host) - success = util.run_command(cmd, output=False) # pylint: disable=E1120 + success = util.run_command(cmd, output=False) # pylint: disable=E1120 return success def restore_failover_dhcp_server(self, subnet): diff --git a/modules/test/dns/dns.Dockerfile b/modules/test/dns/dns.Dockerfile index 1b0703ad3..0197fd72e 100644 --- a/modules/test/dns/dns.Dockerfile +++ b/modules/test/dns/dns.Dockerfile @@ -30,11 +30,5 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin -# Remove incorrect line endings -RUN dos2unix /testrun/bin/* - -# Make sure all the bin files are executable -RUN chmod u+x /testrun/bin/* - # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/ntp/ntp.Dockerfile b/modules/test/ntp/ntp.Dockerfile index c0c742596..33b06287e 100644 --- a/modules/test/ntp/ntp.Dockerfile +++ b/modules/test/ntp/ntp.Dockerfile @@ -16,11 +16,5 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin -# Remove incorrect line endings -RUN dos2unix /testrun/bin/* - -# Make sure all the bin files are executable -RUN chmod u+x /testrun/bin/* - # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/protocol/protocol.Dockerfile b/modules/test/protocol/protocol.Dockerfile index ce7b7b272..6f55520e1 100644 --- a/modules/test/protocol/protocol.Dockerfile +++ b/modules/test/protocol/protocol.Dockerfile @@ -39,11 +39,5 @@ COPY $MODULE_DIR/bin /testrun/bin # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin -# Remove incorrect line endings -RUN dos2unix /testrun/bin/* - -# Make sure all the bin files are executable -RUN chmod u+x /testrun/bin/* - # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/services/services.Dockerfile b/modules/test/services/services.Dockerfile index cad8ea314..3a89fc33c 100644 --- a/modules/test/services/services.Dockerfile +++ b/modules/test/services/services.Dockerfile @@ -30,11 +30,5 @@ COPY $MODULE_DIR/conf /testrun/conf # Copy over all binary files COPY $MODULE_DIR/bin /testrun/bin -# Remove incorrect line endings -RUN dos2unix /testrun/bin/* - -# Make sure all the bin files are executable -RUN chmod u+x /testrun/bin/* - # Copy over all python files COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/testing/unit/conn/captures/monitor.pcap b/testing/unit/conn/captures/monitor.pcap new file mode 100644 index 000000000..0dfb85ff4 Binary files /dev/null and b/testing/unit/conn/captures/monitor.pcap differ diff --git a/testing/unit/conn/captures/startup.pcap b/testing/unit/conn/captures/startup.pcap new file mode 100644 index 000000000..dadd2edbc Binary files /dev/null and b/testing/unit/conn/captures/startup.pcap differ diff --git a/testing/unit/conn/conn_module_test.py b/testing/unit/conn/conn_module_test.py index d31a8051f..906abb754 100644 --- a/testing/unit/conn/conn_module_test.py +++ b/testing/unit/conn/conn_module_test.py @@ -13,13 +13,17 @@ # limitations under the License. """Module run all the Connection module related unit tests""" from port_stats_util import PortStatsUtil +from connection_module import ConnectionModule import os import unittest from common import logger MODULE = 'conn' -# Define the file paths -TEST_FILES_DIR = 'testing/unit/' + MODULE +# Define the directories +TEST_FILES_DIR = '/testing/unit/' + MODULE +OUTPUT_DIR = os.path.join(TEST_FILES_DIR, 'output/') +CAPTURES_DIR = os.path.join(TEST_FILES_DIR, 'captures/') + ETHTOOL_RESULTS_COMPLIANT_FILE = os.path.join(TEST_FILES_DIR, 'ethtool', 'ethtool_results_compliant.txt') ETHTOOL_RESULTS_NONCOMPLIANT_FILE = os.path.join( @@ -34,8 +38,12 @@ ETHTOOL_PORT_STATS_POST_NONCOMPLIANT_FILE = os.path.join( TEST_FILES_DIR, 'ethtool', 'ethtool_port_stats_post_monitor_noncompliant.txt') -LOGGER = None +# Define the capture files to be used for the test +STARTUP_CAPTURE_FILE = os.path.join(CAPTURES_DIR, 'startup.pcap') +MONITOR_CAPTURE_FILE = os.path.join(CAPTURES_DIR, 'monitor.pcap') + +LOGGER = None class ConnectionModuleTest(unittest.TestCase): """Contains and runs all the unit tests concerning Connection @@ -46,6 +54,9 @@ def setUpClass(cls): global LOGGER LOGGER = logger.get_logger('unit_test_' + MODULE) + # Set the MAC address for device in capture files + os.environ['DEVICE_MAC'] = '98:f0:7b:d1:87:06' + # Test the port link status def connection_port_link_compliant_test(self): LOGGER.info('connection_port_link_compliant_test') @@ -117,6 +128,17 @@ def connection_port_speed_autonegotiation_fail_test(self): LOGGER.info(result) self.assertEqual(result[0], False) + # Test proper filtering for ICMP protocol in DHCP packets + def connection_switch_dhcp_snooping_icmp_test(self): + LOGGER.info('connection_switch_dhcp_snooping_icmp_test') + conn_module = ConnectionModule(module=MODULE, + log_dir=OUTPUT_DIR, + results_dir=OUTPUT_DIR, + startup_capture_file=STARTUP_CAPTURE_FILE, + monitor_capture_file=MONITOR_CAPTURE_FILE) + result = conn_module._connection_switch_dhcp_snooping() # pylint: disable=W0212 + LOGGER.info(result) + self.assertEqual(result[0], True) if __name__ == '__main__': suite = unittest.TestSuite() @@ -136,5 +158,9 @@ def connection_port_speed_autonegotiation_fail_test(self): suite.addTest( ConnectionModuleTest('connection_port_speed_autonegotiation_fail_test')) + # DHCP Snooping related tests + suite.addTest( + ConnectionModuleTest('connection_switch_dhcp_snooping_icmp_test')) + runner = unittest.TextTestRunner() runner.run(suite) diff --git a/testing/unit/run.sh b/testing/unit/run.sh index 16aa9e0a0..795a1645b 100644 --- a/testing/unit/run.sh +++ b/testing/unit/run.sh @@ -27,13 +27,11 @@ run_test() { # Define the location in the container to # load the unit test files local UNIT_TEST_DIR_DST="/testing/unit/$MODULE_NAME" - local UNIT_TEST_FILE_DST="$UNIT_TEST_DIR_DST/module_test.py" - - # Define the python path inside the container - local PYTHONPATH="/testrun/python/src" + local UNIT_TEST_FILE_DST="/testrun/python/src/module_test.py" # Build the docker run command - local DOCKER_CMD="sudo docker run --rm -it --name ${MODULE_NAME}-unit-test -e PYTHONPATH=$PYTHONPATH" + local DOCKER_CMD="sudo docker run --rm -it --name ${MODULE_NAME}-unit-test" + # Add volume mounts for the main test file DOCKER_CMD="$DOCKER_CMD -v $UNIT_TEST_FILE_SRC:$UNIT_TEST_FILE_DST" @@ -44,14 +42,14 @@ run_test() { done # Add the container image and entry point - DOCKER_CMD="$DOCKER_CMD --entrypoint python3 test-run/${MODULE_NAME}-test $UNIT_TEST_FILE_DST" - + DOCKER_CMD="$DOCKER_CMD test-run/${MODULE_NAME}-test $UNIT_TEST_FILE_DST" + # Execute the docker command eval $DOCKER_CMD } # Run all test module tests from within their containers -run_test "conn" "ethtool" "output" +run_test "conn" "captures" "ethtool" "output" run_test "dns" "captures" "reports" "output" run_test "ntp" "captures" "reports" "output" run_test "protocol" "captures" "output" diff --git a/testing/unit/tls/certs/_.google.com.crt b/testing/unit/tls/certs/_.google.com.crt index 004c784d2..442bf6ec3 100644 --- a/testing/unit/tls/certs/_.google.com.crt +++ b/testing/unit/tls/certs/_.google.com.crt @@ -1,80 +1,77 @@ -----BEGIN CERTIFICATE----- -MIIOfTCCDWWgAwIBAgIRAJ/CcPio+CfgCR8NxdR88h4wDQYJKoZIhvcNAQELBQAw -RjELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBM -TEMxEzARBgNVBAMTCkdUUyBDQSAxQzMwHhcNMjQwNTA2MTM0MjA5WhcNMjQwNzI5 -MTM0MjA4WjAXMRUwEwYDVQQDDAwqLmdvb2dsZS5jb20wWTATBgcqhkjOPQIBBggq -hkjOPQMBBwNCAATgJirFNxNZgRzkS+uXAw1Z0lHqpQkPUJHRZg9LoEMfkj6fiR8V -OMJKVzDqu1I9IaKaqLv+Dcl7K9ehTZx+3PUeo4IMXjCCDFowDgYDVR0PAQH/BAQD -AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE -FEQ+mbMDHKra3kablwkwj2mqjiPJMB8GA1UdIwQYMBaAFIp0f6+Fze6VzT2c0OJG -FPNxNR0nMGoGCCsGAQUFBwEBBF4wXDAnBggrBgEFBQcwAYYbaHR0cDovL29jc3Au -cGtpLmdvb2cvZ3RzMWMzMDEGCCsGAQUFBzAChiVodHRwOi8vcGtpLmdvb2cvcmVw -by9jZXJ0cy9ndHMxYzMuZGVyMIIKDgYDVR0RBIIKBTCCCgGCDCouZ29vZ2xlLmNv -bYIWKi5hcHBlbmdpbmUuZ29vZ2xlLmNvbYIJKi5iZG4uZGV2ghUqLm9yaWdpbi10 -ZXN0LmJkbi5kZXaCEiouY2xvdWQuZ29vZ2xlLmNvbYIYKi5jcm93ZHNvdXJjZS5n -b29nbGUuY29tghgqLmRhdGFjb21wdXRlLmdvb2dsZS5jb22CCyouZ29vZ2xlLmNh -ggsqLmdvb2dsZS5jbIIOKi5nb29nbGUuY28uaW6CDiouZ29vZ2xlLmNvLmpwgg4q -Lmdvb2dsZS5jby51a4IPKi5nb29nbGUuY29tLmFygg8qLmdvb2dsZS5jb20uYXWC -DyouZ29vZ2xlLmNvbS5icoIPKi5nb29nbGUuY29tLmNvgg8qLmdvb2dsZS5jb20u -bXiCDyouZ29vZ2xlLmNvbS50coIPKi5nb29nbGUuY29tLnZuggsqLmdvb2dsZS5k -ZYILKi5nb29nbGUuZXOCCyouZ29vZ2xlLmZyggsqLmdvb2dsZS5odYILKi5nb29n -bGUuaXSCCyouZ29vZ2xlLm5sggsqLmdvb2dsZS5wbIILKi5nb29nbGUucHSCDyou -Z29vZ2xlYXBpcy5jboIRKi5nb29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboIQ -Ki5nc3RhdGljLWNuLmNvbYIPZ29vZ2xlY25hcHBzLmNughEqLmdvb2dsZWNuYXBw -cy5jboIRZ29vZ2xlYXBwcy1jbi5jb22CEyouZ29vZ2xlYXBwcy1jbi5jb22CDGdr -ZWNuYXBwcy5jboIOKi5na2VjbmFwcHMuY26CEmdvb2dsZWRvd25sb2Fkcy5jboIU -Ki5nb29nbGVkb3dubG9hZHMuY26CEHJlY2FwdGNoYS5uZXQuY26CEioucmVjYXB0 -Y2hhLm5ldC5jboIQcmVjYXB0Y2hhLWNuLm5ldIISKi5yZWNhcHRjaGEtY24ubmV0 -ggt3aWRldmluZS5jboINKi53aWRldmluZS5jboIRYW1wcHJvamVjdC5vcmcuY26C -EyouYW1wcHJvamVjdC5vcmcuY26CEWFtcHByb2plY3QubmV0LmNughMqLmFtcHBy -b2plY3QubmV0LmNughdnb29nbGUtYW5hbHl0aWNzLWNuLmNvbYIZKi5nb29nbGUt -YW5hbHl0aWNzLWNuLmNvbYIXZ29vZ2xlYWRzZXJ2aWNlcy1jbi5jb22CGSouZ29v -Z2xlYWRzZXJ2aWNlcy1jbi5jb22CEWdvb2dsZXZhZHMtY24uY29tghMqLmdvb2ds -ZXZhZHMtY24uY29tghFnb29nbGVhcGlzLWNuLmNvbYITKi5nb29nbGVhcGlzLWNu -LmNvbYIVZ29vZ2xlb3B0aW1pemUtY24uY29tghcqLmdvb2dsZW9wdGltaXplLWNu -LmNvbYISZG91YmxlY2xpY2stY24ubmV0ghQqLmRvdWJsZWNsaWNrLWNuLm5ldIIY -Ki5mbHMuZG91YmxlY2xpY2stY24ubmV0ghYqLmcuZG91YmxlY2xpY2stY24ubmV0 -gg5kb3VibGVjbGljay5jboIQKi5kb3VibGVjbGljay5jboIUKi5mbHMuZG91Ymxl -Y2xpY2suY26CEiouZy5kb3VibGVjbGljay5jboIRZGFydHNlYXJjaC1jbi5uZXSC -EyouZGFydHNlYXJjaC1jbi5uZXSCHWdvb2dsZXRyYXZlbGFkc2VydmljZXMtY24u -Y29tgh8qLmdvb2dsZXRyYXZlbGFkc2VydmljZXMtY24uY29tghhnb29nbGV0YWdz -ZXJ2aWNlcy1jbi5jb22CGiouZ29vZ2xldGFnc2VydmljZXMtY24uY29tghdnb29n -bGV0YWdtYW5hZ2VyLWNuLmNvbYIZKi5nb29nbGV0YWdtYW5hZ2VyLWNuLmNvbYIY -Z29vZ2xlc3luZGljYXRpb24tY24uY29tghoqLmdvb2dsZXN5bmRpY2F0aW9uLWNu -LmNvbYIkKi5zYWZlZnJhbWUuZ29vZ2xlc3luZGljYXRpb24tY24uY29tghZhcHAt -bWVhc3VyZW1lbnQtY24uY29tghgqLmFwcC1tZWFzdXJlbWVudC1jbi5jb22CC2d2 -dDEtY24uY29tgg0qLmd2dDEtY24uY29tggtndnQyLWNuLmNvbYINKi5ndnQyLWNu -LmNvbYILMm1kbi1jbi5uZXSCDSouMm1kbi1jbi5uZXSCFGdvb2dsZWZsaWdodHMt -Y24ubmV0ghYqLmdvb2dsZWZsaWdodHMtY24ubmV0ggxhZG1vYi1jbi5jb22CDiou -YWRtb2ItY24uY29tghRnb29nbGVzYW5kYm94LWNuLmNvbYIWKi5nb29nbGVzYW5k -Ym94LWNuLmNvbYIeKi5zYWZlbnVwLmdvb2dsZXNhbmRib3gtY24uY29tgg0qLmdz -dGF0aWMuY29tghQqLm1ldHJpYy5nc3RhdGljLmNvbYIKKi5ndnQxLmNvbYIRKi5n -Y3BjZG4uZ3Z0MS5jb22CCiouZ3Z0Mi5jb22CDiouZ2NwLmd2dDIuY29tghAqLnVy -bC5nb29nbGUuY29tghYqLnlvdXR1YmUtbm9jb29raWUuY29tggsqLnl0aW1nLmNv -bYILYW5kcm9pZC5jb22CDSouYW5kcm9pZC5jb22CEyouZmxhc2guYW5kcm9pZC5j -b22CBGcuY26CBiouZy5jboIEZy5jb4IGKi5nLmNvggZnb28uZ2yCCnd3dy5nb28u -Z2yCFGdvb2dsZS1hbmFseXRpY3MuY29tghYqLmdvb2dsZS1hbmFseXRpY3MuY29t -ggpnb29nbGUuY29tghJnb29nbGVjb21tZXJjZS5jb22CFCouZ29vZ2xlY29tbWVy -Y2UuY29tgghnZ3BodC5jboIKKi5nZ3BodC5jboIKdXJjaGluLmNvbYIMKi51cmNo -aW4uY29tggh5b3V0dS5iZYILeW91dHViZS5jb22CDSoueW91dHViZS5jb22CFHlv -dXR1YmVlZHVjYXRpb24uY29tghYqLnlvdXR1YmVlZHVjYXRpb24uY29tgg95b3V0 -dWJla2lkcy5jb22CESoueW91dHViZWtpZHMuY29tggV5dC5iZYIHKi55dC5iZYIa -YW5kcm9pZC5jbGllbnRzLmdvb2dsZS5jb22CG2RldmVsb3Blci5hbmRyb2lkLmdv -b2dsZS5jboIcZGV2ZWxvcGVycy5hbmRyb2lkLmdvb2dsZS5jboIYc291cmNlLmFu -ZHJvaWQuZ29vZ2xlLmNughpkZXZlbG9wZXIuY2hyb21lLmdvb2dsZS5jboIYd2Vi -LmRldmVsb3BlcnMuZ29vZ2xlLmNugh1jb2RlbGFicy5kZXZlbG9wZXJzLmdvb2ds -ZS5jbjAhBgNVHSAEGjAYMAgGBmeBDAECATAMBgorBgEEAdZ5AgUDMDwGA1UdHwQ1 -MDMwMaAvoC2GK2h0dHA6Ly9jcmxzLnBraS5nb29nL2d0czFjMy9RT3ZKME4xc1Qy -QS5jcmwwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdwB2/4g/Crb7lVHCYcz1h7o0 -tKTNuyncaEIKn+ZnTFo6dAAAAY9OWu9TAAAEAwBIMEYCIQDgV2MAHik+52n1pXYg -+S1EJKqWrHTZqPIDS8T8xpG4awIhAP1xUt+oS5JVgGvepIzowOnWqnXGMjIPz8un -NOy72DVJAHUASLDja9qmRzQP5WoC+p0w6xxSActW3SyB2bu/qznYhHMAAAGPTlrv -SwAABAMARjBEAiA9rUZy4H3k9tlGwyoh58vqNFxdVuu/TZIwVhrii485TAIgPuxq -CgYM1zCnyUuqzLeU3bEdplYB+pR8tjB/eYohZAswDQYJKoZIhvcNAQELBQADggEB -AMIo22fklccARNPymh0c984wFX6j18QuroTEFJoVg7yAiXsFiHOvCWhkf5Yyt/r7 -h9c9yvauIESITbErpCYLbejuGYL+wgQD9DpU75oEy/ViRBM/bjmV3sDbMHRqZUG7 -jHyWkl3DRmFJngR6i7ROByGbrry4xwQM3hofsF6igdwLJvLfcYigrwk3yFfDUTfj -C0xb0Okp2s4zukUfOQSAy7uWul+mkPEoMXwB6fJYvo3uUgXvhM5fbhXhgJIKWKxD -qiDjg7jlCoOrtBlINJY+PqYO/+L2Pyvqy3m3rM6omwwTT3vnFIgL2qdt+cpTNO9I -9EXEIr7rhXDXY3AUpG0xOs0= +MIIN4zCCDMugAwIBAgIRAMX7uoXXmJy3CoNgnp7ELOMwDQYJKoZIhvcNAQELBQAw +OzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEM +MAoGA1UEAxMDV1IyMB4XDTI0MDcwMTA2MzU0M1oXDTI0MDkyMzA2MzU0MlowFzEV +MBMGA1UEAwwMKi5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +1mu/GyluisEPwCtCgrJeDhhGpQ9pvMzgMsbtBi3cFm0+bGTyNyoTRDSTdUsLRJNJ +BRF2O/M8i6CCi6PrYenOhKOCC88wggvLMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE +DDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTQ+KGTW7bq4kmI +AhTEY+bMVA8+mDAfBgNVHSMEGDAWgBTeGx7teRXUPjckwyG77DQ5bUKyMDBYBggr +BgEFBQcBAQRMMEowIQYIKwYBBQUHMAGGFWh0dHA6Ly9vLnBraS5nb29nL3dyMjAl +BggrBgEFBQcwAoYZaHR0cDovL2kucGtpLmdvb2cvd3IyLmNydDCCCaUGA1UdEQSC +CZwwggmYggwqLmdvb2dsZS5jb22CFiouYXBwZW5naW5lLmdvb2dsZS5jb22CCSou +YmRuLmRldoIVKi5vcmlnaW4tdGVzdC5iZG4uZGV2ghIqLmNsb3VkLmdvb2dsZS5j +b22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIYKi5kYXRhY29tcHV0ZS5nb29n +bGUuY29tggsqLmdvb2dsZS5jYYILKi5nb29nbGUuY2yCDiouZ29vZ2xlLmNvLmlu +gg4qLmdvb2dsZS5jby5qcIIOKi5nb29nbGUuY28udWuCDyouZ29vZ2xlLmNvbS5h +coIPKi5nb29nbGUuY29tLmF1gg8qLmdvb2dsZS5jb20uYnKCDyouZ29vZ2xlLmNv +bS5jb4IPKi5nb29nbGUuY29tLm14gg8qLmdvb2dsZS5jb20udHKCDyouZ29vZ2xl +LmNvbS52boILKi5nb29nbGUuZGWCCyouZ29vZ2xlLmVzggsqLmdvb2dsZS5mcoIL +Ki5nb29nbGUuaHWCCyouZ29vZ2xlLml0ggsqLmdvb2dsZS5ubIILKi5nb29nbGUu +cGyCCyouZ29vZ2xlLnB0gg8qLmdvb2dsZWFwaXMuY26CESouZ29vZ2xldmlkZW8u +Y29tggwqLmdzdGF0aWMuY26CECouZ3N0YXRpYy1jbi5jb22CD2dvb2dsZWNuYXBw +cy5jboIRKi5nb29nbGVjbmFwcHMuY26CEWdvb2dsZWFwcHMtY24uY29tghMqLmdv +b2dsZWFwcHMtY24uY29tggxna2VjbmFwcHMuY26CDiouZ2tlY25hcHBzLmNughJn +b29nbGVkb3dubG9hZHMuY26CFCouZ29vZ2xlZG93bmxvYWRzLmNughByZWNhcHRj +aGEubmV0LmNughIqLnJlY2FwdGNoYS5uZXQuY26CEHJlY2FwdGNoYS1jbi5uZXSC +EioucmVjYXB0Y2hhLWNuLm5ldIILd2lkZXZpbmUuY26CDSoud2lkZXZpbmUuY26C +EWFtcHByb2plY3Qub3JnLmNughMqLmFtcHByb2plY3Qub3JnLmNughFhbXBwcm9q +ZWN0Lm5ldC5jboITKi5hbXBwcm9qZWN0Lm5ldC5jboIXZ29vZ2xlLWFuYWx5dGlj +cy1jbi5jb22CGSouZ29vZ2xlLWFuYWx5dGljcy1jbi5jb22CF2dvb2dsZWFkc2Vy +dmljZXMtY24uY29tghkqLmdvb2dsZWFkc2VydmljZXMtY24uY29tghFnb29nbGV2 +YWRzLWNuLmNvbYITKi5nb29nbGV2YWRzLWNuLmNvbYIRZ29vZ2xlYXBpcy1jbi5j +b22CEyouZ29vZ2xlYXBpcy1jbi5jb22CFWdvb2dsZW9wdGltaXplLWNuLmNvbYIX +Ki5nb29nbGVvcHRpbWl6ZS1jbi5jb22CEmRvdWJsZWNsaWNrLWNuLm5ldIIUKi5k +b3VibGVjbGljay1jbi5uZXSCGCouZmxzLmRvdWJsZWNsaWNrLWNuLm5ldIIWKi5n +LmRvdWJsZWNsaWNrLWNuLm5ldIIOZG91YmxlY2xpY2suY26CECouZG91YmxlY2xp +Y2suY26CFCouZmxzLmRvdWJsZWNsaWNrLmNughIqLmcuZG91YmxlY2xpY2suY26C +EWRhcnRzZWFyY2gtY24ubmV0ghMqLmRhcnRzZWFyY2gtY24ubmV0gh1nb29nbGV0 +cmF2ZWxhZHNlcnZpY2VzLWNuLmNvbYIfKi5nb29nbGV0cmF2ZWxhZHNlcnZpY2Vz +LWNuLmNvbYIYZ29vZ2xldGFnc2VydmljZXMtY24uY29tghoqLmdvb2dsZXRhZ3Nl +cnZpY2VzLWNuLmNvbYIXZ29vZ2xldGFnbWFuYWdlci1jbi5jb22CGSouZ29vZ2xl +dGFnbWFuYWdlci1jbi5jb22CGGdvb2dsZXN5bmRpY2F0aW9uLWNuLmNvbYIaKi5n +b29nbGVzeW5kaWNhdGlvbi1jbi5jb22CJCouc2FmZWZyYW1lLmdvb2dsZXN5bmRp +Y2F0aW9uLWNuLmNvbYIWYXBwLW1lYXN1cmVtZW50LWNuLmNvbYIYKi5hcHAtbWVh +c3VyZW1lbnQtY24uY29tggtndnQxLWNuLmNvbYINKi5ndnQxLWNuLmNvbYILZ3Z0 +Mi1jbi5jb22CDSouZ3Z0Mi1jbi5jb22CCzJtZG4tY24ubmV0gg0qLjJtZG4tY24u +bmV0ghRnb29nbGVmbGlnaHRzLWNuLm5ldIIWKi5nb29nbGVmbGlnaHRzLWNuLm5l +dIIMYWRtb2ItY24uY29tgg4qLmFkbW9iLWNuLmNvbYIUZ29vZ2xlc2FuZGJveC1j +bi5jb22CFiouZ29vZ2xlc2FuZGJveC1jbi5jb22CHiouc2FmZW51cC5nb29nbGVz +YW5kYm94LWNuLmNvbYINKi5nc3RhdGljLmNvbYIUKi5tZXRyaWMuZ3N0YXRpYy5j +b22CCiouZ3Z0MS5jb22CESouZ2NwY2RuLmd2dDEuY29tggoqLmd2dDIuY29tgg4q +LmdjcC5ndnQyLmNvbYIQKi51cmwuZ29vZ2xlLmNvbYIWKi55b3V0dWJlLW5vY29v +a2llLmNvbYILKi55dGltZy5jb22CC2FuZHJvaWQuY29tgg0qLmFuZHJvaWQuY29t +ghMqLmZsYXNoLmFuZHJvaWQuY29tggRnLmNuggYqLmcuY26CBGcuY2+CBiouZy5j +b4IGZ29vLmdsggp3d3cuZ29vLmdsghRnb29nbGUtYW5hbHl0aWNzLmNvbYIWKi5n +b29nbGUtYW5hbHl0aWNzLmNvbYIKZ29vZ2xlLmNvbYISZ29vZ2xlY29tbWVyY2Uu +Y29tghQqLmdvb2dsZWNvbW1lcmNlLmNvbYIIZ2dwaHQuY26CCiouZ2dwaHQuY26C +CnVyY2hpbi5jb22CDCoudXJjaGluLmNvbYIIeW91dHUuYmWCC3lvdXR1YmUuY29t +gg0qLnlvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0aW9uLmNvbYIWKi55b3V0dWJl +ZWR1Y2F0aW9uLmNvbYIPeW91dHViZWtpZHMuY29tghEqLnlvdXR1YmVraWRzLmNv +bYIFeXQuYmWCByoueXQuYmWCGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tghMq +LmFuZHJvaWQuZ29vZ2xlLmNughIqLmNocm9tZS5nb29nbGUuY26CFiouZGV2ZWxv +cGVycy5nb29nbGUuY24wEwYDVR0gBAwwCjAIBgZngQwBAgEwNgYDVR0fBC8wLTAr +oCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd3IyL0dTeVQxTjRQQnJnLmNybDCCAQQG +CisGAQQB1nkCBAIEgfUEgfIA8AB1ANq2v2s/tbYin5vCu1xr6HCRcWy7UYSFNL2k +PTBI1/urAAABkG04oVMAAAQDAEYwRAIgAaPd62uAnl5mGI2sgPrRH/vRicAYxwaQ +hj9qnla8Z2ACIBwm4r6O2jnYSzMfFYoxaWa7Zn+7bgbUb1uUrzEe4xl7AHcA7s3Q +ZNXbGs7FXLedtM0TojKHRny87N7DUUhZRnEftZsAAAGQbTihHQAABAMASDBGAiEA +7+ZDT9MHQdLS4XEbXqbGAkbp7GQIYbSdoZg/DALMIm8CIQD3cwXxry/8R2I9BQXA +7SM0047FardcOzEV40z+kMDJ4jANBgkqhkiG9w0BAQsFAAOCAQEAd5GYYcv4ZG6P +OiQgV8RVO0TFSBfYUwNQDyeFQFyaagPvP3LbsJC5m9X22jdf64HpIcyKr7Aw4JIY +RzhqvlIZ2t8Hf1McRavJzCl4hER1dLNlzhCL1z+Wq/xiRz5VqVOXv1okG5YgGiEW +UQCBXPFM8nTAror5sMk1e/2IQOI4/4A2MhWZBrpVwYyaKEJUcdKLq9EbDRHVfsNo +0BQCnt/thD6IzL3FYWsEF0636Trxd9chE2r3JF30ORJm8q90Xz2aWwrFGlB0i8tW +uYv+WTi3qW9o2lLaqfsSTjoOmtGxANknI/0LARuBST3xSGk5GD+r/r8J/6LcCe8x +qvx81n6dcQ== -----END CERTIFICATE-----