Extract Certificate Transparency SCTs from stapled OCSP responses

BUG=309578

Review URL: https://codereview.chromium.org/102613006

git-svn-id: http://src.chromium.org/svn/trunk/src/third_party/tlslite@241083 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
diff --git a/README.chromium b/README.chromium
index ec2fad8..ed0d793 100644
--- a/README.chromium
+++ b/README.chromium
@@ -38,3 +38,4 @@
   Certificate Timestamps over a TLS extension.
 - patches/fallback_scsv.patch: add support for TLS_FALLBACK_SCSV. See
   https://tools.ietf.org/html/draft-bmoeller-tls-downgrade-scsv-01
+- patches/status_request.patch: add support for sending stapled OCSP responses.
diff --git a/patches/status_request.patch b/patches/status_request.patch
new file mode 100644
index 0000000..15f01d4
--- /dev/null
+++ b/patches/status_request.patch
@@ -0,0 +1,208 @@
+diff --git a/third_party/tlslite/tlslite/TLSConnection.py b/third_party/tlslite/tlslite/TLSConnection.py
+index e6ce187..94ee5eb 100644
+--- a/third_party/tlslite/tlslite/TLSConnection.py
++++ b/third_party/tlslite/tlslite/TLSConnection.py
+@@ -937,8 +937,8 @@ class TLSConnection(TLSRecordLayer):
+                         certChain=None, privateKey=None, reqCert=False,
+                         sessionCache=None, settings=None, checker=None,
+                         reqCAs=None, tlsIntolerant=0,
+-                        signedCertTimestamps=None,
+-                        fallbackSCSV=False):
++                        signedCertTimestamps=None, fallbackSCSV=False,
++                        ocspResponse=None):
+         """Perform a handshake in the role of server.
+ 
+         This function performs an SSL or TLS handshake.  Depending on
+@@ -1014,6 +1014,16 @@ class TLSConnection(TLSRecordLayer):
+         binary 8-bit string) that will be sent as a TLS extension whenever
+         the client announces support for the extension.
+ 
++        @type ocspResponse: str
++        @param ocspResponse: An OCSP response (as a binary 8-bit string) that
++        will be sent stapled in the handshake whenever the client announces
++        support for the status_request extension.
++        Note that the response is sent independent of the ClientHello
++        status_request extension contents, and is thus only meant for testing
++        environments. Real OCSP stapling is more complicated as it requires
++        choosing a suitable response based on the ClientHello status_request
++        extension contents.
++
+         @raise socket.error: If a socket error occurs.
+         @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed
+         without a preceding alert.
+@@ -1024,7 +1034,7 @@ class TLSConnection(TLSRecordLayer):
+         for result in self.handshakeServerAsync(sharedKeyDB, verifierDB,
+                 certChain, privateKey, reqCert, sessionCache, settings,
+                 checker, reqCAs, tlsIntolerant, signedCertTimestamps,
+-                fallbackSCSV):
++                fallbackSCSV, ocspResponse):
+             pass
+ 
+ 
+@@ -1033,7 +1043,7 @@ class TLSConnection(TLSRecordLayer):
+                              sessionCache=None, settings=None, checker=None,
+                              reqCAs=None, tlsIntolerant=0,
+                              signedCertTimestamps=None,
+-                             fallbackSCSV=False):
++                             fallbackSCSV=False, ocspResponse=None):
+         """Start a server handshake operation on the TLS connection.
+ 
+         This function returns a generator which behaves similarly to
+@@ -1053,7 +1063,8 @@ class TLSConnection(TLSRecordLayer):
+             reqCAs=reqCAs,
+             tlsIntolerant=tlsIntolerant,
+             signedCertTimestamps=signedCertTimestamps,
+-            fallbackSCSV=fallbackSCSV)
++            fallbackSCSV=fallbackSCSV, ocspResponse=ocspResponse)
++
+         for result in self._handshakeWrapperAsync(handshaker, checker):
+             yield result
+ 
+@@ -1062,7 +1073,7 @@ class TLSConnection(TLSRecordLayer):
+                                     certChain, privateKey, reqCert,
+                                     sessionCache, settings, reqCAs,
+                                     tlsIntolerant, signedCertTimestamps,
+-                                    fallbackSCSV):
++                                    fallbackSCSV, ocspResponse):
+ 
+         self._handshakeStart(client=False)
+ 
+@@ -1439,10 +1450,14 @@ class TLSConnection(TLSRecordLayer):
+                     sessionID, cipherSuite, certificateType)
+             serverHello.channel_id = clientHello.channel_id
+             if clientHello.support_signed_cert_timestamps:
+-                serverHello.signed_cert_timestamps = signedCertTimestamps
++              serverHello.signed_cert_timestamps = signedCertTimestamps
++            serverHello.status_request = (clientHello.status_request and
++                                          ocspResponse)
+             doingChannelID = clientHello.channel_id
+             msgs.append(serverHello)
+             msgs.append(Certificate(certificateType).create(serverCertChain))
++            if serverHello.status_request:
++                msgs.append(CertificateStatus().create(ocspResponse))
+             if reqCert and reqCAs:
+                 msgs.append(CertificateRequest().create([], reqCAs))
+             elif reqCert:
+diff --git a/third_party/tlslite/tlslite/constants.py b/third_party/tlslite/tlslite/constants.py
+index 23e3dcb..d027ef5 100644
+--- a/third_party/tlslite/tlslite/constants.py
++++ b/third_party/tlslite/tlslite/constants.py
+@@ -22,6 +22,7 @@ class HandshakeType:
+     certificate_verify = 15
+     client_key_exchange = 16
+     finished = 20
++    certificate_status = 22
+     encrypted_extensions = 203
+ 
+ class ContentType:
+@@ -31,7 +32,11 @@ class ContentType:
+     application_data = 23
+     all = (20,21,22,23)
+ 
++class CertificateStatusType:
++    ocsp = 1
++
+ class ExtensionType:
++    status_request = 5  # OCSP stapling
+     signed_cert_timestamps = 18  # signed_certificate_timestamp in RFC 6962
+     channel_id = 30031
+ 
+diff --git a/third_party/tlslite/tlslite/messages.py b/third_party/tlslite/tlslite/messages.py
+index 296f422..497ef60 100644
+--- a/third_party/tlslite/tlslite/messages.py
++++ b/third_party/tlslite/tlslite/messages.py
+@@ -132,6 +132,7 @@ class ClientHello(HandshakeMsg):
+         self.srp_username = None        # a string
+         self.channel_id = False
+         self.support_signed_cert_timestamps = False
++        self.status_request = False
+ 
+     def create(self, version, random, session_id, cipher_suites,
+                certificate_types=None, srp_username=None):
+@@ -182,6 +183,19 @@ class ClientHello(HandshakeMsg):
+                         if extLength:
+                             raise SyntaxError()
+                         self.support_signed_cert_timestamps = True
++                    elif extType == ExtensionType.status_request:
++                        # Extension contents are currently ignored.
++                        # According to RFC 6066, this is not strictly forbidden
++                        # (although it is suboptimal):
++                        # Servers that receive a client hello containing the
++                        # "status_request" extension MAY return a suitable
++                        # certificate status response to the client along with
++                        # their certificate.  If OCSP is requested, they
++                        # SHOULD use the information contained in the extension
++                        # when selecting an OCSP responder and SHOULD include
++                        # request_extensions in the OCSP request.
++                        p.getFixBytes(extLength)
++                        self.status_request = True
+                     else:
+                         p.getFixBytes(extLength)
+                     soFar += 4 + extLength
+@@ -230,6 +244,7 @@ class ServerHello(HandshakeMsg):
+         self.compression_method = 0
+         self.channel_id = False
+         self.signed_cert_timestamps = None
++        self.status_request = False
+ 
+     def create(self, version, random, session_id, cipher_suite,
+                certificate_type):
+@@ -282,6 +297,9 @@ class ServerHello(HandshakeMsg):
+         if self.signed_cert_timestamps:
+             extLength += 4 + len(self.signed_cert_timestamps)
+ 
++        if self.status_request:
++            extLength += 4
++
+         if extLength != 0:
+             w.add(extLength, 2)
+ 
+@@ -299,6 +317,10 @@ class ServerHello(HandshakeMsg):
+             w.add(ExtensionType.signed_cert_timestamps, 2)
+             w.addVarSeq(stringToBytes(self.signed_cert_timestamps), 1, 2)
+ 
++        if self.status_request:
++            w.add(ExtensionType.status_request, 2)
++            w.add(0, 2)
++
+         return HandshakeMsg.postWrite(self, w, trial)
+ 
+ class Certificate(HandshakeMsg):
+@@ -367,6 +389,37 @@ class Certificate(HandshakeMsg):
+             raise AssertionError()
+         return HandshakeMsg.postWrite(self, w, trial)
+ 
++class CertificateStatus(HandshakeMsg):
++    def __init__(self):
++        self.contentType = ContentType.handshake
++
++    def create(self, ocsp_response):
++        self.ocsp_response = ocsp_response
++        return self
++
++    # Defined for the sake of completeness, even though we currently only
++    # support sending the status message (server-side), not requesting
++    # or receiving it (client-side).
++    def parse(self, p):
++        p.startLengthCheck(3)
++        status_type = p.get(1)
++        # Only one type is specified, so hardwire it.
++        if status_type != CertificateStatusType.ocsp:
++            raise SyntaxError()
++        ocsp_response = p.getVarBytes(3)
++        if not ocsp_response:
++            # Can't be empty
++            raise SyntaxError()
++        self.ocsp_response = ocsp_response
++        return self
++
++    def write(self, trial=False):
++        w = HandshakeMsg.preWrite(self, HandshakeType.certificate_status,
++                                  trial)
++        w.add(CertificateStatusType.ocsp, 1)
++        w.addVarSeq(stringToBytes(self.ocsp_response), 1, 3)
++        return HandshakeMsg.postWrite(self, w, trial)
++
+ class CertificateRequest(HandshakeMsg):
+     def __init__(self):
+         self.contentType = ContentType.handshake
diff --git a/tlslite/TLSConnection.py b/tlslite/TLSConnection.py
index c5722d6..0c34536 100644
--- a/tlslite/TLSConnection.py
+++ b/tlslite/TLSConnection.py
@@ -937,8 +937,8 @@
                         certChain=None, privateKey=None, reqCert=False,
                         sessionCache=None, settings=None, checker=None,
                         reqCAs=None, tlsIntolerant=0,
-                        signedCertTimestamps=None,
-                        fallbackSCSV=False):
+                        signedCertTimestamps=None, fallbackSCSV=False,
+                        ocspResponse=None):
         """Perform a handshake in the role of server.
 
         This function performs an SSL or TLS handshake.  Depending on
@@ -1027,6 +1027,16 @@
         TLS_FALLBACK_SCSV and thus reject connections using less than the
         server's maximum TLS version that include this cipher suite.
 
+        @type ocspResponse: str
+        @param ocspResponse: An OCSP response (as a binary 8-bit string) that
+        will be sent stapled in the handshake whenever the client announces
+        support for the status_request extension.
+        Note that the response is sent independent of the ClientHello
+        status_request extension contents, and is thus only meant for testing
+        environments. Real OCSP stapling is more complicated as it requires
+        choosing a suitable response based on the ClientHello status_request
+        extension contents.
+
         @raise socket.error: If a socket error occurs.
         @raise tlslite.errors.TLSAbruptCloseError: If the socket is closed
         without a preceding alert.
@@ -1037,7 +1047,7 @@
         for result in self.handshakeServerAsync(sharedKeyDB, verifierDB,
                 certChain, privateKey, reqCert, sessionCache, settings,
                 checker, reqCAs, tlsIntolerant, signedCertTimestamps,
-                fallbackSCSV):
+                fallbackSCSV, ocspResponse):
             pass
 
 
@@ -1046,7 +1056,7 @@
                              sessionCache=None, settings=None, checker=None,
                              reqCAs=None, tlsIntolerant=0,
                              signedCertTimestamps=None,
-                             fallbackSCSV=False):
+                             fallbackSCSV=False, ocspResponse=None):
         """Start a server handshake operation on the TLS connection.
 
         This function returns a generator which behaves similarly to
@@ -1066,7 +1076,8 @@
             reqCAs=reqCAs,
             tlsIntolerant=tlsIntolerant,
             signedCertTimestamps=signedCertTimestamps,
-            fallbackSCSV=fallbackSCSV)
+            fallbackSCSV=fallbackSCSV, ocspResponse=ocspResponse)
+
         for result in self._handshakeWrapperAsync(handshaker, checker):
             yield result
 
@@ -1075,7 +1086,7 @@
                                     certChain, privateKey, reqCert,
                                     sessionCache, settings, reqCAs,
                                     tlsIntolerant, signedCertTimestamps,
-                                    fallbackSCSV):
+                                    fallbackSCSV, ocspResponse):
 
         self._handshakeStart(client=False)
 
@@ -1452,10 +1463,14 @@
                     sessionID, cipherSuite, certificateType)
             serverHello.channel_id = clientHello.channel_id
             if clientHello.support_signed_cert_timestamps:
-                serverHello.signed_cert_timestamps = signedCertTimestamps
+              serverHello.signed_cert_timestamps = signedCertTimestamps
+            serverHello.status_request = (clientHello.status_request and
+                                          ocspResponse)
             doingChannelID = clientHello.channel_id
             msgs.append(serverHello)
             msgs.append(Certificate(certificateType).create(serverCertChain))
+            if serverHello.status_request:
+                msgs.append(CertificateStatus().create(ocspResponse))
             if reqCert and reqCAs:
                 msgs.append(CertificateRequest().create([], reqCAs))
             elif reqCert:
diff --git a/tlslite/constants.py b/tlslite/constants.py
index 23e3dcb..d027ef5 100644
--- a/tlslite/constants.py
+++ b/tlslite/constants.py
@@ -22,6 +22,7 @@
     certificate_verify = 15
     client_key_exchange = 16
     finished = 20
+    certificate_status = 22
     encrypted_extensions = 203
 
 class ContentType:
@@ -31,7 +32,11 @@
     application_data = 23
     all = (20,21,22,23)
 
+class CertificateStatusType:
+    ocsp = 1
+
 class ExtensionType:
+    status_request = 5  # OCSP stapling
     signed_cert_timestamps = 18  # signed_certificate_timestamp in RFC 6962
     channel_id = 30031
 
diff --git a/tlslite/messages.py b/tlslite/messages.py
index 296f422..497ef60 100644
--- a/tlslite/messages.py
+++ b/tlslite/messages.py
@@ -132,6 +132,7 @@
         self.srp_username = None        # a string
         self.channel_id = False
         self.support_signed_cert_timestamps = False
+        self.status_request = False
 
     def create(self, version, random, session_id, cipher_suites,
                certificate_types=None, srp_username=None):
@@ -182,6 +183,19 @@
                         if extLength:
                             raise SyntaxError()
                         self.support_signed_cert_timestamps = True
+                    elif extType == ExtensionType.status_request:
+                        # Extension contents are currently ignored.
+                        # According to RFC 6066, this is not strictly forbidden
+                        # (although it is suboptimal):
+                        # Servers that receive a client hello containing the
+                        # "status_request" extension MAY return a suitable
+                        # certificate status response to the client along with
+                        # their certificate.  If OCSP is requested, they
+                        # SHOULD use the information contained in the extension
+                        # when selecting an OCSP responder and SHOULD include
+                        # request_extensions in the OCSP request.
+                        p.getFixBytes(extLength)
+                        self.status_request = True
                     else:
                         p.getFixBytes(extLength)
                     soFar += 4 + extLength
@@ -230,6 +244,7 @@
         self.compression_method = 0
         self.channel_id = False
         self.signed_cert_timestamps = None
+        self.status_request = False
 
     def create(self, version, random, session_id, cipher_suite,
                certificate_type):
@@ -282,6 +297,9 @@
         if self.signed_cert_timestamps:
             extLength += 4 + len(self.signed_cert_timestamps)
 
+        if self.status_request:
+            extLength += 4
+
         if extLength != 0:
             w.add(extLength, 2)
 
@@ -299,6 +317,10 @@
             w.add(ExtensionType.signed_cert_timestamps, 2)
             w.addVarSeq(stringToBytes(self.signed_cert_timestamps), 1, 2)
 
+        if self.status_request:
+            w.add(ExtensionType.status_request, 2)
+            w.add(0, 2)
+
         return HandshakeMsg.postWrite(self, w, trial)
 
 class Certificate(HandshakeMsg):
@@ -367,6 +389,37 @@
             raise AssertionError()
         return HandshakeMsg.postWrite(self, w, trial)
 
+class CertificateStatus(HandshakeMsg):
+    def __init__(self):
+        self.contentType = ContentType.handshake
+
+    def create(self, ocsp_response):
+        self.ocsp_response = ocsp_response
+        return self
+
+    # Defined for the sake of completeness, even though we currently only
+    # support sending the status message (server-side), not requesting
+    # or receiving it (client-side).
+    def parse(self, p):
+        p.startLengthCheck(3)
+        status_type = p.get(1)
+        # Only one type is specified, so hardwire it.
+        if status_type != CertificateStatusType.ocsp:
+            raise SyntaxError()
+        ocsp_response = p.getVarBytes(3)
+        if not ocsp_response:
+            # Can't be empty
+            raise SyntaxError()
+        self.ocsp_response = ocsp_response
+        return self
+
+    def write(self, trial=False):
+        w = HandshakeMsg.preWrite(self, HandshakeType.certificate_status,
+                                  trial)
+        w.add(CertificateStatusType.ocsp, 1)
+        w.addVarSeq(stringToBytes(self.ocsp_response), 1, 3)
+        return HandshakeMsg.postWrite(self, w, trial)
+
 class CertificateRequest(HandshakeMsg):
     def __init__(self):
         self.contentType = ContentType.handshake