使用 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 KEY
和BEGIN 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客户端&服务器端