news 2026/9/24 15:01:44

Erlang/OTP Public_Key 应用深度指南:公钥基础设施、PKIX 证书链与数字签名

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Erlang/OTP Public_Key 应用深度指南:公钥基础设施、PKIX 证书链与数字签名
  • 编程语言
  • 语言运行时
  • 标准库
  • 编译器
  • 并发编程

【免费下载链接】otp

Erlang/OTP

项目地址:https://gitcode.com/gh_mirrors/ot/otp
点击查看免费下载

导读

本文围绕 Erlang/OTP 中负责公钥基础设施(Public Key Infrastructure)的public_key应用展开,系统梳理其"库应用、纯内存二进制处理"的设计定位、完整支持的 PKIX 标准族、与crypto/asn1的依赖关系,以及唯一例外的系统 CA 证书加载接口cacerts_load/0,1cacerts_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/1file:write_file/2这类文件操作,把内容以 binary 交给 public_key 处理。

唯一的例外是以下三个函数会真正读取文件:

  • public_key:cacerts_load/0
  • public_key:cacerts_load/1
  • public_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 5280Internet 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-3Diffie-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-2024X509-ML-DSA-2025X509-ML-KEM-2025KEMAlgorithmInformation-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]依次启动asn1crypto等依赖。典型顺序:

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.erlCRL 解析、吊销状态跟踪、CRL 签名验证
pubkey_ocsp.erlOCSP(在线证书状态协议)请求/响应处理
pubkey_pem.erlPEM 格式的编解码
pubkey_pbe.erl基于密码的加密(Password-Based Encryption)
pubkey_os_cacerts.erl从操作系统/文件加载受信任 CA 证书
pubkey_policy_tree.erl证书策略树处理(对应 OTP-26.2 引入的策略支持)
pubkey_ssh.erlSSH 相关密钥格式(dh_gex_group等)
pubkey_translation.erl不同密钥表示之间的转换

核心 API 分组(见 public_key.erl 的导出声明):

  • 格式编解码pem_decode/1pem_encode/1pem_entry_decode/1,2pem_entry_encode/2,3der_decode/2der_encode/2pkix_decode_cert/2pkix_encode/3
  • 加解密encrypt_private/2,3decrypt_private/2,3encrypt_public/2,3decrypt_public/2,3
  • 签名验签sign/3,4verify/4,5
  • 密钥处理generate_key/1compute_key/2,3dh_gex_group/4dh_gex_group_sizes/0
  • 证书操作pkix_sign/2pkix_verify/2pkix_is_self_signed/1pkix_is_issuer/2pkix_issuer_id/2pkix_subject_id/1pkix_normalize_name/1
  • 验证类pkix_path_validation/3pkix_verify_hostname/2,3pkix_crls_validate/3pkix_crl_verify/2pkix_ocsp_validate/5
  • 系统 CA 证书cacerts_get/0cacerts_load/0,1cacerts_clear/0

其中证书路径验证pkix_path_validation/3在 pubkey_cert.erl 中实现,核心机制是逐证书调用verify_fun回调(verify_fun/4类型,见 pubkey_cert.erl):验证过程会依次检查证书有效期(cert_expiredinvalid_validity_dates)、签发者(invalid_issuer)、名称约束(distinguished_name_not_permittedname_not_permitted)、签名(invalid_signature)以及扩展约束,每一步失败都会以{bad_cert, Reason}元组形式交给用户提供的verify_fun决定接受还是拒绝。CRL 验证则位于 pubkey_crl.erl,导出validate/7init_revokation_state/0fresh_crl/3verify_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:

  1. 平台分发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}}
  2. 路径覆盖load/1接收文件路径列表并依序尝试,可用于在load/0不适用的平台上手动指定证书来源;
  3. 缓存:加载结果缓存在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-TypeDEK-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},其中EncInfonot_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.comfoo.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:applicationapplication:start/[1,2]application:load/1)。
  • 编程语言
  • 语言运行时
  • 标准库
  • 编译器
  • 并发编程

【免费下载链接】otp

Erlang/OTP

项目地址:https://gitcode.com/gh_mirrors/ot/otp
点击查看免费下载

相关推荐

上一篇:AutoGPT 前端性能实践:用 better-all 做依赖驱动并行化,消除部分依赖的数据获取瀑布
下一篇:WhiteSur 主题实操指南:6 步装好、调美、卸干净

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 15:01:43

Flowable 监听器使用指南

Flowable 监听器使用指南 在 Flowable 流程引擎中&#xff0c;监听器&#xff08;Listener&#xff09;是扩展流程行为的核心机制之一。它允许开发者在流程执行的特定时刻插入自定义逻辑&#xff0c;而无需修改 BPMN 流程图本身。Flowable 主要提供两种监听器&#xff1a;执行监…

作者头像 李华
网站建设 2026/9/24 14:56:54

QEMU模拟STM32实战:从LED闪烁到工业级嵌入式验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 14:56:54

FPGA_GTY_SerDes_高速串并转换技术说明

1. 概述高速串行通信的核心目标&#xff0c;是解决通信系统中“并行数据位宽大、走线多”与“高速传输希望减少布线数量”之间的矛盾。传统并行接口可能需要&#xff1a;D0 D1 D2 ... D31 CLK Control如果采用高速串行通信&#xff0c;则可以把多位并行数据转换为一条或少量几条…

作者头像 李华
网站建设 2026/9/24 14:55:47

十年iOS开发实战复盘:从Objective-C到Swift与跨平台演进

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华