Search code examples
pythontwistedxml-rpc

Twisted Python TLS server : pass client certificate info to resource / endpoint


I am trying to implement a XML-RPC server with TLS client certificate in Twisted python. Furthermore, the end goal is to allow access to some methods only to a defined list of users (i.e. to a list of certificates).

While I got the first part down, I have difficulties transferring the certificate information down to the XMLRPC.render_POST where I am planning to apply the filter.

I found this answer which describes how to display the common name, but I still have two problems:

  1. The XML-RPC call from the client does not return after receiving the answer, which I guess means that the request is not properly finished
  2. I do not know how to pass this information further down to the XMLRPC resource

Here's what I have so far :

    import sys
    import OpenSSL
    from twisted.python.filepath import FilePath
    from twisted.internet import endpoints
    from twisted.internet import ssl
    from twisted.python import log
    from twisted.web import xmlrpc, server
    from twisted.internet.ssl import Certificate
    from twisted.internet.protocol import Protocol

    class ReportWhichClient(Protocol):
        # adapted from https://stackoverflow.com/a/28682511
        def dataReceived(self, data):
            peerCertificate = Certificate.peerFromTransport(self.transport)
            print(peerCertificate.getSubject().commonName.decode('utf-8'))


    class Example(xmlrpc.XMLRPC):
        """
        An example object to be published.
        """

        def xmlrpc_echo(self, x):
            """
            Return all passed args.
            """
            return x


    def main():
        log.startLogging(sys.stdout)

        from twisted.internet import reactor

        key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,FilePath("my.key").getContent())
        cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,FilePath("my.crt").getContent())
        ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, FilePath("ca.crt").getContent())
        contextFactory = ssl.CertificateOptions(privateKey=key, certificate=cert, verify=True, caCerts=[ca] , requireCertificate=True)

        root = Example()


        endpoint = endpoints.SSL4ServerEndpoint(reactor, 8083,contextFactory)
        mySite = server.Site(root)
        mySite.protocol = ReportWhichClient
        endpoint.listen(mySite)

        reactor.run()


    if __name__ == '__main__':
        main()

Is this the correct approach ? What should I do to have the required information at the resource level ?

Any answer would be great, at this point I have been racking my brain trying many solutions without any result.

Thanks


Solution

  • Ok so the solution was in front of me all this time.

    After re-reading the source code, it appears that transport is present in the request. All I had to do was to add the @withRequest decorator and get all the information from there :

    class Example(xmlrpc.XMLRPC):
    
        @withRequest
        def xmlrpc_echo(self, request, x):
            peerCertificate = Certificate.peerFromTransport(request.transport)
            key = peerCertificate.getPublicKey().original
            # display the client public key in PEM format
            print(OpenSSL.crypto.dump_publickey(OpenSSL.crypto.FILETYPE_PEM, key))
            return x