开源之夏-20

今天就可以把测试框架对齐GM/T 0018 2023了!

一些零散的疑问:

Session

为什么所有的操作都要在Session内完成?

所有操作都要在Session内完成,原因如下:

  1. 资源隔离与安全
    Session(会话)为每个用户/进程分配独立的资源和上下文,防止不同用户/进程间的数据、密钥、状态相互干扰或泄露。

  2. 状态管理
    Session保存操作过程中的临时状态(如密钥句柄、Hash上下文、协商参数等),保证多步操作的连续性和正确性。

  3. 并发支持
    多个Session可并发存在,支持多用户/多线程安全地同时操作同一台设备。

  4. 权限控制
    Session可绑定访问权限(如私钥访问、密钥句柄),便于实现细粒度的安全策略和权限回收。

  5. 资源释放
    Session关闭时,相关的临时密钥、句柄、缓冲区等资源会自动释放,防止资源泄漏和安全隐患。

总结:
Session机制是密码设备/库实现安全隔离、状态管理、并发支持和资源回收的基础,所有操作都必须在Session内完成,才能保证安全性和正确性。

HMAC和可鉴别加密

HMAC和“可鉴别的对称加密”(如GCM/CCM模式)都能实现“认证+完整性”,但原理和应用略有不同:

  1. 工作原理
  • HMAC:先用对称密钥对数据做Hash,生成认证码(MAC),单独用于认证和完整性校验,不加密数据本身。
  • 可鉴别对称加密(如SM4-GCM/CCM):加密数据的同时,自动生成认证标签(Tag),既保证机密性,又保证认证和完整性。
  1. 关系
  • 可鉴别加密=加密+认证(通常内部实现会用类似HMAC/MAC的机制生成Tag)。
  • HMAC只做认证,不加密数据;可鉴别加密同时完成加密和认证。
  1. 应用场景
  • HMAC:用于消息认证、API鉴权、协议认证等,只关心数据是否被篡改、是否来自合法方。
  • 可鉴别加密:用于需要同时保证数据机密性和认证性的场景,如安全通信、加密存储等。
  1. 组合方式
  • 有些协议会“先加密后HMAC”,但现代推荐直接用GCM/CCM等可鉴别加密模式,一步完成。

总结:

  • HMAC只做认证,不能加密数据。
  • 可鉴别对称加密同时加密和认证,安全性更高,使用更方便。
  • 现代安全通信优先推荐可鉴别加密模式(如GCM/CCM),无需单独HMAC。

能返回密钥句柄的函数

后边对称加密的接口会用到密钥句柄,所以需要对这几个接口熟悉
后边测试的时候调用这几个接口,生成密钥句柄。

  • SDF_GenerateKeyWithIPK_RSA

  • SDF_GenerateKeyWithEPK_RSA

  • SDF_ImportKeyWithISK_RSA

  • SDF_GenerateKeyWithIPK_ECC

  • SDF_GenerateKeyWithEPK_ECC

  • SDF_ImportKeyWithISK_ECC

  • SDF_GenerateKeyWithKEK

  • SDF_ImportKeyWithKEK

  • SDF_GenerateKeyWithECC(密钥协商)

  • SDF_GenerateAgreementDataAndKeyWithECC(密钥协商)

KEK(密钥加密密钥)

通常是持久密钥,有设备内部生成并长期安全存储。
专门用于加密/解密会话密钥,实现密钥分级和安全传递。只能在设备内部用于密钥封装和解封装。

  • KEK在设备内部生成、写入并保存,不能导出明文。
  • KEK的生命周期长,通常随设备初始化或密钥管理流程生成,只有设备内部能直接使用。
  • 业务系统或外部应用无法直接获取KEK明文,只能通过索引(如KEKIndex)引用。

IPK/EPK/ISK<=>KEK

问题在于:已经有了像GenerateKeyWithIPK,EPK,ISK这样的过程
还要KEK吗?

KEK(密钥加密密钥)与IPK/EPK/ISK(内部/外部公钥)各有不同的应用场景和安全需求,不是互相替代,而是互补:

1. EPK/ISK(公钥加密/封装会话密钥)
  • 用于跨系统、跨设备安全传递会话密钥。
  • 适合“非对称”场景:如A系统用B的公钥加密会话密钥,B用私钥解封装。
  • 典型场景:分布式系统、远程密钥分发、密钥协商。
  • 安全性高,但运算速度慢(非对称加密)。
2. KEK(对称密钥加密/封装会话密钥)
  • KEK用于同一设备或同一安全域内的密钥封装、迁移、备份
  • KEK属于“对称加密”,速度快,适合高频密钥操作。
  • 运算速度快,适合高频密钥操作。适合“性能优先、安全可控”的场景。
  • 典型场景:设备内密钥备份、密钥迁移、密钥生命周期管理。
3.IPK(内部公钥)
  • IPK用于同一台设备内的不同业务进程/模块之间安全传递会话密钥,防止密钥在内存或进程间被窃取。
  • 但本质上,IPK依然是“非对称加密”,即用公钥加密、私钥解密,安全性高,但运算速度慢。
  • 适合“安全优先、性能次要”的内部隔离场景,或有严格安全隔离需求的多租户设备。
    总结:
    KEK不是用来替代IPK/EPK/ISK,而是为设备内部高效安全地管理和传递密钥提供补充。
    IPK/EPK/ISK适合跨设备/跨域,KEK适合设备内部或同域高效密钥封装,两者共同构成完整的密钥管理体系。

关于SM2签名算法中的Z值

Z值(又称ZA)是SM2签名算法中的一个“用户身份绑定杂凑值”,用于防止公钥替换攻击和实现签名与用户身份的绑定。
有一条业务流程:

  • 公钥:用ExportSignPublicKey_ECC
  • 摘要:用SDF_HashInit/Update/Final (自动SM2预处理)
  • 签名:用InternalSing_ECC
  • 验签:用ExternalVerify_ECC

自动完成SM2预处理和SM3摘要计算,依赖于SDF标准的SDF_HashInit接口的特殊用法:
SM2签名预处理要求:先计算Z值(与公钥、用户ID等相关),再拼接Z||M(M为原始消息),最后对Z||M做SM3杂凑,得到摘要。
这个摘要才是后续签名/验签的输入。

SDF标准规定:

  • 当调用SDF_HashInit时,如果传入了ECC公钥和用户ID参数,设备/库会自动完成Z值计算和拼接。
  • 后续只需用SDF_HashUpdate输入原始消息,设备/库会自动拼接Z||M并做SM3。
  • SDF_HashFinal输出的就是“SM2预处理+SM3”后的摘要。SDF_HashFinal输出的就是“SM2预处理+SM3”后的摘要。

Z值的组成

Z值 = SM3(用户ID || 公钥参数 || 椭圆曲线参数)
包含用户ID(如身份证号、用户名等)、SM2公钥(x, y)、椭圆曲线参数(a, b, G, n, p)等。
这样可以确保签名不仅和消息相关,还和用户身份、公钥绑定。

作用

  • 防止攻击者替换公钥后伪造签名(即“公钥替换攻击”)。
  • 使签名结果与用户身份唯一绑定,提升安全性。
    在SM2签名流程中的位置
    签名前,先计算Z值。
    签名摘要 = SM3(Z || M),M为原始消息。
    验签时同样先计算Z值,再拼接消息做SM3。

密钥协商

SDF_GenerateKeyWithECC(密钥协商)

密钥协商完成后,协商句柄被销毁。返回会话密钥句柄。

总流程:GenerateKeyWithECC发起方调用的。

在SM2密钥协商流程中,发起方先调用 GenerateAgreementDataWithECC 生成自己的协商参数,响应方调用 GenerateAgreementDataAndKeyWithECC 生成自己的协商参数和会话密钥。

然后,发起方收到响应方的协商参数后,调用 GenerateKeyWithECC,结合双方参数计算出会话密钥。

  • SDF_GenerateAgreementDataWithECC

    • SponsorID/SponsorIDLength:协商参数

    • ISKIndex/KeyBits:发送方长期公钥的索引/长度

    • pSponsorPublicKey:接口会自动填充为发起方的长期ECC公钥(由ISKIndex指定)。

    • pSponsorTmpPublicKey:接口会自动生成临时密钥对,并填充临时公钥。

    • &phAgreementHandle:协商句柄

  • SDF_GenerateAgreementDataAndKeyWithECC

    • 接收发送方的协商参数,发送接收方的协商参数,长期公钥,临时公钥
    • 接收会话密钥句柄

这里即使用主公钥(长期密钥)又使用临时公钥,为什么?

需要“发起方主公钥”和“临时公钥”是因为它们在密钥协商中各自承担不同的安全角色,二者缺一不可:

1. 主公钥(长期公钥)的作用

  • 用于身份认证和密钥协商的基础信任锚。
  • 只有主公钥是经过CA/设备认证、长期绑定用户身份的,能防止中间人伪造身份。
  • 协商双方通过主公钥确认对方身份,保证协商不是和攻击者进行。

2. 临时公钥的作用

  • 用于提升前向安全性(forward secrecy)。
  • 每次会话动态生成,协商出的会话密钥与长期密钥无关,即使长期密钥泄露,历史会话密钥也不会被恢复。
  • 防止重放和密钥复用攻击。

3. 为什么要同时用?

  • 只用主公钥:有身份认证,但没有前向安全性,长期密钥一旦泄露,所有历史会话都不安全。
  • 只用临时公钥:有前向安全性,但无法认证对方身份,容易被中间人攻击。
  • 两者结合:既能认证身份,又有前向安全性,是现代密钥协商协议(如SM2密钥协商、ECDHE等)的标准做法。

结论:
主公钥保证身份可信,临时公钥保证每次会话密钥独立且安全。两者结合,才能实现既安全又可靠的密钥协商。
临时公钥不是替代主公钥,而是补充主公钥的安全短板。

柔性数组

检查ECCCipher结构时,标准里给的是 C[]
发现GmSSL有这样的处理:

1
2
3
4
5
6
7
8
9
10
typedef struct ECCCipher_st {
unsigned char x[ECCref_MAX_LEN];
unsigned char y[ECCref_MAX_LEN];
unsigned char M[32];
unsigned int L;
unsigned char C[1];
// Extend sizeof(C) to SM2_MAX_PLAINTEXT_SIZE
// gmssl/sm2.h: SM2_MAX_PLAINTEXT_SIZE = 255
unsigned char C_[254];
} ECCCipher;

C标准允许结构体最后一个成员是C[1](或C[],C99后),是为了实现“柔性数组成员”(flexible array member),用于支持变长结构体。

原理如下:

  • 在C89/C90时代,没有柔性数组,常用C[1]作为结构体最后一个成员,配合malloc分配更大空间,实现变长数据区。
  • 这样定义后,结构体大小只包含C[1]的1字节,但你可以分配更大空间,把多余部分当作C数组的后续元素来用。
  • C99标准正式引入了C[](柔性数组成员),允许结构体最后一个成员是未指定长度的数组,更加规范和安全。

用途:

  • 主要用于协议、密码学等需要变长数据的场景。
  • 方便通过结构体指针访问变长数据区。

总结:C[1]是历史兼容写法,C99后推荐用C[]。它们都用于支持结构体变长数据的内存布局。

历史写法示例(C89/C90):

1
2
3
4
typedef struct {
int len;
char data[1]; // 变长区起点
} MyStruct;

分配更大空间的方法:

1
2
3
4
int datalen = 100;
MyStruct *p = (MyStruct *)malloc(sizeof(MyStruct) + (datalen - 1) * sizeof(char));
p->len = datalen;
// 现在 p->data[0] ~ p->data[99] 都可以安全访问

说明:

  • data[1] 只是占位,实际分配时多分配 (datalen-1) 个字节。
  • 这样结构体末尾的 data 区就能容纳 datalen 字节的内容。
  • 访问时直接用 p->data[i],i=0~datalen-1。

C99后推荐用 data[],分配方式类似。


开源之夏-20
https://43.242.201.154/2025/08/24/开源之夏-20/
Author
Dong
Posted on
August 24, 2025
Licensed under