Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/test/compliant.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,9 @@ Strict cryptographic standards apply to all inbound and outbound traffic via web
### Inbound Connections (Device as a Server / Web Interface)
* The device's embedded web server must support connections via **TLS 1.2** and/or **TLS 1.3**.
* **TLS 1.0 and TLS 1.1 must be explicitly disabled** within the web server configuration.
* The TLS certificate presented by the web server must be valid and signed by a trusted (or test-environment designated) Certificate Authority (CA).
* The TLS certificate presented by the web server must be valid and signed by a trusted (or test-environment designated) Certificate Authority (CA).

### HTTP Server Compliance (RFC 9110)
* Basic Method Support: Verification that the server successfully processes standard **GET** and **HEAD** methods, returning a **200 OK** status.
* **HEAD** Response Semantics: Ensuring that the response body for a **HEAD** request is strictly empty, and critical headers **(Content-Length, Content-Type)** precisely match the **GET** response.
* Unsupported Method Handling: Verification of correct server response to arbitrary or invalid methods, expecting a status code of **405 Method Not Allowed or 501 Not Implemented**.
1 change: 1 addition & 0 deletions modules/test/tls/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Within the ```python/src``` directory, the below tests are executed.
| security.tls.v1_2_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS v1.2 and support for ECDH and ECDSA ciphers | Required if Applicable |
| security.tls.v1_3_server | Check the device web server TLS 1.3 and the certificate is valid | TLS 1.3 certificate is issued to the web browser client when accessed | Informational |
| security.tls.v1_3_client | Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS) | The packet indicates a TLS connection with at least TLS 1.3 using AEAD-based ciphers | Informational |
| security.http.server | Automated RFC 9110 compliance validation of the device's HTTP/HTTPS server | The server responds with status 200 OK for both GET and HEAD methods with an empty HEAD body and matching critical headers, returns 405 or 501 for unsupported methods | Informational |

11 changes: 11 additions & 0 deletions modules/test/tls/conf/module_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@
"Disable connections to unsecure services",
"Ensure any URLs connected to are secure (https)"
]
},
{
"name": "security.http.server",
"test_description": "Automated RFC 9110 compliance validation of the device's HTTP/HTTPS server",
"expected_behavior": "The server responds with status 200 OK for both GET and HEAD methods with an empty HEAD body and matching critical headers, returns 405 or 501 for unsupported methods",
"recommendations": [
"Implement the GET and HEAD methods on the web server",
"Ensure that the HEAD response is empty",
"Ensure that the GET and HEAD response headers (Content-Length, Content-Type) match",
"Ensure that unsupported requests return a 405 or 501 status code"
]
}
]
}
Expand Down
133 changes: 133 additions & 0 deletions modules/test/tls/python/src/http_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
LOGGER = None
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

_MSG_HTTP_NOT_DETECTED = 'No HTTP server detected on the device.'
_MSG_HTTP_COMPLIANT = 'Device HTTP server is COMPLIANT with the RFC 9110'
_MSG_HTTP_NON = 'Device HTTP server is NON-COMPLIANT with the RFC 9110'
_MSG_HTTP_ERROR = 'Error checking HTTP server.'


class HTTPScan():
"""Helper class to scan for all HTTP/HTTPS services for a device"""
Expand Down Expand Up @@ -128,3 +133,131 @@ def scan_for_http_services(self, ip_address):
for port, service_type in results.items():
LOGGER.info(f'Port {port}: {service_type}')
return results

def _http_make_request(
self, method: str,
url: str,
timeout: int = 20
) -> requests.Response | None:
"""Sends HTTP request and handles exceptions uniformly.

Returns:
requests.Response | None
"""

try:
resp = requests.request(
method=method,
url=url,
timeout=timeout,
verify=False # nosec B501
)
return resp
except requests.exceptions.Timeout as e:
msg = f'Timeout during {method.upper()} request to {url}'
LOGGER.error(f'{msg}: {e}')
return None
except requests.exceptions.ConnectionError as e:
msg = f'Connection refused during {method.upper()} request to {url}'
LOGGER.error(f'{msg}: {e}')
return None
Comment thread
hitnik marked this conversation as resolved.
Resolved

def http_server_compliance(self, ip:str) -> tuple[str, str, list[str]]:
"""Checks HTTP server comliance to RFC 9110."""
result_state = 'Feature Not Detected'
result_message = _MSG_HTTP_NOT_DETECTED
result_details = []
try:
ports = self.scan_all_ports(ip)
if not ports:
msg = f'No HTTP/HTTPS ports found on {ip}'
LOGGER.info(msg)
return result_state, result_message, result_details
if 443 in ports:
url = f'https://{ip}'
else:
url = f'http://{ip}'
LOGGER.info(f'Checking HTTP methods on {url}')
try:
get_resp = self._http_make_request('GET', url)
if get_resp is None:
get_ok = False
else:
get_ok = get_resp.status_code == 200
head_resp = self._http_make_request('HEAD', url)
if head_resp is None:
head_ok = False
else:
head_ok = head_resp.status_code == 200
method_resp = self._http_make_request('foobar', url)
except requests.exceptions.RequestException as e:
LOGGER.error(f'Error checking HTTP methods on {url}: {e}')
return 'Error', _MSG_HTTP_ERROR, []
if not get_ok or not head_ok:
LOGGER.info(f'HTTP GET/HEAD method is not supported on {url}')
result_state = 'Non-Compliant'
result_message = _MSG_HTTP_NON
if not get_ok:
result_details.append('Server does not support GET method')
else:
result_details.append('Server supports GET method')
if not head_ok:
result_details.append('Server does not support HEAD method')
else:
result_details.append('Server supports HEAD method')
elif get_ok and head_ok:
LOGGER.info(f'HTTP GET and HEAD methods are supported on {url}')
result_state = 'Compliant'
result_message = _MSG_HTTP_COMPLIANT
result_details.extend([
'Server supports GET method',
'Server supports HEAD method'
])
if head_resp is not None and get_resp is not None:
head_body_len = len(head_resp.content)
head_body_empty = head_body_len == 0
headers_match = True
header_not_match = ''
for header in ('Content-Length', 'Content-Type'):
if header in get_resp.headers or header in head_resp.headers:
if get_resp.headers.get(header) != head_resp.headers.get(header):
headers_match = False
header_not_match = header
break
if not head_body_empty or not headers_match:
result_state = 'Non-Compliant'
result_message = _MSG_HTTP_NON
if not head_body_empty:
LOGGER.info('HEAD response body is not empty')
result_details.append('HEAD response body is not empty')
elif not headers_match:
msg = f'{header_not_match} does not match between GET and HEAD'
LOGGER.info(msg)
result_details.append(f'Header {header_not_match} does not match')
Comment thread
hitnik marked this conversation as resolved.
Resolved
else:
LOGGER.info('HEAD response body is empty.')
LOGGER.info('Headers match between GET and HEAD methods.')
result_details.extend([
'HEAD response body is empty',
'Headers match between GET and HEAD methods'
])
if method_resp is None:
result_state = 'Non-Compliant'
result_message = _MSG_HTTP_NON
msg = 'Device server does not return 501 or 405 for unsupported methods'
LOGGER.info(msg)
result_details.append(msg)
elif method_resp.status_code in (405, 501):
msg = 'Device server returns 501 or 405 for unsupported methods'
LOGGER.info(msg)
result_details.append(msg)
else:
result_state = 'Non-Compliant'
result_message = _MSG_HTTP_NON
msg = 'Device server does not return 501 or 405 for unsupported methods'
LOGGER.info(msg)
result_details.append(msg)
except Exception as e:
LOGGER.error(f'Unexpected error {e}')

return result_state, result_message, result_details
6 changes: 6 additions & 0 deletions modules/test/tls/python/src/tls_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,12 @@ def _security_tls_v1_3_client(self):
'1.3',
unsupported_versions=['1.0', '1.1'])

def _security_http_server(self):
LOGGER.info('Running HTTP server test')
return self._http_scan.http_server_compliance(
self._device_ipv4_addr
)

def _validate_tls_client(self,
client_mac,
tls_version,
Expand Down
4 changes: 4 additions & 0 deletions resources/test_packs/pilot/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
{
"name": "security.tls.v1_3_client",
"required_result": "Informational"
},
{
"name": "security.http.server",
"required_result": "Informational"
}
]
}
4 changes: 4 additions & 0 deletions resources/test_packs/qualification/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
{
"name": "security.tls.v1_3_client",
"required_result": "Informational"
},
{
"name": "security.http.server",
"required_result": "Informational"
}
]
}
Loading
Loading