开源之夏-17
tags:
SDF标准里最后12个是验证测试类函数。
这类接口的功能是被厂商设备实现的。但是为了调用测试其它标准SDF接口,需要我在这里软实现几个。
接上了!上个月学到:Base64是一个重要的工具,能够把字节流数据变成字符串,方便传输。
但是当时根本不理解为什么要用字节流,大整数生成器、随机数生成器生成的数字直接编码成字符串传输不行吗?
BIGNUM → 字节流(BN_bn2bin)→ Base64编码 → 字符串
走字节流(二进制)这条路线的主要原因还是安全:
国际和行业标准(如ASN.1/DER、SDF、PKCS#1等)都规定敏感数据必须用二进制格式,保证互操作和安全。
CMoments: 这类API(不论是厂商扩展还是管理命令)主要用于设备出厂、首次初始化、密钥轮换等场景。
一般只允许管理员或厂商工程师在安全环境下操作,普通业务系统不会直接调用。
生成私钥后,私钥是不是就写入硬件了
GitHub Copilot: 是的,通常情况下,私钥在生成后会直接写入硬件安全模块(HSM/加密机/安全芯片)内部的受保护存储区。写入后:
- 私钥不会导出到外部,也无法被普通业务系统读取或导出。
- 只有硬件内部能使用私钥进行签名、解密等操作,外部只能通过keyIndex等引用方式调用。
- 这样可以最大限度保证私钥安全,防止泄露。
总结:
- 私钥生成后立即写入硬件内部存储,且不可导出。
- 业务系统只通过标准接口和keyIndex使用私钥,无法直接访问私钥内容。
- 这正是SDF等安全接口设计的核心原则。
CMoments:
struct SOFTSDF_SESSION {
SOFTSDF_CONTAINER *container_list;
SOFTSDF_KEY *key_list;
SM3_CTX sm3_ctx;
struct SOFTSDF_SESSION *next;
};
为什么它的softsession使用sm3的数据结构定义的?
GitHub Copilot: 这是因为在 SDF/国密接口实现中,SM3 是国密标准的哈希算法,很多 SDF 操作(如摘要、签名、验签、HMAC 等)都需要用到哈希上下文(context)。
在 soft_sdf 这类软件实现里,SM3_CTX 结构体用于保存哈希运算的中间状态。
将 SM3_CTX 作为 SOFTSDF_SESSION 的成员,主要目的是:
- 支持“分步哈希”操作(如 SDF_HashInit/SDF_HashUpdate/SDF_HashFinal),每个 session 都有独立的哈希上下文,互不干扰。
- 保证多线程/多会话环境下,每个 session 的哈希状态独立,避免数据串扰。
- 方便实现 SDF 标准接口的哈希相关功能。
总结:
- sm3_ctx 作为 session 成员,是为了支持 SDF 标准的哈希操作,每个 session 独立保存哈希状态。
- 这样设计是标准实现方式,便于支持 SDF_HashInit/Update/Final 等接口。
CMoments: 厂商的实际实现也会是这样吗?
GitHub Copilot: 大多数厂商的实际实现也会采用类似的设计思路:
每个 session(会话)结构体中会包含哈希算法(如SM3、SHA256等)的上下文(context)成员,用于支持分步哈希操作(HashInit/Update/Final)和多会话隔离。
但具体实现细节可能会有差异,比如:
- 有的厂商用自己的哈希上下文结构体(不一定叫SM3_CTX,但作用类似)。
- 有的硬件实现哈希上下文直接存在硬件内部,由session句柄间接引用。
- 有的实现支持多种哈希算法,会用联合体或指针保存不同算法的context。
核心思想是一样的:每个session独立保存哈希状态,保证多线程/多会话安全和标准接口兼容。
总结:
- 设计思路和原理与soft_sdf类似,都是为支持标准SDF哈希接口。
- 具体结构体和成员名称可能不同,但功能本质一致。
CMoments: 这个设计有行业标准吗?给我个一般常用的样子示例?
GitHub Copilot: SDF/PKCS#11等加密接口行业内并没有强制规定“session结构体”必须怎么设计,但通用做法是:
每个session结构体中包含与该会话相关的上下文信息(如哈希、对称/非对称密钥、缓冲区等),以支持多会话并发和分步操作。
常见设计示例(伪代码,适用于SDF/PKCS#11/通用HSM):
1 |
|
行业常见要点:
- 每个session独立保存所有与该会话相关的状态和上下文,保证线程安全。
- 哈希上下文通常用union或指针,支持多种算法。
- 结构体成员根据实际支持的算法和功能扩展。
- 业务层通过session句柄操作,底层自动管理上下文。
总结:
- 没有强制标准,但“每会话独立上下文”是业界通用设计。
- 结构体成员根据支持的算法和功能灵活扩展。
如需更详细的行业实现示例(如PKCS#11、OpenSSL ENGINE等),可进一步提供。
CMoments: int SDF_GenerateKeyPair_RSA(void *hSession, unsigned int keyBits, RSArefPublicKey *pubKey, RSArefPrivateKey *priKey);
{
int ret = 0;
return ret;
}
教我如何实现这个
GitHub Copilot: SDF标准并没有规定SDF_GenerateKeyPair_RSA这个接口,但你可以实现一个“软件模拟”版本用于测试。常见做法是调用OpenSSL等密码库生成RSA密钥对,然后填充到RSArefPublicKey和RSArefPrivateKey结构体中。
下面是一个常用实现思路(伪代码,适合软实现/测试):
- 用OpenSSL生成RSA密钥对
- 把OpenSSL的RSA结构体内容拷贝到RSArefPublicKey和RSArefPrivateKey
示例(需链接OpenSSL库):
1 |
|
要点说明:
- 需要在Makefile中加上-lssl -lcrypto
- 结构体字段长度要和你的定义一致
- 只适用于软件测试,实际硬件设备不会导出私钥
如需完整可用代码或移植到你的项目,请告知!
CMoments: BN类接口是干什么的?这里为什么用的不是EVP?这里可以用铜锁库吗?帮我实现
GitHub Copilot: BN类接口(如BIGNUM、BN_bn2binpad等)是OpenSSL用于大整数(多精度整数)运算的底层API,专门处理RSA等公钥算法中的大数(如模数n、指数e、d等)。
- RSA密钥的各个参数(n, e, d, p, q, dmp1, dmq1, iqmp)本质上都是大整数,所以用BN接口来生成、转换和导出。
为什么不用EVP?
- EVP是OpenSSL的高层通用加密接口,适合做加解密、签名等操作,但生成和导出RSA密钥参数时,底层还是要用RSA/BIGNUM接口来访问各个参数。
- 你要把密钥参数填充到SDF结构体,必须能逐个访问这些大数,EVP_PKEY不直接暴露这些字段。
铜锁库(gmssl/铜锁)也可以用,只要能生成RSA密钥并导出各参数,填充到你的结构体即可。用法和OpenSSL类似。
下面是用OpenSSL实现的完整SDF_GenerateKeyPair_RSA(适合软实现/测试):
请确保你的Makefile链接了-lssl -lcrypto。
1 |
|
CMoments: 如果上边应用层的程序员想生成密钥对,也得用BN接口吗?
GitHub Copilot: 应用层程序员如果想“直接生成RSA密钥对并访问各参数”,确实通常需要用到BN(BIGNUM)接口,因为只有BN接口能让你访问和导出RSA的各个大整数参数(n, e, d, p, q等),并填充到自定义结构体(如RSArefPublicKey/RSArefPrivateKey)中。
常见场景如下:
- 你要把密钥参数导出到自定义结构体或文件(如SDF/国密接口、国密格式等),就必须用BN接口逐个导出。
- 如果只是做加解密/签名/验签等高层操作,可以直接用EVP接口,不需要关心底层大数。
总结:
- 需要“访问/导出密钥参数”时,必须用BN接口。
- 只做加解密/签名等高层操作时,用EVP接口即可。
如需BN用法或EVP与BN结合的示例代码,可随时提供。
1 |
|
要点:
- 头文件用
<openssl/rsa.h>
和<openssl/bn.h>
,GmSSL完全兼容。 - 结构体字段长度用你自己头文件的宏。
- 只需链接
-lssl -lcrypto
或-lgmssl
。
如需自动插入到你的sdf_defs.c,请告知!
CMoments: @workspace /explain #include errors detected based on information provided by the configurationProvider setting. Squiggles are disabled for this translation unit (/home/ddd/SDF-Load/sdf_defs.c)., cannot open source file “openssl/rsa”
Workspace: 你的 #include<openssl/rsa>
写法有误,应该是包含头文件(.h),而不是目录。
正确写法:
1 |
|
- OpenSSL 的头文件都以
.h
结尾。 - 确保你的编译命令里有
-I
参数指向 OpenSSL 的头文件目录(如/usr/include/openssl
,但通常系统默认已包含)。
Makefile 示例补充:
如果你的 OpenSSL 安装在标准路径,通常不需要加 -I
。如果是自定义路径,需加:
1 |
|
链接时还需加:
1 |
|
否则会找不到 OpenSSL 的实现。
总结:
- 头文件名要写全:
<openssl/rsa.h>
- 编译时要链接 OpenSSL 库:
-lssl -lcrypto
CMoments:
if (!pubKey || !priKey) return SDR_INARGERR;
为什么要有这个判断?pubKey和priKey不应该就是传进来的空指针吗?
GitHub Copilot: 这个判断是为了防止“传入的指针本身为NULL”,即调用者传递了无效的指针(如SDF_GenerateKeyPair_RSA(…, NULL, NULL)),这样会导致后续对pubKey/priKey的写操作发生段错误(Segmentation Fault)。
- pubKey和priKey应该是“指向结构体的有效指针”,函数内部才能安全地写入数据。
- 如果传进来就是NULL,说明调用者用法有误,应该立即返回错误码(SDR_INARGERR),而不是继续操作。
总结:
- 这是健壮性防护,防止空指针解引用导致程序崩溃。
- 只要传入的是“已分配好空间的指针”,就不会触发这个判断。
CMoments: # define RSA_F4 0x10001L这是多少?干什么用的,是65537吗?
GitHub Copilot: 是的,#define RSA_F4 0x10001L
就是十进制的 65537。
用途:
- 这是RSA密钥生成时常用的公钥指数(public exponent),即e的默认值。
- 65537是一个质数,且二进制表示只有两位1,运算高效且安全性好,所以被广泛用作RSA公钥指数。
在OpenSSL/GmSSL等库中,生成RSA密钥时通常用RSA_F4
(65537)作为e的值。
CMoments: OSSL_DEPRECATEDIN_3_0 int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e,
BN_GENCB *cb);
这是干什么的函数?为什么要这样设计?
GitHub Copilot: RSA_generate_key_ex
是 OpenSSL/GmSSL 用于生成 RSA 密钥对的标准函数。
作用:
- 自动生成一对新的 RSA 密钥(公钥+私钥),并填充到传入的
RSA *rsa
结构体中。 - 你可以指定密钥位数(
bits
,如2048)、公钥指数(e
,如RSA_F4/65537),以及可选的回调(cb
,一般传NULL)。
参数设计原因:
RSA *rsa
:输出参数,生成的密钥对会写入这个结构体。int bits
:指定密钥长度,安全性和性能的权衡。BIGNUM *e
:指定公钥指数,允许自定义(但一般用65537)。BN_GENCB *cb
:生成过程的回调(可用于进度显示等,普通用法传NULL)。
这样设计的好处:
- 灵活:可以自定义密钥长度和公钥指数,适应不同安全需求。
- 兼容性好:与OpenSSL的密钥管理体系一致,便于与其他API协作。
- 安全:内部自动调用高质量随机数生成器,避免手动实现的安全隐患。
总结:
- 这是OpenSSL/GmSSL官方推荐的RSA密钥生成接口,安全、灵活、易用。
- 你只需分配好RSA结构体,设置好参数,调用即可自动生成密钥对。
CMoments: RSA_new()
为什么要这样设计?直接分配结构体不好吗?
GitHub Copilot: RSA_new()
是 OpenSSL/GmSSL 提供的专用“构造函数”,用于安全、正确地分配和初始化 RSA 结构体。
直接分配结构体(如 RSA rsa;
或 malloc(sizeof(RSA))
)是不安全的,原因如下:
- RSA 结构体内部有很多指针成员(如 BIGNUM *n, *e, *d, …),需要单独分配和初始化,不能简单用 memset 或 malloc。
RSA_new()
会自动初始化所有成员,保证后续 API(如密钥生成、释放)能正确工作。- 直接分配结构体会导致内存泄漏、野指针、崩溃等问题,OpenSSL 官方明确禁止这样做。
总结:
- 必须用
RSA_new()
创建,RSA_free()
释放。 - 这是 OpenSSL/GmSSL 的最佳实践和安全要求。
直接分配结构体只适用于没有指针成员的简单结构体,不适用于 RSA 这种复杂对象。
CMoments:
1 |
|
为什么要这样?手动补全padding?
GitHub Copilot: 这样做的原因是:RSArefPublicKey/RSArefPrivateKey结构体里的各个字段(如m、e、d、p、q等)都是“定长字节数组”,而OpenSSL的BIGNUM导出时长度是“实际有效字节数”,可能小于最大长度。
BN_bn2binpad的作用:
- 自动将BIGNUM(大整数)转换为定长字节数组(高位补零),保证结构体字段总是填满(如2048位n就是256字节,不足高位补0)。
- 这样结构体内容才符合SDF/国密等接口的定长要求,便于跨平台、跨语言、跨实现兼容。
如果不用pad,直接BN_bn2bin导出,前面高位可能是脏数据或未初始化,导致密钥解析错误或安全隐患。
总结:
- BN_bn2binpad保证结构体字段“定长、高位补零”,符合标准接口要求。
- 这是SDF/国密等接口与OpenSSL互操作的标准做法。
CMoments: 接口为什么这样叫?为什么要这样设计?OpenSSL的BIGNUM导出时长度是“实际有效字节数”
自动将BIGNUM(大整数)转换为定长字节数组(高位补零),
GitHub Copilot: 接口这样设计、命名(如BN_bn2binpad),是为了解决“不同系统/协议/标准对大整数的字节表示要求不同”的问题:
- OpenSSL的BIGNUM是“变长大整数”,导出时只输出实际有效字节(去掉高位0),节省空间,适合通用加密运算。
- 但很多协议/标准(如SDF、国密、PKCS#1、X.509等)要求密钥参数必须是“定长字节数组”,高位不足要补0,便于结构体对齐、跨平台兼容、二进制序列化。
- BN_bn2binpad就是为这种“定长补零”场景设计的,自动把BIGNUM导出为指定长度的字节数组,高位补0,保证输出总是定长。
这样设计的好处:
- 保证密钥结构体、协议消息等二进制格式一致,易于互操作和标准化。
- 避免手动补零出错,提高安全性和兼容性。
- 便于和硬件、国密等定长接口对接。
总结:
- OpenSSL的BIGNUM导出是变长,节省空间。
- 标准接口/协议要求定长,高位补零。
- BN_bn2binpad自动完成这一步,安全、规范、易用。
CMoments: 现在是不是都是大端存储了?
GitHub Copilot: 是的,绝大多数密码学协议、标准(包括SDF、国密、PKCS#1、X.509、OpenSSL等)在导出/存储大整数(如RSA/ECC密钥参数)时,都是采用大端字节序(Big Endian):
- 高位字节在前,低位字节在后。
- 这也是BN_bn2bin/BN_bn2binpad等OpenSSL导出函数的默认行为。
这样做的原因:
- 保证跨平台、跨语言、跨实现的二进制兼容性。
- 与ASN.1、DER等国际标准一致。
总结:
- 密钥参数导出/存储时,几乎都是大端字节序。
- 你只需按BN_bn2binpad等API导出即可,无需手动调整字节序。
CMoments: 如BN_bn2binpad是什么起名方式?为什么要RSA_get0_?,直接通用一个RSA_get0不行吗?
GitHub Copilot: 这些命名方式体现了OpenSSL的设计哲学和C语言库的通用习惯:
- BN_bn2binpad
- “BN”表示BIGNUM模块,“bn2bin”表示“BIGNUM转二进制”,“pad”表示“补齐到定长”。
- 这种“功能+细节”式命名让API用途一目了然,便于区分(如BN_bn2bin是变长,BN_bn2binpad是定长补零)。
- RSA_get0_*
- OpenSSL的RSA结构体是opaque(不透明)的,不能直接访问成员,必须通过getter/setter函数。
- “get0”表示“返回内部指针,不拷贝数据”,由调用者只读使用,0代表“零拷贝”。
- 每个参数(n, e, d, p, q, dmp1, …)都单独提供getter,便于类型安全、接口清晰、后续扩展。
- 不能用一个通用的RSA_get0,因为每个成员类型、用途不同,C语言不支持泛型getter。
总结:
- OpenSSL命名风格是“模块_功能_细节”,便于维护和扩展。
- “get0_成员名”是C语言安全封装的通用做法,防止误用和类型混淆。
- 这样设计有助于API稳定、易用和向后兼容。
CMoments: bn2bin”表示“BIGNUM转二进制”?
GitHub Copilot: 是的,BN_bn2bin
的意思就是 “BIGNUM(大整数)转二进制(byte array)”。
BN
:BIGNUM模块(OpenSSL大整数类型)bn2bin
:bn(BIGNUM)to(2)bin(binary),即把BIGNUM类型转换为二进制字节数组
这个函数会把BIGNUM的数值内容导出为大端序的字节流,常用于密钥导出、协议编码等场景。
CMoments: 这是不是跟Base64相反?把字节流编码。现在是把编码的数值变成字节流
GitHub Copilot: 不是完全相反,但有一定关联:
BN_bn2bin
是把“大整数(BIGNUM)”的数值内容,直接按大端序转换成原始字节流(binary),没有做任何编码,只是数值的二进制表示。Base64
是把任意字节流(binary)编码成可打印的ASCII字符串,常用于文本传输。
你可以理解为:
BN_bn2bin
:数值 → 字节流(原始二进制)Base64
:字节流 → 可打印字符串(编码)
如果你要把一个大整数安全地传输为文本,通常流程是:
BIGNUM → 字节流(BN_bn2bin)→ Base64编码 → 字符串
所以,BN_bn2bin是“数值转字节流”,Base64是“字节流转字符串”,两者可以配合使用,但不是互为逆操作。
CMoments: OpenSSL的RSA结构体是opaque(不透明)的,不能直接访问成员,必须通过getter/setter函数。为什么会这样?getter是干什么的?
GitHub Copilot: OpenSSL把RSA结构体设计成opaque(不透明)的,主要原因是:
- 封装实现细节,防止用户直接操作内部成员导致内存泄漏、野指针或安全隐患。
- 便于后续升级和兼容,库的实现可以变化,但API接口不变,用户代码无需修改。
- 强制用户通过getter/setter函数访问和修改成员,保证类型安全和正确性。
getter函数的作用:
- “只读”地获取结构体内部成员的指针或副本,防止用户误操作。
- 例如:
RSA_get0_n(rsa)
返回n(模数)的只读指针,用户不能直接修改,只能读取。
总结:
- 这样设计是现代C库的最佳实践,提升安全性、可维护性和兼容性。
- getter就是“安全访问内部成员”的专用函数。
CMoments: 为什么要变成字节流?直接给BIGNUM编码成字符串不一样走了这个流程吗?
GitHub Copilot: 之所以要把BIGNUM变成字节流,是因为:
- 密钥、签名、协议等底层数据交换和存储时,标准要求是“定长二进制格式”,而不是字符串。字节流可以直接用于网络传输、文件存储、硬件接口等,效率高、无歧义。
- 字节流是跨平台、跨语言、跨实现的“中性”表示,任何系统都能正确解析。
- 如果直接把BIGNUM编码成字符串(如十六进制、十进制),会变成长文本,效率低、空间大,还要额外处理编码/解码、补零、大小端等问题。
流程区别:
- “BIGNUM→字节流”是标准的二进制导出,适合协议、密钥、硬件等场景。
- “BIGNUM→字符串”适合人类阅读或文本协议,但不适合底层数据交换。
总结:
- 字节流是密码学和协议的标准数据格式,效率高、兼容性好。
- 字符串编码只是展示或文本传输的辅助方式,不能替代底层二进制格式。
CMoments: 定长二进制格式,为什么会有这样的标准要求
GitHub Copilot: 定长二进制格式作为标准要求,主要有以下原因:
- 协议和接口兼容性:定长格式便于不同厂商、不同平台、不同语言之间的数据交换,避免因长度不一致导致解析歧义或安全漏洞。
- 高效解析与存储:定长数据可以直接定位、快速读取和写入,无需额外长度字段或动态分配,适合硬件、网络、文件等场景。
- 安全性:定长格式能防止信息泄露(如密钥实际长度)、缓冲区溢出等安全问题,减少攻击面。
- 标准化和互操作:国际标准(如ASN.1/DER、PKCS#1、SDF、国密等)都要求密钥、签名等敏感数据用定长二进制,保证各实现间互通。
总结:
- 定长二进制格式让协议、硬件、软件都能高效、安全、无歧义地处理数据,是密码学和安全领域的通用最佳实践。
CMoments: 为什么一定要二进制格式
GitHub Copilot: 一定要用二进制格式,主要原因有:
- 高效:二进制格式体积小、处理快,适合密钥、签名、加密等对性能要求高的场景。
- 无歧义:二进制格式没有字符集、编码方式等歧义,跨平台、跨语言都能准确还原原始数据。
- 直接映射内存/硬件:硬件安全模块、协议栈、文件存储等底层系统都直接操作二进制数据,避免中间转换。
- 安全性:二进制格式不会泄露多余信息(如实际长度、内容特征),更难被意外解析或篡改。
- 标准要求:国际和行业标准(如ASN.1/DER、SDF、PKCS#1等)都规定敏感数据必须用二进制格式,保证互操作和安全。
总结:
二进制格式是密码学、协议和安全领域的通用底层标准,兼容性、效率和安全性都远优于文本格式。