def _send_message(self, message): b = message.render() if len(b) == 0: return # Nothing to do content_type = message.content_type log.debug('Sending {0} record'.format( TLS_CONTENT_TYPE.get(content_type, content_type))) if isinstance(message, Handshake): log.debug('... with sub type {0}'.format( TLS_HANDSHAKE_TYPE.get(message.handshake_type, message.handshake_type))) # Add record header r = RecordHeader3() r.content_type = content_type r.version = self.client_version r.size = len(b) s = r.render() + b while True: sent = self.remote.send(s) if sent == len(s): return else: s = s[sent:] yield 1
def verify(self, certificate): ''' Verify the signature of ``certificate`` using the public key listed for this certificate. >>> issuer = Certificate(...) >>> victim = Certificate(...) >>> issuer.verify(victim) True ''' # Don't bother verifying if we're not a CA certificate if not self.is_ca: log.debug('Attempted to verify from non-CA certificate') return False # Make sure this certificate is suitable for signing extensions = dict((k, v.to_python()) for k, v in self.get_extensions().iteritems()) if not 'keyCertSign' in extensions.get('keyUsage', []): log.debug( 'Attempted to verify from certificate not suitable for signing' ) return False return self.get_public_key().verify( certificate.get_signature(), certificate.get_hash(), certificate.get_signature_algorithm(), )
def get_hash(self): ''' :return: cryptographic hash of the tbsCertificate sequence ''' signature_algorithm = self.get_signature_algorithm() algorithm = signature_algorithm.replace('WithRSAEncryption', '') log.debug('Generating hashed value for {0}'.format(algorithm, )) data = der_encoder.encode(self.tbsCertificate) if algorithm == 'md2': return md2.MD2(data).digest() elif algorithm == 'md5': return hashlib.md5(data).digest() elif algorithm == 'ripemd160': return ripemd160.RIPEMD160(data).digest() elif algorithm == 'sha1': return hashlib.sha1(data).digest() elif algorithm == 'sha224': return hashlib.sha224(data).digest() elif algorithm == 'sha256': return hashlib.sha256(data).digest() elif algorithm == 'sha384': return hashlib.sha384(data).digest() elif algorithm == 'sha512': return hashlib.sha512(data).digest() else: log.error('Unsupported signature algorithm: {0}'.format( signature_algorithm, )) return None
def _check_features(self, address, support): log.debug('Testing features') secure = self._connect(address) all_ciphers = [getattr(CipherSuite, suite) for suite in self.collected['ciphers']] try: for result in secure.handshake( server_name=address[0], cipher_suites=all_ciphers, compression_methods=[0, 1], heartbeat=True, supports_npn=True, status_request=CertificateStatusType.ocsp, ): pass #import sys; sys.exit() # Test features from ServerHello report hello = secure.server_hello support['compression'] = hello.compression_method != 0 support['heartbeat'] = hello.heartbeat support['next_protos'] = hello.next_protos support['secure_renegotiation'] = hello.secure_renegotiation support['session'] = bool(hello.session_id) if support['session']: support['session_id'] = str(hello.session_id).encode('hex') else: support['session_id'] = None support['session_ticket'] = bool(hello.session_ticket) secure.close() except socket.error as error: raise Probe.Skip('Network error: {0}'.format(error))
def get_hash(self): ''' :return: cryptographic hash of the tbsCertificate sequence ''' signature_algorithm = self.get_signature_algorithm() algorithm = signature_algorithm.replace('WithRSAEncryption', '') log.debug('Generating hashed value for {0}'.format( algorithm, )) data = der_encoder.encode(self.tbsCertificate) if algorithm == 'md2': return md2.MD2(data).digest() elif algorithm == 'md5': return hashlib.md5(data).digest() elif algorithm == 'ripemd160': return ripemd160.RIPEMD160(data).digest() elif algorithm == 'sha1': return hashlib.sha1(data).digest() elif algorithm == 'sha224': return hashlib.sha224(data).digest() elif algorithm == 'sha256': return hashlib.sha256(data).digest() elif algorithm == 'sha384': return hashlib.sha384(data).digest() elif algorithm == 'sha512': return hashlib.sha512(data).digest() else: log.error('Unsupported signature algorithm: {0}'.format( signature_algorithm, )) return None
def _work(self, jobs, results): while True: job = jobs.get() if isinstance(job, ThreadPool.Done): log.debug('[{0}] done'.format( threading.currentThread().name, )) # Bye! results.put(ThreadPool.Done()) jobs.task_done() break func = job[0] args = job[1] try: result = func(*args) except Exception as error: log.error('Uncaught exception in thread worker: {0}'.format( error, )) else: results.put(result) finally: jobs.task_done()
def open(self, directory, encoding='ascii'): path = os.path.join(directory, self.filename) log.debug('opening report {0}'.format(path)) if path and self.filename != '-': return codecs.open(path, 'wb', encoding) else: return sys.stdout
def _send_message(self, message): b = message.render() if len(b) == 0: return # Nothing to do content_type = message.content_type log.debug('Sending {0} record'.format( TLS_CONTENT_TYPE.get(content_type, content_type) )) if isinstance(message, Handshake): log.debug('... with sub type {0}'.format( TLS_HANDSHAKE_TYPE.get(message.handshake_type, message.handshake_type) )) # Add record header r = RecordHeader3() r.content_type = content_type r.version = self.client_version r.size = len(b) s = r.render() + b while True: sent = self.remote.send(s) if sent == len(s): return else: s = s[sent:] yield 1
def __init__(self, sequence): super(PublicKey, self).__init__(sequence) algorithm = self.sequence.getComponentByName( 'algorithm').getComponentByName('algorithm') log.debug('Parsing {0} ({1}) public key'.format( friendly_oid(algorithm), str(algorithm), )) self.algorithm = x509.ID_KA_MAP.get(algorithm) if self.algorithm is None: raise TypeError('Unable to handle {0} keys'.format(str(algorithm))) key_bits = self.sequence.getComponentByName('subjectPublicKey') key_parm = self.sequence.getComponentByName( 'algorithm').getComponentByName('parameters') key_type = self.get_type() if key_type == 'DSA': self.key, _ = self._get_DSA_public_key(key_bits, key_parm) elif key_type == 'RSA': self.key, _ = self._get_RSA_public_key(key_bits) elif key_type == 'EC': self.key, _ = self._get_EC_public_key(key_bits, key_parm)
def __init__(self, sequence): super(PublicKey, self).__init__(sequence) algorithm = self.sequence.getComponentByName('algorithm').getComponentByName('algorithm') log.debug('Parsing {0} ({1}) public key'.format( friendly_oid(algorithm), str(algorithm), )) self.algorithm = x509.ID_KA_MAP.get(algorithm) if self.algorithm is None: raise TypeError('Unable to handle {0} keys'.format(str(algorithm))) key_bits = self.sequence.getComponentByName('subjectPublicKey') key_parm = self.sequence.getComponentByName('algorithm').getComponentByName('parameters') key_type = self.get_type() if key_type == 'DSA': self.key, _ = self._get_DSA_public_key(key_bits, key_parm) elif key_type == 'RSA': self.key, _ = self._get_RSA_public_key(key_bits) elif key_type == 'EC': self.key, _ = self._get_EC_public_key(key_bits, key_parm)
def _handshake(self, secure, cipher_suites): log.debug('Selected {0} out of {1} ciphers'.format( len(cipher_suites), len(CipherSuite.all), )) for result in secure.handshake(cipher_suites=cipher_suites, ): pass
def open(self, encoding='ascii'): log.debug('opening report {0}'.format( self.filename, )) if self.filename and self.filename != '-': return codecs.open(self.filename, 'wb', encoding) else: return sys.stdout
def __init__(self, named_curve, key_data): log.debug('Parsing {0} ({1}) named curve'.format( friendly_oid(named_curve), named_curve, )) self.name = friendly_oid(named_curve) self.group = parse_named_curve(named_curve) self.point = parse_binary_point(key_data, self.group['point_size'])
def _handshake(self, secure, cipher_suites): log.debug('Selected {0} out of {1} ciphers'.format( len(cipher_suites), len(CipherSuite.all), )) for result in secure.handshake( cipher_suites=cipher_suites, ): pass
def _recv_server_hello_resume(self, clientHello): ''' Client Server ClientHello (SessionTicket extension) --------> ServerHello (empty SessionTicket extension) NewSessionTicket [ChangeCipherSpec] <-------- Finished [ChangeCipherSpec] Finished --------> Application Data <-------> Application Data ''' for result in self._recv_message( ContentType.handshake, HandshakeType.server_hello ): if result in (0, 1): yield result else: break self.server_hello = result self.version = self.server_hello.server_version # Get ChangeCipherSpec for result in self._recv_message( ( ContentType.change_cipher_spec, ContentType.handshake, ), ( HandshakeType.certificate, ), self.server_hello.certificate_type ): if result in (0, 1): yield result else: break if isinstance(result, ChangeCipherSpec): self.change_cipher_spec = result else: log.debug('Did not receive a change_cipher_spec message') self.change_cipher_spec = None
def probe(self, address, certificates): ''' Retrieves the X.509 certificate from the remote host, being as permissive as we can in terms of TLS protocol support, selected cipher suites and other protocol violations that may occur. Fetched certificates will be added to the ``certificates`` set. Provides the following keys: * ``analysis.features`` Probes that depend on this probe: * 105_analyze_certificate_ * 110_analyze_public_key_ .. _105_analyze_certificate: probe_105_analyze_certificate.html .. _110_analyze_public_key: probe_110_analyze_public_key.html ''' if not address: # Nothing to do raise Probe.Skip('Offline; no address supplied') else: log.info('Fetching certificate from %s:%d' % address) features = {} features['long_client_handshake'] = True try: secure = self._connect(address) cipher = CipherSuite.all try: self._handshake(secure, CipherSuite.all) except Exception as error: log.debug('Long client handshake not supported: {0}'.format( error )) features['long_client_handshake'] = False self._handshake(secure, CipherSuite.basic) for certificate in secure.get_certificate_chain(): certificates.add(certificate) except socket.error, e: raise Probe.Skip('Network error: {0}'.format(e))
def load_probe(filename): global PROBES name = '.'.join([ 'tlsspy', 'probe', os.path.basename(os.path.splitext(filename)[0]), ]) log.debug('Loading {0}'.format(name)) try: module = imp.load_module(name, file(filename), filename, ('.py', 'U', imp.PY_SOURCE)) PROBES[name] = getattr(module, 'PROBES', []) except Exception, e: log.warning('Loading {0} failed "{1}"'.format(name, e)) raise
def probe(self, address, certificates): ''' Retrieves the X.509 certificate from the remote host, being as permissive as we can in terms of TLS protocol support, selected cipher suites and other protocol violations that may occur. Fetched certificates will be added to the ``certificates`` set. Provides the following keys: * ``analysis.features`` Probes that depend on this probe: * 105_analyze_certificate_ * 110_analyze_public_key_ .. _105_analyze_certificate: probe_105_analyze_certificate.html .. _110_analyze_public_key: probe_110_analyze_public_key.html ''' if not address: # Nothing to do raise Probe.Skip('Offline; no address supplied') else: log.info('Fetching certificate from %s:%d' % address) features = {} features['long_client_handshake'] = True try: secure = self._connect(address) cipher = CipherSuite.all try: self._handshake(secure, CipherSuite.all) except Exception as error: log.debug( 'Long client handshake not supported: {0}'.format(error)) features['long_client_handshake'] = False self._handshake(secure, CipherSuite.basic) for certificate in secure.get_certificate_chain(): certificates.add(certificate) except socket.error, e: raise Probe.Skip('Network error: {0}'.format(e))
def analyze(self, address, certificates): if not isinstance(certificates, OrderedSet): certificates = OrderedSet(certificates) info = {'tests': [], 'tests_skipped': []} for Probe in self.probes: log.debug('Running {0}'.format(Probe.__module__)) try: probe = Probe(info) probe.probe(address, certificates) info['tests'].append(Probe.__module__) except Probe.Skip, r: log.warning('Skip {0}: {1}'.format(Probe.__module__, r)) info['tests_skipped'].append(Probe.__module__) except Exception as error: log.error('Uncaught exception: {0}'.format(error)) for line in traceback.format_exc().splitlines(): log.error(line)
def __init__(self, sequence): self.sequence = sequence self.name = friendly_oid(self.sequence.getComponentByName('extnID')) self.critical = bool( self.sequence.getComponentByName('critical')._value) log.debug('Parsing extension {0}'.format(self.name)) self.encoded = self.sequence.getComponentByName('extnValue')._value if self.name in self._decoders: self.decoded = der_decoder.decode( self.encoded, asn1Spec=self._decoders[self.name])[0] else: warnings.warn('Not able to decode extension {0}'.format(self.name)) self.decoded = None self.parsed = self.to_python()
def _probe_named_curves(self, address, cipher_suites, elliptic_curves, support, status): while elliptic_curves: try: curve = self._probe_named_curve(address, cipher_suites, elliptic_curves) if curve is None: break else: log.debug('Elliptic Named Curve {0} supported'.format( TLS_EC_CURVE_NAME.get(curve), )) name = TLS_EC_CURVE_NAME.get(curve) support['curve_name'].append(name) status.append({name: self._get_named_curve_info(curve)}) elliptic_curves.remove(curve) except Exception as error: log.debug('Error Probing Named Curve: {0}'.format(error, )) break
def __init__(self, sequence): self.sequence = sequence self.name = friendly_oid(self.sequence['extnID']) self.critical = bool(self.sequence['critical']._value) log.debug('Parsing extension {0}'.format(self.name)) if self.name in self._decoders: self.encoded = self.sequence.getComponentByName('extnValue')._value self.decoded = der_decoder.decode( self.encoded, asn1Spec=self._decoders[self.name] )[0] else: warnings.warn('Not able to decode extension {0}'.format(self.name)) self.decoded = None self.parsed = self.to_python()
def load_probe(filename): global PROBES name = '.'.join([ 'tlsspy', 'probe', os.path.basename(os.path.splitext(filename)[0]), ]) log.debug('Loading {0}'.format(name)) try: module = imp.load_module( name, file(filename), filename, ('.py', 'U', imp.PY_SOURCE) ) PROBES[name] = getattr(module, 'PROBES', []) except Exception, e: log.warning('Loading {0} failed "{1}"'.format(name, e)) raise
def verify(self, certificate): ''' Verify the signature of ``certificate`` using the public key listed for this certificate. >>> issuer = Certificate(...) >>> victim = Certificate(...) >>> issuer.verify(victim) True ''' log.debug('{0} verify {1}'.format( self, certificate, )) # Don't bother verifying if we're not a CA certificate if not self.is_ca: log.debug('Attempted to verify from non-CA certificate') return False # Make sure this certificate is suitable for signing extensions = dict( (k, v.to_python()) for k, v in self.get_extensions().iteritems()) if not 'keyCertSign' in extensions.get('keyUsage', []): log.debug( 'Attempted to verify from certificate not suitable for signing' ) return False return self.get_public_key().verify( certificate.get_signature(), certificate.get_hash(), certificate.get_signature_algorithm(), )
def _test_cipher(self, address, *cipher_suites): try: secure = self._connect(address) # Construct accepted ciphers, also include a pseudo cipher cipher_suites = list(cipher_suites) if not CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV in cipher_suites: cipher_suites.append( CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) # Consume generator for result in secure.handshake( cipher_suites=cipher_suites, server_name=address[0], ): pass except Exception as error: log.debug('Cipher failed: {0}'.format(error)) return None else: log.debug('Cipher accepted: {0} chosen by server'.format( TLS_CIPHER_SUITE[secure.server_hello.cipher_suite], )) secure.close() return secure.server_hello.cipher_suite
def _recv_server_hello_resume(self, clientHello): ''' Client Server ClientHello (SessionTicket extension) --------> ServerHello (empty SessionTicket extension) NewSessionTicket [ChangeCipherSpec] <-------- Finished [ChangeCipherSpec] Finished --------> Application Data <-------> Application Data ''' for result in self._recv_message(ContentType.handshake, HandshakeType.server_hello): if result in (0, 1): yield result else: break self.server_hello = result self.version = self.server_hello.server_version # Get ChangeCipherSpec for result in self._recv_message(( ContentType.change_cipher_spec, ContentType.handshake, ), (HandshakeType.certificate, ), self.server_hello.certificate_type): if result in (0, 1): yield result else: break if isinstance(result, ChangeCipherSpec): self.change_cipher_spec = result else: log.debug('Did not receive a change_cipher_spec message') self.change_cipher_spec = None
def parse_binary_point(blob, point_size): data = bytearray(blob) point_format = data[0] point_data = data[1:] if point_format in POINT_NULL: if len(point_data) == 0: return dict(x=1, y=1, z=0) else: raise ValueError() elif point_format in POINT_COMPRESSED: log.debug('Parsing compressed point of {0} bytes with size {1}'.format( len(point_data), point_size, )) rest = point_data[point_size:] assert not rest, '{0} bytes remain in compressed binary point'.format( len(rest)) x = bytes_to_long(point_data[:point_size]) y = x * ((point_format - 2) * -1) return dict(x=x, y=y, z=0) elif point_format in POINT_UNCOMPRESSED: log.debug( 'Parsing uncompressed point of {0} bytes with size {1}'.format( len(point_data), point_size, )) rest = point_data[point_size + point_size:] assert not rest, '{0} bytes remain in binary point'.format(len(rest), ) return dict( x=bytes_to_long(point_data[:point_size]), y=bytes_to_long(point_data[point_size:]), z=1, ) else: raise ValueError('Unsupported point format {0:02x}'.format(data[0], ))
def _check_feature_session_resumption(self, address, support): log.debug('Testing session resumption') session_id = bytearray(support['session_id'].decode('hex')) secure = self._connect(address) try: cipher = CipherSuite.filter(key_exchange=('RSA', 'DH')) for result in secure.resume( server_name=address[0], cipher_suites=cipher, session_id=session_id, ): pass if secure.server_hello.session_id == session_id: support['session_resumed'] = True else: support['session_resumed'] = False secure.close() except socket.error as error: raise Probe.Skip('Network error: {0}'.format(error))
def _probe_named_curves(self, address, cipher_suites, elliptic_curves, support, status): while elliptic_curves: try: curve = self._probe_named_curve(address, cipher_suites, elliptic_curves) if curve is None: break else: log.debug('Elliptic Named Curve {0} supported'.format( TLS_EC_CURVE_NAME.get(curve), )) name = TLS_EC_CURVE_NAME.get(curve) support['curve_name'].append(name) status.append({ name: self._get_named_curve_info(curve) }) elliptic_curves.remove(curve) except Exception as error: log.debug('Error Probing Named Curve: {0}'.format( error, )) break
def verify(self, signature, data, signature_algorithm): exponent = self.get_exponent() modulus = self.get_modulus_long() modulus_size = num_bytes(modulus) signature_size = len(signature) if modulus_size != signature_size: log.debug( 'Signature length {0} does not match our key size {1}'.format( signature_size, modulus_size, )) return False prefix = self._add_pkcs1_prefix(data, signature_algorithm) padded = self._add_pkcs1_padding(prefix, 1) c = bytes_to_long(bytearray(signature)) if c >= modulus: log.debug('Signature data exceeds modulus') return False m = pow_mod(c, exponent, modulus) check = long_to_bytes(m, modulus_size) return check == padded
def _test_cipher(self, address, *cipher_suites): try: secure = self._connect(address) # Construct accepted ciphers, also include a pseudo cipher cipher_suites = list(cipher_suites) if not CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV in cipher_suites: cipher_suites.append( CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV ) # Consume generator for result in secure.handshake( cipher_suites=cipher_suites, server_name=address[0], ): pass except Exception as error: log.debug('Cipher failed: {0}'.format(error)) return None else: log.debug('Cipher accepted: {0} chosen by server'.format( TLS_CIPHER_SUITE[secure.server_hello.cipher_suite], )) secure.close() return secure.server_hello.cipher_suite
def _check_features(self, address, support): log.debug('Testing features') secure = self._connect(address) all_ciphers = [ getattr(CipherSuite, suite) for suite in self.collected['ciphers'] ] try: for result in secure.handshake( server_name=address[0], cipher_suites=all_ciphers, compression_methods=[0, 1], heartbeat=True, supports_npn=True, status_request=CertificateStatusType.ocsp, ): pass #import sys; sys.exit() # Test features from ServerHello report hello = secure.server_hello support['compression'] = hello.compression_method != 0 support['heartbeat'] = hello.heartbeat support['next_protos'] = hello.next_protos support['secure_renegotiation'] = hello.secure_renegotiation support['session'] = bool(hello.session_id) if support['session']: support['session_id'] = str(hello.session_id).encode('hex') else: support['session_id'] = None support['session_ticket'] = bool(hello.session_ticket) secure.close() except socket.error as error: raise Probe.Skip('Network error: {0}'.format(error))
def verify(self, signature, data, signature_algorithm): exponent = self.get_exponent() modulus = self.get_modulus_long() modulus_size = num_bytes(modulus) signature_size = len(signature) if modulus_size != signature_size: log.debug( 'Signature length {0} does not match our key size {1}'.format( signature_size, modulus_size, ) ) return False prefix = self._add_pkcs1_prefix(data, signature_algorithm) padded = self._add_pkcs1_padding(prefix, 1) c = bytes_to_long(bytearray(signature)) if c >= modulus: log.debug('Signature data exceeds modulus') return False m = pow_mod(c, exponent, modulus) check = long_to_bytes(m, modulus_size) return check == padded
def _check_elliptic_curves(self, address, support, status): log.debug('Testing Elliptic Curves') ciphers = [getattr(CipherSuite, suite) for suite in self.collected['ciphers'] if '_EC' in suite] if ciphers: log.debug('Discovered {0} usable cipher suites using EC'.format( len(ciphers), )) elliptic_curves = TLS_EC_CURVE_NAME.keys() elliptic_curves.sort() ec_point_formats = TLS_EC_POINT_FORMAT.keys() ec_point_formats.sort() else: log.debug('Discovered no cipher suites using EC, skipping feature') raise Probe.Skip('no cipher suites supporting elliptic curves') try: secure = self._connect(address) for result in secure.handshake( server_name=address[0], cipher_suites=ciphers, elliptic_curves=elliptic_curves, ec_point_formats=ec_point_formats, ): pass except socket.error as error: raise Probe.Skip('Elliptic Curve Point Format handshake failed: ' '{0}'.format(error)) # Map out the names of the EC point formats supported by the server ec_point_formats = secure.server_hello.ec_point_formats support['point_format'] = filter(None, map( TLS_EC_POINT_FORMAT.get, ec_point_formats )) support['curve_name'] = [] support['curve_type'] = None if secure.server_key_exchange is not None: support['curve_type'] = TLS_EC_CURVE_TYPE.get( secure.server_key_exchange.ec_curve_type, 'unsupported', ) if secure.server_key_exchange.ec_curve_type == ECCurveType.named_curve: # Find out what named curves the server supports self._probe_named_curves(address, ciphers, elliptic_curves, support, status)
def _check_elliptic_curves(self, address, support, status): log.debug('Testing Elliptic Curves') ciphers = [ getattr(CipherSuite, suite) for suite in self.collected['ciphers'] if '_EC' in suite ] if ciphers: log.debug('Discovered {0} usable cipher suites using EC'.format( len(ciphers), )) elliptic_curves = TLS_EC_CURVE_NAME.keys() elliptic_curves.sort() ec_point_formats = TLS_EC_POINT_FORMAT.keys() ec_point_formats.sort() else: log.debug('Discovered no cipher suites using EC, skipping feature') raise Probe.Skip('no cipher suites supporting elliptic curves') try: secure = self._connect(address) for result in secure.handshake( server_name=address[0], cipher_suites=ciphers, elliptic_curves=elliptic_curves, ec_point_formats=ec_point_formats, ): pass except socket.error as error: raise Probe.Skip('Elliptic Curve Point Format handshake failed: ' '{0}'.format(error)) # Map out the names of the EC point formats supported by the server ec_point_formats = secure.server_hello.ec_point_formats support['point_format'] = filter( None, map(TLS_EC_POINT_FORMAT.get, ec_point_formats)) support['curve_name'] = [] support['curve_type'] = None if secure.server_key_exchange is not None: support['curve_type'] = TLS_EC_CURVE_TYPE.get( secure.server_key_exchange.ec_curve_type, 'unsupported', ) if secure.server_key_exchange.ec_curve_type == ECCurveType.named_curve: # Find out what named curves the server supports self._probe_named_curves(address, ciphers, elliptic_curves, support, status)
def probe(self, address, certificates): ''' Analyze the public key for each certificate in the ``certificates`` set. Provides the following keys: * ``analysis.public_keys`` Uses the following configuration keys: * ``analyze.public_key.key_sizes``, which is a dictionary, containing: * key type, which is a dictionary, containing: * ``bits``, minimal number of security bits in the key * ``docs``, reference to the minimal size documentation ''' key_infos = [] warnings = defaultdict(list) errors = defaultdict(list) for certificate in certificates: public_key = certificate.get_public_key() log.debug('Analyzing {0} bit {1} key'.format( public_key.get_bits(), public_key.get_type(), )) key_info = dict(status='good') key_bits = public_key.get_bits() key_type = public_key.get_type() key_conf = self.config.get('key_sizes', {}).get(key_type) key_name = '{0} {1} bits'.format(key_type, key_bits) if key_conf: if key_bits < key_conf['bits']: key_info = dict( status='error', reason='{0} bits {1} key is less than {2}: {3}'.format( key_bits, key_type, key_conf['bits'], key_conf['docs'], )) elif key_type == 'rsa': modulus = public_key.get_modulus() exponent = public_key.get_exponent() if modulus < B1023: key_info = dict( status='error', reason='Weak key', ) elif exponent < 65537: key_info = dict( status='error', reason='Weak exponent used 0x{:04x}'.format( exponent, )) else: key_info = dict( status='error', reason='Unsupported public key algorithm', ) key_infos.append({key_name: key_info}) return self.merge( dict( analysis=dict(public_keys=key_infos), errors=errors, warnings=warnings, ))
def probe(self, address, certificates): ''' Analyze the public key for each certificate in the ``certificates`` set. Provides the following keys: * ``analysis.public_keys`` Uses the following configuration keys: * ``analyze.public_key.key_sizes``, which is a dictionary, containing: * key type, which is a dictionary, containing: * ``bits``, minimal number of security bits in the key * ``docs``, reference to the minimal size documentation ''' key_infos = [] warnings = defaultdict(list) errors = defaultdict(list) for certificate in certificates: public_key = certificate.get_public_key() log.debug('Analyzing {0} bit {1} key'.format( public_key.get_bits(), public_key.get_type(), )) key_info = dict(status='good') key_bits = public_key.get_bits() key_type = public_key.get_type() key_conf = self.config.get('key_sizes', {}).get(key_type) key_name = '{0} {1} bits'.format(key_type, key_bits) if key_conf: if key_bits < key_conf['bits']: key_info = dict( status='error', reason='{0} bits {1} key is less than {2}: {3}'.format( key_bits, key_type, key_conf['bits'], key_conf['docs'], ) ) elif key_type == 'rsa': modulus = public_key.get_modulus() exponent = public_key.get_exponent() if modulus < B1023: key_info = dict( status='error', reason='Weak key', ) elif exponent < 65537: key_info = dict( status='error', reason='Weak exponent used 0x{:04x}'.format( exponent, ) ) else: key_info = dict( status='error', reason='Unsupported public key algorithm', ) key_infos.append({key_name: key_info}) return self.merge(dict( analysis=dict(public_keys=key_infos), errors=errors, warnings=warnings, ))
def probe(self, address, certificates): ''' Analyze the cipher suites supported by the server. Also try to establish if the server has a preferred cipher order. Provides the following keys: * ``analysis.ciphers`` * ``analysis.features`` * ``ciphers`` ''' if address is None: raise Probe.Skip('offline; no address supplied') # Features features = {} features['forward_secrecy'] = False # Detect later features['preferred_order'] = False # Detect later # Enlist all ciphers, start with NULL cipher first all_cipher_suites = TLS_CIPHER_SUITE.keys() all_cipher_suites.sort() # Remove our pseudo-cipher, it's added later in the check all_cipher_suites.remove(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) # Test what cipher the server selects cipher_suite1 = self._test_cipher(address, *all_cipher_suites) if cipher_suite1 is None: log.error('No cipher suite selected by server?!') return # Now test what cipher the server selects next all_cipher_suites.remove(cipher_suite1) cipher_suite2 = self._test_cipher(address, *all_cipher_suites) if cipher_suite2 is None: log.error('No cipher suite selected by server?!') return # Now that we have two ciphers, offer them in reverse order. If the # server selects cipher1 again, the server has a preferred order. all_cipher_suites = [ cipher_suite2, cipher_suite1, ] if self._test_cipher(address, *all_cipher_suites) == cipher_suite1: order = True else: order = False # Store feature features['preferred_order'] = order # Start bulk-testing ciphers all_cipher_suites = TLS_CIPHER_SUITE.keys() all_cipher_suites.sort() all_cipher_suites.remove(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) all_cipher_suites.remove(cipher_suite1) all_cipher_suites.remove(cipher_suite2) our_cipher_suites = [ cipher_suite1, cipher_suite2, ] if order: log.info('Serial scanning {0} suites in server order'.format( len(all_cipher_suites), )) while all_cipher_suites: try: cipher_suite = self._test_cipher(address, *all_cipher_suites) except Exception as error: log.debug('None of our suites are accepted, stopping') cipher_suite = None if cipher_suite is None: break else: our_cipher_suites.append(cipher_suite) all_cipher_suites.remove(cipher_suite) else: # If there is no server-preferred order, we can use parallel # (threaded) probing parallel = self.config.get('parallel', 0) if parallel: log.info('Parallel scanning {0} suites'.format( len(all_cipher_suites), )) pool = ThreadPool() for cipher_suite in reversed(all_cipher_suites): pool.add_job(self._test_cipher, (address, cipher_suite)) pool.start(parallel) for cipher_suite in pool.get_results(): if cipher_suite is not None: our_cipher_suites.append(cipher_suite) pool.join() else: log.info('Serial scanning {0} suites'.format( len(all_cipher_suites), )) for cipher_suite in reversed(all_cipher_suites): if self._test_cipher(address, cipher_suite): our_cipher_suites.append(cipher_suite) # Post-processing log.debug('Discovered {0} usable cipher suites'.format( len(our_cipher_suites), )) cipher_names = [] support = [] for cipher in our_cipher_suites: name, info = get_cipher_info(cipher) cipher_names.append(name) if info['encryption'] is None: info.update(dict( status='error', reason='Cipher offers no encryption' )) elif info['authentication'] is None: info.update(dict( status='error', reason='Cipher offers no authentication' )) elif info['encryption'] in ('DES', 'DES40', 'IDEA'): info.update(dict( status='error', reason='Weak encryption', )) elif info['encryption_bits'] < 112: info.update(dict( status='error', reason='Cipher offers weak encryption, only {0} bits'.format( info['encryption_bits'], ) )) elif info['encryption_bits'] < 128: info.update(dict( status='warning', reason='Cipher offers weak encryption, only {0} bits'.format( info['encryption_bits'], ) )) elif info['protocol'] == 'SSL': info.update(dict( status='error', reason='Cipher uses weak SSL implementation', )) else: if info['key_exchange'] in ('DHE', 'ECDHE'): features['forward_secrecy'] = True info['status'] = 'good' support.append({name: info}) self.merge(dict( analysis=dict( ciphers=support, features=features, ), ciphers=cipher_names, ))
def _test_version(self, address, version): ''' Returns the version supported by the server for client version ``version``. :arg address: address tuple :arg version: version tuple, consisting of (``major``, ``minor``) version numbers ''' log.debug('Testing TLS/SSL version 0x{0:02x}{1:02x}'.format(*version)) chello = ClientHello() chello.client_version = version chello.random = get_random_bytes(32) chello.cipher_suites = TLS_CIPHER_SUITE.keys() # Be very permissive packet = chello.render() remote = Remote(address) remote.connect() try: header = RecordHeader3() header.content_type = chello.content_type header.version = version header.size = len(packet) # Send request r = header.render() + packet remote.send_all(r) # Read response try: r = Reader(bytearray(remote.recv(1024))) content_type = r.get(1) except SyntaxError: raise ValueError('Expected record header') if content_type != ContentType.handshake: raise SyntaxError('Expected handshake, got {0} (0x{1:02x})'.format( TLS_CONTENT_TYPE.get(content_type, content_type), content_type, )) header_version = (r.get(1), r.get(1)) # SSLv3/TLSv1.x if header_version >= (0x03, 0x00): header_size = r.get(2) b = r.get_fixed(header_size) if b[0] != HandshakeType.server_hello: raise SyntaxError('Expected server hello, got {0} (0x{1:02x})'.format( TLS_HANDSHAKE_TYPE.get(b[0], b[0]), b[0] )) server_hello = ServerHello().parse(Reader(b[1:])) return server_hello.server_version # SSLv2 else: return header_version finally: remote.close()
def check_trust(self, certificate): ''' Check if the certificate provided in ``certificate`` is trusted by: 1. checking if the certificate has a trust anchor in our trust store, if so, check the certificate validity with the trust store 2. checking if the previous certificate was trusted and is a certificate authority, if so, check the certificate validity with the previously provided certificate in the chain 3. checking if the certificate is self signed ''' log.debug('Analyzing {0}'.format(certificate.get_subject_str())) subject_hash = certificate.get_subject_hash() issuer = certificate.get_issuer() issuer_hash = certificate.get_issuer_hash() issuer_name = issuer.get( 'commonName', issuer.get('organizationName', issuer_hash) ) trusted = (subject_hash in TRUST_STORE) if subject_hash not in self.chain_hash: self.chain.append(certificate) self.chain_hash.append(subject_hash) if self.trust and self.trust[-1]['status'] != 'good': yield dict( status='error', reason='Invalid chain', ) return # Self-signed certificate if subject_hash == issuer_hash: if subject_hash in TRUST_STORE: # Certificate should be able to verify itself issuer = TRUST_STORE[subject_hash] status = issuer.verify(certificate) if status is True: log.debug('Issuer "{0}" in trust store'.format( issuer_name, )) yield dict( status='good', reason='In trust store', ) return elif status is None: yield dict( status='unknown', reason='Unable to verify (local issue)', ) # Untrusted self-signed certificate log.debug('Self-signed certificate in chain') yield dict(trust=dict( status='error', reason='Self-signed certificate in chain', )) return also_check = [] if issuer_hash in TRUST_STORE: issuer = TRUST_STORE[issuer_hash] log.debug('Issuer "{0}" in trust store'.format(issuer_name)) if issuer_hash not in self.chain_hash: also_check.append(issuer) if issuer.verify(certificate): yield dict( status='good', reason='In trust store', ) else: yield dict( status='error', reason='Verification failed', ) elif issuer_hash in self.chain_hash: log.debug('Issuer {0} in trust chain'.format(issuer_name)) issuer = self.chain[self.chain_hash.index(issuer_hash)] if issuer.verify(certificate): yield dict( status='good', reason='In trust chain', ) else: yield dict( status='error', reason='Verification failed', ) else: yield dict( status='error', reason='Issuer {0} unknown'.format(issuer_name), ) for check in also_check: for trust in self.check_trust(check): yield trust
def _recv_message(self, expected_type, secondary_type=None, constructor_type=None): if not isinstance(expected_type, tuple): expected_type = (expected_type, ) while True: for result in self._recv_next_record(): if result in (0, 1): yield result record_header, r = result content_type = record_header.content_type log.debug('Received {0} record'.format( TLS_CONTENT_TYPE.get(content_type, content_type))) if content_type == ContentType.application_data: if r.pos == len(r): continue if content_type not in expected_type: if content_type == ContentType.alert: Alert().parse(r).throw() raise ValueError( 'Unexpected record type {0}, expected {1}'.format( TLS_CONTENT_TYPE.get(content_type, content_type), map(TLS_CONTENT_TYPE.get, expected_type))) # Parse based on content_type if content_type == ContentType.alert: yield Alert().parse(r) elif content_type == ContentType.change_cipher_spec: yield ChangeCipherSpec().parse(r) elif content_type == ContentType.handshake: if not isinstance(secondary_type, tuple): secondary_type = (secondary_type, ) if record_header.v2: sub_type = r.get(1) if sub_type != HandshakeType.client_hello: raise TypeError('Expected client hello') if HandshakeType.client_hello not in secondary_type: raise TypeError('Unexpected message') sub_type = HandshakeType.client_hello else: sub_type = r.get(1) if sub_type not in secondary_type: raise TypeError( 'Unexpected message {0} ({1}), ' 'expected {2}/{3}'.format( sub_type, TLS_HANDSHAKE_TYPE.get(sub_type, 'unknown'), map(TLS_HANDSHAKE_TYPE.get, expected_type), map(TLS_HANDSHAKE_TYPE.get, secondary_type))) log.debug('... with sub type {0}'.format( TLS_HANDSHAKE_TYPE.get(sub_type, sub_type))) if sub_type == HandshakeType.client_hello: yield ClientHello(record_header.v2).parse(r) elif sub_type == HandshakeType.server_hello: yield ServerHello(record_header.v2).parse(r) elif sub_type == HandshakeType.certificate: yield Certificate(constructor_type).parse(r) elif sub_type == HandshakeType.certificate_status: yield CertificateStatus().parse(r) elif sub_type == HandshakeType.server_key_exchange: yield ServerKeyExchange(constructor_type).parse(r) elif sub_type == HandshakeType.server_hello_done: yield ServerHelloDone().parse(r) else: raise AssertionError( TLS_HANDSHAKE_TYPE.get(sub_type, sub_type))
def get_host_name(self, host, port=0): # Try IPv6 resolving try: socket.inet_pton(socket.AF_INET6, host) if have_dns: try: name = reversename.from_address(host) name = str(resolver.query(name, 'PTR')[0]).rstrip('.') log.debug('{0} resolved to {1}'.format(host, name)) return name except Exception as error: log.debug('{0} failed to resolve: {1}'.format(host, error)) return host else: return host except socket.error: pass # Try IPv4 resolving try: socket.inet_pton(socket.AF_INET, host) if have_dns: try: name = reversename.from_address(host) name = str(resolver.query(name, 'PTR')[0]).rstrip('.') log.debug('{0} resolved to {1}'.format(host, name)) return name except Exception as error: log.debug('{0} failed to resolve: {1}'.format(host, error)) return host else: name = socket.gethostbyaddr(host)[0] if name: log.debug('{0} resolved to {1}'.format(host, name)) return name except socket.error: pass # Give up log.debug('{0} failed to resolve: not an IP'.format(host)) return host
def probe(self, address, certificates): ''' Tests for the Heartbleed TLS attack which targets protocols that have the heartbeat extension enabled and do improper boundary checks, such as found in OpenSSL versions between 1.0.1 - 1.0.1f. Provides the following keys: * ``weakness.heartbleed`` ''' if address is None: raise Probe.Skip('offline; no address supplied') weakness = {} weakness['status'] = 'good' weakness['exists'] = False weakness['reason'] = 'Heartbeat not enabled' try: remote = socket.create_connection(address) except socket.error as error: raise Probe.Skip('Network error: {0}'.format(error)) if remote: remote.send(TLS_HELLO) while True: try: typ, version, payload = self._get_msg(remote) except ValueError as error: log.debug('Oops: {0}'.format(error)) remote = None break else: if typ == 22 and ord(payload[0]) == 0x0e: break if remote: remote.send(TLS_HEARTBEAT) while True: remote.send(TLS_HEARTBEAT) try: typ, version, payload = self._get_msg(remote) except ValueError as error: log.debug('Oops: {0}'.format(error)) break else: if typ == 24: log.debug('Received heartbeat response') if len(payload) > 3: weakness['status'] = 'error' weakness['exists'] = True weakness['reason'] = 'Server returned more data ' +\ 'than it should have' else: weakness['reason'] = 'Server-side fixed' break elif typ == 21: # Alert break remote.close() self.merge(dict(weakness=dict(heartbleed=weakness)))
def probe(self, address, certificates): ''' Analyze the available protocol versions and protocol intolerance. The TLS protocol negotiates what protocol version to use like so: 1. The client initiates a TLS handshake, sending a ClientHello packet, including the highest protocol version it supports. 2. The server responds with a ServerHello packet, indicating the higest protocol version it supports, but no higher than the version requested by the client. If the Server not not willing to support older versions, it will respond with an Alert packet in stead. So if the client were to request a hypothetical TLSv2.0 (``0x4000``) protocol version, and the server supports up to TLSv1.2 (``0x0303``), it should reply with version ``0x0303``. Unfortunately a lot of TLS/SSL stacks are broken and respond with an incorrect version, called protocol version intolerance. This may lead to interoperability issues for clients that support newer versions of the protocol. If the server claims to support newer versions, but it doesn't know how to properly respond to the ClientHello, the handshake setup may fail and the client might not be able to connect. Provides the following keys: * ``analysis.protocol_intolerance`` * ``analysis.protocols`` ''' if address is None: raise Probe.Skip('offline; no address supplied') protocols = [] for version, statuses in PROTOCOLS.iteritems(): name = TLS_VERSION[version] try: self._test_version(address, version) status = statuses[STATUS_OK] status['available'] = True protocols.append({name: status}) except Exception as error: status = statuses[STATUS_ERROR] status['available'] = False status['reason'] = status.get( 'reason', 'Not available: {0}'.format(error) ) protocols.append({name: status}) protocol_intolerance = [] for version, max_server_version in TLS_VERSION_TOLERANCE.iteritems(): name = '{0} {1}.{2}'.format( 'TLS' if version[0] > 2 else 'SSL', version[0] - 2, version[1], ) try: server_version = self._test_version(address, version) server_name = '{0} {1}.{2}'.format( 'TLS' if server_version[0] > 2 else 'SSL', server_version[0] - 2, server_version[1], ) if server_version > max_server_version: log.debug('Intolerant for version {0} (got {1} !?)'.format( name, server_name, )) protocol_intolerance.append(name) else: log.debug('Proper response for version {0} (got {1})'.format( name, server_name, )) except Exception as error: log.debug('Intolerant for version {0} (got error: {1})'.format( name, error, )) protocol_intolerance.append(name) protocols.sort() protocols.reverse() protocol_intolerance.sort() self.merge(dict( analysis=dict( protocols=protocols, protocol_intolerance=protocol_intolerance, ) ))
def check_trust(self, certificate): ''' Check if the certificate provided in ``certificate`` is trusted by: 1. checking if the certificate has a trust anchor in our trust store, if so, check the certificate validity with the trust store 2. checking if the previous certificate was trusted and is a certificate authority, if so, check the certificate validity with the previously provided certificate in the chain 3. checking if the certificate is self signed ''' log.debug('Analyzing {0}'.format(certificate.get_subject_str())) subject_hash = certificate.get_subject_hash() issuer = certificate.get_issuer() issuer_hash = certificate.get_issuer_hash() issuer_name = issuer.get('commonName', issuer.get('organizationName', issuer_hash)) trusted = (subject_hash in TRUST_STORE) if subject_hash not in self.chain_hash: self.chain.append(certificate) self.chain_hash.append(subject_hash) if self.trust and self.trust[-1]['status'] != 'good': yield dict( status='error', reason='Invalid chain', ) return # Self-signed certificate if subject_hash == issuer_hash: if subject_hash in TRUST_STORE: # Certificate should be able to verify itself issuer = TRUST_STORE[subject_hash] status = issuer.verify(certificate) if status is True: log.debug('Issuer "{0}" in trust store'.format( issuer_name, )) yield dict( status='good', reason='In trust store', ) return elif status is None: yield dict( status='unknown', reason='Unable to verify (local issue)', ) # Untrusted self-signed certificate log.debug('Self-signed certificate in chain') yield dict(trust=dict( status='error', reason='Self-signed certificate in chain', )) return also_check = [] if issuer_hash in TRUST_STORE: issuer = TRUST_STORE[issuer_hash] log.debug('Issuer "{0}" in trust store'.format(issuer_name)) if issuer_hash not in self.chain_hash: also_check.append(issuer) if issuer.verify(certificate): yield dict( status='good', reason='In trust store', ) else: yield dict( status='error', reason='Verification failed', ) elif issuer_hash in self.chain_hash: log.debug('Issuer {0} in trust chain'.format(issuer_name)) issuer = self.chain[self.chain_hash.index(issuer_hash)] if issuer.verify(certificate): yield dict( status='good', reason='In trust chain', ) else: yield dict( status='error', reason='Verification failed', ) else: yield dict( status='error', reason='Issuer {0} unknown'.format(issuer_name), ) for check in also_check: for trust in self.check_trust(check): yield trust
def _recv_message(self, expected_type, secondary_type=None, constructor_type=None): if not isinstance(expected_type, tuple): expected_type= (expected_type,) while True: for result in self._recv_next_record(): if result in (0, 1): yield result record_header, r = result content_type = record_header.content_type log.debug('Received {0} record'.format( TLS_CONTENT_TYPE.get(content_type, content_type) )) if content_type == ContentType.application_data: if r.pos == len(r): continue if content_type not in expected_type: if content_type == ContentType.alert: Alert().parse(r).throw() raise ValueError( 'Unexpected record type {0}, expected {1}'.format( TLS_CONTENT_TYPE.get(content_type, content_type), map(TLS_CONTENT_TYPE.get, expected_type) ) ) # Parse based on content_type if content_type == ContentType.alert: yield Alert().parse(r) elif content_type == ContentType.change_cipher_spec: yield ChangeCipherSpec().parse(r) elif content_type == ContentType.handshake: if not isinstance(secondary_type, tuple): secondary_type = (secondary_type,) if record_header.v2: sub_type = r.get(1) if sub_type != HandshakeType.client_hello: raise TypeError('Expected client hello') if HandshakeType.client_hello not in secondary_type: raise TypeError('Unexpected message') sub_type = HandshakeType.client_hello else: sub_type = r.get(1) if sub_type not in secondary_type: raise TypeError( 'Unexpected message {0} ({1}), ' 'expected {2}/{3}'.format( sub_type, TLS_HANDSHAKE_TYPE.get(sub_type, 'unknown'), map(TLS_HANDSHAKE_TYPE.get, expected_type), map(TLS_HANDSHAKE_TYPE.get, secondary_type) ) ) log.debug('... with sub type {0}'.format( TLS_HANDSHAKE_TYPE.get(sub_type, sub_type) )) if sub_type == HandshakeType.client_hello: yield ClientHello(record_header.v2).parse(r) elif sub_type == HandshakeType.server_hello: yield ServerHello(record_header.v2).parse(r) elif sub_type == HandshakeType.certificate: yield Certificate(constructor_type).parse(r) elif sub_type == HandshakeType.certificate_status: yield CertificateStatus().parse(r) elif sub_type == HandshakeType.server_key_exchange: yield ServerKeyExchange(constructor_type).parse(r) elif sub_type == HandshakeType.server_hello_done: yield ServerHelloDone().parse(r) else: raise AssertionError(TLS_HANDSHAKE_TYPE.get(sub_type, sub_type))
def probe(self, address, certificates): ''' Analyze the cipher suites supported by the server. Also try to establish if the server has a preferred cipher order. Provides the following keys: * ``analysis.ciphers`` * ``analysis.features`` * ``ciphers`` ''' if address is None: raise Probe.Skip('offline; no address supplied') # Features features = {} features['forward_secrecy'] = False # Detect later features['preferred_order'] = False # Detect later # Enlist all ciphers, start with NULL cipher first all_cipher_suites = TLS_CIPHER_SUITE.keys() all_cipher_suites.sort() # Remove our pseudo-cipher, it's added later in the check all_cipher_suites.remove(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) # Test what cipher the server selects cipher_suite1 = self._test_cipher(address, *all_cipher_suites) if cipher_suite1 is None: log.error('No cipher suite selected by server?!') return # Now test what cipher the server selects next all_cipher_suites.remove(cipher_suite1) cipher_suite2 = self._test_cipher(address, *all_cipher_suites) if cipher_suite2 is None: log.error('No cipher suite selected by server?!') return # Now that we have two ciphers, offer them in reverse order. If the # server selects cipher1 again, the server has a preferred order. all_cipher_suites = [ cipher_suite2, cipher_suite1, ] if self._test_cipher(address, *all_cipher_suites) == cipher_suite1: order = True else: order = False # Store feature features['preferred_order'] = order # Start bulk-testing ciphers all_cipher_suites = TLS_CIPHER_SUITE.keys() all_cipher_suites.sort() all_cipher_suites.remove(CipherSuite.TLS_EMPTY_RENEGOTIATION_INFO_SCSV) all_cipher_suites.remove(cipher_suite1) all_cipher_suites.remove(cipher_suite2) our_cipher_suites = [ cipher_suite1, cipher_suite2, ] if order: log.info('Serial scanning {0} suites in server order'.format( len(all_cipher_suites), )) while all_cipher_suites: try: cipher_suite = self._test_cipher(address, *all_cipher_suites) except Exception as error: log.debug('None of our suites are accepted, stopping') cipher_suite = None if cipher_suite is None: break else: our_cipher_suites.append(cipher_suite) all_cipher_suites.remove(cipher_suite) else: # If there is no server-preferred order, we can use parallel # (threaded) probing parallel = self.config.get('parallel', 0) if parallel: log.info('Parallel scanning {0} suites'.format( len(all_cipher_suites), )) pool = ThreadPool() for cipher_suite in reversed(all_cipher_suites): pool.add_job(self._test_cipher, (address, cipher_suite)) pool.start(parallel) for cipher_suite in pool.get_results(): if cipher_suite is not None: our_cipher_suites.append(cipher_suite) pool.join() else: log.info('Serial scanning {0} suites'.format( len(all_cipher_suites), )) for cipher_suite in reversed(all_cipher_suites): if self._test_cipher(address, cipher_suite): our_cipher_suites.append(cipher_suite) # Post-processing log.debug('Discovered {0} usable cipher suites'.format( len(our_cipher_suites), )) cipher_names = [] support = [] for cipher in our_cipher_suites: name, info = get_cipher_info(cipher) cipher_names.append(name) if info['encryption'] is None: info.update( dict(status='error', reason='Cipher offers no encryption')) elif info['authentication'] is None: info.update( dict(status='error', reason='Cipher offers no authentication')) elif info['encryption'] in ('DES', 'DES40', 'IDEA'): info.update(dict( status='error', reason='Weak encryption', )) elif info['encryption_bits'] < 112: info.update( dict(status='error', reason='Cipher offers weak encryption, only {0} bits'. format(info['encryption_bits'], ))) elif info['encryption_bits'] < 128: info.update( dict(status='warning', reason='Cipher offers weak encryption, only {0} bits'. format(info['encryption_bits'], ))) elif info['protocol'] == 'SSL': info.update( dict( status='error', reason='Cipher uses weak SSL implementation', )) else: if info['key_exchange'] in ('DHE', 'ECDHE'): features['forward_secrecy'] = True info['status'] = 'good' support.append({name: info}) self.merge( dict( analysis=dict( ciphers=support, features=features, ), ciphers=cipher_names, ))