- 编程语言
- 语言运行时
- 标准库
- 编译器
- 并发编程
【免费下载链接】otp
Erlang/OTP
导读
本文围绕 Erlang/OTP 中负责公钥基础设施(Public Key Infrastructure)的public_key应用展开,系统梳理其"库应用、纯内存二进制处理"的设计定位、完整支持的 PKIX 标准族、与crypto/asn1的依赖关系,以及唯一例外的系统 CA 证书加载接口cacerts_load/0,1与cacerts_get/0。读完本文,你将掌握 public_key 的模块架构与核心 API 分布,理解 X.509 证书、CRL、数字签名、证书路径验证与主机名校验在 OTP 中的落地方式,并能直接上手 PEM 编解码、RSA 加解密、签名验签等实战操作。
Public_Key 应用定位:库应用,不碰文件系统
public_key应用处理与公钥相关的文件格式、数字签名以及 X.509 证书(RFC 5280)相关的全部工作,包括:
- 证书路径(certificate path)验证;
- 证书吊销列表(CRL,Certificate Revocation List)验证;
- 证书、密钥、CRL 的其他处理功能。
其最核心的设计约束是:它是一个库应用(library application),自身不读写文件。它期望的输入、返回的输出,都是文件内容或部分文件内容的二进制形式(binary)。也就是说,调用方需要自行完成file:read_file/1、file:write_file/2这类文件操作,把内容以 binary 交给 public_key 处理。
唯一的例外是以下三个函数会真正读取文件:
public_key:cacerts_load/0public_key:cacerts_load/1public_key:cacerts_get/0
它们用于加载操作系统提供的受信任 CA 证书,细节见下文"唯一的文件读取例外"一节。这一设计在 public_key.erl 的模块文档中亦有呼应:所有记录的 ASN.1 结构由规范生成,并通过-include_lib("public_key/include/public_key.hrl")提供给用户使用。
支持的 PKIX 功能全景
public_key对 PKIX 相关标准的支持覆盖证书、密钥、消息语法三大类,完整清单如下(对应 public_key_app.md 的 Supported PKIX functionality 一节):
| 标准 | 内容 | 仓库中的 ASN.1 规范证据 |
|---|---|---|
| RFC 5280 | Internet X.509 公钥基础设施证书与 CRL 配置文件;证书策略(Certificate policies)自 OTP-26.2 起支持 | OTP-PKIX.asn1、OTP-PKIX-Relaxed.asn1 |
| PKCS-1(RFC 3447) | RSA 密码学标准 | PKCS-1.asn1 |
| DSS(FIPS 186-3) | 数字签名标准(DSA 数字签名算法) | DSS.asn1 |
| PKCS-3 | Diffie-Hellman 密钥协商标准 | PKCS-3.asn1 |
| CMS(RFC 5652) | 密码消息语法,含基于原始 PKCS-5 的密码加密(PBE);目前暂不官方支持第 10~12 节的大部分内容(例如属性证书 v2,若证明有用将来可能加入) | CryptographicMessageSyntax-2009.asn1、CryptographicMessageSyntaxAlgorithms-2009.asn1 |
| PKCS-8(RFC 5208) | 私钥信息语法标准 | 由 OTP-PKIX.asn1 及 PKCS-FRAME.set.asn 覆盖 |
| PKCS-10(RFC 5967) | 证书请求语法标准 | PKCS-10.asn1 |
| PKIXCMP(RFC 9810) | 证书管理协议 | PKIXCMP-2023.asn1 |
| PKIXCRMF(RFC 5912) | 证书请求消息格式 | PKIXCRMF-2009.asn1 |
这些 ASN.1 模块并不是"纸上标准"——它们全部在 public_key.app.src 的modules列表中登记为应用模块(如'PKIXCMP-2023'、'PKIXCRMF-2009'、'OCSP-2024-08'、'PKCS-1'、'DSS'等),由 asn1 编译器在构建期生成 Erlang 编解码代码,供public_key的各个pubkey_*模块调用。从该列表还可以看出,仓库正在持续跟进新标准:SLH-DSA-Module-2024、X509-ML-DSA-2025、X509-ML-KEM-2025、KEMAlgorithmInformation-2023等后量子签名与密钥封装算法模块已纳入应用清单,可以推断后续版本会逐步开放相应算法能力。
依赖关系与启动顺序
public_key的运作建立在两个底层应用之上:
- Crypto 应用:负责执行底层密码学运算(RSA/DSA/ECC 加解密、哈希、签名原语等);
- ASN.1 应用:负责处理 PKIX-ASN.1 规范,即上表中的 ASN.1 模块的编译与运行。
因此,这两个应用必须先被加载,public_key才能正常工作。在 public_key.app.src 中可以确认完整的依赖声明:
{applications, [asn1, crypto, kernel, stdlib]}, {runtime_dependencies, ["stdlib-4.0","kernel-8.0","erts-13.0", "crypto-5.8","asn1-5.0"]}在嵌入式(embedded)环境中,依赖应用不会自动启动,必须在启动public_key之前显式调用application:start/[1,2]依次启动asn1、crypto等依赖。典型顺序:
application:start(asn1), application:start(crypto), application:start(public_key).从源码结构看,public_key自身没有注册进程({registered, []})、也没有环境变量配置({env, []}),进一步印证了其纯库应用的属性——它不维护长期运行的进程,所有函数都是同步计算并直接返回结果。
源码架构:模块划分与核心 API
public_key的源码集中在 lib/public_key/src,按职责拆分为以下模块:
| 模块 | 职责 |
|---|---|
| public_key.erl | 唯一对外 API 入口(3424 行),导出全部公开函数 |
| pubkey_cert.erl | 证书解析、证书路径验证、扩展检查、verify_fun 回调机制 |
| pubkey_crl.erl | CRL 解析、吊销状态跟踪、CRL 签名验证 |
| pubkey_ocsp.erl | OCSP(在线证书状态协议)请求/响应处理 |
| pubkey_pem.erl | PEM 格式的编解码 |
| pubkey_pbe.erl | 基于密码的加密(Password-Based Encryption) |
| pubkey_os_cacerts.erl | 从操作系统/文件加载受信任 CA 证书 |
| pubkey_policy_tree.erl | 证书策略树处理(对应 OTP-26.2 引入的策略支持) |
| pubkey_ssh.erl | SSH 相关密钥格式(dh_gex_group等) |
| pubkey_translation.erl | 不同密钥表示之间的转换 |
核心 API 分组(见 public_key.erl 的导出声明):
- 格式编解码:
pem_decode/1、pem_encode/1、pem_entry_decode/1,2、pem_entry_encode/2,3、der_decode/2、der_encode/2、pkix_decode_cert/2、pkix_encode/3; - 加解密:
encrypt_private/2,3、decrypt_private/2,3、encrypt_public/2,3、decrypt_public/2,3; - 签名验签:
sign/3,4、verify/4,5; - 密钥处理:
generate_key/1、compute_key/2,3、dh_gex_group/4、dh_gex_group_sizes/0; - 证书操作:
pkix_sign/2、pkix_verify/2、pkix_is_self_signed/1、pkix_is_issuer/2、pkix_issuer_id/2、pkix_subject_id/1、pkix_normalize_name/1; - 验证类:
pkix_path_validation/3、pkix_verify_hostname/2,3、pkix_crls_validate/3、pkix_crl_verify/2、pkix_ocsp_validate/5; - 系统 CA 证书:
cacerts_get/0、cacerts_load/0,1、cacerts_clear/0。
其中证书路径验证pkix_path_validation/3在 pubkey_cert.erl 中实现,核心机制是逐证书调用verify_fun回调(verify_fun/4类型,见 pubkey_cert.erl):验证过程会依次检查证书有效期(cert_expired、invalid_validity_dates)、签发者(invalid_issuer)、名称约束(distinguished_name_not_permitted、name_not_permitted)、签名(invalid_signature)以及扩展约束,每一步失败都会以{bad_cert, Reason}元组形式交给用户提供的verify_fun决定接受还是拒绝。CRL 验证则位于 pubkey_crl.erl,导出validate/7、init_revokation_state/0、fresh_crl/3、verify_crl_signature/4等接口,用于在路径验证过程中查询吊销状态。
唯一的文件读取例外:系统信任 CA 证书
虽然public_key总体不读写文件,但系统信任 CA 证书这一组接口是例外,它们在 public_key.erl 中定义(OTP 25.0 起):
cacerts_get() -> [combined_cert()]:返回已加载的受信任 CA 证书;若尚未加载则内部调用cacerts_load/0完成加载,加载失败时直接抛出{failed_load_cacerts, Reason}错误;cacerts_load() -> ok | {error, Reason}:加载操作系统提供的受信任 CA 证书;cacerts_load(File) -> ok | {error, Reason}:从指定文件加载受信任 CA 证书;cacerts_clear() -> boolean():清空已加载的 CA 证书缓存,若此前确有加载则返回true。
底层实现集中在 pubkey_os_cacerts.erl:
- 平台分发:
load/0依据os:type()选择加载策略(见 pubkey_os_cacerts.erl)——Linux/OpenBSD/FreeBSD/DragonFly/NetBSD 读取各自的标准系统证书路径,Solaris 走sunos_paths(),macOS 走load_darwin(),Windows 通过 NIF(os_cacerts/0)读取系统证书存储;不支持的平台返回{error, {enotsup, Os}}; - 路径覆盖:
load/1接收文件路径列表并依序尝试,可用于在load/0不适用的平台上手动指定证书来源; - 缓存:加载结果缓存在
persistent_term中,get/0首次调用时若缓存为空会先触发加载(pubkey_os_cacerts.erl)。
更重要的是cacerts_path应用环境变量:当设置该变量时,get/0会优先从该路径加载,而不再走操作系统默认位置(pubkey_os_cacerts.erl)。官方文档给出的命令行设置方式为:
erl -public_key cacerts_path '"/path/to/certs.pem"'使用警告:这一覆盖机制需要使用者自行负责。必须确保替代路径中的证书对该运行系统而言是可信任的,否则等于主动引入了不可信的信任锚,存在安全风险。
错误处理模型:无错误日志器
public_key是库应用,不使用错误日志器(error logger)。所有函数要么成功返回结果,要么直接以运行时错误(runtime error)失败,例如cacerts_get/0在无法加载任何 CA 证书时抛出{failed_load_cacerts, Reason}。这意味着调用方需要通过 try/catch 等 Erlang 异常机制自行处理失败,而不是依赖日志系统发现问题。这也与库应用"不做副作用、纯函数式计算"的设计哲学保持一致。
实战:PEM 编解码、RSA 加解密、签名与主机名校验
本节的完整示例来自官方指南 using_public_key.md,其中的密钥与证书仅供测试使用。
PEM 文件结构
PEM(Privacy Enhanced Mail)是公钥数据的常见存储格式,结构如下:
<text> -----BEGIN <SOMETHING>----- <Attribute> : <Value> <Base64 encoded DER data> -----END <SOMETHING>----- <text>一个文件可包含多个BEGIN/END块;块间的文本行被忽略;属性行除Proc-Type与DEK-Info(用于 DER 数据加密场景)外均被忽略。文件读取由调用方完成,例如:
1> {ok, PemBin} = file:read_file("dsa.pem"). {ok,<<"-----BEGIN DSA PRIVATE KEY-----\nMIIBuw"...>>}解码 DSA 私钥
2> [DSAEntry] = public_key:pem_decode(PemBin). [{'DSAPrivateKey',<<48,130,1,187,...>>, not_encrypted}] 3> Key = public_key:pem_entry_decode(DSAEntry). #'DSAPrivateKey'{version = 0, p = 12900045185019966618...6593, q = 1216700114794736143432235288305776850295620488937, g = 10442040227452349332...47213, y = 87256807980030509074...403143, x = 510968529856012146351317363807366575075645839654}pem_decode/1返回条目三元组{Type, DerBin, EncInfo},其中EncInfo为not_encrypted或{Cipher, Iv};pem_entry_decode/2的第二个参数即口令。
解码带口令的 RSA 私钥
2> [RSAEntry] = public_key:pem_decode(PemBin). [{'RSAPrivateKey',<<224,108,117,...>>, {"DES-EDE3-CBC",<<"kÙeø¼pµL">>}}] 3> Key = public_key:pem_entry_decode(RSAEntry, "abcd1234"). #'RSAPrivateKey'{version = 'two-prime', modulus = 1112355156729921663373...2737107, publicExponent = 65537, ...}这里的口令解密正是由 pubkey_pbe.erl 配合 PEM 头部的Proc-Type/DEK-Info属性完成的。
解码 X.509 证书
1> {ok, PemBin} = file:read_file("cacerts.pem"). 2> [CertEntry1, CertEntry2] = public_key:pem_decode(PemBin). 3> Cert = public_key:pem_entry_decode(CertEntry1).也可以使用pkix_decode_cert/2,它支持两种模式:
otp模式:递归解码为标准 OTP 记录形式(#'OTPCertificate'{},各 RDN 属性值、扩展值均已解出,便于程序直接访问,如#'BasicConstraints'{cA = true}、[keyCertSign, cRLSign]、[{rfc822Name,"peter@erix.ericsson.se"}]);plain模式:等价于pem_entry_decode/1,保持原始 ASN.1 记录。
对证书中局部结构的解码可用public_key:der_decode(Type, DerBin),例如按'X520CommonName'类型解码 CN 值。
编码回 PEM 格式
1> PemEntry = public_key:pem_entry_encode('RSAPublicKey', RSAPubKey). {'RSAPublicKey', <<48,72,...>>, not_encrypted} 2> PemBin = public_key:pem_encode([PemEntry]). <<"-----BEGIN RSA PUBLIC KEY-----\nMEgC...">> 3> file:write_file("rsa_pub_key.pem", PemBin). ok若改用'SubjectPublicKeyInfo'类型,则生成标准的-----BEGIN PUBLIC KEY-----块。
RSA 公钥加解密
%% 私钥加密,公钥解密(传统"数字签名"用法) RsaEncrypted = public_key:encrypt_private(Msg, PrivateKey), Msg = public_key:decrypt_public(RsaEncrypted, PublicKey), %% 公钥加密,私钥解密 RsaEncrypted2 = public_key:encrypt_public(Msg, PublicKey), Msg = public_key:decrypt_private(RsaEncrypted2, PrivateKey),警告:该"原始 RSA"算法已被认为不安全,虽然配合适当的 OpenSSL 密码库存在软件层面的防护,但难以保证安全性,官方强烈建议不要使用(见 using_public_key.md 的 Warning)。
数字签名
Signature = public_key:sign(Msg, sha, PrivateKey), true = public_key:verify(Msg, sha, Signature, PublicKey),若先自行计算摘要,则第二个参数传none:
Digest = crypto:sha(Msg), Signature = public_key:sign(Digest, none, PrivateKey), true = public_key:verify(Digest, none, Signature, PublicKey),证书路径验证
使用pkix_path_validation/3验证证书链,可通过verify_fun选项定制每个证书的检查行为(pubkey_cert.erl 中路径验证每一步都会回调该函数):
Opts = [{verify_fun, fun(Cert, Event, UserState) -> case Event of {bad_cert, Reason} -> {fail, Reason}; % 或 {valid, UserState} 放行 {extension, Ext} -> {unknown, UserState} % 未知扩展交由应用裁决 end end, UserState}], public_key:pkix_path_validation(TrustedCert, CertChain, Opts).主机名校验(RFC 6125)
当客户端校验服务端证书时,除链验证外还需做主机名校验,防止 DNS 劫持等攻击——恶意服务器可能持有一张完全合法(签名有效、未被吊销、链到可信根)但域名不符的证书。public_key:pkix_verify_hostname/2,3实现了 RFC 6125 的默认匹配流程,其中关键规则包括:
- 证书中的 Presented IDs(
Subject的 CN 字段或Subject Alternative Name扩展中的dNSName/uniformResourceIdentifier)须至少与客户端的 Reference IDs 匹配一个; - 若证书含
Subject Alternative Name,则禁止再使用Subject的 CN 做校验; - 通配符仅允许出现在第一个标签且只有一个,如
*.example.com可匹配foo.example.com,但不能匹配example.com或foo.bar.example.com; - 国际化域名(IDN)暂不支持。
调用示例:
public_key:pkix_verify_hostname(CertFromHost, [{uri_id, "https://www.example.net"}]).对于自定义协议,可用fqdn_fun自定义主机名提取、用match_fun重定义匹配逻辑(返回true/false/default,其中default回退到默认规则)。若希望人工"钉住"(pinning)一张不匹配的证书(类似浏览器的"仍然继续"提示),可传入fail_callback:
-include_lib("public_key/include/public_key.hrl"). % 记录定义 ... Fail = fun(#'OTPCertificate'{} = C) -> case in_my_cache(C) orelse my_accept(C) of true -> enter_my_cache(C), true; false -> false end end, public_key:pkix_verify_hostname(CertFromHost, RefIDs, [{fail_callback, Fail}]).注:ssl/tls 等上层应用通常会透传相关选项到
pkix_verify_hostname,日常 TLS 编程中你多半不需要直接调用它。
进一步阅读
- public_key 应用文档:本文所依据的官方应用描述;
- Public Key 使用示例:PEM、RSA、签名、主机名校验的完整交互式示例;
- Public-key Records 指南:所有 ASN.1 生成记录(
#'OTPCertificate'{}、#'RSAPrivateKey'{}等)的字段说明; - public_key API 模块:全部公开函数签名与文档;
- 应用启动与管理可参考
m:application(application:start/[1,2]、application:load/1)。
- 编程语言
- 语言运行时
- 标准库
- 编译器
- 并发编程
【免费下载链接】otp
Erlang/OTP
相关推荐
OpenCore Legacy Patcher 官方 FAQ 技术详解:版本策略、更新机制与 AVX、Metal 兼容性故障排查
OpenCore Legacy Patcher 官方 FAQ 技术详解:版本策略、更新机制与 AVX、Metal 兼容性故障排查 本文基于 OpenCore L
编程语言语言运行时标准库编译器并发编程Diem Crypto 组件深度解析:哈希、签名与密钥派生基础设施
Diem Crypto 组件深度解析:哈希、签名与密钥派生基础设施 Diem 区块链的密码学基础全部封装在 diem crypto crate 中,覆盖哈希、签
区块链金融科技EMQX 5.8.8 升级 Erlang/OTP 26.2.5.14 深度解析:TLS 证书续期竞态修复与 RSA-PSS 签名证书支持
EMQX 5.8.8 升级 Erlang/OTP 26.2.5.14 深度解析:TLS 证书续期竞态修复与 RSA PSS 签名证书支持 本篇技术指南围绕 EM
后端物联网消息队列通信
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考