# 私钥文件加密 本节介绍长安链**私钥文件**的口令加密保护:如何用 `openssl` 与 `cmc` 完成加解密,以及加密后的私钥在 `chainmaker.yml`、`sdk-go`、`sdk-java`、`cmc` 中的配置与使用(含多签场景)。 本章是私钥文件加密的统一说明。各组件配置项的完整清单另见 [长安链配置管理](./长安链配置管理.html)、[Go SDK 使用说明](../sdk/GoSDK使用说明.html)、[Java SDK 使用说明](../sdk/JavaSDK使用说明.html)。 ## 快速开始 以客户端签名私钥为例,加密并投入使用只需几步。**动手前请先读一遍 [前置条件](#privkey-enc-prereq)**: ```bash # 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----- 即为正确 ``` ```yml # 3. 在配置文件中填入口令,务必用单引号包裹 # sdk_config.yml chain_client: user_sign_key_pwd: 'YourP@ssw0rd' # chainmaker.yml(节点侧) node: priv_key_password: 'YourP@ssw0rd' ``` 最容易踩的坑: - 口令中的 `$`、`` ` ``、`!` 等**不要逐个转义**,用单引号包起来即可 —— [口令使用注意事项](#privkey-enc-password) - **secp256k1 与 dilithium2 私钥不支持加密** —— [适用范围](#privkey-enc-scope) 完整的端到端流程(含备份、停机、回滚)见 [完整操作流程](#privkey-enc-workflow)。 ## 功能简介 长安链的节点身份、TLS 连接、客户端交易签名都依赖私钥文件,`chainmaker-cryptogen` 生成的私钥文件默认**明文**落盘。 为降低私钥明文落盘的风险,长安链支持将私钥文件用**口令(password / passphrase)加密**后再落盘,运行时由节点或 SDK 使用配置的口令在**内存中**解密使用: - 私钥文件采用 **OpenSSL 传统加密 PEM 格式**(PEM 头部带 `Proc-Type: 4,ENCRYPTED` 与 `DEK-Info`),可与 `openssl` 命令行互通; - 在此基础上,长安链扩展支持 **SM4-CBC** 算法,以满足国密场景; - 加密是**可选**能力:不配置口令时行为与旧版本完全一致,存量明文私钥无需任何改动。 ## 前置条件 ### 版本要求 私钥文件加密相关能力需按组件版本确认: - 长安链节点、`cmc` 的私钥 PEM 加密与解密、TLS 私钥口令(`net.tls.*`、`rpc.tls.*`)、`cmc crypto` 命令族、`cmc` 管理员私钥 `--encrypt` / `--password`,以及 `chainmaker.yml` 私钥口令的 `env:` 注入,自长安链 `v2.3.10` 起支持; - `sdk-go` 带口令的背书辅助函数,以及 `sdk-go` 对 SM4-CBC 加密私钥的解密,自 `sdk-go v2.3.12` 起提供; - `sdk-java` 对 SM4-CBC 加密私钥的解密,自 `sdk-java v2.3.9` 起提供。 更早版本请先升级。 ### 动手前的确认清单 开始加密之前,请逐项确认: 1. **确认对应组件满足上面的版本要求**,不要直接将长安链节点版本号作为 SDK 的版本要求。 2. **确认私钥文件是 PEM 格式**:`head -1 私钥文件` 应输出 `-----BEGIN ... PRIVATE KEY-----`。若是一行十六进制字符串,说明是 secp256k1 或 dilithium2,**不支持加密**,详见 [适用范围](#privkey-enc-scope)。 3. **确认未启用加密机 / KMS**:`node.pkcs11.enabled` 或 `node.kms.enabled` 为 `true` 时私钥由外部托管,请改用[硬件加密](../cryptography/硬件加密.html)方案。 4. **已离线备份明文私钥**,且备份与口令**分开存放**。口令一旦丢失,加密私钥无法恢复。 ## 适用范围 ### 支持的账户模式与密钥类型 私钥文件加密对 **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 的业务请先确认对应版本是否支持。 ### 支持的加密格式与算法 长安链节点、`sdk-go` 和 `cmc` **仅支持 OpenSSL 传统加密 PEM 格式**,即 PEM 块头部形如: ```text -----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`,详见下文。 ## 使用 openssl 加解密私钥文件 以下示例中,明文私钥为 `admin1.sign.key`,口令为 `YourP@ssw0rd`。 ### ECC(含 SM2)私钥加密 `openssl ec` 默认就输出传统格式,直接加算法参数即可: ```bash # 加密: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 ``` ### RSA 私钥加密 OpenSSL 3.x 中 `openssl rsa` 默认输出 PKCS#8,**必须加 `-traditional`**: ```bash # 输出传统加密格式,头部应有 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 ``` ### 解密与校验 ```bash # 解密回明文 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 ``` ### 口令的安全传入方式 生产环境建议使用以下方式传入: ```bash # 从文件读取(文件首行为口令,读取后自动去掉行尾换行) 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` 会混入口令。检查与转换: > > ```bash > 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` 传入或省略后交互式输入。 ### SM2 私钥的特别说明 `chainmaker-cryptogen` / `cmc key gen` 生成的 SM2 私钥是 **PKCS#8 明文 PEM**(`-----BEGIN PRIVATE KEY-----`)。用 OpenSSL 3.x 对其加密时,输出的 PEM 类型会变成 **`-----BEGIN SM2 PRIVATE KEY-----`**: ```bash 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 均可正常处理。 ## 使用 cmc 加解密私钥文件(cmc crypto) `v2.3.10` 起,`cmc` 提供 `crypto` 命令族: ```text cmc crypto ├── pem-encrypt # 私钥 PEM 加密(AES / SM4) ├── pem-decrypt # 私钥 PEM 解密 ├── encrypt # 通用字符串/文件对称加密(PBE:AES / SM4) └── decrypt # 通用字符串/文件对称解密 ``` ### cmc crypto pem-encrypt ```bash ./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` | 否 | 加密口令,不允许为空;省略时交互式提示输入(隐藏回显),见 [交互式加解密](#privkey-enc-cmc-interactive) | | `--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`。 不同算法的输出对照: ```bash # 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`。** ### cmc crypto pem-decrypt ```bash ./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` 生成的传统格式加密私钥。 ### 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` 的往返产物。 ### 交互式加解密 `cmc crypto` 四个子命令均支持**交互式输入**:当口令(或待处理数据)未通过命令行参数传入时,命令会提示输入,并在终端(TTY)下**关闭回显**(不显示明文),提示信息写到 stderr、不污染 stdout 输出。 #### 私钥文件加解密(pem-encrypt / pem-decrypt) `-p/--password` 省略时,交互式提示输入口令: ```bash # 加密:未传 -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: ``` > 交互式输入时若直接回车(空口令),命令会报错——私钥文件加解密**不允许空口令**。 #### 字符串信息加解密(encrypt / decrypt) `cmc crypto encrypt` / `decrypt` 的口令与待处理数据均可省略,省略项进入交互式输入: | 省略的入参 | 交互式提示 | 回显 | | :--- | :--- | :---: | | 口令(`-p`/`--password-hex`/`--password-base64`/`--password-file` 均未传) | `Enter password:` | 隐藏 | | 待处理数据(`-s`/`-i`/`--data-hex`/`--data-base64` 均未传) | `Enter data:` | 隐藏 | ```bash # 加密:口令与数据都不传,全部交互式输入(均隐藏回显) ./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`)回退为读取标准输入的**全部内容**。这在脚本中可通过管道传入,但需注意此时口令不再隐藏回显。 ## 口令使用注意事项 **口令中的特殊字符是本功能最容易出错的地方**。 ### 口令本身的要求 - 口令**不能为空**。 - 口令**大小写敏感**,含首尾空格时必须用单引号包裹(见下方转义小节)。 - 长安链对口令长度、字符集不做限制,可以包含空格与任意可见字符;建议长度不少于 12 位,混合大小写字母、数字与符号。 - 中文等非 ASCII 口令在 `cmc`、`openssl`、Java SDK 三方之间可稳定互通;口令需跨机器传递时,注意各终端的字符编码设置保持一致。 ### Shell 命令行中的转义 **统一使用单引号包裹口令**,内部所有字符原样传递,无需逐个转义: ```bash # ✅ 单引号,内部所有字符原样传递 ./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 中完全一致。 **单引号唯一处理不了的字符就是单引号自身**。口令中含单引号时,用 `'\''` 的写法闭合再拼接(含义是:结束当前单引号串、插入一个转义的单引号、再开始新的单引号串): ```bash # 目标口令: 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 的[交互式模式](#privkey-enc-cmc-interactive),并及时清理 shell 历史。 ### 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: 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 的单引号字符串不做任何转义处理,`$`、`` ` ``、`\`、`#`、`*`、`&` 等一律按字面读取;唯一的规则是**字符串内的单引号要写成两个连续单引号 `''`**。 ```yml 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' ``` 上表中所有“问题写法”,改用单引号包裹后均可原样读取。 ### 修改口令与轮换 修改口令没有专门命令,采用“先解密再加密”的方式: ```bash ./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`),并重启相应进程。 ## 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` 后才会暴露)。 ### Cert 模式配置示例 ```yml # 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 ``` ### PK / PWK 模式配置示例 PK / PWK 模式的链上身份认证不使用证书,P2P 网络基于节点公私钥对建立连接;但 RPC 启用 TLS 时仍需要配置 TLS 私钥与证书。 PK 模式(`prepare_pk.sh` 生成,`{node_pk_path}` 被替换为组织目录名)。**`node` 与 `net.tls` 引用同一个私钥文件 `node1.key`**,口令必须一致: ```yml 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` 同样引用同一个私钥文件**: ```yml 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` 中。 ### 口令通过环境变量注入 `v2.3.10` 起,上表 5 个口令项都支持 `env:<环境变量名>` 写法:节点启动时从对应环境变量读取真实口令,口令明文不必落在 `chainmaker.yml` 里。写法与 `storage` 下 `password_encrypt.passphrase` 一致,适用于 K8s Secret `envFrom`、systemd `EnvironmentFile` 等注入场景。 ```yml 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' ``` 启动前注入环境变量: ```bash 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'` 会被当成字面口令而解密失败;客户端要从环境变量取口令,请用 [代码方式配置](#privkey-enc-sdk-go-code)。 > > **注意 4**:环境变量对同机其他进程可见(Linux 下可通过 `/proc//environ` 读取),它比配置文件明文安全,但弱于加密机 / KMS。请配合 K8s Secret、权限收紧的 systemd `EnvironmentFile`(`chmod 600`)等机制使用。 ### 生效说明 - 口令项**留空或删除**时,按明文私钥处理,与旧版本行为一致; - 私钥文件是明文但配置了口令时,节点侧会**忽略该口令**并正常启动(不报错); - 口令错误时节点启动失败,核心错误为 `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` 全部留空或删除。参见 [硬件加密](../cryptography/硬件加密.html)。 ## sdk-go 中的配置与使用 ### 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`): ```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 模式示例: ```yml 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:` 语法**(详见 [口令通过环境变量注入](#privkey-enc-env) 注意 3):写成 `'env:XXX'` 会被当成字面口令,解密失败。客户端要从环境变量取口令时,请用下面的代码方式配置。 ### 代码方式配置 也可以不写在配置文件里,改由代码传入(例如从环境变量、配置中心、KMS 读取): ```go 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` 传入私钥字节时口令不参与,加密的私钥字节请先解密再传入: > > ```go > 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), // 必须传明文 > ) > ``` ### 普通调用 私钥口令只影响 `ChainClient` 的初始化过程:`NewChainClient` 内部读取私钥文件并用口令解密,明文私钥仅存在于内存中。**初始化完成后,所有业务接口的调用方式与明文私钥完全一致,业务代码无需任何改动。** ```go 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) ``` ### 多签调用 多签(合约管理、链配置变更、多签合约投票等)需要用**多个管理员私钥**分别对同一个 `Payload` 签名,生成 `EndorsementEntry` 列表。 **`sdk-go v2.3.12` 起,背书辅助函数原生支持加密私钥**,新增了 4 个带口令参数的函数: | 新函数 | 对应的原函数 | 用途 | | :--- | :--- | :--- | | `MakeEndorserWithPathAndPassword` | `MakeEndorserWithPath` | Cert 模式,按文件路径背书 | | `MakePkEndorserWithPathAndPassword` | `MakePkEndorserWithPath` | PK / PWK 模式,按文件路径背书 | | `MakeEndorserWithPassword` | `MakeEndorser` | 私钥 PEM 已在内存中时背书 | | `SignPayloadWithPathAndPassword` | `SignPayloadWithPath` | 只签名,不生成背书条目 | > 加密私钥请改用带 `AndPassword` 的版本;口令参数传 `nil` 表示私钥未加密。 #### Cert 模式 ```go 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: [...]`。含义相同,都按口令错误排查。 #### PK / PWK 模式 PK / PWK 模式下背书成员信息是公钥而不是证书,改用 `MakePkEndorserWithPathAndPassword`: ```go 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)`。 #### 多签合约(MultiSignContract) 多签合约的**发起**与**投票**同样通过 `EndorsementEntry` 携带签名,替换背书构造函数即可: ```go // 发起多签请求(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` 提供): > > ```go > // 注意签名为 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` 等。 ## sdk-java 中的配置与使用 ### 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` | ```yml 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" ``` ### 代码方式配置 ```java 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 不再读取文件,**口令也不参与**。 > > 若持有的是加密私钥字节,请先解密再传入: > > ```java > byte[] plainPEM = CryptoUtils.decryptPrivKeyPem(encryptedPEM, keyPwd); > chainClientConfig.setUserSignKeyBytes(plainPEM); // 必须传明文 > ``` ### 普通调用 与 `sdk-go` 一致,口令仅作用于 `createChainClient` 阶段。客户端创建成功后,所有业务接口调用方式不变: ```java 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` / 节点侧会忽略该口令继续运行。请以配置正确为准,不要依赖任一侧的宽松行为。 ### 多签调用 `sdk-java` 的多签通过构造多个 `User` 对象、再用 `SdkUtils.getEndorsers` 生成背书列表实现。 > **重要:`new User(...)` 的构造入参必须是明文私钥字节。** > > `User` 内部直接调用 `CryptoUtils.getPrivateKeyFromBytes(...)` 解析私钥,不接受口令。 > > 正确做法:**先用 `CryptoUtils.decryptPrivKeyPem(rawBytes, pwd)` 解密,再构造 `User`。** #### Cert 模式 ```java 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); } ``` 发起多签交易: ```java 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); ``` #### PK / PWK 模式 PK / PWK 模式下 `User` 没有签名证书,构造方式如下(`cryptoConfig` 中的 hash 需与链保持一致): ```java 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 模式一致。 ## cmc 客户端中使用加密私钥 ### 普通调用 `cmc` 底层复用 `sdk-go`,**口令从 `--sdk-conf-path` 指定的 `sdk_config.yml` 中读取**,配置项与 [sdk-go 一节](#privkey-enc-sdk-go) 完全相同(`user_sign_key_pwd` / `user_key_pwd` / `user_enc_key_pwd`)。 ```yml # ./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` 命令的使用方式与明文私钥完全一致: ```bash ./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` 参数传入,详见 [多签处理](#privkey-enc-cmc-multisign)。 ### 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` 系列 | ❌ 仅明文 | 对于标记 ❌ 的场景(代付者私钥、离线签名、压测等),请参照 [多签处理](#privkey-enc-cmc-multisign) 中的“先解密到内存文件系统、用完销毁”流程。 ### 多签处理 `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` 一一对应 | 两者的配合关系: ```text --encrypt 未传 → 全部按明文处理,不需要口令 --encrypt 已传: ├─ --password 有值 → 批量口令(数量必须等于管理员私钥数,否则报错) └─ --password 无值 → 逐个交互式输入「请输入 admin{i} 密码」 ├─ 直接回车(空)→ 该私钥为明文 └─ 输入口令 → 该私钥为密文 (仅交互式环节允许明文/密文私钥穿插) ``` > 私钥是否加密由读取 PEM 时自动判定,`--encrypt` 仅用于声明;口令漏传时报 `missing password for encrypted PEM`。 #### 明文管理员私钥(不传 --encrypt,与旧版一致) ```bash ./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 ``` #### 加密管理员私钥:批量口令(--encrypt + --password) ```bash ./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 命令行中的转义](#privkey-enc-shell-escape)。口令本身含逗号时会与分隔符冲突,请改用交互式输入。 #### 加密管理员私钥:交互式输入(--encrypt,不传 --password) ```bash ./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 命令只涉及“一个管理员、一个口令”,交互提示只会出现一次。 #### 兜底做法:先解密到内存文件系统、用完销毁 管理员私钥已支持 `--encrypt`,**不需要**这套流程。下面的脚本是给**不接受口令参数**的私钥准备的通用兜底做法,即 [cmc 各类私钥来源的支持情况](#privkey-enc-cmc-support) 中标记 ❌ 的场景。这里借多签命令做演示,实际使用时把最后一步换成对应的命令即可。 ```bash #!/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` 与解密步骤按私钥分别执行;示例中为简化起见假设口令相同。 ### 字符串信息加解密 除私钥文件外,`cmc crypto encrypt` / `cmc crypto decrypt` 还可对任意字符串或文件做基于口令的对称加密(PBE:PBKDF2 派生密钥,迭代次数 100000),典型用途是加密 `chainmaker.yml` 中 MySQL DSN 里的数据库口令。 ```bash # 加密: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 数据库密码加密](./数据管理.html#sqlPwdEncrypt)**。 > `cmc crypto encrypt` / `decrypt` 的口令与待处理数据均可省略,省略项进入交互式隐藏回显输入,详见 [交互式加解密](#privkey-enc-cmc-interactive)。 ## 完整操作流程(端到端) 以一条已用 `chainmaker-cryptogen` 生成明文私钥、正在运行的 Cert 模式链为例,把节点与客户端私钥全部改为加密存储的完整步骤如下。 **第 0 步:确认密钥类型可加密** ```bash 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 输出格式的注意事项](#privkey-enc-cmc-notice))。 **第 2 步:停止节点**,避免运行中替换私钥文件。 **第 3 步:逐个加密私钥**(示例只加密一个组织的节点私钥,其余同理) ```bash 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 中的配置](#privkey-enc-chainmaker-yml) 补齐 `node.priv_key_password`、`net.tls.priv_key_password`、`rpc.tls.priv_key_password`,口令**用单引号包裹**。 **第 5 步:启动前自检** 先用 `cmc` 单独验证口令与文件匹配,避免节点反复启停: ```bash ./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` 查看,但**该命令会明文打印所有口令**,请勿把输出外发,详见 [安全建议](#privkey-enc-pwd-storage)。 **第 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 步:清理** ```bash unset KEY_PWD # 清除当前 shell 中的口令变量 ``` 按第 3 步的写法,口令通过 `read -rs` 读入,不会进入 shell 历史,无需清理历史文件。**只有当你在某一步把口令直接写在了命令行参数里**(例如 `-p 'YourP@ssw0rd'`),才需要从 `~/.bash_history` / `~/.zsh_history` 中删除对应记录。 同时确认没有遗留的临时明文私钥文件: ```bash find "$CERT_DIR" -name '*.enc' -o -name '*.plain' | head ``` > **回滚**:若加密后节点无法启动且短期内无法定位,从第 1 步的备份恢复明文私钥、删除 `chainmaker.yml` 中的口令配置项,即可回到加密前状态。 ## 常见错误与排查 | 现象 / 报错 | 可能原因 | 处理方式 | | :--- | :--- | :--- | | `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 转义](#privkey-enc-yaml-escape) | | `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 多签处理](#privkey-enc-cmc-multisign) | | `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 的十六进制私钥即属此类 | 这类私钥不支持加密,参见 [适用范围](#privkey-enc-scope) | | `unrecognised object: SM2 PRIVATE KEY`(Java) | SM2 私钥用 OpenSSL 3.x 加密,PEM 类型为 `SM2 PRIVATE KEY` | 改用 `cmc crypto pem-encrypt` 加密,参见 [SM2 私钥的特别说明](#privkey-enc-openssl-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` 混入了口令 | ① 参见 [适用范围](#privkey-enc-scope);② `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` 没传 | 该文件已加密,无需再加密;确需换口令请按 [修改口令与轮换](#privkey-enc-pwd-rotate) 先解密再加密 | | `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" 是底层库的笔误,实际处理的是私钥 | 这类私钥不支持加密,参见 [适用范围](#privkey-enc-scope) | | `no PEM block found in input` | ① 对**非 PEM 文件**执行 `pem-decrypt`,例如 secp256k1 / dilithium2 的十六进制私钥文件;② 私钥文件开头带 **UTF-8 BOM** | ① 参见 [适用范围](#privkey-enc-scope);② 用 `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](#privkey-enc-fileformat) | | `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 多签处理](#privkey-enc-cmc-multisign) 中的兜底做法,先解密到临时目录再使用 | ### 私钥文件不能带 UTF-8 BOM Windows 记事本等编辑器“另存为”时可能在文件开头写入 3 个字节的 BOM(`EF BB BF`),导致长安链侧解析失败。检查与去除: ```bash 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 的是**口令文件**。 ## 安全建议 ### 口令的保管与存放 请同时做到:明文私钥离线备份、口令由可靠的密钥管理流程保管,且**两者分开存放**。 关于“口令能否不写进配置文件”,节点侧与应用侧能做到的程度不同: | 场景 | 口令能否不落配置文件 | 说明 | | :--- | :---: | :--- | | 长安链节点(`chainmaker.yml`) | ✅ 可以(`v2.3.10` 起) | 5 个私钥口令项均支持 `env:<环境变量名>`,节点启动时从环境变量读取,见 [口令通过环境变量注入](#privkey-enc-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` 完全不支持该语法,不要照搬。 ### 通用建议 1. **算法选择**:普通场景使用 `AES`(AES-256-CBC),与 openssl 互通;国密合规场景使用 `SM4`(SM4-CBC)。**不要使用 DES / 3DES**——虽然可解密以兼容存量文件,但已不满足现代安全强度。 2. **保护明文私钥的生命周期**:加密完成后销毁明文文件(`shred -u`),临时解密尽量放在内存文件系统(`/dev/shm`),并用 `trap` 保证异常退出时清理。 3. **文件权限**:私钥文件与 `chainmaker.yml` / `sdk_config.yml` 权限均设为 `0600`,所在目录 `0700`;`cmc crypto` 写出的文件默认已是 `0600`。 4. **避免口令泄露到历史与进程列表**:不要在生产环境用 `-p 明文口令`;使用 openssl 的 `file:` / `env:` / 交互式输入,或 cmc 的[交互式模式](#privkey-enc-cmc-interactive)。 5. **口令分离**:不同组织、不同节点、不同用途(签名 / TLS / 加密)的私钥使用不同口令,避免“一密全失”。 6. **更高安全等级场景**:如果安全要求高于“口令加密文件”,建议使用**加密机(PKCS#11 / SDF)或 KMS** 托管私钥,让私钥永不出安全边界。参见 [硬件加密](../cryptography/硬件加密.html)、[部署启用硬件加密的链](../instructions/部署启用硬件加密的链.html)。 7. **建立轮换机制**:定期轮换私钥口令,轮换后同步更新所有引用配置并滚动重启节点 / 客户端。 > **性能影响**:私钥解密只在节点启动 / 客户端初始化时发生一次,明文私钥仅驻留内存,对交易签名、共识、TLS 握手等运行时路径**没有任何额外开销**。