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

Posted

技术标签:

【中文标题】使用 pyOpenSSL 从证书或其他连接信息中提取公钥【英文标题】:Extract Public Key using pyOpenSSL from certificate or other connection information 【发布时间】:2012-11-30 11:51:45 【问题描述】:

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

虽然我可以使用示例代码生成密钥、连接和通信,但我还没有找到以可用格式获取客户端公钥的方法。在this stackexchange question 中,有人从OpenSSL.crypto.PKey 对象中提取公钥,但无法将其转换为可读格式。因为我可以通过verifyCallback 方法或通过我的协议的任何方法中的self.transport.getPeerCertificate() 访问x509 证书的PKey 对象,所以这将是一个不错的方法。 (不接受)答案建议尝试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 的建议,在 postConnectionCheck 函数中使用 peerX509.as_pem() 不起作用。此外,至少在 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 的 TLSProtocolWrapper 类中添加一个 getCert 方法。即使它被上游接受,它也会破坏我的脚本与除了最先进的 m2crypto 版本之外的任何脚本的兼容性。你会怎么做?

我的外部 OpenSSL 调用

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

【问题讨论】:

相关,见Upgrading my encryption library from Mcrypt to OpenSSL、Replace Mcrypt with OpenSSL和Preparing for removal of Mcrypt in php 7.2 【参考方案1】:

以前的一些答案产生(显然?)工作 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)

【讨论】:

你介意解释一下这是如何工作的更好一点吗@jessica-gadling【参考方案2】:

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

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

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

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

【讨论】:

【参考方案3】:

获取公钥的一种可能性是通过外部调用的 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)

【讨论】:

【参考方案4】:

我最终使用 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 格式的对等证书中获取公钥对我来说确实是个问题。现在它工作得很好。

【讨论】:

以上是关于使用 pyOpenSSL 从证书或其他连接信息中提取公钥的主要内容,如果未能解决你的问题,请参考以下文章

AFNetworking+Python+Flask+pyOpenSSL构建iOS HTTPS客户端&服务器端

让 pyOpenSSL 客户端使用 SSL 会话恢复

谷歌浏览器报错“您的连接不是私密连接攻击者可能会试图从 xxx 窃取您的信息(例如:密码通讯内容或信用卡信息)“

nodejs可以生成SSL证书吗?

python https实现方法

python https实现方法