如何使用自签名SSL CA证书搭建带mTLS的aiohttp服务器?
问题:私有Python API服务器客户端证书认证失败
背景
在Ubuntu 22服务器搭建仅允许指定设备访问的Python Web/API服务器,无域名和第三方CA证书,目标是通过分发.pfx证书实现客户端认证——仅安装证书的设备能访问,无证书的客户端即使忽略安全警告也无法访问。
已生成自签名CA、服务器证书及客户端.pfx,但客户端安装后访问报错,服务器日志提示peer did not return a certificate。
现有证书生成步骤
# 生成CA密钥和证书 openssl genrsa -des3 -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -sha256 -days 824 -out ca.crt # 生成服务器证书 openssl genrsa -out server.key 2048 openssl req -new -key server.key -out server.csr openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 824 -sha256 -extfile server.ext # 生成客户端证书及pfx openssl genrsa -out client.key 2048 openssl req -new -key client.key -out client.csr openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 824 -sha256 -extfile client.ext openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile ca.crt
注:因OpenSSL 3.2.1存在密码bug导致iOS无法安装pfx,当前使用OpenSSL 1.1.1
错误信息
- 客户端浏览器报错:
ERR_CONNECTION_REFUSED - 服务器日志报错:
Traceback (most recent call last): File "/usr/lib/python3.10/asyncio/selector_events.py", line 213, in _accept_connect await waiter File "/usr/lib/python3.10/asyncio/sslproto.py", line 534, in data_received ssldata, appdata = self._sslpipe.feed_ssldata(data) File "/usr/lib/python3.10/asyncio/sslproto.py", line 188, in feed_ssldata self._sslobj.do_handshake() File "/usr/lib/python3.10/ssl.py", line 975, in do_handshake self._sslobj.do_handshake() ssl.SSLError: [SSL: PEER_DID_NOT_RETURN_A_CERTIFICATE] peer did not return a certificate
服务器代码
import asyncio from aiohttp import web import json import ssl import logging async def handler(req): packed = await req.read() data = json.loads(packed) return web.Response(text=f"Thank you for your {data}") async def create_server(): app = web.Application() ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(certfile='server.crt', keyfile='server.key') ssl_context.load_verify_locations('ca.crt') ssl_context.verify_mode = ssl.CERT_REQUIRED app.add_routes([ web.get('/', handler), ]) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, '0.0.0.0', 2343, ssl_context=ssl_context) await site.start() return app async def main(): app = await create_server() await asyncio.sleep(10_000) if __name__ == '__main__': asyncio.run(main(), debug=True)
解决方案
1. 修正证书扩展配置(关键)
服务器和客户端的.ext文件必须正确配置,否则客户端不会主动发送证书:
server.ext 内容
authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment subjectAltName = @alt_names [alt_names] # 替换为服务器IP或主机名,可添加多个 IP.1 = 192.168.1.100 DNS.1 = your-server-hostname
client.ext 内容
authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = clientAuth
必须添加extendedKeyUsage = clientAuth,否则客户端无法将此证书用于身份认证
2. 重新生成证书
删除旧的服务器/客户端证书、csr、serial文件,重新执行生成命令(确保使用上述.ext文件)。
3. 客户端证书安装验证
- Windows:将
client.p12安装到当前用户-个人-证书存储,确认证书“预期目的”包含“客户端身份验证”。 - iOS:安装后需到「设置-通用-VPN与设备管理」信任该证书,部分浏览器访问时需手动指定使用该证书。
4. 服务器SSL上下文补充配置
在现有代码基础上添加协议与加密套件配置:
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.load_cert_chain(certfile='server.crt', keyfile='server.key') ssl_context.load_verify_locations('ca.crt') ssl_context.verify_mode = ssl.CERT_REQUIRED # 添加以下配置,确保服务器正确请求客户端证书 ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 # 禁用不安全的旧协议 ssl_context.set_ciphers('ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384') # 指定安全加密套件
证书使用场景与分发说明
- CA证书(ca.crt):仅在服务器端存储,用于验证客户端/服务器证书的合法性,无需分发给客户端。
- 服务器证书(server.crt + server.key):仅部署在服务器端,用于标识服务器身份,不对外分发。
- 客户端证书(client.p12/pfx):分发给授权访问的设备,安装后设备可向服务器提供身份凭证;建议为每个设备生成唯一证书,便于权限管理。
内容的提问来源于stack exchange,提问作者Fabian
相关产品推荐
相关产品推荐

