Search code examples
pythonauthenticationssltwisted

Use TLS and Python for authentication


I want to make a little update script for a software that runs on a Raspberry Pi and works like a local server. That should connect to a master server in the web to get software updates and also to verify the license of the software. For that I set up two python scripts. I want these to connect via a TLS socket. Then the client checks the server certificate and the server checks if it's one of the authorized clients. I found a solution for this using twisted on this page.

Now there is a problem left. I want to know which client (depending on the certificate) is establishing the connection. Is there a way to do this in Python 3 with twisted?

I'm happy with every answer.


Solution

  • In a word: yes, this is quite possible, and all the necessary stuff is ported to python 3 - I tested all the following under Python 3.4 on my Mac and it seems to work fine.

    The short answer is "use twisted.internet.ssl.Certificate.peerFromTransport" but given that a lot of set-up is required to get to the point where that is possible, I've constructed a fully working example that you should be able to try out and build upon.

    For posterity, you'll first need to generate a few client certificates all signed by the same CA. You've probably already done this, but so others can understand the answer and try it out on their own (and so I could test my answer myself ;-)), they'll need some code like this:

    # newcert.py
    from twisted.python.filepath import FilePath
    from twisted.internet.ssl import PrivateCertificate, KeyPair, DN
    
    def getCAPrivateCert():
        privatePath = FilePath(b"ca-private-cert.pem")
        if privatePath.exists():
            return PrivateCertificate.loadPEM(privatePath.getContent())
        else:
            caKey = KeyPair.generate(size=4096)
            caCert = caKey.selfSignedCert(1, CN="the-authority")
            privatePath.setContent(caCert.dumpPEM())
            return caCert
    
    def clientCertFor(name):
        signingCert = getCAPrivateCert()
        clientKey = KeyPair.generate(size=4096)
        csr = clientKey.requestObject(DN(CN=name), "sha1")
        clientCert = signingCert.signRequestObject(
            csr, serialNumber=1, digestAlgorithm="sha1")
        return PrivateCertificate.fromCertificateAndKeyPair(clientCert, clientKey)
    
    if __name__ == '__main__':
        import sys
        name = sys.argv[1]
        pem = clientCertFor(name.encode("utf-8")).dumpPEM()
        FilePath(name.encode("utf-8") + b".client.private.pem").setContent(pem)
    

    With this program, you can create a few certificates like so:

    $ python newcert.py a
    $ python newcert.py b
    

    Now you should have a few files you can use:

    $ ls -1 *.pem
    a.client.private.pem
    b.client.private.pem
    ca-private-cert.pem
    

    Then you'll want a client which uses one of these certificates, and sends some data:

    # tlsclient.py
    from twisted.python.filepath import FilePath
    from twisted.internet.endpoints import SSL4ClientEndpoint
    from twisted.internet.ssl import (
        PrivateCertificate, Certificate, optionsForClientTLS)
    from twisted.internet.defer import Deferred, inlineCallbacks
    from twisted.internet.task import react
    from twisted.internet.protocol import Protocol, Factory
    
    class SendAnyData(Protocol):
        def connectionMade(self):
            self.deferred = Deferred()
            self.transport.write(b"HELLO\r\n")
        def connectionLost(self, reason):
            self.deferred.callback(None)
    
    
    @inlineCallbacks
    def main(reactor, name):
        pem = FilePath(name.encode("utf-8") + b".client.private.pem").getContent()
        caPem = FilePath(b"ca-private-cert.pem").getContent()
        clientEndpoint = SSL4ClientEndpoint(
            reactor, u"localhost", 4321,
            optionsForClientTLS(u"the-authority", Certificate.loadPEM(caPem),
                                PrivateCertificate.loadPEM(pem)),
        )
        proto = yield clientEndpoint.connect(Factory.forProtocol(SendAnyData))
        yield proto.deferred
    
    import sys
    react(main, sys.argv[1:])
    

    And finally, a server which can distinguish between them:

    # whichclient.py
    from twisted.python.filepath import FilePath
    from twisted.internet.endpoints import SSL4ServerEndpoint
    from twisted.internet.ssl import PrivateCertificate, Certificate
    from twisted.internet.defer import Deferred
    from twisted.internet.task import react
    from twisted.internet.protocol import Protocol, Factory
    
    class ReportWhichClient(Protocol):
        def dataReceived(self, data):
            peerCertificate = Certificate.peerFromTransport(self.transport)
            print(peerCertificate.getSubject().commonName.decode('utf-8'))
            self.transport.loseConnection()
    
    def main(reactor):
        pemBytes = FilePath(b"ca-private-cert.pem").getContent()
        certificateAuthority = Certificate.loadPEM(pemBytes)
        myCertificate = PrivateCertificate.loadPEM(pemBytes)
        serverEndpoint = SSL4ServerEndpoint(
            reactor, 4321, myCertificate.options(certificateAuthority)
        )
        serverEndpoint.listen(Factory.forProtocol(ReportWhichClient))
        return Deferred()
    
    react(main, [])
    

    For simplicity's sake we'll just re-use the CA's own certificate for the server, but in a more realistic scenario you'd obviously want a more appropriate certificate.

    You can now run whichclient.py in one window, then python tlsclient.py a; python tlsclient.py b in another window, and see whichclient.py print out a and then b respectively, identifying the clients by the commonName field in their certificate's subject.

    The one caveat here is that you might initially want to put that call to Certificate.peerFromTransport into a connectionMade method; that won't work. Twisted does not presently have a callback for "TLS handshake complete"; hopefully it will eventually, but until it does, you have to wait until you've received some authenticated data from the peer to be sure the handshake has completed. For almost all applications, this is fine, since by the time you have received instructions to do anything (download updates, in your case) the peer must already have sent the certificate.