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对证书管理做了以下改进:
-
证书自动续期:AGC支持证书自动续期功能。开启后,证书到期前30天自动续签,无需手动操作。目前仅支持发布证书。
-
多证书绑定:一个应用可以绑定多个发布证书,用于密钥轮换场景。新证书生效后,旧证书签名的HAP仍可正常安装和更新,直到旧证书过期。
-
企业证书体系:HarmonyOS 6新增企业级证书管理,支持企业内部分发应用。企业证书签名的应用可以不经过应用市场,在企业内部直接安装。
-
证书透明性强制:所有新申请的发布证书必须记录到CT(Certificate Transparency)日志,防止证书被恶意签发。这是安全增强,开发者无需额外操作。
-
AGC托管密钥:发布密钥可以完全托管在AGC,开发者不再持有私钥。打包时由AGC远程签名,安全性更高。适合对密钥管理要求严格的企业。
总结
证书管理是"不出事觉得不重要,出了事就是大事"的典型。证书过期、密钥泄露、签名不一致------任何一个问题都能让应用停摆。
核心建议:
- 证书续期要提前,至少提前30天处理
- 密钥安全是底线,.p12文件和密码绝对不能泄露
- 多团队统一发布签名,调试签名各管各的
- 自动化检查证书状态,别靠人肉记忆