如何在Vanilla JS前端项目中解析.cer证书文件并提取基础信息
纯前端原生JS解析CER证书方案(无第三方库)
要在纯前端原生JS里解析.cer证书并提取颁发对象、有效期等信息,核心是利用Web Crypto API处理证书,同时兼容CER文件的两种常见格式(DER二进制、PEM Base64编码)。下面是完整的实现方案,直接整合到你的现有代码中:
步骤说明
- 读取上传文件,区分DER/PEM格式并统一转换为DER二进制Buffer
- 手动解析X.509证书的ASN.1结构,提取所需字段(Web Crypto本身不直接返回这些元数据,需自行解析ASN.1)
- 将解析后的信息渲染到页面
完整整合代码
function showFile() { let fileType = file.type; // file是全局变量,存储上传的文件 let validExtensions = ['application/x-x509-ca-cert']; if (validExtensions.includes(fileType)) { let fileReader = new FileReader(); fileReader.onload = (e) => { let certData = e.target.result; let derBuffer; // 区分PEM和DER格式,统一转为DER二进制Buffer if (typeof certData === 'string') { // 处理DataURL格式(如果用readAsDataURL的话) if (certData.startsWith('data:')) { certData = certData.split(',')[1]; } // 识别PEM格式,去除标记后Base64解码 if (certData.includes('-----BEGIN CERTIFICATE-----')) { certData = certData.replace(/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----/g, '').replace(/\s+/g, ''); derBuffer = base64ToArrayBuffer(certData); } else { // 尝试直接Base64解码为DER derBuffer = base64ToArrayBuffer(certData); } } else { // ArrayBuffer直接作为DER格式 derBuffer = certData; } // 解析证书信息并展示 parseCertificate(derBuffer) .then(certInfo => { displayCertInfo(certInfo); }) .catch(err => { alert('证书解析失败:' + err.message); }); }; // 读取文件为ArrayBuffer(推荐,兼容二进制DER文件) fileReader.readAsArrayBuffer(file); } else { alert('Invalid file format'); dropArea.classList.remove('active'); dragText.textContent = 'Drag and drop your file'; } } // Base64转ArrayBuffer工具函数 function base64ToArrayBuffer(base64) { const binaryString = window.atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } // 核心证书解析函数 async function parseCertificate(derBuffer) { const view = new DataView(derBuffer); let offset = 0; // 跳过证书外层序列 offset = skipSequence(view, offset); // 跳过版本号 offset = skipInteger(view, offset); // 跳过序列号 offset = skipInteger(view, offset); // 跳过签名算法 offset = skipSequence(view, offset); // 解析颁发者 const issuer = parseName(view, offset); offset = skipSequence(view, offset); // 解析有效期 const validity = parseValidity(view, offset); offset = skipSequence(view, offset); // 解析颁发对象(主体) const subject = parseName(view, offset); return { issuer: issuer, subject: subject, notBefore: validity.notBefore, notAfter: validity.notAfter }; } // ASN.1解析辅助函数:跳过序列 function skipSequence(view, offset) { if (view.getUint8(offset) !== 0x30) throw new Error("无效的证书结构:预期序列"); offset++; const length = getLength(view, offset); offset += length.lengthSize; return offset + length.value; } // ASN.1解析辅助函数:跳过整数 function skipInteger(view, offset) { if (view.getUint8(offset) !== 0x02) throw new Error("无效的证书结构:预期整数"); offset++; const length = getLength(view, offset); offset += length.lengthSize; return offset + length.value; } // ASN.1解析辅助函数:获取字段长度 function getLength(view, offset) { const firstByte = view.getUint8(offset); if ((firstByte & 0x80) === 0) { return { value: firstByte, lengthSize: 1 }; } const lengthSize = firstByte & 0x7F; let value = 0; for (let i = 0; i < lengthSize; i++) { value = (value << 8) | view.getUint8(offset + 1 + i); } return { value, lengthSize: lengthSize + 1 }; } // 解析X.509名称(颁发者/主体) function parseName(view, offset) { if (view.getUint8(offset) !== 0x30) throw new Error("无效的名称结构"); offset++; const length = getLength(view, offset); offset += length.lengthSize; const endOffset = offset + length.value; const nameParts = []; // OID到常见名称的映射 const oidMap = { "2.5.4.6": "国家(C)", "2.5.4.8": "州/省(ST)", "2.5.4.7": "城市(L)", "2.5.4.10": "组织(O)", "2.5.4.11": "组织单元(OU)", "2.5.4.3": "通用名称(CN)" }; while (offset < endOffset) { offset++; // 跳过RDN序列标记 const rdnLength = getLength(view, offset); offset += rdnLength.lengthSize; const rdnEnd = offset + rdnLength.value; while (offset < rdnEnd) { offset++; // 跳过AttributeTypeAndValue标记 const atvLength = getLength(view, offset); offset += atvLength.lengthSize; // 解析OID const oidResult = parseOID(view, offset); offset += oidResult.length; // 解析值 const valueResult = parseUTF8String(view, offset); offset += valueResult.length; const key = oidMap[oidResult.content] || `OID(${oidResult.content})`; nameParts.push(`${key}: ${valueResult.content}`); } } return nameParts.join('<br>'); } // 解析ASN.1 OID function parseOID(view, offset) { if (view.getUint8(offset) !== 0x06) throw new Error("预期OID字段"); offset++; const length = getLength(view, offset); offset += length.lengthSize; const endOffset = offset + length.value; let oidParts = []; const firstByte = view.getUint8(offset++); oidParts.push(Math.floor(firstByte / 40)); oidParts.push(firstByte % 40); while (offset < endOffset) { let value = 0; let byte; do { byte = view.getUint8(offset++); value = (value << 7) | (byte & 0x7F); } while ((byte & 0x80) !== 0); oidParts.push(value); } return { content: oidParts.join('.'), length: length.lengthSize + 1 + length.value }; } // 解析ASN.1 UTF8字符串 function parseUTF8String(view, offset) { if (view.getUint8(offset) !== 0x0C) throw new Error("预期UTF8字符串"); offset++; const length = getLength(view, offset); offset += length.lengthSize; const bytes = new Uint8Array(view.buffer, offset, length.value); const content = new TextDecoder('utf-8').decode(bytes); return { content, length: length.lengthSize + 1 + length.value }; } // 解析有效期(notBefore/notAfter) function parseValidity(view, offset) { if (view.getUint8(offset) !== 0x30) throw new Error("预期有效期结构"); offset++; const length = getLength(view, offset); offset += length.lengthSize; // 解析起始时间 const notBefore = parseTimeField(view, offset); offset += notBefore.length; // 解析结束时间 const notAfter = parseTimeField(view, offset); return { notBefore: notBefore.date, notAfter: notAfter.date }; } // 解析UTCTime或GeneralizedTime字段 function parseTimeField(view, offset) { const tag = view.getUint8(offset); if (tag !== 0x17 && tag !== 0x18) throw new Error("预期时间字段"); offset++; const length = getLength(view, offset); offset += length.lengthSize; const timeStr = new TextDecoder('ascii').decode(new Uint8Array(view.buffer, offset, length.value)); let date; if (tag === 0x17) { // UTCTime格式:YYMMDDHHMMSSZ,处理年份(YY>=50则为19XX,否则20XX) const year = parseInt(timeStr.slice(0, 2)); const fullYear = year >= 50 ? 1900 + year : 2000 + year; date = new Date(`${fullYear}-${timeStr.slice(2,4)}-${timeStr.slice(4,6)}T${timeStr.slice(6,8)}:${timeStr.slice(8,10)}:${timeStr.slice(10,12)}Z`); } else { // GeneralizedTime格式:YYYYMMDDHHMMSSZ date = new Date(`${timeStr.slice(0,4)}-${timeStr.slice(4,6)}-${timeStr.slice(6,8)}T${timeStr.slice(8,10)}:${timeStr.slice(10,12)}:${timeStr.slice(12,14)}Z`); } return { date, length: length.lengthSize + 1 + length.value }; } // 渲染证书信息到页面 function displayCertInfo(certInfo) { // 替换为你的DOM容器ID const infoContainer = document.getElementById('cert-info-container'); if (!infoContainer) return; infoContainer.innerHTML = ` <div class="cert-info"> <h3>证书基本信息</h3> <div class="info-item"> <span class="label">颁发对象:</span> <span class="value">${certInfo.subject}</span> </div> <div class="info-item"> <span class="label">颁发者:</span> <span class="value">${certInfo.issuer}</span> </div> <div class="info-item"> <span class="label">有效期起始:</span> <span class="value">${certInfo.notBefore.toLocaleString()}</span> </div> <div class="info-item"> <span class="label">有效期结束:</span> <span class="value">${certInfo.notAfter.toLocaleString()}</span> </div> </div> `; }
注意事项
- 兼容性:Web Crypto API支持现代浏览器(Chrome、Firefox、Edge等),IE不支持
- 证书格式:兼容DER二进制和PEM Base64两种常见的CER格式
- ASN.1解析:这里只实现了提取核心字段的逻辑,如果需要更多字段(比如公钥、签名算法),可以扩展对应的ASN.1解析逻辑
内容的提问来源于stack exchange,提问作者Alexander
相关产品推荐
相关产品推荐

