35 lines
1.5 KiB
Python
35 lines
1.5 KiB
Python
"""核对本地邮件 TLS 监听实际使用的证书,仅用于开发环境验收。"""
|
||
|
||
import argparse
|
||
import hashlib
|
||
from pathlib import Path
|
||
import socket
|
||
import ssl
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="核对邮件 TLS 端口的证书指纹")
|
||
parser.add_argument("certificate", type=Path)
|
||
parser.add_argument("--host", default="127.0.0.1")
|
||
parser.add_argument("--server-name", required=True)
|
||
parser.add_argument("--ports", type=int, nargs="+", default=[465, 993, 995])
|
||
args = parser.parse_args()
|
||
pem = args.certificate.read_text(encoding="utf-8")
|
||
leaf = pem.split("-----END CERTIFICATE-----", 1)[0] + "-----END CERTIFICATE-----\n"
|
||
expected = hashlib.sha256(ssl.PEM_cert_to_DER_cert(leaf)).hexdigest()
|
||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||
# 本地自签名证书用精确指纹验证,不改变应用或系统的信任配置。
|
||
context.check_hostname = False
|
||
context.verify_mode = ssl.CERT_NONE
|
||
for port in args.ports:
|
||
with socket.create_connection((args.host, port), timeout=5) as connection:
|
||
with context.wrap_socket(connection, server_hostname=args.server_name) as stream:
|
||
actual = hashlib.sha256(stream.getpeercert(binary_form=True)).hexdigest()
|
||
if actual != expected:
|
||
raise RuntimeError(f"端口 {port} 未加载预期证书")
|
||
print(f"端口 {port}:TLS 握手成功,证书指纹一致,协议 {stream.version()}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|