2. 私钥文件加密
本节介绍长安链私钥文件的口令加密保护:如何用 openssl 与 cmc 完成加解密,以及加密后的私钥在 chainmaker.yml、sdk-go、sdk-java、cmc 中的配置与使用(含多签场景)。
本章是私钥文件加密的统一说明。各组件配置项的完整清单另见 长安链配置管理、Go SDK 使用说明、Java SDK 使用说明。
2.1. 快速开始
以客户端签名私钥为例,加密并投入使用只需几步。动手前请先读一遍 前置条件:
# 0. 先备份明文私钥!口令一旦丢失,加密私钥无法恢复
cp ./client1.sign.key ./client1.sign.key.bak # 备份请另存到安全介质,勿与口令放在一起
# 1. 加密私钥(AES-256-CBC,与 openssl 互通)
cmc crypto pem-encrypt -k ./client1.sign.key -p 'YourP@ssw0rd' -a AES -o ./client1.sign.key.enc
mv ./client1.sign.key.enc ./client1.sign.key
# 2. 启动前自检:确认口令与文件匹配
cmc crypto pem-decrypt -k ./client1.sign.key -p 'YourP@ssw0rd' --out-string | head -1
# 输出 -----BEGIN ... PRIVATE KEY----- 即为正确
# 3. 在配置文件中填入口令,务必用单引号包裹
# sdk_config.yml
chain_client:
user_sign_key_pwd: 'YourP@ssw0rd'
# chainmaker.yml(节点侧)
node:
priv_key_password: 'YourP@ssw0rd'
最容易踩的坑:
完整的端到端流程(含备份、停机、回滚)见 完整操作流程。
2.2. 功能简介
长安链的节点身份、TLS 连接、客户端交易签名都依赖私钥文件,chainmaker-cryptogen 生成的私钥文件默认明文落盘。
为降低私钥明文落盘的风险,长安链支持将私钥文件用口令(password / passphrase)加密后再落盘,运行时由节点或 SDK 使用配置的口令在内存中解密使用:
私钥文件采用 OpenSSL 传统加密 PEM 格式(PEM 头部带
Proc-Type: 4,ENCRYPTED与DEK-Info),可与openssl命令行互通;在此基础上,长安链扩展支持 SM4-CBC 算法,以满足国密场景;
加密是可选能力:不配置口令时行为与旧版本完全一致,存量明文私钥无需任何改动。
2.3. 前置条件
2.3.1. 版本要求
私钥文件加密相关能力需按组件版本确认:
长安链节点、
cmc的私钥 PEM 加密与解密、TLS 私钥口令(net.tls.*、rpc.tls.*)、cmc crypto命令族、cmc管理员私钥--encrypt/--password,以及chainmaker.yml私钥口令的env:<VAR>注入,自长安链v2.3.10起支持;sdk-go带口令的背书辅助函数,以及sdk-go对 SM4-CBC 加密私钥的解密,自sdk-go v2.3.12起提供;sdk-java对 SM4-CBC 加密私钥的解密,自sdk-java v2.3.9起提供。
更早版本请先升级。
2.3.2. 动手前的确认清单
开始加密之前,请逐项确认:
确认对应组件满足上面的版本要求,不要直接将长安链节点版本号作为 SDK 的版本要求。
确认私钥文件是 PEM 格式:
head -1 私钥文件应输出-----BEGIN ... PRIVATE KEY-----。若是一行十六进制字符串,说明是 secp256k1 或 dilithium2,不支持加密,详见 适用范围。确认未启用加密机 / KMS:
node.pkcs11.enabled或node.kms.enabled为true时私钥由外部托管,请改用硬件加密方案。已离线备份明文私钥,且备份与口令分开存放。口令一旦丢失,加密私钥无法恢复。
2.4. 适用范围
2.4.1. 支持的账户模式与密钥类型
私钥文件加密对 Cert 模式、PWK 模式、PK 模式均适用,判定标准是私钥文件本身是否为 PEM 格式。
| 账户模式 | auth_type |
密钥算法 / 曲线 | 私钥文件格式 | 是否支持加密 |
|---|---|---|---|---|
| Cert 模式 | permissionedWithCert |
SM2、ECC_P256 / P384 / P521、RSA | PEM | ✅ |
| PWK 模式 | permissionedWithKey |
SM2、ECC_P256 / P384 / P521、RSA | PEM | ✅ |
| PK 模式 | public |
SM2、ECC_P256 / P384 / P521、RSA | PEM | ✅ |
| PK 模式 | public |
ECC_Secp256k1(k1 曲线) | 32 字节裸私钥的十六进制文本 | ❌ |
| Cert 模式 | permissionedWithCert |
DILITHIUM2(后量子算法) | 裸私钥的十六进制文本 | ❌ |
表中的算法名对应
chainmaker-cryptogen配置文件中的pk_algo。ecc_p384/ecc_p521虽未在配置模板的注释示例中列出,但同样受支持且可加密。
支持加密的私钥文件:
节点签名私钥(
node.priv_key_file);P2P 网络 TLS 私钥、国密双证书加密私钥(
net.tls.priv_key_file、net.tls.priv_enc_key_file);RPC 服务 TLS 私钥、国密双证书加密私钥(
rpc.tls.priv_key_file、rpc.tls.priv_enc_key_file);SDK / cmc 客户端的交易签名私钥、TLS 私钥、国密加密私钥(
user_sign_key_*、user_key_*、user_enc_key_*)。
SDK 支持范围:目前由 sdk-go 与 sdk-java 提供支持(见下文各自章节)。Node.js、Python、Web3.js 等其他语言 SDK 暂未提供私钥口令配置项,使用这些 SDK 的业务请先确认对应版本是否支持。
2.4.2. 支持的加密格式与算法
长安链节点、sdk-go 和 cmc 仅支持 OpenSSL 传统加密 PEM 格式,即 PEM 块头部形如:
-----BEGIN EC PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,b07b87882f18b06e0a28e69e6a78844a
...base64...
-----END EC PRIVATE KEY-----
紧跟在 -----BEGIN----- 之后的这两行,是判断“私钥是否加密、该用什么算法解密”的唯一依据:
| 头部行 | 含义 |
|---|---|
Proc-Type: 4,ENCRYPTED |
标记该 PEM 已加密 |
DEK-Info: AES-256-CBC,b07b...844a |
逗号前是加密算法名,逗号后是十六进制的初始化向量(IV)。DEK 即 Data Encryption Key |
算法(DEK-Info 中的算法名) |
加密(cmc) | 解密(节点 / sdk-go / cmc) | 解密(sdk-java) | 与 openssl 互通 |
|---|---|---|---|---|
AES-256-CBC |
✅(默认) | ✅ | ✅ | ✅ |
AES-192-CBC / AES-128-CBC |
— | ✅ | ✅ | ✅ |
DES-EDE3-CBC(3DES) |
— | ✅ | ✅ | ✅(已不安全,仅用于兼容存量文件) |
DES-CBC(单 DES) |
— | ✅ | ✅ | ⚠️ 见下方说明 |
SM4-CBC |
✅ | ✅ | ✅ | ✅(OpenSSL 3.x 实测,见下) |
关于 DES:仅用于兼容存量文件,请勿用 DES 加密新私钥(OpenSSL 3.x 的
openssl ec -des会静默输出未加密的私钥)。
重要:节点、
sdk-go和cmc不支持 PKCS#8 加密 PEM(文件头为-----BEGIN ENCRYPTED PRIVATE KEY-----),请统一使用 OpenSSL 传统加密格式。Java SDK 支持 PKCS#8 加密格式,但同一份私钥若还要给节点或 Go SDK 使用,仍建议统一采用传统格式。OpenSSL 3.x 中openssl rsa -aes256默认输出 PKCS#8,必须显式加-traditional,详见下文。
2.5. 使用 openssl 加解密私钥文件
以下示例中,明文私钥为 admin1.sign.key,口令为 YourP@ssw0rd。
2.5.1. ECC(含 SM2)私钥加密
openssl ec 默认就输出传统格式,直接加算法参数即可:
# 加密:AES-256-CBC
openssl ec -in ./admin1.sign.key -aes256 -passout pass:'YourP@ssw0rd' -out ./admin1.sign.enc.key
# 查看结果头部,确认为传统加密格式
head -3 ./admin1.sign.enc.key
# -----BEGIN EC PRIVATE KEY-----
# Proc-Type: 4,ENCRYPTED
# DEK-Info: AES-256-CBC,79CB6E3A27D48EFD2B76C79806EBB79E
2.5.2. RSA 私钥加密
OpenSSL 3.x 中 openssl rsa 默认输出 PKCS#8,必须加 -traditional:
# 输出传统加密格式,头部应有 Proc-Type 与 DEK-Info 两行
openssl rsa -in ./rsa.key -traditional -aes256 -passout pass:'YourP@ssw0rd' -out ./rsa.enc.key
head -3 ./rsa.enc.key
# -----BEGIN RSA PRIVATE KEY-----
# Proc-Type: 4,ENCRYPTED
# DEK-Info: AES-256-CBC,8616A8914F923E43C5984C6628019223
2.5.3. 解密与校验
# 解密回明文
openssl ec -in ./admin1.sign.enc.key -passin pass:'YourP@ssw0rd' -out ./admin1.sign.plain.key
# 校验:比对加密前后导出的公钥是否一致
openssl ec -in ./admin1.sign.key -pubout
openssl ec -in ./admin1.sign.plain.key -pubout
2.5.4. 口令的安全传入方式
生产环境建议使用以下方式传入:
# 从文件读取(文件首行为口令,读取后自动去掉行尾换行)
openssl ec -in ./admin1.sign.key -aes256 -passout file:./pw.txt -out ./admin1.sign.enc.key
# 从环境变量读取
export CM_KEY_PWD='YourP@ssw0rd'
openssl ec -in ./admin1.sign.key -aes256 -passout env:CM_KEY_PWD -out ./admin1.sign.enc.key
# 交互式输入(不加 -passout,openssl 会提示输入并隐藏回显)
openssl ec -in ./admin1.sign.key -aes256 -out ./admin1.sign.enc.key
⚠️ 口令文件必须用 LF(Unix)换行保存:openssl 的
file:方式只去掉行尾\n,CRLF 文件行尾的\r会混入口令。检查与转换:od -c ./pw.txt | tail -2 # 出现 \r \n 即为 CRLF,需转换 tr -d '\r' < ./pw.txt > ./pw.lf.txt && mv ./pw.lf.txt ./pw.txt
openssl 的
file:只去行尾\n,cmc 的--password-file去首尾全部空白,同一个口令文件在两个工具间不可互换;口令含首尾空格时请用
-p直接传入(--password-file会去掉首尾空白);
cmc crypto pem-encrypt/pem-decrypt没有--password-file参数,口令通过-p传入或省略后交互式输入。
2.5.5. SM2 私钥的特别说明
chainmaker-cryptogen / cmc key gen 生成的 SM2 私钥是 PKCS#8 明文 PEM(-----BEGIN PRIVATE KEY-----)。用 OpenSSL 3.x 对其加密时,输出的 PEM 类型会变成 -----BEGIN SM2 PRIVATE KEY-----:
openssl ec -in ./sm2.key -aes256 -passout pass:'YourP@ssw0rd' -out ./sm2.enc.key
head -1 ./sm2.enc.key
# -----BEGIN SM2 PRIVATE KEY-----
该 PEM 类型长安链节点、sdk-go、cmc 可以正常解密,但 sdk-java 不识别(报 unrecognised object: SM2 PRIVATE KEY)。
建议:SM2 私钥统一使用
cmc crypto pem-encrypt加密,其输出的 PEM 类型为EC PRIVATE KEY,Go / Java 双栈以及 openssl 均可正常处理。
2.6. 使用 cmc 加解密私钥文件(cmc crypto)
v2.3.10 起,cmc 提供 crypto 命令族:
cmc crypto
├── pem-encrypt # 私钥 PEM 加密(AES / SM4)
├── pem-decrypt # 私钥 PEM 解密
├── encrypt # 通用字符串/文件对称加密(PBE:AES / SM4)
└── decrypt # 通用字符串/文件对称解密
2.6.1. cmc crypto pem-encrypt
./cmc crypto pem-encrypt \
-k ./admin1.sign.key \
-p 'YourP@ssw0rd' \
-a AES \
-o ./admin1.sign.enc.key
# pem encrypt ok, output: ./admin1.sign.enc.key
| 参数 | 缩写 | 必填 | 说明 |
|---|---|---|---|
--key-file |
-k |
是 | 明文私钥 PEM 文件路径 |
--password |
-p |
否 | 加密口令,不允许为空;省略时交互式提示输入(隐藏回显),见 交互式加解密 |
--algo |
-a |
否 | 加密算法:AES(默认,AES-256-CBC)、SM4(SM4-CBC) |
--out-file |
-o |
否 | 输出文件路径,缺省输出到标准输出 |
--out-binary |
-O |
否 | 原样输出(默认行为) |
--out-string |
否 | 原样输出(与 --out-binary 等价) |
|
--out-hex |
否 | 以十六进制文本输出 | |
--out-base64 |
否 | 以 base64 文本输出 |
四个输出格式参数互斥。加密私钥 PEM 本身就是文本,因此
--out-binary与--out-string的实际输出完全相同;只有--out-hex/--out-base64会再做一层编码(用于嵌入其他系统,不能直接作为私钥文件使用)。落盘为可直接使用的私钥文件时保持默认即可;
cmc写出的文件权限为0600。
不同算法的输出对照:
# AES(与 openssl 互通)
./cmc crypto pem-encrypt -k ./sm2.key -p 'YourP@ssw0rd' -a AES -o ./sm2.aes.enc
# -----BEGIN EC PRIVATE KEY-----
# Proc-Type: 4,ENCRYPTED
# DEK-Info: AES-256-CBC,8fba7b5f50b33b011332c1a17e56c4e4
# SM4(SM4-CBC,国密场景)
./cmc crypto pem-encrypt -k ./sm2.key -p 'YourP@ssw0rd' -a SM4 -o ./sm2.sm4.enc
# -----BEGIN EC PRIVATE KEY-----
# Proc-Type: 4,ENCRYPTED
# DEK-Info: SM4-CBC,9160E1A77BC5091ED3A1A62AEF331830
-a SM4的互通性:SM4-CBC 加密的私钥 PEM 在节点、sdk-go、sdk-java、cmc与 OpenSSL 3.x 之间完全互通——OpenSSL 3.x 既能解密(openssl ec/openssl rsa/openssl pkey),也能生成(openssl ec -sm4、openssl rsa -traditional -sm4)。需要注意的是
cmc crypto pem-encrypt的命令行帮助把该算法标注为“SM2 only”,但实现中并无此限制,对 ECC_P256、RSA 私钥同样可用(Go / Java / OpenSSL 三方均验证通过)。建议按帮助文本的保守口径使用——即-a SM4只用于国密(SM2)场景,其他密钥类型用默认的-a AES。
2.6.2. cmc crypto pem-decrypt
./cmc crypto pem-decrypt \
-k ./admin1.sign.enc.key \
-p 'YourP@ssw0rd' \
-o ./admin1.sign.plain.key
# pem decrypt ok, output: ./admin1.sign.plain.key
参数与 pem-encrypt 一致,仅少了 --algo(算法从密文 PEM 的 DEK-Info 中自动识别)。可解密 cmc crypto pem-encrypt 与 openssl 生成的传统格式加密私钥。
2.6.3. pem-decrypt 输出格式的注意事项
pem-decrypt 输出 PEM 的类型标签沿用密文 PEM 的类型,内容按私钥自身的标准编码重新生成。明文私钥本身是 PKCS#8(如 SM2,以及 OpenSSL 3.x genrsa 的默认产物)时,往返后标签与编码会改变,以 SM2 为例:
| 文件 | PEM 类型 | DER 编码 |
|---|---|---|
| cryptogen 生成的 SM2 明文私钥 | PRIVATE KEY |
PKCS#8 |
cmc crypto pem-encrypt 加密后 |
EC PRIVATE KEY |
SEC1(加密) |
cmc crypto pem-decrypt 解密后 |
EC PRIVATE KEY |
PKCS#8 |
变格式后的私钥,长安链全链路(节点、sdk-go、sdk-java、cmc,包括作为 TLS 私钥加载)与 openssl ec 均可正常使用。
建议:加密前保留一份明文私钥的离线备份(存放于安全介质),需要明文时使用备份,不要依赖
pem-decrypt的往返产物。
2.6.4. 交互式加解密
cmc crypto 四个子命令均支持交互式输入:当口令(或待处理数据)未通过命令行参数传入时,命令会提示输入,并在终端(TTY)下关闭回显(不显示明文),提示信息写到 stderr、不污染 stdout 输出。
2.6.4.1. 私钥文件加解密(pem-encrypt / pem-decrypt)
-p/--password 省略时,交互式提示输入口令:
# 加密:未传 -p,进入交互式口令输入(隐藏回显)
./cmc crypto pem-encrypt -k ./admin1.sign.key -a AES -o ./admin1.sign.enc.key
# Enter password: ← 输入的字符不显示,按 Enter 结束
# pem encrypt ok, output: ./admin1.sign.enc.key
# 解密:同样支持交互式输入口令
./cmc crypto pem-decrypt -k ./admin1.sign.enc.key -o ./admin1.sign.plain.key
# Enter password:
交互式输入时若直接回车(空口令),命令会报错——私钥文件加解密不允许空口令。
2.6.4.2. 字符串信息加解密(encrypt / decrypt)
cmc crypto encrypt / decrypt 的口令与待处理数据均可省略,省略项进入交互式输入:
| 省略的入参 | 交互式提示 | 回显 |
|---|---|---|
口令(-p/--password-hex/--password-base64/--password-file 均未传) |
Enter password: |
隐藏 |
待处理数据(-s/-i/--data-hex/--data-base64 均未传) |
Enter data: |
隐藏 |
# 加密:口令与数据都不传,全部交互式输入(均隐藏回显)
./cmc crypto encrypt -a AES --out-base64
# Enter password:
# Enter data:
# uhG/0emY4UfMBbEoVXtJKmCg3WKECJy1WllKLya+JwquSv3ZIAXnFRYRYx7Z2Vd4
# 解密:密文通过参数传入(无需隐藏),仅口令交互式输入
./cmc crypto decrypt -a AES \
--data-base64 'uhG/0emY4UfMBbEoVXtJKmCg3WKECJy1WllKLya+JwquSv3ZIAXnFRYRYx7Z2Vd4' --out-string
# Enter password:
# mysql_pwd_123
非终端(管道 / 重定向)下的行为:口令回退为从标准输入读取一行;待处理数据(
encrypt/decrypt)回退为读取标准输入的全部内容。这在脚本中可通过管道传入,但需注意此时口令不再隐藏回显。
2.7. 口令使用注意事项
口令中的特殊字符是本功能最容易出错的地方。
2.7.1. 口令本身的要求
口令不能为空。
口令大小写敏感,含首尾空格时必须用单引号包裹(见下方转义小节)。
长安链对口令长度、字符集不做限制,可以包含空格与任意可见字符;建议长度不少于 12 位,混合大小写字母、数字与符号。
中文等非 ASCII 口令在
cmc、openssl、Java SDK 三方之间可稳定互通;口令需跨机器传递时,注意各终端的字符编码设置保持一致。
2.7.2. Shell 命令行中的转义
统一使用单引号包裹口令,内部所有字符原样传递,无需逐个转义:
# ✅ 单引号,内部所有字符原样传递
./cmc crypto pem-encrypt -k ./a.key -p 'a$b`c"d#e f!g' -o ./a.enc
单引号内反斜杠是普通字符(
'a\$b'读到的是a\$b而非a$b),不要在单引号里再加转义符。单引号的规则在 shell 与 YAML 中完全一致。
单引号唯一处理不了的字符就是单引号自身。口令中含单引号时,用 '\'' 的写法闭合再拼接(含义是:结束当前单引号串、插入一个转义的单引号、再开始新的单引号串):
# 目标口令: it's@Pass
./cmc crypto pem-encrypt -k ./a.key -p 'it'\''s@Pass' -o ./a.enc
openssl 的 -passin / -passout 同样建议用单引号包裹 pass: 后的内容;口令中含 : 不受影响(只有第一个 : 用于分隔前缀)。
安全提醒:命令行参数会被记录到 shell 历史文件,并可被同机其他用户通过
ps看到。生产环境请优先使用 openssl 的file:/env:/ 交互式输入,或使用 cmc 的交互式模式,并及时清理 shell 历史。
2.7.3. YAML 配置文件中的转义
chainmaker.yml 与 sdk_config.yml 中,不加引号的口令会被 YAML 解析器改写。各种写法的实际结果如下:
| YAML 中的写法 | 实际读到的口令 | 问题 |
|---|---|---|
user_sign_key_pwd: abc #def |
abc |
空格 + # 之后被当成行注释 |
user_sign_key_pwd: abc··(尾部有空格) |
abc |
尾部空格被丢弃 |
user_sign_key_pwd: 0x1F |
31 |
被解析成十六进制整数 |
user_sign_key_pwd: 0123456 |
42798 |
以 0 开头、各位都在 0~7 之间,被当成八进制整数(八进制 0123456 = 十进制 42798) |
user_sign_key_pwd: 0888user_sign_key_pwd: 012345678 |
88812345678 |
含 8/9,不是合法八进制,退回十进制解析,前导 0 被丢掉 |
user_sign_key_pwd: 2026-08-27 |
初始化直接报错 | 被解析成时间戳,反序列化到字符串字段时失败:expected type 'string', got unconvertible type 'time.Time' |
user_sign_key_pwd: 1.50 |
1.5 |
被解析成浮点数,尾随 0 丢失 |
user_sign_key_pwd: null |
空字符串 | 被解析成 null |
user_sign_key_pwd: &abc |
空字符串 | & 被当作 YAML 锚点 |
user_sign_key_pwd: !abc |
空字符串 | ! 被当作 YAML 标签 |
user_sign_key_pwd: a: b |
解析报错 | mapping values are not allowed in this context |
user_sign_key_pwd: *abc |
解析报错 | unknown anchor 'abc' referenced |
user_sign_key_pwd: %abc |
解析报错 | found character that cannot start any token |
user_sign_key_pwd: {abc / [abc / ,abc |
解析报错 | 被当作流式集合起始 |
口令以 > 或竖线开头(如 >abc) |
解析报错 | 被当作折叠 / 字面量块标量 |
规则:口令一律使用单引号包裹。
与 shell 同理,YAML 的单引号字符串不做任何转义处理,
$、`、\、#、*、&等一律按字面读取;唯一的规则是字符串内的单引号要写成两个连续单引号''。
chain_client:
# ✅ 推荐写法:单引号包裹
user_sign_key_pwd: 'a$b`c"d#e f!g'
# ✅ 口令中含单引号 it's@Pass ,写成两个单引号
# user_sign_key_pwd: 'it''s@Pass'
# ✅ 纯数字、以 0 开头、十六进制样式、日期样式的口令也必须加引号
# user_sign_key_pwd: '0x1F'
# user_sign_key_pwd: '0123456'
# user_sign_key_pwd: '20260827'
上表中所有“问题写法”,改用单引号包裹后均可原样读取。
2.7.4. 修改口令与轮换
修改口令没有专门命令,采用“先解密再加密”的方式:
./cmc crypto pem-decrypt -k ./a.enc -p 'OldP@ssw0rd' -o ./a.plain
./cmc crypto pem-encrypt -k ./a.plain -p 'NewP@ssw0rd' -a AES -o ./a.new.enc
shred -u ./a.plain 2>/dev/null || rm -f ./a.plain # 及时销毁中间明文
轮换口令后需同步更新所有引用该私钥的配置文件(chainmaker.yml、sdk_config.yml),并重启相应进程。
2.8. chainmaker.yml 中的配置
节点侧共有 5 个私钥口令配置项:
| 配置项 | 作用 |
|---|---|
node.priv_key_password |
节点签名私钥(node.priv_key_file)的口令 |
net.tls.priv_key_password |
P2P 网络 TLS 私钥(net.tls.priv_key_file)的口令 |
net.tls.priv_enc_key_password |
P2P 国密双证书加密私钥(net.tls.priv_enc_key_file)的口令,仅证书模式的 GMTLS1.1 双证书场景 |
rpc.tls.priv_key_password |
RPC 服务 TLS 私钥(rpc.tls.priv_key_file)的口令 |
rpc.tls.priv_enc_key_password |
RPC 国密双证书加密私钥(rpc.tls.priv_enc_key_file)的口令,仅 GMTLS1.1 双证书场景 |
这些配置项留空即表示对应私钥为明文。配置模板已把 5 个口令项都列出(个别 rpc.tls 口令项在部分模板中仍是注释状态),在配置文件里找不到某个口令项时,按上表的路径手动加上即可,效果相同。
注意 1:
node节点下的priv_enc_key_file(节点国密加密私钥)没有对应的口令配置项,该文件必须保持明文。注意 2(易踩坑):多个配置项会指向同一个私钥文件,且共用关系随账户模式而变,详见下方说明。
默认部署脚本生成的私钥文件共用关系如下:
| 模式 | node.priv_key_file |
net.tls.priv_key_file |
rpc.tls.priv_key_file |
|---|---|---|---|
| Cert | xxx.sign.key(独立) |
xxx.tls.key |
xxx.tls.key(与 net.tls 同一文件) |
| PK / PWK | nodeN.key |
nodeN.key(与 node 同一文件) |
nodeN.tls.key(独立) |
由此带来两条必须注意的规则:
指向同一文件的配置项,口令必须填相同的值。
加密 TLS 私钥时,请一次性把所有相关口令都填上(
rpc.tls.mode为disable时漏填rpc.tls口令不影响启动,启用twoway后才会暴露)。
2.8.1. Cert 模式配置示例
# Blockchain node settings
node:
org_id: wx-org1.chainmaker.org
# 节点签名私钥(已加密)
priv_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.sign.key
# 节点签名私钥口令(私钥未加密时留空)
priv_key_password: 'YourP@ssw0rd'
cert_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.sign.crt
net:
provider: LibP2P
listen_addr: /ip4/0.0.0.0/tcp/11301
tls:
enabled: true
priv_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.key
# P2P TLS 私钥口令
priv_key_password: 'YourP@ssw0rd'
cert_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.crt
# 国密双证书体系(GMTLS1.1)才需要
priv_enc_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.enc.key
priv_enc_key_password: 'YourP@ssw0rd'
cert_enc_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.enc.crt
rpc:
provider: grpc
port: 12301
tls:
mode: twoway
priv_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.key
# RPC TLS 私钥口令
priv_key_password: 'YourP@ssw0rd'
cert_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.crt
# 国密双证书体系(GMTLS1.1)才需要
priv_enc_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.enc.key
priv_enc_key_password: 'YourP@ssw0rd'
cert_enc_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.enc.crt
2.8.2. PK / PWK 模式配置示例
PK / PWK 模式的链上身份认证不使用证书,P2P 网络基于节点公私钥对建立连接;但 RPC 启用 TLS 时仍需要配置 TLS 私钥与证书。
PK 模式(prepare_pk.sh 生成,{node_pk_path} 被替换为组织目录名)。node 与 net.tls 引用同一个私钥文件 node1.key,口令必须一致:
node:
# 节点签名私钥(注意是节点自己的私钥,不是 admin / client 用户私钥)
priv_key_file: ../config/node1/node1.key
priv_key_password: 'YourP@ssw0rd'
net:
tls:
enabled: true
priv_key_file: ../config/node1/node1.key # 与 node.priv_key_file 是同一个文件
priv_key_password: 'YourP@ssw0rd' # 必须与 node.priv_key_password 相同
rpc:
tls:
mode: twoway
priv_key_file: ../config/node1/node1.tls.key # 这个才是独立的文件
priv_key_password: 'YourP@ssw0rd'
cert_file: ../config/node1/node1.tls.crt
# 国密双证书体系(GMTLS1.1)才需要
priv_enc_key_file: ../config/node1/node1.tls.enc.key
priv_enc_key_password: 'YourP@ssw0rd'
cert_enc_file: ../config/node1/node1.tls.enc.crt
# 双向 TLS 客户端根证书路径
client_root_ca_paths:
- ../config/node1/ca
PWK 模式(prepare_pwk.sh 生成,{node_pk_path} 与 {net_pk_path} 都被替换为 node/consensus1/consensus1)。node 与 net.tls 同样引用同一个私钥文件:
node:
org_id: wx-org1.chainmaker.org
priv_key_file: ../config/wx-org1.chainmaker.org/keys/node/consensus1/consensus1.key
priv_key_password: 'YourP@ssw0rd'
net:
tls:
enabled: true
# 与 node.priv_key_file 是同一个文件,口令必须相同
priv_key_file: ../config/wx-org1.chainmaker.org/keys/node/consensus1/consensus1.key
priv_key_password: 'YourP@ssw0rd'
以上路径以默认部署脚本产物为例;目录结构有调整时以实际
priv_key_file取值为准。节点段配置的必须是节点自身的私钥,admin/client用户私钥属于客户端侧,配置在sdk_config.yml中。
2.8.3. 口令通过环境变量注入
v2.3.10 起,上表 5 个口令项都支持 env:<环境变量名> 写法:节点启动时从对应环境变量读取真实口令,口令明文不必落在 chainmaker.yml 里。写法与 storage 下 password_encrypt.passphrase 一致,适用于 K8s Secret envFrom、systemd EnvironmentFile 等注入场景。
node:
priv_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.sign.key
# 从环境变量 CM_NODE_KEY_PWD 读取口令
priv_key_password: 'env:CM_NODE_KEY_PWD'
net:
tls:
priv_key_file: ../config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.tls.key
priv_key_password: 'env:CM_NET_TLS_KEY_PWD'
启动前注入环境变量:
export CM_NODE_KEY_PWD='YourP@ssw0rd'
export CM_NET_TLS_KEY_PWD='YourP@ssw0rd'
./chainmaker start -c ../config/wx-org1.chainmaker.org/chainmaker.yml
取值规则:
| 配置写法 | 结果 |
|---|---|
| 留空 | 按明文私钥处理(与旧版一致) |
'YourP@ssw0rd' |
直接作为口令 |
'env:CM_NODE_KEY_PWD' |
读取环境变量 CM_NODE_KEY_PWD 的值作为口令 |
'env:CM_NODE_KEY_PWD',但变量未设置 |
启动报错 config node.priv_key_password: env variable "CM_NODE_KEY_PWD" is not set,不会静默按明文处理 |
'env:CM_NODE_KEY_PWD',变量设为空串 |
读到空口令,等同于“未配置口令”;明文私钥可正常加载,加密私钥会报 missing password for encrypted PEM |
'env:'(变量名为空) |
启动报错 env variable name is empty |
注意 1:口令字面值不要以
env:开头,否则会被当成环境变量引用。若真实口令确实以env:开头,请改用环境变量方式间接配置,或更换口令。注意 2:口令解析发生在配置加载之后、任何私钥加载之前,
chainmaker start与chainmaker rebuild-dbs都已覆盖,因此rebuild-dbs同样可以用env:提供口令。注意 3(重要):
env:只对chainmaker.yml节点侧的这 5 个口令项生效。sdk_config.yml中的user_sign_key_pwd/user_key_pwd/user_enc_key_pwd不支持该语法,写成'env:XXX'会被当成字面口令而解密失败;客户端要从环境变量取口令,请用 代码方式配置。注意 4:环境变量对同机其他进程可见(Linux 下可通过
/proc/<pid>/environ读取),它比配置文件明文安全,但弱于加密机 / KMS。请配合 K8s Secret、权限收紧的 systemdEnvironmentFile(chmod 600)等机制使用。
2.8.4. 生效说明
口令项留空或删除时,按明文私钥处理,与旧版本行为一致;
私钥文件是明文但配置了口令时,节点侧会忽略该口令并正常启动(不报错);
口令错误时节点启动失败,核心错误为
fail to decrypt PEM: [x509: decryption password incorrect && ...];缺少口令时为missing password for encrypted PEM。出于安全考虑,错误信息中不会回显口令与私钥内容。执行
chainmaker rebuild-dbs等运维命令时,配置文件中的口令同样必须正确。
节点有 3 处独立的私钥加载点,通过日志前缀可以判断是哪一把私钥的口令配错了:
| 出错的私钥 | 日志中的特征 |
|---|---|
节点签名私钥node.priv_key_password |
init blockchain[chain1] failed, fail to initialize identity management service: [...]随后 chainmaker server init failed, init all blockchains fail |
P2P 网络 TLS 私钥net.tls.priv_key_password |
chainmaker server init failed, <解密错误>上一行会打印出错私钥的完整路径: load net tls key file path: /path/to/xxx.key,据此可直接定位 |
RPC 服务 TLS 私钥rpc.tls.priv_key_password |
new gRPC failed, GetTLSConfigWithOptions err: ...随后 rpc server init failed, ...。特征是它出现在 init chain maker server success! 之后——链本身已起来,只是 RPC 服务启动失败 |
与
crypto_engine的关系:crypto_engine不影响口令解密与加密私钥的生成、加载,无需额外调整。
PKCS#11 / KMS 场景:
node.pkcs11.enabled或node.kms.enabled为true时,私钥由加密机 / KMS 托管,不适用本文方案,请确保sdk_config.yml中的user_key_pwd/user_sign_key_pwd/user_enc_key_pwd全部留空或删除。参见 硬件加密。
2.9. sdk-go 中的配置与使用
2.9.1. sdk_config.yml 配置
sdk-go 支持 3 个私钥口令配置项:
| 配置项 | Option 函数 | 对应私钥 | 适用模式 |
|---|---|---|---|
user_sign_key_pwd |
WithUserSignKeyPwd |
交易签名私钥 user_sign_key_file_path |
Cert / PWK / PK |
user_key_pwd |
WithUserKeyPwd |
TLS 连接私钥 user_key_file_path |
Cert / PWK / PK |
user_enc_key_pwd |
WithUserEncKeyPwd |
国密双证书加密私钥 user_enc_key_file_path |
国密 GMTLS 双证书 |
Cert 模式示例(sdk_config.yml):
chain_client:
chain_id: "chain1"
org_id: "wx-org1.chainmaker.org"
# 客户端用户私钥路径(tls通信使用私钥)
user_key_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.key"
# 客户端用户私钥密码(无密码则不需要设置)
user_key_pwd: 'YourP@ssw0rd'
user_crt_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.crt"
# 客户端用户加密私钥路径(国密GMTLS双证书体系)
# user_enc_key_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.enc.key"
# 客户端用户加密私钥密码(无密码则不需要设置)
# user_enc_key_pwd: 'YourP@ssw0rd'
# 客户端用户加密证书路径(国密GMTLS双证书体系)
# user_enc_crt_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.enc.crt"
# 客户端用户交易签名私钥路径(不允许使用tls私钥)
user_sign_key_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.key"
# 客户端用户交易签名私钥密码(无密码则不需要设置)
user_sign_key_pwd: 'YourP@ssw0rd'
user_sign_crt_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.crt"
PK / PWK 模式必须配置交易签名私钥;开启 TLS 时,还需配置 TLS 私钥、口令与证书;使用 GMTLS 双证书时,再增加加密私钥、口令与加密证书。以下为 PK 模式示例:
chain_client:
chain_id: "chain1"
auth_type: public # PWK 模式为 permissionedWithKey
user_sign_key_file_path: "./testdata/crypto-config/node1/admin/admin1/admin1.key"
user_sign_key_pwd: 'YourP@ssw0rd'
# 开启 TLS 时配置
# user_key_file_path: "./testdata/crypto-config/node1/client-tls/client1/client1.tls.key"
# user_key_pwd: 'YourP@ssw0rd'
# user_crt_file_path: "./testdata/crypto-config/node1/client-tls/client1/client1.tls.crt"
# 国密 GMTLS 双证书体系时配置
# user_enc_key_file_path: "./testdata/crypto-config/node1/client-tls/client1/client1.tls.enc.key"
# user_enc_key_pwd: 'YourP@ssw0rd'
# user_enc_crt_file_path: "./testdata/crypto-config/node1/client-tls/client1/client1.tls.enc.crt"
crypto:
hash: SHA256
sdk_config.yml的口令项不支持env:<VAR>语法(详见 口令通过环境变量注入 注意 3):写成'env:XXX'会被当成字面口令,解密失败。客户端要从环境变量取口令时,请用下面的代码方式配置。
2.9.2. 代码方式配置
也可以不写在配置文件里,改由代码传入(例如从环境变量、配置中心、KMS 读取):
import (
"os"
sdk "chainmaker.org/chainmaker/sdk-go/v2"
)
cc, err := sdk.NewChainClient(
sdk.WithConfPath("./testdata/sdk_config.yml"),
// 私钥口令:代码方式配置优先级高于 sdk_config.yml
sdk.WithUserSignKeyPwd(os.Getenv("CM_SIGN_KEY_PWD")),
sdk.WithUserKeyPwd(os.Getenv("CM_TLS_KEY_PWD")),
// 国密双证书场景
// sdk.WithUserEncKeyPwd(os.Getenv("CM_ENC_KEY_PWD")),
)
if err != nil {
return err
}
defer cc.Stop()
优先级:
WithUserSignKeyPwd等 Option 若已设置非空值,则sdk_config.yml中的同名配置不再生效;Option 未设置时才回落到配置文件。
重要:口令只对“私钥文件路径”方式生效。 通过
WithUserSignKeyBytes/WithUserKeyBytes/WithUserEncKeyBytes传入私钥字节时口令不参与,加密的私钥字节请先解密再传入:import "chainmaker.org/chainmaker/common/v2/crypto/asym" plainPEM, err := asym.DecryptPrivateKeyPEM(encryptedPEM, []byte(keyPwd)) if err != nil { return err } cc, err := sdk.NewChainClient( sdk.WithUserSignKeyBytes(plainPEM), // 必须传明文 )
2.9.3. 普通调用
私钥口令只影响 ChainClient 的初始化过程:NewChainClient 内部读取私钥文件并用口令解密,明文私钥仅存在于内存中。初始化完成后,所有业务接口的调用方式与明文私钥完全一致,业务代码无需任何改动。
cc, err := sdk.NewChainClient(
sdk.WithConfPath("./testdata/sdk_config.yml"),
)
if err != nil {
return err
}
defer cc.Stop()
// 与明文私钥场景完全相同
resp, err := cc.InvokeContract("counter-go-1", "increase", "", nil, -1, true)
if err != nil {
return err
}
fmt.Printf("invoke contract resp: %+v\n", resp)
2.9.4. 多签调用
多签(合约管理、链配置变更、多签合约投票等)需要用多个管理员私钥分别对同一个 Payload 签名,生成 EndorsementEntry 列表。
sdk-go v2.3.12 起,背书辅助函数原生支持加密私钥,新增了 4 个带口令参数的函数:
| 新函数 | 对应的原函数 | 用途 |
|---|---|---|
MakeEndorserWithPathAndPassword |
MakeEndorserWithPath |
Cert 模式,按文件路径背书 |
MakePkEndorserWithPathAndPassword |
MakePkEndorserWithPath |
PK / PWK 模式,按文件路径背书 |
MakeEndorserWithPassword |
MakeEndorser |
私钥 PEM 已在内存中时背书 |
SignPayloadWithPathAndPassword |
SignPayloadWithPath |
只签名,不生成背书条目 |
加密私钥请改用带
AndPassword的版本;口令参数传nil表示私钥未加密。
2.9.4.1. Cert 模式
import (
"chainmaker.org/chainmaker/pb-go/v2/common"
sdkutils "chainmaker.org/chainmaker/sdk-go/v2/utils"
)
// 1. 构造 payload
payload, err := cc.CreateContractCreatePayload(contractName, version, byteCode, runtimeType, kvs)
if err != nil {
return err
}
// 2. 多个管理员分别背书(私钥均已加密)
admins := []struct{ key, pwd, crt string }{
{"./crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.key", "Org1@P@ssw0rd",
"./crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.crt"},
{"./crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.key", "Org2@P@ssw0rd",
"./crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.crt"},
{"./crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.sign.key", "Org3@P@ssw0rd",
"./crypto-config/wx-org3.chainmaker.org/user/admin1/admin1.sign.crt"},
}
endorsers := make([]*common.EndorsementEntry, 0, len(admins))
for _, a := range admins {
var pwd []byte // 明文私钥保持 nil
if a.pwd != "" {
pwd = []byte(a.pwd)
}
e, err := sdkutils.MakeEndorserWithPathAndPassword(a.key, a.crt, pwd, payload)
if err != nil {
return err
}
endorsers = append(endorsers, e)
}
// 3. 发送合约管理请求
resp, err := cc.SendContractManageRequest(payload, endorsers, -1, true)
口令错误时,两种模式的报错文案不同:Cert 模式直接返回底层错误 fail to decrypt PEM: [x509: decryption password incorrect && ...];PK / PWK 模式与 SignPayloadWithPathAndPassword 会再包一层,形如 parse private key failed (wrong password or invalid key), fail to decrypt PEM: [...]。含义相同,都按口令错误排查。
2.9.4.2. PK / PWK 模式
PK / PWK 模式下背书成员信息是公钥而不是证书,改用 MakePkEndorserWithPathAndPassword:
import (
"chainmaker.org/chainmaker/common/v2/crypto"
"chainmaker.org/chainmaker/pb-go/v2/common"
sdkutils "chainmaker.org/chainmaker/sdk-go/v2/utils"
)
// PK(public)模式下 orgId 传空字符串;PWK(permissionedWithKey)模式下传实际组织ID
e, err := sdkutils.MakePkEndorserWithPathAndPassword(
keyFilePath,
crypto.HASH_TYPE_SHA256, // 与链配置的 hash 一致,可用 cc.GetHashType() 取得
orgId,
[]byte(keyPwd), // 明文私钥传 nil
payload,
)
私钥 PEM 已经在内存中(例如从 KMS / 配置中心取回),用
MakeEndorserWithPassword(orgId, hashType, memberType, keyPem, memberInfo, password, payload); 只需要签名、不需要背书条目,用SignPayloadWithPathAndPassword(keyFilePath, crtFilePath, password, payload)。
2.9.4.3. 多签合约(MultiSignContract)
多签合约的发起与投票同样通过 EndorsementEntry 携带签名,替换背书构造函数即可:
// 发起多签请求(CreateMultiSignReqPayloadWithGasLimit 只返回 payload,无 error)
payload := cc.CreateMultiSignReqPayloadWithGasLimit(pairs, gasLimit)
resp, err := cc.MultiSignContractReq(payload, endorsers, -1, true)
// 多签投票:用另一个管理员的加密私钥对 payload 背书
voteEndorser, err := makeEndorserWithEncryptedKey(admin2Key, admin2Pwd, admin2Crt, payload)
if err != nil {
return err
}
resp, err = cc.MultiSignContractVote(payload, voteEndorser, true, -1, true)
投票通过后可能还需显式触发:若链配置中
vm.native.multisign.enable_manual_run为true,多签状态变为PASSED后不会自动执行目标交易,需再调用一次MultiSignContractTrig才会真正生效。该方法不接受背书参数,用调用方自身身份发起(自身私钥的口令仍由sdk_config.yml或WithUserSignKeyPwd提供):// 注意签名为 4 个参数:payload、timeout、limit、withSyncResult resp, err = cc.MultiSignContractTrig(payload, -1, &common.Limit{GasLimit: 100000}, true)用
cc.MultiSignContractQuery(payload.TxId)可查看当前状态与已投票数。
投票返回
CONTRACT_FAIL且ContractResult.Message为the status of multiSignInfo is not `PROCESSING`时,与私钥加密无关:说明该多签请求已经不在PROCESSING状态——常见于目标资源的权限规则是ANY,第一票投完即PASSED,后续管理员再投就会被合约拒绝。需要多个管理员真正各投一票时,请把该资源的规则配成MAJORITY/ALL等。
2.10. sdk-java 中的配置与使用
2.10.1. sdk_config_tls.yml 配置
sdk-java 支持与 sdk-go 完全一致的 3 个口令配置项。注意 sdk-java 仓库中提供的示例配置文件名为 sdk_config_tls.yml(另有 sdk_config_pk_tls.yml、sdk_config_pwk.yml),没有 sdk_config.yml:
| 配置项 | 对应私钥 | Java Setter |
|---|---|---|
user_sign_key_pwd |
交易签名私钥 | ChainClientConfig#setUserSignKeyPwd |
user_key_pwd |
TLS 连接私钥 | ChainClientConfig#setUserKeyPwd |
user_enc_key_pwd |
国密双证书加密私钥 | ChainClientConfig#setUserEncKeyPwd |
chain_client:
chain_id: "chain1"
org_id: "wx-org1.chainmaker.org"
# 客户端用户私钥路径(tls通信使用私钥)
user_key_file_path: "src/test/resources/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.key"
# 客户端用户私钥密码(无密码则不需要设置)
user_key_pwd: 'YourP@ssw0rd'
user_crt_file_path: "src/test/resources/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.crt"
# 客户端用户加密私钥密码(国密GMTLS双证书体系,无密码则不需要设置)
# user_enc_key_pwd: 'YourP@ssw0rd'
# 客户端用户交易签名私钥路径(不允许使用tls私钥)
user_sign_key_file_path: "src/test/resources/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.key"
# 客户端用户交易签名私钥密码(无密码则不需要设置)
user_sign_key_pwd: 'YourP@ssw0rd'
user_sign_crt_file_path: "src/test/resources/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.crt"
2.10.2. 代码方式配置
ChainClientConfig chainClientConfig = sdkConfig.getChainClient();
// 从环境变量 / 配置中心读取口令,避免明文写在配置文件中
chainClientConfig.setUserSignKeyPwd(System.getenv("CM_SIGN_KEY_PWD"));
chainClientConfig.setUserKeyPwd(System.getenv("CM_TLS_KEY_PWD"));
// 国密双证书场景
// chainClientConfig.setUserEncKeyPwd(System.getenv("CM_ENC_KEY_PWD"));
ChainManager chainManager = ChainManager.getInstance();
ChainClient chainClient = chainManager.createChainClient(sdkConfig);
重要:口令只对“私钥文件路径”方式生效。
与
sdk-go相同,sdk-java也是字节方式优先于文件路径:一旦调用setUserSignKeyBytes(...)/setUserKeyBytes(...)/setUserEncKeyBytes(...)传入私钥字节,SDK 不再读取文件,口令也不参与。若持有的是加密私钥字节,请先解密再传入:
byte[] plainPEM = CryptoUtils.decryptPrivKeyPem(encryptedPEM, keyPwd); chainClientConfig.setUserSignKeyBytes(plainPEM); // 必须传明文
2.10.3. 普通调用
与 sdk-go 一致,口令仅作用于 createChainClient 阶段。客户端创建成功后,所有业务接口调用方式不变:
ResultOuterClass.TxResponse responseInfo =
chainClient.invokeContract(CONTRACT_NAME, "increase", null, params,
rpcCallTimeout, syncResultTimeout);
与 sdk-go 的行为差异:当私钥文件未加密但配置了口令时,
sdk-java会明确抛出异常Private key file is not encrypted, but a password is configured, please check the configuration; 而sdk-go/ 节点侧会忽略该口令继续运行。请以配置正确为准,不要依赖任一侧的宽松行为。
2.10.4. 多签调用
sdk-java 的多签通过构造多个 User 对象、再用 SdkUtils.getEndorsers 生成背书列表实现。
重要:
new User(...)的构造入参必须是明文私钥字节。
User内部直接调用CryptoUtils.getPrivateKeyFromBytes(...)解析私钥,不接受口令。正确做法:先用
CryptoUtils.decryptPrivKeyPem(rawBytes, pwd)解密,再构造User。
2.10.4.1. Cert 模式
import org.chainmaker.sdk.User;
import org.chainmaker.sdk.utils.CryptoUtils;
import org.chainmaker.sdk.utils.FileUtils;
import org.chainmaker.sdk.utils.SdkUtils;
/**
* 使用加密私钥构造 Cert 模式的管理员 User
* pwd 为 null 或空字符串时按明文私钥处理
*/
private static User newAdminUserWithEncryptedKey(String orgId,
String signKeyPath, String signKeyPwd, String signCertPath,
String tlsKeyPath, String tlsKeyPwd, String tlsCertPath) throws Exception {
byte[] signKeyBytes = decryptIfNeeded(FileUtils.getResourceFileBytes(signKeyPath), signKeyPwd);
byte[] tlsKeyBytes = decryptIfNeeded(FileUtils.getResourceFileBytes(tlsKeyPath), tlsKeyPwd);
return new User(orgId,
signKeyBytes,
FileUtils.getResourceFileBytes(signCertPath),
tlsKeyBytes,
FileUtils.getResourceFileBytes(tlsCertPath),
null, null, false);
}
private static byte[] decryptIfNeeded(byte[] rawBytes, String pwd) throws Exception {
if (pwd == null || pwd.isEmpty()) {
return rawBytes;
}
return CryptoUtils.decryptPrivKeyPem(rawBytes, pwd);
}
发起多签交易:
User adminUser1 = newAdminUserWithEncryptedKey(ORG_ID1,
ADMIN1_KEY_PATH, "Org1@P@ssw0rd", ADMIN1_CERT_PATH,
ADMIN1_TLS_KEY_PATH, "Org1@P@ssw0rd", ADMIN1_TLS_CERT_PATH);
User adminUser2 = newAdminUserWithEncryptedKey(ORG_ID2, /* ... */);
User adminUser3 = newAdminUserWithEncryptedKey(ORG_ID3, /* ... */);
// 1. 构造 payload
Request.Payload payload = chainClient.createContractCreatePayload(
CONTRACT_NAME, "1", byteCode, ContractOuterClass.RuntimeType.WASMER, null);
// 2. 多个管理员背书
Request.EndorsementEntry[] endorsementEntries =
SdkUtils.getEndorsers(payload, new User[]{adminUser1, adminUser2, adminUser3});
// 3. 发送合约管理请求
ResultOuterClass.TxResponse responseInfo = chainClient.sendContractManageRequest(
payload, endorsementEntries, rpcCallTimeout, syncResultTimeout);
2.10.4.2. PK / PWK 模式
PK / PWK 模式下 User 没有签名证书,构造方式如下(cryptoConfig 中的 hash 需与链保持一致):
byte[] adminKeyBytes = CryptoUtils.decryptPrivKeyPem(
FileUtils.getResourceFileBytes(ADMIN1_PK_PATH), "Org1@P@ssw0rd");
byte[] adminTlsKeyBytes = CryptoUtils.decryptPrivKeyPem(
FileUtils.getResourceFileBytes(ADMIN1_PK_TLS_PRI_KEY_PATH), "Org1@P@ssw0rd");
User adminUser1 = new User("public",
adminKeyBytes,
null,
adminTlsKeyBytes,
FileUtils.getResourceFileBytes(ADMIN1_PK_TLS_CERT_PATH),
null, null, cryptoConfig, false);
adminUser1.setAuthType(AuthType.Public.getMsg()); // PWK 模式使用 AuthType.PermissionedWithKey
后续 SdkUtils.getEndorsers(payload, users) 的用法与 Cert 模式一致。
2.11. cmc 客户端中使用加密私钥
2.11.1. 普通调用
cmc 底层复用 sdk-go,口令从 --sdk-conf-path 指定的 sdk_config.yml 中读取,配置项与 sdk-go 一节 完全相同(user_sign_key_pwd / user_key_pwd / user_enc_key_pwd)。
# ./testdata/sdk_config.yml
chain_client:
chain_id: "chain1"
org_id: "wx-org1.chainmaker.org"
user_key_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.key"
user_key_pwd: 'YourP@ssw0rd'
user_crt_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.tls.crt"
user_sign_key_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.key"
user_sign_key_pwd: 'YourP@ssw0rd'
user_sign_crt_file_path: "./testdata/crypto-config/wx-org1.chainmaker.org/user/client1/client1.sign.crt"
配置好后,cmc 命令的使用方式与明文私钥完全一致:
./cmc client contract user invoke \
--contract-name=counter-go-1 \
--method=increase \
--sdk-conf-path=./testdata/sdk_config.yml \
--sync-result=true
注意:
cmc目前没有用于传入私钥口令的命令行参数。使用--user-signkey-file-path、--user-tlskey-file-path覆盖私钥路径时,口令仍取自sdk_config.yml;若覆盖的私钥使用了不同口令,请改用独立的sdk_config.yml。而管理员(admin)私钥(
--admin-key-file-paths)的口令自v2.3.10起可通过--encrypt与--password参数传入,详见 多签处理。
2.11.2. cmc 各类私钥来源的支持情况
cmc 各私钥来源对口令的支持情况如下:
| 私钥来源 | 涉及命令 / 参数 | 是否支持加密私钥 |
|---|---|---|
sdk_config.yml 中的 user_sign_key_file_path / user_key_file_path / user_enc_key_file_path |
所有 cmc client、cmc query、cmc archive 等需要连链的命令 |
✅(由 user_*_pwd 提供口令) |
--admin-key-file-paths 管理员私钥(多签 / 单签背书) |
所有带该参数的命令:cmc client contract、cmc client chainconfig、cmc client certmanage、cmc client cert alias、cmc gas、cmc pubkey、cmc tee 等 |
✅(通过 --encrypt + --password 提供口令) |
--payer-key-file-path 代付者私钥 |
cmc client ...(Gas 代付)、cmc gas set-payer |
❌ 仅明文 |
--user-signkey-file-path / --user-tlskey-file-path 覆盖路径 |
cmc client ... |
⚠️ 路径被覆盖,但口令仍取自 sdk_config.yml |
| 离线签名工具的私钥 | cmc payload sign |
❌ 仅明文 |
| 压测工具的私钥 | cmc parallel |
❌ 仅明文 |
| 公钥导出 / 证书工具的私钥 | cmc key export_pub、cmc cert 系列 |
❌ 仅明文 |
对于标记 ❌ 的场景(代付者私钥、离线签名、压测等),请参照 多签处理 中的“先解密到内存文件系统、用完销毁”流程。
2.11.3. 多签处理
cmc 的多签通过 --admin-key-file-paths(Cert 模式还需 --admin-crt-file-paths,PWK 模式还需 --admin-org-ids)指定多个管理员私钥,用逗号分隔。
自 v2.3.10 起,管理员私钥支持加密,通过两个新增参数提供口令。这两个参数自动挂载到所有带 --admin-key-file-paths 的命令上(见上表),无需逐个命令确认:
| 参数 | 说明 |
|---|---|
--encrypt |
布尔开关,声明管理员私钥已加密、需要口令。不传时默认所有管理员私钥均为明文 |
--password |
批量口令,逗号分隔,与 --admin-key-file-paths 一一对应 |
两者的配合关系:
--encrypt 未传 → 全部按明文处理,不需要口令
--encrypt 已传:
├─ --password 有值 → 批量口令(数量必须等于管理员私钥数,否则报错)
└─ --password 无值 → 逐个交互式输入「请输入 admin{i} 密码」
├─ 直接回车(空)→ 该私钥为明文
└─ 输入口令 → 该私钥为密文
(仅交互式环节允许明文/密文私钥穿插)
私钥是否加密由读取 PEM 时自动判定,
--encrypt仅用于声明;口令漏传时报missing password for encrypted PEM。
2.11.3.1. 明文管理员私钥(不传 –encrypt,与旧版一致)
./cmc client contract user create \
--contract-name=counter-go-1 \
--runtime-type=DOCKER_GO \
--byte-code-path=./testdata/counter-go-demo/counter-go.7z \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.key \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.crt \
--sync-result=true
2.11.3.2. 加密管理员私钥:批量口令(–encrypt + –password)
./cmc client contract user create \
--contract-name=counter-go-1 \
--runtime-type=DOCKER_GO \
--byte-code-path=./testdata/counter-go-demo/counter-go.7z \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.key \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.crt \
--encrypt \
--password='Org1@P@ssw0rd,Org2@P@ssw0rd' \
--sync-result=true
口令列表用单引号包裹,避免
$、`等被 shell 解释,参见 Shell 命令行中的转义。口令本身含逗号时会与分隔符冲突,请改用交互式输入。
2.11.3.3. 加密管理员私钥:交互式输入(–encrypt,不传 –password)
./cmc client contract user create \
--contract-name=counter-go-1 \
--runtime-type=DOCKER_GO \
--byte-code-path=./testdata/counter-go-demo/counter-go.7z \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.key,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.key \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.crt \
--encrypt \
--sync-result=true
# 请输入 admin1 密码: ← 隐藏回显,输入 admin1 口令后回车
# 请输入 admin2 密码: ← 隐藏回显
交互式环节对每个管理员提示一次“请输入 admin{i} 密码”;直接回车表示该私钥为明文,因此允许“第一个管理员私钥已加密、第二个为明文”的穿插场景。
多签投票(
multi-sign vote)只取第 1 个管理员的私钥与证书,因此 vote 命令只涉及“一个管理员、一个口令”,交互提示只会出现一次。
2.11.3.4. 兜底做法:先解密到内存文件系统、用完销毁
管理员私钥已支持 --encrypt,不需要这套流程。下面的脚本是给不接受口令参数的私钥准备的通用兜底做法,即 cmc 各类私钥来源的支持情况 中标记 ❌ 的场景。这里借多签命令做演示,实际使用时把最后一步换成对应的命令即可。
#!/usr/bin/env bash
set -euo pipefail
# 临时目录优先放在内存文件系统(Linux 的 /dev/shm),避免明文私钥落到磁盘。
# 注意:变量名不要用 TMPDIR——那是 mktemp 等工具的保留环境变量。
if [ -d /dev/shm ]; then
KEYDIR=$(mktemp -d /dev/shm/cmc-multisign.XXXXXX)
else
KEYDIR=$(mktemp -d) # 无 /dev/shm 时退回普通临时目录
fi
chmod 700 "$KEYDIR"
# 确保脚本退出时(包括异常退出)销毁明文私钥
cleanup() {
command -v shred >/dev/null 2>&1 && shred -u "$KEYDIR"/*.key 2>/dev/null
rm -rf "$KEYDIR"
}
trap cleanup EXIT
read -rs -p "管理员私钥口令: " ADMIN_KEY_PWD && echo
# 1. 逐个解密管理员私钥到临时目录
for i in 1 2; do
./cmc crypto pem-decrypt \
-k "./testdata/crypto-config/wx-org${i}.chainmaker.org/user/admin1/admin1.sign.key" \
-p "$ADMIN_KEY_PWD" \
-o "$KEYDIR/admin${i}.sign.key"
# cmc 失败时退出码仍为 0,必须显式校验产物
if ! grep -q -- "-----BEGIN" "$KEYDIR/admin${i}.sign.key" 2>/dev/null; then
echo "admin${i} 私钥解密失败(口令是否正确?)" >&2
exit 1
fi
done
# 2. 用临时明文私钥执行多签命令
./cmc client contract user create \
--contract-name=counter-go-1 \
--runtime-type=DOCKER_GO \
--byte-code-path=./testdata/counter-go-demo/counter-go.7z \
--version=1.0 \
--sdk-conf-path=./testdata/sdk_config.yml \
--admin-key-file-paths="$KEYDIR/admin1.sign.key,$KEYDIR/admin2.sign.key" \
--admin-crt-file-paths=./testdata/crypto-config/wx-org1.chainmaker.org/user/admin1/admin1.sign.crt,./testdata/crypto-config/wx-org2.chainmaker.org/user/admin1/admin1.sign.crt \
--sync-result=true
# 3. 退出时由 trap 自动销毁临时明文私钥
若各私钥口令不同,请把
read与解密步骤按私钥分别执行;示例中为简化起见假设口令相同。
2.11.4. 字符串信息加解密
除私钥文件外,cmc crypto encrypt / cmc crypto decrypt 还可对任意字符串或文件做基于口令的对称加密(PBE:PBKDF2 派生密钥,迭代次数 100000),典型用途是加密 chainmaker.yml 中 MySQL DSN 里的数据库口令。
# 加密:SM4-CBC(对应 storage 配置中的 alg: sm4-cbc)
./cmc crypto encrypt -a SM4 -p 'Passphrase@123' -s 'mysql_pwd_123' --out-base64
# uhG/0emY4UfMBbEoVXtJKmCg3WKECJy1WllKLya+JwquSv3ZIAXnFRYRYx7Z2Vd4
# 解密校验
./cmc crypto decrypt -a SM4 -p 'Passphrase@123' \
--data-base64 'uhG/0emY4UfMBbEoVXtJKmCg3WKECJy1WllKLya+JwquSv3ZIAXnFRYRYx7Z2Vd4' --out-string
# mysql_pwd_123
# AES-GCM(对应 storage 配置中的 alg: aes-gcm)
./cmc crypto encrypt -a AES -m GCM -p 'Passphrase@123' -s 'mysql_pwd_123' --out-base64
输出每次都不同(密文含随机盐与 IV / nonce),属正常现象,任意一次的输出填入配置均可使用。
这两个子命令与私钥文件加密无关,完整的参数说明、与 storage.password_encrypt 的参数对应关系、DSN 改写方式与排错,统一见 数据管理 · SQL 数据库密码加密。
cmc crypto encrypt/decrypt的口令与待处理数据均可省略,省略项进入交互式隐藏回显输入,详见 交互式加解密。
2.12. 完整操作流程(端到端)
以一条已用 chainmaker-cryptogen 生成明文私钥、正在运行的 Cert 模式链为例,把节点与客户端私钥全部改为加密存储的完整步骤如下。
第 0 步:确认密钥类型可加密
head -1 ./config/wx-org1.chainmaker.org/certs/node/consensus1/consensus1.sign.key
# 出现 -----BEGIN ... PRIVATE KEY----- 才可以继续;
# 若是一行十六进制字符串,说明是 secp256k1 或 dilithium2,不支持加密
第 1 步:离线备份明文私钥
把整个 crypto-config 目录备份到安全介质。加密之后如需明文,从备份取用,不要依赖 pem-decrypt 的往返产物(见 pem-decrypt 输出格式的注意事项)。
第 2 步:停止节点,避免运行中替换私钥文件。
第 3 步:逐个加密私钥(示例只加密一个组织的节点私钥,其余同理)
set -euo pipefail
CERT_DIR=./config/wx-org1.chainmaker.org/certs/node/consensus1
read -rs -p "私钥口令: " KEY_PWD && echo # 交互式读取,口令不会进入 shell 历史
for f in consensus1.sign.key consensus1.tls.key; do
# 幂等保护:已加密的文件跳过,避免二次加密
if grep -q "Proc-Type: 4,ENCRYPTED" "$CERT_DIR/$f"; then
echo "跳过(已加密): $f"; continue
fi
rm -f "$CERT_DIR/$f.enc"
./cmc crypto pem-encrypt -k "$CERT_DIR/$f" -p "$KEY_PWD" -a AES -o "$CERT_DIR/$f.enc"
# 必须显式校验产物:cmc 报错时退出码仍为 0
if ! grep -q "Proc-Type: 4,ENCRYPTED" "$CERT_DIR/$f.enc" 2>/dev/null; then
echo "加密失败: $f(请检查上方 cmc 的错误输出)" >&2
exit 1
fi
mv "$CERT_DIR/$f.enc" "$CERT_DIR/$f" # 校验通过才覆盖;已在第 1 步备份
done
⚠️ cmc 失败时退出码仍为
0(错误只打印到 stderr)。脚本中请显式检查输出文件是否符合预期,不要依赖退出码与set -e。
第 4 步:修改 chainmaker.yml
按 chainmaker.yml 中的配置 补齐 node.priv_key_password、net.tls.priv_key_password、rpc.tls.priv_key_password,口令用单引号包裹。
第 5 步:启动前自检
先用 cmc 单独验证口令与文件匹配,避免节点反复启停:
./cmc crypto pem-decrypt -k "$CERT_DIR/consensus1.sign.key" -p "$KEY_PWD" --out-string | head -1
# 输出 -----BEGIN ... PRIVATE KEY----- 即为口令正确
第 6 步:启动节点并观察日志
启动后确认没有 fail to decrypt PEM / missing password for encrypted PEM 相关错误,并确认节点能正常出块、P2P 连接正常。
若需要确认口令确实被读进了配置,可以用
chainmaker config -c ./chainmaker.yml查看,但该命令会明文打印所有口令,请勿把输出外发,详见 安全建议。
第 7 步:处理客户端侧
对 SDK / cmc 使用的 user_sign_key_file_path、user_key_file_path 重复第 3、4 步,并在 sdk_config.yml 中补齐 user_sign_key_pwd、user_key_pwd。
第 8 步:清理
unset KEY_PWD # 清除当前 shell 中的口令变量
按第 3 步的写法,口令通过 read -rs 读入,不会进入 shell 历史,无需清理历史文件。只有当你在某一步把口令直接写在了命令行参数里(例如 -p 'YourP@ssw0rd'),才需要从 ~/.bash_history / ~/.zsh_history 中删除对应记录。
同时确认没有遗留的临时明文私钥文件:
find "$CERT_DIR" -name '*.enc' -o -name '*.plain' | head
回滚:若加密后节点无法启动且短期内无法定位,从第 1 步的备份恢复明文私钥、删除
chainmaker.yml中的口令配置项,即可回到加密前状态。
2.13. 常见错误与排查
| 现象 / 报错 | 可能原因 | 处理方式 |
|---|---|---|
input PEM is not encrypted |
私钥文件是明文,或是 PKCS#8 加密格式(BEGIN ENCRYPTED PRIVATE KEY) |
确认文件头部含 Proc-Type: 4,ENCRYPTED;PKCS#8 加密请改用 openssl ... -traditional 重新生成 |
fail to decrypt PEM: [x509: decryption password incorrect && ...] |
口令错误,或 YAML 中的口令被解析器改写。SDK 侧完整前缀为 decrypt user sign key failed, asym: parse private key PEM: ... |
用 cmc crypto pem-decrypt 单独验证口令;检查 YAML 是否用单引号包裹,参见 YAML 转义 |
missing password for encrypted PEM |
私钥已加密但未配置口令。各处的完整前缀不同:SDK/cmc 读取自身私钥为 PrivateKeyFromPEM failed, ...;节点 P2P TLS 为 chainmaker server init failed, ...;多签背书(--admin-key-file-paths)传了加密私钥但未传 --encrypt 或未提供口令时同样报此错 |
若是自身私钥,补齐 priv_key_password / user_sign_key_pwd;若是多签背书私钥,补 --encrypt 与 --password,参见 cmc 多签处理 |
parse user key file to privateKey obj failed, missing password for encrypted PEM(Cert 模式签名私钥 / TLS 私钥)PrivateKeyFromPEM failed, missing password for encrypted PEM(PK / PWK 模式签名私钥)——均为已配了口令却仍报错 |
通过 WithUserSignKeyBytes / setUserSignKeyBytes 等字节方式传入了加密私钥——此时口令不生效。两种模式的报错包装不同,含义相同 |
先用 asym.DecryptPrivateKeyPEM(Go)/ CryptoUtils.decryptPrivKeyPem(Java)解密后再传入,或改用文件路径方式 |
Private key file is not encrypted, but a password is configured(Java) |
私钥是明文,但配置了口令 | 去掉口令配置,或改用加密私钥 |
Invalid padding: padding byte mismatch / Invalid padding: invalid padding length N(Java) |
口令错误——但仅出现在 SM4-CBC 解密路径,该路径的报错不提口令。AES 与 PKCS#8 路径会给出 Failed to decrypt private key, please check the password |
按口令错误排查;用 cmc crypto pem-decrypt 单独验证口令是否正确 |
Unsupported PEM format, unable to decrypt private key: null(Java) |
配置了口令,但私钥文件不是 PEM——secp256k1 / dilithium2 的十六进制私钥即属此类 | 这类私钥不支持加密,参见 适用范围 |
unrecognised object: SM2 PRIVATE KEY(Java) |
SM2 私钥用 OpenSSL 3.x 加密,PEM 类型为 SM2 PRIVATE KEY |
改用 cmc crypto pem-encrypt 加密,参见 SM2 私钥的特别说明 |
password must not be empty, use -p to specify |
cmc crypto pem-encrypt/pem-decrypt 交互式输入时直接回车,输入了空口令 |
输入非空口令,或通过 -p 参数指定口令 |
Could not find private key from ...(openssl) |
该报错非常含糊,有两种成因:① 目标是 secp256k1 / dilithium2 私钥(十六进制文本,非 PEM);② 口令错误——常见于口令文件为 CRLF 换行、行尾 \r 混入了口令 |
① 参见 适用范围;② od -c 口令文件 检查是否含 \r |
parse private key PEM failed: failed to parse private key |
-k 传错了文件(例如把 .crt 证书当成私钥),或私钥文件损坏、被截断 |
用 head -1 确认文件是私钥而非证书;用 openssl asn1parse -in xxx 检查完整性 |
parse private key PEM failed: missing password for encrypted PEM(执行 pem-encrypt 时) |
对已经加密过的私钥再次执行 pem-encrypt。报错文字有误导性——它指的是“读取输入私钥”缺口令,不是 -p 没传 |
该文件已加密,无需再加密;确需换口令请按 修改口令与轮换 先解密再加密 |
unsupported pem cipher algorithm XXX, supported: AES, SM4 |
-a 只接受 AES 与 SM4。常见误写是照搬 DEK-Info 里的 AES-256-CBC |
改为 -a AES 或 -a SM4 |
parse private key PEM failed: fail to decode public key: [encoding/hex: invalid byte: U+000A] |
对 secp256k1 / dilithium2 的十六进制私钥执行 pem-encrypt,且文件带尾换行(chainmaker-cryptogen 的原始产物无尾换行,经编辑器保存后可能被加上)。报错文案中的 "public key" 是底层库的笔误,实际处理的是私钥 |
这类私钥不支持加密,参见 适用范围 |
no PEM block found in input |
① 对非 PEM 文件执行 pem-decrypt,例如 secp256k1 / dilithium2 的十六进制私钥文件;② 私钥文件开头带 UTF-8 BOM |
① 参见 适用范围;② 用 head -c 3 检查文件前三字节,为 ef bb bf 即带 BOM,需去除 |
failed to parse private key(节点 / sdk-go)Unsupported PEM format ... : null(Java) |
私钥文件带 UTF-8 BOM,PEM 头部不在文件起始位置。明文私钥带 BOM 同样会导致节点启动失败 | 用 head -c 3 检查并去除 BOM,详见 私钥文件不能带 UTF-8 BOM |
key file must not be empty, use -k to specify |
cmc crypto pem-encrypt/pem-decrypt 未传 -k |
补充 -k 参数指定私钥文件 |
read key file XXX failed: no such file or directory(读输入)write output file XXX failed: ...(写输出) |
前者是 -k 的路径写错或不可读;后者是 -o 的目标目录不存在或无写权限 |
用绝对路径,或先 ls 确认文件存在、目录可写 |
asym: invalid private key PEM(SDK 初始化时) |
配置了 user_*_pwd,但对应私钥文件不是 PEM——常见于 secp256k1 / dilithium2 私钥,或 PKCS#11 / KMS 的密钥索引文件 |
这些场景不支持私钥文件口令,删除对应的 user_*_pwd 配置 |
| 节点启动正常但口令似乎“没生效” | 私钥本身是明文,Go 侧会忽略多余的口令 | 用 head -3 私钥文件 确认是否真的已加密 |
代付命令报 missing password for encrypted PEM |
--payer-key-file-path(代付者私钥)指向了加密私钥——该参数不接受口令 |
参见 cmc 多签处理 中的兜底做法,先解密到临时目录再使用 |
2.13.1. 私钥文件不能带 UTF-8 BOM
Windows 记事本等编辑器“另存为”时可能在文件开头写入 3 个字节的 BOM(EF BB BF),导致长安链侧解析失败。检查与去除:
head -c 3 ./privkey.key | od -An -tx1 # 输出 ef bb bf 即为带 BOM
perl -i -pe 's/^\x{EF}\x{BB}\x{BF}//' ./privkey.key # 去除 BOM
换行符不影响私钥文件(全部行转 CRLF 后 openssl、cmc、节点 / SDK 四方均能正常解密);需要注意 CRLF 的是口令文件。
2.14. 安全建议
2.14.1. 口令的保管与存放
请同时做到:明文私钥离线备份、口令由可靠的密钥管理流程保管,且两者分开存放。
关于“口令能否不写进配置文件”,节点侧与应用侧能做到的程度不同:
| 场景 | 口令能否不落配置文件 | 说明 |
|---|---|---|
长安链节点(chainmaker.yml) |
✅ 可以(v2.3.10 起) |
5 个私钥口令项均支持 env:<环境变量名>,节点启动时从环境变量读取,见 口令通过环境变量注入 |
| SDK / 自研应用 | ✅ 可以 | 配置文件中留空,运行时由代码从环境变量、K8s Secret、配置中心读取后写入:sdk.WithUserSignKeyPwd(...) / chainClientConfig.setUserSignKeyPwd(...) |
| cmc 的管理员私钥(多签 / 背书) | ✅ 可以 | 传 --encrypt 但不传 --password,改为交互式输入,口令既不落盘也不进 shell 历史 |
| cmc 客户端自身私钥 | ❌ 不可以 | 口令只能来自 sdk_config.yml,且 user_*_pwd 不支持 env: 语法 |
无论是否使用
env:,都请把chainmaker.yml当作与私钥同等敏感的文件对待:权限设为0600、通过 K8s Secret / systemd 凭据等机制挂载整个配置文件、纳入审计范围。
⚠️
chainmaker config会明文打印所有口令(私钥口令、加密机口令、数据库 DSN 等,不做脱敏)。不要把输出贴到工单、聊天群或日志系统;确需分享时请先删除敏感字段,重定向输出时按敏感文件管理。
与数据库口令的写法差异:
v2.3.10起,同一份chainmaker.yml中storage的password_encrypt.passphrase与 5 个私钥口令项都支持env:<环境变量名>,写法一致;唯一区别是环境变量存在但为空串时,数据库口令会报错,私钥口令则按“未配置口令”处理。但sdk_config.yml中的user_*_pwd完全不支持该语法,不要照搬。
2.14.2. 通用建议
算法选择:普通场景使用
AES(AES-256-CBC),与 openssl 互通;国密合规场景使用SM4(SM4-CBC)。不要使用 DES / 3DES——虽然可解密以兼容存量文件,但已不满足现代安全强度。保护明文私钥的生命周期:加密完成后销毁明文文件(
shred -u),临时解密尽量放在内存文件系统(/dev/shm),并用trap保证异常退出时清理。文件权限:私钥文件与
chainmaker.yml/sdk_config.yml权限均设为0600,所在目录0700;cmc crypto写出的文件默认已是0600。避免口令泄露到历史与进程列表:不要在生产环境用
-p 明文口令;使用 openssl 的file:/env:/ 交互式输入,或 cmc 的交互式模式。口令分离:不同组织、不同节点、不同用途(签名 / TLS / 加密)的私钥使用不同口令,避免“一密全失”。
更高安全等级场景:如果安全要求高于“口令加密文件”,建议使用加密机(PKCS#11 / SDF)或 KMS 托管私钥,让私钥永不出安全边界。参见 硬件加密、部署启用硬件加密的链。
建立轮换机制:定期轮换私钥口令,轮换后同步更新所有引用配置并滚动重启节点 / 客户端。
性能影响:私钥解密只在节点启动 / 客户端初始化时发生一次,明文私钥仅驻留内存,对交易签名、共识、TLS 握手等运行时路径没有任何额外开销。