def __init__( self, server_info: ServerConnectivityInfo, scan_command: HttpHeadersScanCommand, raw_hsts_header: Optional[str], raw_hpkp_header: Optional[str], raw_expect_ct_header: Optional[str], hpkp_report_only: bool, cert_chain: List[Certificate], ) -> None: super().__init__(server_info, scan_command) self.hsts_header = ParsedHstsHeader(raw_hsts_header) if raw_hsts_header else None self.hpkp_header = ParsedHpkpHeader(raw_hpkp_header, hpkp_report_only) if raw_hpkp_header else None self.expect_ct_header = ParsedExpectCTHeader(raw_expect_ct_header) if raw_expect_ct_header else None self.verified_certificate_chain: List[Certificate] = [] try: main_trust_store = TrustStoresRepository.get_default().get_main_store() self.verified_certificate_chain = main_trust_store.build_verified_certificate_chain(cert_chain) except CouldNotBuildVerifiedChainError: pass # Is the pinning configuration valid? self.is_valid_pin_configured = None self.is_backup_pin_configured = None if self.verified_certificate_chain and self.hpkp_header: # Is one of the configured pins in the current server chain? self.is_valid_pin_configured = False server_pin_list = [CertificateUtils.get_hpkp_pin(cert) for cert in self.verified_certificate_chain] for pin in self.hpkp_header.pin_sha256_list: if pin in server_pin_list: self.is_valid_pin_configured = True break # Is a backup pin configured? self.is_backup_pin_configured = set(self.hpkp_header.pin_sha256_list) != set(server_pin_list)
def __init__( self, server_info, # type: ServerConnectivityInfo scan_command, # type: HttpHeadersScanCommand raw_hsts_header, # type: Text raw_hpkp_header, # type: Text hpkp_report_only, # type: bool cert_chain # type: List[cryptography.x509.Certificate] ): # type: (...) -> None super(HttpHeadersScanResult, self).__init__(server_info, scan_command) self.hsts_header = ParsedHstsHeader(raw_hsts_header) if raw_hsts_header else None self.hpkp_header = ParsedHpkpHeader(raw_hpkp_header, hpkp_report_only) if raw_hpkp_header else None self.verified_certificate_chain = [] try: main_trust_store = TrustStoresRepository.get_default().get_main_store() self.verified_certificate_chain = main_trust_store.build_verified_certificate_chain(cert_chain) except CouldNotBuildVerifiedChainError: pass # Is the pinning configuration valid? self.is_valid_pin_configured = None self.is_backup_pin_configured = None if self.verified_certificate_chain and self.hpkp_header: # Is one of the configured pins in the current server chain? self.is_valid_pin_configured = False server_pin_list = [CertificateUtils.get_hpkp_pin(cert) for cert in self.verified_certificate_chain] for pin in self.hpkp_header.pin_sha256_list: if pin in server_pin_list: self.is_valid_pin_configured = True break # Is a backup pin configured? self.is_backup_pin_configured = set(self.hpkp_header.pin_sha256_list) != set(server_pin_list)
def test(self): intermediate_path = os.path.join( os.path.dirname(__file__), '..', 'utils', 'DigiCertSHA2ExtendedValidationServerCA.pem') with open(intermediate_path) as intermediate_file: intermediate_pem = intermediate_file.read().encode('ascii') leaf_path = os.path.join(os.path.dirname(__file__), '..', 'utils', 'github.com.pem') with open(leaf_path) as leaf_file: leaf_pem = leaf_file.read().encode('ascii') certificate_chain = [ load_pem_x509_certificate(leaf_pem, default_backend()), load_pem_x509_certificate(intermediate_pem, default_backend()) ] found_mozilla = False for trust_store in TrustStoresRepository.get_default().get_all_stores( ): verified_chain = trust_store.build_verified_certificate_chain( certificate_chain) self.assertTrue(verified_chain) if trust_store.name == 'Mozilla': found_mozilla = True # The GH certificate is EV self.assertTrue( trust_store.is_extended_validation(certificate_chain[0])) self.assertTrue(found_mozilla)
def process_task( self, server_info: ServerConnectivityInfo, scan_command: PluginScanCommand ) -> 'CertificateInfoScanResult': if not isinstance(scan_command, CertificateInfoScanCommand): raise ValueError('Unexpected scan command') final_trust_store_list = TrustStoresRepository.get_default().get_all_stores() if scan_command.custom_ca_file: if not os.path.isfile(scan_command.custom_ca_file): raise ValueError('Could not open supplied CA file at "{}"'.format(scan_command.custom_ca_file)) final_trust_store_list.append(TrustStore(scan_command.custom_ca_file, 'Custom --ca_file', 'N/A')) # Workaround for https://github.com/pyca/cryptography/issues/3495 default_backend() thread_pool = ThreadPool() for trust_store in final_trust_store_list: # Try to connect with each trust store thread_pool.add_job((self._get_and_verify_certificate_chain, [server_info, trust_store])) # Start processing the jobs; one thread per trust thread_pool.start(len(final_trust_store_list)) # Store the results as they come certificate_chain: List[Certificate] = [] path_validation_result_list = [] path_validation_error_list = [] ocsp_response = None for (job, result) in thread_pool.get_result(): (_, (_, trust_store)) = job certificate_chain, validation_result, _ocsp_response = result # Keep the OCSP response if the validation was succesful and a response was returned if _ocsp_response: ocsp_response = _ocsp_response # Store the returned verify string for each trust store path_validation_result_list.append(PathValidationResult(trust_store, validation_result)) # Store thread pool errors last_exception = None for (job, exception) in thread_pool.get_error(): (_, (_, trust_store)) = job path_validation_error_list.append(PathValidationError(trust_store, exception)) last_exception = exception thread_pool.join() if len(path_validation_error_list) == len(final_trust_store_list): # All connections failed unexpectedly; raise an exception instead of returning a result raise last_exception # type: ignore # All done return CertificateInfoScanResult(server_info, scan_command, certificate_chain, path_validation_result_list, path_validation_error_list, ocsp_response)
def process_task( self, server_info: ServerConnectivityInfo, scan_command: PluginScanCommand ) -> 'CertificateInfoScanResult': if not isinstance(scan_command, CertificateInfoScanCommand): raise ValueError('Unexpected scan command') final_trust_store_list = TrustStoresRepository.get_default().get_all_stores() if scan_command.custom_ca_file: if not os.path.isfile(scan_command.custom_ca_file): raise ValueError('Could not open supplied CA file at "{}"'.format(scan_command.custom_ca_file)) final_trust_store_list.append(TrustStore(scan_command.custom_ca_file, 'Custom --ca_file', 'N/A')) # Workaround for https://github.com/pyca/cryptography/issues/3495 default_backend() thread_pool = ThreadPool() for trust_store in final_trust_store_list: # Try to connect with each trust store thread_pool.add_job((self._get_and_verify_certificate_chain, [server_info, trust_store])) # Start processing the jobs; one thread per trust thread_pool.start(len(final_trust_store_list)) # Store the results as they come certificate_chain: List[Certificate] = [] path_validation_result_list = [] path_validation_error_list = [] ocsp_response = None for (job, result) in thread_pool.get_result(): (_, (_, trust_store)) = job certificate_chain, validation_result, _ocsp_response = result # Keep the OCSP response if the validation was succesful and a response was returned if _ocsp_response: ocsp_response = _ocsp_response # Store the returned verify string for each trust store path_validation_result_list.append(PathValidationResult(trust_store, validation_result)) # Store thread pool errors last_exception = None for (job, exception) in thread_pool.get_error(): (_, (_, trust_store)) = job path_validation_error_list.append(PathValidationError(trust_store, exception)) last_exception = exception thread_pool.join() if len(path_validation_error_list) == len(final_trust_store_list): # All connections failed unexpectedly; raise an exception instead of returning a result raise last_exception # type: ignore # All done return CertificateInfoScanResult(server_info, scan_command, certificate_chain, path_validation_result_list, path_validation_error_list, ocsp_response)
def process_task( self, server_info: ServerConnectivityInfo, scan_command: PluginScanCommand ) -> 'HttpHeadersScanResult': if not isinstance(scan_command, HttpHeadersScanCommand): raise ValueError('Unexpected scan command') if server_info.tls_wrapped_protocol not in [TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS]: raise ValueError('Cannot test for HTTP headers on a StartTLS connection.') # Perform the SSL handshake mozilla_store = TrustStoresRepository.get_default().get_main_store() ssl_connection = server_info.get_preconfigured_ssl_connection(ssl_verify_locations=mozilla_store.path) try: ssl_connection.connect() try: verified_chain_as_pem = ssl_connection.ssl_client.get_verified_chain() except CouldNotBuildVerifiedChain: verified_chain_as_pem = None # Send an HTTP GET request to the server ssl_connection.ssl_client.write(HttpRequestGenerator.get_request(host=server_info.hostname)) # We do not follow redirections because the security headers must be set on the first page according to # https://hstspreload.appspot.com/: # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS # header (rather than the page it redirects to)." http_response = HttpResponseParser.parse_from_ssl_connection(ssl_connection.ssl_client) finally: ssl_connection.close() if http_response.version == 9: # HTTP 0.9 => Probably not an HTTP response raise ValueError('Server did not return an HTTP response') # Parse the certificate chain verified_chain = [ load_pem_x509_certificate(cert_as_pem.encode('ascii'), backend=default_backend()) for cert_as_pem in verified_chain_as_pem ] if verified_chain_as_pem else None # Parse each header hsts_header = StrictTransportSecurityHeader.from_http_response(http_response) expect_ct_header = ExpectCtHeader.from_http_response(http_response) hpkp_header = PublicKeyPinsHeader.from_http_response(http_response) hpkp_report_only_header = PublicKeyPinsReportOnlyHeader.from_http_response(http_response) return HttpHeadersScanResult( server_info, scan_command, hsts_header, hpkp_header, hpkp_report_only_header, expect_ct_header, verified_chain )
def __init__( self, server_info: ServerConnectivityInfo, scan_command: HttpHeadersScanCommand, raw_hsts_header: Optional[str], raw_hpkp_header: Optional[str], raw_expect_ct_header: Optional[str], hpkp_report_only: bool, cert_chain: List[Certificate], ) -> None: super().__init__(server_info, scan_command) self.hsts_header = ParsedHstsHeader( raw_hsts_header) if raw_hsts_header else None self.hpkp_header = ParsedHpkpHeader( raw_hpkp_header, hpkp_report_only) if raw_hpkp_header else None self.expect_ct_header = ParsedExpectCtHeader( raw_expect_ct_header) if raw_expect_ct_header else None self.verified_certificate_chain: List[Certificate] = [] try: main_trust_store = TrustStoresRepository.get_default( ).get_main_store() self.verified_certificate_chain = main_trust_store.build_verified_certificate_chain( cert_chain) except CouldNotBuildVerifiedChainError: pass # Is the pinning configuration valid? self.is_valid_pin_configured = None self.is_backup_pin_configured = None if self.verified_certificate_chain and self.hpkp_header: # Is one of the configured pins in the current server chain? self.is_valid_pin_configured = False server_pin_list = [ CertificateUtils.get_hpkp_pin(cert) for cert in self.verified_certificate_chain ] for pin in self.hpkp_header.pin_sha256_list: if pin in server_pin_list: self.is_valid_pin_configured = True break # Is a backup pin configured? self.is_backup_pin_configured = set( self.hpkp_header.pin_sha256_list) != set(server_pin_list)
def process_task(self, server_info, scan_command): # type: (ServerConnectivityInfo, CertificateInfoScanCommand) -> CertificateInfoScanResult final_trust_store_list = list(TrustStoresRepository.get_all()) if scan_command.custom_ca_file: if not os.path.isfile(scan_command.custom_ca_file): raise ValueError(u'Could not open supplied CA file at "{}"'.format(scan_command.custom_ca_file)) final_trust_store_list.append(TrustStore(scan_command.custom_ca_file, u'Custom --ca_file', u'N/A')) thread_pool = ThreadPool() for trust_store in final_trust_store_list: # Try to connect with each trust store thread_pool.add_job((self._get_and_verify_certificate_chain, (server_info, trust_store))) # Start processing the jobs; one thread per trust thread_pool.start(len(final_trust_store_list)) # Store the results as they come certificate_chain = [] path_validation_result_list = [] path_validation_error_list = [] ocsp_response = None for (job, result) in thread_pool.get_result(): (_, (_, trust_store)) = job certificate_chain, validation_result, ocsp_response = result # Store the returned verify string for each trust store path_validation_result_list.append(PathValidationResult(trust_store, validation_result)) # Store thread pool errors last_exception = None for (job, exception) in thread_pool.get_error(): (_, (_, trust_store)) = job path_validation_error_list.append(PathValidationError(trust_store, exception)) last_exception = exception thread_pool.join() if len(path_validation_error_list) == len(final_trust_store_list): # All connections failed unexpectedly; raise an exception instead of returning a result raise RuntimeError(u'Could not connect to the server; last error: {}'.format(last_exception)) # All done return CertificateInfoScanResult(server_info, scan_command, certificate_chain, path_validation_result_list, path_validation_error_list, ocsp_response)
def test(self): intermediate_path = os.path.join( os.path.dirname(__file__), u'..', u'utils', u'DigiCertSHA2ExtendedValidationServerCA.pem') with open(intermediate_path) as intermediate_file: intermediate_pem = intermediate_file.read() leaf_path = os.path.join(os.path.dirname(__file__), u'..', u'utils', u'github.com.pem') with open(leaf_path) as leaf_file: leaf_pem = leaf_file.read() certificate_chain = [ Certificate.from_pem(leaf_pem), Certificate.from_pem(intermediate_pem) ] for trust_store in TrustStoresRepository.get_all(): verified_chain = trust_store.build_verified_certificate_chain( certificate_chain) self.assertTrue(verified_chain)
def test(self): intermediate_path = os.path.join(os.path.dirname(__file__), '..', 'utils', 'DigiCertSHA2ExtendedValidationServerCA.pem') with open(intermediate_path) as intermediate_file: intermediate_pem = intermediate_file.read().encode('ascii') leaf_path = os.path.join(os.path.dirname(__file__), '..', 'utils', 'github.com.pem') with open(leaf_path) as leaf_file: leaf_pem = leaf_file.read().encode('ascii') certificate_chain = [load_pem_x509_certificate(leaf_pem, default_backend()), load_pem_x509_certificate(intermediate_pem, default_backend())] found_mozilla = False for trust_store in TrustStoresRepository.get_default().get_all_stores(): verified_chain = trust_store.build_verified_certificate_chain(certificate_chain) self.assertTrue(verified_chain) if trust_store.name == 'Mozilla': found_mozilla = True # The GH certificate is EV self.assertTrue(trust_store.is_extended_validation(certificate_chain[0])) self.assertTrue(found_mozilla)
def __init__(self, server_info, scan_command, raw_hsts_header, raw_hpkp_header, hpkp_report_only, cert_chain): # type: (ServerConnectivityInfo, HttpHeadersScanCommand, Text, Text, bool, List[X509Certificate]) -> None super(HttpHeadersScanResult, self).__init__(server_info, scan_command) self.hsts_header = ParsedHstsHeader( raw_hsts_header) if raw_hsts_header else None self.hpkp_header = ParsedHpkpHeader( raw_hpkp_header, hpkp_report_only) if raw_hpkp_header else None parsed_certificate_chain = [ Certificate.from_nassl(x509_cert) for x509_cert in cert_chain ] self.verified_certificate_chain = [] try: self.verified_certificate_chain = TrustStoresRepository.get_main( ).build_verified_certificate_chain(parsed_certificate_chain) except CouldNotBuildVerifiedChainError: pass # Is the pinning configuration valid? self.is_valid_pin_configured = None self.is_backup_pin_configured = None if self.verified_certificate_chain and self.hpkp_header: # Is one of the configured pins in the current server chain? self.is_valid_pin_configured = False server_pin_list = [ cert.hpkp_pin for cert in self.verified_certificate_chain ] for pin in self.hpkp_header.pin_sha256_list: if pin in server_pin_list: self.is_valid_pin_configured = True break # Is a backup pin configured? self.is_backup_pin_configured = set( self.hpkp_header.pin_sha256_list) != set(server_pin_list)
def __init__( self, server_info: ServerConnectivityInfo, scan_command: CertificateInfoScanCommand, certificate_chain: List[Certificate], path_validation_result_list: List[PathValidationResult], path_validation_error_list: List[PathValidationError], ocsp_response: OcspResponse ) -> None: super().__init__(server_info, scan_command) # Find the first trust store that successfully validated the certificate chain self.successful_trust_store = None # Sort the path_validation_result_list so the same successful_trust_store always get picked for a given server # because threading timings change the order of path_validation_result_list def sort_function(path_validation: PathValidationResult) -> str: return path_validation.trust_store.name.lower() path_validation_result_list.sort(key=sort_function) for path_result in path_validation_result_list: if path_result.is_certificate_trusted: self.successful_trust_store = path_result.trust_store self.ocsp_response = None self.is_ocsp_response_trusted = None self.ocsp_response_status = None if ocsp_response: self.ocsp_response_status = ocsp_response.status # We only keep the dictionary as a nassl.OcspResponse is not pickable self.ocsp_response = ocsp_response.as_dict() if self.successful_trust_store and self.ocsp_response_status == OcspResponseStatusEnum.SUCCESSFUL: try: ocsp_response.verify(self.successful_trust_store.path) self.is_ocsp_response_trusted = True except OcspResponseNotTrustedError: self.is_ocsp_response_trusted = False self.certificate_chain = certificate_chain # Check if it is EV - we only have the EV OIDs for Mozilla self.is_leaf_certificate_ev = TrustStoresRepository.get_default().get_main_store().is_extended_validation( self.certificate_chain[0] ) # Look for the Must-Staple extension has_must_staple = CertificateUtils.has_ocsp_must_staple_extension(self.certificate_chain[0]) self.certificate_has_must_staple_extension = has_must_staple # Look for the certificate transparency extension self.certificate_included_scts_count = CertificateUtils.count_scts_in_sct_extension(self.certificate_chain[0]) # Try to build the verified chain self.verified_certificate_chain: List[Certificate] = [] self.is_certificate_chain_order_valid = True if self.successful_trust_store: try: self.verified_certificate_chain = self.successful_trust_store.build_verified_certificate_chain( self.certificate_chain ) except InvalidCertificateChainOrderError: self.is_certificate_chain_order_valid = False except AnchorCertificateNotInTrustStoreError: pass self.has_anchor_in_certificate_chain = None if self.verified_certificate_chain: self.has_anchor_in_certificate_chain = self.verified_certificate_chain[-1] in self.certificate_chain self.path_validation_result_list = path_validation_result_list self.path_validation_error_list = path_validation_error_list try: CertificateUtils.matches_hostname(certificate_chain[0], server_info.tls_server_name_indication) self.certificate_matches_hostname = True except CertificateError: self.certificate_matches_hostname = False # Check if a SHA1-signed certificate is in the chain # Root certificates can still be signed with SHA1 so we only check leaf and intermediate certificates self.has_sha1_in_certificate_chain = None if self.verified_certificate_chain: self.has_sha1_in_certificate_chain = False for cert in self.verified_certificate_chain[:-1]: if isinstance(cert.signature_hash_algorithm, hashes.SHA1): self.has_sha1_in_certificate_chain = True break # Check if this is a distrusted Symantec-issued chain self.symantec_distrust_timeline = _SymantecDistructTester.get_distrust_timeline(self.verified_certificate_chain)
def perform(self) -> CertificateChainDeploymentAnalysisResult: """Run the analysis. """ leaf_cert = self.received_certificate_chain[0] # OCSP Must-Staple has_ocsp_must_staple = False try: tls_feature_ext = leaf_cert.extensions.get_extension_for_oid(ExtensionOID.TLS_FEATURE) for feature_type in tls_feature_ext.value: if feature_type == cryptography.x509.TLSFeatureType.status_request: has_ocsp_must_staple = True break except ExtensionNotFound: pass # Received chain order is_chain_order_valid = True previous_issuer = None for index, cert in enumerate(self.received_certificate_chain): current_subject = cert.subject if index > 0: # Compare the current subject with the previous issuer in the chain if current_subject != previous_issuer: is_chain_order_valid = False break try: previous_issuer = cert.issuer except KeyError: # Missing issuer; this is okay if this is the last cert previous_issuer = u"missing issuer {}".format(index) # Check if it is EV - we only have the EV OIDs for Mozilla is_leaf_certificate_ev = TrustStoresRepository.get_default().get_main_store().is_extended_validation( self.received_certificate_chain[0] ) # Check for Signed Timestamps number_of_scts: Optional[int] = 0 try: # Look for the x509 extension sct_ext = leaf_cert.extensions.get_extension_for_oid( ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS ) if isinstance(sct_ext.value, cryptography.x509.UnrecognizedExtension): # The version of OpenSSL on the system is too old and can't parse the SCT extension number_of_scts = None # Count the number of entries in the extension number_of_scts = len(sct_ext.value) except ExtensionNotFound: pass # Check if the anchor was sent by the server has_anchor_in_certificate_chain = None if self.verified_certificate_chain: has_anchor_in_certificate_chain = self.verified_certificate_chain[-1] in self.received_certificate_chain # Check hostname validation try: CertificateUtils.matches_hostname(leaf_cert, self.server_hostname) certificate_matches_hostname = True except CertificateError: certificate_matches_hostname = False # Check if a SHA1-signed certificate is in the chain # Root certificates can still be signed with SHA1 so we only check leaf and intermediate certificates has_sha1_in_certificate_chain = None if self.verified_certificate_chain: has_sha1_in_certificate_chain = False for cert in self.verified_certificate_chain[:-1]: if isinstance(cert.signature_hash_algorithm, hashes.SHA1): has_sha1_in_certificate_chain = True break # Check if this is a distrusted Symantec-issued chain verified_chain_has_legacy_symantec_anchor = None if self.verified_certificate_chain: symantec_distrust_timeline = _SymantecDistructTester.get_distrust_timeline(self.verified_certificate_chain) verified_chain_has_legacy_symantec_anchor = True if symantec_distrust_timeline else False # Check the OCSP response if there is one is_ocsp_response_trusted = None ocsp_response_status = None if self.received_ocsp_response: ocsp_response_status = self.received_ocsp_response.status if self.trust_store_used_to_build_verified_chain \ and ocsp_response_status == OcspResponseStatusEnum.SUCCESSFUL: try: self.received_ocsp_response.verify(self.trust_store_used_to_build_verified_chain.path) is_ocsp_response_trusted = True except OcspResponseNotTrustedError: is_ocsp_response_trusted = False return CertificateChainDeploymentAnalysisResult( leaf_certificate_subject_matches_hostname=certificate_matches_hostname, leaf_certificate_has_must_staple_extension=has_ocsp_must_staple, leaf_certificate_is_ev=is_leaf_certificate_ev, leaf_certificate_signed_certificate_timestamps_count=number_of_scts, received_chain_contains_anchor_certificate=has_anchor_in_certificate_chain, received_chain_has_valid_order=is_chain_order_valid, verified_chain_has_sha1_signature=has_sha1_in_certificate_chain, verified_chain_has_legacy_symantec_anchor=verified_chain_has_legacy_symantec_anchor, ocsp_response_is_trusted=is_ocsp_response_trusted, ocsp_response_status=ocsp_response_status, )
def parse_command_line(self) -> Tuple[List[ServerConnectivityTester], List[ServerStringParsingError], Any]: """Parses the command line used to launch SSLyze. """ (args_command_list, args_target_list) = self._parser.parse_args() if args_command_list.update_trust_stores: # Just update the trust stores and do nothing TrustStoresRepository.update_default() raise TrustStoresUpdateCompleted() # Handle the --targets_in command line and fill args_target_list if args_command_list.targets_in: if args_target_list: raise CommandLineParsingError('Cannot use --targets_list and specify targets within the command line.') try: # Read targets from a file with open(args_command_list.targets_in) as f: for target in f.readlines(): if target.strip(): # Ignore empty lines if not target.startswith('#'): # Ignore comment lines args_target_list.append(target.strip()) except IOError: raise CommandLineParsingError('Can\'t read targets from input file \'{}.'.format( args_command_list.targets_in)) if not args_target_list: raise CommandLineParsingError('No targets to scan.') # Handle the --regular command line parameter as a shortcut if self._parser.has_option('--regular'): if getattr(args_command_list, 'regular'): setattr(args_command_list, 'regular', False) for cmd in self.REGULAR_CMD: setattr(args_command_list, cmd, True) # Sanity checks on the command line options # Prevent --quiet and --xml_out - if args_command_list.xml_file and args_command_list.xml_file == '-' and args_command_list.quiet: raise CommandLineParsingError('Cannot use --quiet with --xml_out -.') # Prevent --quiet and --json_out - if args_command_list.json_file and args_command_list.json_file == '-' and args_command_list.quiet: raise CommandLineParsingError('Cannot use --quiet with --json_out -.') # Prevent --xml_out - and --json_out - if args_command_list.json_file and args_command_list.json_file == '-' \ and args_command_list.xml_file and args_command_list.xml_file == '-': raise CommandLineParsingError('Cannot use --xml_out - with --json_out -.') # Sanity checks on the client cert options client_auth_creds = None if bool(args_command_list.cert) ^ bool(args_command_list.key): raise CommandLineParsingError('No private key or certificate file were given. See --cert and --key.') elif args_command_list.cert: # Private key formats if args_command_list.keyform == 'DER': key_type = OpenSslFileTypeEnum.ASN1 elif args_command_list.keyform == 'PEM': key_type = OpenSslFileTypeEnum.PEM else: raise CommandLineParsingError('--keyform should be DER or PEM.') # Let's try to open the cert and key files try: client_auth_creds = ClientAuthenticationCredentials(args_command_list.cert, args_command_list.key, key_type, args_command_list.keypass) except ValueError as e: raise CommandLineParsingError('Invalid client authentication settings: {}.'.format(e.args[0])) # HTTP CONNECT proxy http_tunneling_settings = None if args_command_list.https_tunnel: try: http_tunneling_settings = HttpConnectTunnelingSettings.from_url(args_command_list.https_tunnel) except ValueError as e: raise CommandLineParsingError('Invalid proxy URL for --https_tunnel: {}.'.format(e.args[0])) # STARTTLS tls_wrapped_protocol = TlsWrappedProtocolEnum.PLAIN_TLS if args_command_list.starttls: if args_command_list.starttls not in self.START_TLS_PROTOCOLS: raise CommandLineParsingError(self.START_TLS_USAGE) else: # StartTLS was specified if args_command_list.starttls in self.STARTTLS_PROTOCOL_DICT.keys(): # Protocol was given in the command line tls_wrapped_protocol = self.STARTTLS_PROTOCOL_DICT[args_command_list.starttls] # Create the server connectivity tester for each specified servers # A limitation when using the command line is that only one client_auth_credentials and http_tunneling_settings # can be specified, for all the servers to scan good_server_list = [] bad_server_list = [] for server_string in args_target_list: try: hostname, ip_address, port = CommandLineServerStringParser.parse_server_string(server_string) except ServerStringParsingError as e: # Will happen if the server string is malformed bad_server_list.append(e) continue try: # TODO(AD): Unicode hostnames may fail on Python2 # hostname = hostname.decode('utf-8') server_info = ServerConnectivityTester( hostname=hostname, port=port, ip_address=ip_address, tls_wrapped_protocol=tls_wrapped_protocol, tls_server_name_indication=args_command_list.sni, xmpp_to_hostname=args_command_list.xmpp_to, client_auth_credentials=client_auth_creds, http_tunneling_settings=http_tunneling_settings ) good_server_list.append(server_info) except ValueError as e: # Will happen for example if xmpp_to is specified for a non-XMPP connection raise CommandLineParsingError(e.args[0]) # Command line hacks # Handle --starttls=auto now that we parsed the server strings if args_command_list.starttls == 'auto': for server_info in good_server_list: # We use the port number to deduce the protocol if server_info.port in self.STARTTLS_PROTOCOL_DICT.keys(): server_info.tls_wrapped_protocol = self.STARTTLS_PROTOCOL_DICT[server_info.port] # Handle --http_get now that we parsed the server strings # Doing it here is hacky as the option is defined within PluginOpenSSLCipherSuites if args_command_list.http_get: for server_info in good_server_list: if server_info.port == 443: server_info.tls_wrapped_protocol = TlsWrappedProtocolEnum.HTTPS return good_server_list, bad_server_list, args_command_list
def test_update_default(self): repo = TrustStoresRepository.update_default() self.assertTrue(repo.get_main_store()) self.assertEqual(len(repo.get_all_stores()), 5)
def __init__( self, server_info, # type: ServerConnectivityInfo scan_command, # type: CertificateInfoScanCommand certificate_chain, # type: List[cryptography.x509.Certificate] path_validation_result_list, # type: List[PathValidationResult] path_validation_error_list, # type: List[PathValidationError] ocsp_response # type: OcspResponse ): # type: (...) -> None super(CertificateInfoScanResult, self).__init__(server_info, scan_command) # Find the first trust store that successfully validated the certificate chain self.successful_trust_store = None # Sort the path_validation_result_list so the same successful_trust_store always get picked for a given server # because threading timings change the order of path_validation_result_list def sort_function(path_validation): # type: (PathValidationResult) -> Text return path_validation.trust_store.name.lower() path_validation_result_list.sort(key=sort_function) for path_result in path_validation_result_list: if path_result.is_certificate_trusted: self.successful_trust_store = path_result.trust_store self.ocsp_response = None self.is_ocsp_response_trusted = None if ocsp_response: # We only keep the dictionary as a nassl.OcspResponse is not pickable self.ocsp_response = ocsp_response.as_dict() if self.successful_trust_store: try: ocsp_response.verify(self.successful_trust_store.path) self.is_ocsp_response_trusted = True except OcspResponseNotTrustedError: self.is_ocsp_response_trusted = False self.certificate_chain = certificate_chain # Check if it is EV - we only have the EV OIDs for Mozilla self.is_leaf_certificate_ev = TrustStoresRepository.get_main( ).is_extended_validation(self.certificate_chain[0]) # Try to build the verified chain self.verified_certificate_chain = [] self.is_certificate_chain_order_valid = True if self.successful_trust_store: try: self.verified_certificate_chain = self.successful_trust_store.build_verified_certificate_chain( self.certificate_chain) except InvalidCertificateChainOrderError: self.is_certificate_chain_order_valid = False except AnchorCertificateNotInTrustStoreError: pass self.has_anchor_in_certificate_chain = None if self.verified_certificate_chain: self.has_anchor_in_certificate_chain = self.verified_certificate_chain[ -1] in self.certificate_chain self.path_validation_result_list = path_validation_result_list self.path_validation_error_list = path_validation_error_list try: CertificateUtils.matches_hostname( certificate_chain[0], server_info.tls_server_name_indication) self.certificate_matches_hostname = True except CertificateError: self.certificate_matches_hostname = False # Check if a SHA1-signed certificate is in the chain # Root certificates can still be signed with SHA1 so we only check leaf and intermediate certificates self.has_sha1_in_certificate_chain = None if self.verified_certificate_chain: self.has_sha1_in_certificate_chain = False for cert in self.verified_certificate_chain[:-1]: if isinstance(cert.signature_hash_algorithm, hashes.SHA1): self.has_sha1_in_certificate_chain = True break
def process_task( self, server_info: ServerConnectivityInfo, scan_command: PluginScanCommand ) -> 'CertificateInfoScanResult': if not isinstance(scan_command, CertificateInfoScanCommand): raise ValueError('Unexpected scan command') final_trust_store_list = TrustStoresRepository.get_default().get_all_stores() if scan_command.custom_ca_file: custom_ca_file_path = Path(scan_command.custom_ca_file) if not custom_ca_file_path.is_file(): raise ValueError(f'Could not open supplied CA file at "{custom_ca_file_path}"') final_trust_store_list.append(TrustStore(custom_ca_file_path, 'Custom --ca_file', 'N/A')) # Workaround for https://github.com/pyca/cryptography/issues/3495 default_backend() thread_pool = ThreadPool() for trust_store in final_trust_store_list: # Try to connect with each trust store thread_pool.add_job((self._get_and_verify_certificate_chain, [server_info, trust_store])) # Start processing the jobs; one thread per trust thread_pool.start(len(final_trust_store_list)) # Store the results as they come path_validation_result_list = [] path_validation_error_list = [] ocsp_response = None received_chain = None for (job, result) in thread_pool.get_result(): (_, (_, trust_store)) = job received_chain_as_pem, verified_chain_as_pem, validation_result, _ocsp_response = result # Parse the certificates using the cryptography module if not received_chain: received_chain = [ load_pem_x509_certificate(pem_cert.encode('ascii'), backend=default_backend()) for pem_cert in received_chain_as_pem ] verified_chain = [ load_pem_x509_certificate(cert_as_pem.encode('ascii'), backend=default_backend()) for cert_as_pem in verified_chain_as_pem ] if verified_chain_as_pem else None # Keep the OCSP response if the validation was successful and a response was returned if _ocsp_response: ocsp_response = _ocsp_response # Store the returned verify string for each trust store path_validation_result_list.append(PathValidationResult(trust_store, verified_chain, validation_result)) # Store thread pool errors last_exception = None for (job, exception) in thread_pool.get_error(): (_, (_, trust_store)) = job path_validation_error_list.append(PathValidationError(trust_store, exception)) last_exception = exception thread_pool.join() if len(path_validation_error_list) == len(final_trust_store_list): # All connections failed unexpectedly; raise an exception instead of returning a result raise last_exception # type: ignore if not received_chain: raise ValueError('Error: Could not retrieve the server certificate chain') # All done return CertificateInfoScanResult( server_info, scan_command, received_chain, path_validation_result_list, path_validation_error_list, ocsp_response )
def test_get_default(self): repo = TrustStoresRepository.get_default() assert repo.get_main_store() assert len(repo.get_all_stores()) == 5
def sslyzeScan(threadname, connection, url): logfile_connection = 'log/' + scriptname + '-error-connections.log' logfile_other = 'log/' + scriptname + '-error-other.log' logfile_scan = 'log/' + scriptname + '-error-other.log' has_ip = 0 is_reachable = 0 has_ip = getIP(url) if has_ip: try: server_tester = ServerConnectivityTester(hostname=url) server_info = server_tester.perform() is_reachable = 1 except ServerConnectivityError: with open(logfile_connection, 'a') as log: log.write('Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not connect to host: ' + url + '\n') except Exception: with open(logfile_other, 'a') as log: log.write('Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Thrown error for host: ' + url + '\n') else: with open(logfile_connection, 'a') as log: log.write('Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not resolve host: ' + url + '\n') if (is_reachable): concurrent_scanner = ConcurrentScanner() concurrent_scanner.queue_scan_command(server_info, Sslv20ScanCommand()) concurrent_scanner.queue_scan_command(server_info, Sslv30ScanCommand()) concurrent_scanner.queue_scan_command(server_info, Tlsv10ScanCommand()) concurrent_scanner.queue_scan_command(server_info, Tlsv11ScanCommand()) concurrent_scanner.queue_scan_command(server_info, Tlsv12ScanCommand()) concurrent_scanner.queue_scan_command(server_info, Tlsv13ScanCommand()) concurrent_scanner.queue_scan_command(server_info, HeartbleedScanCommand()) concurrent_scanner.queue_scan_command(server_info, HttpHeadersScanCommand()) concurrent_scanner.queue_scan_command(server_info, CertificateInfoScanCommand()) # Process the results for scan_result in concurrent_scanner.get_results(): # Sometimes a scan command can unexpectedly fail (as a bug); it is returned as a PluginRaisedExceptionResult if isinstance(scan_result, PluginRaisedExceptionScanResult): with open(logfile_scan, 'a') as log: log.write('Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', Scan command failed: {}'.format( scan_result.as_text()) + '\n') if isinstance(scan_result.scan_command, Sslv20ScanCommand): ssl_version = "sslv2" try: if len(scan_result.accepted_cipher_list) == 0: supports_sslv2 = 0 else: supports_sslv2 = 1 for cipher in scan_result.accepted_cipher_list: cipher = (u'{}'.format(cipher.name)) sql_command = ( 'insert into ' + tbl_supported_ciphers + '(url,cipher,version) values (%s,%s,%s)') sql_data = (url, cipher, ssl_version) cur0 = connection.cursor() cur0.execute(sql_command, sql_data) connection.commit() cur0.close() except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get sslv2 attributes for host: ' + url + '\n') supports_sslv2 = 0 if isinstance(scan_result.scan_command, Sslv30ScanCommand): ssl_version = "sslv3" try: if len(scan_result.accepted_cipher_list) == 0: supports_sslv3 = 0 else: supports_sslv3 = 1 for cipher in scan_result.accepted_cipher_list: cipher = (u'{}'.format(cipher.name)) sql_command = ( 'insert into ' + tbl_supported_ciphers + '(url,cipher,version) values (%s,%s,%s)') sql_data = (url, cipher, ssl_version) cur0 = connection.cursor() cur0.execute(sql_command, sql_data) connection.commit() cur0.close() except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get sslv3 attributes for host: ' + url + '\n') supports_sslv3 = 0 if isinstance(scan_result.scan_command, Tlsv10ScanCommand): ssl_version = "tlsv10" try: if len(scan_result.accepted_cipher_list) == 0: supports_tlsv10 = 0 else: supports_tlsv10 = 1 for cipher in scan_result.accepted_cipher_list: cipher = (u'{}'.format(cipher.name)) sql_command = ( 'insert into ' + tbl_supported_ciphers + '(url,cipher,version) values (%s,%s,%s)') sql_data = (url, cipher, ssl_version) cur0 = connection.cursor() cur0.execute(sql_command, sql_data) connection.commit() cur0.close() except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get tlsv10 attributes for host: ' + url + '\n') supports_tlsv10 = 0 if isinstance(scan_result.scan_command, Tlsv11ScanCommand): ssl_version = "tlsv11" try: if len(scan_result.accepted_cipher_list) == 0: supports_tlsv11 = 0 else: supports_tlsv11 = 1 for cipher in scan_result.accepted_cipher_list: cipher = (u'{}'.format(cipher.name)) sql_command = ( 'insert into ' + tbl_supported_ciphers + '(url,cipher,version) values (%s,%s,%s)') sql_data = (url, cipher, ssl_version) cur0 = connection.cursor() cur0.execute(sql_command, sql_data) connection.commit() cur0.close() except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get tlsv11 attributes for host: ' + url + '\n') supports_tlsv11 = 0 if isinstance(scan_result.scan_command, Tlsv12ScanCommand): ssl_version = "tlsv12" try: if len(scan_result.accepted_cipher_list) == 0: supports_tlsv12 = 0 else: supports_tlsv12 = 1 for cipher in scan_result.accepted_cipher_list: cipher = (u'{}'.format(cipher.name)) sql_command = ( 'insert into ' + tbl_supported_ciphers + '(url,cipher,version) values (%s,%s,%s)') sql_data = (url, cipher, ssl_version) cur0 = connection.cursor() cur0.execute(sql_command, sql_data) connection.commit() cur0.close() except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get tlsv12 attributes for host: ' + url + '\n') supports_tlsv12 = 0 if isinstance(scan_result.scan_command, Tlsv13ScanCommand): ssl_version = "tlsv13" try: if len(scan_result.accepted_cipher_list) == 0: supports_tlsv13 = 0 else: supports_tlsv13 = 1 for cipher in scan_result.accepted_cipher_list: cipher = (u'{}'.format(cipher.name)) sql_command = ( 'insert into ' + tbl_supported_ciphers + '(url,cipher,version) values (%s,%s,%s)') sql_data = (url, cipher, ssl_version) cur0 = connection.cursor() cur0.execute(sql_command, sql_data) connection.commit() cur0.close() except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get tlsv13 attributes for host: ' + url + '\n') supports_tlsv13 = 0 if isinstance(scan_result.scan_command, HeartbleedScanCommand): vulnerable_heartbleed = 0 try: if (scan_result.is_vulnerable_to_heartbleed): vulnerable_heartbleed = 1 except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get heartbleed attribute for host: ' + url + '\n') vulnerable_heartbleed = 0 if isinstance(scan_result.scan_command, HttpHeadersScanCommand): hsts_preload_set = 0 hsts_include_subdomains_set = 0 hsts_max_age_set = 0 hsts_supported = 0 hpkp_supported = 0 try: if (scan_result.hsts_header): hsts_supported = 1 if (scan_result.hsts_header.preload): hsts_preload_set = 1 if (scan_result.hsts_header.include_subdomains ) == True: hsts_include_subdomains_set = 1 hsts_max_age_set = scan_result.hsts_header.max_age except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get hsts attributes for host: ' + url + '\n') hsts_preload_set = 0 hsts_include_subdomains_set = 0 hsts_max_age_set = 0 hsts_supported = 0 try: if (scan_result.hpkp_header): hpkp_supported = 1 except AttributeError: hpkp_supported = 0 if isinstance(scan_result.scan_command, CertificateInfoScanCommand): chain_is_trusted = 0 try: if (scan_result.verified_certificate_chain): chain_is_trusted = 1 except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get hpkp attributes for host: ' + url + '\n') chain_is_trusted = 0 cert_matches_hostname = 0 cert_is_ev = False try: CertificateUtils.matches_hostname( scan_result.certificate_chain[0], server_info.tls_server_name_indication) cert_matches_hostname = 1 except CertificateError: cert_matches_hostname = 0 except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get certificate_chain attribute for host: ' + url + '\n') try: cert_is_ev = TrustStoresRepository.get_default( ).get_main_store().is_extended_validation( scan_result.certificate_chain[0]) except AttributeError: with open(logfile_scan, 'a') as log: log.write( 'Error, ' + time.strftime("%Y-%m-%d %H:%M:%S") + ', ' + threadname + ': Could not get extended_validation attribute for host: ' + url + '\n') sql_cmd = ( 'insert into ' + tbl_ssl_options + '(url,heartbleed_vulnerable,hsts_supported,hsts_preload_set,hsts_include_subdomains_set,hsts_max_age_set,hpkp_supported,chain_is_trusted,match_hostname,is_ev,sslv2,sslv3,tlsv10,tlsv11,tlsv12,tlsv13) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)' ) sql_dat = (url, vulnerable_heartbleed, hsts_supported, hsts_preload_set, hsts_include_subdomains_set, hsts_max_age_set, hpkp_supported, chain_is_trusted, cert_matches_hostname, cert_is_ev, supports_sslv2, supports_sslv3, supports_tlsv10, supports_tlsv11, supports_tlsv12, supports_tlsv13) cur = connection.cursor() cur.execute(sql_cmd, sql_dat) connection.commit() cur.close()
def process_task( self, server_info: ServerConnectivityInfo, scan_command: PluginScanCommand) -> "HttpHeadersScanResult": if not isinstance(scan_command, HttpHeadersScanCommand): raise ValueError("Unexpected scan command") if server_info.tls_wrapped_protocol not in [ TlsWrappedProtocolEnum.PLAIN_TLS, TlsWrappedProtocolEnum.HTTPS ]: raise ValueError( "Cannot test for HTTP headers on a StartTLS connection.") # Perform the SSL handshake mozilla_store = TrustStoresRepository.get_default().get_main_store() ssl_connection = server_info.get_preconfigured_ssl_connection( ssl_verify_locations=mozilla_store.path) try: ssl_connection.connect() try: verified_chain_as_pem = ssl_connection.ssl_client.get_verified_chain( ) except CouldNotBuildVerifiedChain: verified_chain_as_pem = None # Send an HTTP GET request to the server ssl_connection.ssl_client.write( HttpRequestGenerator.get_request(host=server_info.hostname)) # We do not follow redirections because the security headers must be set on the first page according to # https://hstspreload.appspot.com/: # "If you are serving an additional redirect from your HTTPS site, that redirect must still have the HSTS # header (rather than the page it redirects to)." http_response = HttpResponseParser.parse_from_ssl_connection( ssl_connection.ssl_client) finally: ssl_connection.close() if http_response.version == 9: # HTTP 0.9 => Probably not an HTTP response raise ValueError("Server did not return an HTTP response") # Parse the certificate chain verified_chain = ([ load_pem_x509_certificate(cert_as_pem.encode("ascii"), backend=default_backend()) for cert_as_pem in verified_chain_as_pem ] if verified_chain_as_pem else None) # Parse each header hsts_header = StrictTransportSecurityHeader.from_http_response( http_response) expect_ct_header = ExpectCtHeader.from_http_response(http_response) hpkp_header = PublicKeyPinsHeader.from_http_response(http_response) hpkp_report_only_header = PublicKeyPinsReportOnlyHeader.from_http_response( http_response) return HttpHeadersScanResult( server_info, scan_command, hsts_header, hpkp_header, hpkp_report_only_header, expect_ct_header, verified_chain, )
def parse_command_line( self ) -> Tuple[List[ServerConnectivityTester], List[ServerStringParsingError], Any]: """Parses the command line used to launch SSLyze. """ (args_command_list, args_target_list) = self._parser.parse_args() if args_command_list.update_trust_stores: # Just update the trust stores and do nothing TrustStoresRepository.update_default() raise TrustStoresUpdateCompleted() # Handle the --targets_in command line and fill args_target_list if args_command_list.targets_in: if args_target_list: raise CommandLineParsingError( "Cannot use --targets_list and specify targets within the command line." ) try: # Read targets from a file with open(args_command_list.targets_in) as f: for target in f.readlines(): if target.strip(): # Ignore empty lines if not target.startswith( "#"): # Ignore comment lines args_target_list.append(target.strip()) except IOError: raise CommandLineParsingError( "Can't read targets from input file '{}.".format( args_command_list.targets_in)) if not args_target_list: raise CommandLineParsingError("No targets to scan.") # Handle the --regular command line parameter as a shortcut if self._parser.has_option("--regular"): if getattr(args_command_list, "regular"): setattr(args_command_list, "regular", False) for cmd in self.REGULAR_CMD: setattr(args_command_list, cmd, True) # Sanity checks on the command line options # Prevent --quiet and --xml_out - if args_command_list.xml_file and args_command_list.xml_file == "-" and args_command_list.quiet: raise CommandLineParsingError( "Cannot use --quiet with --xml_out -.") # Prevent --quiet and --json_out - if args_command_list.json_file and args_command_list.json_file == "-" and args_command_list.quiet: raise CommandLineParsingError( "Cannot use --quiet with --json_out -.") # Prevent --xml_out - and --json_out - if (args_command_list.json_file and args_command_list.json_file == "-" and args_command_list.xml_file and args_command_list.xml_file == "-"): raise CommandLineParsingError( "Cannot use --xml_out - with --json_out -.") # Sanity checks on the client cert options client_auth_creds = None if bool(args_command_list.cert) ^ bool(args_command_list.key): raise CommandLineParsingError( "No private key or certificate file were given. See --cert and --key." ) elif args_command_list.cert: # Private key formats if args_command_list.keyform == "DER": key_type = OpenSslFileTypeEnum.ASN1 elif args_command_list.keyform == "PEM": key_type = OpenSslFileTypeEnum.PEM else: raise CommandLineParsingError( "--keyform should be DER or PEM.") # Let's try to open the cert and key files try: client_auth_creds = ClientAuthenticationCredentials( args_command_list.cert, args_command_list.key, key_type, args_command_list.keypass) except ValueError as e: raise CommandLineParsingError( "Invalid client authentication settings: {}.".format( e.args[0])) # HTTP CONNECT proxy http_tunneling_settings = None if args_command_list.https_tunnel: try: http_tunneling_settings = HttpConnectTunnelingSettings.from_url( args_command_list.https_tunnel) except ValueError as e: raise CommandLineParsingError( "Invalid proxy URL for --https_tunnel: {}.".format( e.args[0])) # STARTTLS tls_wrapped_protocol = TlsWrappedProtocolEnum.PLAIN_TLS if args_command_list.starttls: if args_command_list.starttls not in self.START_TLS_PROTOCOLS: raise CommandLineParsingError(self.START_TLS_USAGE) else: # StartTLS was specified if args_command_list.starttls in self.STARTTLS_PROTOCOL_DICT.keys( ): # Protocol was given in the command line tls_wrapped_protocol = self.STARTTLS_PROTOCOL_DICT[ args_command_list.starttls] # Create the server connectivity tester for each specified servers # A limitation when using the command line is that only one client_auth_credentials and http_tunneling_settings # can be specified, for all the servers to scan good_server_list = [] bad_server_list = [] for server_string in args_target_list: try: hostname, ip_address, port = CommandLineServerStringParser.parse_server_string( server_string) except ServerStringParsingError as e: # Will happen if the server string is malformed bad_server_list.append(e) continue try: # TODO(AD): Unicode hostnames may fail on Python2 # hostname = hostname.decode('utf-8') server_info = ServerConnectivityTester( hostname=hostname, port=port, ip_address=ip_address, tls_wrapped_protocol=tls_wrapped_protocol, tls_server_name_indication=args_command_list.sni, xmpp_to_hostname=args_command_list.xmpp_to, client_auth_credentials=client_auth_creds, http_tunneling_settings=http_tunneling_settings, ) good_server_list.append(server_info) except ValueError as e: # Will happen for example if xmpp_to is specified for a non-XMPP connection raise CommandLineParsingError(e.args[0]) # Command line hacks # Handle --starttls=auto now that we parsed the server strings if args_command_list.starttls == "auto": for server_info in good_server_list: # We use the port number to deduce the protocol if server_info.port in self.STARTTLS_PROTOCOL_DICT.keys(): server_info.tls_wrapped_protocol = self.STARTTLS_PROTOCOL_DICT[ server_info.port] # Handle --http_get now that we parsed the server strings # Doing it here is hacky as the option is defined within PluginOpenSSLCipherSuites if args_command_list.http_get: for server_info in good_server_list: if server_info.port == 443: server_info.tls_wrapped_protocol = TlsWrappedProtocolEnum.HTTPS return good_server_list, bad_server_list, args_command_list
def as_xml(self): xml_output = Element(self.scan_command.get_cli_argument(), title=self.scan_command.get_title()) # Certificate chain cert_chain_attrs = { 'isChainOrderValid': str(self.is_certificate_chain_order_valid), 'suppliedServerNameIndication': self.server_info.tls_server_name_indication, 'containsAnchorCertificate': str(False) if not self.has_anchor_in_certificate_chain else str(True), 'hasMustStapleExtension': str(self.certificate_has_must_staple_extension), 'includedSctsCount': str(self.certificate_included_scts_count), } cert_chain_xml = Element('receivedCertificateChain', attrib=cert_chain_attrs) for cert_xml in self._certificate_chain_to_xml(self.certificate_chain): cert_chain_xml.append(cert_xml) xml_output.append(cert_chain_xml) # Trust trust_validation_xml = Element('certificateValidation') # Hostname validation host_validation_xml = Element( 'hostnameValidation', serverHostname=self.server_info.tls_server_name_indication, certificateMatchesServerHostname=str( self.certificate_matches_hostname)) trust_validation_xml.append(host_validation_xml) # Path validation that was successful for path_result in self.path_validation_result_list: path_attrib_xml = { 'usingTrustStore': path_result.trust_store.name, 'trustStoreVersion': path_result.trust_store.version, 'validationResult': path_result.verify_string } # Things we only do with the Mozilla store: EV certs if self.is_leaf_certificate_ev and TrustStoresRepository.get_main( ) == path_result.trust_store: path_attrib_xml['isExtendedValidationCertificate'] = str( self.is_leaf_certificate_ev) path_valid_xml = Element('pathValidation', attrib=path_attrib_xml) trust_validation_xml.append(path_valid_xml) # Path validation that ran into errors for path_error in self.path_validation_error_list: error_txt = 'ERROR: {}'.format(path_error.error_message) path_attrib_xml = { 'usingTrustStore': path_result.trust_store.name, 'trustStoreVersion': path_result.trust_store.version, 'error': error_txt } trust_validation_xml.append( Element('pathValidation', attrib=path_attrib_xml)) # Verified chain if self.verified_certificate_chain: verified_cert_chain_xml = Element( 'verifiedCertificateChain', { 'hasSha1SignedCertificate': str(self.has_sha1_in_certificate_chain), 'suppliedServerNameIndication': self.server_info.tls_server_name_indication, 'successfulTrustStore': self.successful_trust_store.name, 'hasMustStapleExtension': str(self.certificate_has_must_staple_extension), 'includedSctsCount': str(self.certificate_included_scts_count), }) for cert_xml in self._certificate_chain_to_xml( self.verified_certificate_chain): verified_cert_chain_xml.append(cert_xml) trust_validation_xml.append(verified_cert_chain_xml) xml_output.append(trust_validation_xml) # OCSP Stapling ocsp_xml = Element( 'ocspStapling', attrib={ 'isSupported': 'False' if self.ocsp_response is None else 'True' }) if self.ocsp_response: if self.ocsp_response_status != OcspResponseStatusEnum.SUCCESSFUL: ocsp_resp_xmp = Element( 'ocspResponse', attrib={'status': self.ocsp_response_status.name}) else: ocsp_resp_xmp = Element('ocspResponse', attrib={ 'isTrustedByMozillaCAStore': str(self.is_ocsp_response_trusted), 'status': self.ocsp_response_status.name }) responder_xml = Element('responderID') responder_xml.text = self.ocsp_response['responderID'] ocsp_resp_xmp.append(responder_xml) produced_xml = Element('producedAt') produced_xml.text = self.ocsp_response['producedAt'] ocsp_resp_xmp.append(produced_xml) ocsp_xml.append(ocsp_resp_xmp) xml_output.append(ocsp_xml) # All done return xml_output
def test_update_default(self): repo = TrustStoresRepository.update_default() assert repo.get_main_store() assert len(repo.get_all_stores()) == 5
def test_get_default(self): repo = TrustStoresRepository.get_default() self.assertTrue(repo.get_main_store()) self.assertGreater(len(repo.get_all_stores()), 5)
def as_text(self): text_output = [self._format_title(self.COMMAND_TITLE)] text_output.extend(self._get_basic_certificate_text()) # Trust section text_output.extend(['', self._format_title('Certificate - Trust')]) # Hostname validation server_name_indication = self.server_info.tls_server_name_indication if self.server_info.tls_server_name_indication != self.server_info.hostname: text_output.append( self._format_field("SNI enabled with virtual domain:", server_name_indication)) hostname_validation_text = 'OK - Certificate matches {hostname}'.format(hostname=server_name_indication) \ if self.certificate_matches_hostname \ else 'FAILED - Certificate does NOT match {hostname}'.format(hostname=server_name_indication) text_output.append( self._format_field('Hostname Validation:', hostname_validation_text)) # Path validation that was successfully tested for path_result in self.path_validation_result_list: if path_result.is_certificate_trusted: # EV certs - Only Mozilla supported for now ev_txt = '' if self.is_leaf_certificate_ev and TrustStoresRepository.get_main( ) == path_result.trust_store: ev_txt = ', Extended Validation' path_txt = 'OK - Certificate is trusted{}'.format(ev_txt) else: path_txt = 'FAILED - Certificate is NOT Trusted: {}'.format( path_result.verify_string) text_output.append( self._format_field( self.TRUST_FORMAT.format( store_name=path_result.trust_store.name, store_version=path_result.trust_store.version), path_txt)) # Path validation that ran into errors for path_error in self.path_validation_error_list: error_txt = 'ERROR: {}'.format(path_error.error_message) text_output.append( self._format_field( self.TRUST_FORMAT.format( store_name=path_result.trust_store.name, store_version=path_result.trust_store.version), error_txt)) # Print the Common Names within the certificate chain cns_in_certificate_chain = [ CertificateUtils.get_printable_name(cert.subject) for cert in self.certificate_chain ] text_output.append( self._format_field('Received Chain:', ' --> '.join(cns_in_certificate_chain))) # Print the Common Names within the verified certificate chain if validation was successful if self.verified_certificate_chain: cns_in_certificate_chain = [ CertificateUtils.get_printable_name(cert.subject) for cert in self.verified_certificate_chain ] verified_chain_txt = ' --> '.join(cns_in_certificate_chain) else: verified_chain_txt = self.NO_VERIFIED_CHAIN_ERROR_TXT text_output.append( self._format_field('Verified Chain:', verified_chain_txt)) if self.verified_certificate_chain: chain_with_anchor_txt = 'OK - Anchor certificate not sent' if not self.has_anchor_in_certificate_chain \ else 'WARNING - Received certificate chain contains the anchor certificate' else: chain_with_anchor_txt = self.NO_VERIFIED_CHAIN_ERROR_TXT text_output.append( self._format_field('Received Chain Contains Anchor:', chain_with_anchor_txt)) chain_order_txt = 'OK - Order is valid' if self.is_certificate_chain_order_valid \ else 'FAILED - Certificate chain out of order!' text_output.append( self._format_field('Received Chain Order:', chain_order_txt)) if self.verified_certificate_chain: sha1_text = 'OK - No SHA1-signed certificate in the verified certificate chain' \ if not self.has_sha1_in_certificate_chain \ else 'INSECURE - SHA1-signed certificate in the verified certificate chain' else: sha1_text = self.NO_VERIFIED_CHAIN_ERROR_TXT text_output.append( self._format_field('Verified Chain contains SHA1:', sha1_text)) # OCSP stapling text_output.extend( ['', self._format_title('Certificate - OCSP Stapling')]) if self.ocsp_response is None: text_output.append( self._format_field( '', 'NOT SUPPORTED - Server did not send back an OCSP response.' )) else: ocsp_trust_txt = 'OK - Response is trusted' \ if self.is_ocsp_response_trusted \ else 'FAILED - Response is NOT trusted' ocsp_resp_txt = [ self._format_field('OCSP Response Status:', self.ocsp_response['responseStatus']), self._format_field('Validation w/ Mozilla Store:', ocsp_trust_txt), self._format_field('Responder Id:', self.ocsp_response['responderID']) ] if 'successful' in self.ocsp_response['responseStatus']: ocsp_resp_txt.extend([ self._format_field( 'Cert Status:', self.ocsp_response['responses'][0]['certStatus']), self._format_field( 'Cert Serial Number:', self.ocsp_response['responses'] [0]['certID']['serialNumber']), self._format_field( 'This Update:', self.ocsp_response['responses'][0]['thisUpdate']), self._format_field( 'Next Update:', self.ocsp_response['responses'][0]['nextUpdate']) ]) text_output.extend(ocsp_resp_txt) # All done return text_output
def test_get_default(self): repo = TrustStoresRepository.get_default() self.assertTrue(repo.get_main_store()) self.assertEqual(len(repo.get_all_stores()), 5)
def __init__( self, server_info: ServerConnectivityInfo, scan_command: CertificateInfoScanCommand, certificate_chain: List[Certificate], path_validation_result_list: List[PathValidationResult], path_validation_error_list: List[PathValidationError], ocsp_response: OcspResponse ) -> None: super().__init__(server_info, scan_command) # Find the first trust store that successfully validated the certificate chain self.successful_trust_store = None # Sort the path_validation_result_list so the same successful_trust_store always get picked for a given server # because threading timings change the order of path_validation_result_list def sort_function(path_validation: PathValidationResult) -> str: return path_validation.trust_store.name.lower() path_validation_result_list.sort(key=sort_function) for path_result in path_validation_result_list: if path_result.is_certificate_trusted: self.successful_trust_store = path_result.trust_store self.ocsp_response = None self.is_ocsp_response_trusted = None self.ocsp_response_status = None if ocsp_response: self.ocsp_response_status = ocsp_response.status # We only keep the dictionary as a nassl.OcspResponse is not pickable self.ocsp_response = ocsp_response.as_dict() if self.successful_trust_store and self.ocsp_response_status == OcspResponseStatusEnum.SUCCESSFUL: try: ocsp_response.verify(self.successful_trust_store.path) self.is_ocsp_response_trusted = True except OcspResponseNotTrustedError: self.is_ocsp_response_trusted = False self.certificate_chain = certificate_chain # Check if it is EV - we only have the EV OIDs for Mozilla self.is_leaf_certificate_ev = TrustStoresRepository.get_default().get_main_store().is_extended_validation( self.certificate_chain[0] ) # Look for the Must-Staple extension has_must_staple = CertificateUtils.has_ocsp_must_staple_extension(self.certificate_chain[0]) self.certificate_has_must_staple_extension = has_must_staple # Look for the certificate transparency extension self.certificate_included_scts_count = CertificateUtils.count_scts_in_sct_extension(self.certificate_chain[0]) # Try to build the verified chain self.verified_certificate_chain: List[Certificate] = [] self.is_certificate_chain_order_valid = True if self.successful_trust_store: try: self.verified_certificate_chain = self.successful_trust_store.build_verified_certificate_chain( self.certificate_chain ) except InvalidCertificateChainOrderError: self.is_certificate_chain_order_valid = False except AnchorCertificateNotInTrustStoreError: pass self.has_anchor_in_certificate_chain = None if self.verified_certificate_chain: self.has_anchor_in_certificate_chain = self.verified_certificate_chain[-1] in self.certificate_chain self.path_validation_result_list = path_validation_result_list self.path_validation_error_list = path_validation_error_list try: CertificateUtils.matches_hostname(certificate_chain[0], server_info.tls_server_name_indication) self.certificate_matches_hostname = True except CertificateError: self.certificate_matches_hostname = False # Check if a SHA1-signed certificate is in the chain # Root certificates can still be signed with SHA1 so we only check leaf and intermediate certificates self.has_sha1_in_certificate_chain = None if self.verified_certificate_chain: self.has_sha1_in_certificate_chain = False for cert in self.verified_certificate_chain[:-1]: if isinstance(cert.signature_hash_algorithm, hashes.SHA1): self.has_sha1_in_certificate_chain = True break # Check if this is a distrusted Symantec-issued chain self.symantec_distrust_timeline = _SymantecDistructTester.get_distrust_timeline(self.verified_certificate_chain)
def test_update_default(self): repo = TrustStoresRepository.update_default() self.assertTrue(repo.get_main_store()) self.assertGreater(len(repo.get_all_stores()), 5)