diff --git a/tests/tlstest.py b/tests/tlstest.py index 53122fd6..8a050387 100755 --- a/tests/tlstest.py +++ b/tests/tlstest.py @@ -423,8 +423,31 @@ def connect(): #print(" Next-Protocol Negotiated: %s" % connection.next_proto) assert(connection.next_proto == b'spdy/2') connection.close() - - print('Test 25 - good standard XMLRPC https client') + + print("Test 25.a - FALLBACK_SCSV") + synchro.recv(1) + connection = connect() + settings = HandshakeSettings() + settings.sendFallbackSCSV = True + connection.handshakeClientCert(settings=settings) + testConnClient(connection) + connection.close() + + print("Test 25.b - FALLBACK_SCSV") + synchro.recv(1) + connection = connect() + settings = HandshakeSettings() + settings.sendFallbackSCSV = True + settings.maxVersion = (3, 2) + try: + connection.handshakeClientCert(settings=settings) + assert() + except TLSRemoteAlert as alert: + if alert.description != AlertDescription.inappropriate_fallback: + raise + connection.close() + + print('Test 26 - good standard XMLRPC https client') address = address[0], address[1]+1 synchro.recv(1) try: @@ -441,7 +464,7 @@ def connect(): synchro.recv(1) assert server.pow(2,4) == 16 - print('Test 26 - good tlslite XMLRPC client') + print('Test 27 - good tlslite XMLRPC client') transport = XMLRPCTransport(ignoreAbruptClose=True) server = xmlrpclib.Server('https://%s:%s' % address, transport) synchro.recv(1) @@ -449,22 +472,22 @@ def connect(): synchro.recv(1) assert server.pow(2,4) == 16 - print('Test 27 - good XMLRPC ignored protocol') + print('Test 28 - good XMLRPC ignored protocol') server = xmlrpclib.Server('http://%s:%s' % address, transport) synchro.recv(1) assert server.add(1,2) == 3 synchro.recv(1) assert server.pow(2,4) == 16 - - print("Test 28 - Internet servers test") + + print("Test 29 - Internet servers test") try: i = IMAP4_TLS("cyrus.andrew.cmu.edu") i.login("anonymous", "anonymous@anonymous.net") i.logout() - print("Test 28: IMAP4 good") + print("Test 30: IMAP4 good") p = POP3_TLS("pop.gmail.com") p.quit() - print("Test 29: POP3 good") + print("Test 31: POP3 good") except socket.error as e: print("Non-critical error: socket error trying to reach internet server: ", e) @@ -843,7 +866,25 @@ def server_bind(self): testConnServer(connection) connection.close() - print("Tests 25-27 - XMLRPXC server") + print("Test 25.a - FALLBACK_SCSV") + synchro.send(b'R') + connection = connect() + connection.handshakeServer(certChain=x509Chain, privateKey=x509Key) + testConnServer(connection) + connection.close() + + print("Test 25.b - FALLBACK_SCSV") + synchro.send(b'R') + connection = connect() + try: + connection.handshakeServer(certChain=x509Chain, privateKey=x509Key) + assert() + except TLSLocalAlert as alert: + if alert.description != AlertDescription.inappropriate_fallback: + raise + connection.close() + + print("Tests 26-28 - XMLRPXC server") address = address[0], address[1]+1 class Server(TLSXMLRPCServer): diff --git a/tlslite/constants.py b/tlslite/constants.py index 4165de03..5445d93b 100644 --- a/tlslite/constants.py +++ b/tlslite/constants.py @@ -104,6 +104,7 @@ class AlertDescription: protocol_version = 70 insufficient_security = 71 internal_error = 80 + inappropriate_fallback = 86 user_canceled = 90 no_renegotiation = 100 unknown_psk_identity = 115 @@ -115,6 +116,9 @@ class CipherSuite: # We actually don't do any renegotiation, but this # prevents renegotiation attacks TLS_EMPTY_RENEGOTIATION_INFO_SCSV = 0x00FF + + # draft-ietf-tls-downgrade-scsv-03 + TLS_FALLBACK_SCSV = 0x5600 TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA = 0xC01A TLS_SRP_SHA_WITH_AES_128_CBC_SHA = 0xC01D diff --git a/tlslite/errors.py b/tlslite/errors.py index 06826b39..2c523176 100644 --- a/tlslite/errors.py +++ b/tlslite/errors.py @@ -63,6 +63,7 @@ class TLSAlert(TLSError): AlertDescription.protocol_version: "protocol_version",\ AlertDescription.insufficient_security: "insufficient_security",\ AlertDescription.internal_error: "internal_error",\ + AlertDescription.inappropriate_fallback: "inappropriate_fallback",\ AlertDescription.user_canceled: "user_canceled",\ AlertDescription.no_renegotiation: "no_renegotiation",\ AlertDescription.unknown_psk_identity: "unknown_psk_identity"} diff --git a/tlslite/handshakesettings.py b/tlslite/handshakesettings.py index ee37c30f..7053edd9 100644 --- a/tlslite/handshakesettings.py +++ b/tlslite/handshakesettings.py @@ -95,6 +95,9 @@ class HandshakeSettings(object): Note that TACK support is not standardized by IETF and uses a temporary TLS Extension number, so should NOT be used in production software. + + @type sendFallbackSCSV: bool + @ivar sendFallbackSCSV: Whether to, as a client, send FALLBACK_SCSV. """ def __init__(self): self.minKeySize = 1023 @@ -106,6 +109,7 @@ def __init__(self): self.minVersion = (3,1) self.maxVersion = (3,3) self.useExperimentalTackExtension = False + self.sendFallbackSCSV = False # Validates the min/max fields, and certificateTypes # Filters out unsupported cipherNames and cipherImplementations @@ -119,6 +123,7 @@ def _filter(self): other.certificateTypes = self.certificateTypes other.minVersion = self.minVersion other.maxVersion = self.maxVersion + other.sendFallbackSCSV = self.sendFallbackSCSV if not cipherfactory.tripleDESPresent: other.cipherNames = [e for e in self.cipherNames if e != "3des"] diff --git a/tlslite/tlsconnection.py b/tlslite/tlsconnection.py index b516e008..85eb0306 100644 --- a/tlslite/tlsconnection.py +++ b/tlslite/tlsconnection.py @@ -506,6 +506,12 @@ def _clientSendClientHello(self, settings, session, srpUsername, else: assert(False) + #Add any SCSVs. These are not real cipher suites, but signaling + #values which reuse the cipher suite field in the ClientHello. + wireCipherSuites = list(cipherSuites) + if settings.sendFallbackSCSV: + wireCipherSuites.append(CipherSuite.TLS_FALLBACK_SCSV) + #Initialize acceptable certificate types certificateTypes = settings._getCertificateTypes() @@ -519,7 +525,7 @@ def _clientSendClientHello(self, settings, session, srpUsername, else: clientHello = ClientHello() clientHello.create(settings.maxVersion, getRandomBytes(32), - session.sessionID, cipherSuites, + session.sessionID, wireCipherSuites, certificateTypes, session.srpUsername, reqTack, nextProtos is not None, @@ -529,7 +535,7 @@ def _clientSendClientHello(self, settings, session, srpUsername, else: clientHello = ClientHello() clientHello.create(settings.maxVersion, getRandomBytes(32), - bytearray(0), cipherSuites, + bytearray(0), wireCipherSuites, certificateTypes, srpUsername, reqTack, nextProtos is not None, @@ -1257,6 +1263,13 @@ def _serverGetClientHello(self, settings, certChain, verifierDB, #Set the version to the client's version self.version = clientHello.client_version + #Detect if the client performed an inappropriate fallback. + if clientHello.client_version < settings.maxVersion and \ + CipherSuite.TLS_FALLBACK_SCSV in clientHello.cipher_suites: + for result in self._sendError(\ + AlertDescription.inappropriate_fallback): + yield result + #If resumption was requested and we have a session cache... if clientHello.session_id and sessionCache: session = None