You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Electron发送自定义客户端证书触发ERR_SSL_CLIENT_AUTH_CERT_NEEDED错误排查

问题描述

尝试从Electron向基于gevent的后端发送.pem格式客户端证书时,遇到错误:
(node:18946) electron: Failed to load URL: https://127.0.0.1:5050/ with error: ERR_SSL_CLIENT_AUTH_CERT_NEEDED

前端代码

const create_cert_principle_issuer = (cert_pem_data) => {
    const cert_pem_obj = pki.certificateFromPem(cert_pem_data)
    const ca_cert_principle_obj = {}
    // 将node-forge证书键名映射为Electron证书结构的键名
    const mapping = {
        'countryName': 'country',
        'stateOrProvinceName': 'state',
        'localityName': 'locality',
        'organizationName': 'organizations',
        'organizationalUnitName': 'organizationUnits',
        'commonName': 'commonName'
    }
    for (let key in mapping){
        ca_cert_principle_obj[mapping[key]] = ""
    }
    for (const attribute of cert_pem_obj.issuer.attributes) {
            if (attribute.name in mapping) {
                let new_keyname = mapping[attribute.name]
                ca_cert_principle_obj[new_keyname] = attribute.value
            }
    }
    return ca_cert_principle_obj
}
const create_cert_from_PEM = (cert_pem_data, parent_cert_data) => {
    const is_self_signed = (parent_cert_data === false);
    const subjectPrinciple = create_cert_principle(cert_pem_data)
    const subjectNodeCert = pki.certificateFromPem(cert_pem_data)
    let certDer = forge.pem.decode(cert_pem_data)[0].body
    let md = forge.md.sha1.create()
    md.start()
    md.update(certDer)
    let digest = md.digest()
    let hex = digest.toHex()
    log.info('digest is, ', hex)
    const electron_cert = {
        data: cert_pem_data,
        subject: subjectPrinciple,
        subjectName: subjectPrinciple.commonName,
        serialNumber: subjectNodeCert.serialNumber,
        validStart: subjectNodeCert.validity.notBefore.getTime() / 1000,
        validExpiry: subjectNodeCert.validity.notAfter.getTime() / 1000,
        issuer : create_cert_principle_issuer(cert_pem_data),
        issuerName : subjectNodeCert.issuer.commonName,
        fingerprint: hex
    }
    if(!is_self_signed){
        electron_cert.issuerCert = create_cert_from_PEM(parent_cert_data, false)
    }
    return electron_cert
}

app.on('select-client-certificate', (event, webContents, url, list, send_cert) => {
    event.preventDefault()
    const client_cert_data = fs.readFileSync(path.resolve(certs_path, 'client-auth', 'cert.pem'))
    let cert = create_cert_from_PEM(client_cert_data.toString(), false)
    send_cert([cert])
})

后端代码

this_dir = os.path.dirname(os.path.realpath(__file__))
certfile = os.path.join(this_dir, "certificates", "fullchain.pem")
keyfile = os.path.join(this_dir, "certificates", "privkey.pem")
cafile = os.path.join(this_dir, "certificates", "my-root-ca.crt.pem")
if prod:
    secure_context = SSLContext(ssl.PROTOCOL_TLS_SERVER, server_side=True)
    secure_context.verify_mode = ssl.CERT_REQUIRED
    secure_context.load_verify_locations(cafile=cafile)
    secure_context.load_cert_chain(certfile=certfile, keyfile=keyfile)
    http_server = WSGIServer(("127.0.0.1", 5050), app, ssl_context=secure_context)
    http_server.serve_forever()

前端通过select-client-certificate事件监听器加载客户端证书PEM文件,按照Electron的Certificate结构构建证书对象并发送。生成的证书结构与Electron默认证书一致,但调用send_cert后仍触发ERR_SSL_CLIENT_AUTH_CERT_NEEDED错误,仿佛证书无效或未发送。

已确认证书有效:openssl验证通过,且通过Axios发送证书能成功请求:

const agent = new https.Agent({
    ca: fs.readFileSync(path.resolve(certs_path, 'ca', 'my-root-ca.crt.pem')),
    cert:  fs.readFileSync(path.resolve(certs_path, 'client-auth', 'cert.pem')),
    key:  fs.readFileSync(path.resolve(certs_path, 'client-auth', 'privkey.pem')),
    servername: '127.0.0.1',
    port: 5050,
})
axios.get(url, {httpsAgent: agent})
        .then(res => {
            // 尝试加载URL验证是否可用
            mainWindow.loadURL(url).then((res)=>{
                console.log("Loaded Successfully")
            })
        })

可能的原因及解决方案

1. 未提供客户端证书的私钥

Electron的select-client-certificate事件中,你构建的证书对象只包含了证书本身(data字段是PEM证书),但缺少对应的私钥。Axios请求中同时提供了cert和key,而Electron完成SSL客户端认证需要证书+私钥的完整配对。

Electron的Certificate结构的data字段实际支持PKCS#12格式(.pfx/.p12),这种格式包含了证书和对应的私钥。你可以用openssl将PEM证书和私钥打包成PKCS#12文件:

openssl pkcs12 -export -in cert.pem -inkey privkey.pem -out client.p12

然后读取该文件的内容作为data字段的值传入。

2. 证书对象的fingerprint格式不匹配

你用SHA1生成的fingerprint是连续小写十六进制,但Chromium/Electron期望的fingerprint是大写十六进制且用冒号分隔(例如A1:B2:C3:D4:...)。格式不匹配会导致Electron无法识别证书有效性,进而被后端拒绝。

修改fingerprint生成逻辑:

let hex = digest.toHex().toUpperCase();
const fingerprint = hex.match(/.{1,2}/g).join(':');

3. 证书链不完整

后端可能要求完整的客户端证书链,但你只发送了客户端证书,未包含中间CA证书。检查你的客户端证书是否需要搭配中间CA才能被根CA信任,若需要,将中间CA证书也加入到PKCS#12包中,或者在构建Electron证书对象时正确设置issuerCert字段。

4. Chromium未信任根CA

Electron的Chromium引擎使用独立的证书存储,即使Axios中指定了ca,Chromium可能未信任你的根CAmy-root-ca.crt.pem。可以通过代码强制信任该CA:

const { session } = require('electron');
session.defaultSession.setCertificateVerifyProc((request, callback) => {
  // 直接信任目标服务器的证书链
  if (request.host === '127.0.0.1') {
    callback(0); // 0表示信任
  } else {
    callback(-3); // 使用默认验证逻辑
  }
});

或者将根CA安装到系统证书存储中,让Chromium自动信任。


内容的提问来源于stack exchange,提问作者Keane Dixon Wong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 19:14:51