3

I'm currently trying to write a python server script which should authenticate the current client based on its public key. Since I'm using twisted, the example in the twisted documenteation got me started.

While I can generate keys, connect and communicate using the example code, I have not yet found a way to get the public key of the client in a usable format. In this stackexchange question somebody extracts the public key from an OpenSSL.crypto.PKey object but cannot transform it to a readable format. Since in I have access to the PKey object of the x509 certificate in the verifyCallback method or via self.transport.getPeerCertificate() from any method of my Protocol, this would be a good way to go. The (not accepted) answer suggests to try crypto.dump_privatekey(PKey). Unfortunately, this does not really yield the expected result: While the BEGIN PRIVATE KEY and BEGIN PRIVATE KEY in the answer could be fixed by an easy text replacement function, the base64 string seems not match the public key. I've extracted the public key with openssl rsa -in client.key -pubout > client.pub as mentioned here. It does not match the result of the dump_privatekey function.

While there still is an open bug towards OpenSSL on launchpad, it is not yet fixed. It was reported 19 Month ago, and there is some recent (October 2012) activity on it, I do not have any hope of a fast fix in the repos.

Do you have any other ideas how I could get the public key in a format comparable to the client.pub file I have mentioned above? Perhaps there is a twisted or OpenSSL connection specific object which holds this information. Please note that I have to store the public key in the protocol object such that I can access it later.

Why is no Answer accepted?

M2Crypto by J.F. Sebastian

Sorry, that I had not thought of a possibility where I cannot correlate the certificate to the connection. I've added the requirement that I have to store the public key inside the protocol instance. Thus, using peerX509.as_pem() inside the postConnectionCheck function as suggested by J.F. Sebastian does not work. Furthermore, at least in version 0.21.1-2ubuntu3 of python-m2crypto I have to call peerX509.get_rsa().as_pem() to get the right public key. Using peerX509.as_pem(None) (since peerX509.as_pem() still wants a passphrase) yields excactly the same output as crypto.dump_privatekey(PKey) in PyOpenSSL. Maybe there is a bug.

Besides this, the answer showed me a possible way to write another workaround by using the following Echo protocol class:

class Echo(Protocol):
    def dataReceived(self, data):
        """As soon as any data is received, write it back."""
        if self.transport.checked and not self.pubkeyStored:
            self.pubkeyStored = True
            x509 = m2.ssl_get_peer_cert(self.transport.ssl._ptr())
            if x509 is not None:
                x509 = X509.X509(x509, 1)
                pk = x509.get_pubkey()
                self.pubkey = pk.get_rsa().as_pem()
                print pk.as_pem(None)
            print self.pubkey
        self.transport.write(data)

As you can see this uses some internal classes which I'd like to prevent. I'm hesitating submitting a small patch which would add a getCert method to the TLSProtocolWrapper class in M2Crypto.SSL.TwistedProtocolWrapper. Even if it was accepted upstream, it would break compatibility of my script with any but the most cut-of-the-edge versions of m2crypto. What would you do?

External OpenSSL call by me

Well, its an ugly workaround based on external system commands just which seems to me even worse than accessing non-public attributes.

Community
  • 1
  • 1
sumpfomat
  • 41
  • 1
  • 1
  • 6
  • Related, see [Upgrading my encryption library from Mcrypt to OpenSSL](http://stackoverflow.com/q/43329513/608639), [Replace Mcrypt with OpenSSL](http://stackoverflow.com/q/9993909/608639) and [Preparing for removal of Mcrypt in PHP 7.2](http://stackoverflow.com/q/42696657/608639) – jww Jun 10 '17 at 10:36

4 Answers4

6

Some of previous answers produce (apparently?) working PEM public key files, but so far as I've tried, none of them produce the same output that the 'openssl rsa -pubout -in priv.key' does. This is pretty important to my test suite, and after poking around in the (0.15.1) PyOpenSSL code, this works well for both standard PKey objects and the public-key-only PKey objects created by the x509.get_pubkey() method:

from OpenSSL import crypto
from OpenSSL._util import lib as cryptolib


def pem_publickey(pkey):
    """ Format a public key as a PEM """
    bio = crypto._new_mem_buf()
    cryptolib.PEM_write_bio_PUBKEY(bio, pkey._pkey)
    return crypto._bio_to_string(bio)


key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
print pem_publickey(key)
Jessica Gadling
  • 739
  • 5
  • 3
2

The analog of openssl rsa -in client.key -pubout > client.pub command in M2Crypto (a more complete openssl wrapper than pyOpenSSL) is:

def save_pub_key(cert, filename):
    cert.get_pubkey().get_rsa().save_pub_key(filename)

You can use M2Crypto instead of pyOpenSSL with twisted. To add ssl capabilities to the echo server:

from twisted.internet import protocol, reactor

class Echo(protocol.Protocol):
    def dataReceived(self, data):
        self.transport.write(data)

class EchoFactory(protocol.Factory):
    def buildProtocol(self, addr):
        return Echo()

You could:

import sys
from twisted.python import log

from M2Crypto import SSL, X509
from M2Crypto.SSL import Checker
from M2Crypto.SSL.TwistedProtocolWrapper import listenSSL

log.startLogging(sys.stderr)    
cert = X509.load_cert('client.crt')
check = Checker.Checker(peerCertHash=cert.get_fingerprint('sha1'))

def postConnectionCheck(peerX509, expectedHost):
    log.msg("client cert in pem format:\n", peerX509.as_pem())
    save_pub_key(peerX509, 'client.pub')
    return check(peerX509, host=None) # don't check client hostname

class SSLContextFactory:
    def getContext(self):
        ctx = SSL.Context()
        ctx.load_verify_locations(cafile='ca.crt')
        ctx.load_cert(certfile='server.crt', keyfile='server.key',
                      callback=lambda *a,**kw: 'keyfile passphrase')
        ctx.set_verify(SSL.verify_peer | SSL.verify_fail_if_no_peer_cert,
                       depth=9, callback=None)
        return ctx
listenSSL(8000, EchoFactory(), SSLContextFactory(),
          interface='localhost',  reactor=reactor,
          postConnectionCheck=postConnectionCheck)
reactor.run()

To try it, create self-signed certificates:

$ openssl req -new -x509 -nodes -out server.crt -keyout server.key
# NOTE: server.key is unencrypted!
$ cp {server,client}.crt
$ cp {server,client}.key
$ cp {server,ca}.crt

and connect to the server:

$ openssl s_client -cert client.crt -key client.key -CAfile ca.crt \
   -verify 9 -connect localhost:8000 -no_ssl2

The server saves the client's public key to the client.pub file. It is identical to the one created by openssl command:

$ openssl rsa -in client.key -pubout > openssl_client.pub
$ diff -s {openssl_,}client.pub
jfs
  • 399,953
  • 195
  • 994
  • 1,670
1

One possibility to get the public key is an ugly workaround by piping the pem version of the x509 certificate through an externally called openssl instance:

def extractpublickey(x509):
    x509pem = dump_certificate(FILETYPE_PEM,x509)
    ossl = Popen(['openssl','x509','-pubkey','-noout'] , stdout=PIPE, stderr=PIPE, stdin=PIPE)
    (stdout,_) = ossl.communicate(x509pem)
    res = ""
    if stdout[:26] != ("-----BEGIN PUBLIC KEY-----") or stdout[-24:] != ("-----END PUBLIC KEY-----"):
        raise AttributeError("Could not extract key from x509 certificate in PEM mode: %s"%x509pem)
    else:
        res = stdout
    return res

class Echo(Protocol):
    def dataReceived(self, data):
        """As soon as any data is received, write it back."""
        if self.transport.getPeerCertificate() == None:
            print("unknown client")
        else: 
            print extractpublickey(self.transport.getPeerCertificate())
        self.transport.write(data)
sumpfomat
  • 41
  • 1
  • 1
  • 6
1

I made it working finally using pyOpenSSL and DerSequence class from Crypto.Util.asn1 (pyasn1 library).

Here is a method from my RSAKey class (pkey is a OpenSSL.crypto.PKey instance) :

from OpenSSL.crypto import dump_privatekey, FILETYPE_ASN1
from Crypto.PublicKey import RSA
from Crypto.Util.asn1 import DerSequence
from base64 import b64decode, b64encode

...

def fromPKey_PublicKey(self, pkey):
    src = dump_privatekey(FILETYPE_ASN1, pkey)
    pub_der = DerSequence()
    pub_der.decode(src)
    self.key = RSA.construct((long(pub_der._seq[1]), long(pub_der._seq[2])))

The key here is that first item in pub_der._seq is zero and we dont need it. Than you can convert RSA key stored in self.key to any format you want:

def toPEM_PublicKey(self):
    pemSeq = DerSequence()
    pemSeq[:] = [ self.key.key.n, self.key.key.e ]
    s = b64encode(pemSeq.encode())
    src = '-----BEGIN RSA PUBLIC KEY-----\n'
    while True:
        src += s[:64] + '\n'
        s = s[64:]
        if s == '':
            break
    src += '-----END RSA PUBLIC KEY-----'
    return src

I am currently working with CSpace project, it uses "ncrypt" library ( this is another OpenSSL wrapper ) wich is not supported anymore and it gives SegFault on Linux. So I decided to replace ncrypt library with pyOpenSSL because I am using it in my project called DataHaven.NET. And getting a public key from the peer certificate in PEM format really was a problem for me. Now it is working just fine.

JJ Geewax
  • 10,342
  • 1
  • 37
  • 49
Veselin Penev
  • 77
  • 1
  • 5