新建会话
Ctrl
K
Kimi+
历史会话

SM2私钥解密与会话登记流程

int SDF_GetPrivateKeyAccessRight( void *hSessionHandle, unsigned int uiKeyIndex, unsigned char *pucPassword, unsigned int uiPwdLength) { int ret = SDR_OK; SOFTSDF_SESSION *session; SOFTSDF_CONTAINER *container = NULL; char *pass = NULL; char filename[FILENAME_MAX_LEN]; FILE *file = NULL; if (deviceHandle == NULL) { error_print(); return SDR_STEPERR; } if (hSessionHandle == NULL) { error_puts("Invalid session handle"); return SDR_INARGERR; } session = deviceHandle->session_list; while (session != NULL && session != hSessionHandle) { session = session->next; } if (session == NULL) { error_print(); return SDR_INARGERR; } if (pucPassword == NULL || uiPwdLength == 0) { error_puts("Invalid password or password length"); return SDR_INARGERR; } pass = (char *)malloc(uiPwdLength + 1); if (pass == NULL) { error_print(); return SDR_NOBUFFER; } memcpy(pass, pucPassword, uiPwdLength); pass[uiPwdLength] = 0; if (strlen(pass) != uiPwdLength) { error_print(); ret = SDR_INARGERR; goto end; } // create container container = (SOFTSDF_CONTAINER *)malloc(sizeof(*container)); if (container == NULL) { error_print(); ret = SDR_NOBUFFER; goto end; } memset(container, 0, sizeof(*container)); container->key_index = uiKeyIndex; // load sign_key snprintf(filename, FILENAME_MAX_LEN, "sm2sign-%u.pem", uiKeyIndex); file = fopen(filename, "r"); if (file == NULL) { perror("Error opening file"); fprintf(stderr, "open failure %s\n", filename); ret = SDR_KEYNOTEXIST; goto end; } if (sm2_private_key_info_decrypt_from_pem(&container->sign_key, pass, file) != 1) { error_print(); ret = SDR_GMSSLERR; goto end; } fclose(file); // load enc_key snprintf(filename, FILENAME_MAX_LEN, "sm2enc-%u.pem", uiKeyIndex); file = fopen(filename, "r"); if (file == NULL) { perror("Error opening file"); ret = SDR_KEYNOTEXIST; goto end; } if (sm2_private_key_info_decrypt_from_pem(&container->enc_key, pass, file) != 1) { error_print(); ret = SDR_GMSSLERR; goto end; } // append container to container_list if (session->container_list == NULL) { session->container_list = container; } else { SOFTSDF_CONTAINER *current = session->container_list; while (current->next != NULL) { current = current->next; } current->next = container; } container = NULL; ret = SDR_OK; end: if (container) { memset(container, 0, sizeof(*container)); free(container); } if (pass) { memset(pass, 0, uiPwdLength); free(pass); } if (file) fclose(file); return ret; } 给我讲讲这个软实现的函数
这个函数的名字是 SDF_GetPrivateKeyAccessRight,它并不是“去获取访问权限”,而是用口令把一对 SM2 私钥(签名密钥和加密密钥)解密出来,并登记到当前会话里,此后会话就能用这两把密钥做签名、解密等运算。可以把整个流程看成一次“登录私钥”的动作。下面分步骤说明。

  1. 入口检查
    • 设备句柄 deviceHandle、会话句柄 hSessionHandle 必须有效。
    • pucPassword/uiPwdLength 不能为 NULL/0。
    • 把密码拷到本地缓冲区,并在末尾补 0,再检查 strlen 是否等于 uiPwdLength(防止密码里出现 0x00 的异常输入)。
  2. 建立容器 SOFTSDF_CONTAINER
    • 容器保存“本次登录”以后需要用到的密钥对象。
    • 容器用 key_index 做区分,同一个会话可以登录多个 key_index。
  3. 读取并解密私钥文件
    • 签名私钥:sm2sign-{key_index}.pem
    • 加密私钥:sm2enc-{key_index}.pem
    • 两个文件都用同一套口令解密,内部调用 gmssl 的 sm2_private_key_info_decrypt_from_pem。
    • 只要有一个文件打不开或解密失败,就返回 SDR_KEYNOTEXIST / SDR_GMSSLERR。
  4. 把容器挂到会话链表
    • session->container_list 是一个单链表,新节点插到末尾。
    • 成功后 container 指针被置 NULL,防止 end 标签里二次释放。
  5. 清理
    • 失败路径上若已分配 container、pass,则清零后释放;已打开的 FILE* 关闭。

函数返回值对照
  • SDR_OK 成功,私钥已登记到会话
  • SDR_STEPERR 设备未初始化
  • SDR_INARGERR 会话、密码参数有误
  • SDR_NOBUFFER malloc 失败
  • SDR_KEYNOTEXIST 找不到对应编号的私钥文件
  • SDR_GMSSLERR 私钥文件内容或口令错误,解密失败

一句话总结
SDF_GetPrivateKeyAccessRight 就是“用口令把磁盘上的两把 SM2 私钥(sign/enc)加载到内存,并绑定到当前会话”,之后所有需要签名或解密的操作都会去会话的 container_list 里找对应的密钥。
我测试这个函数,该传入什么参数?
先把“环境”布置好,再谈参数。
一、准备环境
  1. 先成功调用 SDF_OpenDevice/SDF_OpenSession,拿到有效的
    • deviceHandle(全局变量,函数里会检查它)
    • hSessionHandle(就是你得到的会话句柄)
  2. 在工作目录(进程当前目录)里放好两个 PEM 文件,名字必须是:
    sm2sign-<uiKeyIndex><uiKeyIndex>
  3. 记下当初生成私钥时用的口令(就是本函数的 pucPassword)。
二、调用示例(C 片段)
c
复制
void *hSession = NULL;        // 假设前面 SDF_OpenSession 已填充
unsigned int keyIndex = 1;    // 与文件名中的数字保持一致
char *password = "12345678";  // 当初加密私钥用的口令

int rv = SDF_GetPrivateKeyAccessRight(
            hSession,
            keyIndex,
            (unsigned char *)password,
            (unsigned int)strlen(password)
         );

if (rv != SDR_OK) {
    printf("GetAccessRight failed, ret = 0x%08X\n", rv);
} else {
    printf("Login OK, now you can use key pair %u\n", keyIndex);
}
三、常见踩坑
  • 文件必须叫 sm2sign-1.pem / sm2enc-1.pem,数字与 uiKeyIndex 一致;名字错了就 SDR_KEYNOTEXIST。
  • 口令输错会返回 SDR_GMSSLERR。
  • 如果之前没有成功调用 SDF_OpenDevice/OpenSession,deviceHandle 为 NULL,会返回 SDR_STEPERR。
pass[uiPwdLength] = 0; 这里为什么要补0
补 0 是为了把 uiPwdLength 字节长的“裸二进制口令”立即变成合法的 C 字符串,这样后面才能安全地用 strlen(pass) 去检查“是否含有 0x00”——这是库函数对字符串做长度验证的标准做法。
具体原因拆成三点:
  1. C 语言的字符串以 \0 结尾;strlen 会一直读到第一个 \0 为止。
  2. 如果用户传来的 pucPassword 里本身带有 0x00(例如有人故意把 "123\0456" 传进来),memcpy 得到的 pass 如果不补 0,那么 strlen(pass) 就会提前终止,结果小于 uiPwdLength,于是代码就能检测出这种异常输入并返回 SDR_INARGERR。
  3. 某些底层 crypto 函数(例如后面调用的 sm2_private_key_info_decrypt_from_pem)也要求传入以 \0 结尾的字符串,而不是裸二进制长度;补 0 可避免越界读取。
因此:
pass[uiPwdLength] = 0; 既是长度校验的需要,也是把二进制口令转换成 C 字符串的必要步骤。
为什么这样做,这其中的安全校验原理是什么?
把 pass[uiPwdLength] = 0 之后立刻用 strlen(pass) != uiPwdLength 做判断,这个看似简单的动作实际上在做两层安全校验:

  1. 检测“口令中是否含有 0x00”——防止嵌入截断攻击(embedded-NUL attack)
    • 如果攻击者把口令故意写成 "123\0456"(即第 3 字节是 0x00),
    ‑ 不补 0 时,strlen 只会数到 "123",返回 3,看起来比 uiPwdLength=7 小,程序就能立刻发现异常;
    ‑ 如果不做这一检查,后面 crypto 函数把 pass 当字符串使用时,会把 "123" 当成完整口令,剩下的 \0456 被忽略,导致口令强度骤降或出现与预期不符的密钥派生结果。
    • 因此 strlen(pass) != uiPwdLength 等价于“只要口令里出现 0x00 就拒绝”,堵住这类畸形输入。
  2. 保证后续 API 只拿到合法 C 字符串
    • OpenSSL/GMSSL 的 PEM 口令回调、PKCS#8 解密函数都要求参数是以 \0 结尾的 char *,而不是“二进制长度 + 起始指针”。
    • 补上 0 可以避免越界读取,防止信息泄露或内存破坏。

一句话总结
补 0 并立即用 strlen 比对长度,是用最小成本完成
“拒绝含 NUL 的畸形口令 + 把二进制口令安全转成 C 字符串”
这两件事,从而消除接口误用和潜在攻击面。
这个函数的名字是 SDF_GetPrivateKeyAccessRight,它并不是“去获取访问权限”,而是**用口令把一对 SM2 私钥(签名密钥和加密密钥)解密出来,并登记到当前会话里**,此后会话就能用这两把密钥做签名、解密等运算。可以把整个流程看成一次“登录私钥”的动作。下面分步骤说明。 ------------------------------------------------ 1. 入口检查 - 设备句柄 deviceHandle、会话句柄 hSessionHandle 必须有效。 - pucPassword/uiPwdLength 不能为 NULL/0。 - 把密码拷到本地缓冲区,并在末尾补 0,再检查 strlen 是否等于 uiPwdLength(防止密码里出现 0x00 的异常输入)。 2. 建立容器 SOFTSDF_CONTAINER - 容器保存“本次登录”以后需要用到的密钥对象。 - 容器用 key_index 做区分,同一个会话可以登录多个 key_index。 3. 读取并解密私钥文件 - 签名私钥:sm2sign-{key_index}.pem - 加密私钥:sm2enc-{key_index}.pem - 两个文件都用同一套口令解密,内部调用 gmssl 的 sm2_private_key_info_decrypt_from_pem。 - 只要有一个文件打不开或解密失败,就返回 SDR_KEYNOTEXIST / SDR_GMSSLERR。 4. 把容器挂到会话链表 - session->container_list 是一个单链表,新节点插到末尾。 - 成功后 container 指针被置 NULL,防止 end 标签里二次释放。 5. 清理 - 失败路径上若已分配 container、pass,则清零后释放;已打开的 FILE* 关闭。 ------------------------------------------------ 函数返回值对照 - SDR_OK 成功,私钥已登记到会话 - SDR_STEPERR 设备未初始化 - SDR_INARGERR 会话、密码参数有误 - SDR_NOBUFFER malloc 失败 - SDR_KEYNOTEXIST 找不到对应编号的私钥文件 - SDR_GMSSLERR 私钥文件内容或口令错误,解密失败 ------------------------------------------------ 一句话总结 SDF_GetPrivateKeyAccessRight 就是“用口令把磁盘上的两把 SM2 私钥(sign/enc)加载到内存,并绑定到当前会话”,之后所有需要签名或解密的操作都会去会话的 container_list 里找对应的密钥。 为什么这样做,这其中的安全校验原理是什么?
安全校验原理可以归结为一句话:
“在把任何密钥材料真正变成可用状态之前,必须同时验证三件事——身份(谁在用)+ 完整性(文件没被篡改/缺失)+ 机密性(口令正确且不含危险字符)”。
函数里每一步都在为这三件事服务:
  1. 身份校验
    • 先验证 deviceHandle 和 hSessionHandle 是否有效,确保“调用者确实经过 SDF_OpenDevice / SDF_OpenSession 流程,拥有合法会话”。
    • 这一步等价于“门禁系统先刷卡”,防止离线伪造会话句柄。
  2. 完整性校验
    • 必须同时存在 sm2sign-{idx}.pem 和 sm2enc-{idx}.pem,缺一不可。
    • 只要有一个文件缺失或无法打开就立即 SDR_KEYNOTEXIST,避免半把钥匙被加载,导致后续运算出现“签名/加密不匹配”或“算法不可用”。
  3. 机密性校验
    a) 口令长度与内容
    • pucPassword==NULL || uiPwdLength==0 → 拒绝空口令。
    • strlen(pass) != uiPwdLength → 检测口令里是否混入了 0x00,防止“截断攻击”或“多口令混淆”。
      b) 口令正确性
    • 实际用口令去解密 PEM,失败即 SDR_GMSSLERR,确保“只有知道正确口令的人才能解锁私钥”。
      c) 内存安全
    • malloc 失败返回 SDR_NOBUFFER,避免在内存不足时强行继续,造成后续越界。
    • 成功/失败路径全程清零敏感缓冲区,减小残留口令在内存里的时间窗口。
最终效果:
只有把“合法会话 + 完整的一对私钥文件 + 正确且格式干净的口令”全部凑齐,函数才允许把私钥对象挂到 container_list,后续调用才能使用这两把 SM2 密钥。任何环节不合规都会立即失败并清理痕迹,从而把风险降到最低。
为什么要用这样的私钥文件在这里?为什么要这样的口令校验?
先把“为什么”拆成两层:
A. 为什么非得把私钥放在 PEM 文件里,而不是直接烧进 HSM 或固化到代码里?
B. 为什么又要用 口令 去解锁,而不是用更复杂的“管理员+用户”双因子或硬件 PIN?
这两个问题其实都源于同一个现实:这是一个“软实现”(Soft-SDF)——没有真正的安全芯片,只能用通用操作系统 + 文件系统来模拟 SDF 接口。于是:
  1. 用文件 = 唯一可行的持久化手段
    • 没有 EEPROM/Flash 让密钥“断电也还在”。
    • 没有专用安全存储通道,只能依赖 OS 的文件系统。
    • PEM(PKCS#8 EncryptedPrivateKeyInfo)已经是跨平台、库支持最成熟的“带口令加密私钥”格式,OpenSSL/GMSSL 直接能解,减少自研格式带来的风险与工作量。
    • 文件路径按 key_index 命名,天然支持“多密钥、多用户”场景,无需额外数据库。
  2. 用口令 = 成本最低、接口最兼容的“软保护”
    • 软实现无法提供物理 PIN-pad、指纹、UKey 等硬件双因子;口令就成了“最低配”的机密因子。
    • 口令直接作为对称密钥(PEM 里通常是 PBKDF2 + SM4-CBC 或 AES-CBC)去加密私钥文件,实现“存储加密”。只要口令强度够,文件泄露也无法还原私钥。
    • 口令校验放在函数内部,可以立即拒绝错误尝试,防止离线暴力破解(文件本身没有速率限制,但函数调用次数可以被上层应用/守护进程统计)。
    • 对调用者(上层应用或测试脚本)来说,只需传一个 char *,接口与标准 PKCS#11/SDF 规范保持一致,学习成本最低。
一句话:
在“软实现”环境里,PEM 文件 + 口令 是“能用、够安全、好维护”的最小可行方案;换成硬件 HSM 就根本不需要文件,也无需在函数里做口令校验,而是由芯片内部完成 PIN/指纹/管理员密钥的复杂校验逻辑。
如果是硬件,接口也长这样呀?口令是干什么的?、
不,真正的硬件实现里 接口虽然名字一样(SDF_GetPrivateKeyAccessRight),但语义和实现完全不同,口令的角色也发生了根本变化。可以把两种场景对照着看:
  1. 软实现(当前代码)
    • 没有安全存储 → 私钥只能放在磁盘文件里 → 函数必须“用口令把文件解密再加载”。
    • 口令 = 私钥文件的 解密钥匙。
  2. 真正硬件(PCI-E/SM2 安全芯片)
    • 私钥 永远只存在于芯片内部 NVM/EEPROM,外部根本看不到明文。
    • 接口仍然提供 pucPassword/uiPwdLength,但芯片内部的处理流程通常是:
      ┌------------------------------------------------┐
      │ ① 芯片收到 key_index │
      │ ② 在内部 ACL 表里查: │
      │ • 该 key 是否已启用? │
      │ • 需要口令 / PIN / 管理员密钥 / 指纹 中的哪一种? │
      │ ③ 把用户传入的口令与芯片内部保存的 口令摘要 比对 │
      │ ④ 比对成功 → 把 key 的“使用权限”标记为已授权 │
      │ 比对失败 → 返回 SDR_GMSSLERR / SDR_PINERROR │
      └------------------------------------------------┘
    • 口令不再用于解密任何文件,而是作为 访问控制因子(类似 PIN 码),告诉芯片“现在允许本次会话使用这把内部私钥”。
    • 芯片内部有重试计数器、锁定策略、甚至双因子(管理员+用户),这些逻辑完全不暴露给驱动层。
因此:
• 软实现里,口令是“把文件解密”;
• 硬件实现里,口令是“向芯片证明你有权使用内部密钥”。
接口原型保持完全一致,是为了 上层应用零改动;但底层行为从“软件解密”变成了“硬件 ACL 校验”。
而是作为 访问控制因子(类似 PIN 码),告诉芯片“现在允许本次会话使用这把内部私钥”。 为什么要这样做?如果没有这个会出现什么问题?
在硬件里把口令当作“访问控制因子”而不是“解密钥匙”,根本目的是:把“密钥本身”和“谁有权用它”这两件事解耦;一旦缺了这层校验,就会出现三个致命风险:
  1. 任何拿到会话句柄的人都能随意用私钥
    在真正的 HSM 里,会话句柄只是操作系统内核里一个 32/64 位整数,如果芯片不做额外 ACL 判断,攻击者只要:
    • 用调试器把会话句柄“偷”出来,
    • 或者直接伪造一个句柄值,
      就能调用 SDF_InternalSign/Decrypt,让芯片把内部私钥当“公共钥匙”用,签名、解密随便做——相当于私钥“裸奔”。
  2. 无法区分“用户密钥”和“系统密钥”
    一台 HSM 往往存放多把密钥(用户密钥、CA 根密钥、日志签名密钥……)。没有口令/PIN 这一层,应用层就无法区分“谁该用哪把”,所有会话都能调用所有 key_index,造成 权限爆炸。
  3. 无法防御“丢失或被盗”的场景
    真实部署中,服务器整机可能被偷、PCI-E 卡可能被拔走插到别的机器。如果芯片内部没有口令/PIN + 重试锁定:
    • 拿到硬件的人直接通电就能用所有密钥;
    • 或者把卡插到自己写的恶意驱动里,暴力枚举 key_index 做签名。
      口令/PIN 就是“最后一道锁”,再配合内部重试计数器和自毁机制,把 物理安全 也纳入防护范围。
因此,口令作为访问控制因子,相当于在硬件内部建立了一个“最小授权”模型:
“会话句柄只是‘进门’,口令才是‘开抽屉’;两者缺一不可”。缺了它,整个 HSM 就退化成一块“谁都可用的通用加速器”,私钥的机密性和完整性同时丧失。
多讲讲“最小授权”模型和HSM
下面把「最小授权」模型与 HSM(Hardware Security Module)放到同一张图里讲透:
先给一个一句话定义,再拆成 5 个维度,最后用一张“日常比喻”巩固记忆。
──────────────────
一句话定义
「最小授权」在 HSM 里就是:
“钥匙永远在保险柜里,谁、什么时候、能开哪把锁、最多开几次,都由保险柜自己说了算;除此之外,连看都看不到钥匙。”
──────────────────
  1. 角色与粒度
    • 传统 OS:root 可以读所有文件。
    • HSM:
    – 分成 Admin、Operator、User、Audit 四种角色;
    – 每把内部密钥可再细粒度绑定「角色 + 口令/PIN + 计数器 + 策略」。
    – 即使 Admin 也只能「初始化/备份」而无法直接用私钥签名,签名动作只能由 User 角色完成。
    结果:单点泄露不会扩散到整把钥匙。
──────────────────
2. 会话 vs. 授权
• 会话句柄 = 进门卡(仅认证“我到了门口”)。
• 口令/PIN/生物 = 抽屉钥匙(认证“我能开哪把锁”)。
• 授权对象 = ACL 条目(key_index → 角色 → 口令摘要 → 计数器)。
会话和授权解耦后,产生两个好处:
– 会话可复用(长连接),但每次运算仍要重新校验授权,减少频繁插卡。
– 会话句柄泄露也无法绕过授权,因为没有抽屉钥匙。
──────────────────
3. 计数器与锁定
• 口令重试计数器(Retry Counter):N 次错误 → 锁定该密钥或整张卡。
• 时间窗计数器(Rate Limit):M 秒内只能 T 次运算,防自动化爆破。
• 自毁阈值(Destruct Threshold):连续攻击达到上限 → 密钥清零或芯片熔丝熔断。
──────────────────
4. 物理安全边界
• 真随机源、CPU、NVM、电源检测网全部封装在同一颗安全芯片里。
• 外部总线只走加密/签名指令和返回结果,私钥明文永远不会出现在芯片引脚。
• 温度、电压、光、电磁异常即触发“环境传感器”,立即锁死或清零。
这就是“保险柜外壳”——任何撬柜动作都会让钥匙自毁。
──────────────────
5. 合规与审计
• FIPS 140-2/3 Level 3/4 要求:
– 角色分离、最小授权、物理防撬、审计日志必须不可改(append-only)。
• 日志记录「谁在什么时刻对哪把密钥执行了什么操作」,即使 Admin 也无法删除,满足金融、政务、CA 的合规审计需求。
──────────────────
日常比喻
把 HSM 想成「银行金库的自动保险柜」:
• 金库门卡(会话)只能让你进到走廊。
• 每个抽屉都有单独数字转盘锁(口令/PIN + 计数器)。
• 金库墙壁是防爆的(物理安全)。
• 任何撬锁尝试都会触发报警并喷墨烧毁钥匙(自毁)。
• 摄像头 24h 录像,录像带一次写入不可删(审计日志)。
这样,即便有人偷走整座金库,只要不掌握抽屉密码,仍旧拿不到钱——这就是最小授权在 HSM 里的全部意义。
那服务器都被偷了,为什么会没有口令?
服务器被偷并不意味着口令一定同时落入攻击者手里——这就是“两层保管”的意义:
  1. 口令不在服务器硬盘上
    • 口令由运维人员或密钥管理员手动输入(开机启动、udev 规则、或 systemd-ask-password),只存在于内存,不落盘。
    • 也可以借助密钥分发系统(如 HashiCorp Vault、KMIP 服务器、HSM 的分段密钥),服务器每次启动时动态拉取口令片段,重启即失。
  2. 口令不在 HSM 里存储明文
    • HSM 只保存口令摘要或PIN 派生密钥,验证失败即锁卡。
    • 摘要算法带 salt + 迭代,离线暴力破解耗时极高。
  3. 物理隔离的“口令保管人”
    • 典型做法是双人双控:服务器放在 IDC,口令写在密封信封或密码卡中,由另一位安全官保管;
    • 需要紧急启动时,两人同时到场输入,缺一不可。
  4. 即使偷走整机也无法重启
    • 服务器断电后,HSM 会丢失“已授权”标记,口令必须重新输入。
    • 攻击者即使把硬盘整块镜像,也拿不到口令;把主机搬到异地,也无法让 HSM 重新授权。
因此,“服务器被偷” 只等于偷走了硬件外壳,口令通常以人脑、密码卡、远程 KMS等形式单独保管,不会随机器一起丢失。
把“密钥本身”和“谁有权用它”这两件事解耦;顺着这个思路,回答我: 为什么有了数字签名技术,还要用消息鉴别?它们都是将消息和拥有消息的实体可信地联系起来?和这个密钥本身与谁有权拥有它解耦 有什么联系? 为什么还有身份鉴别技术?三者之间的联系是什么?背后的思想是什么?
把四件事放在同一条“信任链”上看,就能看清它们如何层层解耦、又层层衔接:
  1. 密钥本身
    只是一串随机比特,本身没有任何“意义”。
  2. 谁有权用它(最小授权 / 访问控制)
    通过 PIN、口令、ACL、HSM 角色,把“实体→密钥使用权”绑定,但不透露密钥。
  3. 消息鉴别(MAC/HMAC/SM3-MAC …)
    解决“这条消息在传输途中是否被篡改”。
    它用的对称密钥本身并不表明“谁”产生的消息,只表明“知道这把对称密钥的人”认可该消息。
    → 密钥与身份未解耦:拿到密钥就能伪造 MAC。
  4. 数字签名(非对称签名 / SM2-Sig …)
    用私钥签名、公钥验证。
    公钥可以公开,私钥永远留在 HSM。
    → 密钥与身份完成解耦:
    • 私钥只负责“计算签名”;
    • 身份由“公钥证书”把公钥→实体绑定;
    • 谁有权用私钥由 HSM 的访问控制决定。
      于是形成“实体 / 密钥使用权 / 签名结果”三层分离。
  5. 身份鉴别(挑战-响应、TLS 握手、Kerberos …)
    解决“此刻与我通信的实体是不是声称的那个实体”。
    它往往组合使用 MAC 或数字签名:
    • 早期系统资源受限 → 用对称 MAC(快速,但共享密钥)
    • 现代系统 → 用数字签名(无需共享密钥,可撤销、可审计)。
      → 身份鉴别协议把“实体身份”与“密钥使用权”再次解耦:
      “我现在证明我拥有私钥使用权,但私钥不会离开 HSM,也不会被协议泄露。”

一句话总结背后的统一思想
“把敏感资产(密钥、消息、身份)从逻辑与物理上逐层最小化暴露,任何一层泄露都不足以破坏整体信任。”
  • 密钥本身 → 只存 HSM,永不导出。
  • 消息完整性 → MAC/签名,只暴露结果,不暴露密钥。
  • 身份识别 → 证书或协议,只暴露可验证的断言,不暴露私钥。
这就是“最小授权”原则从“谁能用密钥”扩展到“谁能产生可信消息”再到“谁能证明自己是某个实体”的完整链路。
这个叫什么?是零信任架构吗
不是“零信任架构”,而是信息安全里更早、更底层的一套原则——
最小权限原则(Principle of Least Privilege,PoLP) + 分层信任模型(Layered Trust)。
• 最小权限原则:每一层只拿到完成自己任务所必需的最小权力。
‑ 密钥 → 只留在 HSM;
‑ 消息 → 只拿到签名/ MAC 结果;
‑ 身份 → 只拿到可验证的断言。
• 分层信任模型:把“密钥本身 / 密钥使用权 / 消息完整性 / 身份鉴别”逐层解耦,任何单点失守都不会导致整体信任崩塌。
零信任(Zero Trust Architecture)是在“网络 + 身份 + 设备”层面做的“永不信任、持续验证”,属于更高层、更宏观的体系架构;而这里讨论的仍是密码学原语及其访问控制的经典安全设计思想。
消息能被篡改因此需要完整性校验,能被被截获因此需要身份鉴别,那数字签名呢?
数字签名 同时 解决了三件事,而不是“只解决其中一件”:
  1. 完整性——任何比特被改,签名就验证失败。
  2. 身份鉴别——只有拥有对应私钥的实体才能产生有效签名,验证方用公钥/证书即可确认“是谁签的”。
  3. 不可否认性(Non-repudiation)——签名方事后无法抵赖“我没签过”,因为私钥只有 TA 能掌握。
因此:
• “篡改”对应完整性;
• “截获”对应身份鉴别;
• “事后抵赖”对应不可否认;
数字签名在一条操作里就一次性把这三重风险全部覆盖。