2. 私钥文件加密

本节介绍长安链私钥文件的口令加密保护:如何用 opensslcmc 完成加解密,以及加密后的私钥在 chainmaker.ymlsdk-gosdk-javacmc 中的配置与使用(含多签场景)。

本章是私钥文件加密的统一说明。各组件配置项的完整清单另见 长安链配置管理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,ENCRYPTEDDEK-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. 动手前的确认清单

开始加密之前,请逐项确认:

  1. 确认对应组件满足上面的版本要求,不要直接将长安链节点版本号作为 SDK 的版本要求。

  2. 确认私钥文件是 PEM 格式head -1 私钥文件 应输出 -----BEGIN ... PRIVATE KEY-----。若是一行十六进制字符串,说明是 secp256k1 或 dilithium2,不支持加密,详见 适用范围

  3. 确认未启用加密机 / KMSnode.pkcs11.enablednode.kms.enabledtrue 时私钥由外部托管,请改用硬件加密方案。

  4. 已离线备份明文私钥,且备份与口令分开存放。口令一旦丢失,加密私钥无法恢复。

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_algoecc_p384 / ecc_p521 虽未在配置模板的注释示例中列出,但同样受支持且可加密。

支持加密的私钥文件

  • 节点签名私钥(node.priv_key_file);

  • P2P 网络 TLS 私钥、国密双证书加密私钥(net.tls.priv_key_filenet.tls.priv_enc_key_file);

  • RPC 服务 TLS 私钥、国密双证书加密私钥(rpc.tls.priv_key_filerpc.tls.priv_enc_key_file);

  • SDK / cmc 客户端的交易签名私钥、TLS 私钥、国密加密私钥(user_sign_key_*user_key_*user_enc_key_*)。

SDK 支持范围:目前由 sdk-gosdk-java 提供支持(见下文各自章节)。Node.js、Python、Web3.js 等其他语言 SDK 暂未提供私钥口令配置项,使用这些 SDK 的业务请先确认对应版本是否支持。

2.4.2. 支持的加密格式与算法

长安链节点、sdk-gocmc 仅支持 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-gocmc 不支持 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-gocmc 可以正常解密,但 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-gosdk-javacmc 与 OpenSSL 3.x 之间完全互通——OpenSSL 3.x 既能解密(openssl ec / openssl rsa / openssl pkey),也能生成(openssl ec -sm4openssl 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-encryptopenssl 生成的传统格式加密私钥。

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-gosdk-javacmc,包括作为 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 口令在 cmcopenssl、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.ymlsdk_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: 0888
user_sign_key_pwd: 012345678
888
12345678
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.ymlsdk_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 口令项在部分模板中仍是注释状态),在配置文件里找不到某个口令项时,按上表的路径手动加上即可,效果相同。

注意 1node 节点下的 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.modedisable 时漏填 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} 被替换为组织目录名)。nodenet.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)。nodenet.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 里。写法与 storagepassword_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 startchainmaker 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、权限收紧的 systemd EnvironmentFilechmod 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.enablednode.kms.enabledtrue 时,私钥由加密机 / 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_runtrue,多签状态变为 PASSED不会自动执行目标交易,需再调用一次 MultiSignContractTrig 才会真正生效。该方法不接受背书参数,用调用方自身身份发起(自身私钥的口令仍由 sdk_config.ymlWithUserSignKeyPwd 提供):

// 注意签名为 4 个参数:payload、timeout、limit、withSyncResult
resp, err = cc.MultiSignContractTrig(payload, -1, &common.Limit{GasLimit: 100000}, true)

cc.MultiSignContractQuery(payload.TxId) 可查看当前状态与已投票数。

投票返回 CONTRACT_FAILContractResult.Messagethe 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.ymlsdk_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 clientcmc querycmc archive 等需要连链的命令 ✅(由 user_*_pwd 提供口令)
--admin-key-file-paths 管理员私钥(多签 / 单签背书) 所有带该参数的命令cmc client contractcmc client chainconfigcmc client certmanagecmc client cert aliascmc gascmc pubkeycmc 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_pubcmc 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_passwordnet.tls.priv_key_passwordrpc.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_pathuser_key_file_path 重复第 3、4 步,并在 sdk_config.yml 中补齐 user_sign_key_pwduser_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 只接受 AESSM4。常见误写是照搬 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.ymlstoragepassword_encrypt.passphrase 与 5 个私钥口令项都支持 env:<环境变量名>,写法一致;唯一区别是环境变量存在但为空串时,数据库口令会报错,私钥口令则按“未配置口令”处理。但 sdk_config.yml 中的 user_*_pwd 完全不支持该语法,不要照搬。

2.14.2. 通用建议

  1. 算法选择:普通场景使用 AES(AES-256-CBC),与 openssl 互通;国密合规场景使用 SM4(SM4-CBC)。不要使用 DES / 3DES——虽然可解密以兼容存量文件,但已不满足现代安全强度。

  2. 保护明文私钥的生命周期:加密完成后销毁明文文件(shred -u),临时解密尽量放在内存文件系统(/dev/shm),并用 trap 保证异常退出时清理。

  3. 文件权限:私钥文件与 chainmaker.yml / sdk_config.yml 权限均设为 0600,所在目录 0700cmc crypto 写出的文件默认已是 0600

  4. 避免口令泄露到历史与进程列表:不要在生产环境用 -p 明文口令;使用 openssl 的 file: / env: / 交互式输入,或 cmc 的交互式模式

  5. 口令分离:不同组织、不同节点、不同用途(签名 / TLS / 加密)的私钥使用不同口令,避免“一密全失”。

  6. 更高安全等级场景:如果安全要求高于“口令加密文件”,建议使用加密机(PKCS#11 / SDF)或 KMS 托管私钥,让私钥永不出安全边界。参见 硬件加密部署启用硬件加密的链

  7. 建立轮换机制:定期轮换私钥口令,轮换后同步更新所有引用配置并滚动重启节点 / 客户端。

性能影响:私钥解密只在节点启动 / 客户端初始化时发生一次,明文私钥仅驻留内存,对交易签名、共识、TLS 握手等运行时路径没有任何额外开销