Skip to content
This repository has been archived by the owner on Dec 17, 2021. It is now read-only.

Add support for client authentication #286

Merged
merged 31 commits into from
Mar 20, 2019
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c35efea
Added ca-file argument for pshtt and added pshtt output fields for Cl…
echudow Oct 11, 2018
324054b
Fixed bug in ca-file argument.
echudow Oct 11, 2018
0f516cf
Merge remote-tracking branch '18F/master'
jsf9k Nov 13, 2018
99f5eb9
Merge remote-tracking branch '18F/master'
jsf9k Nov 23, 2018
03866d5
Remove some whitespace that offends flake8
jsf9k Nov 23, 2018
8eb43b8
Merge branch 'master' into master
jsf9k Nov 30, 2018
c89e956
Reorder some of the values when they are written to CSV
jsf9k Nov 30, 2018
05b3083
Added specific info to sslyze for weak cipher tasks
echudow Dec 18, 2018
cf81846
Merged changes
echudow Dec 18, 2018
652d6a2
Merged changes
echudow Dec 18, 2018
95aa0f5
Make flake8 happy
jsf9k Dec 18, 2018
333d23e
Merge branch 'master' into master
jsf9k Dec 18, 2018
a8b66d1
Merge branch 'master' into master
jsf9k Dec 28, 2018
d6e1970
Refactored to handle some errors
echudow Jan 21, 2019
133a5fc
Added an HTTPS Live field
echudow Jan 21, 2019
80d27f1
Merge branch 'master' of https://github.com/echudow/domain-scan
echudow Jan 21, 2019
aeb980d
Added public trust with intermediate certs arg
echudow Mar 7, 2019
10458ca
Merge remote-tracking branch '18F/master'
jsf9k Mar 11, 2019
07be179
Make flake8 happy
jsf9k Mar 11, 2019
050e26b
Added ca_file option to help validate and construct higher trust chains
echudow Mar 15, 2019
860392e
trustymail updates
echudow Mar 15, 2019
15bd499
trustymail updates
echudow Mar 15, 2019
4240b17
Make a few whitespace changes to make flake8 happy
jsf9k Mar 17, 2019
369f91b
Fix scanners/trustymail.py file
jsf9k Mar 17, 2019
c676313
Adding extra trustymail command-line arguments back for compatibility
echudow Mar 17, 2019
af596de
Merge branch 'master' of https://github.com/18F/domain-scan
echudow Mar 17, 2019
cf8a291
Put back the action='store_true' statements
jsf9k Mar 18, 2019
0848d11
Adjust tests to pass now that some options have action='store_true'
jsf9k Mar 18, 2019
e513aa7
Don't convert None to False
echudow Mar 18, 2019
b812928
Update to use the latest pshtt and trustymail, which correctly handle…
jsf9k Mar 20, 2019
2019622
Update to use pshtt 0.6.1
jsf9k Mar 20, 2019
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
16 changes: 12 additions & 4 deletions scanners/pshtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
)

Expand Down Expand Up @@ -120,14 +122,20 @@ def to_rows(data):


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",
]


Expand Down
147 changes: 124 additions & 23 deletions scanners/sslyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,6 +39,9 @@
# 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.
Expand Down Expand Up @@ -225,6 +230,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')
Expand Down Expand Up @@ -254,6 +272,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"
Expand Down Expand Up @@ -289,9 +314,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.
Expand All @@ -301,6 +326,9 @@ def run_sslyze(data, environment, options):
if certs:
data['certs'] = analyze_certs(certs)

if reneg:
analyze_reneg(data, reneg)

return data


Expand Down Expand Up @@ -333,6 +361,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
Expand All @@ -349,11 +381,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):
Expand Down Expand Up @@ -391,6 +441,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):
Expand All @@ -405,10 +460,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"):
Copy link
Contributor

Choose a reason for hiding this comment

The 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")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified to:

data['certs']['expired_certificate'] = not(leaf.not_valid_before < now < leaf.not_valid_after)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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":
Expand Down Expand Up @@ -497,16 +568,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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could simply be:

if reneg.accepts_client_renegotiation and not reneg.supports_secure_renegotiation:

And then further, the whole block could be:

data['config']['insecure_renegotiation'] = reneg.accepts_client_renegotiation and not reneg.supports_secure_renegotiation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -535,38 +616,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))
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

logging.debug("\t\t%s scan.", scan_type)

Which matches more what is done in the documentation, where the parameters (scan_type in this case) are passed to logging.debug as arguments, rather than doing the substitution first: https://docs.python.org/3/library/logging.html#logging.Logger.debug

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be logging.error(...) ?

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.
Expand All @@ -589,7 +685,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to:

sslv2 = sslv3 = tlsv1 = tlsv1_1 = tlsv1_2 = tlsv1_3 = certs = reneg = None

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


# Queue them all up
queue(Sslv20ScanCommand())
Expand All @@ -602,6 +698,9 @@ def queue(command):
if options.get("sslyze-certs", True) is True:
queue(CertificateInfoScanCommand())

if options.get("sslyze-reneg", True) is True:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be if options.get("sslyze-reneg", True):

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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():
Expand All @@ -626,6 +725,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)
Expand All @@ -640,11 +741,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
Expand Down
12 changes: 7 additions & 5 deletions scanners/trustymail.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,16 @@ def to_rows(data):

headers = [
"Live",
"MX Record", "Mail Servers", "Mail Server Ports Tested",
"MX Record", "MX Record DNSSEC", "Mail Servers", "Mail Server Ports Tested",
"Domain Supports SMTP", "Domain Supports SMTP Results",
"Domain Supports STARTTLS", "Domain Supports STARTTLS Results",
"SPF Record", "Valid SPF", "SPF Results",
"DMARC Record", "Valid DMARC", "DMARC Results",
"DMARC Record on Base Domain", "Valid DMARC Record on Base Domain",
"DMARC Results on Base Domain", "DMARC Policy", "DMARC Policy Percentage",
"SPF Record", "SPF Record DNSSEC", "Valid SPF", "SPF Results",
"DMARC Record", "DMARC Record DNSSEC", "Valid DMARC", "DMARC Results",
"DMARC Record on Base Domain", "DMARC Record on Base Domain DNSSEC",
"Valid DMARC Record on Base Domain", "DMARC Results on Base Domain",
"DMARC Policy", "DMARC Subdomain Policy", "DMARC Policy Percentage",
"DMARC Aggregate Report URIs", "DMARC Forensic Report URIs",
"DMARC Has Aggregate Report URI", "DMARC Has Forensic Report URI",
"DMARC Reporting Address Acceptance Error",
"Syntax Errors", "Debug Info"
]
Loading