使用pyOpenSSL从证书或其他连接信息中提取公钥

时间:2012-12-12 17:39:30

标签: python twisted public-key pyopenssl

我正在尝试编写一个python服务器脚本,该脚本应根据其公钥对当前客户端进行身份验证。由于我正在使用扭曲,example in the twisted documenteation让我开始。

虽然我可以使用示例代码生成密钥,连接和通信,但我还没有找到以可用格式获取客户端公钥的方法。在this stackexchange question中,有人从OpenSSL.crypto.PKey对象中提取公钥,但无法将其转换为可读格式。由于我可以使用PKey方法访问x509证书的verifyCallback对象,或者通过self.transport.getPeerCertificate()从我的协议的任何方法访问,这将是一个很好的方法。 (未接受)答案建议尝试crypto.dump_privatekey(PKey)。不幸的是,这并没有真正产生预期的结果: 虽然答案中的BEGIN PRIVATE KEYBEGIN PRIVATE KEY可以通过简单的文本替换功能修复,但base64字符串似乎与公钥不匹配。我已按照here提取的openssl rsa -in client.key -pubout > client.pub提取了公钥。它与dump_privatekey函数的结果不匹配。

虽然仍有open bug towards OpenSSL on launchpad,但尚未修复。据报道,19个月前,有一些最近(2012年10月)的活动,我没有希望在回购中快速修复。

您是否有任何其他想法如何以与我上面提到的client.pub文件相当的格式获取公钥?也许存在扭曲或OpenSSL连接特定对象来保存此信息。请注意,我必须将公钥存储在协议对象中,以便以后可以访问它。

为什么没有接受答案?

由J.F. Sebastian撰写的M2Crypto

抱歉,我没想到我无法将证书与连接关联起来。我已经添加了必须将公钥存储在协议实例中的要求。因此,如J.F.Sebastian建议的那样在peerX509.as_pem()函数内使用postConnectionCheck不起作用。此外,至少在python-m2crypto的0.21.1-2ubuntu3版本中,我必须调用peerX509.get_rsa().as_pem()来获取正确的公钥。使用peerX509.as_pem(None)(因为peerX509.as_pem()仍需要密码短语)在PyOpenSSL中产生与crypto.dump_privatekey(PKey)完全相同的输出。也许有一个错误。

除此之外,答案向我展示了使用以下Echo协议类编写另一种解决方法的可能方法:

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)

正如您所看到的,它使用了一些我想要阻止的内部类。我在犹豫是否提交一个小补丁,它会在M2Crypto.SSL.TwistedProtocolWrapper中为getCert类添加TLSProtocolWrapper方法。即使它被上游接受,它也会破坏我的脚本与m2crypto最简洁的版本之间的兼容性。你会做什么?

我的外部OpenSSL呼叫

嗯,这是一个基于外部系统命令的难看的解决方法,在我看来比访问非公共属性更糟糕。

4 个答案:

答案 0 :(得分:6)

以前的一些答案产生(显然是?)正在工作的PEM公钥文件,但据我所知,它们都没有产生与'openssl rsa -pubout -in priv.key'相同的输出。这对我的测试套件非常重要,并且在(0.15.1)PyOpenSSL代码中进行了探讨之后,这对于标准PKey对象和x509.get_pubkey()方法创建的仅公钥的PKey对象都很有效:

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)

答案 1 :(得分:2)

M2Crypto中的openssl rsa -in client.key -pubout > client.pub命令的模拟(比pyOpenSSL更完整的openssl包装)是:

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

您可以使用M2Crypto代替pyOpenSSL扭曲。要将ssl功能添加到echo服务器:

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()

你可以:

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()

要尝试,请创建自签名证书:

$ 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

并连接到服务器:

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

服务器将客户端的公钥保存到client.pub文件。它与openssl命令创建的命令相同:

$ openssl rsa -in client.key -pubout > openssl_client.pub
$ diff -s {openssl_,}client.pub

答案 2 :(得分:1)

获取公钥的一种可能性是通过外部调用的openssl实例来管理x509证书的pem版本,这是一种丑陋的解决方法:

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)

答案 3 :(得分:0)

我最终使用来自Crypto.Util.asn1(pyasn1库)的pyOpenSSL和DerSequence类。

这是我的RSAKey类的一个方法(pkey是一个OpenSSL.crypto.PKey实例):

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])))

这里的关键是pub_der._seq中的第一项是零,我们不需要它。 您可以将存储在self.key中的RSA密钥转换为您想要的任何格式:

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

我目前正在使用CSpace项目,它使用“ncrypt”库(这是另一个OpenSSL包装器),它不再受支持,它在Linux上提供了SegFault。所以我决定用pyOpenSSL替换ncrypt库,因为我在我的项目DataHaven.NET中使用它。从PEM格式的对等证书中获取公钥确实对我来说是一个问题。现在工作得很好。