From f1fcfe3df88ce736eef4b7f94f609e0c0b7051c4 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Tue, 28 May 2024 13:21:41 -0600 Subject: [PATCH 1/3] Add methods to allow known protocols that dont fit into the strict tls client detection methods Add QUIC protocol to approved protocols Start moving unit tests to a docker container for consistency --- .../tls/bin/get_tls_client_connections.sh | 58 ++++++++++------- modules/test/tls/python/src/tls_util.py | 61 +++++++++++++++--- testing/unit/run_tests.sh | 26 ++++---- .../unit/tls/captures/monitor_with_quic.pcap | Bin 0 -> 102080 bytes testing/unit/tls/tls_module_test.py | 15 +++++ testing/unit/unit_test.Dockerfile | 41 ++++++++++++ 6 files changed, 154 insertions(+), 47 deletions(-) create mode 100644 testing/unit/tls/captures/monitor_with_quic.pcap create mode 100644 testing/unit/unit_test.Dockerfile diff --git a/modules/test/tls/bin/get_tls_client_connections.sh b/modules/test/tls/bin/get_tls_client_connections.sh index f036c2154..3263803b5 100755 --- a/modules/test/tls/bin/get_tls_client_connections.sh +++ b/modules/test/tls/bin/get_tls_client_connections.sh @@ -1,26 +1,34 @@ -#!/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. - -CAPTURE_FILE="$1" -SRC_IP="$2" - -TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" -TSHARK_FILTER="ip.src == $SRC_IP and tls" - -response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) - -echo "$response" +#!/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. + +CAPTURE_FILE="$1" +SRC_IP="$2" +PROTOCOL=$3 + +TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" +TSHARK_FILTER="ip.src == $SRC_IP and tls" + +# Add a protocol filter if defined +if [ ! -n "$PROTOCOL" ];then + TSHARK_FILTER="$TSHARK_FILTER and $PROTOCOL" +fi + +echo "$TSHARK_FIlTER" + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" \ No newline at end of file diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index 368108bdd..02247c2a1 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -37,6 +37,8 @@ ipaddress.ip_network('172.16.0.0/12'), ipaddress.ip_network('192.168.0.0/16') ] +#Define the allowed protocols as tshark filters +DEFAULT_ALLOWED_PROTOCOLS=['quic'] class TLSUtil(): @@ -46,13 +48,15 @@ def __init__(self, logger, bin_dir=DEFAULT_BIN_DIR, cert_out_dir=DEFAULT_CERTS_OUT_DIR, - root_certs_dir=DEFAULT_ROOT_CERTS_DIR): + root_certs_dir=DEFAULT_ROOT_CERTS_DIR, + allowed_protocols=DEFAULT_ALLOWED_PROTOCOLS): global LOGGER LOGGER = logger self._bin_dir = bin_dir self._cert_out_dir = cert_out_dir self._dev_cert_file = 'device_cert.crt' self._root_certs_dir = root_certs_dir + self._allowed_protocols = allowed_protocols def get_public_certificate(self, host, @@ -452,11 +456,14 @@ def get_non_tls_packetes(self, client_ip, capture_files): return combined_packets # Resolve all connections from the device that use TLS - def get_tls_client_connection_packetes(self, client_ip, capture_files): + def get_tls_client_connection_packetes(self, client_ip, capture_files, protocol=None): combined_packets = [] for capture_file in capture_files: bin_file = self._bin_dir + '/get_tls_client_connections.sh' args = f'"{capture_file}" {client_ip}' + if args is not None: + args += f' {protocol}' + LOGGER.info("conn pacets args: " + str(args)) command = f'{bin_file} {args}' response = util.run_command(command) packets = json.loads(response[0].strip()) @@ -504,9 +511,10 @@ def parse_packets(self, packets, capture_file): hello_packets.append(hello_packet) return hello_packets - def process_hello_packets(self, hello_packets, tls_version='1.2'): + def process_hello_packets(self, hello_packets, allowed_protocol_client_ips, tls_version='1.2'): # Validate the ciphers only for tls 1.2 client_hello_results = {'valid': [], 'invalid': []} + if tls_version == '1.2': for packet in hello_packets: if packet['dst_ip'] not in str(client_hello_results['valid']): @@ -524,8 +532,14 @@ def process_hello_packets(self, hello_packets, tls_version='1.2'): client_hello_results['invalid'].remove(invalid_packet) else: LOGGER.info('Invalid ciphers detected') - if packet['dst_ip'] not in str(client_hello_results['invalid']): - client_hello_results['invalid'].append(packet) + if packet['dst_ip'] not in allowed_protocol_client_ips: + if packet['dst_ip'] not in str(client_hello_results['invalid']): + client_hello_results['invalid'].append(packet) + else: + LOGGER.info('Allowing protocol connection, cipher check failure ignored.') + protocol_name = allowed_protocol_client_ips[packet['dst_ip']] + packet['protocol_details'] = f'\nAllowing {protocol_name} traffic to {packet["dst_ip"]}' + client_hello_results['valid'].append(packet) else: # No cipher check for TLS 1.3 client_hello_results['valid'] = hello_packets @@ -610,6 +624,21 @@ def get_tls_client_connection_ips(self, client_ip, capture_files): tls_dst_ips.add(str(dst_ip)) return tls_dst_ips + # Check if the device has made any outbound connections that use any + # allowed protocols that do not fit into a direct TLS packet inspection + def get_allowed_protocol_client_connection_ips(self, client_ip, capture_files): + LOGGER.info('Checking client for TLS Protocol client connections') + tls_dst_ips = {} # Store unique destination IPs with the protocol name + for protocol in self._allowed_protocols: + packets = self.get_tls_client_connection_packetes( + client_ip=client_ip, capture_files=capture_files, protocol=protocol) + + for packet in packets: + dst_ip = ipaddress.ip_address(packet['_source']['layers']['ip.dst'][0]) + tls_dst_ips[str(dst_ip)] = protocol + + return tls_dst_ips + def is_private_ip(self, ip): # Check if an IP is within any private IP subnet for subnet in PRIVATE_SUBNETS: @@ -621,7 +650,13 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): LOGGER.info('Validating client for TLS: ' + tls_version) hello_packets = self.get_hello_packets(capture_files, client_ip, tls_version) - client_hello_results = self.process_hello_packets(hello_packets, + + # Resolve allowed protocol connections that require + # additional consideration beyond packet inspection + allowed_protocol_client_ips = self.get_allowed_protocol_client_connection_ips(client_ip,capture_files) + + LOGGER.info(f'Protocol IPS: {allowed_protocol_client_ips}') + client_hello_results = self.process_hello_packets(hello_packets, allowed_protocol_client_ips, tls_version) handshakes = {'complete': [], 'incomplete': []} @@ -662,7 +697,11 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): if len(handshakes['incomplete']) > 0: for result in handshakes['incomplete']: tls_client_details += 'Incomplete handshake detected from server: ' - tls_client_details += result + '\n' + tls_client_details += result + '.' + hello_result = client_hello_results[result] + if 'protocol_details' in hello_result: + tls_client_details += hello_result['protocol_details'] + tls_client_details += '\n' if len(handshakes['complete']) > 0: # If we haven't already failed the test from previous checks # allow a passing result @@ -670,7 +709,12 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): tls_client_valid = True for result in handshakes['complete']: tls_client_details += 'Completed handshake detected from server: ' - tls_client_details += result + '\n' + tls_client_details += result + '.' + for packet in client_hello_results['valid']: + if result in packet['dst_ip']: + if 'protocol_details' in packet: + tls_client_details += packet['protocol_details'] + tls_client_details += '\n' else: LOGGER.info('No client hello packets detected') tls_client_details = 'No client hello packets detected' @@ -682,7 +726,6 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): # Resolve all TLS related client connections tls_client_ips = self.get_tls_client_connection_ips(client_ip, capture_files) - # Filter out all outbound TLS connections regardless on whether # or not they were validated. If they were not validated, # they will already be failed by those tests and we only diff --git a/testing/unit/run_tests.sh b/testing/unit/run_tests.sh index 2658636dd..726019b23 100644 --- a/testing/unit/run_tests.sh +++ b/testing/unit/run_tests.sh @@ -36,26 +36,26 @@ PYTHONPATH="$PYTHONPATH:$PWD/modules/test/ntp/python/src" # Set the python path with all sources export PYTHONPATH -# Run the DHCP Unit tests -python3 -u $PWD/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py -python3 -u $PWD/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py +# # Run the DHCP Unit tests +# python3 -u $PWD/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py +# python3 -u $PWD/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py -# Run the Conn Module Unit Tests -python3 -u $PWD/testing/unit/conn/conn_module_test.py +# # Run the Conn Module Unit Tests +# python3 -u $PWD/testing/unit/conn/conn_module_test.py # Run the TLS Module Unit Tests python3 -u $PWD/testing/unit/tls/tls_module_test.py -# Run the DNS Module Unit Tests -python3 -u $PWD/testing/unit/dns/dns_module_test.py +# # Run the DNS Module Unit Tests +# python3 -u $PWD/testing/unit/dns/dns_module_test.py -# Run the NMAP Module Unit Tests -python3 -u $PWD/testing/unit/nmap/nmap_module_test.py +# # Run the NMAP Module Unit Tests +# python3 -u $PWD/testing/unit/nmap/nmap_module_test.py -# Run the NTP Module Unit Tests -python3 -u $PWD/testing/unit/ntp/ntp_module_test.py +# # Run the NTP Module Unit Tests +# python3 -u $PWD/testing/unit/ntp/ntp_module_test.py -# # Run the Report Unit Tests -python3 -u $PWD/testing/unit/report/report_test.py +# # # Run the Report Unit Tests +# python3 -u $PWD/testing/unit/report/report_test.py popd >/dev/null 2>&1 \ No newline at end of file diff --git a/testing/unit/tls/captures/monitor_with_quic.pcap b/testing/unit/tls/captures/monitor_with_quic.pcap new file mode 100644 index 0000000000000000000000000000000000000000..728e9877c07a3ebc57ba8ffd907699c3b172d9f1 GIT binary patch literal 102080 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 a?|B0<{o^~B>BVIrw@b_Bc@i)kn34d8LV;NT literal 0 HcmV?d00001 diff --git a/testing/unit/tls/tls_module_test.py b/testing/unit/tls/tls_module_test.py index e8c4afec4..2294f7659 100644 --- a/testing/unit/tls/tls_module_test.py +++ b/testing/unit/tls/tls_module_test.py @@ -300,6 +300,19 @@ def security_tls_client_unsupported_tls_client(self): print(str(test_results)) self.assertFalse(test_results[0]) + # Scan a known capture without u unsupported TLS traffic to + # generate a fail result + def security_tls_client_allowed_protocols_test(self): + print('\nsecurity_tls_client_allowed_protocols_test') + capture_file = os.path.join(CAPTURES_DIR, 'monitor_with_quic.pcap') + + # Run the client test + test_results = TLS_UTIL.validate_tls_client(client_ip='10.10.10.15', + tls_version='1.2', + capture_files=[capture_file]) + print(str(test_results)) + self.assertTrue(test_results[0]) + def tls_module_report_test(self): print('\ntls_module_report_test') os.environ['DEVICE_MAC'] = '38:d1:35:01:17:fe' @@ -518,5 +531,7 @@ def tls_module_ca_cert_spaces_test(self): suite.addTest(TLSModuleTest('tls_module_local_ca_cert_test')) suite.addTest(TLSModuleTest('tls_module_ca_cert_spaces_test')) + suite.addTest(TLSModuleTest('security_tls_client_allowed_protocols_test')) + runner = unittest.TextTestRunner() runner.run(suite) diff --git a/testing/unit/unit_test.Dockerfile b/testing/unit/unit_test.Dockerfile new file mode 100644 index 000000000..3551684d0 --- /dev/null +++ b/testing/unit/unit_test.Dockerfile @@ -0,0 +1,41 @@ +# Image name: test-run/ntp-test +FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 + +RUN apt-get update + +# Set DEBIAN_FRONTEND to noninteractive mode +ENV DEBIAN_FRONTEND=noninteractive + +ARG MODULE_DIR=modules/test +ARG UNIT_TEST_DIR=testing/unit +ARG FRAMEWORK_DIR=framework + +# Install common software +RUN apt-get install -yq net-tools iputils-ping tzdata tcpdump iproute2 jq python3 python3-pip dos2unix nmap wget --fix-missing + +# Install framework python modules +COPY $FRAMEWORK_DIR/ /testrun/$FRAMEWORK_DIR + +# Load all the test modules +COPY $MODULE_DIR/ /testrun/$MODULE_DIR +COPY $UNIT_TEST_DIR /testrun/$UNIT_TEST_DIR + +# Install required software for TLS module +RUN apt-get update && apt-get install -y tshark + +# Install all python requirements for framework +RUN pip3 install -r /testrun/framework/requirements.txt + +# Install all python requirements for the TLS module +RUN pip3 install -r /testrun/modules/test/tls/python/requirements.txt + +# Remove incorrect line endings +RUN dos2unix /testrun/modules/test/tls/bin/* +RUN dos2unix /testrun/testing/unit/* + +# Make sure all the bin files are executable +RUN chmod u+x /testrun/modules/test/tls/bin/* + +WORKDIR /testrun/testing/unit/ + +#ENTRYPOINT [ "./run_tests.sh" ] \ No newline at end of file From c536601543bac2bac8506981068dafa9505024ee Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Thu, 30 May 2024 11:06:25 -0600 Subject: [PATCH 2/3] Fix client connections protocol method script Downgrade cryptography and pyOpenSSL libraries Move unit testing into docker for TLS module --- .../tls/bin/get_tls_client_connections.sh | 4 +- modules/test/tls/python/requirements.txt | 4 +- modules/test/tls/python/src/tls_util.py | 2 +- testing/unit/build.sh | 17 +++ testing/unit/run.sh | 17 +++ testing/unit/tls/certs/_.google.com.crt | 43 ++++---- testing/unit/tls/tls_module_test.py | 100 ++++++++++-------- testing/unit/unit_test.Dockerfile | 3 +- 8 files changed, 116 insertions(+), 74 deletions(-) create mode 100644 testing/unit/build.sh create mode 100644 testing/unit/run.sh diff --git a/modules/test/tls/bin/get_tls_client_connections.sh b/modules/test/tls/bin/get_tls_client_connections.sh index 3263803b5..e2e6da91b 100755 --- a/modules/test/tls/bin/get_tls_client_connections.sh +++ b/modules/test/tls/bin/get_tls_client_connections.sh @@ -22,12 +22,10 @@ TSHARK_OUTPUT="-T json -e ip.src -e tcp.dstport -e ip.dst" TSHARK_FILTER="ip.src == $SRC_IP and tls" # Add a protocol filter if defined -if [ ! -n "$PROTOCOL" ];then +if [ -n "$PROTOCOL" ];then TSHARK_FILTER="$TSHARK_FILTER and $PROTOCOL" fi -echo "$TSHARK_FIlTER" - response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) echo "$response" diff --git a/modules/test/tls/python/requirements.txt b/modules/test/tls/python/requirements.txt index 8dc825edd..d4dad89a1 100644 --- a/modules/test/tls/python/requirements.txt +++ b/modules/test/tls/python/requirements.txt @@ -1,5 +1,5 @@ -cryptography==42.0.7 -pyOpenSSL==24.0.0 +cryptography==38.0.0 # Do not upgrade until TLS module can be fixed to account for removed x509 property in version 39 +pyOpenSSL==23.0.0 lxml==5.1.0 # Requirement of pyshark but if upgraded automatically above 5.1 will cause a python crash pyshark==0.6 requests==2.31.0 diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index 02247c2a1..24ff40fcb 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -461,7 +461,7 @@ def get_tls_client_connection_packetes(self, client_ip, capture_files, protocol= for capture_file in capture_files: bin_file = self._bin_dir + '/get_tls_client_connections.sh' args = f'"{capture_file}" {client_ip}' - if args is not None: + if protocol is not None: args += f' {protocol}' LOGGER.info("conn pacets args: " + str(args)) command = f'{bin_file} {args}' diff --git a/testing/unit/build.sh b/testing/unit/build.sh new file mode 100644 index 000000000..71f45f109 --- /dev/null +++ b/testing/unit/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash -e + +# 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. + +docker build -f testing/unit/unit_test.Dockerfile -t test-run/unit-test . \ No newline at end of file diff --git a/testing/unit/run.sh b/testing/unit/run.sh new file mode 100644 index 000000000..1ae4d6287 --- /dev/null +++ b/testing/unit/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash -e + +# 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. + +docker run --rm -it --name unit-test test-run/unit-test /bin/bash ./run_tests.sh \ No newline at end of file diff --git a/testing/unit/tls/certs/_.google.com.crt b/testing/unit/tls/certs/_.google.com.crt index 8c5519fa3..004c784d2 100644 --- a/testing/unit/tls/certs/_.google.com.crt +++ b/testing/unit/tls/certs/_.google.com.crt @@ -1,15 +1,15 @@ -----BEGIN CERTIFICATE----- -MIIOXTCCDUWgAwIBAgIRAJF3iEqsDpoECedLdRgGyygwDQYJKoZIhvcNAQELBQAw +MIIOfTCCDWWgAwIBAgIRAJ/CcPio+CfgCR8NxdR88h4wDQYJKoZIhvcNAQELBQAw RjELMAkGA1UEBhMCVVMxIjAgBgNVBAoTGUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBM -TEMxEzARBgNVBAMTCkdUUyBDQSAxQzMwHhcNMjQwMjE5MDgwMzU0WhcNMjQwNTEz -MDgwMzUzWjAXMRUwEwYDVQQDDAwqLmdvb2dsZS5jb20wWTATBgcqhkjOPQIBBggq -hkjOPQMBBwNCAAQ7UkR1HmtagYbBBsPVqVik2xZO85oyD4jqvtxb2zKUdBCN3n+E -YFxtMs4KiRDvMzp3C/xWfNaMoF3X7/sDUm3ro4IMPjCCDDowDgYDVR0PAQH/BAQD +TEMxEzARBgNVBAMTCkdUUyBDQSAxQzMwHhcNMjQwNTA2MTM0MjA5WhcNMjQwNzI5 +MTM0MjA4WjAXMRUwEwYDVQQDDAwqLmdvb2dsZS5jb20wWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAATgJirFNxNZgRzkS+uXAw1Z0lHqpQkPUJHRZg9LoEMfkj6fiR8V +OMJKVzDqu1I9IaKaqLv+Dcl7K9ehTZx+3PUeo4IMXjCCDFowDgYDVR0PAQH/BAQD AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYE -FO620PFJar/4etLB8PcVeSgTJybuMB8GA1UdIwQYMBaAFIp0f6+Fze6VzT2c0OJG +FEQ+mbMDHKra3kablwkwj2mqjiPJMB8GA1UdIwQYMBaAFIp0f6+Fze6VzT2c0OJG FPNxNR0nMGoGCCsGAQUFBwEBBF4wXDAnBggrBgEFBQcwAYYbaHR0cDovL29jc3Au cGtpLmdvb2cvZ3RzMWMzMDEGCCsGAQUFBzAChiVodHRwOi8vcGtpLmdvb2cvcmVw -by9jZXJ0cy9ndHMxYzMuZGVyMIIJ7wYDVR0RBIIJ5jCCCeKCDCouZ29vZ2xlLmNv +by9jZXJ0cy9ndHMxYzMuZGVyMIIKDgYDVR0RBIIKBTCCCgGCDCouZ29vZ2xlLmNv bYIWKi5hcHBlbmdpbmUuZ29vZ2xlLmNvbYIJKi5iZG4uZGV2ghUqLm9yaWdpbi10 ZXN0LmJkbi5kZXaCEiouY2xvdWQuZ29vZ2xlLmNvbYIYKi5jcm93ZHNvdXJjZS5n b29nbGUuY29tghgqLmRhdGFjb21wdXRlLmdvb2dsZS5jb22CCyouZ29vZ2xlLmNh @@ -62,18 +62,19 @@ dWJla2lkcy5jb22CESoueW91dHViZWtpZHMuY29tggV5dC5iZYIHKi55dC5iZYIa YW5kcm9pZC5jbGllbnRzLmdvb2dsZS5jb22CG2RldmVsb3Blci5hbmRyb2lkLmdv b2dsZS5jboIcZGV2ZWxvcGVycy5hbmRyb2lkLmdvb2dsZS5jboIYc291cmNlLmFu ZHJvaWQuZ29vZ2xlLmNughpkZXZlbG9wZXIuY2hyb21lLmdvb2dsZS5jboIYd2Vi -LmRldmVsb3BlcnMuZ29vZ2xlLmNuMCEGA1UdIAQaMBgwCAYGZ4EMAQIBMAwGCisG -AQQB1nkCBQMwPAYDVR0fBDUwMzAxoC+gLYYraHR0cDovL2NybHMucGtpLmdvb2cv -Z3RzMWMzL2ZWSnhiVi1LdG1rLmNybDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB1 -ADtTd3U+LbmAToswWwb+QDtn2E/D9Me9AA0tcm/h+tQXAAABjcCbl6wAAAQDAEYw -RAIgSsPqmrwU1+TmKxlq0lWFp8HhAWMNr8TpdCsZZ2hIZIQCIHHVd134qMevyD/v -xTWo8mVLjIJIbBUcO8Lm0alcvvkXAHYAdv+IPwq2+5VRwmHM9Ye6NLSkzbsp3GhC -Cp/mZ0xaOnQAAAGNwJuV/QAABAMARzBFAiA90a0s0Fw86i60HTH7XDtVIHnOE9sr -iosICjMRaNAzHgIhANdDshIUQaZkQM2lWPLTvmT3DKZqVMFnA5Aq425HslpPMA0G -CSqGSIb3DQEBCwUAA4IBAQBAjDmy7+UAyQqr2eYerPnwfAaSkD+3kcrbCW0Z656D -rejG3DE2yJ3q/Ao8OqZfg5hC/YpOaMvZMTwvdmLb6urWpgGdgEGVaWW+SHGrckVJ -scLqOVJ1ceXWMhMnp5QHtIhMHsBVwKPT+QM058b6oro2xamIACbAGC6eaem/TF0e -05holHHlBFPZk94PdLfB9f+nzobuWk6K+IaNihsCSgea5C31W1eWW9sE9Z9i3UXa -jbkTdgNtgRKT5HOoz4V+VPeR4uR/VBrQLrx1FsdICP6fCzG4lj1k9dJl3BLRk4xk -RJvLoyFyoRJMiqSpMcxNg7YTgK1ttUUj5BWT6rofaTxp +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= -----END CERTIFICATE----- diff --git a/testing/unit/tls/tls_module_test.py b/testing/unit/tls/tls_module_test.py index 2294f7659..98acb103f 100644 --- a/testing/unit/tls/tls_module_test.py +++ b/testing/unit/tls/tls_module_test.py @@ -25,6 +25,8 @@ import http.client import shutil import logging +import socket +import requests MODULE = 'tls' # Define the file paths @@ -40,6 +42,8 @@ LOCAL_REPORT_NO_CERT = os.path.join(REPORTS_DIR, 'tls_report_no_cert_local.md') CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json' +INTERNET_IFACE='eth0' + TLS_UTIL = None PACKET_CAPTURE = None @@ -264,7 +268,7 @@ def test_client_tls(self, capture_file = OUTPUT_DIR + '/client_tls.pcap' # Resolve the client ip used - client_ip = self.get_interface_ip('eth0') + client_ip = self.get_interface_ip(INTERNET_IFACE) # Genrate TLS outbound traffic if tls_generate is None: @@ -392,52 +396,56 @@ def make_tls_connection(self, port, tls_version, disable_valid_ciphers=False): - # Create the SSL context with the desired TLS version and options - context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - context.options |= ssl.PROTOCOL_TLS - - if disable_valid_ciphers: - # Create a list of ciphers that do not use ECDH or ECDSA - ciphers_str = [ - 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', - 'TLS_AES_128_GCM_SHA256', 'AES256-GCM-SHA384', - 'PSK-AES256-GCM-SHA384', 'PSK-CHACHA20-POLY1305', - 'RSA-PSK-AES128-GCM-SHA256', 'DHE-PSK-AES128-GCM-SHA256', - 'AES128-GCM-SHA256', 'PSK-AES128-GCM-SHA256', 'AES256-SHA256', - 'AES128-SHA' - ] - context.set_ciphers(':'.join(ciphers_str)) - - if tls_version != '1.1': - context.options |= ssl.OP_NO_TLSv1 # Disable TLS 1.0 - context.options |= ssl.OP_NO_TLSv1_1 # Disable TLS 1.1 - else: - context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 - context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 - - if tls_version == '1.3': - context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 - elif tls_version == '1.2': - context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 - - # Create the HTTPS connection with the SSL context - connection = http.client.HTTPSConnection(hostname, port, context=context) - - # Perform the TLS handshake manually try: - connection.connect() + # Create the SSL context with the desired TLS version and options + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + if disable_valid_ciphers: + # Create a list of ciphers that do not use ECDH or ECDSA + ciphers_str = [ + 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + 'TLS_AES_128_GCM_SHA256', 'AES256-GCM-SHA384', + 'PSK-AES256-GCM-SHA384', 'PSK-CHACHA20-POLY1305', + 'RSA-PSK-AES128-GCM-SHA256', 'DHE-PSK-AES128-GCM-SHA256', + 'AES128-GCM-SHA256', 'PSK-AES128-GCM-SHA256', 'AES256-SHA256', + 'AES128-SHA' + ] + context.set_ciphers(':'.join(ciphers_str)) + + # Disable specific TLS versions based on the input + if tls_version != '1.1': + context.options |= ssl.OP_NO_TLSv1 # Disable TLS 1.0 + context.options |= ssl.OP_NO_TLSv1_1 # Disable TLS 1.1 + else: + context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 + context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 + + if tls_version == '1.3': + context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 + elif tls_version == '1.2': + context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 + + # Create an SSL/TLS socket + with socket.create_connection((hostname, port), timeout=10) as sock: + with context.wrap_socket(sock, server_hostname=hostname) as secure_sock: + # Get the server's certificate in PEM format + cert_pem = ssl.DER_cert_to_PEM_cert(secure_sock.getpeercert(True)) + + except ConnectionRefusedError: + print(f'Connection to {hostname}:{port} was refused.') + except socket.gaierror: + print(f'Failed to resolve the hostname {hostname}.') except ssl.SSLError as e: - print('Failed to make connection: ' + str(e)) + print(f'SSL error occurred: {e}') + except socket.timeout: + print('Socket timeout error') - # At this point, the TLS handshake is complete. - # You can do any further processing or just close the connection. - connection.close() def start_capture(self, timeout): global PACKET_CAPTURE - PACKET_CAPTURE = sniff(iface='eth0', timeout=timeout) + PACKET_CAPTURE = sniff(iface=INTERNET_IFACE, timeout=timeout) def start_capture_thread(self, timeout): # Start the packet capture in a separate thread to avoid blocking. @@ -508,7 +516,7 @@ def tls_module_ca_cert_spaces_test(self): suite.addTest(TLSModuleTest('security_tls_v1_2_fail_server_test')) suite.addTest(TLSModuleTest('security_tls_v1_2_none_server_test')) - # TLS 1.3 server tests + # # TLS 1.3 server tests suite.addTest(TLSModuleTest('security_tls_v1_3_server_test')) # TLS client tests suite.addTest(TLSModuleTest('security_tls_v1_2_client_test')) @@ -521,10 +529,10 @@ def tls_module_ca_cert_spaces_test(self): # Test the results options for tls server tests suite.addTest(TLSModuleTest('security_tls_server_results_test')) - # Test various report module outputs - suite.addTest(TLSModuleTest('tls_module_report_test')) - suite.addTest(TLSModuleTest('tls_module_report_ext_test')) - suite.addTest(TLSModuleTest('tls_module_report_no_cert_test')) + # # Test various report module outputs + # suite.addTest(TLSModuleTest('tls_module_report_test')) + # suite.addTest(TLSModuleTest('tls_module_report_ext_test')) + # suite.addTest(TLSModuleTest('tls_module_report_no_cert_test')) # Test signature validation methods suite.addTest(TLSModuleTest('tls_module_trusted_ca_cert_chain_test')) diff --git a/testing/unit/unit_test.Dockerfile b/testing/unit/unit_test.Dockerfile index 3551684d0..3de2ec974 100644 --- a/testing/unit/unit_test.Dockerfile +++ b/testing/unit/unit_test.Dockerfile @@ -1,7 +1,7 @@ # Image name: test-run/ntp-test FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 -RUN apt-get update +RUN apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y # Set DEBIAN_FRONTEND to noninteractive mode ENV DEBIAN_FRONTEND=noninteractive @@ -35,6 +35,7 @@ RUN dos2unix /testrun/testing/unit/* # Make sure all the bin files are executable RUN chmod u+x /testrun/modules/test/tls/bin/* +RUN chmod u+x /testrun/testing/unit/run_tests.sh WORKDIR /testrun/testing/unit/ From 6024a5061e4fad6e271715d0bf0b3477727ae433 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Thu, 30 May 2024 11:27:28 -0600 Subject: [PATCH 3/3] Misc cleanup and pylint issues --- modules/test/tls/python/src/tls_util.py | 45 +++++++++++------- testing/unit/tls/tls_module_test.py | 61 ++++++++++++++----------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index 24ff40fcb..d8c1d7a16 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -38,7 +38,7 @@ ipaddress.ip_network('192.168.0.0/16') ] #Define the allowed protocols as tshark filters -DEFAULT_ALLOWED_PROTOCOLS=['quic'] +DEFAULT_ALLOWED_PROTOCOLS = ['quic'] class TLSUtil(): @@ -49,14 +49,15 @@ def __init__(self, bin_dir=DEFAULT_BIN_DIR, cert_out_dir=DEFAULT_CERTS_OUT_DIR, root_certs_dir=DEFAULT_ROOT_CERTS_DIR, - allowed_protocols=DEFAULT_ALLOWED_PROTOCOLS): + allowed_protocols=None): global LOGGER LOGGER = logger self._bin_dir = bin_dir self._cert_out_dir = cert_out_dir self._dev_cert_file = 'device_cert.crt' self._root_certs_dir = root_certs_dir - self._allowed_protocols = allowed_protocols + if allowed_protocols is None: + self._allowed_protocols = DEFAULT_ALLOWED_PROTOCOLS def get_public_certificate(self, host, @@ -456,14 +457,16 @@ def get_non_tls_packetes(self, client_ip, capture_files): return combined_packets # Resolve all connections from the device that use TLS - def get_tls_client_connection_packetes(self, client_ip, capture_files, protocol=None): + def get_tls_client_connection_packetes(self, + client_ip, + capture_files, + protocol=None): combined_packets = [] for capture_file in capture_files: bin_file = self._bin_dir + '/get_tls_client_connections.sh' args = f'"{capture_file}" {client_ip}' if protocol is not None: args += f' {protocol}' - LOGGER.info("conn pacets args: " + str(args)) command = f'{bin_file} {args}' response = util.run_command(command) packets = json.loads(response[0].strip()) @@ -511,10 +514,13 @@ def parse_packets(self, packets, capture_file): hello_packets.append(hello_packet) return hello_packets - def process_hello_packets(self, hello_packets, allowed_protocol_client_ips, tls_version='1.2'): + def process_hello_packets(self, + hello_packets, + allowed_protocol_client_ips, + tls_version='1.2'): # Validate the ciphers only for tls 1.2 client_hello_results = {'valid': [], 'invalid': []} - + if tls_version == '1.2': for packet in hello_packets: if packet['dst_ip'] not in str(client_hello_results['valid']): @@ -536,10 +542,12 @@ def process_hello_packets(self, hello_packets, allowed_protocol_client_ips, tls_ if packet['dst_ip'] not in str(client_hello_results['invalid']): client_hello_results['invalid'].append(packet) else: - LOGGER.info('Allowing protocol connection, cipher check failure ignored.') + LOGGER.info( + 'Allowing protocol connection, cipher check failure ignored.') protocol_name = allowed_protocol_client_ips[packet['dst_ip']] - packet['protocol_details'] = f'\nAllowing {protocol_name} traffic to {packet["dst_ip"]}' - client_hello_results['valid'].append(packet) + packet['protocol_details'] = ( + f'\nAllowing {protocol_name} traffic to {packet["dst_ip"]}') + client_hello_results['valid'].append(packet) else: # No cipher check for TLS 1.3 client_hello_results['valid'] = hello_packets @@ -626,12 +634,13 @@ def get_tls_client_connection_ips(self, client_ip, capture_files): # Check if the device has made any outbound connections that use any # allowed protocols that do not fit into a direct TLS packet inspection - def get_allowed_protocol_client_connection_ips(self, client_ip, capture_files): + def get_allowed_protocol_client_connection_ips(self, client_ip, + capture_files): LOGGER.info('Checking client for TLS Protocol client connections') tls_dst_ips = {} # Store unique destination IPs with the protocol name for protocol in self._allowed_protocols: packets = self.get_tls_client_connection_packetes( - client_ip=client_ip, capture_files=capture_files, protocol=protocol) + client_ip=client_ip, capture_files=capture_files, protocol=protocol) for packet in packets: dst_ip = ipaddress.ip_address(packet['_source']['layers']['ip.dst'][0]) @@ -653,11 +662,13 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): # Resolve allowed protocol connections that require # additional consideration beyond packet inspection - allowed_protocol_client_ips = self.get_allowed_protocol_client_connection_ips(client_ip,capture_files) + allowed_protocol_client_ips = ( + self.get_allowed_protocol_client_connection_ips(client_ip, + capture_files)) LOGGER.info(f'Protocol IPS: {allowed_protocol_client_ips}') - client_hello_results = self.process_hello_packets(hello_packets, allowed_protocol_client_ips, - tls_version) + client_hello_results = self.process_hello_packets( + hello_packets, allowed_protocol_client_ips, tls_version) handshakes = {'complete': [], 'incomplete': []} for packet in client_hello_results['valid']: @@ -699,7 +710,7 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): tls_client_details += 'Incomplete handshake detected from server: ' tls_client_details += result + '.' hello_result = client_hello_results[result] - if 'protocol_details' in hello_result: + if 'protocol_details' in hello_result: tls_client_details += hello_result['protocol_details'] tls_client_details += '\n' if len(handshakes['complete']) > 0: @@ -712,7 +723,7 @@ def validate_tls_client(self, client_ip, tls_version, capture_files): tls_client_details += result + '.' for packet in client_hello_results['valid']: if result in packet['dst_ip']: - if 'protocol_details' in packet: + if 'protocol_details' in packet: tls_client_details += packet['protocol_details'] tls_client_details += '\n' else: diff --git a/testing/unit/tls/tls_module_test.py b/testing/unit/tls/tls_module_test.py index 98acb103f..f51e4eea2 100644 --- a/testing/unit/tls/tls_module_test.py +++ b/testing/unit/tls/tls_module_test.py @@ -22,11 +22,9 @@ import time import netifaces import ssl -import http.client import shutil import logging import socket -import requests MODULE = 'tls' # Define the file paths @@ -42,7 +40,7 @@ LOCAL_REPORT_NO_CERT = os.path.join(REPORTS_DIR, 'tls_report_no_cert_local.md') CONF_FILE = 'modules/test/' + MODULE + '/conf/module_config.json' -INTERNET_IFACE='eth0' +INTERNET_IFACE = 'eth0' TLS_UTIL = None PACKET_CAPTURE = None @@ -352,6 +350,16 @@ def tls_module_report_ext_test(self): tls_capture_file=pcap_file) report_out_path = tls.generate_module_report() + # Read the generated report + with open(report_out_path, 'r', encoding='utf-8') as file: + report_out = file.read() + + # Read the local good report + with open(LOCAL_REPORT_EXT, 'r', encoding='utf-8') as file: + report_local = file.read() + + self.assertEqual(report_out, report_local) + def tls_module_report_no_cert_test(self): print('\ntls_module_report_no_cert_test') os.environ['DEVICE_MAC'] = '' @@ -403,35 +411,35 @@ def make_tls_connection(self, context.verify_mode = ssl.CERT_NONE if disable_valid_ciphers: - # Create a list of ciphers that do not use ECDH or ECDSA - ciphers_str = [ - 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', - 'TLS_AES_128_GCM_SHA256', 'AES256-GCM-SHA384', - 'PSK-AES256-GCM-SHA384', 'PSK-CHACHA20-POLY1305', - 'RSA-PSK-AES128-GCM-SHA256', 'DHE-PSK-AES128-GCM-SHA256', - 'AES128-GCM-SHA256', 'PSK-AES128-GCM-SHA256', 'AES256-SHA256', - 'AES128-SHA' - ] - context.set_ciphers(':'.join(ciphers_str)) + # Create a list of ciphers that do not use ECDH or ECDSA + ciphers_str = [ + 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', + 'TLS_AES_128_GCM_SHA256', 'AES256-GCM-SHA384', + 'PSK-AES256-GCM-SHA384', 'PSK-CHACHA20-POLY1305', + 'RSA-PSK-AES128-GCM-SHA256', 'DHE-PSK-AES128-GCM-SHA256', + 'AES128-GCM-SHA256', 'PSK-AES128-GCM-SHA256', 'AES256-SHA256', + 'AES128-SHA' + ] + context.set_ciphers(':'.join(ciphers_str)) # Disable specific TLS versions based on the input if tls_version != '1.1': - context.options |= ssl.OP_NO_TLSv1 # Disable TLS 1.0 - context.options |= ssl.OP_NO_TLSv1_1 # Disable TLS 1.1 + context.options |= ssl.OP_NO_TLSv1 # Disable TLS 1.0 + context.options |= ssl.OP_NO_TLSv1_1 # Disable TLS 1.1 else: - context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 - context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 + context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 + context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 if tls_version == '1.3': - context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 + context.options |= ssl.OP_NO_TLSv1_2 # Disable TLS 1.2 elif tls_version == '1.2': - context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 + context.options |= ssl.OP_NO_TLSv1_3 # Disable TLS 1.3 # Create an SSL/TLS socket with socket.create_connection((hostname, port), timeout=10) as sock: with context.wrap_socket(sock, server_hostname=hostname) as secure_sock: # Get the server's certificate in PEM format - cert_pem = ssl.DER_cert_to_PEM_cert(secure_sock.getpeercert(True)) + ssl.DER_cert_to_PEM_cert(secure_sock.getpeercert(True)) except ConnectionRefusedError: print(f'Connection to {hostname}:{port} was refused.') @@ -442,7 +450,6 @@ def make_tls_connection(self, except socket.timeout: print('Socket timeout error') - def start_capture(self, timeout): global PACKET_CAPTURE PACKET_CAPTURE = sniff(iface=INTERNET_IFACE, timeout=timeout) @@ -480,16 +487,16 @@ def tls_module_local_ca_cert_test(self): def tls_module_ca_cert_spaces_test(self): print('\tls_module_ca_cert_spaces_test') # Make a tmp folder to make a differnt CA directory - tmp_dir = os.path.join(TEST_FILES_DIR,'tmp') + tmp_dir = os.path.join(TEST_FILES_DIR, 'tmp') if os.path.exists(tmp_dir): shutil.rmtree(tmp_dir) os.makedirs(tmp_dir, exist_ok=True) # Move and rename the TestRun CA root with spaces - ca_file = os.path.join(ROOT_CERTS_DIR,'Testrun_CA_Root.crt') - ca_file_with_spaces = os.path.join(tmp_dir,'Testrun CA Root.crt') - shutil.copy(ca_file,ca_file_with_spaces) + ca_file = os.path.join(ROOT_CERTS_DIR, 'Testrun_CA_Root.crt') + ca_file_with_spaces = os.path.join(tmp_dir, 'Testrun CA Root.crt') + shutil.copy(ca_file, ca_file_with_spaces) - cert_path = os.path.join(CERT_DIR,'device_cert_local.crt') + cert_path = os.path.join(CERT_DIR, 'device_cert_local.crt') log = logger.get_logger('unit_test_' + MODULE) log.setLevel(logging.DEBUG) tls_util = TLSUtil(log, @@ -498,7 +505,7 @@ def tls_module_ca_cert_spaces_test(self): root_certs_dir=tmp_dir) cert_valid = tls_util.validate_local_ca_signature( - device_cert_path=cert_path) + device_cert_path=cert_path) self.assertEqual(cert_valid[0], True)