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

如何在Vanilla JS前端项目中解析.cer证书文件并提取基础信息

纯前端原生JS解析CER证书方案(无第三方库)

要在纯前端原生JS里解析.cer证书并提取颁发对象、有效期等信息,核心是利用Web Crypto API处理证书,同时兼容CER文件的两种常见格式(DER二进制、PEM Base64编码)。下面是完整的实现方案,直接整合到你的现有代码中:

步骤说明

  1. 读取上传文件,区分DER/PEM格式并统一转换为DER二进制Buffer
  2. 手动解析X.509证书的ASN.1结构,提取所需字段(Web Crypto本身不直接返回这些元数据,需自行解析ASN.1)
  3. 将解析后的信息渲染到页面

完整整合代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 07:27:29