Electron Certificate 对象详解:X.509 证书在 JS 侧的数据结构与实战用法
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
本文围绕 Electron 官方结构文档 Certificate 展开,逐一解析Certificate对象的 10 个字段(PEM 数据、签发者/主题主体、序列号、有效期、指纹)在 Chromium 底层是如何生成的,并说明它在app的证书错误事件、客户端证书选择、session.setCertificateVerifyProc与 macOS 证书信任对话框这四个典型场景中的实际用法,帮助读者在 Electron 应用中正确读取、校验和信任 X.509 证书。
1. Certificate 对象字段总览
Certificate对象是 Electron 中所有涉及 TLS/X.509 证书的 API 共用的数据结构。按照 docs/api/structures/certificate.md 的定义,它包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
data | string | PEM 编码的证书数据 |
issuer | CertificatePrincipal | 签发者(Issuer)主体 |
issuerName | string | 签发者的 Common Name |
issuerCert | Certificate | 签发者证书(当证书非自签名时存在) |
subject | CertificatePrincipal | 主题(Subject)主体 |
subjectName | string | 主题的 Common Name |
serialNumber | string | 十六进制字符串表示的序列号 |
validStart | number | 证书生效时间,Unix 秒级时间戳 |
validExpiry | number | 证书过期时间,Unix 秒级时间戳 |
fingerprint | string | 证书指纹 |
其中issuer和subject两个字段是嵌套的CertificatePrincipal对象,其结构定义在 docs/api/structures/certificate-principal.md:
commonNamestring - 通用名称(CN)organizationsstring[] - 组织名称列表organizationUnitsstring[] - 组织单位列表localitystring - 城市(Locality)statestring - 州或省countrystring - 国家或地区
2. 源码实现:字段如何从 Chromium 的 X509Certificate 生成
Electron 中 JS 侧的Certificate对象并非手工拼装,而是由 gin 类型转换器统一生成。核心实现在 shell/common/gin_converters/net_converter.cc,它将 Chromium 网络栈的net::X509Certificate转换为 V8 对象:
// shell/common/gin_converters/net_converter.cc // static v8::Local<v8::Value> Converter<scoped_refptr<net::X509Certificate>>::ToV8( v8::Isolate* isolate, const scoped_refptr<net::X509Certificate>& val) { std::string encoded_data; net::X509Certificate::GetPEMEncoded(val->cert_buffer(), &encoded_data); gin::DataObjectBuilder builder(isolate); builder.Set("data", encoded_data) .Set("issuer", val->issuer()) .Set("issuerName", val->issuer().GetDisplayName()) .Set("subject", val->subject()) .Set("subjectName", val->subject().GetDisplayName()) .Set("serialNumber", base::HexEncode(val->serial_number())) .Set("validStart", val->valid_start().InSecondsFSinceUnixEpoch()) .Set("validExpiry", val->valid_expiry().InSecondsFSinceUnixEpoch()) .Set("fingerprint", net::HashValue(net::HASH_VALUE_SHA256, val->CalculateFingerprint256(val->cert_buffer())) .ToString()); // ... issuerCert 处理 }从这段实现可以确认原文档中每个字段的确切含义:
data:由net::X509Certificate::GetPEMEncoded对原始证书字节缓冲做 PEM 编码得到,是标准-----BEGIN CERTIFICATE-----格式字符串,可直接交给crypto模块或命令行工具处理;issuerName/subjectName:调用CertPrincipal::GetDisplayName(),即主体中用于展示的 Common Name;serialNumber:对原始序列号字节做base::HexEncode十六进制编码后的字符串,注意这是小写十六进制且不加分隔符,与部分工具输出带冒号的形式不同,比较时需要归一化;validStart/validExpiry:通过InSecondsFSinceUnixEpoch()得到的 Unix 秒级时间戳(1970-01-01 起的秒数),JS 中需乘以 1000 才能传给Date;fingerprint:对整张 DER 编码证书计算 SHA-256 摘要(CalculateFingerprint256),并用net::HashValue格式化为指纹字符串,可用于在日志、缓存或白名单中唯一标识一张证书;issuerCert:仅当证书携带中间证书链(intermediate_buffers()非空)时才被设置——代码取第一张中间证书作为直接签发者,并把它后面的其余中间证书挂到该签发者证书下,从而递归形成证书链。这解释了为什么issuerCert只有在“非自签名”时才有值:自签名根证书没有中间证书缓冲,字段即为空。
2.1 反向转换:Certificate 也能传回 C++
同一文件还实现了FromV8方向(net_converter.cc):JS 传入的Certificate对象会被CertFromData读取其中的data字段,用CreateCertificateListFromBytes以FORMAT_SINGLE_CERTIFICATE模式解析出叶子证书,若还携带issuerCert则把两者重新组装成带中间链的X509Certificate。这正是app.on('select-client-certificate')中能把回调选出的证书对象直接交还给网络栈的原因。
CertificatePrincipal的转换同样在 net_converter.cc 中实现,六个字段与 Chromium 的net::CertPrincipal一一对应:
return gin::DataObjectBuilder(isolate) .Set("commonName", val.common_name) .Set("organizations", val.organization_names) .Set("organizationUnits", val.organization_unit_names) .Set("locality", val.locality_name) .Set("state", val.state_or_province_name) .Set("country", val.country_name) .Build();3. 实战场景:Certificate 对象出现在哪些 API 中
Certificate是只读的结构对象,本身没有方法,它的价值体现在被以下 Electron API 作为参数返回或接收。
3.1 处理服务器证书校验失败:app/webContents的certificate-error
当 URL 的服务器证书校验失败时,app会发出certificate-error事件(见 docs/api/app.md),回调参数中的certificate即Certificate对象,error为错误码字符串:
const { app } = require('electron') app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { if (url === 'https://github.com') { // 校验逻辑。 event.preventDefault() callback(true) } else { callback(false) } })要信任该证书,需调用event.preventDefault()并执行callback(true)。此时certificate对象可直接用于审计:例如打印certificate.subjectName、certificate.validExpiry判断是否因过期被拒,或把certificate.fingerprint记入日志。docs/api/web-contents.md 中webContents的certificate-error事件语义相同,并额外提供isTrusted布尔量指示证书是否可被视为可信。
3.2 选择客户端证书:select-client-certificate
当服务器要求双向 TLS(mTLS)客户端证书时,Electron 从平台证书存储中取出候选列表,以Certificate[]形式通过select-client-certificate事件交给应用(见 docs/api/app.md):
const { app } = require('electron') app.on('select-client-certificate', (event, webContents, url, list, callback) => { event.preventDefault() callback(list[0]) })list中每一项都是完整的Certificate对象,可以在callback(list[i])之前依据subject.commonName、issuerName等字段挑选合适的证书。注意两点(同样记录在 app.md 中):
webContents可能为null——当请求来自主进程的net.request/net.fetch,或来自设置了respondToAuthRequestsFromMainProcess: true的 utility process 时,请求并不源于某个渲染进程;- 未调用
event.preventDefault()时,Electron 默认使用证书存储中的第一张证书。
如 2.1 节所述,回调传入的证书对象会经由FromV8转回 C++ 侧的X509Certificate,所以传给callback的必须是事件给定的list中的条目(保留issuerCert链信息),而不是自行用data字段重新构造的对象。
3.3 自定义验证流程:session.setCertificateVerifyProc
session的setCertificateVerifyProc允许用 JS 完全接管证书验证(见 docs/api/session.md)。回调中的request同时携带certificate(服务器证书)与validatedCertificate(经过验证的那张,两者可能不同),以及isIssuedByKnownRoot、verificationResult、errorCode等字段:
const { BrowserWindow } = require('electron') const win = new BrowserWindow() win.webContents.session.setCertificateVerifyProc((request, callback) => { const { hostname } = request if (hostname === 'github.com') { callback(0) } else { callback(-2) } })约定行为:
callback(0):接受证书,并禁用 Certificate Transparency 校验;callback(-2):拒绝证书;- 其他取值为 Chromium 网络栈的证书错误码;
- 调用
setCertificateVerifyProc(null)恢复默认验证逻辑; - 文档明确提示:该回调结果会被网络服务缓存,测试时不要假设每次请求都会重新触发。
isIssuedByKnownRoot为false通常意味着证书来自本地安装了根 CA 的 MITM 代理(如企业代理),此时若verificationResult不是OK,官方文档建议不要信任——这是安全审计时直接可用的判据。
3.4 macOS 证书信任对话框:dialog.showCertificateTrustDialog
Certificate对象也可作为输入参数:dialog.showCertificateTrustDialog(见 docs/api/dialog.md)接收options.certificate与options.message,在 macOS 上弹出模态对话框让用户决定是否信任/导入该证书:
- 提供
window参数时,对话框会挂载为父窗口的 sheet(模态); - Windows 上能力受限:系统自带确认对话框,
message不生效,window参数被忽略。
典型流程是:先在certificate-error事件里拿到未受信任的certificate对象,展示subjectName/issuerName给用户确认,再调用信任对话框完成导入——全程不需要离开 JS 层解析 PEM。
3.5 相关但形态不同的证书 API
除上述以Certificate对象为中心的场景外,仓库文档中还有一组证书相关但不返回Certificate对象的 API,注意区分:
app.importCertificate(docs/api/app.md):以 PKCS12 文件路径和密码导入平台证书存储,输入是文件而非Certificate对象;app.on('select-client-certificate')配套的密码解锁请求(app.md 中的tokenName/hostname参数):当解锁客户端证书需要密码时触发;- 命令行开关
--ignore-certificate-errors(docs/api/command-line-switches.md):全局忽略证书错误,属于调试手段,不应在生产应用中使用。
4. 使用 Certificate 字段时的注意事项
- 时间戳单位:
validStart/validExpiry是秒不是毫秒,new Date(cert.validExpiry * 1000)才是可读日期; - 指纹算法:
fingerprint固定为整张 DER 证书的 SHA-256,且经由net::HashValue格式化输出。若要与系统工具(如openssl x509 -fingerprint -sha256)的输出比对,注意两边分隔符与大小写可能不同,建议归一化后再比较; issuerCert的可选项语义:字段缺失(自签名或服务器未提供中间链)不代表证书无效,仅表示Certificate链信息不完整;从FromV8实现看,跨 API 传递证书时应原样透传整个对象;issuerName与issuer.commonName的取舍:两者在常规情况下相同,但GetDisplayName()是展示用名称,做机器判断时建议优先使用结构化的issuer.commonName/subject.commonName;- 只读对象:
Certificate是 ginDataObjectBuilder生成的纯数据对象,没有方法,不应被修改后重复传入其他 API。
5. 小结
Certificate对象是 Electron 把 Chromiumnet::X509Certificate暴露给 JS 的统一窗口:data提供可再加工的 PEM 原文,issuer/subject提供结构化主体信息,serialNumber、validStart、validExpiry、fingerprint提供可程序化比对的唯一标识与有效期,issuerCert则递归承载中间证书链。理解 net_converter.cc 中的双向转换逻辑后,就能在certificate-error、select-client-certificate、setCertificateVerifyProc、showCertificateTrustDialog等场景中正确地消费和回传证书数据,实现从“绕过错误”到“真正审计证书”的工程化升级。
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考