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

