eCapture v2 来了,AI Agent 写了 90% 的代码 写这篇文章的时候,eCapture 已经发布到 v2.2.1 了。从 v2.0.0 到现在,短短三个月内经历了 5 个版本迭代。 趁着版本刚稳,笔者来认真梳理一下 v2 系列到底做了哪些事——不只是给用户看的 Feature,还有架构层面那些"没有功劳只有苦劳"的内功修炼。 一、先说结论 v2 系列最核心的变化可以用一句话概括:把整个项目从"能用"变成了"可以持续演进"。 表面上,你可能感知到的是:GoTLS 支持连接四元组了,pcap 写入更稳了,Android 的兼容性更好了。 但水面以下,是整个内部架构的推倒重建——原来杂乱的 user/ 目录彻底删掉了,所有 8 个 Probe 被重新实现,工厂模式、观察者模式、模板方法模式,该用的设计模式全用上了。 第一部分:面向用户 1. GoTLS 终于有四元组了 很多同学用 eCapture 抓 Go 应用的 TLS 流量,一直有个痛点:抓到的明文没有连接信息,不知道这条流量来自哪个 IP、哪个端口。 v2.0.1 和 v2.1.0 里,@zenyanle 接连贡献了两个 PR(#947, #960),给 GoTLS Probe 加上了从 tls.Conn 里提取 fd、进而获取连接四元组的能力。 现在抓到的 GoTLS 明文,连接的源/目 IP 和端口都有了。对于要做流量审计或安全分析的同学,这个功能很关键。 2. pcap 写入更可靠了 v2.0.0 引入了缓冲 pcapng 写入,带上了接口元数据;v2.2.0 修掉了一个 Close() 里的竞态条件——之前在高并发场景下,DSB keylog 写入会出现数据乱序或丢失的情况。 现在写 pcap 文件,序列化保证了,关闭时也不会丢尾巴了。 3. Android 支持更成熟 BoringSSL Android 16 的 offset 更新(#885) Android 模拟器的 DNS 解析问题修复(#957) Android e2e PCAP 模式自动探测活跃网络接口(#976) Build tag 从 androidgki 统一改名为 ecap_android(#930) 对于在 Android 上用 eCapture 做流量分析的同学,v2 系列的稳定性比 v1 强了不止一个量级。 4. 重新支持 --cgroup_path 参数 v2.0.0 的大重构里,tls 子命令的 --cgroup_path 参数被不小心砍掉了。v2.2.1 把它恢复了(#975)。 用 cgroup 做隔离抓包的同学,可以放心升级了。 5. 全覆盖的 E2E 测试体系 v2.0.0 里加入了 72+ 个 E2E 测试场景,覆盖了 bash、tls、gnutls、gotls 全部模块,以及 pcap/keylog/text 三种抓包模式。 这意味着每次发版前,核心路径都会被自动验证一遍。用户升级时踩到"老功能突然坏了"这种坑的概率,大幅降低。 第二部分:面向开发者 Warning:以下是面向 eCapture 开发者或想给项目贡献代码的同学的内容,只是日常使用的话可以不用往下看。 1. 架构重构:user/ 目录彻底消失了 v1 时代,所有 Probe 的代码都堆在 user/ 目录下,随着支持的协议越来越多,这个目录越来越难维护——命名不统一、接口不一致、职责边界模糊。 v2.0.0 用 7 个 Phase 完成了完整的迁移: 所有 8 个 Probe(Bash、Zsh、MySQL、Postgres、OpenSSL、GnuTLS、NSPR、GoTLS)完成了标准化迁移。每个 Probe 现在都有统一的文件结构: internal/probe/<name>/ config.go # 配置,嵌入 BaseConfig,实现 Validate() event.go # 事件结构,实现 DecodeFromBytes() register.go # init() 里调用 factory.RegisterProbe() <name>_probe.go # 嵌入 BaseProbe,实现 Initialize/Start/Stop/Close 2. 核心设计模式 v2 引入了三个设计模式,彻底解耦了各个组件: 工厂模式:每个 Probe 在 register.go 的 init() 里自注册,CLI 命令通过 factory.NewProbe() 创建实例,互不依赖。 模板方法模式:BaseProbe 实现公共流程,子 Probe 只需覆写差异化逻辑。 观察者模式:事件通过 EventDispatcher 分发,Output Writer(file/pcap/keylog/websocket)作为订阅者,彼此完全解耦。 3. 事件流转全链路 从 eBPF 内核态抓到数据,到最终写入文件或推送到远端,完整路径如下: 这个链路在 v2 里被完整地标准化了。之前 v1 里部分 Probe 是直接写文件,绕过了 Dispatcher,导致远端推送(ecaptureQ 模式)下数据丢失——这个 Bug 在 v2.1.0 的 #964 里修掉了。 4. 破坏性变更备忘 如果你有基于 eCapture 源码开发的二次开发代码,升级 v2 需要注意以下几点: 变更项 v1 v2 Probe 代码位置 user/ internal/probe/ Build tag(Android) androidgki ecap_android eBPF bytecode 目录 项目根 ebpfassets/ Probe 注册方式 手动初始化 init() 自注册 + Factory 更丰富的官方文档 V2.0.0 里,官方网站也做了重构。 除了官网首页,使用文档也经历了大幅度的扩充和重构,分为概述、架构设计、捕获模块、输出格式、开发指南等多个章节,覆盖了从用户入门到开发者贡献的全流程。 番外:AI Agent 写了多少代码? 最近 AI Agent 是个大热词,笔者被问到最多的问题就是:eCapture v2 里,到底有多少代码是 AI 写的? 说个实际数据。从 v2.0.0 到 v2.2.1,GitHub 上合并了约 37 个 PR,分布如下: 贡献者 PR 数量 占比 @Copilot(GitHub Copilot AI Agent) 13 ~35% @cfc4n(笔者) 21 ~57% 社区贡献者(@zenyanle、@Carl Chen 等) 3 ~8% v2.0.0 那次大重构是 AI 参与度最高的版本:8 个 Probe 的标准化迁移、E2E 测试框架搭建,10 个 PR 直接由 @Copilot 提交,占当版 PR 总量的近 45%。 再看代码行数。说实话,这个数字笔者自己都没料到会这么极端。v2 系列 90% 的代码是 AI 写的,笔者的角色更像产品经理加 Code Reviewer。 贡献者 新增行数 删除行数 新增占比 @Copilot(AI Agent) 28,426 10,392 ~90% @cfc4n(笔者) 2,097 781 ~7% 社区贡献者 1,082 96 ~3% 但聪明的同学一定想问:AI 是怎么知道该做什么的? 答案是:每一个 AI PR 背后,都对应着笔者写的一个详细 Issue。任务描述写得越清楚,AI 出活越靠谱;描述含糊的,烂 PR 照样打回去重写。那段时间的工作流,基本是"笔者当产品经理+Code Reviewer,AI 当执行工程师"。 AI Agent 擅长的是模式清晰、重复度高的任务——比如"按照这个标准模板,把剩余 6 个 Probe 全部重构一遍",这种活交给 AI 效率极高。但涉及 eBPF 内核行为的细节、并发竞态的根因分析,还是得靠人来兜底。 v2 这次实践下来,笔者的感受是:AI Agent 不是替代开发者,而是把开发者从重复劳动里解放出来,让人能把精力放在真正需要判断力的地方。 最后 v2 是一次迟到很久的重构。老实说,v1 时代的 user/ 目录,笔者自己看着都头疼。这次借着 AI Coding Agent 的帮助,总算把欠的技术债还掉了大半。 后面 v2.x 的迭代重心会放在功能上——更多 TLS 库版本支持、更丰富的过滤能力、更完善的远端推送协议。架构的活算是告一段落了。 有问题欢迎到 GitHub 提 Issue,PR 更欢迎。 GitHub: https://github.com/gojue/ecapture 官网: https://ecapture.cc 2026-04-06 0 0
eCapture v0.9.0支持IPv6、GnuTLS密钥捕获、Zsh审计,国内CDN上线 自上篇文章〔L4LB四层负载均衡IP伪造漏洞〕以来,已经快四五个月没更新,不是笔者太懒,而是实在太忙了,忙得连放屁的时间都没有。得益于社区朋友的贡献,增加了一些新特性,今天来分享给大家。 eCapture v0.9.0版本发布 新功能: 支持Docker镜像拉取,添加对 OpenSSL 3.4.0 的支持,支持 gnutls 的 keylog 和 pcap 模式,允许捕获 IPv6 数据包,拆分日志记录器。 错误修复: 修复了多个系统和功能的关键错误,包括在 Ubuntu 24.04 上的初始化脚本问题和 arm64 版本的兼容性问题。 改进: 更新了日志记录器,优化了构建过程,添加了新的工作流和文档更新。 科普:eCapture是什么 eCapture旁观者是一个无需CA证书,无侵入的HTTPS/TLS明文抓包工具。可以在Linux 4.18以上版本使用,同时也支持Android arm64 5.5以上版本。项目发布三年累计14000颗星。 功能介绍 eCapture 有8个模块,分别支持openssl/gnutls/nspr/boringssl/gotls等类库的TLS/SSL加密类库的明文捕获、Bash、Mysql、PostGres软件审计。 bash 捕获bash、zsh命令行的输入输出 gnutls 捕获基于gnutls类库加密通讯的明文内容 gotls 捕获使用Golang语言编写的,基于内置crypt类库实现TLS/HTTPS加密通讯的明文内容 mysqld 捕获Mysqld的SQL查询,适用于数据库审计场景,支持Mysqld 5.6/5.7/8.0等 nss 捕获基于nss类库加密通讯的明文内容 postgres 支持postgres 10+的数据库审计,捕获查询语句 tls 捕获基于Openssl/Boringssl的加密通讯的明文内容,支持Openssl 1.0.x/1.1.x/3.x系列所有版本,支持BoringSSL所有发行版本 你可以通过ecapture -h来查看这些自命令列表。 重大更新 支持Docker Image镜像启动,可以到Docker Hub下载。 支持gnutls模块的keylog 和 pcap 模式对Gnutls TLS类库的密钥捕获、pcapNG格式文件存储; 支持文本模式-m text下,HTTP 2.0的内容解码; 支持IPv6 协议的网络包捕获; 支持非Android的boringSSL库加密通讯的明文、密钥、pcapNG捕获; 支持Zsh Shell的命令执行安全审计; 支持OpenSSL模块的文本模式下,五元组关联数据; 增加eCapture项目的国内下载地址:https://ecapture.cc/zh/download/ (自费CDN小水管,各位黑客大哥别把我帐号刷欠费了) Gnutls TLS类库捕获 在eCapture的gnutls模块中,新增TLS加密通讯的密钥捕获,以及保存为pcapNG格式文件。 目标进程类库确认 以使用gnutls类库的git命令为例,当执行git pull时,会调用git-remote-http程序来访问远程仓库,该程序使用Gnutls类库 ldd /usr/lib/git-core/git-remote-http linux-vdso.so.1 (0x0000ffff9c6ff000) libcurl-gnutls.so.4 => /lib/aarch64-linux-gnu/libcurl-gnutls.so.4 (0x0000ffff9c550000) libpcre2-8.so.0 => /lib/aarch64-linux-gnu/libpcre2-8.so.0 (0x0000ffff9c4b0000) ... 演示过程 完整演示如下: 首先,在终端中启动ecapture sudo ecapture gnutls -m pcapng -i ens160 -w gnutls-github.pcapng tcp port 443 之后,在另外一个终端拉取git仓库 git pull 演示结果 eCapture的结果输出: sudo ecapture gnutls -m pcapng -i ens160 -w gnutls-github.pcapng tcp port 443 2024-12-15T13:31:38Z INF AppName="eCapture(旁观者)" 2024-12-15T13:31:38Z INF HomePage=https://ecapture.cc 2024-12-15T13:31:38Z INF Repository=https://github.com/gojue/ecapture 2024-12-15T13:31:38Z INF Author="CFC4N <cfc4ncs@gmail.com>" 2024-12-15T13:31:38Z INF Description="Capturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64." 2024-12-15T13:31:38Z INF Version=linux_arm64:v0.9.0:6.5.0-1025-azure 2024-12-15T13:31:38Z INF Listen=localhost:28256 2024-12-15T13:31:38Z INF eCapture running logs logger= 2024-12-15T13:31:38Z INF the file handler that receives the captured event eventCollector= 2024-12-15T13:31:38Z INF listen=localhost:28256 2024-12-15T13:31:38Z INF https server starting...You can upgrade the configuration file via the HTTP interface. 2024-12-15T13:31:38Z INF Kernel Info=5.15.167 Pid=150461 2024-12-15T13:31:38Z INF BTF bytecode mode: CORE. btfMode=0 2024-12-15T13:31:38Z INF GnuTlsProbe init eBPFProgramType=PcapNG model=PcapNG 2024-12-15T13:31:38Z INF module initialization. isReload=false moduleName=EBPFProbeGNUTLS 2024-12-15T13:31:38Z INF Module.Run() 2024-12-15T13:31:38Z INF GnuTLS version found Version=3.7.3 2024-12-15T13:31:38Z INF GnuTLS binary path binaryPath=/lib/aarch64-linux-gnu/libgnutls.so.30 elfType=2 2024-12-15T13:31:38Z INF BPF bytecode loaded bytecode filename=user/bytecode/gnutls_3_7_3_kern_core.o 2024-12-15T13:31:38Z INF Hook type: Gnutls elf ElfType=2 IFindex=2 IFname=ens160 PcapFilter="tcp port 443" binrayPath=/lib/aarch64-linux-gnu/libgnutls.so.30 2024-12-15T13:31:38Z INF Hook masterKey function: gnutls_handshake 2024-12-15T13:31:38Z INF target all process. 2024-12-15T13:31:38Z INF packets saved into pcapng file. pcapng path=/home/cfc4n/gnutls-github.pcapng 2024-12-15T13:31:39Z INF perfEventReader created mapSize(MB)=4 2024-12-15T13:31:39Z INF perfEventReader created mapSize(MB)=4 2024-12-15T13:31:39Z INF module started successfully. isReload=false moduleName=EBPFProbeGNUTLS 2024-12-15T13:31:43Z INF CLIENT_RANDOM save success ClientRandom=8517a6741b2a67369079309c2eac84094aacecef39a5514c6da25da7e219401d TlsVersion=GNUTLS_TLS1_3 eBPFProgramType=PcapNG 2024-12-15T13:31:44Z INF packets saved into pcapng file. count=35 2024-12-15T13:31:46Z INF packets saved into pcapng file. count=2 ^C2024-12-15T13:32:15Z INF packets saved into pcapng file. count=37 2024-12-15T13:32:15Z INF Module closed,message recived from Context 2024-12-15T13:32:15Z INF iModule module close 2024-12-15T13:32:15Z INF bye bye. 可以清楚得看到TLS通讯协议是GNUTLS_TLS1_3,密钥的ClientRandom是8517a6741b2a67369079309c2eac84094aacecef39a5514c6da25da7e219401d。 Wirehark查看网络包 如图,不光能看到网络包对应发起的进程信息,还能看到HTTPS的明文内容。 Docker Hub eCapture的Docker Hub仓库地址为:https://hub.docker.com/r/gojue/ecapture ,当前最新版为 v0.9.1 。 # 拉取镜像 docker pull gojue/ecapture:latest # 运行 docker run --rm --privileged=true --net=host -v ${宿主机文件路径}:${容器内路径} gojue/ecapture ARGS Zsh Shell审计 启动命令sudo ecapture zsh,即可捕获zsh上运行的所有命令,对于安全审计场景,还是比较方便快捷的。 其他更多功能,请查看项目变更日志。 变更日志 功能 feat: 在 gnutls 中支持 keylog 和 pcap 模式 feat: 移除 tcp 数据包限制 kern: 支持 openssl 最新版本(3.4.0, 1.1.1w 等) feat: 以文本模式解析 http2 数据 feat: 允许捕获 ipv6 数据包 kern: 在 ebpf TC hook 中支持 uid/pid 过滤 user: 拆分记录器,将其分为记录器和事件收集器 kern: 添加对非 Android boringssl 库的支持 kern: 调整密钥获取的时机,以区分 TLS feat: 支持捕获 zsh 命令 feat: 用远程地址信息丰富地址信息 feat: 支持新版本检测功能 Bug 修复 builder: 修复 init 脚本在 ubuntu 24.04 系统上无法运行的问题 #667 修复 uprobe 类型 hook 的参数错误问题 修复 arm64 版本无法工作的 bug (#649) builder: docerk 构建错误:未找到头文件 修复 Golang TLS 模块中读取函数返回值的问题 fix: 修复在调试模式下 make 时未声明标识符的错误 fix: 回退到默认版本并发出警告 fix: 创建 output.log 失败 pkg: 修复在关闭的通道上发送数据的问题 fix: HEAD 请求中的 DumpResponse 错误 fix: 截断的主体转储错误 user: 修复 #553,hashLen 不允许超过 64 字节 android 版本编译失败 user: 修复 #542,masterkey 被多次写入 pcapng fix: pcap 过滤器未按预期工作 fix: 修复 ecapture Docker 镜像 CVE-2024-24790 fix: 处理器打印 incoming chan is full 并退出 改进 cli: 使用格式化记录器 rs/zerolog chore: 在构建过程中使用 -tags 'netgo' 以避免由于不同 Linux 发行版中的 glibc 版本不同而导致的 SIGSEGV 常量参数通知 cli: 更新 docker 使用说明 添加可能的 Linux 内核配置路径 user: 在 gotls 模块中格式化 clientRandom 字符串 cli: 支持记录器级别 使用 kprobe/__sys_connect 替代 uprobe/connect makefile: 优化内核头文件的生成机制 添加 dockerfile BPF 名称应在 _core/_noncore 后附加 user: 为面向服务的架构做准备 docs: 修复日语翻译 chore: 在 rpmBuild.spec 中移除未使用的标志 BuildRequires workflows: 更新 Linux 源 tgz 文件版本 build(deps): 将 golang.org/x/crypto 从 0.23.0 升级到 0.31.0 feat: 检测 CAP_BPF pkg: 添加 http2 请求/响应单元测试 feat: 在销毁 socket 时清理映射 user: 调整内核版本显示的时机,避免过低 感谢 感谢社区小伙伴的贡献:Leon Hwang、SenberHu、yuweizzz、sancppp、Nadeshiko Manju、行旅途、w568w 等,一起继续为爱发电。 2024-12-26 0 0
eCapture旁观者:Android HTTPS明文抓包,无需CA证书 前言 Android上抓包HTTPS是不是越来越难了?高版本无法添加CA证书,抓包软件依赖太多,VPN模式、或HOOK程序时,会被APP检测到。对抗成本愈加增高。有什么万能的工具吗? 是的,eCapture for Android来了。以后在Android上抓HTTPS通讯包,再也不用安装CA证书了,再也不用下载一堆python依赖环境了,再也不用重打包ssl类库了,再也不用改一堆手机参数了,一键启用,简单明了。 eCapture简介 eCapture是一款无需CA证书即可抓获HTTPS明文的软件。支持pcapng格式,支持Wireshark直接查看。基于eBPF技术,仅需root权限,即可一键抓包。 eCapture中文名旁观者,即 当局者迷,旁观者清。 2022年年初上海疫情期间,笔者开始编写并开源,至今已经半年,GitHub上已经 4200个星星。 eCapture是基于eBPF技术实现的抓包软件,依赖系统内核是否支持eBPF。目前支持在操作系统上,支持了X86_64\ARM64的Linux kernel 4.18以上内核,支持了ARM64 Android(Linux) kernel 5.4以上版本。最新版是在2022年9月9日发布的v0.4.3版本。 演示视频 下载后,一条命令启动,干净利索。 ./ecapture tls -w ecapture.pcapng 先看演示视频,演示环境为Ubuntu 21.04、Android 5.4 (Pixel 6)。 模块功能 eCapture支持tls、bash、mysqld、postgres等模块的信息提取与捕获。本文仅讨论tls这个HTTPS/TLS明文捕获模块。 加密通讯明文捕获--tls模块 tls模块在加密通讯类库上,支持了openssl、gnutls、nspr/nss、boringssl等类库。 但在Android上,pcapng模式只支持boringssl,文本模式则都支持。 如何使用eCapture 环境依赖 操作系统 Linux kernel 4.18以上,Android kernel 5.4以上。 支持BPF,可选支持BTF(eCapture版本不同) root权限 版本选择 BPF CO-RE特性为BTF通用的格式,用于做跨内核版本兼容。有的Android手机是没开启BTF。可以查看系统配置确认,CONFIG_DEBUG_INFO_BTF=y为开启;CONFIG_DEBUG_INFO_BTF=n为关闭;其他为不支持BPF,无法使用eCapture。 cfc4n@vm-server:~$# cat /boot/config-`uname -r` | grep CONFIG_DEBUG_INFO_BTF CONFIG_DEBUG_INFO_BTF=y Android系统上,config是gzip压缩的,且配置文件目录也变了。可使用zcat /proc/config.gz命令代替。 eCapture默认发行了支持CO-RE的ELF程序。Android版会发行一个5.4内核不支持BTF(即没有CO-RE)的版本。 下载后,可以通过./ecapture -v确认。 非CO-RE版本的version信息中包含编译时的内核版本。 # no CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:5.4.0-104-generic # CO-RE eCapture version: linux_aarch64:0.4.2-20220906-fb34467:[CORE] 若版本不符合自己需求,可以自行编译,步骤见文末。 全局参数介绍 全局参数重点看如下几个 root@vm-server:/home/cfc4n/# ecapture -h --hex[=false] print byte strings as hex encoded strings -l, --log-file="" -l save the packets to file -p, --pid=0 if pid is 0 then we target all pids -u, --uid=0 if uid is 0 then we target all users --hex 用于stdout输出场景,展示结果的十六进制,用于查看非ASCII字符,在内容加密、编码的场景特别有必要。 -l, --log-file= 保存结果的文件路径。 -p, --pid=0 捕获的目标进程,默认为0,则捕获所有进程。 -u, --uid=0 捕获的目标用户,默认为0,则捕获所有用户,对Android来说,是很需要的参数。 模块参数 root@vm-server:/home/cfc4n/project/ssldump# bin/ecapture tls -h OPTIONS: --curl="" curl or wget file path, use to dectet openssl.so path, default:/usr/bin/curl --firefox="" firefox file path, default: /usr/lib/firefox/firefox. --gnutls="" libgnutls.so file path, will automatically find it from curl default. --gobin="" path to binary built with Go toolchain. -h, --help[=false] help for tls -i, --ifname="" (TC Classifier) Interface name on which the probe will be attached. --libssl="" libssl.so file path, will automatically find it from curl default. --nspr="" libnspr44.so file path, will automatically find it from curl default. --port=443 port number to capture, default:443. --pthread="" libpthread.so file path, use to hook connect to capture socket FD.will automatically find it from curl. --wget="" wget file path, default: /usr/bin/wget. -w, --write="" write the raw packets to file as pcapng format. -i参数 -i参数为网卡的名字,Linux上默认为eth0,Android上默认为wlan0,你可以用这个参数自行指定。 输出模式 输出格式支持两种格式,文本跟pcapng文件。有三个参数, 默认,全局参数,输出文本结果到stdout -l 全局参数,保存文本结果的文件路径 -w 仅tls模块参数,保存pcapng结果的文件路径 类库路径 Linux上支持多种类库,不同类库的路径也不一样。 类库 参数路径 默认值 openssl/boringssl --libssl Linux自动查找,Android为/apex/com.android.conscrypt/lib64/libssl.so gnutls --gnutls Linux自动查找,Android pcapng模式暂未支持 nspr/nss --nspr Linux自动查找,Android pcapng模式暂未支持 文本模式 -l 或者不加 -w 参数将启用该模式。 支持openssl、boringssl、gnutls、nspr/nss等多种TLS加密类库。 支持DTLS、TLS1.0至TLS1.3等所有版本的加密协议。 支持-p、-u等所有全局过滤参数。 pcapng模式 -w 参数启用该模式,并用-i选择网卡名,Linux系统默认为eth0,Android系统默认为wlan0, 仅支持openssl、boringssl两个类库的数据捕获。暂不支持TLS 1.3协议。 类库与参数支持 在Linux系统上,大部分类库与参数都是可以支持的。但在Android系统上,因为内核与ARM架构的原因,支持的参数上,有一定的差异。 不同模式的参数支持 -p、-u两个全局参数,支持文本模式,不支持pcapng模式。这是因为pcapng模式是使用eBPF TC技术实现。 模式 -p -u --libsso --port 文本 ✅ ✅ ✅ ✅ pcapng ❌ ❌ ✅ ✅ 不同模式的类库以协议支持 模式 openssl(类库) boringssl(类库) TLS 1.0/1.1/1.2(协议) TLS 1.3(协议) 文本 ✅ ✅ ✅ ✅ pcapng ✅ ✅ ✅ ❌ pcapng模式暂时不支持TLS 1.3,TLS 1.3密钥捕获功能已经开发完成,只是遇到一些BUG,还在解决中。 笔者不是openssl的专家,对TLS 协议也不太熟。需要补充这两块的知识,解决起来成本比较高,也欢迎对这块擅长的朋友一起来解决。 与tcpdump联合使用 eCapture基于eBPF TC,实现了流量捕获,并保存到pcapng文件中。基于eBPF Uprobe实现了TLS Master Secret的捕获。并基于Wireshark的Decryption Secrets Block (DSB)标准,实现了gopacket的DSB功能,合并网络包与密钥,保存到pcapng中。 eCapture在网络包捕获上,没有tcpdump强大,不支持丰富的参数。你可以用eCapture捕获master secrets,用tcpdump捕获网络包,然后使用wiresahrk自定义设置密钥文件,配合使用。 网络包捕获 tcpdump 的常规用法,不再赘述。 密钥捕获 同时启用ecapture ,模式可以选文本或者pcapng,都会保存TLS的master secrets密钥数据到ecapture_masterkey.log中。 网络包查看 用Wireshark打开网络包文件,设置这个master key文件,之后就可以看到TLS解密后的明文了。 配置路径:Wireshark --> Preferences --> Protocols --> TLS --> (Pre)-Master-Secret log filename 参数 指定路径 默认路径 在Android上,Google使用了boring ssl类库,也就是C++语言在libssl基础上的包装。默认情况下,会使用/apex/com.android.conscrypt/lib64/libssl.so路径。 APP的类库确认 你可以使用lsof -p {APP PID}|grep libssl来确认。 若不是默认路径,则可以使用--libssl参数来指定。 高级用法 如果你需要查看的APP是自定义SSL类库,那么你可以自助修改eCapture来实现。 自定义函数名与offset 首先,需要确定HOOK函数的函数名或者符号表地址。 没有源码 如果你没有类库源码可以通过IDA等软件静态分析、动态调试,确定SSL Write的地址offset。在配置填写在user/module/probe_openssl.go文件中,对应的probe配置部分。 { Section: "uprobe/SSL_write", EbpfFuncName: "probe_entry_SSL_write", AttachToFuncName: "SSL_write", UprobeOffset: 0xFFFF00, // TODO BinaryPath: binaryPath, }, offset自动计算 如果你有源码,则可以自行阅读源码确定函数名或者符号表的地址。对于结构体的成员属性读取,则可以通过offsetof宏来自动计算。 // g++ -I include/ -I src/ ./src/offset.c -o off #include <stdio.h> #include <stddef.h> #include <ssl/internal.h> #include <openssl/base.h> #include <openssl/crypto.h> #define SSL_STRUCT_OFFSETS \ X(ssl_st, session) \ X(ssl_st, s3) \ X(ssl_session_st, secret) \ X(ssl_session_st, secret_length) \ X(bssl::SSL3_STATE, client_random) \ X(bssl::SSL_HANDSHAKE, new_session) \ X(bssl::SSL_HANDSHAKE, early_session) \ X(bssl::SSL3_STATE, hs) \ X(bssl::SSL3_STATE, established_session) \ X(bssl::SSL_HANDSHAKE, expected_client_finished_) struct offset_test { /* data */ int t1; bssl::UniquePtr<SSL_SESSION> session; }; int main() { printf("typedef struct ssl_offsets { // DEF \n"); #define X(struct_name, field_name) \ printf(" int " #struct_name "_" #field_name "; // DEF\n"); SSL_STRUCT_OFFSETS #undef X printf("} ssl_offsets; // DEF\n\n"); printf("/* %s */\nssl_offsets openssl_offset_%d = { \n", OPENSSL_VERSION_TEXT, OPENSSL_VERSION_NUMBER); #define X(struct_name, field_name) \ printf(" ." #struct_name "_" #field_name " = %ld,\n", \ offsetof(struct struct_name, field_name)); SSL_STRUCT_OFFSETS #undef X printf("};\n"); return 0; } 参数提取 对于参数,你需要确认被HOOK函数的参数类型,以便确认读取方式,可以参考kern/openssl_kern.c内的SSL_write函数实现。 编译 ARM Linux 编译 公有云厂商大部分都提供了ARM64 CPU服务器,笔者选择了腾讯云的。在广州六区中,名字叫标准型SR1(SR1即ARM 64CPU),最低配的SR1.MEDIUM2 2核2G即满足编译环境。可以按照按量计费方式购买,随时释放,比较划算。 操作系统选择ubuntu 20.04 arm64。 ubuntu@VM-0-5-ubuntu:~$sudo apt-get update ubuntu@VM-0-5-ubuntu:~$sudo apt-get install --yes wget git build-essential pkgconf libelf-dev llvm-12 clang-12 linux-tools-generic linux-tools-common ubuntu@VM-0-5-ubuntu:~$wget https://golang.google.cn/dl/go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.18.linux-arm64.tar.gz ubuntu@VM-0-5-ubuntu:~$for tool in "clang" "llc" "llvm-strip" do sudo rm -f /usr/bin/$tool sudo ln -s /usr/bin/$tool-12 /usr/bin/$tool done ubuntu@VM-0-5-ubuntu:~$export GOPROXY=https://goproxy.cn ubuntu@VM-0-5-ubuntu:~$export PATH=$PATH:/usr/local/go/bin 编译方法 ANDROID=1 make 命令编译支持core版本的二进制程序。 ANDROID=1 make nocore命令编译仅支持当前内核版本的二进制程序。 当Wireshark被eBPF增强 Wireshark + eBPF = eCapture 贡献者 感谢chriskaliX 、chenhengqi、vincentmli、huzai9527、yihong0618、sfx(4ft35t)等朋友的贡献。 感谢tiann Weishu在Android这个需求上的推动。 招聘 Leader直招,没有中间商赚差价。面向RASP、HIDS产品,JVM、Linux内核入侵检测等安全研发职位 JAVA资深研发工程师 golang高级工程师 更多职位见 https://www.cnxct.com/jobs/ 2022-09-13 4 0
[转] 写给工程师:关于证书(certificate)和公钥基础设施(PKI)的一切 转载序 本文转载 http://arthurchiao.art/blog/everything-about-pki-zh/ ,感谢译者ArthurChiao、作者的贡献。在之前,我写过一篇关于HTTPS证书吊销机制的文章,那时也一直对PKI体系混乱的知识点有很多盲点,也一直想搞清楚这些,文章末尾还立下flag,至今两年,惨遭打脸。 这几天在学习eBPF领域知识,在ArthurChiao的博客上学习了很多,也就遇到了这篇文章,以借花献佛,转给各位看官,也算填平了两年前的flag吧。 译者序 本文翻译自 2018 年的一篇英文博客:Everything you should know about certificates and PKI but are too afraid to ask,作者 MIKE MALONE。 这篇长文并不是枯燥、零碎地介绍 PKI、X.509、OID 等概念,而是从前因后果、历史沿革的角度把这些东西串联起来,逻辑非常清晰,让读者知其然,更知其所以然。 证书和 PKI 的目标其实很简单:将名字关联到公钥(bind names to public keys)。 加密方式的演进: MAC 最早的验证消息是否被篡改的方式,发送消息时附带一段验证码 | 双方共享同一密码,做哈希;最常用的哈希算法:HMAC | \/ Signature 解决 MAC 存在的一些问题;双方不再共享同一密码,而是使用密钥对 | | \/ PKC 公钥加密,或称非对称加密,最常用的一种 Signature 方式 | 公钥给别人,私钥自己留着; | 发送给我的消息:别人用 *我的公钥* 加密;我用我的私钥解密 \/ Certificate 公钥加密的基础,概念:CA/issuer/subject/relying-party/... | 按功能来说,分为两种 | |---用于 *签名*(签发其他证书) 的证书 |---用于 *加解密* 的证书 证书(certificate)相关格式及其关系(沉重的历史负担): 最常用的格式 | 信息比 X.509 更丰富的格式 | 其他格式 mTLS 等常用 Java 常用 微软常用 .p7b .p7c .pfx .p12 X.509 v3 PKCS#7 PKCS#12 SSH 证书 PGP 证书 =====> 证书格式 \ | / (封装格式,证书结构体) \ | / \ | / \ | / \-------------+----------------/ | ASN.1 (类似于 JSON、ProtoBuf 等) =====> 描述格式 | /-------------+----------------\ / | \ / | \ / | \ / | \ DER PEM =====> 编码格式 二进制格式 文本格式 (序列化) .der .pem .crt .cer 一些解释: X.509 从结构上定义证书中应该包含的信息,例如签发者、秘钥等等;但使用哪个格式(例如 JSON 还是 YAML 还是 ASN.1)来描述,并不属于 X.509 的内容; ASN.1 是 X.509 的描述格式(或者说用 ASN.1 格式来定义 X.509),类似于现在的 protobuf; ASN 中有很多数据类型,除了常见的整形、字符串等类型,还有一个称为 OID 的特殊类型,用点分整数表示,例如 2.5.4.3,有点像 URI 或 IP 地址,在设计上是全球唯一标识符。 ASN.1 只是一种描述格式,并未定义如何序列化为比特流,因此又引出了 ASN.1 的编码格式。 ASN.1 与其编码格式的关系,与 unicode 与 utf8 的关系类似。 DER:一种二进制编码格式 PEM:一种文本编码格式,通常以 .pem、.crt 或 .cer 为后缀。 某些场景下,X.509 信息不够丰富,因此又设计了一些信息更丰富(例如可以包含证书链、秘钥)的证书封装格式,包括 PKCS #7 和 #12。 仍然用 ASN.1 格式描述 基本都是用 DER 编码 以上提到的东西,再加上 CA、信任仓库、信任链、certificate path validation、CSR、证书生命周期管理、SPIFFE 等还没有提到但也与加密相关的东西,统称为公钥基础设施(PKI)。 翻译时调整了一些配图,也加了几张新图,以方便展示和理解。 由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。 以下是译文。 前言 证书(certificates)与 PKI(public key infrastructure,公钥基础设施)很难。我认识的很多非常聪明的人也会绕过这一主题。 我个人也很长时间没去碰这些内容,但说起来很讽刺,我没去碰的原因是不懂,所以不好意思问 —— 然后更不懂,自然更不好意思问 ——如此形成恶性循环。 为什么要学习 PKI 最终我还是硬着头皮学习了这些东西,因为我觉得PKI 能使一个人在加解密层面(乃至更大的安全层面)去思考如何定义一个系统。具体来说,PKI 技术, 都是通用的、厂商无关的(universal and vendor neutral); 适用于任何地方,因此即使系统可分布在世界各地,彼此之间也能安全地通信; 在概念上很简单,并且非常灵活;如果使用我们的 TLS everywhere 模型,那甚至连 VPN 都不需要了。 总之一句话:非常强大! 本文目的 在深入理解了 PKI 之后,我很后悔没有早点学这些东西。 PKI 非常强大且有趣,虽然它背后的数学原理很复杂,一些相关标准也设计地非常愚蠢(巴洛克式的复杂),但其 核心概念其实非常简单; 证书是识别(identify)代码和设备的最佳方式, 而 identity(身份)对安全、监控、指标等很多东西都非常有用; 使用证书并不是太难,不会难于学习一门新语言或一种新数据库。 那为什么大家对这些内容望而却步呢?我认为主要是缺少很好的文档,所以经常看地云里雾里,半途而弃。 本文试图弥补这一缺失。我认为大部分工程师花一个小时读完本文后,头脑中都将有具备了那些关于加解密的最重要概念和最常见使用场景 —— 这正是本文的目的 —— 一小时只是很小的一个投资,而且这些内容是无法通过其他途径学到的。 本文将用到以下两个开源工具: step CLI step certificates 极简 TL; DR(太长不读) 证书和 PKI 的目的是:将名字关联到公钥(bind names to public keys)。 这是关于证书和 PKI 的最高抽象,其他都属于实现细节。 术语 本文将用到以下术语。 Entity(实体) Entity 是任何存在的东西(anything that exists) —— 即使只在逻辑或概念上存在(even if only exists logically or conceptually)。 例如, 你的计算机是一个 entity, 你写的代码也是一个 entity, 你自己也是一个 entity, 你吃的杂粮饼也是一个 entity, 你六岁时见过的幽灵也是一个 entity —— 即使你妈告诉你幽灵并不存在,这只是你的臆想。 Identity(身份) 每个 entity(实体)都有一个 identity(身份)。要精确定义这个概念比较困难,这么来说吧:identity 是使你之所以为你(what makes you you)的东西,懂吗? 具体到计算机领域,identity 通常用一系列属性来表示,描述某个具体的 entity,这里的属性包括 group、age、location、favorite color、shoe size 等等。 Identifier(身份标识符) Identifier 跟 identity 还不是一个东西:每个 identifier 都是一个唯一标识符,也唯一地关联到某个有 identity 的 entity。 例如,我是 Mike,但 Mike 并不是我的 identity,而只是个 name —— 虽然二者在我们小范围的讨论中是同义的。 Claim(声明) & Authentication(认证) 一个 entity 能 claim(声明)说,它拥有某个或某些 name。 其他 entity 能够对这个 claim 进行认证(authenticate),以确认这份声明的真假。 一般来说,认证的目的是确认某些 claim 的合法性。 Claim 不是只能关联到 name,还可以关联到别的东西。例如,我能 claim 任何东西:my age, your age, access rights, the meaning of life 等等。 Subscriber & CA & relying party (RP) 能作为一个证书的 subject 的 entity,称为 subscriber(证书 owner)或 end entity。 对应地,subscriber 的证书有时也称为 end entity certificates 或 leaf certificates,原因在后面讨论 certificate chains 时会介绍。 CA(certificate authority,证书权威)是给 subscriber 颁发证书的 entity,是一种 certificate issuer(证书颁发者)。 CA 的证书,通常称为 root certificate 或 intermediate certificate,具体取决于 CA 类型。 Relying party 是 使用证书的用户(certificate user),它验证由 CA 颁发(给 subscriber)的证书是否合法。 一个 entity 可以同时是一个 subscriber 和一个 relying party。也就是说,单个 entity 既有自己的证书,又使用其他证书来认证 remote peers,例如双向 TLS(mutual TLS,mTLS)场景。 小结 对于我们接下来的讨论,这些术语就够了。下面将进入正题,看如何在实际中实现证书的声明和认证。 想了解更多相关术语,可参考 RFC 4949。 MAC(消息认证码)和 signature(签名) MAC(message authentication code)和 HMAC(hash-based MAC) MAC(消息认证码)是一小段数据,用于验证某个 entity 发送的消息未被篡改。其基本原理如下图所示: MAC/HMAC 原理。图片来自:okta.com 对消息(message)和双方都知道的一个密码(shared secret,a password)做哈希,得到的哈希值就是 MAC; 发送方将消息连带 MAC 一起发给接收方; 接收方收到消息之后,用同一个密码来计算 MAC,然后跟消息中提供的 MAC 对比。如果相同,就证明未被篡改。 关于哈希: 哈希是单向的,因此无法从输出反推输入。这一点至关重要:否则截获消息的人就可以根据 MAC 和哈希函数反推出你的 secrets。 此外,生成 MAC 的哈希算法选择也至关重要,本文不会展开,但提醒一点:不要试图用自己设计的 MAC 算法。 最常用的 MAC 算法是 HMAC(hash-based message authentication code)。 Signature(签名) 讨论 MAC 其实是为了引出 signature(签名)这一主题。 签名在概念上与 MAC 类似,但不是用共享 secret 的方式,而是使用一对秘钥(key pair): MAC 方式中,至少有两个 entity 需要知道共享的 secret,也就是消息的发送方和接收方。双方都可以生成 MAC,因此给定一个合法的 MAC,我们是无法知道是谁生成的。 签名就不同了。签名能用公钥(public key)验证,但只能用相应的私钥(private key)生成。因此对于接收方来说,它只能验证签名是否合法,而无法生成同样的签名。 如果只有一个 entity 知道秘钥,那这种特性称为 non-repudiation(不可否认性):持有私钥的人无法否认(repudiate)数据是由他签名的这一事实。 小结 MAC 与 signature 都叫做签名,是因为它们和现实世界中的签名是很像的。例如,如果想让某人同意某事,并且事后还能证明他们当时的确同意了,就把问题写下来,然后让他们手写签字(签名)。 Public key cryptography(公钥加密,或称非对称加密) 证书和 PKI 的基础是公钥加密(public key cryptography),也叫非对称加密(asymmetric cryptography)。 秘钥对 公钥加密系统使用秘钥对(key pair)加解密。一个秘钥对包含: 一个私钥(private key):owner 持有,解密用,不要分享给任何人; 这一点非常重要,值得重复一遍:公钥加密系统的安全性取决于私钥(private key)的机密性。 一个公钥(public key):加密用,可分发和共享给别人; 秘钥可以做的事情: 加解密:公钥(public key)加密,私钥(private key)解密。 签名:私钥(private key)对数据进行签名(sign some data);任何有公钥的人都可以对签名进行验证,证明这个签名确实是私钥生成的。 公钥加密系统使计算机能“看到”对方 公钥加密是数学给计算机科学的神秘礼物,其数学基础显然很复杂,但如果只是使用,那并不理解它的每一步数学原理。公钥加密使计算机能做一些之前无法做的事情:它们现在能看到对方是谁了。 这句话的意思是说,公钥加密使一台计算(或代码)能向其他计算机或程序证明,不用直接分享某些信息,它也能知道该信息。更具体来说, 以前要证明你有密码,就必须向别人展示这个密码。但展示之后,任何有这个密码的人就都能使用它了。 私钥却与此不同。你能通过私钥对我的身份进行认证(authenticate my identity),但却无法假冒我。 例如,你发给我一个大随机数,我对这个随机数进行签名,然后将再发送给你。你能用公钥对这个签名进行认证,确认这个签名(消息)确实来自我。这就是一种证明你在和我(而不是别的其他的人)通信的很好证据。这使得网络上的计算机能有效地知道它们在和谁通信。 这听起来是一件如此理所当然的事情,但仔细地想一下,网络上只有流动的 0 和 1,你怎么知道消息来自谁,在和谁通信?因此公钥加密系统是一个非常伟大的发明。 证书(certificate):计算机和代码的驾驶证 前面说道,公钥加密系统使我们能知道和谁在通信,但这个的前提是:要知道(有)对方的公钥。 那么,如果对方不知道我的公钥怎么办?这就轮到证书出场了。 想一下,我们需求其实非常简单: 首先要将公钥和它的 owner 信息发给对方; 但光有这个信息还不行,还要让对方相信这些信息; 证书就是用来解决这个问题的,解决方式是请一个双方都信任的权威机构对以上信息作出证明(签名)。 证书的内容:(subscriber 的)公钥+名字 证书是一个数据结构,其中包含一个 public key 和一个 name; 权威机构对证书进行签名,签名的大概意思是:public key xxx 关联到了 name xx; 对证书进行签名的 entity 称为 issuer(或 certificate authority, CA),证书中的 entity 称为 subject。 举个例子,如果某个 Issuer 为 Bob 签发了一张证书,其中的内容就可以解读如下: Some Issuer says Bob's public key is 01:23:42... 证书是权威机构颁发的身份证明,并没有什么神奇之处 其中 Some Issuer 是证书的签发者(证书权威),证书是为了证明这是 Bob 的公钥,Some Issuer 也是这个声明的签字方。 证书的本质:基于对 issuer 公钥的信任来学习其他公钥 由上可知,如果知道 Some Issuer 的公钥,就可以通过验证签名的方式来对它(用私钥)签发的证书进行认证(authenticate)。如果如果你信任 Some Issuer,那你就可以信任这个声明。 因此,证书使大家能基于对 issuer 公钥的信任和知识,来学习到其他 entity 的公钥(上面的例子中就是 Bob)。这就是证书的本质。 与驾照的类比 证书就像是计算机/代码的驾照或护照。如果你之前从未见过我,但信任车管局,那你可以用我的驾照做认证: 首先验证驾照是真的(检查 hologram 等), 然后人脸和照片上对的上, 然后看名字是我,等等。 计算机用证书做类似的事情:如果之前从未和其他电脑通信,但信任一些证书权威,那可以用证书来认证: 首先验证证书是合法的(用证书签发者的公钥检查签名等), 然后提取证书中的(subscriber 的)公钥和名字, 然后用 subscriber 的公钥,通过网络验证该 subscriber 的签名; 查看名字是否正确等等。 证书内容解析举例 下面是个真实的证书: 还是与驾照类比: 驾照:描述了你是否有资格开车; 证书:描述你是否是一个 CA,你的公钥能否用来签名或加密。 二者都有有效期。 上图中有大量的细节,很多东西将在下面讨论到。但归根结底还是本文最开始总结的那句话:证书不过是一个将名字关联到公钥(bind names to public keys)的东西。其他都是实现细节。 证书编码格式及历史演进 接下来看一看证书在底层的表示(represented as bits and bytes)。 这部分内容复杂且相当令人沮丧。事实上,我怀疑证书和秘钥诡异的编码方式是导致 PKI 如此混乱和令人沮丧的根源。 X.509 证书 一般来说,人们提到“证书”而没有加额外限定词时,指的都是 X.509 v3证书。 更准确地说,他们指的是RFC 5280 中描述、CA/Browser Forum Baseline Requirements中进一步完善的PKIX 变种。 换句话说,指的是浏览器理解并用来做 HTTPS(HTTP over TLS)的那些证书。 也有其他的证书格式,例如著名的 SSH 和 PGP 都有它们各自的格式。 本文主要关注 X.509,理解了 X.509,其他格式都是类似的。由于这些证书使用广泛,因此有很好的函数库,而且也用在浏览器之外的场景。毫无疑问,它们是internal PKI 颁发的最常见证书格式。重要的是,这些证书在很多 TLS/HTTPS 客户端/服务端程序中都是开箱即用的。 X.509 起源:电信领域 了解一点 X.509 的历史对理解它会有很大帮助。 X.509 在 1988 年作为国际电信联盟(ITU)X.500 项目的一部分首次标准化。这是通信(telecom)领域的标准,想通过它构建一个全球电话簿(global telephone book)。虽然这个项目没有成功,但却留下了一些遗产,X.509 就是其中之一。 如果查看 X.509 的证书,会看到其中包含了 locality、state、country 等信息,之前可能会有疑问为什么为 web 设计的证书会有这些东西,现在应该明白了,因为X.509 并不是为 web 设计的。 ASN.1:数据抽象格式 X.509 构建在 ASN.1 (Abstract Syntax Notation,抽象语法标注)之上,后者是另一个ITU-T 标准 (X.208 and X.680)。 ASN.1 定义数据类型, 可以将 ASN.1 理解成 X.509 的 JSON, 但实际上更像 protobuf、thrift 或 SQL DDL。 RFC 5280 用 ASN.1 来定义 X.509 证书,其中包括名字、秘钥、签名等信息。 OID (object identitfier) ASN.1 除了有常见的数据类型,如整形、字符串、集合、列表等,还有一个不常见但很重要的类型:OID(object identifier,对象标识符)。 OID 与 URI 有些像,但比 URI 要怪。 OID (在设计上)是全球唯一标识符。 在结构上,OID 是在一个 hierarchical namespace 中的一个整数序列(例如 2.5.4.3)。 可以用 OID 来 tag 一段数据的类型。例如,一个 string 本来只是一个 string,但可以 tag 一个 OID 2.5.4.3,然后就变成了一个特殊 string:这是X.509 的通用名字(common name) 字段。 ASN.1 编码格式 ASN.1 只是抽象(abstract),因为这个标准并未定义在数据层应该如何表示(represented as bits and bytes)。ASN.1 与其编码格式的关系,就像 unicode 与 utf8 的区别。因此,有很多种编码规则(encoding rules),描述具体如何表示 ASN.1 数据。原以为增加这层额外的抽象会有所帮助,但实际证明大部分情况下反而徒增烦恼。 DER (distinguished encoding rules):二进制格式 ASN.1 有很多种编码规则,但用于 X.509 和其他加密相关的,只有一种常见格式:DER —— 虽然有时也会用到 non-canonical的 basic encoding rules (BER,基础编码规则) 。 DER 是非常简单的 TLV(type-length-value)编码,但实际上用户无需关心这些,因为函数库封装好了。但不要高兴得太早 —— 虽然我们不必关心 DER 的编解码,但要能判断给定的某个 X.509 证书是 DER 还是其他类型编码的。这里的其他类型包括: 一些比 DER 更友好的格式, 封装了证书及其他额外信息的格式(something more than just a certificate)。 DER 编码的证书通常以 .der 为后缀。 PEM (privacy enhanced email):文本格式 DER 是二进制格式,不便复制粘贴。因此大部分证书都是以PEM 格式打包的,这是另一个历史怪胎。 如果你熟悉 MIME 的话,二者是比较类似的:由 header、base64 编码的 payload、footer 三部分组成。header 中有标签(label)来描述 payload。例如下面是一个 PEM 编码的 X.509 证书: -----BEGIN CERTIFICATE----- MIIBwzCCAWqgAwIBAgIRAIi5QRl9kz1wb+SUP20gB1kwCgYIKoZIzj0EAwIwGzEZ MBcGA1UEAxMQTDVkIFRlc3QgUm9vdCBDQTAeFw0xODExMDYyMjA0MDNaFw0yODEx BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRc+LHppFk8sflIpm/XKpbNMwx3 SDAfBgNVHSMEGDAWgBTirEpzC7/gexnnz7ozjWKd71lz5DAKBggqhkjOPQQDAgNH ADBEAiAejDEfua7dud78lxWe9eYxYcM93mlUMFIzbWlOJzg+rgIgcdtU9wIKmn5q FU3iOiRP5VyLNmrsQD3/ItjUN1f1ouY= -----END CERTIFICATE----- 但令人震惊的时,即便如此简单的功能,在实现上也已经出现混乱:PEM labels 在不同工具之间是不一致的。RFC 7468 试图标准化 PEM 的使用规范,但也并不完整,不是所有工具都遵循这个规范。 PEM 编码的证书通常以 .pem、.crt 或 .cer 为后缀。再次提醒,这只是“通常”情况,实际上某些工具可能并不遵循这些惯例。 下面介绍几个前面提到的“其他类型的打包格式”。 比 X.509 信息更丰富的证书打包(封装)格式 X.509 只是一种常用的证书格式,但有人觉得这种格式能装的信息不够多,因此又定义了一些比 X.509 更大的数据结构(但仍然用 ASN.1),能将证书、秘钥以及其他东西封装(打包)到一起。因此,有时说我需要“一个证书”时,其实真正说的是包(package)中包含的那个“证书”(a certificate in one of theseenvelopes),而不是这个包本身。 PKCS #7:Java 中常用 你可能会遇到的是一个称为 PKCS(Public Key Cryptography Standards,公钥加密标准)的标准的一部分,它由 RSA labs 发布(真实历史要 更加复杂一些,本文不展开)。 其中的第一个标准是 PKCS#7,后面被IETF 重新冠名为 Cryptographic Message Syntax (CMS),其中可以包含多个证书(以 full certificate chain 方式编码,后面会看到)。 PKCS#7 在 Java 中使用广泛。常见扩展名是 .p7b and .p7c。 PKCS #12:微软常用 另一个常见的打包格式 PKCS#12,它能将一个证书链(这一点与 PKCS#7 类似)连同一个(加密之后的)私钥打包到一起。 微软的产品多用这种格式,常见后缀.pfx and .p12。 再次说明,PKCS#7 和 PKCS#12 envelopes 仍然使用 ASN.1,这意味着它们都能以原始 DER、BER 或 PEM 的格式编码。从我个人的经验来看,二者几乎都是 DER 编码的。 秘钥编解码 秘钥编码(Key encoding)的过程与以上描述的类似(复杂): 用某种 ASN.1 数据结构描述秘钥(key); 用 DER 做二进制编码,或用 PEM (hopefully with a useful header) 做一些稍微友好一些的表示。 秘钥的解密过程(deciphering),一半是是科学,一半是艺术。 如果足够幸运,根据 RFC 7468 就能找到其中的 PEM payload; 椭圆曲线秘钥通常符合 RFC 7468 规范,虽然这里看起来似乎也并没有什么标准。 下面是一个 PEM 编码的椭圆曲线秘钥(PEM-encoded elliptic curve key): $ step crypto keypair --kty EC --no-password --insecure ec.pub ec.prv $ cat ec.pub ec.prv -----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc73/+JOESKlqWlhf0UzcRjEe7inF uu2z1DWxr+2YRLfTaJOm9huerJCh71z5lugg+QVLZBedKGEff5jgTssXHg== -----END PUBLIC KEY----- -----BEGIN EC PRIVATE KEY----- MHcCAQEEICjpa3i7ICHSIqZPZfkJpcRim/EAmUtMFGJg6QjkMqDMoAoGCCqGSM49 AwEHoUQDQgAEc73/+JOESKlqWlhf0UzcRjEe7inFuu2z1DWxr+2YRLfTaJOm9hue rJCh71z5lugg+QVLZBedKGEff5jgTssXHg== -----END EC PRIVATE KEY----- 其他秘钥,通常用 PEM label "PRIVATE KEY" 描述 PEM 编码的 PKCS#8 格式私钥 PEM label "PRIVATE KEY" 描述的秘钥,通常暗示这是一个PKCS#8 payload,这是一种私钥(private key)封装格式,其中包含秘钥类型和其他 metadata。 密码加密的私钥 用密码来加密私钥也很常见(private keys encrypted using a password),这里的密码可以是 a shared secret or symmetric key。看起来大致如下(Proc-Type and DEK-Info 是 PEM 的一部分,表示这个 PEM 的 payload 是用 AES-256-CBC 加密的): -----BEGIN EC PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-256-CBC,b3fd6578bf18d12a76c98bda947c4ac9 qdV5u+wrywkbO0Ai8VUuwZO1cqhwsNaDQwTiYUwohvot7Vw851rW/43poPhH07So sdLFVCKPd9v6F9n2dkdWCeeFlI4hfx+EwzXLuaRWg6aoYOj7ucJdkofyRyd4pEt+ Mj60xqLkaRtphh9HWKgaHsdBki68LQbObLOz4c6SyxI= -----END EC PRIVATE KEY----- PKCS#8 对象也能被加密,这种情况下 header label 应该是 "ENCRYPTED PRIVATE KEY" per RFC 7468。这种情况下不会看到 Proc-Type 和 Dek-Info headers,因为这些信息此时编码到了 payload 中。 公钥、私钥常见扩展名 公钥:.pub or .pem, 私钥:.prv, .key, or .pem。 但再次说明,有些工具或组织可能并不遵循业界惯例。 小结 ASN.1 用于定义数据类型,例如证书(certificate)和秘钥(key)——就像用 JSON 定义一个 request body —— X.509 用 ASN.1 定义。 DER 是一组将 ASN.1 编码成二进制(比特和字节)的编码规则(encoding rules)。 PKCS#7 and PKCS#12 是比 X.509 更大的数据结构(封装格式),也用 ASN.1 定义,其中能包含除了证书之外的其他东西。二者分别在 Java 和 Microsoft 产品中使用较多。 DER 编码之后是二进制数据,不方便复制粘贴,因此大部分证书都是用 PEM 编码的,它用 base64 对 DER 进行编码,然后再加上自己的 label。 私钥通常用是 PEM 编码的 PKCS#8 对象,但有时也会用密码来加密。 如果觉得以上内容理解起来很杂乱,那并不是你的问题,而是加密领域的现状就是如此。我已经尽力了。 PKI (Public Key Infrastructure) 至此我们已经知道了证书的来历和样子,但这仅仅是本文的一半。下面看证书是如何创建和使用的。 Public key infrastructure (PKI) 是一个统称,包括了我们在如下与证书和秘钥管理及交互操作时需要用到的所有东西:签发、分发、存放、使用、验证、撤回等等。就像“数据库基础设施” 一样,这个名词是有意取的这样模糊的。 证书是大部分 PKI 的构建模块,而证书权威是其基础。 PKI 包括了 libraries, cron jobs, protocols, conventions, clients, servers, people, processes, names, discovery mechanisms, and all the other stuff you'll need to use public key cryptography effectively。 自己从头开始构建一个 PKI 是一件极其庞大的工作,但实际上 一些简单的 PKI 甚至并不使用证书。例如, 编辑 ~/.ssh/authorized_keys 文件时,就是在配置一个简单的无证书形式的(certificate-less)PKI,SSH 通过这种方式在扁平文件内实现 public key 和 name 的绑定; PGP 用证书,但不用 CA,而是用一个 web-of-trust model; 甚至可以 用区块链 来 assign name 并将它们 bind 到 public key。 如果从头开始构建一个 PKI,唯一确定的事情是:你需要用到公钥(public keys),其他东西都随设计而异。 下文将主要关注 web 领域使用的 PKI,以及基于 Web PKI 技术、遵循现有标准的 internal PKI。 证书和 PKI 的目标其实很简单:将名字关联到公钥(bind names to public keys)。在下面的内容中,不要忘了这一点。 Web PKI vs Internal PKI 浏览器访问 HTTPS 链接时会用到 Web PKI。虽然也有一些问题,但它大大提升了 web 的安全性,而且基本上对用户透明。在访问互联网 web 服务时,应该在所有可能的情况下都启用它。 Web PKI 由 RFC 5280 定义,CA/Browser Forum (a.k.a., CA/B or CAB Forum) 对其进行了进一步完善。 有时也称为 "Internet PKI" 或 PKIX (after the working group that created it). PKIX 和 CAB Forum 文档涵盖了很大内容。它们定义了前面讨论的各种证书、还定义什么是 “name” 以及位于证书中什么位置、能使用什么签名算法、RP 如何判断 issuer 的证书、如何指定证书的 validity period (issue and expiry dates)、撤回、certificate path validation、CA 判断某人是否拥有一个域名等等。 Web PKI 很重要,是因为浏览器默认使用 Web PKI 证书。 Internal PKI 是用户为自己的产品基础设施使用的 PKI,这些产品包括 服务、容器、虚拟机等; 企业 IT 应用; 公司终端设备,例如笔记本电脑、手机等; 其他需要识别的代码或设备。 Internal PKI 使你能认证和建立加密通道,这样你的服务就可以安全地在公网上的任意位置互相通信了。 有了 Web PKI,为什么还要使用自己的 internal PKI? 首先,简单来说:Web PKI 设计中并没有考虑内部使用场景。即使有了 Let's Encrypt 这样的提供免费证书和自动化交付的 CA,用户还是需要自己处理 rate limits 和availability 之类的事情。如果有很多 service,部署很频繁,就非常不方便。 另外,Web PKI 中,用户对证书生命周期、撤回机制、续约过程、秘钥类型、算法等等很多重要的细节都没有控制权,或只有很少控制权。而下文将会看到,这些都是非常重要的东西。 最后,CA/Browser Forum Baseline Requirements实际上禁止将 Web PKI CA 关联到 internal IPs (e.g., 10.0.0.0/8)及 internal DNS names that aren't fully-qualified and resolvable in public global DNS (e.g., you can't bind a kubernetes cluster DNS name like foo.ns.svc.cluster.local)。如果需要在证书中绑定到这些 name,或者签发大量证书,或者控制证书细节,就需要自己的 internal PKI. 下面一节将看到,信任(或缺乏信任)是避免将 Web PKI 用于内部场景的另一个原因。 总结起来,建议: 面向公网的服务或 API,使用 Web PKI; 其他所有场景,都使用 internal PKI。 Trust & Trustworthiness Trust Stores(信任仓库) 前面介绍到,证书可解读为一个 statement 或 claim,例如: Issuer(签发者)说,该 subject 的公钥是 xxx。 Issuer 会对这份声明进行签名,relying party 能(通过 issuer 的公钥)验证(authenticate)签名是否合法。但这里其实跳过了一个重要问题:relying party 是如何知道 issuer 的公钥的? 预配置信任的根证书 答案其实很简单:relying parties 在自己的 trust store(信任库)预先配置了一个它信任的根证书(trusted root certificates,也称为 trust anchors)列表, 预配置的具体方式(the manner in which this pre-configuration occurs),是 PKI 非常重要的一面: 一种方式是从另一个 PKI 来 bootstrap:可以用一些自动化工具,通过 SSH 将 root 证书拷贝到 relying party。这里用到里前面提到的 SSH PKI。 如果是在 cloud 上,那 PKI 依赖层次(信任链)又深了一步:SSH PKI 是由 Web PKI 加上认证方式来 bootstrap 的,这里的认证是你创建 cloud 账户时选择的认证方式。 信任链 如果沿着这个信任链(chain of trust)回溯足够远,最后总能找到人(people):每个信任链都终结在现实世界(meatspace)。 下面这个图画地更清楚一些, Image credit: Cilium TLS inspection 根证书自签名 信任仓库中的根证书是自签名的(self-signed):issuer 和 subject相同。逻辑上,这种 statement 表示的是: Mike 说:Mike 的公钥是 xxx。 自签名的证书保证了该证书的 subject/issuer 知道对应的私钥,但任何人都可以生成一个自签名的证书,这个证书中可以写任何他们想写的名字(name)。 因此证书的起源(provenance)就非常关键:一个自签名的证书,只有当它进入信任仓库的过程是可信任时,才应该信任这个根证书。 在 macOS 上,信任仓库是由 Keychain 管理的。 在一些 Linux 发行版上,可能只是 /etc 或其他路径下面的一些文件。 如果你的用户能修改这些文件,那最好先确认是你信任这些用户的。 信任仓库的来源 所以,信任仓库又从哪里来?对于 Web PKI 来说,最重要的relying parties 就是浏览器。主流浏览器默认使用的信任仓库 ——及其他任何使用 TLS 的东西 —— 都是由四个组织维护的: Apple's root certificate:iOS/macOS 程序 Microsoft's root certificate program:Windows 使用 Mozilla's root certificate program: Mozilla 产品使用,由于其开放和透明,也作为其他一些信任仓库从基础 (e.g., for many Linux distributions) Google 未维护 root certificate program(Chrome 通常使用所在计算的操作系统的信任仓库),但维护了自己的黑名单,列出了自己不信任的根证书或特定证书。 (ChromeOS builds off of Mozilla's certificate program) 操作系统的信任仓库 操作系统中的信任仓库通常都是系统自带的。 Firefox 自带了自己的信任仓库(通过 TLS 从 mozilla.org 分发 ——bootstrapping off of Web PKI using some other trust store)。 编程语言和其他非浏览器的东西例如 curl,通过默认用操作系统的信任仓库。 因此,这个信任仓库通常情况下,会被该系统上预装的很多东西默认使用;通过软件更新(通常使用另一个 PKI 来签名)而更新。 信任仓库中通常包含了超过 100 个由这些程序维护的常见证书权威(certificate authorities)。其中一些著名的: Let's Encrypt Symantec DigiCert Entrust 如果想编程控制: Cloudflare's cfssl project maintains a github repository that includes the trusted certificates from various trust stores to assist with certificate bundling (which we'll discuss momentarily). For a more human-friendly experience you can query Censys to see which certificates are trusted by Mozilla,Apple, and Microsoft. Trustworthiness(可靠性) 这 100 多个证书权威在理论上是可信的(trusted) —— 浏览器和其他一些软件默认情况下信任由这些权威颁发的证书。 但是,这并不意味着它们是可靠的(trustworthy)。已经出现过 Web PKI 证书权威向政府机构提供假证书的事故,以便窥探流量(snoop on traffic)或仿冒某些网站。这类“受信任的” CA 中,其中在司法管辖权之外的地方运营 —— 包括民主国家和专制国家。 NSA 利用每个可能的机会来削弱 Web PKI。2011 年,两个“受信任的”证书权威DigiNotar and Comodo 都被攻陷了。DigiNotar 证书泄露可能与 NSA 相关。 此外,还有大量 CA 签发格式不对或不兼容的证书。因此,虽然按业界规范来说 这些 CA 是受信的,但按照经验来说它们是不可靠(不靠谱)的。 我们很快就会看到,Web PKI 的安全性取决于安全性最弱的权威(the least secure CA)的安全性。这显然不是我们希望的。 浏览器社区已经在采取行动来解决这些问题。CA/Browser Forum Baseline Requirements 规定了这些受信的证书权威在签发证书时应该遵守的规则。作为 WebTrust audit 项目的一部分,在将 CA 加入到某些信任仓库(例如 Mozilla 的)之前,会对 CA 合规性进行审计。 如果内部场景(internal stuff)已经在使用 TLS,你可能大部分情况下并不需要信任这些 public CA。如果信任了,就为 NSA 和其他组织打开了一扇地狱之门:你的系统安全性将取决于 100 多个组织中安全性最弱的那一个。 Federation 证书欺骗的风险 令事情更糟糕的是,Web PKI relying parties (RPs) 信任它们的信任仓库中任何 CA签发给任何 subscriber 的证书。结果是 Web PKI 整体的安全性取决于所有 Web PKI CA 中最弱的那个。2011 DigiNotar 攻击就说明了这个问题: 作为攻击的一部分,给 google.com 签发了一个假证书,这个证书被大部分浏览器和操作系统信任,而它们不管 google 和 DigiNotar 没有任何关系这一事实。 还有类似的欺骗证书颁发给了 Yahoo!, Mozilla, The Tor Project。 最终的解决方式是将 DigiNotar 的根证书从主流信任仓库中移除,但显然在此期间已经造成了大量破坏。 最近,森海塞尔(Sennheiser)因为在它们的 HeadSetup APP 信任仓库中安装了一个自签名的根证书引起了一次重大安全事故, 他们将相应的私钥(private key)嵌入在了 app 的配置中, 任何人都能从中提取这个私钥,然后颁发证书给任何 domain, 因此,任何在自己的信任仓库中添加了 Sennheiser 证书的,都将会信任这些欺骗证书。 这完全摧毁了 TLS 带来的好处,太糟糕了! 改进措施 已经有一些机制来减少此类风险: Certificate Authority Authorization (CAA) allows you to restrict which CAs can issue certificates for your domain using a special DNS record. Certificate Transparency (CT) (RFC 6962) mandates that CAs submit every certificate they issue to an impartial observer that maintains a public certificate log to detect fraudulently issued certificates. Cryptographic proof of CT submission is included in issued certificate HTTP Public Key Pinning (HPKP or just "pinning") lets a subscriber (a website) tell an RP (a browser) to only accept certain public keys in certificates for a particular domain. 这里存在的问题是:缺少 RP 端的支持。CAB Forum now mandates CAA checks in browsers. Some browsers also have some support for CT and HPKP. 但对于其他 RPs (e.g., most TLS standard library implementations) 这些东西几乎都是没有贯彻执行的。This issue will come up repeatedly: a lot of certificate policy must be enforced by RPs, and RPs can rarely be bothered. If RPs don't check CAA records and don't require proof of CT submission this stuff doesn't do much good. Internal PKI 使用单独的信任仓库 在任何情况下,如果使用自己的 internal PKI,都应该为 internal 服务维护一个单独的信任仓库。即, 不要将你的根证书直接加到系统已有的信任仓库, 而应该配置 internal TLS 只使用你自己的根证书。 Internal PKI 细粒度控制:CAA & SPIFFE 如果想在内部实现更好的联邦(federation) —— 例如限制 internal CA 能签发哪些证书, 可以试试 CAA records 然后对 RPs 进行恰当配置。 还可以看看 SPIFFE,这是一个还在不断发展的项目,目标是对一些 internal PKI 相关的问题进行标准化。 什么是证书权威(Certificate Authority)? 前面已经讨论了很多 CA 相关的东西,但我们还没定义什么是 CA。 一个证书权威(CA)就是一个受信任的证书颁发者。 CA 通过对一个证书进行签名,对一个公钥和名字之间的绑定关系(binding)做担保。 本质上来说,一个 CA 只不过是另一个证书加上用来签其他证书的相应私钥。 显然需要一些逻辑和过程来将这些东西串联起来。CA 需要将它的证书分发到信任仓库,接受和处理证书请求,颁发证书给 subscriber。 一个暴露此类 API 给外部调用、自动化这些过程的 CA 称为在线证书权威(online CA)。 在信任仓库中那些自签名的根证书 称为根证书权威(root CA)。 Web PKI 不能自动化签发证书 CAB Forum Baseline Requirements 4.3.1 明确规定:一个 Web PKI CA 的 root private key只能通过 issue a direct command 来签发证书。 换句话说,Web PKI root CA 不能自动化证书签名(certificate signing)过程。 对于任何大的 CA operation 来说,无法在线完成都是一个问题。不可能每次签发一个证书时,都人工敲一个命令。 这样规定是出于安全考虑。 Web PKI root certificates 广泛存在于信任仓库中,很难被撤回。截获一个 root CA private key 理论上将影响几十亿的人和设备。 因此,最佳实践就是,确保 root private keys 是离线的(offline),理想情况下在一些专用硬件上,连接到某些物理空间隔离的设备上,有很好的物理安全性,有严格的使用流程。 一些 internal PKI 也遵循类似的实践,但实际上并没有这个必要。 如果能自动化 root certificate rotation (例如,通过配置管理或编排工具,更新信任仓库),你就能轻松地 rotate 一个 compromised root key。 由于人们如此沉迷于 internal PKI 的根秘钥管理,导致 internal PKI 的部署效率大大降低。你的 AWS root account credentials 至少也是机密信息,你又是如何管理它的呢? Intermediates, Chains, and Bundling 在 root CA offline 的前提下,为使证书 issuance 可扩展(例如,使自动化成为可能),root private key 只在很少情况下使用, 用来签发几个intermediate certificates。 然后 intermediate CA(也称为 subordinate CAs)用相应的 intermediate private keys 来签发 leaf certificates to subscribers。 如下图所示: Image credit: Cilium TLS inspection 下面这张图把签发关系展示地更清楚, Image credit: Cilium TLS inspection Intermediates 通常并不包含在信任仓库中,所以撤回或 roate 比较容易,因此通过 intermediate CA,就实现了 certificate issuance 的在线和自动化(online and automated)。 这种 leaf、intermediate、root 组成的证书捆绑(bundle)机制,形成了一个证书链(certificate chain)。 leaf 由 intermediate 签发, intermediate 又由 root 签发, root 自签名(signs itself)。 技术上来说,上面都是简化的例子,你可以创建更长的 chain 和更复杂的图(例如,cross-certification)。但不推荐这么做,因为复杂性很快会失控。在任何情况下,end entity certificates 都是叶子节点,这也是称为叶子证书(leaf certificate)的原因。 当配置一个 subscriber 时(例如,Apache、Nginx、Linkderd、Envoy),通常不仅需要叶子证书,还需要一个包含了 intermediates 的 certificate bundle。 有时会用 PKCS#7 和 PKCS#12,因为它们能包含一个完整的证书链(certificate chain)。 更多情况下,证书链编码成一个简单的空行隔开的 PEM 对象(sequence of line-separated PEM objects)。 Some stuff expects the certs to be ordered from leaf to root, other stuff expects root to leaf, and some stuff doesn't care.More annoying inconsistency. Google and Stack Overflow help here. Or trial and error. 下面是一个例子: $ cat server.crt -----BEGIN CERTIFICATE----- MIICFDCCAbmgAwIBAgIRANE187UXf5fn5TgXSq65CMQwCgYIKoZIzj0EAwIwHzEd ... MBsGA1UEAxMUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMTgxMjA1MTc0OTQ0WhcN HO3iTsozZsCuqA34HMaqXveiEie4AiEAhUjjb7vCGuPpTmn8HenA5hJplr+Ql8s1 d+SmYsT0jDU= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIBuzCCAWKgAwIBAgIRAKBv/7Xs6GPAK4Y8z4udSbswCgYIKoZIzj0EAwIwFzEV ... BRvPAJZb+soYP0tnObqWdplmO+krWmHqCWtK8hcCIHS/es7GBEj3bmGMus+8n4Q1 x8YmK7ASLmSCffCTct9Y -----END CERTIFICATE----- Again, annoying and baroque, but not rocket science. RP:Certificate path validation 由于 intermediate certificates 并未包含在信任仓库中,因此需要与leaf certificates 一样分发和验证。 前面已经介绍,配置 subscriber 时需要提供这些 intermediates,subscribers 随后再将它们传给 RP。 如果使用 TLS,那这个过程发生在 TLS 握手时。 当一个 subscriber 将它的证书发给 relying party 时,其中会包含所有能证明来自信任的根证书的 intermediates。 relying party 通过一个称为 certificate path validation 的过程来验证 leaf 和 intermediate certificates 。 完整的 certificate path validation算法比较复杂。包括了 checking certificate expirations revocation status various certificate policies key use restrictions a bunch of other stuff 显然,PKI RP 准确实现这个算法是非常关键的。 如果关闭 certificate path validation(例如,curl -k),用户将面临重大风险,所以不要关闭。 完成正确的 TLS 并没有那么难,certificate path validation 是TLS 中完成认证(authentication)的部分。 可能有人会说,channel 已经是加密的了,因此关闭没关系 —— 错,有关系。没有认证(authentication)的加密是毫无价值的 —— 这就像在教堂忏悔:你说的话都是私密的,但却并不知道帘幕后面的人是谁 —— 只不过这里不是教堂,而是互联网。 秘钥和证书的生命周期 在能通过 TLS 等协议使用证书之前,要先配置如何从 CA 获取一个证书。逻辑上来说这是一个相当简单的过程: 需要证书的 subscriber 自己先生成一个 key pair,然后通过请求发送给 CA, CA 检查其中关联的 name 是否正确,如果正确就签名并返回一个证书。 证书会过期,过期的证书就不会被 RP 信任了。如果证书快过期了而还想继续用它,就需要续期(renew )并轮转(rotate)它。如果想在一个证书过期之前就让 RP 停止信任它,就需要执行撤销(revoke)。 与 PKI 相关的大部分东西一样,这些看似简单的过程实际上都充满坑。其中也隐藏了计算机科学中最难的两个问题:缓存一致性和命名(naming)。但另一方面,一旦理解了背后的原理,再反过来看实际在用的一些东西就简单多了。 Naming things(命名相关) DN (distinguished names) 历史上,X.509 使用 X.500 distinguished names (DN) 来命名证书的使用者(name the subject of a certificate),即 subscriber。一个 DN 包含了一个 common name (对作者我来说,就是 “Mike Malone”),此外还可以包含locality、country、organization、organizational unit 及其他一些东西(数字电话簿相关)。 没人理解 DN,它在互联网上也没什么意义。 应该避免使用 DN。如果真的要用,也要尽量保持简单。 无需使用全部字段,实际上,也不应该使用全部字段。 common name 可能就是需要用到的全部字段了,如果你是一个 thrill seeker ,可以在用上一个 organization name。 PKIX 规定一个网站的 DNS hostname 应该关联到 DN common name。最近,CAB Forum 已经废弃了这个规定,使整个 DN 字段变成可选的(Baseline Requirements, sections 7.1.4.2)。 SAN (subject alternative name) 现代最佳实践使用subject alternative name (SAN) X.509 extension来 bind 证书中的 name。 常用的 SAN 有四种类型,绑定的都是广泛使用的名字: domain names (DNS) email addresse IP addresse URI 在我们讨论的上下文中,这些都是唯一的,而且它们能很好地映射到我们想识别的东西: email addresses for people domain names and IP addresses for machines and code, URIs if you want to get fancy 应该使用 SAN。 注意,Web PKI 允许在一个证书内 bind 多个 name,name 也也允许通配符。也就是说, 一个证书可以有多个 SNA,也可以有类似 *.smallstep.com 这样的 SAN。 这对有多个域名的的网站来说很有用。 生成 key pairs 有了 name 之后,需要先生成一个密钥对,然后才能创建证书。前面提到:PKI 的安全性在根本上取决于一个简单的事实:只有与证书中的 subscriber name 对应的 entity,才应该拥有与该证书对应的私钥。为确保这个条件成立, 最佳实践是让 subscriber 生成它自己的密钥对,这样就只有它自己知道私钥。 绝对应该避免通过网络发送私钥。 生成证书时使用什么类型的秘钥?这一主题值得单独写一篇文章,这里只提供一点快速指导(截止 2018.12)。 如今有一个缓慢但清晰的从 RSA 转向椭圆曲线秘钥的趋势(ECDSA或 EdDSA)。 如果决定使用 RSA 秘钥,确保它们至少是 2048 比特长,但也不要超过 4096 比特。 如果使用 ECDSA,那 P-256 曲线可能是最好选择(secp256k1 or prime256v1 in openssl),除非你担心 NSA,这种情况下你可以选择更 fancier 一些的东西,例如 EdDSA with Curve25519(但对这些秘钥的支持还不是太好)。 下面是用 openssl 生成一个椭圆曲线 P-256 key pair 的例子: $ openssl ecparam -name prime256v1 -genkey -out k.prv $ openssl ec -in k.prv -pubout -out k.pub #也可以用 step 生成 $ step crypto keypair --kty EC --curve P-256 k.pub k.prv 还可以通过编程来生成这些证书,这样能做到证书不落磁盘。 Issuance(确保证书中的信息都是对的) subscriber 有了一个 name 和一对 key 之后,下一步就是从 CA 获取一个 leaf certificate。 对 CA 来说,它需要认证(证明)两件事情: subscriber 证书中的公钥,确实是该 subscriber 的公钥(例如,验证该 subscriber 知道对应的私钥);这一步通常通过一个简单的技术机制实现:证书签名请求(certificate signing request, CSR)。 证书中将要绑定的 name,确实是该 subscriber 的 name。这一步要难很多。抽象来说,这个过程称为 identity proofing(身份证明)或 registration(注册). Certificate signing requests(证书签名请求,PKCS#10) Subscriber 请求一个证书时,会向 CA 会提交一个 certificate signing request (CSR)。 CSR 也是一个 ASN.1 结构,定义在 PKCS#10。 与证书类似,CSR 数据结构包括一个公钥、一个名字和一个签名。 CSR 是自签名的,用与 CRS 中公钥对应的私钥自签名。 这个签名用于证明该 subscriber 有对应的私钥,能对任何用其公钥加密的东西进行解密。 还使即使 CSR 被复制或转发,都没有能篡改其中的内容(篡改无效)。 CSR 中包括了很多证书细节配置项。但在实际中,大部分配置项都会被 CA 忽略。大部分 CA 都使用自己的固定模板,或提供一个 administrative 接口来收集这些信息。 用 step 命令创建一个密钥对和 CSR 的例子: $ step certificate create -csr test.smallstep.com test.csr test.key OpenSSL 功能也非常强大,但 用起来不够方便。 Identity proofing(身份证明过程) CA 收到一个 CSR 并验证签名之后,接下来需要确认证书中绑定的 name 是否真的是这个 subscriber 的 name。这项工作很棘手。证书的核心功能是能让 RP 对 subscriber 进行认证。因此,如果一个证书都还没有颁发,CA 如何对这个 subscriber 进行认证呢? 答案是:分情况。 Web PKI 证明身份过程 Web PKI 有三种类型的证书,它们最大的区别就是如何识别 subscriber,以及它们所用到的 identity proofing 机制。 这三种证书是: domain validation (DV,域验证) DV 证书绑定的是 DNS name,CA 在颁发时需要验证的这个 domain name 确实是由该 subscriber 控制的。 证明过程通常是通过一个简单的流程,例如 给 WHOIS 记录中该 domain name 的管理员发送一封确认邮件。 ACME protocol(最初由 Let's Encrypt 开发和使用)改进了这种方式,更加自动化:不再用邮件验证,而是由 ACME CA 提出一个 challenge,该 subscriber 通过完成这个问题来证明它拥有这个域名。challenge 部分属于 ACME 规范的扩展部门,常见的包括: 在指定的 URL 上提供一个随机数(HTTP challenge) 在 DNS TXT 记录中放置一个随机数(DNS challenge) organization validation (OV,组织验证) OV 和下面将介绍的 EV 证书构建在 DV 证书之上,它们包括了 name 和域名所属组织的位置信息(location)。 OV 和 EV 证书不仅仅将证书关联到域名,还关联到控制这个域名的法律实体(legal entity)。 OV 证书的验证过程,不同的 CA 并不统一。为解决这个问题,CAB Forum 引入了 EV 证书。 extended validation (EV,扩展验证) EV 证书包含的基本信息与 OV 是一样的,但强制要求严格验证(identity proofing)。 EV 过程需要几天或几个星期,其中可能包括公网记录搜索(public records searches)和公司人员(用笔)签署的(纸质)证词。 这些完成之后,当相应网站时,某些浏览器会在 URL 栏中显示该组织的名称。例如: 但除了这个场景之外,EV certificates 并未得到广泛使用,Web PKI RP 也未强依赖它。 本质上来说,每个 Web PKI RP 只需要 DV 级别的 assurance 就行了,也就是确保域名是被该 subscriber 控制的。重要的是能理解一个 DV 证书在设计上的意思和在实际上做了什么: 在设计上,希望通过它证明:请求这个证书的 entity 拥有对应的域名; 在实际上,真正完成的操作是:在某个时间,请求这个证书的 entity 能读一封邮件,或配置一条 DNS 记录,或能通过 HTTP serve 一个指定随机数等等。 但话说回来,DNS、电子邮件和 BGP 这些底层基础设施本身的安全性也并没有做到足够好,针对这些基础设施的攻击还是时有发生,目的之一就是获取证书。 Internal PKI 证明身份过程 上面是 Web PKI 的身份证明过程,再来看 internal PKI 的身份证明过程。 实际上,用户可以使用任何方式来做 internal PKI 的 identity proofing,并且效果可能比 Web PKI 依赖 DNS 或邮件方式的效果更好。 乍听起来好像很难,但其实不难,因为可以利用已有的受信基础设施:用来搭建基础设施的工具,也能用来为这些基础设施之上的服务创建和证明安全身份。 如果用户已经信任 Chef/Puppet/Ansible/Kubernetes,允许它们将代码放到服务器上,那也应该信任它们能完成 identity attestations 如果在 AWS 上,可以用 instance identity documents 如果在 GCP:GCP Azure provisioning infrastructure 必须理解 identity 的概念,这样才能将正确的代码放到正确的位置。此外,用户必须信任这套机制。基于这些知识和信任,才能配置 RP 信任仓库、将 subscribers 纳入你的 internal PKI 管理范围。而完成这些功能全部所需做的就是:设计和实现某种方式,能让provisioning infrastructure 在每个服务启动时,能将它们的 identity 告诉你的 CA。顺便说一句,这正是step certificates 解决的事情。 Expiration(过期) 证书通常都会过期。虽然这不是强制规定,但一般都这么做。设置一个过期时间非常重要, 证书都是分散在各处的:通常 RP 在验证一个证书时,并没有某个中心式权威能感知到(这个操作)。 如果没有过期时间,证书将永久有效。 安全领域的一条经验就是:时间过的越久,凭证被泄露的概率就越接近 100%。 因此,设置过期时间非常重要。具体来说,X.509 证书中包含一个有效时间范围: issued at not before not after:过了这个时间,证书就过期了。 这个机制看起来设计良好,但实际上也是有一些不足的: 首先,没有什么能阻止 RP 错误地(或因为糟糕的设计)接受一个过期证书; 其次,证书是分散的。验证证书是否过期是每个 RP 的责任,而有时它们会出乱子。例如,RP 依赖的系统时钟不对时。最坏的情况就是系统时钟被重置为了 unix epoch(1970.1.1),此时它无法信任任何证书。 在 subscriber 侧,证书过期后,私钥要处理得当: 如果一个密钥对之前是用来签名/认证的(例如,基于 TLS), 应该在不需要这个密钥对之后,立即删除私钥。 保留已经失效的签名秘钥(signing key)会导致不必要的风险:对谁都已经没有用处,反而会被拿去仿冒签名。 如果密钥对是用来加密的,情况就不同了。 只要还有数据是用这个加密过的,就需要留着这个私钥。 这就是为什么很多人会说,不要用同一组秘钥来同时做签名和加密(signing and encryption)。因为当一个用于签名的私钥过期时,无法实现秘钥生命周期的最佳管理:最终不得不保留着这个私钥,因为解密还要用它。 Renewal(续期) 证书快过期时,如果还想继续使用,就需要续期。 Web PKI 证书续期 Web PKI 实际上并没有标准的续期过期: 没有一个标准方式来延长证书的合法时间, 一般是直接用一个新证书替换过期的。 因此续期过程和 issuance 过程是一样的:生成并提交一个 CSR,然后完成 identity proofing。 Internal PKI 证书续期 对于 internal PKI 我们能做的更好。 最简单的方式是: 用 mTLS 之类的协议对老证书续期。 CA 能对 subscriber 提供的客户端证书进行认证(authenticate),重签一个更长的时间,然后返回这个证书。 这使得续期过程很容易自动化,而且强制 subscriber 定期与中心权威保持沟通。 基于这种机制能轻松构建一个证书的监控和撤销基础设施。 小结 证书的续期过程其实并不是太难,最难的是记得续期这件事。 几乎每个管理过公网证书的人,都经历过证书过期导致的生产事故,例如这个。我的建议是: 发现问题之后,一定要全面排查,解决能发现的所有此类问题。 另外,使用生命周期比较短的证书。这会反过来逼迫你们优化和自动化整个流程。 Let's Encrypt 使自动化非常容易,它签发 90 天有效期的证书,因此对 Web PKI 来说非常合适。对于 internal PKI,建议有效期签的更短:24 小时或更短。有一些实现上的挑战 ——hitless certificate rotation可能比较棘手 —— 但这些工作是值得的。 用 step 检查证书过期时间: step certificate inspect cert.pem --format json | jq .validity.end step certificate inspect https://smallstep.com --format json | jq .validity.end 将这种命令行封装到监控采集脚本,就可以实现某种程度的监控和自动化。 Revocation(撤销) 如果一个私钥泄露了,或者一个证书已经不再用了,就需要撤销它。即希望: 明确地将其标记为非法的, 所有 RP 都不再信任这个证书了,即使它还未过期。 但实际上,撤销证书过程也是一团糟。 主动撤销的困难 与过期类似,执行撤回的职责在 RP。 与过期不同的是,撤销状态无法编码在证书中。RP 只能依靠某些带外过程(out-of-band process)来判断证书的撤销状态。 除非显式配置,否则大部分 Web PKI TLS RP 并不关注撤销状态。换句话说,默认情况下,大部分 TLS 实现都乐于接受已经撤销的证书。 Internal PKI:被动撤销机制 Internal PKI 的趋势是接受这个现实,然后试图通过被动撤销(passive revocation)机制来弥补,具体来说就是签发生命周期很短的证书,这样就使撤销过程变得不再那么重要了。想撤销一个证书时,直接不给它续期就行了,过一段时间就会自动过期。 可以看到,这个机制有效的前提就是使用生命周期很短的证书。具体有多短? 取决于你的威胁模型(安全专家说了算)。 24 小时是很常见的,但也有短到 5 分钟的。 如果生命周期太短,显然也会给可扩展性和可用性带来挑战:每次续期都需要与 online CA 交互,因此 CA 有性能压力。 如果缩短了证书的生命周期,记得确保你的时钟是同步的,否则就有罪受了。 对于 web 和其他的被动撤销不适合的场景,如果认真思考之后发现真的需要撤销功能,那有两个选择: CRL(,证书撤销列表,RFC 5280) OCSP(Online Certificate Signing Protocol,在线证书签名协议,RFC 2560) 主动检查机制:CRL(Certificate Revocation Lists) CRL 定义在 RFC 5280 中,这是一个相当庞杂的 RFC,还定义了很多其他东西。简单来是,CRL 是一个有符号整数序列,用来识别已撤销的证书。 这个维护在一个 CRL distribution point 服务中,每个证书中都包含指向这个服务的 URL。工作流程:每个 RP 下载这个列表并缓存到本地,在对证书进行验证时,从本地缓存查询撤销状态。但这里也有一些明显的问题: CRL 可能很大, distribution point 也可能失效。 RP 的 CRL 缓存同步经常是天级的,因此如果一个证书撤销了,可能要几天之后才能同步到这个状态。 此外,RP fail open 也很常见 —— CRL distribution point 挂了之后,就接受这个证书。这显然是一个安全问题:只要对 CRL distribution point 发起 DDoS 攻击,就能让 RP 接受一个已经撤销的证书。 因此,即使已经在用 CRL,也应该考虑使用短时证书来保持 CRL size 比较小。CRL 只需要包含已撤销但还未过期的证书的 serial numbers,因此证书生命周期越短,CRL 越短。 主动检查机制:OCSP(Online Certificate Signing Protocol) 主动检查机制除了 CRL 之外,另一个选择是 OCSP,它允许 RP 实时查询一个 OCSP responder:指定证书的 serial number 来获取这个证书的撤销状态。 与 CRL distribution point 类似,OCSP responder URL 也包含在证书中。这样看,OCSP 似乎更加友好,但实际上它也有自己的问题。对于 Web PKI,它引入了验证的隐私问题: 每次查询 OCSP responder,使得它能看到我正在访问哪个网站。 此外,它还增加了每个 TLS 连接的开销:需要一个额外请求来检查证实的撤销状态。 与 CRL 一样,很多 RPs (including browsers) 会在 OCSP responder 失效时直接认为证书有效(未撤销)。 主动检查机制:OCSP stapling(合订,绑定) OCSP stapling 是 OCSP 的一个变种,目的是解决以上提到的那些问题。 相比于让 RP 每次都去查询 OCSP responder,OCSP stapling 中让证书的 subscriber 来做这件事情。OCSP response 是一个经过签名的、时间较短的证词(signed attestation),证明这个证书未被撤销。 attestation 包含在 subscriber 和 RP 的 TLS handshake ("stapled to" the certificate) 中。这给 RP 提供了相对比较及时的撤销状态,而不用每次都去查询 OCSP responder。subscriber 可以在 signed OCSP response 过期之前多次使用它。这减少了 OCSP 的负担,也解决了 OCSP 的隐私问题。 但是,所有这些东西其实最终都像是一个 鲁布·戈德堡装置(Rube Goldberg Device) , 鲁布·戈德堡机械(Rube Goldberg machine)是一种被设计得过度复杂的机械组合,以迂回曲折的方法去完成一些其实是非常简单的工作,例如倒一杯茶,或打一只蛋等等。设计者必须计算精确,令机械的每个部件都能够准确发挥功用,因为任何一个环节出错,都极有可能令原定的任务不能达成。解释来自 知乎。 如果让 subscribers 去 CA 获取一些生命周期很短的证词(signed attestation)来证明对应的证书并没有过期,为什么不直接干掉中间环节,直接使用生命周期很短的证书呢? 使用证书 虽然理解 PKI 需要以上长篇大论,但在实际中用证书其实是非常简单的。 下面以 TLS 为例,其他方式也是类似的: 配置 PKI relying party 使用哪个根证书;对于 Web PKI,通常已经默认配置了正确的根证书,这一步可以跳过。 配置 PKI subscriber 使用哪个证书和私钥(或如何生成自己的密钥对、如何提交 CSR);某个 entity (code, device, server, etc) 既是 RP 又是 subscriber 是很常见的。这样的 entities 需要同时配置根证书、证书和私钥。 下面是个完整例子,展示 certificate issuance, root certificate distribution, and TLS client (RP) and server (subscriber) configuration: 希望这展示了使用 internal PKI 和 TLS 是如何简单直接。 有了这样的基础,就无需使用自签名的证书或做一些危险的事情,例如禁用 certificate path validation(curl -k)。 几乎每个 TLS client/server 都支持这些参数;但是,它们又几乎都不关注秘钥和证书的声明周期:都假设证书会出现在磁盘上的恰当位置,有人或服务会帮它们完成 rotate等工作。这项生命周期相关的工作才是难点。 结束语 公钥加密系统使计算机能在网络上看到对方("see" across networks)。 如果我有公钥,就能“看到”你有对应的私钥,但我自己是无法使用这个私钥的。 如果还没有对方的公钥,就需要证书来帮忙。证书将公钥和私钥拥有者的名字(name)相关联,它们就像是计算机和代码的驾照。 证书权威(CA)用它们的私钥对证书进行签名,对这些绑定关系作出担保,它们就像是车管局(DMV), 如果你出示一张车管局颁发的驾照,脸长得也和驾照上的照片一样,那别人就可以认为你就是驾照上这个人(名字)。同理,如果你是唯一知道某个秘钥的 entity,你给我的证书也是从我信任的某个 CA 来的,那我就认为证书中的 name 就是你。 现实中, 大部分证书都是 X.509 v3 证书,用 ASN.1 格式定义,通常序列化为 PEM-encoded DER。 相应的私钥通常表示为 PKCS#8 objects,也序列化为 PEM-encoded DER。 如果你用 Java 或微软的产品,可能会遇到 PKCS#7 and PKCS#12 封装格式。 加密领域有沉重的历史包袱,使当前的这些东西学起来、用起来非常让人沮丧,这比一项技术因为太难而不想学更加令人沮丧。 PKI 是使用公钥基础设施时涉及到的所有东西的统称:names, key types,certificates, CAs, cron jobs, libraries 等。 Web PKI 是浏览器默认使用的 PKI。Web PKI CA 是受信但不可靠的(trusted but not trustworthy)。 Internal PKI 是用户自己构建和维护的 PKI。需要它是因为 Web PKI 并不是针对 internal 使用场景设计的,Internal PKI 更易于自动化和扩展,并且能让用户控制很多细节,例如 naming and certificate lifetime。 建议公网上使用 Web PKI,内网使用自己的 internal PKI(例如,use TLS 来替代 VPN)。 Smallstep Certificate Manager 使构建 internal PKI 非常简单。 要获得一个证书,需要命令和生成证书。建议 name 用 SAN: DNS SANs for code and machines EMAIL SANs for people 如果这些都不能用,就用 URI SAN 秘钥类型(key type)是很大一个主题,但几乎不重要:你可以随便修改秘钥类型,而且实际上加密本身(crypto)并不是 PKI 中最弱的一环。 要从 CA 获取一个证书,需要提交一个 CSR 并证明申请者的身份(identity)。使用生命周期较短的证书和 passive revocation。自动化证书续期过程。不要禁用 certificate path validation。 最后还是那句话:证书和 PKI 将名字关联到公钥(bind names to public keys)。其他都是细节。 延伸阅读(译注) 更多相关内容或实践,推荐: Illustrated X.509 Certificate,2020 超详细图解 X.509 证书。 Cilium TLS inspection,2021 图解 X.509 证书、信任链,及 Cilium/hubble L7 实战。 What Is an X.509 Certificate? What to Know About PKI Certificates 本文转载自 http://arthurchiao.art/blog/everything-about-pki-zh/ 。 2021-10-12 2 0
TLS handshake Fatal Alert Close Notify的错误排查 前置知识 浏览器同源安全策略 WebSocket\TCP通讯协议 HTTPS TLS 握手机制 CA证书信任链 strace、dtruss系统调用动态跟踪 keychain安全机制 macOS错误日志体系 背景 前段时间接了一个项目----MOA,一款安装在macOS、Win办公电脑软件,是公司内部安全管理客户端,用来保护员工PC安全的软件。 我之前没有接触过macOS的开发,对objective-C(以下简称OC)语言也不熟悉,我这边的几个研发兄弟也一样,都是做Linux C、C++出身,对于这个陌生的领域,他们倒是不拒绝,很乐意去尝试。 在MOA新的版本中,需要具备与SSO单点登陆系统联动,控制内网SSO的准入功能,研发开发完成后,进行了灰度部署。 在灰度时,遇到一个BUG,浏览器的websocket无法与本地wss服务通讯,遇到tls handshake的错误,导致很多人无法登陆SSO,无法正常办公。 研发兄弟们也都是忙于熟悉OC语言,熟悉macOS的运行机制,忙于做业务功能开发,没时间排查这问题,以至于这BUG导致项目延期两周,影响上线进度。产品经理也在周会上表达了不满,开始对研发的技术能力产生怀疑。。。又到了拯救地球的时刻了,我决定来查一下这BUG。 运行机制介绍 关于这个SSO的通讯机制,工作过程是这样的: 假设:SSO的地址是https://sso.mt.com,MOA监听的地址是localhost.moa.mt.com MOA进程创建HTTP SERVER,监听loopback地址的某端口; 在用户打开浏览器登陆SSO时,SSO的域名会返回一段JS给浏览器; 浏览器执行后,会通过Websocket的方式连接loopback的这个端口,进行通讯; 那么问题来了,这种场景下,除了Websocket还有其他方式吗? 浏览器的同源策略对WS、WSS协议有影响吗? 对浏览器安全比较了解的同学很容易就给出了答案,走Websocket是最合适的方案,同源策略并不对Websocket做限制。 但是,浏览器的当前页面的协议是HTTPS时,Websocket也必须是TLS协议才行,也就是说对应WSS协议。 在macOS上开启Websocket,并且还要支持TLS通讯加密,需要在macOS上做证书私钥的部署,我们RD同学考虑到私钥泄露的风险,决定走自签CA证书的方式,在MOA部署时,将CA证书写入到macOS的Keychain根证书链中。 只不过这一步需要弹出KeyChain Access钥匙串访问的授权密码窗口,让用户授权。 现象 在SSO阻断功能的版本灰度后,收到其他部门的反馈,有好多同事的电脑显示SSO无法与MOA通讯,重启电脑后就好了。一共灰度一百多个人,出问题的就有四五个,而且不知道如何重现这个BUG。这就很让人头疼,我为了重现,尝试可很多办法,安装、卸载、再安装、再卸载,循环往复各种尝试重现,这个BUG终于重现了,但我也不知道具体的重现办法。 从技术上描述这个BUG是这样的,macOS上浏览器的https://sso.mt.com/xxx的Websocket无法连接到本地监听的localhost.moa.mt.com地址,报告网络错误。 对于这种错误,稍微对网络通讯debug的同学,都知道用tcpdump或者wireshark来抓包分析,抓到的数据包截图如下: 从这张通讯图上,你能发现问题出在哪了吗? TLS握手到哪个阶段了?谁先关闭的socket?关闭之前都做了什么通讯? Frame 7: 63 bytes on wire (504 bits), 63 bytes captured (504 bits) on interface 0 Null/Loopback Internet Protocol Version 4, Src: localhost (127.0.0.1), Dst: localhost (127.0.0.1) Transmission Control Protocol, Src Port: sun-sea-port (16161), Dst Port: 63655 (63655), Seq: 1, Ack: 518, Len: 7 Transport Layer Security TLSv1.2 Record Layer: Alert (Level: Fatal, Description: Close Notify) Content Type: Alert (21) Version: TLS 1.2 (0x0303) Length: 2 Alert Message Level: Fatal (2) Description: Close Notify (0) 机智如你,是Server给Client发了Level: Fatal, Description: Close Notify错误后,直接发了FIN包,进行关闭TCP SOCKET。 TLS Fatal Alert Close Notify 什么是TLS handshakeFatal Close Notify错误?第一次见到这个错吧?别担心,我也是。。。为什么在TLS 握手时,Client发了Client Hello后,Server没回Server Hello?而是回了这个错误? 在Openssl的文档SSL_shutdown - shut down a TLS/SSL connection 说明中,找到了这个错误的描述 SSL_shutdown() shuts down an active TLS/SSL connection. It sends the close_notify shutdown alert to the peer. SSL_shutdown() tries to send the close_notify shutdown alert to the peer. Whether the operation succeeds or not, the SSL_SENT_SHUTDOWN flag is set and a currently open session is considered closed and good and will be kept in the session cache for further reuse. Note that SSL_shutdown() must not be called if a previous fatal error has occurred on a connection i.e. if SSL_get_error() has returned SSL_ERROR_SYSCALL or SSL_ERROR_SSL. 从openssl官方文档中的SSL_shutdown函数描述里可以找到close_notify的介绍,当调用SSL_shutdown()函数后,将发送close_notify到对应的peer中。不管是client还是server,都可以发送这个错误。 在SSL_alert_type_string函数的介绍如下: A warning alert is sent, when a non-fatal error condition occurs. The "close notify" alert is sent as a warning alert. Other examples for non-fatal errors are certificate errors ("certificate expired", "unsupported certificate"), for which a warning alert may be sent. (The sending party may however decide to send a fatal error.) The receiving side may cancel the connection on reception of a warning alert on it discretion. Several alert messages must be sent as fatal alert messages as specified by the TLS RFC. A fatal alert always leads to a connection abort. 发送方可以决定是否发送Fatal级别的错误,在我们抓包的数据来看,是Moa Client(也就是说Server端)发送了Fatal级别的错误,原因是可能因为certificate expired、unsupported certificate"等等。但具体的错误,还是需要继续排查确认。 排查 那么,既然知道了TLS handshake的Fatal Close Nofity错误是服务端发起的,那么为什么呢?服务端的进程发生了什么问题?这如何排查?进程不能重启,重启后BUG现象就丢失了。 引用IBM developerworks上的一句话: 有时候,需要了解在应用程序内部正在发生的情况。例如,应用程序可能会运行失败,而又没有显示有帮助的错误消息,或者系统服务没有按照预期的方式运行。在这些情况下,您可能不掌握应用程序源代码,因此无法通过传统的调试过程寻找问题的原因。跟踪提供了一种替代方法。 在进行跟踪时,监视应用程序和操作系统之间的交互,常常会检查应用程序在执行期间调用的操作系统函数。 通过跟踪应用程序,可以查明以下方面的情况: 内存使用量和映射内存的调用 在执行期间打开和关闭的文件 对不同文件的读和写操作 为给定的应用程序装载的库 对于遇到的这个问题,我们要想了解到TLS Close Notify之前,都做了些什么操作,正因为我们程序不能重启,不知道如何重现,所以动态跟踪系统调用的做法最符合我们需求。 Linux strace 在Linux上,我们常用strace来对进程进行系统调用的跟踪探测。比如下面的例子 root@cnxct:~#strace -ff -tt -s 1000 -o 111.log -p 800 00:08:40.417260 futex(0x1ddbf08, FUTEX_WAIT_PRIVATE, 0, NULL) = 0 00:08:43.380903 futex(0x1ddbf20, FUTEX_WAIT_PRIVATE, 2, NULL) = 0 00:08:43.381001 setsockopt(232, SOL_TCP, TCP_NODELAY, [1], 4) = -1 EOPNOTSUPP (Operation not supported) 00:08:43.381053 gettid() = 9757 00:08:43.381077 setsockopt(232, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0 00:08:43.381102 openat(AT_FDCWD, "/dev/urandom", O_RDONLY) = 252 00:08:43.381134 read(252, "K8\232Nl\26\rU\354\312\221\260\20563+v=\367\372\371\317W\f>.\304\221\342B$\375", 32) = 32 00:08:43.381162 close(252) = 0 00:08:43.381603 recvfrom(232, "\22\0\0\0", 4, MSG_DONTWAIT, NULL, NULL) = 4 00:08:43.381629 recvfrom(232, "\3SET NAMES utf8mb4", 18, MSG_DONTWAIT, NULL, NULL) = 18 能看到,strace的结果中,会有当前系统的CPU时间、syscall函数、参数、返回值等,有了系统时间,则会很方便的跟其他调试信息关联。 macOS dtruss 在macOS上,也有想对应的工具,dtruss,之前调试时,用过几次,命令参数、输出结果跟strace差别太大,总是不习惯。 现在遇到问题了,逃不开,只好开始学习这个工具的使用。 参数上 cfc4n@cnxct-mt:~$ dtruss USAGE: dtruss [-acdefholLs] [-t syscall] { -p PID | -n name | command | -W name } -p PID # examine this PID -n name # examine this process name -t syscall # examine this syscall only -W name # wait for a process matching this name -a # print all details -c # print syscall counts -d # print relative times (us) -e # print elapsed times (us) -f # follow children -l # force printing pid/lwpid -o # print on cpu times -s # print stack backtraces -L # don't print pid/lwpid -b bufsize # dynamic variable buf size eg, dtruss df -h # run and examine "df -h" dtruss -p 1871 # examine PID 1871 dtruss -n tar # examine all processes called "tar" dtruss -f test.sh # run test.sh and follow children 从参数上来看, -p PID、-n name都是选择跟踪的目标的 -c、-d、-o等参数是统计对应系统调用执行时占用的CPU时间,在性能分析的场景下很有用。 -s参数是打印系统调用对应的函数调用栈,这个参数对本次bug的排查会很有帮助。 工具参数熟悉后,整理思路开始跟踪吧。 BUG对应的系统调用范围确定 在我们去触发BUG时,程序本身也还在运行,在执行其他业务逻辑。那我们如何确定bug产生的过程对应的系统调用记录范围? 系统调用执行时的时间 前面提到,在Linux平台上,strace的日志会带有系统当前时间,其他的检测软件中也会有记录系统时间,这样关联就比较方便。但macOS平台上,dtruss是没有这个系统当前时间输出的,这样就没法便捷的关联BUG时间段,没法确定syscall日志的范围,很头疼。 比如tcpdump或wireshark捕获的网络通讯包中,也会有系统时间,哪怕没法跟dtruss日志关联,但也还是要抓包捕获,来确认BUG确实产生了。开tcpdump,选择loopback网卡(因为是127.0.0.1这个回环网址,所以是loopback网卡),设定参数port 16161,开启嗅探,同时开始重现BUG。 系统syscall的API入手关联 在wireshark的分析中,确认BUG重现后,着手分析dtruss日志。 从我们研发的经验上考虑,浏览器向MOA发起wss TCP连接时,wss server会去调用accept系统调用来处理,那么我们只要确定在大约的时间段里,找到accept的syscall,就能定位到这个bug产生最开始的入口点。 cfc4n@cnxct-mt:~$ dtruss -p 78158 dtrace: system integrity protection is on, some features will not be available dtrace: failed to initialize dtrace: DTrace requires additional privileges cfc4n@cnxct-mt:~$ please dtruss -p 78158 Password: dtrace: system integrity protection is on, some features will not be available SYSCALL(args) = return workq_kernreturn(0x40, 0x700005CCEB80, 0x1) = 0 Err#-2 kevent_qos(0xFFFFFFFFFFFFFFFF, 0x7FFEE0E63920, 0x1) = 0 0 workq_kernreturn(0x40, 0x700005CCEB80, 0x1) = 0 Err#-2 workq_kernreturn(0x40, 0x700005CCEB80, 0x1) = 0 Err#-2 accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0 fcntl(0x19, 0x4, 0x4) = 0 0 setsockopt(0x19, 0xFFFF, 0x1022) = 0 0 kevent_id(0x6000036A8500, 0x700005CCEB80, 0x0) = 1 0 thread_selfid(0x0, 0x0, 0x0) = 21736566 0 lstat64("/Applications/Firefox.app/Contents/Resources\0", 0x700005D50F70, 0x0) = -1 Err#2 open("/Applications/MOA.app/Contents/Resources/MOA.app/Contents/Resources/server.p12\0", 0x0, 0x1B6) = 26 0 fstat64(0x1A, 0x700005D512B0, 0x0) = 0 0 dtrace: error on enabled probe ID 2174 (ID 168: syscall::read:return): invalid kernel access in action #12 at DIF offset 68 close(0x1A) = 0 0 open("/Users/cfc4n/Library/Keychains/login.keychain-db\0", 0x0, 0x0) = 27 0 __mac_syscall(0x7FFF7833ACD0, 0x52, 0x700005D4F9E0) = 0 0 lstat64("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-gMs51v\0", 0x700005D4FC48, 0x0) = 0 0 fstatfs64(0x1B, 0x700005D4FC48, 0x0) = 0 0 open_dprotected_np(0x600003BFA080, 0xB01, 0xFFFFFFFFFFFFFFFF) = -1 Err#17 open_dprotected_np(0x600003BFA080, 0x901, 0xFFFFFFFFFFFFFFFF) = 28 0 fcntl(0x1B, 0x30, 0x1) = 0 0 fcntl(0x1C, 0x30, 0x1) = 0 0 fcntl(0x1C, 0x4C, 0x1) = 0 0 __mac_syscall(0x7FFF7833ACD0, 0x53, 0x700005D4EFE8) = 0 0 fstat64_extended(0x1C, 0x700005D4F1A8, 0x7FD4A3D4CE10) = 0 0 fchmod_extended(0x1C, 0x1F5, 0x14) = 0 0 fstat64(0x1C, 0x700005D4EF50, 0x0) = 0 0 fchflags(0x1C, 0x0, 0x0) = 0 0 fchown(0x1C, 0x1F5, 0x14) = 0 0 fchmod(0x1C, 0x1A4, 0x0) = 0 0 fsetattrlist(0x1C, 0x700005D4F000, 0x700005D4EFE0) = 0 0 fstat64(0x1B, 0x700005D4FBA0, 0x0) = 0 0 fstat64_extended(0x1C, 0x700005D4FC48, 0x7FD4A3D4CE10) = 0 0 close(0x1B) = 0 0 close(0x1C) = 0 0 utimes("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-gMs51v\0", 0x0, 0x0) = 0 0 rename("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-gMs51v\0", "/Users/cfc4n/Library/Keychains/login.keychain-db\0") = 0 0 flock(0x1A, 0x8, 0x0) = 0 0 close(0x1A) = 0 0 open("/Applications/MOA.app/Contents/Resources/MOA.app/Contents/Info.plist\0", 0x0, 0x1B6) = 26 0 fsync(0x1B, 0x0, 0x0) = 0 0 close(0x1B) = 0 0 lstat64("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-rmdB2z\0", 0x700005D4F988, 0x0) = 0 0 stat64_extended(0x600003BF5D60, 0x700005D4F988, 0x7FD4A3F210C0) = 0 0 chmod_extended(0x600003BF5D60, 0x1F5, 0x14) = 0 0 lstat64_extended(0x600000299F00, 0x7FD4A3F20FC8, 0x7FD4A3F210C0) = 0 0 open("/Users/cfc4n/Library/Keychains/login.keychain-db\0", 0x0, 0x0) = 27 0 workq_kernreturn(0x100, 0x700005D51B80, 0x2) = 0 Err#-2 getentropy(0x700005D51780, 0x20, 0x0) = 0 0 getentropy(0x700005D517D0, 0x40, 0x0) = 0 0 kevent_id(0x6000036BE600, 0x700005C4AD80, 0x1) = 0 0 kevent_id(0x6000036BE600, 0x700005C4AD50, 0x1) = 0 0 dtrace: error on enabled probe ID 2172 (ID 170: syscall::write:return): invalid kernel access in action #12 at DIF offset 68 kevent_id(0x6000036B1F80, 0x700005C4B6F0, 0x1) = 0 0 workq_kernreturn(0x100, 0x700005D51B80, 0x2) = 0 Err#-2 kevent_id(0x6000036A8500, 0x700005C4BB80, 0x1) = 0 0 close(0x19) = 0 0 workq_kernreturn(0x100, 0x700005C4BB80, 0x1) = 0 Err#-2 bsdthread_ctl(0x100, 0x0, 0x2DE13) = 0 0 workq_kernreturn(0x20, 0x0, 0x1) = 0 0 workq_kernreturn(0x100, 0x700005D51B80, 0x1) = 0 Err#-2 (数据有删减)从上面的dtruss日志来看,accept的syscall很显眼accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0,那么,如何来理解这段日志呢? macOS的syscall系统调用也是跟unix的一致,参考BSD System Calls 手册 ACCEPT(2) BSD System Calls Manual NAME accept -- accept a connection on a socket SYNOPSIS #include <sys/socket.h> int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len); RETURN VALUES The call returns -1 on error and the global variable errno is set to indicate the error. If it succeeds, it returns a non-negative integer that is a descriptor for the accepted socket. 从这个syscall的说明来看,参数部分并不重要,重要的是RETURN VALUES的部分,这是TCP SOCKET连接后的所属FD文件描述符,也就是我们BUG重现时,这个网络连接在这个MOA进程中的FD。 BUG出现的对应的accept 找到BUG重现后的FD,那么再找到FD被关闭的地方即可。从dtruss的日志accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0来看,这个FD是25,聪明的你立刻就想到,只要找到close的地方就能确定到BUG出现的syscall日志范围。 BUG出现的对应的close 直接搜索close的系统调用吧,很明显在close(0x19) = 0 0就是,只不过十进制的 25 被显示成十六进制的0x19了。 确定BUG出现的范围 对于这个BUG来说,已经确定了其产生对应的syscall日志范围,那么剩下的事情只要顺着close的地方倒推往上来找。 跟踪 从上面的dtruss结果来看,并没有找到准确的TCP SOCKET write的系统调用,只看到几处dtrace: error on enabled probe ID 2172 (ID 170: syscall::write:return): invalid kernel access in action #12 at DIF offset 68 的错误,字面上理解,无法启用相关进程的syscall:write的探测。那么我们将无法定位到到底是哪里的syscall向TCP SOCKET的FD里写入了Fatal Close Notify的错误。 回到dtruss的参数中,可以看到还有一个参数-s用来打印程序运行的stack backtraces。进行重新重现BUG: cfc4n@cnxct-mt:~$ please dtruss -p 78158 -adfs Password: dtrace: system integrity protection is on, some features will not be available PID/THRD RELATIVE ELAPSD CPU SYSCALL(args) = return 78158/0x1527e26: 11190 88 2 bsdthread_ctl(0x100, 0x0, 0x1335B) = 0 0 libsystem_kernel.dylib`__bsdthread_ctl+0xa libdispatch.dylib`_dispatch_set_priority_and_mach_voucher_slow+0xa0 libdispatch.dylib`_dispatch_set_priority_and_voucher_slow+0xc9 libdispatch.dylib`_dispatch_mach_msg_invoke+0x282 libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_mach_invoke+0x1e1 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 0x10ef9e000 78158/0x1527e26: 11276 106 23 workq_kernreturn(0x20, 0x0, 0x1) = 0 0 libsystem_kernel.dylib`__workq_kernreturn+0xa libdispatch.dylib`_dispatch_root_queue_poke_slow+0xac LaunchServices`LSNotificationReceiver::receiveNotificationFromServer(_xpc_connection_s*, void*)+0x3a3 LaunchServices`___ZN22LSNotificationReceiver23getNotificationListenerEv_block_invoke.10+0x184 libxpc.dylib`_xpc_connection_call_event_handler+0x38 libxpc.dylib`_xpc_connection_mach_event+0x3a5 libdispatch.dylib`_dispatch_client_callout4+0x9 libdispatch.dylib`_dispatch_mach_msg_invoke+0x1b4 libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_mach_invoke+0x1e1 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 0x10ef9e000 ... //很多,有删减 78158/0x152e9e7: 294 101 21 accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0 libsystem_kernel.dylib`__accept+0xa itsec-agent`0x000000010edc0023+0x44 libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_continuation_pop+0x19e libdispatch.dylib`_dispatch_source_invoke+0x808 libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 78158/0x152ecde: 1209 37 31 open("/Users/cfc4n/Library/Keychains//.fl34AC2A0A\0", 0x200, 0x124) = 26 0 libsystem_kernel.dylib`__open+0xa Security`Security::DbModifier::modifyDatabase()+0x5d Security`Security::AppleDatabase::dataInsert(Security::DbContext&, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*)+0x54 Security`Security::DatabaseSession::DataInsert(long, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*, cssm_db_unique_record*&)+0xbb Security`cssm_DataInsert(cssm_dl_db_handle, unsigned int, cssm_db_record_attribute_data const*, cssm_data const*, cssm_db_unique_record**)+0x68 Security`CSSM_DL_DataInsert+0x88 Security`SSDatabaseImpl::ssInsert(unsigned int, cssm_db_record_attribute_data const*, cssm_data const*)+0xc5 Security`SSCSPDLSession::makeReferenceKey(SSCSPSession&, unsigned int, Security::CssmKey&, SSDatabase&, unsigned int, Security::CssmData const*)+0xb09 Security`SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x17a Security`non-virtual thunk to SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x21 Security`cssm_UnwrapKey(long, unsigned long long, cssm_context const*, cssm_key const*, cssm_key const*, unsigned int, unsigned int, cssm_data const*, cssm_resource_control_context const*, cssm_key*, cssm_data*, unsigned long long)+0xd0 Security`CSSM_UnwrapKey+0xc7 Security`P12Coder::safeContentsParse(cssm_data const&, SecNssCoder&)+0xd2c Security`impExpPkcs12Import+0x8a0 Security`SecKeychainItemImport+0xd30 Security`SecPKCS12Import+0x100 itsec-agent`0x000000010edd8efe+0x10b itsec-agent`0x000000010ed9f5b8+0x36 itsec-agent`0x000000010edce367+0x39 itsec-agent`0x000000010edce297+0x38 78158/0x152ecde: 15213 33 30 lstat64("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-3plX5x\0", 0x700005C49A78, 0x0) = 0 0 libsystem_kernel.dylib`lstat$INODE64+0xa Security`Security::DbModifier::commit()+0xda3 Security`Security::AppleDatabase::dataInsert(Security::DbContext&, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*)+0xb1 Security`Security::DatabaseSession::DataInsert(long, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*, cssm_db_unique_record*&)+0xbb Security`cssm_DataInsert(cssm_dl_db_handle, unsigned int, cssm_db_record_attribute_data const*, cssm_data const*, cssm_db_unique_record**)+0x68 Security`CSSM_DL_DataInsert+0x88 Security`SSDatabaseImpl::ssInsert(unsigned int, cssm_db_record_attribute_data const*, cssm_data const*)+0xc5 Security`SSCSPDLSession::makeReferenceKey(SSCSPSession&, unsigned int, Security::CssmKey&, SSDatabase&, unsigned int, Security::CssmData const*)+0xb09 Security`SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x17a Security`non-virtual thunk to SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x21 Security`cssm_UnwrapKey(long, unsigned long long, cssm_context const*, cssm_key const*, cssm_key const*, unsigned int, unsigned int, cssm_data const*, cssm_resource_control_context const*, cssm_key*, cssm_data*, unsigned long long)+0xd0 Security`CSSM_UnwrapKey+0xc7 Security`P12Coder::safeContentsParse(cssm_data const&, SecNssCoder&)+0xd2c Security`impExpPkcs12Import+0x8a0 Security`SecKeychainItemImport+0xd30 Security`SecPKCS12Import+0x100 itsec-agent`0x000000010edd8efe+0x10b itsec-agent`0x000000010ed9f5b8+0x36 itsec-agent`0x000000010edce367+0x39 itsec-agent`0x000000010edce297+0x38 ... //有删减 78158/0x152e9e7: 1150 71 4 kevent_id(0x6000036BE600, 0x700005CCDD50, 0x1) = 0 0 libsystem_kernel.dylib`kevent_id+0xa libdispatch.dylib`_dispatch_event_loop_end_ownership+0x2b6 libdispatch.dylib`_dispatch_sync_complete_recurse+0x2c libdispatch.dylib`_dispatch_sync_invoke_and_complete_recurse+0x5b libdispatch.dylib`_dispatch_sync_f_slow+0xc3 libcorecrypto.dylib`ccrng_cryptographic_generate+0x132 libcorecrypto.dylib`ccn_random_bits+0x2b libcorecrypto.dylib`ccec_projectify+0x59 libcorecrypto.dylib`ccec_make_pub_from_priv+0xce libcorecrypto.dylib`ccec_generate_key_internal_fips+0xa0 libcorecrypto.dylib`ccec_sign_internal+0x1c9 libcorecrypto.dylib`ccec_sign+0x57 libcorecrypto.dylib`ccec_pairwise_consistency_check+0x84 libcorecrypto.dylib`ccec_generate_key_fips+0x21 libcoretls.dylib`sslEcdhCreateKey+0x40 libcoretls.dylib`SSLEncodeServerKeyExchange+0x253 libcoretls.dylib`SSLPrepareAndQueueMessage+0x27 libcoretls.dylib`SSLAdvanceHandshake+0xb37 libcoretls.dylib`SSLProcessHandshakeRecordInner+0xea libcoretls.dylib`tls_handshake_process+0x55 dtrace: error on enabled probe ID 2172 (ID 170: syscall::write:return): invalid kernel access in action #13 at DIF offset 68 78158/0x152e9e7: 2915 11 5 kevent_id(0x6000036B1F80, 0x700005CCE6F0, 0x1) = 0 0 libsystem_kernel.dylib`kevent_id+0xa libdispatch.dylib`_dispatch_event_loop_poke+0x205 itsec-agent`0x000000010edc2126+0x2e4 itsec-agent`0x000000010edc9109+0x185 itsec-agent`0x000000010edc8655+0x9d8 itsec-agent`0x000000010edc827e+0x96 libdispatch.dylib`_dispatch_call_block_and_release+0xc libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_lane_serial_drain+0x25a libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 78158/0x152e9e7: 3016 17 13 close(0x19) = 0 0 libsystem_kernel.dylib`close+0xa libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_continuation_pop+0x19e libdispatch.dylib`_dispatch_source_invoke+0x48b libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 找到close(0x19)的地方,往上倒推几个syscall日志,可以定位到真正往TCP SOCKET 写入Fatal Close Notify的地方是在libcoretls.dylib\SSLEncodeServerKeyExchange+0x253`。 定位到服务端的密钥交换过程中,EncodeServer时出现了异常。 回到dtruss的其他日志,能看到在调用Security动态链接库中的impExpPkcs12Import方法里,有大量的/Users/cfc4n/Library/Keychains/login.keychain-db的读写。意味着MOA进程在频繁大量的读写keychain。 除了Keychain的频繁读写,还看到了大量MOA主进程的访问 78158/0x152ecde: 18541 85 31 open("/Applications/MOA.app/Contents/Resources/MOA.app/Contents/MacOS/itsec-agent\0", 0x0, 0x1B6) = 26 0 libsystem_kernel.dylib`__open+0xa Security`Security::OSXVerifier::makeLegacyHash(Security::OSXCode*, unsigned char*)+0x101 Security`Security::OSXVerifier::OSXVerifier(Security::OSXCode*)+0x1b1 Security`Security::KeychainCore::TrustedApplication::TrustedApplication()+0x70 Security`Security::KeychainCore::Access::Access(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)+0x70 Security`P12Coder::safeContentsParse(cssm_data const&, SecNssCoder&)+0xe02 Security`impExpPkcs12Import+0x8a0 Security`SecKeychainItemImport+0xd30 Security`SecPKCS12Import+0x100 itsec-agent`0x000000010edd8efe+0x10b itsec-agent`0x000000010ed9f5b8+0x36 itsec-agent`0x000000010edce367+0x39 itsec-agent`0x000000010edce297+0x38 libdispatch.dylib`_dispatch_call_block_and_release+0xc libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_lane_serial_drain+0x25a libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd CodeSigning是macOS的对二进制程序进行数字签名的机制,以确保进程文件没有被篡改,安全保障机制详情见:About Code Signing ,可以确认的是,MOA进程在每次accept TCP连接时,都进行了主进程的签名校验。难道程序签名校验失败了?因为频繁的重新安装进程,而这个老进程又没退出,但文件内容却被覆盖了? 可以这么怀疑,但还是需要证据证明。 但问题定位到这里,好像卡死了,我不确定Security链接库是否还会写入其他日志,也没有时间去看这个链接库的源码。 根据之前做黑盒分析的经验,决定到系统日志里找找看,macOS的系统日志的进程是console.app。 关联系统日志 在控制台中,搜索MOA的进程名itsec-agent,找BUG重现时的时间点,能看到这些报错信息,而根据dtruss中捕获的Keychains的临时文件名,也能关联得到这些日志。 默认 12:05:49.086566 +0800 itsec-agent caught CssmError: -2147416017 CSSMERR_CSP_ACL_ENTRY_TAG_NOT_FOUND 默认 12:05:49.095362 +0800 itsec-agent CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED 默认 12:05:53.326473 +0800 itsec-agent 0x6000008ffb80 commited /Users/cfc4n/Library/Keychains/login.keychain-db.sb-dbe9e6c6-5v8ECF to /Users/cfc4n/Library/Keychains/login.keychain-db 默认 12:05:53.331646 +0800 itsec-agent CSSM Exception: -2147413719 CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA 默认 12:06:01.089562 +0800 itsec-agent 0x6040008fb980 commited /Users/cfc4n/Library/Keychains/login.keychain-db.sb-dbe9e6c6-yiHh6n to /Users/cfc4n/Library/Keychains/login.keychain-db 默认 12:06:01.096920 +0800 itsec-agent CSSM Exception: -2147413719 CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA 默认 12:06:01.108208 +0800 itsec-agent 0x6040008fc080 commited /Users/cfc4n/Library/Keychains/login.keychain-db.sb-dbe9e6c6-yrTtXC to /Users/cfc4n/Library/Keychains/login.keychain-db 默认 12:06:01.113267 +0800 itsec-agent CSSM Exception: -2147413719 CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA 默认 12:06:01.146973 +0800 itsec-agent caught CssmError: -2147416017 CSSMERR_CSP_ACL_ENTRY_TAG_NOT_FOUND 默认 12:06:01.156853 +0800 itsec-agent CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED 默认 12:06:01.160022 +0800 itsec-agent CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED 这些报错信息,我也是第一次见,在网上稍微搜索相关知识了解,能看到这是macOS的Openssl的类库源码中的报错信息,比如CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA、CSSMERR_CSP_OPERATION_AUTH_DENIED等。 在apple的开发者社区中,看到一个相似的案例: inexplicable CSSMERR_CSP_OPERATION_AUTH_DENIED,里面提到这个错误有很多情况产生 apple的CodeSign keychain证书链中的证书itemid丢失 等等,结合前面dtruss的排查结论,问题方向定位到self-sign自签CA证书上。 定位 其实,这里我的MacBook死机电脑重启了,BUG无法重现,但问题的大体方向几乎定位到了。在前面的结论下,以及搜索到的这些案例,我决定使用下面两种方式吃重现 删除Keychain的自签CA,模拟itemid丢失; 覆盖MOA主进程,让MacOS的CodeSigning机制检测失败; 对于方式一,测试结果如下 MOA继续运行 删除了KeyChain里的自签CA证书 重新访问这个TCP SOCKET 问题并没有复现,但出现了一个相近的错误Description: Certificate Unknown,之后也是服务端发送FIN包后关闭连接。 虽然问题没有复现,但我觉得真相已经很近了,配合CodeSigning的检测问题,需要模拟主进程文件被替换的场景,我又想到一种重现方式 清空删除所有MOA安装包 打开wireshark,抓loopback网卡的port 16161端口 安装老版MOA,按照业务流程走一遍,确保所有功能正常 安装新版MOA,chrome开隐私模式(确保不用上次的COOKIE),登陆SSO MOA弹出申请keychain权限的窗口,点“拒绝” 问题重现可以从wireshark的抓包窗口中确认,看到了Tls handshake时的Fatal Close Notify错误 嗯,问题重现,BUG根本原因找到了。但Keychain的安全机制我还不太清楚,时间关系,暂时放过吧,以后有机会再研究。 解题思路 回顾整个排查过程,我并不清楚OC代码是如何写的,都是黑盒分析,完全是看系统调用syscall来知道这个软件运行的行为,定位BUG问题所在的。 有很多知识是跨平台通用的,比如网络通讯原理、动态跟踪技术、动态调试技术、syscall系统调用、二进制文件执行机制等技术,跟所在的是Windows、Linux还是macOS关系不大。 系统调用syscall跟踪软件在Win上有 Process Monitor ,我在2016年黑盒分析英雄联盟启动器时使用过,之前也没做过Windows的程序研发,只是借助Linux的解题思路找对应得替代软件而已。比如下图: 同样,这次遇到macOS的BUG也是,只要找对应得软件就好,解决问题的思路是通用的。 Linux上有strace,macOS上有dtruss,对于更详细的动态跟踪软件,还有Dtrace、SystemTap等我也在学习中,以后如果有机会,我会继续给大家分享。 通用知识 对于这种BUG的解决,重要的是解题思路,遇到问题该如何处理,如何粗略定位问题所在,从哪里入手?靠猜吗?东一锤子西一榔头的靠运气吗? 就怕你整天TCP协议栈倒背如流,操作系统知识滚瓜烂熟,垃圾回收机制如数家珍,但每次一遇到问题,就一脸懵逼、束手无策,这就很不匹配。 大部分主动学习能力并不强,都是靠事件驱动,比如有BUG产生了,才不得不去学习陌生领域的知识,去解决问题,顺便积累经验。可怕的是有部分人BUG发生就发生了,重启能解决就算,从来不关心根本原因是什么。下次遇到同类型的问题时,依旧是束手无策。 套用道友codedump的一句话知行合一(出自王阳明),珍惜每一次BUG的机会,历练自己。在解决每个问题时,是对你整个知识体系的梳理,对逻辑思维的考验,对技术技能的检查,检验自己、磨练自己、强化自己。 解决方案 WSS的https server监听时,使用的tls证书,要走正规CA签发,避免自签self-sign的CA频繁读写keychain ,避免对keychain的root权限获取。(域名解析到127.0.0.1,拿到私钥的安全风险也很小) MOA升级时,先杀死老版本的所有进程,再进行安装、覆盖、启动。 itsec-agent进程 codesign签名。 本文重点 美团点评信息安全中心招聘,职位包括不限于 iOS/macOS技术专家 Web应用防火墙架构师 JAVA\golang\C++高级工程师 数据安全资深专家 数仓安全架构师 基础安全专家 风险数据挖掘专家 IoT安全专家 移动安全工程师、专家 操作系统安全工程师、专家 更多职位信息,见:同学,我看你骨骼精奇,是干安全/研发的好苗子:https://mp.weixin.qq.com/s/9Y9bAUt51gPvy2ItiFRBCA 。 关注本BLOG微信公众号榫卯江湖,回复JD、JOBS、招聘、工作获取更多职位信息。 相似的案例 inexplicable CSSMERR_CSP_OPERATION_AUTH_DENIED : Certificate-private key combo failure: https://forums.developer.apple.com/thread/99801 Internal error (errSSLInternal) on SSLHandshake:https://forums.developer.apple.com/thread/92193 参考资料 macOS Code Signing In Depth 跟踪 UNIX 应用程序的解决方案 openssl类库的SSL_alert_type_string函数手册 macOS平台Security类库源码的CSSMERRCSP*相关错误 https://docs.apigee.com/api-platform/troubleshoot/runtime/ssl-handshake-failures: Re: inexplicable CSSMERR_CSP_OPERATION_AUTH_DENIED: 扩展知识 http://oliveryang.net/2016/07/linux-perf-tools-tips/ System Integrity Protection – Adding another layer to Apple’s security model SystemTap Beginners Guide 2019-09-09 0 0
你不在意的HTTPS证书吊销机制 缘起 偶刷《长安十二时辰》,午睡时,梦到我穿越到了唐朝,在长安城中的靖安司,做了一天的靖安司司丞。 当徐宾遇害消失的时候我不在司内,当时的情形我不得而知。后来徐宾醒了,据他描述说“通传陆三”是暗桩,险些致徐宾于死地。而擅长大案牍术的高智商人才居然被一个普通通传的几句话骗至险境,实在丢了我的脸。 陆三是通传,熟知靖安司的号令传递系统望楼信号,他是暗桩的消息,传遍整个机构。这让张小敬和姚汝能认为望楼系统已无法完成消息保密传送的功能,其实他们根本不了解这望楼。 整个望楼系统由“传递系统+加密系统”组成,靖安司作为一个军事级别的机构,信息传递绝对是多重加密的。只看懂望楼图案,或者只有密码本都是破译不了密码的,对于通传陆三是暗桩的影响,也只需要更换密码本即可。这些可是我学了RSA非对称加密后设计的望楼系统,早就评估过这些风险了。 即使HTTPS通讯中,丢了密钥也... 嗯?如果HTTPS证书私钥丢了,会怎样?是不是也没法防范这个私钥被利用了? 想到这个问题,我突然从梦中惊醒,去温故一下证书吊销机制。 疑问 HTTPS的证书过期是谁来判断? 证书的合法性又是谁检查的呢? 什么时候触发? 影响性能吗? 如何吊销证书? HTTPS的请求是客户端(浏览器)发起的,他是如何知道证书被吊销的? 验证HTTPS证书的过程是什么样的? HTTPS 通讯过程 大家都清楚,HTTPS的握手是在TCP握手完成后,流程都熟的很,但还是要温故一下: 客户端发送Client Hello,附带支持的加密套件算法、版本、以及随机数Random1. 服务端选择合适的加密套件,返回Server Hello,附带一个随机数Random2. 服务端将自己的证书Certificate发送给客户端。 客户端将自己的证书Certificate发送给服务端(证书登录认证的情况)。 客户端校验证书Certificate Verify。 生成Random3,再用三个Random1,Random2,Random3计算PreMaster Key,并用公钥加密。 Client Cipher Spec,客户端通知服务端,编码改变后的消息,都通过对称加密来进行。 Encrypted HandShake Message 是一个finish消息,表示客户端握手完成,将使用协商好的密钥进行加密,发送给服务端。服务端收到后,对消息进行解密,如果能解密成功,则表明双方是同一个密钥。 对于证书检验这块,相信很多人都不太了解,甚至都不知道会检验什么内容,那么下面我们就来了解一下。 证书完整性验证 使用RAS公钥加密来验证证书上的签名是否合法,如果签名无效,则可认定证书被修改,直接报错。 证书有效性验证 CA在颁发证书时,都为每个证书设定了有效期。包括开始时间与结束时间。系统当前时间不在证书起止时间的话,都认为证书是无效的。 证书吊销状态检测 如果,证书在有效期之内需要丢了怎么办?需要吊销证书了,那么这里就多了一个证书吊销状态的检测。用户将需要吊销的证书通知到CA服务商,CA服务商通知浏览器该证书的撤销状态。 来看一个证书吊销后的浏览器提醒 Chrome返回了NET::ERR_CERT_REVOKED,并且拒绝继续访问,更不提供强制访问的接口,没了继续访问的手动点击链接。 验证发行者 HTTPS数字证书的使用分两个角色 证书发行方issuer,有签名密钥的私钥 证书申请方subject,使用证书公钥进行身份验证的用户 浏览器检查证书的发行者字段与证书路径中上级证书的Suject字段相同。 为了增加安全性,大多数PKI实现还验证发型方的密钥、签名跟当前证书的密钥相同。 但对于信任链来说,根证书自己签发的,也就是说它们的issuer和subject是一样的。 同时,这些CA根证书都是被操作系统、浏览器等直接打入系统的。比如 检查域名(IP)规范 中间CA提供了对域名证书的管理以及颁发颁发的颗粒度度控制。证书的生效范围会限于固定域名、域名范围(包括子域)或者固定IP。 比如下图是https://www.baidu.com的HTTPS证书DNS信息 上图所示,DNS范围包含了多个域名,同时二级以及二级以上域名都支持范围形式。以*通配义字符表示。但*.example.com的二级域名范围就不能包含a.b.example.com这个三级域名。同时,DNS范围也支持IP的,只是IP不支持范围形式,必须把所有IP列表都放入列表中。(PS:百度的证书管理可真乱。) 检查策略约束 法律策略相关检测(略)。 证书的吊销状态检测方式 上面提到了浏览器(客户端)在验证证书合法性时的验证范围,我们暂时只关注证书吊销信息的检测,下面我们仔细来了解一下两种检测机制的实现原理。 Certificate Revocation Lists (CRL) CA会定期更新发布撤销证书列表,Certificate Revocation Lists (以下简称CRL),RFC 5280: Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile。 CRL分布在公共可用的存储库中,浏览器可以在验证证书时获取并查阅CA的最新CRL。 该方法的一个缺陷是撤销的时间粒度限于CRL发布期。只有在计划更新所有当前发布的CRL之后,才会通知浏览器撤销。 各家签名CA厂商的策略不一样,有的是几小时,有的是几天,甚至几周。 2015年,美国几所大学的学生论文中,统计了当时的CA证书吊销情况,如下图: 这个统计可以看出,CA证书厂商的CRL数量不一,大部分是30-50个,而GoDaddy有300多个CRL的地址,同时有近30W个证书是吊销状态的,文件大小平均达到了1M。 证书的CRL信息 CRL信息是CA在颁发证书时,写入在X.509 v的扩展区域的,比如alipay.com的证书信息: 可以看到,其CRL信息为http://crl3.digicert.com/SecureSiteCAG2.crl 以及http://crl4.digicert.com/SecureSiteCAG2.crl CRL 检测流程 可以想象一下,在浏览器去校验证书合法性时,还要去下载一个1M的文件后,再去校验。校验通过后才去连想要访问的网站服务器,这相当浪费时间、效率。 同时,很多浏览器所处网络是有网络访问限制的,可能在一个局域网,比如我们村,或者物理距离非常远,存在严重的网络延迟问题。 Online Certificate Status Protocol (OCSP) 为了解决单个文件大,延迟性高等问题,迎来了新的解决方案Onlie Certificate StatusOCSP(以下简称OCSP)。 在RFC2560X.509 Internet Public Key Infrastructure Online Certificate Status Protocol - OCSP的描述中,浏览器从在线OCSP服务器(也称为OCSP Response Server)请求证书的撤销状态,OCSP Server予以响应。这种方法避免CRL更新延迟问题。同样的,X.509 v3证书的OCSP信息也是存储在拓展信息中,如alipay.com证书那张图的绿色框内部分。 OCSP 检测流程 浏览器在获得Web服务器的公钥证书后,开始验证公钥的合法性,这里会向该公钥的扩展信息中提供的OCSP Server地址发送OCSP Request,获得响应后,确认证书有效,再继续跟Web服务器通信。 OCSP的优点 相对于CRL方式,证书吊销后,CA Server可以立刻将吊销信息发送给浏览器,生效时间快。响应包内容短,不像CRL的响应包,都将近1M以上。 OCSP的缺点 浏览器的每次HTTPS请求创建,都需要连接CA OCSP Server进行验证,有的浏览器所在IP与CA OCSP Server的网络并不是通畅的,比如我们村。而且,OCSP的验证有网络IO,花费了很长的时间,严重影响了浏览器访问服务器的用户体验。 在浏览器发送服务器HTTPS证书序号到CA OCSP Server时,也将暴露了用户的隐私,将用户访问的网址透漏给了CA OCSP Server。 OCSP机制衍生出来的问题 设想一个场景,你是浏览器企业,研发的浏览器在检查证书吊销状态时,得不到OCSP server的响应,你会如何选择? 如果你选择拒绝该证书信息,并且拒绝后续的HTTPS通讯,那么这个方式称之为Hard-fail 如果你选择信任该证书,认为没有被吊销,那么这个方式称之为Soft-fail 如果是hard-fail模式,那浏览器对任何HTTPS服务器访问的先决条件都取决于OCSP Server,这将是一个致命的单点故障,对于具有资深架构设计经验的你来说,你会这么选择吗? 如果是soft-fail模式,取不到OCSP Server的响应就忽略了,那么,要这个机制还有啥意义呢?要你有何用? OCSP Stapling OCSP Stapling的方案是解决了CRL、OCSP的缺点,将通过OCSP Server获取证书吊销状况的过程交给Web 服务器来做,Web 服务器不光可以直接查询OCSP信息,规避网络访问限制、OCSP服务器离用户的物理距离较远等问题,还可以将查询响应缓存起来,给其他浏览器使用。由于OCSP的响应也是具备CA RSA私钥签名的,所以不用担心伪造问题。 解决了访问慢的问题 解决了用户隐私泄露的问题 通讯过程如上,但对于Web服务器在去OCSP Server查询证书状态时,也同样面临无法获取到OCSP Response的问题,在响应给浏览器时,浏览器也还是要面临hard-fail、soft-fail的选择问题,这很让浏览器头大。 OCSP Must-Staple 面对hard-fail、soft-fail的问题,各家浏览器厂商的态度都不一样。同时,不管浏览器如何选择,都不能满足广大域名用户的需求,那么不如把这个选择交给域名用户自己。 为此,OCSP Must-Staple应然而生了,浏览器必须检测OCSP响应。域名证书创建时,自定义设定启用这个选项,将这个信息打入X.509 v3的扩展中,浏览器读取后,强制进行OCSP检测,走hard-fail模式。这个规范被起草在 X.509v3 Extension: OCSP Stapling Required draft-hallambaker-muststaple-00 ,不过,暂未被采纳为RFC标准。 CA厂商支持 目前支持该扩展的证书的CA厂商有Let's Encrypt。 如果使用的是openssl 1.1.0 以前的版本,可以使用11.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05 来指定。RFC 比如生成csr的时候,在openssl.cnf中增加: [v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names 1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05 如果是使用openssl 1.1.0或更高的版本,可以这样设置: [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names tlsfeature = status_request 各平台上浏览器对证书吊销的支持情况 Mac Safari 在Mac OS X 10.7 (Lion)以后,Safari/macOS默认情况下,不检测CRLs、OCSP,走自己的key chain体系。(资料比较少,apple官方也搜不到几条) Windows Internet Explorer Windows Vista系统开始,Internet Explorer 7浏览器内置了CryptoAPI,来支持OCSP方式检测证书吊销情况。检测范围包括issuer发行商的证书、subject服务器的证书。 为什么IE访问HTTPS的网站时,会比别的浏览器慢?你应该已经知道答案了。 Mozilla Firefox 在2010年时,Mozilla Firefox的所有版本都支持OCSP方式检测。在Firefox 3里把OCSP检测机制设定为默认机制。 在以后的版本更新中,Firefox针对中级证书跟域名证书做了不同的策略 中级证书的吊销状态验证 在2015年,Firefox 37开始,针对中级证书的检测,Mozilla也启用了自研的证书吊销状况检查机制OneCRL来代替OCSP机制,目的还是想解决CRL、OCSP机制的缺点。 而中级证书是不能采用OCSP stapling方式,不允许被缓存的。所以... 还有, RFC 6961 defines a mechanism for stapling OCSP responses for CA certificates. Since FIrefox does not rely on OCSP to validate intermediate certificates, we have no plans to implement support for this. Firefox 官方短期内并无支持Multi-staple的打算。 域名证书的吊销状态验证 在Firefox的官方WIKI上,提到针对域名证书的吊销验证分为如下5个方案: Short-Lived Certificates ,默认情况下,针对有效期少于10天的证书,直接跳过验证,认为不安全。可以在security.pki.cert_short_lifetime_in_days参数里配置。 OCSP Stapling,跟RFC规范一样。如果security.OCSP.enabled的值为0,则忽略OCSP response。 OCSP Must-staple,跟RFC规范一样。可以通过设置security.ssl.enable_ocsp_must_staple或security.ssl.enable_ocsp_stapling 参数来禁用。 OCSP,跟RFC规范一样。如果OCSP的响应在2秒(EV证书是10秒)内没返回,则直接忽略。 CRLite chrome 2012年,Chrome禁用了CRLs、OCSP检测: Google Chrome Will No Longer Check for Revoked SSL Certificates Online ,使用了自行设计的证书校验机制 CRLSets 那么,Chrome这么选择的理由是什么呢? 显然,通过上面CRL跟OCSP两种验证方式的比较来看,前者时效性不行,后者影响性能。那么,google Chrome就决定自建证书吊销状态验证系统。 Chrome的安全工程师Adam Langley在他的BLOG上写了一篇文章:《Revocation checking and Chrome's CRL》, 对于Chrome的HTTPS证书验证这块,Adam Langley可是非常有看法的,非常反对使用CRL、OCSP的方式来校验证书吊销状态,连续写了好几篇关于证书吊销状态检测的文章,同时,也在chromium的开发者主页上的安全板块有提到:What's the story with certificate revocation? Chrome's primary mechanism for checking the revocation status of HTTPS certificates is CRLsets. Chrome also supports Online Certificate Status Protocol (OCSP). However, the effectiveness of OCSP is is essentially 0 unless the client fails hard (refuses to connect) if it cannot get a live, valid OCSP response. No browser has OCSP set to hard-fail by default, for good reasons explained by Adam Langley (see [Revocation still doesn't work](No, don't enable revocation checking) and https://www.imperialviolet.org/2014/04/19/revchecking.html). Stapled OCSP with the Must Staple option (hard-fail if a valid OCSP response is not stapled to the certificate) is a much better solution to the revocation problem than non-stapled OCSP. CAs and browsers are working toward that solution (see the Internet-Draft: http://tools.ietf.org/html/draft-hallambaker-tlssecuritypolicy-03). Additionally, non-stapled OCSP poses a privacy problem: in order to check the status of a certificate, the client must query an OCSP responder for the status of the certificate, thus exposing a user's HTTPS browsing history to the responder (a third party). That said, you can use enterprise policies to enable soft-fail OCSP (http://www.chromium.org/administrators/policy-list-3#EnableOnlineRevocationChecks) and hard-fail OCSP for local trust anchors (http://www.chromium.org/administrators/policy-list-3#RequireOnlineRevocationChecksForLocalAnchors). Chrome performs online checking for Extended Validation certificates if it does not already have a non-expired CRLSet entry covering the domain. If Chrome does not get a response, it simply downgrades the security indicator to Domain Validated. See also this bug for more discussion of the user-facing UX: https://crbug.com/361820. 但这也不是完美解决了这个问题,来自An Evaluation of the Effectiveness of Chrome's CRLSets This means that Chrome's CRLSet, which currently lists the serial numbers of 24,206 revoked certificates, reflects only 1.08% of the revoked certificates collected by Websense in one hour. 这篇文章中提到CRLSet的最大问题是包含的证书吊销数据太少,个别情况下占了主流CRL证书吊销信息的2%不到。而且,CRLSets的更新也不是那么及时,chrome为了用户体验,为了性能,为了用户隐私,牺牲了一点点安全性,也是可以理解的。但好像对不起最安全浏览器的称号了。[手动狗头]😂 汇总 如上图,在2015年,Northeastern University、University of Maryland、Duke University等几所大学的研究人员分析了市场上流行的五个浏览器(多个平台、多个版本),把各个浏览器在不同级别证书下的支持情况绘制了表格。(论文在参考文献中给出) 从图中可以看出,我们印象中最安全的浏览器Chrome表现却让你大跌眼镜,在HTTPS的安全性这块,表现最差。上面我们也谈到,chrome为了访问速度隐私等用户体验考虑,忽略了CRL、OCSP等证书吊销状态检测机制,牺牲了TLS这块的安全性支持。 而IE浏览器却出乎我们意料,在HTTPS的安全性这块,支持的非常好,可以说是这几个浏览器中最安全的了,可是...可是他网页打开速度慢啊...😂 截至至今天(2019年7月16日),已经4年过去了,很多浏览器、WebServer的支持情况也有很大的变化,并不能反映出当下互联网的现状,这些数据也作为参考吧。 附:WebServer的支持情况 The Apache web server has supported OCSP stapling since v2.3.3 (ref). The nginx web server has supported OCSP stapling since v1.3.7 (ref). 总结 证书泄露的危害 那么,证书丢了,会对网站安全产生很大影响吗? 回头看下使用该证书的条件 具备证书 具备域名 浏览器访问该服务器 如果几个都具备,那么你就是该网站的官方了。 在安全界,有个攻击手段,叫Man-in-the-middle attack中间人攻击,如果证书被黑客拿到,搭建一个具备相同域名的网站,通过DNS污染的方式使得用户浏览器方位该域名,那么可以成为一个反向代理,把用户的请求解析后,伪造程客户端来跟真实的Web服务器通讯,从而获取双方通信的明文信息,达到攻击的目的。 那这种情况怎么防范?中间人攻击的防御方式是开启客户端对证书的认证,但你的证书私钥又丢了,那能咋办? 通过本文前面章节的了解到,这种情况,也只能去 主动到CA厂商那申请证书吊销了,不管有几个浏览器支持,也得申请,毕竟,这损失能减少一点是一点。 证书泄露了怎么办? 证书泄露了怎么办?从浏览器的支持情况来看,好像及时申请吊销了证书,也不能对丢失的证书起到太大的防范作用,很多浏览器并不是很支持的嘛。 不过,多少也能避免一些损失,毕竟IE浏览器对这块的支持力度还是很大的嘛。 注 本文的参考文献,大部分都是5-6年前的资料,这么多年过去了,互联网技术产品日新月异,里面很多结论早已不符合现状,尤其是浏览器当今对证书吊销状态检测的支持情况。部分内容,仅作为参考,便于读者去了解技术变迁的背景知识。 本文会持续修正文中错别字、排版、错误数据等,建议回到原文阅读。 立个flag 过段时间,把整个X.509 v3的知识点都整理一下,做个内部分享? PEM、CRT、PFX、PKCS#7、PKCS#12、pfx、DER、JKS、KDB、CER、KEY、CSR、CRT、CRL都是什么? ASN.1 又是什么? 立起?🐶🐶🐶 招聘 macOS技术专家 Java高级工程师 详情见https://www.cnxct.com/jobs/ 参考文献 RFC3280 Internet X.509 Public Key Infrastructure Certificate High-reliability OCSP stapling and why it matters Security Certificate Revocation AwarenessThe case for “OCSP Must-Staple” Security Certificate Revocation AwarenessSpecific Implementations Security Sidebar: My Browser Has No Idea Your Certificate Was Just Revoked SSL certificate revocation and how it is broken in practice Revoking Intermediate Certificates: Introducing OneCRL Revocation doesn't work (18 Mar 2011) Revocation checking and Chrome's CRL (05 Feb 2012) No, don't enable revocation checking (19 Apr 2014) Revocation still doesn't work (29 Apr 2014) An End-to-End Measurement of Certificate Revocation in the Web’s PKI 结束 嗯?话说《长安十二时辰》中望楼消息传送机制的加固呢? 嗨,梦都醒了,谁还记得这事啊。 2019-07-16 +1 5 0
golang游戏项目中使用 tls、https 与etcd v3服务通讯 在全球同服的游戏架构中,网络结构一般设计成集群的形式,用户随便在哪个服务器节点上都可以玩,也就要求每个节点的环境都是一致的,节点之间是可以自由下线上线的,用户是可以在每个节点自由注册的...等等,各种需求的出现,会需要一款能主动发现节点上线线,能统计全局所有服务器负载、状态,能实时下发配置等信息到所有节点的这么一款软件,那么就用到 ETCD 了。 etcd 作为 高可用分布式配置同步的软件,一般只对内网服务,偶尔也会暴露到外网。不管是内网,还是外网,都需要安全认证这一步,而其默认是有以及用户名、密码的认证方式,只是需要对每一个索引做权限设定。etcd 官方也后 TSL 的认证,同时也只是 HTTPS 形式传输数据,以确保哪怕在内网传输,也保证数据安全性。 ETCD server的配置的下载地址:https://github.com/coreos/etcd/releases/download/v3.2.7/etcd-v3.2.7-linux-amd64.tar.gz ,解压后,可看到其版本信息 如果不需要证书,不需要鉴权就可以访问,则可以直接使用了。。。 对于需要确保加密传输,需要权限认证的服务,那么还是需要证书来保证的,关于证书生成,,借助工具 cfssl 为了确保游戏集群管理的 ETCD 的高可用,一般都会有多个 ETCD 服务同时运行组成一个 ETCD 的集群。 ETCD 才调用 RAFT 的算法进行主节点选举,日志同步等等。ETCD 节点之间的通讯,也需要证书的认证。为了便于以后的etcd节点变更,一般都会以域名形式来提供服务,故需要解析几个 etcd 的域名,我这里以etcd1.cnxct.com、etcd2.cnxct.com,同时解析到对应服务器上。我这里的例子是解析到本机。 创建证书临时存放的目录(为了看起来好区别) 查看一下生成的文件列表: 在这里的列表中,分为client.pem公钥,client-key.pem私钥。以及 ca.pem 公钥,以及几个 CSR证书请求文件;ca-key.pem 为 自制 ca 私钥文件,需要妥善保管,不能外泄。 证书生成好了,验证一下证书的有效性 如上,目测证书有效。将证书复制到 etcd 的配置目录 开始配置etcd的配置文件 我在从官方下载下来的二进制包中 ,并没有配置文件的例子文件,而且网上搜到的,几乎都是早起etcd 2 时代的 linux 上conf 文件常见配置格式(就是那种井号注释的那种)。在 etcd 3,都搞成了 yaml 格式了。 在网上用 google、baidu 搜了好久,压根找不到yaml的例子。后来在 github 官网找到了 yaml 的例子地址 , https://github.com/coreos/etcd/blob/master/etcd.conf.yml.sample ,这个强烈建议 etcd 项目组将这个配置的例子放到打包的二进制压缩包里。 这里稍微说一下,在3.1.9时, listen-client-urls 跟 listen-client-urls 两个参数,还是可以直接使用域名来配置的。 3.2.x 就不支持了,只能填正确的 IP 来配置。 initial-advertise-peer-urls跟advertise-client-urls两个参数可继续使用域名形式的地址。 测试一下 CA 证书在 etcd 服务器上是否正常工作 同样,测试 client 证书在 etcd 的服务上是否正常使用 如上结果,可以看出,两个证书均正常使用。 开始在 Golang 的 client 中使用证书文件: etcd 本身就是用 golang 语言编写的,项目托管在 etcd 的 github地址,客户端类库在client目录,以及 clientv3下,前一个适用与etcd v2的版本,后一个适用与etcd v3的版本,在 golang 的包里,就是github.com/coreos/etcd/clientv3。在我的项目中,我使用的是v3版本的客户端,使用证书,建立链接的伪代码如下 在游戏集群中,游戏服务器的节点在整个集群中是唯一的,每个节点都拥有一个唯一的节点 ID,而这个唯一 ID 在 ETCD 里的注册,需要用到CAS原子性的操作,在 V2的客户但里,是Set方法时,Options的PrevExist参数是 false 来实现。 在 V3里,是Txn来开启事务,实现 CAS 操作 在游戏服务器节点启动后,把自己注册到 ETCD 后,将从 ETCD 加载自己节点所属的配置信息,比如若是 gateway 节点的话,加载自己将转发到逻辑服务器的节点列表,加载客户端版本过滤信息等等。伪代码如下: 同样,在游戏集群中,经常会遇到配置的调整,在以前的方法中,都是挨个到服务器上修改配置再重新加载,或者高级一点的,自己研发 GMtools 推送消息到游戏的服务进程等等。我们在使用 etcd 时,使用 etcd 的 watch 功能,监控某个 KEY 的变动,若有变化,则立刻刷新到进程中的配置环境上,进行配置变更。当然,有些 DB 链接的 DSN,这些不会跟着改的。这种需求可以使用this.kapi.Watch(context.TODO(),keyName。 对于整个集群的配置管理,一般在 gmtools 里做统一配置管理,将配置写入到 ETCD,再有游戏服务器节点启动时加载,或者游戏服务器节点 watch 监控,遇到更新时也自动同步到其环境中。事例如下: 综上所述,对 etcd server 开启了 https 的加密传输,开启了 tls 的证书认证。可放心的在内网或者公网来传输数据,修改下发数据,确保不会恶意或无意的修改污染了。。。 附golang Test 的截图: Coreos Etcd分布式集群 讨论 QQ 群: 2017-09-15 1 0