证书不同、规格不同 — 搭建对外网关
目标
搭建通过双向 TLS 与两个外部机构对接的外联前置机。为每个机构签发证书,把机构配置档案放在数据里,翻译成机构规格后发送,只从允许的地址接收,并监控证书到期。
为什么重要
外部机构不会迁就我们的标准。如果不把规格、证书、允许地址的差异关在外联前置机这一个地方,每增加一个机构,就得修改内层系统。而且对外故障的常客——证书到期、证书弄混、长度基准的误解——都可以在这一层拦住或提早发觉。
步骤
- 用
python3 /opt/lab/fixtures/eaimw/fep/pki.py init /root/eaimw/fep/partners创建两个机构的 CA。为每个机构生成私钥和 CSR(主体中包含O=LabHub Bank,CN 中包含labhub),并用python3 /opt/lab/fixtures/eaimw/fep/pki.py sign A <CSR> /root/eaimw/fep/a.crt、… sign B <CSR> /root/eaimw/fep/b.crt获得签名。密钥放在/root/eaimw/fep/a.key·/root/eaimw/fep/b.key。 - 根据定义书
/opt/lab/fixtures/eaimw/fep/ORGS.md,编写/root/eaimw/fep/orgs.json。最上层的键是机构代码(201·301),值为host·port·format(fixed/json)·encoding(euc_kr/utf-8)·len_includes_self(A 为 true,B 为 null)·ca·cert·key(绝对路径)·allow_ips(列表)。 - 启动机构 A 服务器(
nohup python3 /opt/lab/fixtures/eaimw/fep/org_server.py A > /root/eaimw/fep/a.out 2>&1 &,9441),用openssl s_client提供 a.crt、a.key 和机构 A 的 CA,发送一行PING,把收到的PONG <지문>(占位符为指纹)保存到/root/eaimw/fep/a-ping.txt。 - 在同一条命令中换上 b.crt、b.key 去连接机构 A,把标准输出和标准错误一起(
2>&1)保存到/root/eaimw/fep/wrong.txt。握手必须被拒绝。 /root/eaimw/fep/fepgw.py send <기관코드> <요청JSON파일>(占位符依次为机构代码、请求 JSON 文件):从配置档案(FEP_ORGS,默认/root/eaimw/fep/orgs.json)中找到机构,用该机构的证书建立双向 TLS 连接,并用该机构的 CA 校验机构服务器证书。如果有环境变量FEP_PORT_<기관코드>(占位符为机构代码),端口就用它。201 把请求{guid,date,amount,acct,name}按 A 规格定长(长度包含自身)发送,并把结果以{"org","rspCode","guid","realName","clientFp"}的一行 JSON 输出;301 把{guid,custId}通过POST /v1/credit-check发送,并以{"org","rspCode","guid","grade","clientFp"}输出。响应码转换为 LH-STD(00→0000,14→B202,30→E102,其余 E500)。/root/eaimw/fep/inbound.py --port <P>:接收外部机构发起的连接(明文 TCP,A 规格)。如果来源地址不在任何机构的allow_ips中,就不读取而直接断开,并在FEP_LOG(默认/root/eaimw/fep/inbound.log)中留下一行DENY <주소>(占位符为地址)。如果是允许的地址,就返回 A 规格的响应(报文类型0210、相同的交易唯一编号、响应码00、实名栏为 20 个空格、指纹栏为 64 个-,长度包含自身)。/root/eaimw/fep/certcheck.py --warn-days <N>:对配置档案中的每个机构,按机构代码顺序输出一行<기관코드> <만료일 YYYY-MM-DD> <남은일수> <OK|WARN>(占位符依次为机构代码、到期日、剩余天数)。剩余天数小于 N 就是 WARN,只要有一个 WARN,就以非 0 的退出码结束。
参考
- 密钥和 CSR:
openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout a.key -out a.csr -subj "/O=LabHub Bank/CN=labhub-fep-a" - 确认连接:
printf 'PING\n' | openssl s_client -connect 127.0.0.1:9441 -cert a.crt -key a.key -CAfile partners/A/ca.crt -quiet - Python 双向 TLS:
ctx = ssl.create_default_context(cafile=CA); ctx.load_cert_chain(CERT, KEY)之后ctx.wrap_socket(sock, server_hostname="127.0.0.1")。机构服务器证书中以 SAN 形式包含 IP 127.0.0.1。HTTPS 用urllib.request.urlopen(req, context=ctx)。 - 证书指纹:
openssl x509 -in a.crt -outform DER | sha256sum。到期日:用 Python 的ssl.cert_time_to_seconds()读取openssl x509 -in a.crt -noout -enddate的值。 - 常见错误:机构 A 的长度写成了不含自身的 4 字节;关闭机构服务器证书校验(
CERT_NONE);所有机构使用同一张证书。
为每个机构签发证书
创建两个机构 CA,为每个机构生成密钥和 CSR 并获得签名,得到 a.crt/a.key 和 b.crt/b.key。
私钥放在我们这边,只发送 CSR。机构 A 必须由 A 签名,机构 B 必须由 B 签名——如果同一个 CA 给两者都签名,机构就无法拒绝对方的证书。
把机构配置档案做成数据
根据 ORGS.md,在 /root/eaimw/fep/orgs.json 中写入两个机构的地址、格式、编码、长度基准、证书和允许 IP。
为了不让代码中出现机构名称,把各机构不同的值全部转移到这个文件里。长度基准对 JSON 机构不适用,所以是 null。
与机构 A 通过双向 TLS 对接
启动机构 A 服务器,用 a.crt 发送 PING,并把 PONG <指纹> 保存到 /root/eaimw/fep/a-ping.txt。
给 openssl s_client 同时提供 -cert、-key(我的身份)和 -CAfile(机构 CA,校验服务器)。加上 -quiet,就只留下响应正文。指纹是机构所看到的我的证书。
换装证书就会被拒绝
用 b.crt、b.key 去连接机构 A,并把输出和错误一起保存到 /root/eaimw/fep/wrong.txt。
只把第 3 步命令中的证书和密钥换掉。拒绝原因(TLS alert)会输出到标准错误,所以请用 2>&1 一起保存。
按机构选择后发送
让 /root/eaimw/fep/fepgw.py send <机构> 用各机构的证书和规格发送,并输出把结果规范化为 LH-STD 代码的一行 JSON。
从配置档案中查看 format 来选择适配器。A 在生成完整正文之后,用 len(正文)+4 作为长度(包含自身)。服务器校验用 create_default_context(cafile=机构 CA),我的身份用 load_cert_chain。
入站一方——只从允许的地址接收
让 /root/eaimw/fep/inbound.py --port P 只接收来自配置档案 allow_ips 的连接并以 A 规格应答,其余的断开并记录 DENY。
socketserver 处理器的 self.client_address[0] 就是来源地址。允许列表是所有机构配置档案中 allow_ips 的合集。拒绝时不要读取,直接返回。
提前知道到期
让 /root/eaimw/fep/certcheck.py --warn-days N 为每个机构输出到期日、剩余天数、OK/WARN,只要有 WARN,就以非 0 的退出码结束。
读取证书的 notAfter,以天数计算它与现在(UTC)的差。有的机构更换要花几周,所以基准要设得宽裕些——监控工具通过退出码来发现。