You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

运行aiosmtpd遇SSLV3_ALERT_CERTIFICATE_EXPIRED,如何获取过期证书名?

解决aiosmtpd SSL证书过期错误时获取证书名称的方法

1. sys.excepthook的局限性

默认的sys.excepthook无法直接获取过期证书的名称,因为该错误是在asyncio的SSLProtocol内部抛出的,栈跟踪仅包含SSL协议和传输对象,没有直接暴露证书元数据。而且asyncio事件循环内的异常通常不会触发sys.excepthook,需要专门处理asyncio层面的异常。

2. 可行的解决方案

方案一:自定义SSL上下文的验证回调

在创建SSL上下文时,通过set_verify方法设置自定义验证函数,在握手过程中提前检查证书有效期,同时提取证书名称:

import ssl
import datetime
from aiosmtpd.controller import Controller
from aiosmtpd.handlers import Sink

def validate_certificate(cert, hostname):
    # 解析证书有效期
    not_after_str = cert["notAfter"]
    not_after = datetime.datetime.strptime(not_after_str, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=datetime.timezone.utc)
    now = datetime.datetime.now(datetime.timezone.utc)
    
    # 检查证书是否过期
    if not_after < now:
        # 提取证书通用名称(CN)
        cert_subject = dict(item[0] for item in cert["subject"])
        cert_name = cert_subject.get("commonName", "未知证书")
        print(f"检测到过期证书: {cert_name}")
        # 抛出包含证书名称的异常,方便后续捕获处理
        raise ssl.SSLError(f"证书 {cert_name} 已过期")
    
    # 返回True表示验证通过
    return True

# 构建SSL上下文
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(certfile="your_server_cert.pem", keyfile="your_server_key.pem")
# 设置验证模式和自定义回调函数
ssl_context.set_verify(ssl.CERT_REQUIRED, validate_certificate)

# 启动aiosmtpd服务
handler = Sink()
controller = Controller(handler, hostname="0.0.0.0", port=465, ssl_context=ssl_context)
controller.start()
print("SMTP服务已启动")

方案二:处理asyncio事件循环的异常

如果需要在错误发生后捕获并提取信息,可以为asyncio事件循环设置自定义异常处理器,尝试从异常关联的protocol对象中获取SSL信息(注意:握手失败后SSL对象可能已不可用,此方法可靠性低于方案一):

import asyncio
import ssl
from aiosmtpd.controller import Controller
from aiosmtpd.handlers import Sink

def handle_async_exception(loop, context):
    exc = context.get("exception")
    if isinstance(exc, ssl.SSLError) and "CERTIFICATE_EXPIRED" in str(exc):
        protocol = context.get("protocol")
        if protocol and hasattr(protocol, "_sslobj"):
            try:
                # 尝试获取已协商的证书(握手失败时可能返回None)
                peer_cert = protocol._sslobj.getpeercert()
                if peer_cert:
                    cert_subject = dict(item[0] for item in peer_cert["subject"])
                    cert_name = cert_subject.get("commonName", "未知证书")
                    print(f"过期证书名称: {cert_name}")
            except Exception:
                print("无法提取证书信息")
    # 调用默认异常处理逻辑
    loop.default_exception_handler(context)

# 配置事件循环
loop = asyncio.get_event_loop()
loop.set_exception_handler(handle_async_exception)

# 构建SSL上下文并启动服务
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(certfile="your_server_cert.pem", keyfile="your_server_key.pem")

handler = Sink()
controller = Controller(handler, hostname="0.0.0.0", port=465, ssl_context=ssl_context, loop=loop)
controller.start()
print("SMTP服务已启动")
loop.run_forever()

总结

优先推荐方案一,因为它在握手过程中主动检查证书,能可靠提取证书名称并提前终止无效连接;方案二作为补充,适用于需要事后捕获异常的场景,但可靠性较低。

内容的提问来源于stack exchange,提问作者Cyril N.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 11:22:36