def _command_resum(self, target): """ Tests the server for session resumption support using session IDs and TLS session tickets (RFC 5077). """ NB_THREADS = 5 MAX_RESUM = 5 thread_pool = ThreadPool() for _ in xrange(MAX_RESUM): # Test 5 resumptions with session IDs thread_pool.add_job( (self._resume_with_session_id, (target, ), 'session_id')) thread_pool.start(NB_THREADS) # Test TLS tickets support while threads are running try: (ticket_supported, ticket_reason) = self._resume_with_session_ticket(target) ticket_error = None except Exception as e: ticket_error = str(e.__class__.__module__) + '.' + \ str(e.__class__.__name__) + ' - ' + str(e) # Format session ID results (txt_resum, xml_resum) = self._format_resum_id_results(thread_pool, MAX_RESUM) if ticket_error: ticket_txt = 'Error: ' + ticket_error else: ticket_txt = 'Supported' if ticket_supported \ else 'Not Supported - ' + ticket_reason+'.' cmd_title = 'Session Resumption' txt_result = [self.PLUGIN_TITLE_FORMAT.format(cmd_title)] RESUM_FORMAT = ' {0:<27} {1}' txt_result.append( RESUM_FORMAT.format('With Session IDs:', txt_resum[0])) txt_result.extend(txt_resum[1:]) txt_result.append( RESUM_FORMAT.format('With TLS Session Tickets:', ticket_txt)) # XML output xml_resum_ticket_attr = {} if ticket_error: xml_resum_ticket_attr['error'] = ticket_error else: xml_resum_ticket_attr['isSupported'] = str(ticket_supported) if not ticket_supported: xml_resum_ticket_attr['reason'] = ticket_reason xml_resum_ticket = Element('sessionResumptionWithTLSTickets', attrib=xml_resum_ticket_attr) xml_result = Element('resum', title=cmd_title) xml_result.append(xml_resum) xml_result.append(xml_resum_ticket) thread_pool.join() return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command, args): output_format = ' {0:<25} {1}' ctSSL_initialize(zlib=True) ssl_ctx = SSL_CTX.SSL_CTX('tlsv1') # sslv23 hello will fail for specific servers such as post.craigslist.org ssl_connect = SSLyzeSSLConnection(self._shared_settings, target,ssl_ctx, hello_workaround=True) try: # Perform the SSL handshake ssl_connect.connect() compression_status = ssl_connect._ssl.get_current_compression() finally: ssl_connect.close() ctSSL_cleanup() # Text output if compression_status: comp_txt = 'Enabled ' + compression_status comp_xml = {'isSupported':'True','type':compression_status.strip('()')} else: comp_txt = 'Disabled' comp_xml = {'isSupported':'False'} cmd_title = 'Compression' txt_result = [self.PLUGIN_TITLE_FORMAT.format(cmd_title)] txt_result.append(output_format.format("Compression Support:", comp_txt)) # XML output xml_el = Element('compression', comp_xml) xml_result = Element(command, title = cmd_title) xml_result.append(xml_el) return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command, args): if self._shared_settings['starttls']: raise Exception('Cannot use --hsts with --starttls.') hsts_supported = self._get_hsts_header(target) if hsts_supported: hsts_timeout = hsts_supported hsts_supported = True # Text output cmd_title = 'HTTP Strict Transport Security' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title)] if hsts_supported: txt_result.append( self.FIELD_FORMAT("OK - HSTS header received:", hsts_timeout)) else: txt_result.append( self.FIELD_FORMAT( "NOT SUPPORTED - Server did not send an HSTS header.", "")) # XML output xml_hsts_attr = {'sentHstsHeader': str(hsts_supported)} if hsts_supported: xml_hsts_attr['hstsHeaderValue'] = hsts_timeout xml_hsts = Element('hsts', attrib=xml_hsts_attr) xml_result = Element('hsts', title=cmd_title) xml_result.append(xml_hsts) return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command, args): if self._shared_settings['starttls']: raise Exception('Cannot use --hsts with --starttls.') FIELD_FORMAT = ' {0:<35}{1}'.format hsts_supported = self._get_hsts_header(target) if hsts_supported: hsts_timeout = hsts_supported hsts_supported = True # Text output cmd_title = 'HTTP Strict Transport Security' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title)] if hsts_supported: txt_result.append(FIELD_FORMAT("Supported:", hsts_timeout)) else: txt_result.append(FIELD_FORMAT("Not supported.", "")) # XML output xml_hsts_attr = {'hsts_header_found': str(hsts_supported)} if hsts_supported: xml_hsts_attr['hsts_header'] = hsts_timeout xml_hsts = Element('hsts', attrib=xml_hsts_attr) xml_result = Element(self.__class__.__name__, command=command, title=cmd_title) xml_result.append(xml_hsts) return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command, args): ctSSL_initialize() try: (can_reneg, is_secure) = self._test_renegotiation(target) finally: ctSSL_cleanup() # Text output reneg_txt = 'Honored' if can_reneg else 'Rejected' secure_txt = 'Supported' if is_secure else 'Not supported' cmd_title = 'Session Renegotiation' txt_result = [self.PLUGIN_TITLE_FORMAT.format(cmd_title)] RENEG_FORMAT = ' {0:<35} {1}' txt_result.append( RENEG_FORMAT.format('Client-initiated Renegotiations:', reneg_txt)) txt_result.append( RENEG_FORMAT.format('Secure Renegotiation: ', secure_txt)) # XML output xml_reneg_attr = { 'canBeClientInitiated': str(can_reneg), 'isSecure': str(is_secure) } xml_reneg = Element('sessionRenegotiation', attrib=xml_reneg_attr) xml_result = Element(command, title=cmd_title) xml_result.append(xml_reneg) return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command): """ Connects to the target server and tries to get acceptable CAs for client cert """ (_, _, _, ssl_version) = target ssl_conn = create_sslyze_connection(target, self._shared_settings, ssl_version) res = [] try: # Perform the SSL handshake ssl_conn.connect() except ClientCertificateRequested: # The server asked for a client cert res = ssl_conn.get_client_CA_list() finally: ssl_conn.close() text_output = [self.PLUGIN_TITLE_FORMAT(self.CMD_TITLE)] if res: xml_output = Element(command, title=self.CMD_TITLE, isProvided="True") for ca in res: text_output.append(self.FIELD_FORMAT('', str(ca))) ca_xml = Element('ca') ca_xml.text = ca xml_output.append(ca_xml) else: xml_output = Element(command, title=self.CMD_TITLE, isProvided="False") return PluginBase.PluginResult(text_output, xml_output)
def process_task(self, target, command, args): if self._shared_settings['starttls']: raise Exception('Cannot use --httpredirect with --starttls.') self._target = target self._ip = target[1] self._timeout = self._shared_settings['timeout'] (status, redirect_header) = self._get_redirect_header() redirect_installed = False if redirect_header and status > 0: redirect_installed = True # Text output cmd_title = 'HTTP to HTTPS Redirect' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title)] if redirect_installed: txt_result.append(self.FIELD_FORMAT("OK - HTTP to HTTPS header received:", redirect_header)) else: txt_result.append(self.FIELD_FORMAT("NOT INSTALLED - Server did not send an HTTP to HTTPS redirect header.", "")) if status < 0: txt_result.append(self.FIELD_FORMAT(redirect_header, "")) # XML output xml_redirect_attr = {'isInstalled': str(redirect_installed)} if redirect_installed: xml_redirect_attr['location'] = redirect_header xml_redirect_attr['status'] = str(status) if status < 0: xml_redirect_attr['error'] = redirect_header xml_result = Element('HTTPredirect', title=cmd_title, attrib=xml_redirect_attr) return PluginBase.PluginResult(txt_result, xml_result)
def _command_resum_rate(self, target): """ Performs 100 session resumptions with the server in order to estimate the session resumption rate. """ # Create a thread pool and process the jobs NB_THREADS = 20 MAX_RESUM = 100 thread_pool = ThreadPool() for _ in xrange(MAX_RESUM): thread_pool.add_job((self._resume_with_session_id, (target, ))) thread_pool.start(NB_THREADS) # Format session ID results (txt_resum, xml_resum) = self._format_resum_id_results(thread_pool, MAX_RESUM) # Text output cmd_title = 'Resumption Rate with Session IDs' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title) + ' ' + txt_resum[0]] txt_result.extend(txt_resum[1:]) # XML output xml_result = Element('resum_rate', title=cmd_title) xml_result.append(xml_resum) thread_pool.join() return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command, args): OUT_FORMAT = ' {0:<25} {1}'.format sslConn = create_sslyze_connection(target, self._shared_settings) try: # Perform the SSL handshake sslConn.connect() compName = sslConn.get_current_compression_name() except ClientAuthenticationError: # The server asked for a client cert compName = sslConn.get_current_compression_name() finally: sslConn.close() # Text output if compName: compTxt = 'Enabled ' + compName compXml = {'isSupported': 'True', 'type': compName.strip('()')} else: compTxt = 'Disabled' compXml = {'isSupported': 'False'} cmdTitle = 'Compression' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] txtOutput.append(OUT_FORMAT("Compression Support:", compTxt)) # XML output xmlNode = Element('compression', compXml) xmlOutput = Element(command, title=cmdTitle) xmlOutput.append(xmlNode) return PluginBase.PluginResult(txtOutput, xmlOutput)
def process_task(self, target, command, args): (clientReneg, secureReneg) = self._test_renegotiation(target) # Text output clientTxt = 'VULNERABLE - Server honors client-initiated renegotiations' if clientReneg else 'OK - Rejected' secureTxt = 'OK - Supported' if secureReneg else 'VULNERABLE - Secure renegotiation not supported' cmdTitle = 'Session Renegotiation' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] txtOutput.append( self.FIELD_FORMAT('Client-initiated Renegotiations:', clientTxt)) txtOutput.append(self.FIELD_FORMAT('Secure Renegotiation:', secureTxt)) # XML output xmlReneg = Element('sessionRenegotiation', attrib={ 'canBeClientInitiated': str(clientReneg), 'isSecure': str(secureReneg) }) xmlOutput = Element(command, title=cmdTitle) xmlOutput.append(xmlReneg) return PluginBase.PluginResult(txtOutput, xmlOutput)
def process_task(self, target, command, args): (clientReneg, secureReneg) = self._test_renegotiation(target) # Text output clientTxt = 'Honored' if clientReneg else 'Rejected' secureTxt = 'Supported' if secureReneg else 'Not supported' cmdTitle = 'Session Renegotiation' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] outFormat = ' {0:<35}{1}'.format txtOutput.append( outFormat('Client-initiated Renegotiations:', clientTxt)) txtOutput.append(outFormat('Secure Renegotiation:', secureTxt)) # XML output xmlReneg = Element('sessionRenegotiation', attrib={ 'canBeClientInitiated': str(clientReneg), 'isSecure': str(secureReneg) }) xmlOutput = Element(command, title=cmdTitle) xmlOutput.append(xmlReneg) return PluginBase.PluginResult(txtOutput, xmlOutput)
class PluginHSTS(PluginBase.PluginBase): interface = PluginBase.PluginInterface(title="PluginHSTS", description=('')) interface.add_command( command="hsts", help="Checks support for HTTP Strict Transport Security " "(HSTS) by collecting any Strict-Transport-Security field present in " "the HTTP response sent back by the server.", dest=None) def process_task(self, target, command, args): if self._shared_settings['starttls']: raise Exception('Cannot use --hsts with --starttls.') output_format = ' {0:<25} {1}' hsts_supported = False hsts_timeout = "" (host, addr, port, sslVersion) = target connection = httplib.HTTPSConnection(host) try: connection.connect() connection.request("HEAD", "/", headers={"Connection": "close"}) response = connection.getresponse() headers = response.getheaders() for (field, data) in headers: if field == 'strict-transport-security': hsts_supported = True hsts_timeout = data except httplib.HTTPException as ex: print "Error: %s" % ex finally: connection.close() # Text output cmd_title = 'HSTS' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title)] if hsts_supported: txt_result.append(output_format.format("Supported:", hsts_timeout)) else: txt_result.append(output_format.format("Not supported.", "")) # XML output xml_hsts_attr = {'hsts_header_found': str(hsts_supported)} if hsts_supported: xml_hsts_attr['hsts_header'] = hsts_timeout xml_hsts = Element('hsts', attrib=xml_hsts_attr) xml_result = Element(self.__class__.__name__, command=command, title=cmd_title) xml_result.append(xml_hsts) return PluginBase.PluginResult(txt_result, xml_result)
class PluginHeartbleed(PluginBase.PluginBase): interface = PluginBase.PluginInterface("PluginHeartbleed", "") interface.add_command( command="heartbleed", help=( "Tests the server(s) for the OpenSSL Heartbleed vulnerability (experimental).")) def process_task(self, target, command, args): OUT_FORMAT = ' {0:<35}{1}'.format (host, ip, port, sslVersion) = target if sslVersion == SSLV23: # Could not determine the preferred SSL version - client cert was required ? sslVersion = TLSV1 # Default to TLS 1.0 target = (host, ip, port, sslVersion) sslConn = create_sslyze_connection(target, self._shared_settings) sslConn.sslVersion = sslVersion # Needed by the heartbleed payload # Awful hack #1: replace nassl.sslClient.do_handshake() with a heartbleed # checking SSL handshake so that all the SSLyze options # (startTLS, proxy, etc.) still work sslConn.do_handshake = new.instancemethod(do_handshake_with_heartbleed, sslConn, None) heartbleed = None try: # Perform the SSL handshake sslConn.connect() except HeartbleedSent: # Awful hack #2: directly read the underlying network socket heartbleed = sslConn._sock.recv(16381) finally: sslConn.close() # Text output if heartbleed is None: raise Exception("Error: connection failed.") elif '\x01\x01\x01\x01\x01\x01\x01\x01\x01' in heartbleed: # Server replied with our hearbeat payload heartbleedTxt = 'VULNERABLE' heartbleedXml = 'True' else: heartbleedTxt = 'NOT vulnerable' heartbleedXml = 'False' cmdTitle = 'Heartbleed' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] txtOutput.append(OUT_FORMAT("OpenSSL Heartbleed:", heartbleedTxt)) # XML output xmlOutput = Element(command, title=cmdTitle) if heartbleed: xmlNode = Element('heartbleed', isVulnerable=heartbleedXml) xmlOutput.append(xmlNode) return PluginBase.PluginResult(txtOutput, xmlOutput)
def shorten_url(self, url): '''Return a shortened version of a URL passed in using bitly ''' try: return self.bitly.shorten(url)['url'] except: log.err('[Error]: bitly traceback: {}'.format( traceback.format_exc())) raise pb.CommandError(u'[Error]: Invalid URL', pm=True)
def ytSearch(self, args, irc): '''yt [search term(s)] -- Search YouTube and return the first result ''' try: if not args: raise pb.CommandError(u'[Error]: Missing YouTube search terms', pm=False) terms = u'+'.join(args) r = requests.get(self.YT_SEARCH.format(terms)) soup = BeautifulSoup(r.text) result = soup.find('div', { 'class': 'yt-lockup-content' }).find('a')['href'] url = u'https://youtube.com{}'.format(result) return u'{} | {}'.format(url, self.youtube_data(url)) except: log.err('[Error]: yt {}'.format(sys.exc_info()[0])) raise pb.CommandError(u'[Error]: Could not contact YouTube', pm=True)
class PluginCompression(PluginBase.PluginBase): interface = PluginBase.PluginInterface(title="PluginCompression", description="") interface.add_command( command="compression", help="Tests the server for Zlib compression support.", dest=None) def process_task(self, target, command, args): OUT_FORMAT = ' {0:<35}{1}'.format sslConn = create_sslyze_connection(target, self._shared_settings) # Make sure OpenSSL was built with support for compression to avoid false negatives if 'zlib compression' not in sslConn.get_available_compression_methods( ): raise RuntimeError( 'OpenSSL was not built with support for zlib / compression. Did you build nassl yourself ?' ) try: # Perform the SSL handshake sslConn.connect() compName = sslConn.get_current_compression_method() except ClientCertificateRequested: # The server asked for a client cert compName = sslConn.get_current_compression_method() finally: sslConn.close() # Text output if compName: compTxt = 'Supported' else: compTxt = 'Disabled' cmdTitle = 'Compression' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] txtOutput.append(OUT_FORMAT("DEFLATE Compression:", compTxt)) # XML output xmlOutput = Element(command, title=cmdTitle) if compName: xmlNode = Element('compressionMethod', type="DEFLATE") xmlOutput.append(xmlNode) return PluginBase.PluginResult(txtOutput, xmlOutput)
def joke(self, args, irc): '''(joke) -- Random joke from goodbadjokes.com ''' try: r = requests.get(self.JOKE_URL) if r.status_code != 200: log.err('Error]: Status code of {} for joke'.format(r.status_code)) return soup = BeautifulSoup(r.text) joke = soup.find('span', {'class': 'joke-content'}) q = joke.find('dt').text a = joke.find('dd').text return '{} {}'.format(q, a) except: log.err('[Error]: {}'.format(sys.exc_info()[0])) raise pb.CommandError('[Error]: Cannot contact goodbadjokes.com', pm=False)
def process_task(self, target, command, arg): # Get the certificate and validate it against all the trust stores (cert, verify_result) = self._get_cert(target, TRUST_STORE_PATHS) # Results formatting # Text output - display each trust store and the validation result cmdTitle = 'Multi Trust Store Validation' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] isCertTrusted = True fingerprint = cert.get_SHA1_fingerprint() txtOutput.append( self.FIELD_FORMAT('Certificate SHA1 Fingerprint:', fingerprint)) for trustStorePath in TRUST_STORE_PATHS: if verify_result[trustStorePath] != 'ok': isCertTrusted = False txtOutput.append( self.FIELD_FORMAT( "Validation w/ '" + os.path.split(trustStorePath)[1] + "': ", verify_result[trustStorePath])) # XML output. xmlOutput = Element(command, title=cmdTitle) trustStoresXml = Element('trustStoreList', isTrustedByAllTrustStores=str(isCertTrusted)) for trustStorePath in TRUST_STORE_PATHS: # Add the result of each trust store trustStoresXml.append( Element('trustStore', filePath=os.path.split(trustStorePath)[1], verifyResult=verify_result[trustStorePath])) xmlOutput.append(trustStoresXml) # Add the certificate certXml = Element('certificate', sha1Fingerprint=fingerprint) for (key, value) in cert.as_dict().items(): certXml.append(_keyvalue_pair_to_xml(key, value)) xmlOutput.append(certXml) return PluginBase.PluginResult(txtOutput, xmlOutput)
def process_task(self, target, command, args): if self._shared_settings['starttls']: raise Exception('Cannot use --hsts with --starttls.') hsts_header = self._get_hsts_header(target) hsts_supported = False if hsts_header: hsts_supported = True # Text output cmd_title = 'HTTP Strict Transport Security' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title)] if hsts_supported: txt_result.append( self.FIELD_FORMAT("OK - HSTS header received:", hsts_header)) else: txt_result.append( self.FIELD_FORMAT( "NOT SUPPORTED - Server did not send an HSTS header.", "")) # XML output xml_hsts_attr = {'isSupported': str(hsts_supported)} if hsts_supported: # Do some light parsing of the HSTS header hsts_header_split = hsts_header.split('max-age=')[1].split(';') hsts_max_age = hsts_header_split[0].strip() hsts_subdomains = False if len(hsts_header_split ) > 1 and 'includeSubdomains' in hsts_header_split[1]: hsts_subdomains = True xml_hsts_attr['maxAge'] = hsts_max_age xml_hsts_attr['includeSubdomains'] = str(hsts_subdomains) xml_hsts = Element('httpStrictTransportSecurity', attrib=xml_hsts_attr) xml_result = Element('hsts', title=cmd_title) xml_result.append(xml_hsts) return PluginBase.PluginResult(txt_result, xml_result)
def process_task(self, target, command, args): sslConn = create_sslyze_connection(target, self._shared_settings) # Make sure OpenSSL was built with support for compression to avoid false negatives if 'zlib compression' not in sslConn.get_available_compression_methods(): raise RuntimeError('OpenSSL was not built with support for zlib / compression. Did you build nassl yourself ?') try: # Perform the SSL handshake sslConn.connect() compName = sslConn.get_current_compression_method() except ClientCertificateRequested: # The server asked for a client cert compName = sslConn.get_current_compression_method() finally: sslConn.close() # Text output if compName: compTxt = 'VULNERABLE - Server supports Deflate compression' else: compTxt = 'OK - Compression disabled' cmdTitle = 'Deflate Compression' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] txtOutput.append(self.FIELD_FORMAT(compTxt, "")) # XML output xmlOutput = Element(command, title=cmdTitle) if compName: xmlNode = Element('compressionMethod', type="DEFLATE", isSupported="True") xmlOutput.append(xmlNode) else: xmlNode = Element('compressionMethod', type="DEFLATE", isSupported="False") xmlOutput.append(xmlNode) return PluginBase.PluginResult(txtOutput, xmlOutput)
class PluginCertInfo(PluginBase.PluginBase): interface = PluginBase.PluginInterface(title="PluginCertInfo", description='') interface.add_command( command="certinfo", help="Verifies the validity of the server(s) certificate(s) against " "various trust stores, checks for support for OCSP stapling, and " "prints relevant fields of " "the certificate. CERTINFO should be 'basic' or 'full'.", dest="certinfo") interface.add_option( option="ca_file", help="Local Certificate Authority file (in PEM format), to verify the " "validity of the server(s) certificate(s) against.", dest="ca_file") TRUST_FORMAT = '{store_name} CA Store ({store_version}):'.format def process_task(self, target, command, arg): if arg == 'basic': txt_output_generator = self._get_basic_text elif arg == 'full': txt_output_generator = self._get_full_text else: raise Exception("PluginCertInfo: Unknown command.") (host, _, _, _) = target thread_pool = ThreadPool() if 'ca_file' in self._shared_settings and self._shared_settings[ 'ca_file']: AVAILABLE_TRUST_STORES[self._shared_settings['ca_file']] = ( 'Custom --ca_file', 'N/A') for (store_path, _) in AVAILABLE_TRUST_STORES.iteritems(): # Try to connect with each trust store thread_pool.add_job((self._get_cert, (target, store_path))) # Start processing the jobs thread_pool.start(len(AVAILABLE_TRUST_STORES)) # Store the results as they come x509_cert_chain = [] (verify_dict, verify_dict_error, x509_cert, ocsp_response) = ({}, {}, None, None) for (job, result) in thread_pool.get_result(): (_, (_, store_path)) = job (x509_cert_chain, verify_str, ocsp_response) = result # Store the returned verify string for each trust store x509_cert = x509_cert_chain[ 0] # First cert is always the leaf cert store_info = AVAILABLE_TRUST_STORES[store_path] verify_dict[store_info] = verify_str if x509_cert is None: # This means none of the connections were successful. Get out for (job, exception) in thread_pool.get_error(): raise exception # Store thread pool errors for (job, exception) in thread_pool.get_error(): (_, (_, store_path)) = job error_msg = str( exception.__class__.__name__) + ' - ' + str(exception) store_info = AVAILABLE_TRUST_STORES[store_path] verify_dict_error[store_info] = error_msg thread_pool.join() # Results formatting # Text output - certificate info text_output = [self.PLUGIN_TITLE_FORMAT('Certificate - Content')] text_output.extend(txt_output_generator(x509_cert)) # Text output - trust validation text_output.extend( ['', self.PLUGIN_TITLE_FORMAT('Certificate - Trust')]) # Hostname validation if self._shared_settings['sni']: text_output.append( self.FIELD_FORMAT("SNI enabled with virtual domain:", self._shared_settings['sni'])) # TODO: Use SNI name for validation when --sni was used host_val_dict = { X509_NAME_MATCHES_SAN: 'OK - Subject Alternative Name matches', X509_NAME_MATCHES_CN: 'OK - Common Name matches', X509_NAME_MISMATCH: 'FAILED - Certificate does NOT match ' + host } text_output.append( self.FIELD_FORMAT("Hostname Validation:", host_val_dict[x509_cert.matches_hostname(host)])) # Path validation that was successful for ((store_name, store_version), verify_str) in verify_dict.iteritems(): verify_txt = 'OK - Certificate is trusted' if (verify_str in 'ok') \ else 'FAILED - Certificate is NOT Trusted: ' + verify_str # EV certs - Only Mozilla supported for now if (verify_str in 'ok') and ('Mozilla' in store_info): if self._is_ev_certificate(x509_cert): verify_txt += ', Extended Validation' text_output.append( self.FIELD_FORMAT( self.TRUST_FORMAT(store_name=store_name, store_version=store_version), verify_txt)) # Path validation that ran into errors for ((store_name, store_version), error_msg) in verify_dict_error.iteritems(): verify_txt = 'ERROR: ' + error_msg text_output.append( self.FIELD_FORMAT( self.TRUST_FORMAT(store_name=store_name, store_version=store_version), verify_txt)) # Print the Common Names within the certificate chain cns_in_cert_chain = [] for cert in x509_cert_chain: cert_identity = self._extract_subject_cn_or_oun(cert) cns_in_cert_chain.append(cert_identity) text_output.append( self.FIELD_FORMAT('Certificate Chain Received:', str(cns_in_cert_chain))) # Text output - OCSP stapling text_output.extend( ['', self.PLUGIN_TITLE_FORMAT('Certificate - OCSP Stapling')]) text_output.extend(self._get_ocsp_text(ocsp_response)) # XML output xml_output = Element(command, argument=arg, title='Certificate Information') # XML output - certificate chain: always return the full certificate for each cert in the chain cert_chain_xml = Element('certificateChain') # First add the leaf certificate cert_chain_xml.append( self._format_cert_to_xml(x509_cert_chain[0], 'leaf', self._shared_settings['sni'])) # Then add every other cert in the chain for cert in x509_cert_chain[1:]: cert_chain_xml.append( self._format_cert_to_xml(cert, 'intermediate', self._shared_settings['sni'])) xml_output.append(cert_chain_xml) # XML output - trust trust_validation_xml = Element('certificateValidation') # Hostname validation is_hostname_valid = 'False' if (x509_cert.matches_hostname(host) == X509_NAME_MISMATCH) else 'True' host_validation_xml = Element( 'hostnameValidation', serverHostname=host, certificateMatchesServerHostname=is_hostname_valid) trust_validation_xml.append(host_validation_xml) # Path validation - OK for ((store_name, store_version), verify_str) in verify_dict.iteritems(): path_attrib_xml = { 'usingTrustStore': store_name, 'trustStoreVersion': store_version, 'validationResult': verify_str } # EV certs - Only Mozilla supported for now if (verify_str in 'ok') and ('Mozilla' in store_info): path_attrib_xml['isExtendedValidationCertificate'] = str( self._is_ev_certificate(x509_cert)) trust_validation_xml.append( Element('pathValidation', attrib=path_attrib_xml)) # Path validation - Errors for ((store_name, store_version), error_msg) in verify_dict_error.iteritems(): path_attrib_xml = { 'usingTrustStore': store_name, 'trustStoreVersion': store_version, 'error': error_msg } trust_validation_xml.append( Element('pathValidation', attrib=path_attrib_xml)) xml_output.append(trust_validation_xml) # XML output - OCSP Stapling if ocsp_response is None: ocsp_attr_xml = {'isSupported': 'False'} ocsp_xml = Element('ocspStapling', attrib=ocsp_attr_xml) else: ocsp_attr_xml = {'isSupported': 'True'} ocsp_xml = Element('ocspStapling', attrib=ocsp_attr_xml) ocsp_resp_attr_xml = { 'isTrustedByMozillaCAStore': str(ocsp_response.verify(MOZILLA_STORE_PATH)) } ocsp_resp_xmp = Element('ocspResponse', attrib=ocsp_resp_attr_xml) for (key, value) in ocsp_response.as_dict().items(): ocsp_resp_xmp.append(_keyvalue_pair_to_xml(key, value)) ocsp_xml.append(ocsp_resp_xmp) xml_output.append(ocsp_xml) return PluginBase.PluginResult(text_output, xml_output) # FORMATTING FUNCTIONS @staticmethod def _format_cert_to_xml(x509_cert, x509_cert_position_in_chain_txt, sni_txt): cert_attrib_xml = {'sha1Fingerprint': x509_cert.get_SHA1_fingerprint()} if x509_cert_position_in_chain_txt: cert_attrib_xml['position'] = x509_cert_position_in_chain_txt if sni_txt: cert_attrib_xml['suppliedServerNameIndication'] = sni_txt cert_xml = Element('certificate', attrib=cert_attrib_xml) cert_as_pem_xml = Element('asPEM') cert_as_pem_xml.text = x509_cert.as_pem().strip() cert_xml.append(cert_as_pem_xml) for (key, value) in x509_cert.as_dict().items(): # Sanitize OpenSSL's output if 'subjectPublicKeyInfo' in key: # Remove the bit suffix so the element is just a number for the key size if 'publicKeySize' in value.keys(): value['publicKeySize'] = value['publicKeySize'].split( ' bit')[0] # Add the XML element cert_xml.append(_keyvalue_pair_to_xml(key, value)) return cert_xml def _get_ocsp_text(self, ocsp_resp): if ocsp_resp is None: return [ self.FIELD_FORMAT( 'NOT SUPPORTED - Server did not send back an OCSP response.', '') ] ocsp_resp_dict = ocsp_resp.as_dict() ocsp_trust_txt = 'OK - Response is trusted' if ocsp_resp.verify(MOZILLA_STORE_PATH) \ else 'FAILED - Response is NOT trusted' ocsp_resp_txt = [ self.FIELD_FORMAT('OCSP Response Status:', ocsp_resp_dict['responseStatus']), self.FIELD_FORMAT('Validation w/ Mozilla\'s CA Store:', ocsp_trust_txt), self.FIELD_FORMAT('Responder Id:', ocsp_resp_dict['responderID']) ] if 'successful' not in ocsp_resp_dict['responseStatus']: return ocsp_resp_txt ocsp_resp_txt.extend([ self.FIELD_FORMAT('Cert Status:', ocsp_resp_dict['responses'][0]['certStatus']), self.FIELD_FORMAT( 'Cert Serial Number:', ocsp_resp_dict['responses'][0]['certID']['serialNumber']), self.FIELD_FORMAT('This Update:', ocsp_resp_dict['responses'][0]['thisUpdate']), self.FIELD_FORMAT('Next Update:', ocsp_resp_dict['responses'][0]['nextUpdate']) ]) return ocsp_resp_txt @staticmethod def _is_ev_certificate(cert): cert_dict = cert.as_dict() try: policy = cert_dict['extensions']['X509v3 Certificate Policies'][ 'Policy'] if policy[0] in MOZILLA_EV_OIDS: return True except: return False return False @staticmethod def _get_full_text(cert): return [cert.as_text()] @staticmethod def _extract_subject_cn_or_oun(cert): try: # Extract the CN if there's one cert_name = cert.as_dict()['subject']['commonName'] except KeyError: # If no common name, display the organizational unit instead try: cert_name = cert.as_dict()['subject']['organizationalUnitName'] except KeyError: # Give up cert_name = 'No Common Name' return cert_name def _get_basic_text(self, cert): cert_dict = cert.as_dict() try: # Extract the CN if there's one common_name = cert_dict['subject']['commonName'] except KeyError: common_name = 'None' try: # Extract the CN from the issuer if there's one issuer_name = cert_dict['issuer']['commonName'] except KeyError: issuer_name = str(cert_dict['issuer']) text_output = [ self.FIELD_FORMAT("SHA1 Fingerprint:", cert.get_SHA1_fingerprint()), self.FIELD_FORMAT("Common Name:", common_name), self.FIELD_FORMAT("Issuer:", issuer_name), self.FIELD_FORMAT("Serial Number:", cert_dict['serialNumber']), self.FIELD_FORMAT("Not Before:", cert_dict['validity']['notBefore']), self.FIELD_FORMAT("Not After:", cert_dict['validity']['notAfter']), self.FIELD_FORMAT("Signature Algorithm:", cert_dict['signatureAlgorithm']), self.FIELD_FORMAT( "Public Key Algorithm:", cert_dict['subjectPublicKeyInfo']['publicKeyAlgorithm']), self.FIELD_FORMAT( "Key Size:", cert_dict['subjectPublicKeyInfo']['publicKeySize']) ] try: # Print the Public key exponent if there's one; EC public keys don't have one for example text_output.append( self.FIELD_FORMAT( "Exponent:", "{0} (0x{0:x})".format( int(cert_dict['subjectPublicKeyInfo']['publicKey'] ['exponent'])))) except KeyError: pass try: # Print the SAN extension if there's one text_output.append( self.FIELD_FORMAT( 'X509v3 Subject Alternative Name:', cert_dict['extensions'] ['X509v3 Subject Alternative Name'])) except KeyError: pass return text_output def _get_cert(self, target, store_path): """ Connects to the target server and uses the supplied trust store to validate the server's certificate. Returns the server's certificate and OCSP response. """ (_, _, _, ssl_version) = target ssl_conn = create_sslyze_connection(target, self._shared_settings, ssl_version, sslVerifyLocations=store_path) # Enable OCSP stapling ssl_conn.set_tlsext_status_ocsp() try: # Perform the SSL handshake ssl_conn.connect() ocsp_resp = ssl_conn.get_tlsext_status_ocsp_resp() x509_cert_chain = ssl_conn.get_peer_cert_chain() (_, verify_str) = ssl_conn.get_certificate_chain_verify_result() except ClientCertificateRequested: # The server asked for a client cert # We can get the server cert anyway ocsp_resp = ssl_conn.get_tlsext_status_ocsp_resp() x509_cert_chain = ssl_conn.get_peer_cert_chain() (_, verify_str) = ssl_conn.get_certificate_chain_verify_result() finally: ssl_conn.close() return x509_cert_chain, verify_str, ocsp_resp
def process_task(self, target, command, arg): if arg == 'basic': txt_output_generator = self._get_basic_text elif arg == 'full': txt_output_generator = self._get_full_text else: raise Exception("PluginCertInfo: Unknown command.") (host, _, _, _) = target thread_pool = ThreadPool() if 'ca_file' in self._shared_settings and self._shared_settings[ 'ca_file']: AVAILABLE_TRUST_STORES[self._shared_settings['ca_file']] = ( 'Custom --ca_file', 'N/A') for (store_path, _) in AVAILABLE_TRUST_STORES.iteritems(): # Try to connect with each trust store thread_pool.add_job((self._get_cert, (target, store_path))) # Start processing the jobs thread_pool.start(len(AVAILABLE_TRUST_STORES)) # Store the results as they come x509_cert_chain = [] (verify_dict, verify_dict_error, x509_cert, ocsp_response) = ({}, {}, None, None) for (job, result) in thread_pool.get_result(): (_, (_, store_path)) = job (x509_cert_chain, verify_str, ocsp_response) = result # Store the returned verify string for each trust store x509_cert = x509_cert_chain[ 0] # First cert is always the leaf cert store_info = AVAILABLE_TRUST_STORES[store_path] verify_dict[store_info] = verify_str if x509_cert is None: # This means none of the connections were successful. Get out for (job, exception) in thread_pool.get_error(): raise exception # Store thread pool errors for (job, exception) in thread_pool.get_error(): (_, (_, store_path)) = job error_msg = str( exception.__class__.__name__) + ' - ' + str(exception) store_info = AVAILABLE_TRUST_STORES[store_path] verify_dict_error[store_info] = error_msg thread_pool.join() # Results formatting # Text output - certificate info text_output = [self.PLUGIN_TITLE_FORMAT('Certificate - Content')] text_output.extend(txt_output_generator(x509_cert)) # Text output - trust validation text_output.extend( ['', self.PLUGIN_TITLE_FORMAT('Certificate - Trust')]) # Hostname validation if self._shared_settings['sni']: text_output.append( self.FIELD_FORMAT("SNI enabled with virtual domain:", self._shared_settings['sni'])) # TODO: Use SNI name for validation when --sni was used host_val_dict = { X509_NAME_MATCHES_SAN: 'OK - Subject Alternative Name matches', X509_NAME_MATCHES_CN: 'OK - Common Name matches', X509_NAME_MISMATCH: 'FAILED - Certificate does NOT match ' + host } text_output.append( self.FIELD_FORMAT("Hostname Validation:", host_val_dict[x509_cert.matches_hostname(host)])) # Path validation that was successful for ((store_name, store_version), verify_str) in verify_dict.iteritems(): verify_txt = 'OK - Certificate is trusted' if (verify_str in 'ok') \ else 'FAILED - Certificate is NOT Trusted: ' + verify_str # EV certs - Only Mozilla supported for now if (verify_str in 'ok') and ('Mozilla' in store_info): if self._is_ev_certificate(x509_cert): verify_txt += ', Extended Validation' text_output.append( self.FIELD_FORMAT( self.TRUST_FORMAT(store_name=store_name, store_version=store_version), verify_txt)) # Path validation that ran into errors for ((store_name, store_version), error_msg) in verify_dict_error.iteritems(): verify_txt = 'ERROR: ' + error_msg text_output.append( self.FIELD_FORMAT( self.TRUST_FORMAT(store_name=store_name, store_version=store_version), verify_txt)) # Print the Common Names within the certificate chain cns_in_cert_chain = [] for cert in x509_cert_chain: cert_identity = self._extract_subject_cn_or_oun(cert) cns_in_cert_chain.append(cert_identity) text_output.append( self.FIELD_FORMAT('Certificate Chain Received:', str(cns_in_cert_chain))) # Text output - OCSP stapling text_output.extend( ['', self.PLUGIN_TITLE_FORMAT('Certificate - OCSP Stapling')]) text_output.extend(self._get_ocsp_text(ocsp_response)) # XML output xml_output = Element(command, argument=arg, title='Certificate Information') # XML output - certificate chain: always return the full certificate for each cert in the chain cert_chain_xml = Element('certificateChain') # First add the leaf certificate cert_chain_xml.append( self._format_cert_to_xml(x509_cert_chain[0], 'leaf', self._shared_settings['sni'])) # Then add every other cert in the chain for cert in x509_cert_chain[1:]: cert_chain_xml.append( self._format_cert_to_xml(cert, 'intermediate', self._shared_settings['sni'])) xml_output.append(cert_chain_xml) # XML output - trust trust_validation_xml = Element('certificateValidation') # Hostname validation is_hostname_valid = 'False' if (x509_cert.matches_hostname(host) == X509_NAME_MISMATCH) else 'True' host_validation_xml = Element( 'hostnameValidation', serverHostname=host, certificateMatchesServerHostname=is_hostname_valid) trust_validation_xml.append(host_validation_xml) # Path validation - OK for ((store_name, store_version), verify_str) in verify_dict.iteritems(): path_attrib_xml = { 'usingTrustStore': store_name, 'trustStoreVersion': store_version, 'validationResult': verify_str } # EV certs - Only Mozilla supported for now if (verify_str in 'ok') and ('Mozilla' in store_info): path_attrib_xml['isExtendedValidationCertificate'] = str( self._is_ev_certificate(x509_cert)) trust_validation_xml.append( Element('pathValidation', attrib=path_attrib_xml)) # Path validation - Errors for ((store_name, store_version), error_msg) in verify_dict_error.iteritems(): path_attrib_xml = { 'usingTrustStore': store_name, 'trustStoreVersion': store_version, 'error': error_msg } trust_validation_xml.append( Element('pathValidation', attrib=path_attrib_xml)) xml_output.append(trust_validation_xml) # XML output - OCSP Stapling if ocsp_response is None: ocsp_attr_xml = {'isSupported': 'False'} ocsp_xml = Element('ocspStapling', attrib=ocsp_attr_xml) else: ocsp_attr_xml = {'isSupported': 'True'} ocsp_xml = Element('ocspStapling', attrib=ocsp_attr_xml) ocsp_resp_attr_xml = { 'isTrustedByMozillaCAStore': str(ocsp_response.verify(MOZILLA_STORE_PATH)) } ocsp_resp_xmp = Element('ocspResponse', attrib=ocsp_resp_attr_xml) for (key, value) in ocsp_response.as_dict().items(): ocsp_resp_xmp.append(_keyvalue_pair_to_xml(key, value)) ocsp_xml.append(ocsp_resp_xmp) xml_output.append(ocsp_xml) return PluginBase.PluginResult(text_output, xml_output)
class PluginSessionResumption(PluginBase.PluginBase): interface = PluginBase.PluginInterface( title="PluginSessionResumption", description=("Analyzes the target server's SSL session " "resumption capabilities.")) interface.add_command( command="resum", help=("Tests the server(s) for session resumption support using " "session IDs and TLS session tickets (RFC 5077).")) interface.add_command( command="resum_rate", help=("Performs 100 session resumptions with the server(s), " "in order to estimate the session resumption rate."), aggressive=True) def process_task(self, target, command, args): if command == 'resum': result = self._command_resum(target) elif command == 'resum_rate': result = self._command_resum_rate(target) else: raise Exception("PluginSessionResumption: Unknown command.") return result def _command_resum_rate(self, target): """ Performs 100 session resumptions with the server in order to estimate the session resumption rate. """ # Create a thread pool and process the jobs NB_THREADS = 20 MAX_RESUM = 100 thread_pool = ThreadPool() for _ in xrange(MAX_RESUM): thread_pool.add_job((self._resume_with_session_id, (target, ))) thread_pool.start(NB_THREADS) # Format session ID results (txt_resum, xml_resum) = self._format_resum_id_results(thread_pool, MAX_RESUM) # Text output cmd_title = 'Resumption Rate with Session IDs' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title) + ' ' + txt_resum[0]] txt_result.extend(txt_resum[1:]) # XML output xml_result = Element('resum_rate', title=cmd_title) xml_result.append(xml_resum) thread_pool.join() return PluginBase.PluginResult(txt_result, xml_result) def _command_resum(self, target): """ Tests the server for session resumption support using session IDs and TLS session tickets (RFC 5077). """ NB_THREADS = 5 MAX_RESUM = 5 thread_pool = ThreadPool() for _ in xrange(MAX_RESUM): # Test 5 resumptions with session IDs thread_pool.add_job( (self._resume_with_session_id, (target, ), 'session_id')) thread_pool.start(NB_THREADS) # Test TLS tickets support while threads are running try: (ticket_supported, ticket_reason) = self._resume_with_session_ticket(target) ticket_error = None except Exception as e: ticket_error = str(e.__class__.__name__) + ' - ' + str(e) # Format session ID results (txt_resum, xml_resum) = self._format_resum_id_results(thread_pool, MAX_RESUM) if ticket_error: ticket_txt = 'ERROR: ' + ticket_error else: ticket_txt = 'OK - Supported' if ticket_supported \ else 'NOT SUPPORTED - ' + ticket_reason+'.' cmd_title = 'Session Resumption' txt_result = [self.PLUGIN_TITLE_FORMAT(cmd_title)] RESUM_FORMAT = ' {0:<35}{1}'.format txt_result.append(RESUM_FORMAT('With Session IDs:', txt_resum[0])) txt_result.extend(txt_resum[1:]) txt_result.append(RESUM_FORMAT('With TLS Session Tickets:', ticket_txt)) # XML output xml_resum_ticket_attr = {} if ticket_error: xml_resum_ticket_attr['error'] = ticket_error else: xml_resum_ticket_attr['isSupported'] = str(ticket_supported) if not ticket_supported: xml_resum_ticket_attr['reason'] = ticket_reason xml_resum_ticket = Element('sessionResumptionWithTLSTickets', attrib=xml_resum_ticket_attr) xml_result = Element('resum', title=cmd_title) xml_result.append(xml_resum) xml_result.append(xml_resum_ticket) thread_pool.join() return PluginBase.PluginResult(txt_result, xml_result) @staticmethod def _format_resum_id_results(thread_pool, MAX_RESUM): # Count successful/failed resumptions nb_resum = 0 for completed_job in thread_pool.get_result(): (job, (is_supported, reason_str)) = completed_job if is_supported: nb_resum += 1 # Count errors and store error messages error_list = [] for failed_job in thread_pool.get_error(): (job, exception) = failed_job error_msg = str( exception.__class__.__name__) + ' - ' + str(exception) error_list.append(error_msg) nb_error = len(error_list) nb_failed = MAX_RESUM - nb_error - nb_resum # Text output SESSID_FORMAT = '{4} ({0} successful, {1} failed, {2} errors, {3} total attempts).{5}'.format sessid_try = '' if nb_resum == MAX_RESUM: sessid_stat = 'OK - Supported' elif nb_failed == MAX_RESUM: sessid_stat = 'NOT SUPPORTED' elif nb_error == MAX_RESUM: sessid_stat = 'ERROR' else: sessid_stat = 'PARTIALLY SUPPORTED' sessid_try = ' Try --resum_rate.' sessid_txt = SESSID_FORMAT(str(nb_resum), str(nb_failed), str(nb_error), str(MAX_RESUM), sessid_stat, sessid_try) ERRORS_FORMAT = ' ERROR #{0}: {1}'.format txt_result = [sessid_txt] # Add error messages if error_list: i = 0 for error_msg in error_list: i += 1 txt_result.append(ERRORS_FORMAT(str(i), error_msg)) # XML output sessid_xml = str(nb_resum == MAX_RESUM) xml_resum_id_attr = { 'totalAttempts': str(MAX_RESUM), 'errors': str(nb_error), 'isSupported': sessid_xml, 'successfulAttempts': str(nb_resum), 'failedAttempts': str(nb_failed) } xml_resum_id = Element('sessionResumptionWithSessionIDs', attrib=xml_resum_id_attr) # Add errors if error_list: for error_msg in error_list: xml_resum_error = Element('error') xml_resum_error.text = error_msg xml_resum_id.append(xml_resum_error) return txt_result, xml_resum_id def _resume_with_session_id(self, target): """ Performs one session resumption using Session IDs. """ session1 = self._resume_ssl_session(target) try: # Recover the session ID session1_id = self._extract_session_id(session1) except IndexError: return False, 'Session ID not assigned' if session1_id == '': return False, 'Session ID empty' # Try to resume that SSL session session2 = self._resume_ssl_session(target, session1) try: # Recover the session ID session2_id = self._extract_session_id(session2) except IndexError: return False, 'Session ID not assigned' # Finally, compare the two Session IDs if session1_id != session2_id: return False, 'Session ID assigned but not accepted' return True, '' def _resume_with_session_ticket(self, target): """ Performs one session resumption using TLS Session Tickets. """ # Connect to the server and keep the SSL session session1 = self._resume_ssl_session(target, tlsTicket=True) try: # Recover the TLS ticket session1_tls_ticket = self._extract_tls_session_ticket(session1) except IndexError: return False, 'TLS ticket not assigned' # Try to resume that session using the TLS ticket session2 = self._resume_ssl_session(target, session1, tlsTicket=True) try: # Recover the TLS ticket session2_tls_ticket = self._extract_tls_session_ticket(session2) except IndexError: return False, 'TLS ticket not assigned' # Finally, compare the two TLS Tickets if session1_tls_ticket != session2_tls_ticket: return False, 'TLS ticket assigned but not accepted' return True, '' @staticmethod def _extract_session_id(ssl_session): """ Extracts the SSL session ID from a SSL session object or raises IndexError if the session ID was not set. """ session_string = ((ssl_session.as_text()).split("Session-ID:"))[1] session_id = (session_string.split("Session-ID-ctx:"))[0].strip() return session_id @staticmethod def _extract_tls_session_ticket(ssl_session): """ Extracts the TLS session ticket from a SSL session object or raises IndexError if the ticket was not set. """ session_string = (( ssl_session.as_text()).split("TLS session ticket:"))[1] session_tls_ticket = (session_string.split("Compression:"))[0] return session_tls_ticket def _resume_ssl_session(self, target, sslSession=None, tlsTicket=False): """ Connect to the server and returns the session object that was assigned for that connection. If ssl_session is given, tries to resume that session. """ sslConn = create_sslyze_connection(target, self._shared_settings) if not tlsTicket: # Need to disable TLS tickets to test session IDs, according to rfc5077: # If a ticket is presented by the client, the server MUST NOT attempt # to use the Session ID in the ClientHello for stateful session resumption sslConn.set_options(SSL_OP_NO_TICKET) # Turning off TLS tickets. if sslSession: sslConn.set_session(sslSession) try: # Perform the SSL handshake sslConn.connect() newSession = sslConn.get_session() # Get session data finally: sslConn.close() return newSession
def process_task(self, target, command, arg): (_, _, _, sslVersion) = target # Get the server's cert chain sslConn = create_sslyze_connection(target, self._shared_settings, sslVersion) try: # Perform the SSL handshake sslConn.connect() certChain = sslConn.get_peer_cert_chain() except ClientCertificateRequested: # The server asked for a client cert # We can get the server cert chain anyway certChain = sslConn.get_peer_cert_chain() finally: sslConn.close() outputXml = Element(command, title=self.CMD_TITLE) outputTxt = [self.PLUGIN_TITLE_FORMAT(self.CMD_TITLE)] # Is this cert chain affected ? leafNotAfter = datetime.datetime.strptime( certChain[0].as_dict()['validity']['notAfter'], "%b %d %H:%M:%S %Y %Z") if leafNotAfter.year < 2016: # Not affected - the certificate expires before 2016 outputTxt.append( self.FIELD_FORMAT('OK - Leaf certificate expires before 2016.', '')) outputXml.append( Element('chromeSha1Deprecation', isServerAffected=str(False))) else: certsWithSha1 = [] for cert in certChain: if self._is_root_cert(cert): # Ignore root certs as they are unaffected continue if "sha1" in cert.as_dict()['signatureAlgorithm']: certsWithSha1.append(cert) if certsWithSha1 == []: # Not affected - no certificates used SHA-1 in the chain outputTxt.append( self.FIELD_FORMAT( 'OK - Certificate chain does not contain any SHA-1 certificate.', '')) outputXml.append( Element('chromeSha1Deprecation', isServerAffected=str(False))) else: # Server is affected leafCertNotAfter = certChain[0].as_dict( )['validity']['notAfter'] outputXml2 = Element('chromeSha1Deprecation', isServerAffected=str(True), leafCertificateNotAfter=leafCertNotAfter) chrome39Txt = 'OK' chrome40Txt = 'OK' if leafNotAfter.year == 2016 and leafNotAfter.month < 6: chrome41Txt = self.CHROME_MINOR_ERROR_TXT elif leafNotAfter.year == 2016 and leafNotAfter.month >= 6: chrome40Txt = self.CHROME_MINOR_ERROR_TXT chrome41Txt = self.CHROME_MINOR_ERROR_TXT else: # Certificate expires in 2017 chrome39Txt = self.CHROME_MINOR_ERROR_TXT chrome40Txt = self.CHROME_NEUTRAL_TXT chrome41Txt = self.CHROME_INSECURE_TXT # Text output certsWithSha1Txt = [ '"{0}"'.format( PluginCertInfo._extract_subject_CN_or_OUN(cert)) for cert in certsWithSha1 ] outputTxt.append( self.FIELD_FORMAT("Chrome 39 behavior:", chrome39Txt)) outputTxt.append( self.FIELD_FORMAT("Chrome 40 behavior:", chrome40Txt)) outputTxt.append( self.FIELD_FORMAT("Chrome 41 behavior:", chrome41Txt)) outputTxt.append( self.FIELD_FORMAT("Leaf certificate notAfter field:", leafCertNotAfter)) outputTxt.append( self.FIELD_FORMAT("SHA1-signed certificates:", certsWithSha1Txt)) # XML output affectedCertsXml = Element('sha1SignedCertificates') for cert in certsWithSha1: affectedCertsXml.append( PluginCertInfo._format_cert_to_xml( cert, '', self._shared_settings['sni'])) outputXml2.append(affectedCertsXml) outputXml2.append( Element('chrome39', behavior=chrome39Txt, isAffected=str(False) if chrome39Txt is 'OK' else str(True))) outputXml2.append( Element('chrome40', behavior=chrome40Txt, isAffected=str(False) if chrome40Txt is 'OK' else str(True))) outputXml2.append( Element('chrome41', behavior=chrome41Txt, isAffected=str(True))) outputXml.append(outputXml2) return PluginBase.PluginResult(outputTxt, outputXml)
class PluginChromeSha1Deprecation(PluginBase.PluginBase): interface = PluginBase.PluginInterface(title="PluginChromeSha1Deprecation", description=('')) interface.add_command( command="chrome_sha1", help= "Determines if the server will be affected by Google Chrome's SHA-1 deprecation plans. See " "http://googleonlinesecurity.blogspot.com/2014/09/gradually-sunsetting-sha-1.html for more information" ) CMD_TITLE = "Google Chrome SHA-1 Deprecation Status" # Chrome icon descriptions CHROME_MINOR_ERROR_TXT = 'AFFECTED - SHA1-signed certificate(s) will trigger the "Secure, but minor errors" icon.' CHROME_NEUTRAL_TXT = 'AFFECTED - SHA1-signed certificate(s) will trigger the "Neutral, lacking security" icon.' CHROME_INSECURE_TXT = 'AFFECTED - SHA1-signed certificate(s) will trigger the "Affirmatively insecure" icon.' def process_task(self, target, command, arg): (_, _, _, sslVersion) = target # Get the server's cert chain sslConn = create_sslyze_connection(target, self._shared_settings, sslVersion) try: # Perform the SSL handshake sslConn.connect() certChain = sslConn.get_peer_cert_chain() except ClientCertificateRequested: # The server asked for a client cert # We can get the server cert chain anyway certChain = sslConn.get_peer_cert_chain() finally: sslConn.close() outputXml = Element(command, title=self.CMD_TITLE) outputTxt = [self.PLUGIN_TITLE_FORMAT(self.CMD_TITLE)] # Is this cert chain affected ? leafNotAfter = datetime.datetime.strptime( certChain[0].as_dict()['validity']['notAfter'], "%b %d %H:%M:%S %Y %Z") if leafNotAfter.year < 2016: # Not affected - the certificate expires before 2016 outputTxt.append( self.FIELD_FORMAT('OK - Leaf certificate expires before 2016.', '')) outputXml.append( Element('chromeSha1Deprecation', isServerAffected=str(False))) else: certsWithSha1 = [] for cert in certChain: if self._is_root_cert(cert): # Ignore root certs as they are unaffected continue if "sha1" in cert.as_dict()['signatureAlgorithm']: certsWithSha1.append(cert) if certsWithSha1 == []: # Not affected - no certificates used SHA-1 in the chain outputTxt.append( self.FIELD_FORMAT( 'OK - Certificate chain does not contain any SHA-1 certificate.', '')) outputXml.append( Element('chromeSha1Deprecation', isServerAffected=str(False))) else: # Server is affected leafCertNotAfter = certChain[0].as_dict( )['validity']['notAfter'] outputXml2 = Element('chromeSha1Deprecation', isServerAffected=str(True), leafCertificateNotAfter=leafCertNotAfter) chrome39Txt = 'OK' chrome40Txt = 'OK' if leafNotAfter.year == 2016 and leafNotAfter.month < 6: chrome41Txt = self.CHROME_MINOR_ERROR_TXT elif leafNotAfter.year == 2016 and leafNotAfter.month >= 6: chrome40Txt = self.CHROME_MINOR_ERROR_TXT chrome41Txt = self.CHROME_MINOR_ERROR_TXT else: # Certificate expires in 2017 chrome39Txt = self.CHROME_MINOR_ERROR_TXT chrome40Txt = self.CHROME_NEUTRAL_TXT chrome41Txt = self.CHROME_INSECURE_TXT # Text output certsWithSha1Txt = [ '"{0}"'.format( PluginCertInfo._extract_subject_CN_or_OUN(cert)) for cert in certsWithSha1 ] outputTxt.append( self.FIELD_FORMAT("Chrome 39 behavior:", chrome39Txt)) outputTxt.append( self.FIELD_FORMAT("Chrome 40 behavior:", chrome40Txt)) outputTxt.append( self.FIELD_FORMAT("Chrome 41 behavior:", chrome41Txt)) outputTxt.append( self.FIELD_FORMAT("Leaf certificate notAfter field:", leafCertNotAfter)) outputTxt.append( self.FIELD_FORMAT("SHA1-signed certificates:", certsWithSha1Txt)) # XML output affectedCertsXml = Element('sha1SignedCertificates') for cert in certsWithSha1: affectedCertsXml.append( PluginCertInfo._format_cert_to_xml( cert, '', self._shared_settings['sni'])) outputXml2.append(affectedCertsXml) outputXml2.append( Element('chrome39', behavior=chrome39Txt, isAffected=str(False) if chrome39Txt is 'OK' else str(True))) outputXml2.append( Element('chrome40', behavior=chrome40Txt, isAffected=str(False) if chrome40Txt is 'OK' else str(True))) outputXml2.append( Element('chrome41', behavior=chrome41Txt, isAffected=str(True))) outputXml.append(outputXml2) return PluginBase.PluginResult(outputTxt, outputXml) @staticmethod def _is_root_cert(cert): # Root certificates are not affected by the deprecation of SHA1 # However a properly configured server should not send the CA cert in the chain so I'm not using this for now if not ROOT_CERTS: #Parse the Mozilla Store into roots f = open(MOZILLA_STORE_PATH, 'r') f_contents = "\n".join(f.readlines()) root_certs = f_contents.split("-----BEGIN CERTIFICATE-----") for r in root_certs: if not r.strip(): continue r = r.replace("-----END CERTIFICATE-----", "") r = r.replace("\n", "") r = r.replace("\r", "") d = base64.b64decode(r) ROOT_CERTS.append(hashlib.sha1(d).hexdigest()) return cert.get_SHA1_fingerprint() in ROOT_CERTS
class PluginSessionRenegotiation(PluginBase.PluginBase): interface = PluginBase.PluginInterface("PluginSessionRenegotiation", "") interface.add_command( command="reneg", help=( "Tests the server(s) for client-initiated " 'renegotiation and secure renegotiation support.')) def process_task(self, target, command, args): (clientReneg, secureReneg) = self._test_renegotiation(target) # Text output clientTxt = 'VULNERABLE - Server honors client-initiated renegotiations' if clientReneg else 'OK - Rejected' secureTxt = 'OK - Supported' if secureReneg else 'VULNERABLE - Secure renegotiation not supported' cmdTitle = 'Session Renegotiation' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] txtOutput.append(self.FIELD_FORMAT('Client-initiated Renegotiations:', clientTxt)) txtOutput.append(self.FIELD_FORMAT('Secure Renegotiation:', secureTxt)) # XML output xmlReneg = Element('sessionRenegotiation', attrib = {'canBeClientInitiated' : str(clientReneg), 'isSecure' : str(secureReneg)}) xmlOutput = Element(command, title=cmdTitle) xmlOutput.append(xmlReneg) return PluginBase.PluginResult(txtOutput, xmlOutput) def _test_renegotiation(self, target): """ Checks whether the server honors session renegotiation requests and whether it supports secure renegotiation. """ sslConn = create_sslyze_connection(target, self._shared_settings) try: # Perform the SSL handshake sslConn.connect() secureReneg = sslConn.get_secure_renegotiation_support() try: # Let's try to renegotiate sslConn.do_renegotiate() clientReneg = True # Errors caused by a server rejecting the renegotiation except socket.error as e: if 'connection was forcibly closed' in str(e.args): clientReneg = False elif 'reset by peer' in str(e.args): clientReneg = False else: raise #except socket.timeout as e: # result_reneg = 'Rejected (timeout)' except OpenSSLError as e: if 'handshake failure' in str(e.args): clientReneg = False elif 'no renegotiation' in str(e.args): clientReneg = False else: raise # Should be last as socket errors are also IOError except IOError as e: if 'Nassl SSL handshake failed' in str(e.args): clientReneg = False else: raise finally: sslConn.close() return (clientReneg, secureReneg)
class PluginMultipleTrustStores(PluginBase.PluginBase): interface = PluginBase.PluginInterface(title="PluginMultipleTrustStores", description=('')) interface.add_command( command="truststores", help= "Verifies the validity of target server's certificate chain against " "all the trust stores available as PEM files within ./plugins/data/.", dest=None) FIELD_FORMAT = ' {0:<40}{1}'.format def process_task(self, target, command, arg): # Get the certificate and validate it against all the trust stores (cert, verify_result) = self._get_cert(target, TRUST_STORE_PATHS) # Results formatting # Text output - display each trust store and the validation result cmdTitle = 'Multi Trust Store Validation' txtOutput = [self.PLUGIN_TITLE_FORMAT(cmdTitle)] isCertTrusted = True fingerprint = cert.get_SHA1_fingerprint() txtOutput.append( self.FIELD_FORMAT('Certificate SHA1 Fingerprint:', fingerprint)) for trustStorePath in TRUST_STORE_PATHS: if verify_result[trustStorePath] != 'ok': isCertTrusted = False txtOutput.append( self.FIELD_FORMAT( "Validation w/ '" + os.path.split(trustStorePath)[1] + "': ", verify_result[trustStorePath])) # XML output. xmlOutput = Element(command, title=cmdTitle) trustStoresXml = Element('trustStoreList', isTrustedByAllTrustStores=str(isCertTrusted)) for trustStorePath in TRUST_STORE_PATHS: # Add the result of each trust store trustStoresXml.append( Element('trustStore', filePath=os.path.split(trustStorePath)[1], verifyResult=verify_result[trustStorePath])) xmlOutput.append(trustStoresXml) # Add the certificate certXml = Element('certificate', sha1Fingerprint=fingerprint) for (key, value) in cert.as_dict().items(): certXml.append(_keyvalue_pair_to_xml(key, value)) xmlOutput.append(certXml) return PluginBase.PluginResult(txtOutput, xmlOutput) def _get_cert(self, target, trustStoreList): """ Connects to the target server and returns the server's certificate Also performs verification against multiple trust stores. """ verifyResults = {} for trustStorePath in trustStoreList: (host, ip, port, sslVersion) = target sslConn = create_sslyze_connection( target, self._shared_settings, sslVersion, sslVerifyLocations=trustStorePath) try: # Perform the SSL handshake sslConn.connect() x509Cert = sslConn.get_peer_certificate() (verifyCode, verifyStr) = sslConn.get_certificate_chain_verify_result() except ClientCertificateError: # The server asked for a client cert # We can get the server cert anyway x509Cert = sslConn.get_peer_certificate() (verifyCode, verifyStr) = sslConn.get_certificate_chain_verify_result() finally: sslConn.close() verifyResults[trustStorePath] = verifyStr return (x509Cert, verifyResults)
def process_task(self, target, command, args): MAX_THREADS = 30 if command in ['sslv2', 'sslv3', 'tlsv1', 'tlsv1_1', 'tlsv1_2']: ssl_version = command else: raise Exception("PluginOpenSSLCipherSuites: Unknown command.") # Get the list of available cipher suites for the given ssl version ctSSL_initialize(multithreading=True) ctx = SSL_CTX.SSL_CTX(ssl_version) ctx.set_cipher_list('ALL:NULL:@STRENGTH') ssl = SSL.SSL(ctx) cipher_list = ssl.get_cipher_list() # Create a thread pool NB_THREADS = min(len(cipher_list), MAX_THREADS) # One thread per cipher thread_pool = ThreadPool() # Scan for every available cipher suite for cipher in cipher_list: thread_pool.add_job( (self._test_ciphersuite, (target, ssl_version, cipher))) # Scan for the preferred cipher suite thread_pool.add_job((self._pref_ciphersuite, (target, ssl_version))) # Start processing the jobs thread_pool.start(NB_THREADS) result_dicts = { 'preferredCipherSuite': {}, 'acceptedCipherSuites': {}, 'rejectedCipherSuites': {}, 'errors': {} } # Store the results as they come for completed_job in thread_pool.get_result(): (job, result) = completed_job if result is not None: (result_type, ssl_cipher, keysize, msg) = result (result_dicts[result_type])[ssl_cipher] = (msg, keysize) # Store thread pool errors for failed_job in thread_pool.get_error(): (job, exception) = failed_job ssl_cipher = str(job[1][2]) error_msg = str(exception.__class__.__module__) + '.' \ + str(exception.__class__.__name__) + ' - ' + str(exception) result_dicts['errors'][ssl_cipher] = (error_msg, None) thread_pool.join() ctSSL_cleanup() # Generate results return PluginBase.PluginResult( self._generate_txt_result(result_dicts, command), self._generate_xml_result(result_dicts, command))
class PluginOpenSSLCipherSuites(PluginBase.PluginBase): interface = PluginBase.PluginInterface( "PluginOpenSSLCipherSuites", "Scans the target server for supported OpenSSL cipher suites.") interface.add_command( command="sslv2", help="Lists the SSL 2.0 OpenSSL cipher suites supported by the server.", dest=None) interface.add_command( command="sslv3", help="Lists the SSL 3.0 OpenSSL cipher suites supported by the server.", dest=None) interface.add_command( command="tlsv1", help="Lists the TLS 1.0 OpenSSL cipher suites supported by the server.", dest=None) interface.add_command( command="tlsv1_1", help="Lists the TLS 1.1 OpenSSL cipher suites supported by the server.", dest=None) interface.add_command( command="tlsv1_2", help="Lists the TLS 1.2 OpenSSL cipher suites supported by the server.", dest=None) interface.add_option( option='http_get', help="Option - For each cipher suite, sends an HTTP GET request after " "completing the SSL handshake and returns the HTTP status code.", dest=None) interface.add_option( option='hide_rejected_ciphers', help="Option - Hides the (usually long) list of cipher suites that were" " rejected by the server.", dest=None) def process_task(self, target, command, args): MAX_THREADS = 30 if command in ['sslv2', 'sslv3', 'tlsv1', 'tlsv1_1', 'tlsv1_2']: ssl_version = command else: raise Exception("PluginOpenSSLCipherSuites: Unknown command.") # Get the list of available cipher suites for the given ssl version ctSSL_initialize(multithreading=True) ctx = SSL_CTX.SSL_CTX(ssl_version) ctx.set_cipher_list('ALL:NULL:@STRENGTH') ssl = SSL.SSL(ctx) cipher_list = ssl.get_cipher_list() # Create a thread pool NB_THREADS = min(len(cipher_list), MAX_THREADS) # One thread per cipher thread_pool = ThreadPool() # Scan for every available cipher suite for cipher in cipher_list: thread_pool.add_job( (self._test_ciphersuite, (target, ssl_version, cipher))) # Scan for the preferred cipher suite thread_pool.add_job((self._pref_ciphersuite, (target, ssl_version))) # Start processing the jobs thread_pool.start(NB_THREADS) result_dicts = { 'preferredCipherSuite': {}, 'acceptedCipherSuites': {}, 'rejectedCipherSuites': {}, 'errors': {} } # Store the results as they come for completed_job in thread_pool.get_result(): (job, result) = completed_job if result is not None: (result_type, ssl_cipher, keysize, msg) = result (result_dicts[result_type])[ssl_cipher] = (msg, keysize) # Store thread pool errors for failed_job in thread_pool.get_error(): (job, exception) = failed_job ssl_cipher = str(job[1][2]) error_msg = str(exception.__class__.__module__) + '.' \ + str(exception.__class__.__name__) + ' - ' + str(exception) result_dicts['errors'][ssl_cipher] = (error_msg, None) thread_pool.join() ctSSL_cleanup() # Generate results return PluginBase.PluginResult( self._generate_txt_result(result_dicts, command), self._generate_xml_result(result_dicts, command)) # == INTERNAL FUNCTIONS == # FORMATTING FUNCTIONS def _generate_txt_result(self, result_dicts, ssl_version): cipher_format = ' {0:<32}{1:<35}' title_format = ' {0:<32} ' keysize_format = '{0:<25}{1:<14}' title_txt = self.PLUGIN_TITLE_FORMAT.format(ssl_version.upper() + ' Cipher Suites') txt_result = [title_txt] txt_titles = [('preferredCipherSuite', 'Preferred Cipher Suite:'), ('acceptedCipherSuites', 'Accepted Cipher Suite(s):'), ('errors', 'Undefined - An unexpected error happened:'), ('rejectedCipherSuites', 'Rejected Cipher Suite(s):')] if self._shared_settings['hide_rejected_ciphers']: txt_titles.pop(3) txt_result.append('') txt_result.append( title_format.format('Rejected Cipher Suite(s): Hidden')) for (result_type, result_title) in txt_titles: # Sort the cipher suites by results result_list = sorted(result_dicts[result_type].iteritems(), key=lambda (k, v): (v, k), reverse=True) # Add a new line and title txt_result.append('') if len(result_list) == 0: # No ciphers txt_result.append(title_format.format(result_title + ' None')) else: txt_result.append(title_format.format(result_title)) # Add one line for each ciphers for (cipher_txt, (msg, keysize)) in result_list: if keysize: cipher_txt = keysize_format.format(cipher_txt, keysize) txt_result.append(cipher_format.format(cipher_txt, msg)) return txt_result def _generate_xml_result(self, result_dicts, command): xml_result = Element(command, title=command.upper() + ' Cipher Suites') for (result_type, result_dict) in result_dicts.items(): xml_dict = Element(result_type) # Sort the cipher suites by name to make the XML diff-able result_list = sorted(result_dict.items(), key=lambda (k, v): (k, v), reverse=False) # Add one element for each ciphers for (ssl_cipher, (msg, keysize)) in result_list: cipher_xml_attr = {'name': ssl_cipher, 'connectionStatus': msg} if keysize: cipher_xml_attr['keySize'] = keysize cipher_xml = Element('cipherSuite', attrib=cipher_xml_attr) xml_dict.append(cipher_xml) xml_result.append(xml_dict) return xml_result # SSL FUNCTIONS def _test_ciphersuite(self, target, ssl_version, ssl_cipher): """ Initiates a SSL handshake with the server, using the SSL version and cipher suite specified. """ ssl_ctx = SSL_CTX.SSL_CTX(ssl_version) ssl_ctx.set_verify(constants.SSL_VERIFY_NONE) ssl_ctx.set_cipher_list(ssl_cipher) # ssl_connect can be an HTTPS connection or an SMTP STARTTLS connection ssl_connect = SSLyzeSSLConnection(self._shared_settings, target, ssl_ctx) try: # Perform the SSL handshake ssl_connect.connect() except SSLHandshakeRejected as e: return ('rejectedCipherSuites', ssl_cipher, None, str(e)) else: ssl_cipher = ssl_connect._ssl.get_current_cipher() if 'ADH' in ssl_cipher or 'AECDH' in ssl_cipher: keysize = 'Anon' # Anonymous, let s not care about the key size else: keysize = str( ssl_connect._ssl.get_current_cipher_bits()) + ' bits' status_msg = ssl_connect.post_handshake_check() return ('acceptedCipherSuites', ssl_cipher, keysize, status_msg) finally: ssl_connect.close() return def _pref_ciphersuite(self, target, ssl_version): """ Initiates a SSL handshake with the server, using the SSL version and cipher suite specified. """ ssl_ctx = SSL_CTX.SSL_CTX(ssl_version) ssl_ctx.set_verify(constants.SSL_VERIFY_NONE) # ssl_connect can be an HTTPS connection or an SMTP STARTTLS connection ssl_connect = SSLyzeSSLConnection(self._shared_settings, target, ssl_ctx, hello_workaround=True) try: # Perform the SSL handshake ssl_connect.connect() ssl_cipher = ssl_connect._ssl.get_current_cipher() if 'ADH' in ssl_cipher or 'AECDH' in ssl_cipher: keysize = 'Anon' # Anonymous, let s not care about the key size else: keysize = str( ssl_connect._ssl.get_current_cipher_bits()) + ' bits' status_msg = ssl_connect.post_handshake_check() return ('preferredCipherSuite', ssl_cipher, keysize, status_msg) except: return None finally: ssl_connect.close() return
def process_task(self, target, command, arg): try: # Get the certificate and the result of the cert validation (cert, certVerifyStr, ocspResp) = self._get_cert(target) except: raise trustedCert = True if 'ok' in certVerifyStr else False # Results formatting # Text output - certificate txt_result = [self.PLUGIN_TITLE_FORMAT('Certificate')] if arg == 'basic': cert_txt = self._get_basic_text(cert) elif arg == 'full': cert_txt = [cert.as_text()] else: raise Exception("PluginCertInfo: Unknown command.") fingerprint = cert.get_SHA1_fingerprint() # Cert chain validation trust_txt = 'Certificate is Trusted' if trustedCert \ else 'Certificate is NOT Trusted: ' + certVerifyStr is_ev = self._is_ev_certificate(cert) if is_ev: trust_txt = trust_txt + ' - Extended Validation' # Hostname validation txt_result.append( self.FIELD_FORMAT("Validation w/ Mozilla's CA Store:", trust_txt)) # TODO: Use SNI name when --sni was used is_host_valid = self._is_hostname_valid(cert, target) host_txt = 'OK - ' + is_host_valid + ' Matches' if is_host_valid \ else 'MISMATCH' txt_result.append(self.FIELD_FORMAT("Hostname Validation:", host_txt)) txt_result.append(self.FIELD_FORMAT('SHA1 Fingerprint:', fingerprint)) txt_result.append('') txt_result.extend(cert_txt) # Text output - OCSP stapling txt_result.append('') txt_result.append(self.PLUGIN_TITLE_FORMAT('OCSP Stapling')) txt_result.extend(self._get_ocsp_text(ocspResp)) # XML output: always return the full certificate host_xml = True if is_host_valid \ else False xml_result = Element(command, argument=arg, title='Certificate') trust_xml_attr = { 'isTrustedByMozillaCAStore': str(trustedCert), 'sha1Fingerprint': fingerprint, 'isExtendedValidation': str(is_ev), 'hasMatchingHostname': str(host_xml) } if certVerifyStr: trust_xml_attr['reasonWhyNotTrusted'] = certVerifyStr trust_xml = Element('certificate', attrib=trust_xml_attr) # Add certificate in PEM format PEMcert_xml = Element('asPEM') PEMcert_xml.text = cert.as_pem().strip() trust_xml.append(PEMcert_xml) for (key, value) in cert.as_dict().items(): trust_xml.append(_keyvalue_pair_to_xml(key, value)) xml_result.append(trust_xml) # XML output: OCSP Stapling if ocspResp is None: oscpAttr = {'error': 'Server did not send back an OCSP response'} ocspXml = Element('ocspStapling', attrib=oscpAttr) else: oscpAttr = { 'isTrustedByMozillaCAStore': str(ocspResp.verify(MOZILLA_CA_STORE)) } ocspXml = Element('ocspResponse', attrib=oscpAttr) for (key, value) in ocspResp.as_dict().items(): ocspXml.append(_keyvalue_pair_to_xml(key, value)) xml_result.append(ocspXml) return PluginBase.PluginResult(txt_result, xml_result)