Skip to content

Commit c3c50cd

Browse files
authored
Added integration test for host verification (#355)
* update ssl context config * clean up comments * add integration test for host SAN check * Revert "update ssl context config" This reverts commit e91526a. * update the test handle websocket connection * fix test * Reapply "update ssl context config" This reverts commit e503233. * update test to catch ssl error * set keep alive sec to 60
1 parent 604b999 commit c3c50cd

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# This integration test verifies that TLS hostname verification is enforced
2+
# when connecting to AWS IoT Core.
3+
#
4+
# It tests two scenarios:
5+
# 1. A connection to the correct endpoint hostname succeeds (hostname matches certificate).
6+
# 2. A connection using the endpoint's IP address (hostname mismatch) fails with an SSL error
7+
# because the certificate's CN/SAN contains the DNS name but not the IP address.
8+
#
9+
# Without the hostname verification fix, scenario 2 would have succeeded because
10+
# only the CA chain was validated, not the hostname. With the fix, it correctly rejects.
11+
12+
13+
import random
14+
import string
15+
import time
16+
import ssl
17+
import socket
18+
import sys
19+
sys.path.insert(0, "./test-integration/IntegrationTests/TestToolLibrary")
20+
sys.path.insert(0, "./test-integration/IntegrationTests/TestToolLibrary/SDKPackage")
21+
22+
import TestToolLibrary.checkInManager as checkInManager
23+
import TestToolLibrary.MQTTClientManager as MQTTClientManager
24+
from TestToolLibrary.skip import skip_when_match
25+
from TestToolLibrary.skip import ModeIsALPN
26+
from TestToolLibrary.skip import Python2VersionLowerThan
27+
from TestToolLibrary.skip import Python3VersionLowerThan
28+
29+
30+
CLIENT_ID = "integrationTestHostnameVerification_" + "".join(random.choice(string.ascii_lowercase) for i in range(4))
31+
32+
33+
############################################################################
34+
# Main #
35+
# Check inputs
36+
myCheckInManager = checkInManager.checkInManager(2)
37+
myCheckInManager.verify(sys.argv)
38+
39+
host = myCheckInManager.host
40+
rootCA = "./test-integration/Credentials/rootCA.crt"
41+
certificate = "./test-integration/Credentials/certificate.pem.crt"
42+
privateKey = "./test-integration/Credentials/privateKey.pem.key"
43+
mode = myCheckInManager.mode
44+
45+
############################################################################
46+
# Test 1: Connection to correct hostname should SUCCEED
47+
############################################################################
48+
print("=" * 60)
49+
print("Test 1: Connection to correct hostname should SUCCEED")
50+
print("=" * 60)
51+
52+
myMQTTClientManager = MQTTClientManager.MQTTClientManager()
53+
client = myMQTTClientManager.create_connected_mqtt_core(CLIENT_ID, host, rootCA, certificate, privateKey, mode=mode)
54+
55+
if client is None:
56+
print("FAILED: Could not connect to correct hostname: " + host)
57+
exit(4)
58+
59+
print("PASSED: Successfully connected to: " + host)
60+
client.disconnect()
61+
time.sleep(1)
62+
63+
############################################################################
64+
# Test 2: Connection using IP address (hostname mismatch) should FAIL
65+
############################################################################
66+
print("")
67+
print("=" * 60)
68+
print("Test 2: Connection using IP address (hostname mismatch) should FAIL")
69+
print("=" * 60)
70+
71+
# Resolve the real endpoint to its IP address
72+
try:
73+
real_ip = socket.gethostbyname(host)
74+
print("Resolved " + host + " to IP: " + real_ip)
75+
except socket.gaierror:
76+
print("SKIPPED: Could not resolve hostname.")
77+
exit(0)
78+
79+
# Create an AWSIoTMQTTClient using the IP address as the endpoint.
80+
# The server's certificate SAN has the DNS name (e.g., *.iot.us-east-1.amazonaws.com)
81+
# but NOT the IP address.
82+
# With hostname verification (our fix): TLS rejects because IP is not in certificate SAN.
83+
# Without hostname verification (old bug): TLS accepts because only CA chain is checked.
84+
mismatchClient = myMQTTClientManager.create_nonconnected_mqtt_core(CLIENT_ID, real_ip, rootCA, certificate, privateKey, mode=mode)
85+
86+
connection_failed = False
87+
try:
88+
mismatchClient.connect(60)
89+
print("FAILED: Connection using IP address should have been rejected by hostname verification!")
90+
mismatchClient.disconnect()
91+
exit(4)
92+
except ssl.SSLCertVerificationError as e:
93+
print("PASSED: Connection correctly rejected with SSLCertVerificationError: " + str(e))
94+
connection_failed = True
95+
except ssl.SSLError as e:
96+
print("PASSED: Connection correctly rejected with SSLError: " + str(e))
97+
connection_failed = True
98+
except socket.error as e:
99+
if "SSL" in str(e) or "certificate" in str(e).lower() or "hostname" in str(e).lower():
100+
print("PASSED: Connection correctly rejected with socket error: " + str(e))
101+
connection_failed = True
102+
else:
103+
print("FAILED: Unexpected socket error (not SSL-related): " + str(e))
104+
exit(4)
105+
except Exception as e:
106+
# The SDK wraps SSL errors in its own exception types
107+
error_msg = str(e).lower()
108+
if "ssl" in error_msg or "certificate" in error_msg or "hostname" in error_msg or "tls" in error_msg:
109+
print("PASSED: Connection correctly rejected: " + str(type(e).__name__) + ": " + str(e))
110+
connection_failed = True
111+
else:
112+
print("FAILED: Unexpected exception: " + str(type(e).__name__) + ": " + str(e))
113+
exit(4)
114+
115+
if not connection_failed:
116+
print("FAILED: Connection using IP address was not rejected by hostname verification!")
117+
exit(4)
118+
119+
print("")
120+
print("=" * 60)
121+
print("ALL TESTS PASSED: Hostname verification is working correctly.")
122+
print("=" * 60)

test-integration/run/run.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ else
139139
"IntegrationTestClientReusability.py") Scale=""
140140
;;
141141
"IntegrationTestJobsClient.py") Scale=""
142+
;;
143+
"IntegrationTestHostnameVerification.py") Scale=""
142144
esac
143145

144146
python ${TEST_DIR}${file} ${TestMode} ${TestHost} ${Scale}

0 commit comments

Comments
 (0)