HarmonyOS开发:证书管理——证书申请与更新

HarmonyOS开发:证书管理------证书申请与更新

📌 核心要点:证书是HarmonyOS应用签名的信任根基,从申请到续期到多团队协作,证书管理贯穿应用全生命周期,搞不好就面临应用无法安装、无法更新的灾难。

背景与动机

你的应用上线了,用户量稳步增长,一切看起来都很好。突然有一天,你发现证书快过期了------还剩7天。你慌了,赶紧去续期,但续期要审核,审核要3个工作日。证书过期了,新包签不了,旧包装不了,用户更新不了,新用户安装不了。

这不是吓你,这种事真发生过。

证书管理看起来是"运维"的事,跟开发没关系?错。证书体系的设计直接影响你的开发流程、团队协作、发布节奏。不了解证书体系,你连调试设备都注册不明白;不会续期,应用可能随时"断供";不会多团队证书管理,协作就是一团乱麻。

核心原理

华为开发者证书体系全景

HarmonyOS的证书体系是层级结构,从根CA到开发者证书,层层签发、层层信任:
#mermaid-svg-b2JWBE26tbrReXpN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b2JWBE26tbrReXpN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b2JWBE26tbrReXpN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b2JWBE26tbrReXpN .error-icon{fill:#552222;}#mermaid-svg-b2JWBE26tbrReXpN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b2JWBE26tbrReXpN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b2JWBE26tbrReXpN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b2JWBE26tbrReXpN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b2JWBE26tbrReXpN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b2JWBE26tbrReXpN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b2JWBE26tbrReXpN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b2JWBE26tbrReXpN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b2JWBE26tbrReXpN .marker.cross{stroke:#333333;}#mermaid-svg-b2JWBE26tbrReXpN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b2JWBE26tbrReXpN p{margin:0;}#mermaid-svg-b2JWBE26tbrReXpN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-b2JWBE26tbrReXpN .cluster-label text{fill:#333;}#mermaid-svg-b2JWBE26tbrReXpN .cluster-label span{color:#333;}#mermaid-svg-b2JWBE26tbrReXpN .cluster-label span p{background-color:transparent;}#mermaid-svg-b2JWBE26tbrReXpN .label text,#mermaid-svg-b2JWBE26tbrReXpN span{fill:#333;color:#333;}#mermaid-svg-b2JWBE26tbrReXpN .node rect,#mermaid-svg-b2JWBE26tbrReXpN .node circle,#mermaid-svg-b2JWBE26tbrReXpN .node ellipse,#mermaid-svg-b2JWBE26tbrReXpN .node polygon,#mermaid-svg-b2JWBE26tbrReXpN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b2JWBE26tbrReXpN .rough-node .label text,#mermaid-svg-b2JWBE26tbrReXpN .node .label text,#mermaid-svg-b2JWBE26tbrReXpN .image-shape .label,#mermaid-svg-b2JWBE26tbrReXpN .icon-shape .label{text-anchor:middle;}#mermaid-svg-b2JWBE26tbrReXpN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b2JWBE26tbrReXpN .rough-node .label,#mermaid-svg-b2JWBE26tbrReXpN .node .label,#mermaid-svg-b2JWBE26tbrReXpN .image-shape .label,#mermaid-svg-b2JWBE26tbrReXpN .icon-shape .label{text-align:center;}#mermaid-svg-b2JWBE26tbrReXpN .node.clickable{cursor:pointer;}#mermaid-svg-b2JWBE26tbrReXpN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b2JWBE26tbrReXpN .arrowheadPath{fill:#333333;}#mermaid-svg-b2JWBE26tbrReXpN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b2JWBE26tbrReXpN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b2JWBE26tbrReXpN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b2JWBE26tbrReXpN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b2JWBE26tbrReXpN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b2JWBE26tbrReXpN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b2JWBE26tbrReXpN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b2JWBE26tbrReXpN .cluster text{fill:#333;}#mermaid-svg-b2JWBE26tbrReXpN .cluster span{color:#333;}#mermaid-svg-b2JWBE26tbrReXpN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b2JWBE26tbrReXpN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b2JWBE26tbrReXpN rect.text{fill:none;stroke-width:0;}#mermaid-svg-b2JWBE26tbrReXpN .icon-shape,#mermaid-svg-b2JWBE26tbrReXpN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b2JWBE26tbrReXpN .icon-shape p,#mermaid-svg-b2JWBE26tbrReXpN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b2JWBE26tbrReXpN .icon-shape .label rect,#mermaid-svg-b2JWBE26tbrReXpN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b2JWBE26tbrReXpN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b2JWBE26tbrReXpN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b2JWBE26tbrReXpN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-b2JWBE26tbrReXpN .root>*{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .root span{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .root tspan{fill:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .mid>*{fill:#F39C12!important;stroke:#D68910!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .mid span{fill:#F39C12!important;stroke:#D68910!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .mid tspan{fill:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .cert>*{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .cert span{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .cert tspan{fill:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .action>*{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .action span{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .action tspan{fill:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .output>*{fill:#9B59B6!important;stroke:#8E44AD!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .output span{fill:#9B59B6!important;stroke:#8E44AD!important;color:#fff!important;}#mermaid-svg-b2JWBE26tbrReXpN .output tspan{fill:#fff!important;} 华为根CA
华为中间CA
开发者发布证书
调试证书
Profile签名证书
应用签名
调试签名
Profile签发
发布HAP
调试HAP
签名Profile .p7b

证书类型详解

HarmonyOS涉及四种证书/密钥文件,各有用途:

类型 文件格式 用途 有效期 签发方
发布证书 .cer 证明开发者身份 1-3年 华为CA
调试证书 .cer 开发调试用 1年 华为CA
密钥库 .p12 存放私钥,用于签名 无限制 开发者自生成
签名Profile .p7b 包含包名、权限、设备列表 1年 华为AGC

关键理解.cer是公钥证书,证明"你是谁";.p12是私钥,用来"签名";.p7b是Profile,定义"你能干什么"。

证书申请完整流程

#mermaid-svg-BEORIkG3AIdGqz8S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BEORIkG3AIdGqz8S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BEORIkG3AIdGqz8S .error-icon{fill:#552222;}#mermaid-svg-BEORIkG3AIdGqz8S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BEORIkG3AIdGqz8S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BEORIkG3AIdGqz8S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BEORIkG3AIdGqz8S .marker.cross{stroke:#333333;}#mermaid-svg-BEORIkG3AIdGqz8S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BEORIkG3AIdGqz8S p{margin:0;}#mermaid-svg-BEORIkG3AIdGqz8S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BEORIkG3AIdGqz8S .cluster-label text{fill:#333;}#mermaid-svg-BEORIkG3AIdGqz8S .cluster-label span{color:#333;}#mermaid-svg-BEORIkG3AIdGqz8S .cluster-label span p{background-color:transparent;}#mermaid-svg-BEORIkG3AIdGqz8S .label text,#mermaid-svg-BEORIkG3AIdGqz8S span{fill:#333;color:#333;}#mermaid-svg-BEORIkG3AIdGqz8S .node rect,#mermaid-svg-BEORIkG3AIdGqz8S .node circle,#mermaid-svg-BEORIkG3AIdGqz8S .node ellipse,#mermaid-svg-BEORIkG3AIdGqz8S .node polygon,#mermaid-svg-BEORIkG3AIdGqz8S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BEORIkG3AIdGqz8S .rough-node .label text,#mermaid-svg-BEORIkG3AIdGqz8S .node .label text,#mermaid-svg-BEORIkG3AIdGqz8S .image-shape .label,#mermaid-svg-BEORIkG3AIdGqz8S .icon-shape .label{text-anchor:middle;}#mermaid-svg-BEORIkG3AIdGqz8S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BEORIkG3AIdGqz8S .rough-node .label,#mermaid-svg-BEORIkG3AIdGqz8S .node .label,#mermaid-svg-BEORIkG3AIdGqz8S .image-shape .label,#mermaid-svg-BEORIkG3AIdGqz8S .icon-shape .label{text-align:center;}#mermaid-svg-BEORIkG3AIdGqz8S .node.clickable{cursor:pointer;}#mermaid-svg-BEORIkG3AIdGqz8S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BEORIkG3AIdGqz8S .arrowheadPath{fill:#333333;}#mermaid-svg-BEORIkG3AIdGqz8S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BEORIkG3AIdGqz8S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BEORIkG3AIdGqz8S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BEORIkG3AIdGqz8S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BEORIkG3AIdGqz8S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BEORIkG3AIdGqz8S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BEORIkG3AIdGqz8S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BEORIkG3AIdGqz8S .cluster text{fill:#333;}#mermaid-svg-BEORIkG3AIdGqz8S .cluster span{color:#333;}#mermaid-svg-BEORIkG3AIdGqz8S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BEORIkG3AIdGqz8S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BEORIkG3AIdGqz8S rect.text{fill:none;stroke-width:0;}#mermaid-svg-BEORIkG3AIdGqz8S .icon-shape,#mermaid-svg-BEORIkG3AIdGqz8S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BEORIkG3AIdGqz8S .icon-shape p,#mermaid-svg-BEORIkG3AIdGqz8S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BEORIkG3AIdGqz8S .icon-shape .label rect,#mermaid-svg-BEORIkG3AIdGqz8S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BEORIkG3AIdGqz8S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BEORIkG3AIdGqz8S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BEORIkG3AIdGqz8S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-BEORIkG3AIdGqz8S .start>*{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .start span{fill:#2ECC71!important;stroke:#25A55A!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .start tspan{fill:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .process>*{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .process span{fill:#4A90D9!important;stroke:#2C5F8A!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .process tspan{fill:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .decision>*{fill:#F39C12!important;stroke:#D68910!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .decision span{fill:#F39C12!important;stroke:#D68910!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .decision tspan{fill:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .error>*{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .error span{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .error tspan{fill:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .output>*{fill:#9B59B6!important;stroke:#8E44AD!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .output span{fill:#9B59B6!important;stroke:#8E44AD!important;color:#fff!important;}#mermaid-svg-BEORIkG3AIdGqz8S .output tspan{fill:#fff!important;} 是

生成密钥对 .p12
生成证书请求 .csr
登录华为开发者联盟
提交CSR申请证书
审核通过?
下载证书 .cer
修改申请信息
登录AGC创建应用
上传证书配置Profile
下载Profile .p7b
配置签名信息到工程
完成! 可以打包签名

代码实战

基础用法:证书申请全流程

第一步:生成密钥对和CSR

bash 复制代码
# 1. 生成RSA 2048位私钥
openssl genrsa -out private_key.pem 2048

# 2. 生成证书签名请求(CSR)
openssl req -new -key private_key.pem -out cert_request.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=YourCompany/OU=MobileDev/CN=YourApp"

# 3. 将私钥导出为PKCS12格式(.p12文件)
openssl pkcs12 -export -out YourApp_Release.p12 \
  -inkey private_key.pem \
  -name "release_key" \
  -passout pass:YourStrongPassword123!

# 4. 验证.p12文件
openssl pkcs12 -in YourApp_Release.p12 -noout -passin pass:YourStrongPassword123!

第二步:在华为开发者联盟申请证书

登录 https://developer.huawei.com → 证书管理 → 新增证书:

  • 证书类型:选择"发布证书"或"调试证书"
  • 上传CSR文件:选择刚才生成的cert_request.csr
  • 提交审核

审核通过后下载.cer证书文件。

第三步:在AGC创建应用并下载Profile

登录 https://agc.huawei.com → 我的应用 → 创建项目 → 添加应用:

  • 填写应用包名(必须和module.json5中的包名一致)
  • 选择签名证书(上传刚才下载的.cer文件)
  • 配置调试设备(如果是调试Profile,需要添加设备UDID)
  • 下载Profile文件(.p7b)

第四步:配置到工程中

typescript 复制代码
// build-profile.json5
{
  "app": {
    "signingConfigs": [
      {
        "name": "release",
        "type": "HarmonyOS",
        "material": {
          "certpath": "signing/YourApp_Release.cer",
          "storeFile": "signing/YourApp_Release.p12",
          "storePassword": "YourStrongPassword123!",
          "keyAlias": "release_key",
          "keyPassword": "YourStrongPassword123!",
          "profile": "signing/YourApp_Release.p7b",
          "signAlg": "SHA256withECDSA"
        }
      }
    ],
    "products": [
      {
        "name": "default",
        "signingConfig": "release"
      }
    ]
  }
}

进阶用法:证书续期与更新

证书过期了怎么办?不能直接"续期"旧证书,而是要重新申请。但好消息是,不需要重新生成密钥对------用同一个.p12文件生成新的CSR就行。

typescript 复制代码
// scripts/cert-renew.ets
// 证书续期辅助工具

import { X509Certificate } from 'crypto';

interface CertInfo {
  subject: string;          // 证书主题
  issuer: string;           // 签发者
  serialNumber: string;     // 序列号
  notBefore: Date;          // 生效时间
  notAfter: Date;           // 过期时间
  fingerprint: string;      // 指纹
  isExpired: boolean;       // 是否已过期
  daysRemaining: number;    // 剩余天数
}

// 解析证书信息
function parseCertificate(cerPath: string): CertInfo | null {
  try {
    const certData = fs.readFileSync(cerPath, 'utf-8');
    const cert = new X509Certificate(certData);

    const now = new Date();
    const notAfter = new Date(cert.validity.notAfter);
    const daysRemaining = Math.floor(
      (notAfter.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
    );

    return {
      subject: cert.subject.toString(),
      issuer: cert.issuer.toString(),
      serialNumber: cert.serialNumber,
      notBefore: new Date(cert.validity.notBefore),
      notAfter: notAfter,
      fingerprint: cert.fingerprint256,
      isExpired: daysRemaining <= 0,
      daysRemaining: daysRemaining
    };
  } catch (error) {
    console.error(`解析证书失败: ${error}`);
    return null;
  }
}

// 批量检查所有证书
function checkAllCertificates(signingDir: string): void {
  const cerFiles = fs.readdirSync(signingDir)
    .filter(f => f.endsWith('.cer'));

  console.log('===== 证书状态检查 =====\n');

  for (const cer of cerFiles) {
    const info = parseCertificate(path.join(signingDir, cer));
    if (!info) continue;

    const status = info.isExpired ? '❌ 已过期' :
      info.daysRemaining <= 30 ? '⚠️ 即将过期' : '✅ 正常';

    console.log(`[${cer}]`);
    console.log(`  状态: ${status}`);
    console.log(`  过期时间: ${info.notAfter.toLocaleDateString()}`);
    console.log(`  剩余天数: ${info.daysRemaining}`);
    console.log(`  指纹: ${info.fingerprint.substring(0, 32)}...`);
    console.log('');
  }
}

// 生成续期CSR(使用现有密钥对)
function generateRenewalCSR(
  p12Path: string,
  p12Password: string,
  outputPath: string
): boolean {
  try {
    // 从.p12中提取私钥
    const cmd = `openssl pkcs12 -in "${p12Path}" -nocerts -nodes ` +
      `-passin pass:${p12Password} -out temp_key.pem`;
    execSync(cmd);

    // 用现有私钥生成新CSR
    const csrCmd = `openssl req -new -key temp_key.pem ` +
      `-out "${outputPath}" ` +
      `-subj "/C=CN/ST=Beijing/L=Beijing/O=YourCompany/OU=Dev/CN=YourApp"`;
    execSync(csrCmd);

    // 清理临时文件
    fs.unlinkSync('temp_key.pem');

    console.log(`✅ 续期CSR已生成: ${outputPath}`);
    return true;
  } catch (error) {
    console.error(`❌ 生成CSR失败: ${error}`);
    return false;
  }
}

证书更新后的迁移流程:

bash 复制代码
# 1. 用新证书替换旧证书
cp NewApp_Release.cer signing/YourApp_Release.cer

# 2. 用新证书重新生成Profile
# 登录AGC → 应用信息 → 签名配置 → 更新证书 → 重新下载Profile

# 3. 替换Profile
cp NewApp_Release.p7b signing/YourApp_Release.p7b

# 4. 重新打包签名
hvigorw clean
hvigorw assembleHap --mode module -p module=entry@default -p product=default

# 5. 验证新签名
# 安装到设备测试
hdc install entry/build/default/outputs/default/entry-default-signed.hap

完整示例:多团队证书管理

大型项目中,多个团队协作开发,证书管理是个大问题。怎么让各团队用各自的调试证书,但发布时统一用发布证书?

typescript 复制代码
// scripts/team-cert-manager.ets
// 多团队证书管理工具

interface TeamCertConfig {
  teamName: string;
  debugCert: {
    cerPath: string;
    p12Path: string;
    p12Password: string;    // 实际应从安全存储读取
    keyAlias: string;
    profilePath: string;
  };
  releaseCert: {
    cerPath: string;        // 所有团队共用发布证书
    p12Path: string;
    p12Password: string;    // 只有CI环境有权限访问
    keyAlias: string;
    profilePath: string;
  };
}

// 团队配置
const teamConfigs: TeamCertConfig[] = [
  {
    teamName: 'team_payment',
    debugCert: {
      cerPath: 'signing/teams/payment/debug.cer',
      p12Path: 'signing/teams/payment/debug.p12',
      p12Password: '${PAYMENT_DEBUG_PWD}',
      keyAlias: 'payment_debug',
      profilePath: 'signing/teams/payment/debug.p7b'
    },
    releaseCert: {
      cerPath: 'signing/release/release.cer',
      p12Path: 'signing/release/release.p12',
      p12Password: '${RELEASE_PWD}',
      keyAlias: 'release_key',
      profilePath: 'signing/release/release.p7b'
    }
  },
  {
    teamName: 'team_social',
    debugCert: {
      cerPath: 'signing/teams/social/debug.cer',
      p12Path: 'signing/teams/social/debug.p12',
      p12Password: '${SOCIAL_DEBUG_PWD}',
      keyAlias: 'social_debug',
      profilePath: 'signing/teams/social/debug.p7b'
    },
    releaseCert: {
      cerPath: 'signing/release/release.cer',
      p12Path: 'signing/release/release.p12',
      p12Password: '${RELEASE_PWD}',
      keyAlias: 'release_key',
      profilePath: 'signing/release/release.p7b'
    }
  }
];

// 根据团队和构建类型生成签名配置
function generateSigningConfig(
  teamName: string,
  buildType: 'debug' | 'release'
): object {
  const config = teamConfigs.find(t => t.teamName === teamName);
  if (!config) {
    throw new Error(`未找到团队配置: ${teamName}`);
  }

  const cert = buildType === 'debug' ? config.debugCert : config.releaseCert;

  return {
    name: `${teamName}_${buildType}`,
    type: 'HarmonyOS',
    material: {
      certpath: cert.cerPath,
      storeFile: cert.p12Path,
      storePassword: cert.p12Password,
      keyAlias: cert.keyAlias,
      keyPassword: cert.p12Password,
      profile: cert.profilePath,
      signAlg: 'SHA256withECDSA'
    }
  };
}

// 生成完整的build-profile.json5
function generateBuildProfile(): object {
  const signingConfigs: object[] = [];
  const products: object[] = [];

  for (const team of teamConfigs) {
    // 调试签名配置
    signingConfigs.push(generateSigningConfig(team.teamName, 'debug'));
    // 发布签名配置
    signingConfigs.push(generateSigningConfig(team.teamName, 'release'));

    // 对应的构建产物
    products.push({
      name: `${team.teamName}_debug`,
      signingConfig: `${team.teamName}_debug`
    });
    products.push({
      name: `${team.teamName}_release`,
      signingConfig: `${team.teamName}_release`
    });
  }

  return {
    app: {
      signingConfigs,
      products
    }
  };
}

CI环境中的证书安全处理:

bash 复制代码
#!/bin/bash
# ci-setup-signing.sh - CI环境签名配置脚本

# 从CI环境变量读取签名信息(不要硬编码!)
RELEASE_P12_PATH=${CI_RELEASE_P12_PATH:-""}
RELEASE_P12_PWD=${CI_RELEASE_P12_PWD:-""}
RELEASE_CER_PATH=${CI_RELEASE_CER_PATH:-""}
RELEASE_PROFILE_PATH=${CI_RELEASE_PROFILE_PATH:-""}

# 验证必要的环境变量
if [[ -z "$RELEASE_P12_PATH" || -z "$RELEASE_P12_PWD" ]]; then
  echo "❌ 缺少发布签名环境变量"
  exit 1
fi

# 将签名文件复制到工程目录(CI的临时工作空间)
mkdir -p signing/release
cp "$RELEASE_P12_PATH" signing/release/release.p12
cp "$RELEASE_CER_PATH" signing/release/release.cer
cp "$RELEASE_PROFILE_PATH" signing/release/release.p7b

# 写入签名配置(使用环境变量替换密码)
cat > signing/release/config.json << EOF
{
  "storePassword": "$RELEASE_P12_PWD",
  "keyPassword": "$RELEASE_P12_PWD"
}
EOF

echo "✅ 签名配置完成"

踩坑与注意事项

坑1:CSR信息与开发者账号不一致

申请证书时,CSR中的组织信息(/O=字段)必须与华为开发者账号的实名认证信息一致。不一致的话,审核会被拒绝。

解法 :生成CSR时,-subj参数中的/O=填写开发者账号认证的公司名或个人名。不确定的话,先登录开发者联盟查看认证信息。

坑2:调试证书设备数量限制

一个调试证书最多注册100台设备。如果团队设备多,或者频繁更换设备,很容易超限。

解法

  • 定期清理不再使用的设备
  • 团队共享调试设备,避免每人注册多台
  • 使用无线调试减少物理设备依赖

坑3:证书更新后旧包无法覆盖安装

证书更新后,新签名的HAP和旧签名的HAP使用不同证书,无法直接覆盖安装。必须先卸载旧版,再安装新版------这意味着用户数据会丢失。

解法 :这是签名体系的设计限制,无法绕过。所以证书一定要在过期前续期 ,不要等到过期了才处理。续期时使用同一个密钥对(.p12文件),新证书和旧证书的公钥相同,签名验证可以兼容。

坑4:Profile中的包名与工程不一致

AGC创建应用时填的包名,必须和AppScope/app.json5中的bundleName完全一致。哪怕差一个字母,签名Profile都无法使用。

解法 :创建AGC应用时,直接从app.json5中复制包名,不要手动输入。

坑5:多团队证书冲突

多个团队各自申请调试证书,但发布时需要统一签名。如果各团队的调试Profile中设备列表不同,可能导致某些设备只能装某些团队的包。

解法:发布签名统一管理,只有CI环境有权访问发布密钥。各团队各自管理调试签名,互不干扰。调试阶段各团队用各自的调试证书,发布阶段统一走CI用发布证书签名。

HarmonyOS 6适配说明

HarmonyOS 6对证书管理做了以下改进:

  1. 证书自动续期:AGC支持证书自动续期功能。开启后,证书到期前30天自动续签,无需手动操作。目前仅支持发布证书。

  2. 多证书绑定:一个应用可以绑定多个发布证书,用于密钥轮换场景。新证书生效后,旧证书签名的HAP仍可正常安装和更新,直到旧证书过期。

  3. 企业证书体系:HarmonyOS 6新增企业级证书管理,支持企业内部分发应用。企业证书签名的应用可以不经过应用市场,在企业内部直接安装。

  4. 证书透明性强制:所有新申请的发布证书必须记录到CT(Certificate Transparency)日志,防止证书被恶意签发。这是安全增强,开发者无需额外操作。

  5. AGC托管密钥:发布密钥可以完全托管在AGC,开发者不再持有私钥。打包时由AGC远程签名,安全性更高。适合对密钥管理要求严格的企业。

总结

证书管理是"不出事觉得不重要,出了事就是大事"的典型。证书过期、密钥泄露、签名不一致------任何一个问题都能让应用停摆。

核心建议:

  • 证书续期要提前,至少提前30天处理
  • 密钥安全是底线,.p12文件和密码绝对不能泄露
  • 多团队统一发布签名,调试签名各管各的
  • 自动化检查证书状态,别靠人肉记忆