diff --git a/modules/test/tls/bin/get_tls_client_connections.sh b/modules/test/tls/bin/get_tls_client_connections.sh index f036c2154..e2e6da91b 100755 --- a/modules/test/tls/bin/get_tls_client_connections.sh +++ b/modules/test/tls/bin/get_tls_client_connections.sh @@ -1,26 +1,32 @@ -#!/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 + +response=$(tshark -r "$CAPTURE_FILE" $TSHARK_OUTPUT $TSHARK_FILTER) + +echo "$response" \ No newline at end of file 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 368108bdd..d8c1d7a16 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,16 @@ 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=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 + if allowed_protocols is None: + self._allowed_protocols = DEFAULT_ALLOWED_PROTOCOLS def get_public_certificate(self, host, @@ -452,11 +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): + 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}' command = f'{bin_file} {args}' response = util.run_command(command) packets = json.loads(response[0].strip()) @@ -504,9 +514,13 @@ 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 +538,16 @@ 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 +632,22 @@ 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,8 +659,16 @@ 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, - tls_version) + + # 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': []} for packet in client_hello_results['valid']: @@ -662,7 +708,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 +720,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 +737,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/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/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 000000000..728e9877c Binary files /dev/null and b/testing/unit/tls/captures/monitor_with_quic.pcap differ 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 e8c4afec4..f51e4eea2 100644 --- a/testing/unit/tls/tls_module_test.py +++ b/testing/unit/tls/tls_module_test.py @@ -22,9 +22,9 @@ import time import netifaces import ssl -import http.client import shutil import logging +import socket MODULE = 'tls' # Define the file paths @@ -40,6 +40,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 +266,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: @@ -300,6 +302,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' @@ -335,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'] = '' @@ -379,52 +404,55 @@ 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 + 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)) - - # At this point, the TLS handshake is complete. - # You can do any further processing or just close the connection. - connection.close() + print(f'SSL error occurred: {e}') + except socket.timeout: + print('Socket timeout error') 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. @@ -459,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, @@ -477,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) @@ -495,7 +523,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')) @@ -508,15 +536,17 @@ 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')) 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..3de2ec974 --- /dev/null +++ b/testing/unit/unit_test.Dockerfile @@ -0,0 +1,42 @@ +# Image name: test-run/ntp-test +FROM ubuntu@sha256:e6173d4dc55e76b87c4af8db8821b1feae4146dd47341e4d431118c7dd060a74 + +RUN apt-get update && apt-get upgrade -y && apt-get dist-upgrade -y + +# 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/* +RUN chmod u+x /testrun/testing/unit/run_tests.sh + +WORKDIR /testrun/testing/unit/ + +#ENTRYPOINT [ "./run_tests.sh" ] \ No newline at end of file