-
Notifications
You must be signed in to change notification settings - Fork 137
Add support for client authentication #286
Changes from all commits
c35efea
324054b
0f516cf
99f5eb9
03866d5
8eb43b8
c89e956
05b3083
cf81846
652d6a2
95aa0f5
333d23e
a8b66d1
d6e1970
133a5fc
80d27f1
aeb980d
10458ca
07be179
050e26b
860392e
15bd499
4240b17
369f91b
c676313
af596de
cf8a291
0848d11
e513aa7
b812928
2019622
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ | |
# Measure a site's HTTP behavior using DHS NCATS' pshtt tool. | ||
|
||
# Network timeout for each internal pshtt HTTP request. | ||
pshtt_timeout = 20 | ||
pshtt_timeout = 5 | ||
|
||
# Default to a custom user agent that can be overridden via an environment | ||
# variable | ||
|
@@ -92,7 +92,9 @@ def scan(domain, environment, options): | |
{ | ||
'timeout': pshtt_timeout, | ||
'user_agent': user_agent, | ||
'debug': options.get("debug", False) | ||
'debug': options.get("debug", False), | ||
'ca_file': options.get("ca_file"), | ||
'pt_int_ca_file': options.get("pt_int_ca_file") | ||
} | ||
) | ||
|
||
|
@@ -108,26 +110,26 @@ def to_rows(data): | |
row = [] | ||
for field in headers: | ||
value = data[field] | ||
|
||
# TODO: Fix this upstream | ||
if (field != "HSTS Header") and (field != "HSTS Max Age") and (field != "Redirect To"): | ||
if value is None: | ||
value = False | ||
|
||
row.append(value) | ||
|
||
return [row] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really intend to return a list of the row list? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @konklone did this intentionally. This is what all the scanners (are supposed to) do. If we change it here we need to change it in all scanners, and we also need to change the code that gathers all the CSV rows into a single results file. All doable, but I think that is best done in a separate PR. |
||
|
||
|
||
headers = [ | ||
"Canonical URL", "Live", "Redirect", "Redirect To", | ||
"Canonical URL", "Live", | ||
"Redirect", "Redirect To", | ||
jsf9k marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"Valid HTTPS", "Defaults to HTTPS", "Downgrades HTTPS", | ||
"Strictly Forces HTTPS", "HTTPS Bad Chain", "HTTPS Bad Hostname", | ||
"HTTPS Expired Cert", "HTTPS Self Signed Cert", | ||
"HSTS", "HSTS Header", "HSTS Max Age", "HSTS Entire Domain", | ||
"HSTS Preload Ready", "HSTS Preload Pending", "HSTS Preloaded", | ||
"Base Domain HSTS Preloaded", "Domain Supports HTTPS", | ||
"Domain Enforces HTTPS", "Domain Uses Strong HSTS", "Unknown Error", | ||
"Domain Enforces HTTPS", "Domain Uses Strong HSTS", | ||
"HTTPS Live", "HTTPS Full Connection", "HTTPS Client Auth Required", | ||
"HTTPS Publicly Trusted", "HTTPS Custom Truststore Trusted", | ||
"IP", "Server Header", "Server Version", "HTTPS Cert Chain Length", | ||
"HTTPS Probably Missing Intermediate Cert", "Notes", | ||
"Unknown Error", | ||
] | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,13 +13,15 @@ | |
### | ||
|
||
import logging | ||
import datetime | ||
from typing import Any | ||
|
||
from sslyze.server_connectivity_tester import ServerConnectivityTester, ServerConnectivityError | ||
from sslyze.synchronous_scanner import SynchronousScanner | ||
from sslyze.concurrent_scanner import ConcurrentScanner, PluginRaisedExceptionScanResult | ||
from sslyze.plugins.openssl_cipher_suites_plugin import Tlsv10ScanCommand, Tlsv11ScanCommand, Tlsv12ScanCommand, Tlsv13ScanCommand, Sslv20ScanCommand, Sslv30ScanCommand | ||
from sslyze.plugins.certificate_info_plugin import CertificateInfoScanCommand | ||
from sslyze.plugins.session_renegotiation_plugin import SessionRenegotiationScanCommand | ||
from sslyze.ssl_settings import TlsWrappedProtocolEnum | ||
|
||
import idna | ||
|
@@ -37,6 +39,10 @@ | |
# Advertise Lambda support | ||
lambda_support = True | ||
|
||
# File with custom root and intermediate certs that should be trusted | ||
# for verifying the cert chain | ||
CA_FILE = None | ||
|
||
|
||
# If we have pshtt data, use it to skip some domains, and to adjust | ||
# scan hostnames to canonical URLs where we can. | ||
|
@@ -225,6 +231,19 @@ def to_rows(data): | |
|
||
row['certs'].get('is_symantec_cert'), | ||
row['certs'].get('symantec_distrust_date'), | ||
|
||
row['config'].get('any_export'), | ||
row['config'].get('any_NULL'), | ||
row['config'].get('any_MD5'), | ||
row['config'].get('any_less_than_128_bits'), | ||
|
||
row['config'].get('insecure_renegotiation'), | ||
|
||
row['certs'].get('certificate_less_than_2048'), | ||
row['certs'].get('md5_signed_certificate'), | ||
row['certs'].get('sha1_signed_certificate'), | ||
row['certs'].get('expired_certificate'), | ||
|
||
str.join(', ', row.get('ciphers', [])), | ||
|
||
row.get('errors') | ||
|
@@ -254,6 +273,13 @@ def to_rows(data): | |
"EV Trusted OIDs", "EV Trusted Browsers", | ||
|
||
"Is Symantec Cert", "Symantec Distrust Date", | ||
|
||
"Any Export", "Any NULL", "Any MD5", "Any Less Than 128 Bits", | ||
"Insecure Renegotiation", | ||
"Certificate Less Than 2048", | ||
"MD5 Signed Certificate", "SHA-1 Signed Certificate", | ||
"Expired Certificate", | ||
|
||
"Accepted Ciphers", | ||
|
||
"Errors" | ||
|
@@ -289,9 +315,9 @@ def run_sslyze(data, environment, options): | |
|
||
# Whether sync or concurrent, get responses for all scans. | ||
if sync: | ||
sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs = scan_serial(scanner, server_info, data, options) | ||
sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs, reneg = scan_serial(scanner, server_info, data, options) | ||
else: | ||
sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs = scan_parallel(scanner, server_info, data, options) | ||
sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs, reneg = scan_parallel(scanner, server_info, data, options) | ||
|
||
# Only analyze protocols if all the scanners functioned. | ||
# Very difficult to draw conclusions if some worked and some did not. | ||
|
@@ -301,6 +327,9 @@ def run_sslyze(data, environment, options): | |
if certs: | ||
data['certs'] = analyze_certs(certs) | ||
|
||
if reneg: | ||
analyze_reneg(data, reneg) | ||
|
||
return data | ||
|
||
|
||
|
@@ -333,6 +362,10 @@ def analyze_protocols_and_ciphers(data, sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, t | |
all_rc4 = True | ||
all_dhe = True | ||
any_3des = False | ||
any_export = False | ||
any_NULL = False | ||
any_MD5 = False | ||
any_less_than_128_bits = False | ||
|
||
for cipher in accepted_ciphers: | ||
name = cipher.openssl_name | ||
|
@@ -349,11 +382,29 @@ def analyze_protocols_and_ciphers(data, sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, t | |
else: | ||
all_dhe = False | ||
|
||
if ("EXPORT" in name): | ||
any_export = True | ||
|
||
if ("NULL" in name): | ||
any_NULL = True | ||
|
||
if ("MD5" in name): | ||
any_MD5 = True | ||
|
||
parts = name.split('_') | ||
for p in parts: | ||
if (p.isdigit()) and (int(p) < 128): | ||
any_less_than_128_bits = True | ||
|
||
data['config']['any_rc4'] = any_rc4 | ||
data['config']['all_rc4'] = all_rc4 | ||
data['config']['any_dhe'] = any_dhe | ||
data['config']['all_dhe'] = all_dhe | ||
data['config']['any_3des'] = any_3des | ||
data['config']['any_export'] = any_export | ||
data['config']['any_NULL'] = any_NULL | ||
data['config']['any_MD5'] = any_MD5 | ||
data['config']['any_less_than_128_bits'] = any_less_than_128_bits | ||
|
||
|
||
def analyze_certs(certs): | ||
|
@@ -391,6 +442,11 @@ def analyze_certs(certs): | |
else: | ||
data['certs']['key_length'] = None | ||
|
||
if(data['certs']['key_length'] < 2048): | ||
data['certs']['certificate_less_than_2048'] = True | ||
else: | ||
data['certs']['certificate_less_than_2048'] = False | ||
|
||
if isinstance(leaf_key, rsa.RSAPublicKey): | ||
leaf_key_type = "RSA" | ||
elif isinstance(leaf_key, dsa.DSAPublicKey): | ||
|
@@ -405,10 +461,26 @@ def analyze_certs(certs): | |
# Signature of the leaf certificate only. | ||
data['certs']['leaf_signature'] = leaf.signature_hash_algorithm.name | ||
|
||
if(leaf.signature_hash_algorithm.name == "MD5"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two blocks could be simplified to: sig_alg = leaf.signature_hash_algorithm.name
data['certs']['md5_signed_certificate'] = (sig_alg == "MD5")
data['certs']['sha1_signed_certificate'] = (sig_alg == "SHA1") There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that is better. |
||
data['certs']['md5_signed_certificate'] = True | ||
else: | ||
data['certs']['md5_signed_certificate'] = False | ||
|
||
if(leaf.signature_hash_algorithm.name == "SHA1"): | ||
data['certs']['sha1_signed_certificate'] = True | ||
else: | ||
data['certs']['sha1_signed_certificate'] = False | ||
|
||
# Beginning and expiration dates of the leaf certificate | ||
data['certs']['not_before'] = leaf.not_valid_before | ||
data['certs']['not_after'] = leaf.not_valid_after | ||
|
||
now = datetime.datetime.now() | ||
if (now < leaf.not_valid_before) or (now > leaf.not_valid_after): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be simplified to:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
data['certs']['expired_certificate'] = True | ||
else: | ||
data['certs']['expired_certificate'] = False | ||
|
||
any_sha1_served = False | ||
for cert in served_chain: | ||
if parse_cert(cert).signature_hash_algorithm.name == "sha1": | ||
|
@@ -497,16 +569,26 @@ def cert_issuer_name(parsed): | |
return attrs[0].value | ||
|
||
|
||
# Analyze the results of a renegotiation test | ||
def analyze_reneg(data, reneg): | ||
if (reneg.accepts_client_renegotiation is True) and (reneg.supports_secure_renegotiation is False): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could simply be:
And then further, the whole block could be:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
data['config']['insecure_renegotiation'] = True | ||
else: | ||
data['config']['insecure_renegotiation'] = False | ||
|
||
|
||
# Given CipherSuiteScanResult, whether the protocol is supported | ||
def supported_protocol(result): | ||
return (len(result.accepted_cipher_list) > 0) | ||
|
||
|
||
# SSlyze initialization boilerplate | ||
def init_sslyze(hostname, port, starttls_smtp, options, sync=False): | ||
global network_timeout | ||
global network_timeout, CA_FILE | ||
|
||
network_timeout = int(options.get("network_timeout", network_timeout)) | ||
if options.get('ca_file'): | ||
CA_FILE = options['ca_file'] | ||
|
||
tls_wrapped_protocol = TlsWrappedProtocolEnum.PLAIN_TLS | ||
if starttls_smtp: | ||
|
@@ -535,38 +617,53 @@ def init_sslyze(hostname, port, starttls_smtp, options, sync=False): | |
# Run each scan in-process, one at a time. | ||
# Takes longer, but no multi-process funny business. | ||
def scan_serial(scanner, server_info, data, options): | ||
errors = 0 | ||
|
||
def run_scan(scan_type, command, errors): | ||
if(errors >= 2): | ||
return None, errors | ||
logging.debug("\t\t{} scan.".format(scan_type)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I think with the logging calls, it might actually be preferred to use formatting similar to:
Which matches more what is done in the documentation, where the parameters ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not at all opposed to this change, but I think we should make it wholesale, across all scanners, in a separate pull request. |
||
result = None | ||
try: | ||
result = scanner.run_scan_command(server_info, command) | ||
except Exception as err: | ||
logging.warning("{}: Error during {} scan.".format(server_info.hostname, scan_type)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be |
||
logging.debug("{}: Exception during {} scan: {}".format(server_info.hostname, scan_type, err)) | ||
errors = errors + 1 | ||
return result, errors | ||
|
||
logging.debug("\tRunning scans in serial.") | ||
logging.debug("\t\tSSLv2 scan.") | ||
sslv2 = scanner.run_scan_command(server_info, Sslv20ScanCommand()) | ||
logging.debug("\t\tSSLv3 scan.") | ||
sslv3 = scanner.run_scan_command(server_info, Sslv30ScanCommand()) | ||
logging.debug("\t\tTLSv1.0 scan.") | ||
tlsv1 = scanner.run_scan_command(server_info, Tlsv10ScanCommand()) | ||
logging.debug("\t\tTLSv1.1 scan.") | ||
tlsv1_1 = scanner.run_scan_command(server_info, Tlsv11ScanCommand()) | ||
logging.debug("\t\tTLSv1.2 scan.") | ||
tlsv1_2 = scanner.run_scan_command(server_info, Tlsv12ScanCommand()) | ||
logging.debug("\t\tTLSv1.3 scan.") | ||
tlsv1_3 = scanner.run_scan_command(server_info, Tlsv13ScanCommand()) | ||
sslv2, errors = run_scan("SSLv2", Sslv20ScanCommand(), errors) | ||
sslv3, errors = run_scan("SSLv3", Sslv30ScanCommand(), errors) | ||
tlsv1, errors = run_scan("TLSv1.0", Tlsv10ScanCommand(), errors) | ||
tlsv1_1, errors = run_scan("TLSv1.1", Tlsv11ScanCommand(), errors) | ||
tlsv1_2, errors = run_scan("TLSv1.2", Tlsv12ScanCommand(), errors) | ||
tlsv1_3, errors = run_scan("TLSv1.3", Tlsv13ScanCommand(), errors) | ||
|
||
certs = None | ||
if options.get("sslyze_certs", True) is True: | ||
|
||
if errors < 2 and options.get("sslyze_certs", True) is True: | ||
try: | ||
logging.debug("\t\tCertificate information scan.") | ||
certs = scanner.run_scan_command(server_info, CertificateInfoScanCommand()) | ||
# Let generic exceptions bubble up. | ||
certs = scanner.run_scan_command(server_info, CertificateInfoScanCommand(ca_file=CA_FILE)) | ||
except idna.core.InvalidCodepoint: | ||
logging.warning(utils.format_last_exception()) | ||
data['errors'].append("Invalid certificate/OCSP for this domain.") | ||
certs = None | ||
except Exception as err: | ||
logging.warning("{}: Error during certificate information scan.".format(server_info.hostname)) | ||
logging.debug("{}: Exception during certificate information scan: {}".format(server_info.hostname, err)) | ||
else: | ||
certs = None | ||
|
||
reneg = None | ||
if options.get("sslyze_reneg", True) is True: | ||
reneg, errors = run_scan("Renegotiation", SessionRenegotiationScanCommand(), errors) | ||
else: | ||
reneg = None | ||
|
||
logging.debug("\tDone scanning.") | ||
|
||
return sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs | ||
return sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs, reneg | ||
|
||
|
||
# Run each scan in parallel, using multi-processing. | ||
|
@@ -589,7 +686,7 @@ def queue(command): | |
return None, None, None, None, None, None, None | ||
|
||
# Initialize commands and result containers | ||
sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs = None, None, None, None, None, None | ||
sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs, reneg = None, None, None, None, None, None, None, None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be simplified to:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
# Queue them all up | ||
queue(Sslv20ScanCommand()) | ||
|
@@ -602,6 +699,9 @@ def queue(command): | |
if options.get("sslyze-certs", True) is True: | ||
queue(CertificateInfoScanCommand()) | ||
|
||
if options.get("sslyze-reneg", True) is True: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should just be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🦅 👁️ |
||
queue(CertificateInfoScanCommand()) | ||
|
||
# Reassign them back to predictable places after they're all done | ||
was_error = False | ||
for result in scanner.get_results(): | ||
|
@@ -626,6 +726,8 @@ def queue(command): | |
tlsv1_3 = result | ||
elif type(result.scan_command) == CertificateInfoScanCommand: | ||
certs = result | ||
elif type(result.scan_command) == SessionRenegotiationScanCommand: | ||
reneg = result | ||
else: | ||
error = "Couldn't match scan result with command! %s" % result | ||
logging.warning("\t%s" % error) | ||
|
@@ -640,11 +742,11 @@ def queue(command): | |
|
||
# There was an error during async processing. | ||
if was_error: | ||
return None, None, None, None, None, None, None | ||
return None, None, None, None, None, None, None, None | ||
|
||
logging.debug("\tDone scanning.") | ||
|
||
return sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs | ||
return sslv2, sslv3, tlsv1, tlsv1_1, tlsv1_2, tlsv1_3, certs, reneg | ||
|
||
|
||
# EV Guidelines OID | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you're removing all of this logic, then this block can collapse to
row = [data[field] for field in headers]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good eye!