开源之夏-16

内容是新建项目,加载GmSSL的软实现动态库,进行库加载,函数绑定,运行测试时的疑惑。
重点要知道不同算法及接口的数据结构定义,业务流程。我的工作30%是写代码,%70是测试。
老师要求这部分理解扎实,测试的时候会快很多。

另一个是关于构建项目,符号声明、库加载,函数绑定,运行测试,这些动作放到一个.c文件中很难维护。
要点是将.h用于声明.c用于定义,把项目结构拆开。便于开发和维护。

CMoments: 标准里说的这个参数是什么意思?我该如何解决这个参数的问题

GitHub Copilot: 图片中标准描述的参数是 phDeviceHandle(指针类型 HANDLE *),它的作用是用于返回设备句柄。你调用 SDF_OpenDevice 时,需要传入一个 HANDLE 类型变量的指针,函数会在内部初始化并填充这个变量,作为后续操作设备的凭证。

如何解决参数问题:

  1. 定义一个 HANDLE 类型变量(通常是 void* 或 typedef)。
  2. 调用 SDF_OpenDevice 时,传入该变量的地址(&handle)。
  3. 检查返回值是否为0,判断是否打开成功。
  4. 后续操作都用这个 handle。

示例代码(假设 HANDLE 是 void*):

1
2
3
4
5
6
7
HANDLE hDevice = NULL;
LONG ret = SDF_OpenDevice(&hDevice);
if (ret == 0) {
// 打开成功,可以用 hDevice 进行后续操作
} else {
// 打开失败,处理错误
}

总结:phDeviceHandle 是“输出参数”,用于返回设备句柄,传入时要用变量的地址。

CMoments: phDeviceHandle[out],表示它是一个数组?

GitHub Copilot: 不是,phDeviceHandle[out] 并不表示它是一个数组。

这里的 [out] 是文档的标注方式,意思是“输出参数”,即这个参数用于返回数据(由函数填充)。phDeviceHandle 实际上是一个指向 HANDLE 类型的指针(HANDLE*),用于返回单个设备句柄,不是数组。

你只需要定义一个 HANDLE 变量,并把它的地址传给函数即可。例如:

1
2
HANDLE hDevice;
LONG ret = SDF_OpenDevice(&hDevice);

这样就可以正确使用该参数。

CMoments: AsymAlgAbility: [133632, 256]
啥意思哇

GitHub Copilot: AsymAlgAbility 是一个8字节(64位)字段,分为两部分:

  • 前4字节(如 133632):表示支持的非对称算法,用算法标识按位或运算的结果。每一位代表支持某种算法。
  • 后4字节(如 256):表示支持的最大算法模长(如RSA密钥长度),同样是按位或或直接数值。

你的例子 [133632, 256],意思是:

  • 133632:设备支持的非对称算法集合(具体支持哪些算法要查算法标识的定义)。
  • 256:支持的最大模长为256位(比如最大支持256位的RSA密钥)。

总结:这是用来描述设备支持哪些非对称算法及其最大密钥长度的能力字段。

CMoments: 133632这个从哪里查找

GitHub Copilot: 133632 这个数值是“支持的非对称算法标识按位或运算的结果”。具体每一位代表什么算法,需要查阅你所用标准或设备厂商提供的“算法标识定义表”。

常见做法:

  • 查阅设备或接口标准文档中关于“非对称算法标识”的章节,通常会有类似如下的定义:
    • 0x0001 代表 RSA
    • 0x0002 代表 ECC
    • 0x0004 代表 SM2
    • …(每一位或每个值代表一种算法)

133632 转为十六进制是 0x20A80。你可以将 133632 按位与算法标识表进行比对,判断支持哪些算法。

总结:请查阅你所用标准或设备文档中“非对称算法标识”部分,找到每个标识对应的算法,然后用 133632(或 0x20A80)与这些标识做按位与,判断支持哪些算法。

CMoments: 什么叫支持的非对称算法标识按位运算的结果?这意味着什么?

GitHub Copilot: “支持的非对称算法标识按位运算的结果”意思是:每种非对称算法都有一个唯一的标识(通常是一个数值,比如 0x0001、0x0002、0x0004 等),设备支持多种算法时,会把这些标识用按位或(|)运算组合成一个数。

这意味着:

  • 结果的每一位(bit)代表是否支持某种算法。
  • 你可以用按位与(&)运算判断设备是否支持某个算法。

举例:

  • 假设 0x0001 代表RSA,0x0002代表ECC,0x0004代表SM2。
  • 如果设备支持RSA和SM2,则结果是 0x0001 | 0x0004 = 0x0005。
  • 你拿结果和某个算法标识做 & 运算,如果结果不为0,说明支持该算法。

所以,这个数值是“支持算法集合”的编码方式,便于用位运算快速判断支持哪些算法。

CMoments: 你拿结果和某个算法标识做 & 运算,如果结果不为0,说明支持该算法。为什么?

GitHub Copilot: 这是因为每种算法标识在结果值中占据唯一的二进制位(bit),用按位或(|)组合后,每一位代表是否支持对应算法。

举例说明:

  • 假设算法A标识是0x01(二进制0001),算法B标识是0x02(二进制0010)。
  • 如果设备支持A和B,结果就是0x01 | 0x02 = 0x03(二进制0011)。
  • 你要判断是否支持A,就用结果 & 0x01,得到1(不为0),说明支持A。
  • 判断是否支持B,用结果 & 0x02,得到2(不为0),说明支持B。
  • 如果用结果 & 0x04(算法C),得到0,说明不支持C。

本质:每一位为1表示支持该算法,&运算后不为0就说明那一位是1,即支持该算法。

CMoments: 为什么随机数会乱码?

GitHub Copilot: 你看到的“乱码”是因为你用 %s 以字符串方式打印了原始随机字节数据。随机数是二进制数据,可能包含不可打印字符或0(字符串结束符),所以用 %s 打印会出现乱码或截断。

正确做法:用循环按十六进制格式输出每个字节,例如:

1
2
3
4
for (int i = 0; i < uiLength; i++) {
printf("%02X ", (unsigned char)buf[i]);
}
printf("\n");

这样可以直观显示每个随机字节的值,不会乱码。

CMoments: 什么叫循环十六进制格式?

GitHub Copilot: “循环十六进制格式”就是用循环遍历每个字节,并用十六进制(%02X)格式打印出来。

举例说明:

1
2
3
4
for (int i = 0; i < uiLength; i++) {
printf("%02X ", (unsigned char)buf[i]);
}
printf("\n");

这样每个字节都会以两位十六进制数显示,比如“1A 3F 00 FF …”,而不是乱码或不可见字符。这种方式常用于显示原始二进制数据。

CMoments: 这样的随机数怎么用?不是数的随机数?

GitHub Copilot: 你得到的随机数其实是“随机字节序列”,本质上就是一串随机的二进制数据。它可以用来:

  • 作为加密密钥、IV、盐等安全用途
  • 生成随机数值(比如转换为int、float等)
  • 生成随机字符串、token等

如果你想把这些字节用作“数值”,可以把部分字节组合成整数。例如:

1
2
3
4
5
// 假设 buf 里有4个字节
unsigned int num = ((unsigned char)buf[0] << 24) |
((unsigned char)buf[1] << 16) |
((unsigned char)buf[2] << 8) |
((unsigned char)buf[3]);

如果你只需要一个0~255的随机数,可以直接用 buf[0]。

总结:这些“随机数”是原始的随机字节,你可以根据需要转换成各种类型的随机数值或数据。

CMoments: 这是伪随机数还是真随机数

GitHub Copilot: 这取决于你调用的 SDF_GenerateRandom(或类似接口)底层实现:

  • 如果底层硬件有专用的随机数发生器(TRNG,真随机数发生器),生成的是“真随机数”。
  • 如果底层用的是算法(如PRNG,伪随机数发生器),那就是“伪随机数”。

在密码设备(如国密SDF标准)中,通常要求生成“真随机数”,但具体要看你设备的实现和文档说明。你可以查阅设备手册或厂商资料确认。

CMoments: Segmentation fault (core dumped)
SDF-Load$

GitHub Copilot: 你的程序出现了“Segmentation fault (core dumped)”(段错误),这通常是因为访问了无效的内存地址。常见原因有:

  1. 指针未初始化或已被释放后再次使用。
  2. 数组越界访问。
  3. 传递给函数的指针参数不正确。
  4. 动态库函数返回错误后仍然继续使用无效句柄。

建议排查方法:

  • 检查所有指针变量是否正确初始化。
  • 检查 buf、hDevice、hSession 等变量在使用前是否有效。
  • 检查 SDF_ 系列函数的返回值,出错时不要继续后续操作。
  • 用 gdb 调试,查看崩溃发生在哪一行。

如果需要具体定位,请提供 main.c 相关代码和 gdb 的 backtrace 信息,我可以帮你详细分析。

CMoments: SDF-Load$ gcc -o main main.c -ldl
SDF-Load$
SDF-Load$ ./main
before SDF_OpenDevice
after SDF_OpenDevice, ret=0
Before SDF_OpenSession
After SDF_OpenSession,ret=0
After SDF_CloseSession,ret=0
After SDF_GetDeviceInfo,ret=0
DeviceInfo:
IssuerName: GmSSL Project (http://gmssl.org)
SerialNumber: Soft SDF
FirmwareVersion: 202406220010200
DeviceVersion: 1
StandardVersion: 1
AsymAlgAbility: [133632, 256]
SymAlgAbility: 1055
HashAlgAbility: 1
BufferSize: 262144
After SDF_GenerateRandom,ret=0
Generated Random Bytes: �����A���U8�����dwN�u
1D 0F E5 B6 DC C0 9E CA 41 2A BD AA FA 55 38 A9 F6 9E ED FE 64 77 4E BF 75 00 EE 69 6D 62 24 A5
After SDF_GetPrivateKeyAccessRight,ret=0
After SDF_ReleasePrivateKeyAccessRight: ret=0
After SDF_GetPrivateKeyAccessRight,ret=0
Segmentation fault (core dumped)
SDF-Load$ gcc -o main main.c -ldl
SDF-Load$ ./main
before SDF_OpenDevice
after SDF_OpenDevice, ret=0
Before SDF_OpenSession
After SDF_OpenSession,ret=0
After SDF_CloseSession,ret=0
After SDF_GetDeviceInfo,ret=0
DeviceInfo:
IssuerName: GmSSL Project (http://gmssl.org)
SerialNumber: Soft SDF
FirmwareVersion: 202406220010200
DeviceVersion: 1
StandardVersion: 1
AsymAlgAbility: [133632, 256]
SymAlgAbility: 1055
HashAlgAbility: 1
BufferSize: 262144
After SDF_GenerateRandom,ret=0
Generated Random Bytes: ;�v804�|�J�.�p�2�����X��’ZS��w
3B C5 76 38 30 34 B0 7C E6 4A BF 2E A9 70 CA 32 0E A2 BE 90 8A C5 58 9D 88 27 5A 53 0F 86 F1 77
After SDF_GetPrivateKeyAccessRight,ret=0
After SDF_ReleasePrivateKeyAccessRight: ret=0
After SDF_GetPrivateKeyAccessRight,ret=0
Segmentation fault (core dumped)
SDF-Load$ SDF-Load$ gcc -o main main.c -ldl
SDF-Load$
SDF-Load$ ./main
before SDF_OpenDevice
after SDF_OpenDevice, ret=0
Before SDF_OpenSession
After SDF_OpenSession,ret=0
After SDF_CloseSession,ret=0
After SDF_GetDeviceInfo,ret=0
DeviceInfo:
IssuerName: GmSSL Project (http://gmssl.org)
SerialNumber: Soft SDF
FirmwareVersion: 202406220010200
DeviceVersion: 1
StandardVersion: 1
AsymAlgAbility: [133632, 256]
SymAlgAbility: 1055
HashAlgAbility: 1
BufferSize: 262144
After SDF_GenerateRandom,ret=0
Generated Random Bytes: �����A
���U8�����dwN�u
1D 0F E5 B6 DC C0 9E CA 41 2A BD AA FA 55 38 A9 F6 9E ED FE 64 77 4E BF 75 00 EE 69 6D 62 24 A5
After SDF_GetPrivateKeyAccessRight,ret=0
After SDF_ReleasePrivateKeyAccessRight: ret=0
After SDF_GetPrivateKeyAccessRight,ret=0
Segmentation fault (core dumped)
SDF-Load$ gcc -o main main.c -ldl
SDF-Load$ ./main
before SDF_OpenDevice
after SDF_OpenDevice, ret=0
Before SDF_OpenSession
After SDF_OpenSession,ret=0
After SDF_CloseSession,ret=0
After SDF_GetDeviceInfo,ret=0
DeviceInfo:
IssuerName: GmSSL Project (http://gmssl.org)
SerialNumber: Soft SDF
FirmwareVersion: 202406220010200
DeviceVersion: 1
StandardVersion: 1
AsymAlgAbility: [133632, 256]
SymAlgAbility: 1055
HashAlgAbility: 1
BufferSize: 262144
After SDF_GenerateRandom,ret=0
Generated Random Bytes: ;�v804�|�J�.�p�2�����X��’ZS��w
3B C5 76 38 30 34 B0 7C E6 4A BF 2E A9 70 CA 32 0E A2 BE 90 8A C5 58 9D 88 27 5A 53 0F 86 F1 77
After SDF_GetPrivateKeyAccessRight,ret=0
After SDF_ReleasePrivateKeyAccessRight: ret=0
After SDF_GetPrivateKeyAccessRight,ret=0
Segmentation fault (core dumped)
SDF-Load$

帮我分析原因

GitHub Copilot: 你的程序出现段错误(Segmentation fault)的原因很可能是多次重复打开和关闭设备/会话,导致句柄失效或未初始化,尤其是在最后一组操作:

1
2
3
4
5
6
7
OpenDevice(&hDevice);
OpenSession(hDevice, &hSession);
GetPrivateKeyAccessRight(...);
printf(...);

RSArefPublicKey pubKey;
int ret8 = ExportSignPublicKey(hSession, keyIndex, &pubKey);

问题分析:

  1. 你多次调用 OpenDeviceOpenSession,但每次都没有检查返回值是否为0(成功),如果失败,hDevicehSession 可能是无效指针。
  2. 你多次 CloseDeviceCloseSession,但没有将句柄置为 NULL,后续又用同一个变量,可能导致重复关闭或使用已关闭的句柄。
  3. 你在最后一次 GetPrivateKeyAccessRight 后,直接调用 ExportSignPublicKey,但没有检查 GetPrivateKeyAccessRight 是否成功。
  4. 如果 ExportSignPublicKey 内部访问了无效的 hSession,就会段错误。

建议修改

  1. 每次打开/关闭设备和会话后,检查返回值。
  2. 不要重复打开/关闭设备和会话,尽量每次只打开一次,最后关闭一次。
  3. 每次关闭后,将句柄变量置为 NULL。
  4. 所有函数调用后都检查返回值,失败时不要继续后续操作。

重点修正示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ...existing code...
// 建议只打开一次设备和会话,最后关闭
void *hDevice = NULL;
void *hSession = NULL;
int ret = OpenDevice(&hDevice);
if (ret != 0 || hDevice == NULL) {
printf("OpenDevice failed: %d\n", ret);
dlclose(handle);
return 1;
}
int ret1 = OpenSession(hDevice, &hSession);
if (ret1 != 0 || hSession == NULL) {
printf("OpenSession failed: %d\n", ret1);
CloseDevice(hDevice);
dlclose(handle);
return 1;
}

// ...后续所有操作都用 hDevice 和 hSession...

// 结束时关闭
CloseSession(hSession);
CloseDevice(hDevice);
dlclose(handle);
return 0;
// ...existing code...

结论

你的段错误很可能是重复打开/关闭设备和会话,导致句柄失效,或者未检查返回值导致使用了无效指针。建议合并设备和会话的打开/关闭流程,并严格检查每一步的返回值,避免使用无效句柄。

CMoments: 在vim中如何给选中的内容缩进

GitHub Copilot: 在 Vim 中给选中的内容缩进的方法如下:

  1. 进入可视模式,选中你要缩进的内容(比如按 V 进入行可视模式,然后用 j/k 或方向键选中多行)。
  2. 按下 > 键即可向右缩进选中的内容(缩进宽度由 shiftwidth 决定)。
  3. 如果要向左缩进,按 < 键。

示例操作:

  • V 选中一行,j 向下扩展选区,> 向右缩进。
  • 也可以用 gv 重新选中上次的可视区域,再次缩进。

多次缩进:

  • 选中后按 3> 可以右缩进3次。

普通模式下缩进多行:

  • 例如 :10,20> 表示将第10到20行右缩进一层。

希望对你有帮助!

CMoments: @workspace int SDF_GenerateKeyWithEPK_ECC(
void *hSessionHandle,
unsigned int uiKeyBits,
unsigned int uiAlgID,
ECCrefPublicKey *pucPublicKey,
ECCCipher *pucKey,
void **phKeyHandle)
{
SOFTSDF_SESSION *session;
SM2_POINT point;
SM2_Z256_POINT public_key;
SM2_KEY sm2_key;
SOFTSDF_KEY *key;
SM2_CIPHERTEXT ctxt;

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 (uiKeyBits%8 != 0 || uiKeyBits/8 > SOFTSDF_MAX_KEY_SIZE) {    error_print();    return SDR_INARGERR;}if (uiAlgID != SGD_SM2_3) {    error_print();    return SDR_INARGERR;}if (pucPublicKey == NULL || pucKey == NULL || phKeyHandle == NULL) {    error_print();    return SDR_INARGERR;}// load public keymemset(&point, 0, sizeof(point));memcpy(point.x, pucPublicKey->x + ECCref_MAX_LEN - 32, 32);memcpy(point.y, pucPublicKey->y + ECCref_MAX_LEN - 32, 32);if (sm2_z256_point_from_bytes(&public_key, (uint8_t *)&point) != 1) {    error_print();    return SDR_INARGERR;}if (sm2_key_set_public_key(&sm2_key, &public_key) != 1) {    error_print();    return SDR_INARGERR;}// generate keykey = (SOFTSDF_KEY *)malloc(sizeof(*key));if (key == NULL) {    error_print();    return SDR_NOBUFFER;}memset(key, 0, sizeof(*key));if (rand_bytes(key->key, uiKeyBits/8) != 1) {    error_print();    free(key);    return SDR_GMSSLERR;}key->key_size = uiKeyBits/8;// encrypt key with external public keyif (sm2_do_encrypt(&sm2_key, key->key, key->key_size, &ctxt) != 1) {    error_print();    free(key);    return SDR_GMSSLERR;}memset(pucKey, 0, sizeof(*pucKey));memcpy(pucKey->x + ECCref_MAX_LEN - 32, ctxt.point.x, 32);memcpy(pucKey->y + ECCref_MAX_LEN - 32, ctxt.point.y, 32);memcpy(pucKey->M, ctxt.hash, 32);pucKey->L = ctxt.ciphertext_size;memcpy(pucKey->C, ctxt.ciphertext, ctxt.ciphertext_size);// append key to key_listif (session->key_list == NULL) {    session->key_list = key;} else {    SOFTSDF_KEY *current = session->key_list;    while (current->next != NULL) {        current = current->next;    }    current->next = key;}*phKeyHandle = key;return SDR_OK;

}
这是这个函数的软实现,当前代码中,我该如何测试它?

Workspace: Collecting workspace information你可以通过调用 SDF_GenerateKeyWithEPK_ECC 的动态库接口来测试它。在你的 main.c 代码中,已经有了相关的调用框架。你只需要补全参数准备和调用部分。下面是具体建议:

1. 参数准备

你需要准备:

  • hSession:已打开的 session 句柄
  • uiKeyBits:比如 128 或 256
  • uiAlgID:应为 SGD_SM2_3(你需要确认其值,通常是宏定义)
  • pucPublicKey:一个有效的 ECCrefPublicKey 结构体,通常通过 SDF_ExportEncPublicKey_ECC 获得
  • pucKeyECCCipher 结构体指针
  • phKeyHandlevoid* 指针

2. 示例代码

补全你 main 函数中的相关部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...existing code...
unsigned int keyIndex = 1;
unsigned int uiKeyBits = 128;
unsigned int uiAlgID = SGD_SM2_3; // 你需要定义或确认这个宏的值
ECCrefPublicKey pubKey;
ECCCipher cipher;
void *phKeyHandle = NULL;

// 1. 获取公钥
int ret_pub = ExportEncPublicKey_ECC(hSession, keyIndex, &pubKey);
printf("ExportEncPublicKey_ECC ret = %d\n", ret_pub);

// 2. 调用生成密钥函数
int ret_gen = GenerateKeyWithEPK_ECC(hSession, uiKeyBits, uiAlgID, &pubKey, &cipher, &phKeyHandle);
printf("GenerateKeyWithEPK_ECC ret = %d\n", ret_gen);

// 3. 检查结果
if (ret_gen == 0) {
printf("Key generated and encrypted successfully.\n");
// 你可以打印 cipher 结构体内容进行验证
} else {
printf("Key generation failed.\n");
}
// ...existing code...

3. 注意事项

  • SGD_SM2_3 需要在你的代码中定义(查找头文件或相关文档)。
  • 确保 session 已经打开且有效。
  • ExportEncPublicKey_ECC 必须成功,否则 pubKey 无效。
  • 你可以通过打印 cipher 的内容来进一步验证加密结果。

相关符号

  • SDF_ExportEncPublicKey_ECC
  • SDF_GenerateKeyWithEPK_ECC
  • ECCrefPublicKey
  • ECCCipher

如需进一步自动化测试,可将上述代码块放入循环或单元测试框架中。

CMoments: // softsdfinit -kek 1 -key 1 -pass P@ssw0rd
我已经用这个命令生成了需要的文件。教我如何调用?SGD_SM2_3 是什么?

GitHub Copilot: 你已经用 softsdfinit -kek 1 -key 1 -pass P@ssw0rd 初始化了密钥文件,可以直接调用 SDF_GenerateKeyWithEPK_ECC。下面是详细说明:


1. SGD_SM2_3 是什么?

SGD_SM2_3 是国密标准中SM2密钥交换算法的标识符(算法ID),通常定义在头文件里。例如:

1
#define SGD_SM2_3 0x00020100

你可以在你的 SDF 头文件或标准文档中查找确认。如果没有定义,可以自己加上如上定义。


2. 如何调用 SDF_GenerateKeyWithEPK_ECC

步骤如下:

  1. 打开设备和会话
  2. 导出ECC加密公钥(用 SDF_ExportEncPublicKey_ECC
  3. 准备参数并调用 SDF_GenerateKeyWithEPK_ECC
  4. 关闭会话和设备

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...existing code...
#define SGD_SM2_3 0x00020100 // 如头文件未定义,需手动添加

void *hDevice = NULL;
void *hSession = NULL;
unsigned int keyIndex = 1;
int ret = OpenDevice(&hDevice);
if (ret != 0) { printf("OpenDevice failed\n"); return 1; }
ret = OpenSession(hDevice, &hSession);
if (ret != 0) { printf("OpenSession failed\n"); CloseDevice(hDevice); return 1; }

// 1. 导出ECC加密公钥
ECCrefPublicKey ecc_pubKey;
ret = ExportEncPublicKey_ECC(hSession, keyIndex, &ecc_pubKey);
if (ret != 0) { printf("ExportEncPublicKey_ECC failed\n"); CloseSession(hSession); CloseDevice(hDevice); return 1; }

// 2. 调用 SDF_GenerateKeyWithEPK_ECC
ECCCipher ecc_cipher;
void *phKeyHandle = NULL;
unsigned int keyBits = 128; // 或256,按需求
ret = GenerateKeyWithEPK_ECC(hSession, keyBits, SGD_SM2_3, &ecc_pubKey, &ecc_cipher, &phKeyHandle);
printf("GenerateKeyWithEPK_ECC ret = %d\n", ret);

// 3. 关闭
CloseSession(hSession);
CloseDevice(hDevice);
// ...existing code...

3. 说明

  • keyIndex:你初始化时指定的密钥索引(通常为1)
  • keyBits:密钥长度,常用128或256
  • SGD_SM2_3:算法ID,必须与设备支持的值一致
  • ecc_pubKey:通过 ExportEncPublicKey_ECC 获得
  • ecc_cipher:输出的加密密钥结构体
  • phKeyHandle:输出的密钥句柄

4. 你需要做的

  1. 在代码中补充 #define SGD_SM2_3 0x00020100
  2. 按上面示例调用流程补全你的 main 函数
  3. 编译并运行

如遇到具体报错,可贴出错误信息进一步排查。

CMoments: 为什么要先导出ECC加密公钥?这里用的加密公钥是自己导出的?

GitHub Copilot: 你需要先导出ECC加密公钥,是因为 SDF_GenerateKeyWithEPK_ECC 这个接口的设计就是用外部公钥(通常是通信对方的加密公钥)来加密生成的对称密钥,实现密钥协商或安全分发。

在你的测试代码中,你导出的加密公钥其实是设备自身的加密公钥,这在本地测试时是常见做法。实际应用中,通常会用通信对方的加密公钥(比如A给B发密钥,A用B的加密公钥加密)。

流程解释:

  1. 你用 ExportEncPublicKey_ECC 导出设备的ECC加密公钥,获得 ecc_pubKey
  2. 调用 SDF_GenerateKeyWithEPK_ECC,用这个公钥加密生成的对称密钥,结果密文放在 ecc_cipher 里。
  3. 这样,只有拥有对应私钥的人(比如设备自己或通信对方)才能解密这个密钥。

总结:

  • 你现在用的是“自己导出的加密公钥”,适合本地测试。
  • 实际应用时,应该用对方的加密公钥,这样生成的密钥只有对方能解密和使用,保证安全性。

CMoments: 实际应用起来的业务过程是什么样子的?

GitHub Copilot: 实际应用中,SDF_GenerateKeyWithEPK_ECC 这样的接口通常用于密钥协商或密钥分发,主要业务流程如下:


1. 典型业务场景

假设有两个通信方:A(客户端)B(服务器/设备),A 想安全地和B协商一个对称密钥用于后续加密通信。

步骤如下:

  1. B生成并发布自己的ECC加密公钥

    • B用 SDF_ExportEncPublicKey_ECC 导出自己的加密公钥,安全地分发给A。
  2. A用B的公钥生成会话密钥并加密

    • A调用 SDF_GenerateKeyWithEPK_ECC,传入B的加密公钥,设备内部生成一个随机对称密钥,并用B的公钥加密,得到密文(ECCCipher)。
    • A将密文(加密后的会话密钥)发送给B。
  3. B用自己的私钥解密会话密钥

    • B收到密文后,用自己的私钥解密,恢复出A生成的对称密钥。
    • 这样A和B都拥有了同一个对称密钥,可以用于后续加密通信。

2. 业务意义

  • 密钥协商安全:对称密钥只在设备内部生成和使用,外部无法直接获取明文密钥。
  • 防止窃听:即使密钥协商过程被窃听,攻击者也无法获得对称密钥,因为只有B有私钥能解密。
  • 标准兼容:符合国密SM2密钥协商/分发流程。

3. 你的代码流程(本地测试)

你现在的代码是本地自测流程,等价于“自己和自己协商密钥”,实际业务部署时,公钥和密文应在不同系统/设备间传递。


4. 总结

  • 实际业务过程:一方导出公钥,另一方用公钥生成密钥并加密,密文传回,私钥方解密,双方获得同一对称密钥。
  • 用途:安全协商会话密钥、密钥分发、加密通信等。

如需更详细的业务流程图或代码示例,可继续提问!

CMoments: B导出自己的加密公钥后,是如何安全分发给A的?

GitHub Copilot: B导出自己的加密公钥后,安全分发给A的常见方式有:

  1. 通过证书分发
    B将自己的加密公钥封装在数字证书(如SM2证书)中,由权威CA签名。A通过安全渠道获取并验证证书,提取公钥。

  2. 通过安全通道传输
    B通过已建立的安全通道(如SSL/TLS、VPN、物理隔离网络等)将公钥直接发送给A。

  3. 通过可信第三方平台
    B将公钥上传到可信的密钥管理平台或注册中心,A从该平台安全下载。

  4. 线下介质分发
    B将公钥写入U盘、光盘等物理介质,通过人工线下交付给A。

注意:

  • 公钥分发时要防止中间人攻击,A必须验证公钥的真实性(如校验证书签名或通过多渠道核对)。
  • 切勿通过明文、无认证的网络直接分发公钥,否则可能被篡改。

总结:
安全分发的核心是确保A拿到的公钥确实是B的,且未被篡改。实际应用中,最常用的是数字证书和安全通道。

CMoments: phKeyHandle是做什么的?

GitHub Copilot: phKeyHandle 是一个输出参数,用于返回生成的对称密钥的“句柄”(handle)。

具体作用如下:

  • 当你调用 SDF_GenerateKeyWithEPK_ECC 时,设备/库会在内部生成一个对称密钥,并用外部公钥加密后返回密文(ECCCipher)。
  • 同时,设备/库会把这个对称密钥的引用(即“句柄”)通过 phKeyHandle 返回给你。这个句柄本质上是一个指针或整数,代表这把密钥在设备/库内部的唯一标识。
  • 后续你如果要用这把密钥(比如加解密、导出、销毁等),都需要通过这个句柄来操作,而不是直接操作明文密钥。

总结:
phKeyHandle 让你可以安全地管理和使用设备内部生成的密钥,而不用直接接触明文密钥内容,提高了安全性。

CMoments: @workspace
#define SGD_SM2_3 0x00020800

像这种定义,SDF标准里没有给,但是测试调用的时候,传入算法号还要用,我完成SDF标准接口的映射,和TSAPI_这层的封装,也要用这些符号吧?我去哪里找到这些符号的定义?我是否需要把这些定义抄到.h文件?

Workspace: Collecting workspace information是的,需要这些算法号常量,且应集中放在一个公共头文件里,供 SDF 接口、TSAPI 封装层以及你的调用方统一引用。最佳做法:

  • 优先使用你当前 lib 的配套头文件(厂商/SoftSDF/GmSSL SDK 会提供 SDF.h/SGD.h 一类头)。这样可以确保与 libsoftsdf.so 内部一致,避免算法号不匹配。
  • 如果当前项目缺少这些头,先在工程内创建一个公共头,至少把你要用到的常量放进去(例如 SGD_SM2_3),然后所有代码都包含它,避免在各处重复 define。

建议新增头文件并在 main.c 中引用,替换散落的宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
#pragma once
#include <stdint.h>

// 按照你当前库的实际取值,先放入用到的常量
// 注意:这些值应与 libsoftsdf.so 的实现一致
#define SGD_SM2_3 0x00020800u // SM2加密/密钥包装(SDF_SM2_3)

// 如需扩展,再补充其它算法号,并保持与库一致
// #define SGD_SM2_1 ...
// #define SGD_SM2_2 ...
// #define SGD_SM4_ECB ...
// ...
1
2
3
4
5
6
7
8
9
10
// ...existing code...
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include "sdf_defs.h" // 引入集中定义
#define RSAref_MAX_BITS 4096
#define RSAref_MAX_LEN ((RSAref_MAX_BITS + 7) / 8)
// ...existing code...
// 删除这里原来的 #define SGD_SM2_3 0x00020800
// ...existing code...

如需在 TSAPI 封装层里做映射,集中写一个映射函数,避免各处硬编码:

1
2
3
4
5
6
7
8
9
10
11
// 不指定路径:请在你的 TSAPI 封装源文件处添加
// ...existing code...
// 示例:将 TSAPI 的算法枚举映射到 SDF 算法号
static unsigned int TSAPIAlgToSDF(unsigned int tsapiAlg) {
switch (tsapiAlg) {
// case TSAPI_SM2_ENC: return SGD_SM2_3;
// 根据你 TSAPI 实际枚举完善其它映射
default: return 0; // 未知算法
}
}
// ...existing code...

重点提示:

  • 算法号是二进制常量,必须与库内部一致。不要随意猜测值,最好从该库的配套头文件获取。如果找不到配套头,先用你当前可用、已验证能跑通的值集中定义于公共头,后续再对齐官方头文件。
  • 你的调用已经使用了 SGD_SM2_3 传入 SDF_GenerateKeyWithEPK_ECC,只要公共头里的值与库一致即可测试通过。

CMoments: 这些头从哪里找?

GitHub Copilot: 你需要用库配套的头文件(包含算法号和结构体定义),不要自己瞎定义。否则一旦值或结构体布局不一致,就会出现你之前的段错误。

哪里找

  • 首选:使用 libsoftsdf.so 所在项目(GmSSL/SoftSDF)的配套头。安装后通常在以下位置之一:
    • /usr/local/include/sgd.h, /usr/local/include/sdf.h
    • /usr/include/sgd.h, /usr/include/sdf.h
    • 如果是源码编译安装,头一般在 GmSSL 源码的 include/ 目录下(如 include/sgd.h、include/sdf.h)。
  • 在本机快速查找:
    • find /usr/local/include /usr/include -iname “sgd*.h” -o -iname “sdf*.h”
    • grep -R “SGD_SM2_3” /usr/local/include /usr/include
  • 若系统未安装开发头,请到提供 libsoftsdf.so 的同一工程/发行包中取头文件,或安装对应的 -dev 包;务必与 .so 版本匹配。

是否要把定义抄到 .h

  • 不建议自己抄。应直接使用配套头(sgd.h/sdf.h)。确实找不到时,临时在项目里放一份“第三方头”的拷贝,并确保与 .so 版本一致。

如何在你的工程中使用

  • 引入配套头,并删除你手写的那些宏和结构体(避免重复定义与不一致)。
1
2
3
4
5
// ...existing code...
#include "sgd.h" // 含算法ID,如 SGD_SM2_3/SM4 等
#include "sdf.h" // 含 DEVICEINFO/ECCrefPublicKey/ECCCipher 等官方结构体和原型
// 建议删除你自己写的 #define SGD_SM2_3 和各类 *ref* 结构体定义,使用官方定义
// ...existing code...

补充

  • 不同厂商/实现对算法号的取值可能不同,必须以你这份 libsoftsdf.so 的配套头为准。
  • 宏如 SGD_SM2_3、SGD_SM4_* 等、以及 DEVICEINFO/ECCrefPublicKey/ECCCipher 的字段布局,都应以配套头为准,否则传参会造成崩溃。

CMoments: @workspace 当前项目的typedef部分,我已经对齐了SDF GM/T0018 2023标准,现在,如你所看到的部分代码,我试图通过加载动态库测试函数返回0,确定功能基本正常,但是现在代码很不方便,单文件很大,而且主函数测试的时候也很费劲,有没有个办法帮我整理项目?形成一个小的测试框架?作为我后续开发过程中一个验证工具。

教我,指导我,一步一步该如何做。而不是直接帮我改代码。

Workspace: Collecting workspace information当然可以,下面是针对你当前项目如何逐步整理和搭建一个小型C测试框架的详细指导:


1. 拆分头文件和实现文件

目标:把结构体、宏、typedef等放到.h文件,主逻辑和测试放到.c文件。

  • 新建 sdf_defs.h,把所有结构体、宏、typedef等复制进去。
  • 在 main.c 里用 #include "sdf_defs.h" 替换原有的定义。

2. 将测试用例和主程序分离

目标:主程序只负责调度,测试用例单独函数实现。

  • 新建 testcases.ctestcases.h
  • 每个测试写成 int test_xxx(void) 形式,返回0表示通过。
  • testcases.h 里声明所有测试函数。

3. 实现一个简单的测试调度器

目标:main函数只负责调用所有测试用例并统计结果。

  • 在 main.c 里,包含 testcases.h
  • 写一个循环,依次调用每个测试函数,统计通过/失败数量,最后输出汇总。

4. 编写Makefile或VSCode tasks.json

目标:一条命令编译所有文件,生成可执行文件。

  • 新建 Makefile,内容类似:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    CC=gcc
    CFLAGS=-Wall -g
    OBJS=main.o testcases.o

    all: main

    main: $(OBJS)
    $(CC) $(CFLAGS) -o main $(OBJS) -ldl

    %.o: %.c
    $(CC) $(CFLAGS) -c $<

    clean:
    rm -f *.o main
  • 或者在 tasks.json 里配置编译命令。

5. 逐步迁移和完善测试用例

目标:每个SDF接口写一个对应的测试函数,便于后续扩展。

  • 例如 test_export_enc_public_key_ecc()test_generate_key_with_epk_ecc() 等。
  • 每个测试函数内部自己打开/关闭设备和session,保证互不影响。

6. 持续集成和扩展

目标:后续每开发一个新功能,都写对应的测试函数,保证代码质量。

  • 可以考虑用脚本自动运行测试,或集成到CI工具。

示例结构

1
2
3
4
5
6
7
8
/home/ddd/SDF-Load/
├── sdf_defs.h
├── testcases.h
├── testcases.c
├── main.c
├── Makefile
├── libsoftsdf.so
└── ...

你可以这样开始

  1. 新建 sdf_defs.h,把所有结构体和宏移进去。
  2. 新建 testcases.htestcases.c,写第一个测试函数,比如导出ECC公钥。
  3. 修改 main.c,只保留测试调度和统计。
  4. Makefile,尝试编译运行。
  5. 逐步把原有测试代码拆分成独立测试函数。

如需具体某一步的代码模板或遇到问题,可以随时问我!

CMoments: 函数绑定部分放在哪里?

GitHub Copilot: 函数绑定(即 dlsym 获取函数指针)建议单独封装成一个初始化函数,放在一个新的源文件(如 sdf_bind.c)和头文件(如 sdf_bind.h)中。这样可以让主程序更简洁,后续测试用例也能方便复用。

推荐做法

  1. 新建头文件 sdf_bind.h
    声明所有 SDF 函数指针和初始化函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // sdf_bind.h
    #pragma once
    #include "sdf_defs.h"

    extern SDF_OpenDevice OpenDevice;
    extern SDF_CloseDevice CloseDevice;
    // ...其它函数指针...

    int sdf_bind_init(void *handle);
  2. 新建实现文件 sdf_bind.c
    定义所有函数指针,并实现 sdf_bind_init

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // sdf_bind.c
    #include <dlfcn.h>
    #include <stdio.h>
    #include "sdf_bind.h"

    SDF_OpenDevice OpenDevice = NULL;
    SDF_CloseDevice CloseDevice = NULL;
    // ...其它函数指针...

    int sdf_bind_init(void *handle) {
    OpenDevice = (SDF_OpenDevice)dlsym(handle, "SDF_OpenDevice");
    CloseDevice = (SDF_CloseDevice)dlsym(handle, "SDF_CloseDevice");
    // ...其它函数绑定...
    // 可加判空和错误提示
    return 0;
    }
  3. 在主程序中调用初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // main.c
    #include "sdf_bind.h"

    int main() {
    void *handle = dlopen("./libsoftsdf.so", RTLD_LAZY);
    if (!handle) { /* ... */ }
    sdf_bind_init(handle); // 只需一行
    // 后续直接用 OpenDevice 等函数指针
    }

优点:

  • 结构清晰,便于维护和扩展
  • 多个测试文件可复用同一绑定逻辑
  • 主程序更简洁

如需更详细的模板或遇到具体问题,可继续提问!

CMoments: #include “testcases.h”
#include “sdf_bind.h”
#include “sdf_defs.h”
#include <stdio.h>

testcases.c要include这些吗?

GitHub Copilot: 是的,testcases.c 应该包含这些头文件:

  • #include "testcases.h":声明测试用例函数,供主程序调用。
  • #include "sdf_bind.h":使用动态库绑定的 SDF 函数指针。
  • #include "sdf_defs.h":包含结构体、宏、类型定义等。
  • #include <stdio.h>:用于输入输出(如 printf)。

这样可以确保你的测试代码能正确调用接口、使用类型,并输出调试信息。

总结:
testcases.c 需要 #include "testcases.h", #include "sdf_bind.h", #include "sdf_defs.h", #include <stdio.h>,这样写是对的。

CMoments: 那main.c是不是就不用include”sdf_defs.h和sdf_bind.h了

GitHub Copilot: 如果 main.c 只负责加载动态库、初始化绑定和调度测试用例(比如只调用 sdf_bind_inittest_all),理论上只需要包含 testcases.h,因为具体的 SDF 类型和绑定细节都在 testcases.c 里处理了。

你可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <dlfcn.h>
#include "testcases.h"

int main() {
void *handle = dlopen("./libsoftsdf.so", RTLD_LAZY);
if (!handle) {
printf("dlopen error: %s\n", dlerror());
return 1;
}
sdf_bind_init(handle); // 如果 testcases.h 里声明了它,这里也可以用
test_all(); // 调用你的测试入口
return 0;
}

注意:

  • 如果 main.c 里直接用到了 SDF 的结构体、宏或函数指针,还是要包含对应头文件。
  • 如果只是调度测试和初始化,#include "testcases.h" 就够了。

总结
只要 main.c 不直接用 SDF 相关类型和函数指针,可以不用 #include "sdf_defs.h"#include "sdf_bind.h",只包含 testcases.h 即可。

CMoments: 重复include同一个库会出问题吗

GitHub Copilot: 在C语言中,重复 #include 同一个头文件本身不会出问题,前提是该头文件内部有“头文件保护”(通常用 #ifndef/#define/#endif 包裹)。
例如:

1
2
3
4
#ifndef SDF_DEFS_H
#define SDF_DEFS_H
// ...内容...
#endif

这样无论你在不同源文件、甚至同一个文件多次 #include "sdf_defs.h",编译器只会处理一次,不会重复定义。

动态库(.so)
动态库的加载(如 dlopen)和头文件无关。你可以在多个源文件 include 同一个声明头文件,只要链接时只加载一次 .so 即可。

总结:

  • 头文件重复 include:有保护宏就没问题。
  • 动态库重复 include 头文件:没问题,实际只加载一次。
  • 切记不要在不同源文件里重复定义同名的全局变量或函数实现,否则会链接错误。

CMoments: 显然,我没写。

GitHub Copilot: 你说得对,你只声明了 OpenDeviceCloseDevice,还没有声明其它 SDF 接口的函数指针。
如果你后续测试或开发中需要用到更多 SDF 接口(比如 OpenSessionGetDeviceInfo 等),应该在 sdf_bind.h 里补充声明,并在 sdf_bind.c 里补充定义和绑定。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
// sdf_bind.h
#pragma once
#include "sdf_defs.h"

extern SDF_OpenDevice OpenDevice;
extern SDF_CloseDevice CloseDevice;
extern SDF_OpenSession OpenSession;
extern SDF_CloseSession CloseSession;
extern SDF_GetDeviceInfo GetDeviceInfo;
// ...根据需要继续添加...

int sdf_bind_init(void *handle);

注意:

  • 这些类型(如 SDF_OpenSession)需要在 sdf_defs.h 里有 typedef 定义。
  • 你用到哪个接口,就在这里声明哪个函数指针。

这样,所有测试和主程序都可以通过 #include "sdf_bind.h" 访问这些函数指针。

CMoments: 头文件重复 include:有保护宏就没问题。
如果没有呢?

GitHub Copilot: 如果没有头文件保护宏(即没有 #ifndef ... #define ... #endif#pragma once),重复 include 头文件会导致以下问题

  1. 重复定义结构体、类型、宏
    编译器会多次看到同样的结构体、typedef、宏定义,通常不会报错,但会增加编译负担。

  2. 重复声明全局变量或函数
    如果头文件里有全局变量定义(如 int a = 0;),会导致链接错误(multiple definition)。

  3. 重复定义函数(inline 或 static)
    如果头文件里有 inlinestatic 函数实现,重复 include 可能导致符号冲突或代码膨胀。

  4. 编译报错或链接错误
    最常见的是“redefinition of …”或“multiple definition of …”等错误。

结论:
没有头文件保护宏,重复 include 头文件会导致各种编译和链接问题。
建议所有头文件都加上 #pragma once 或传统的 include guard。

CMoments:
extern SDF_OpenDevice OpenDevice;
extern SDF_CloseDevice CloseDevice;

为什么这里要extern

GitHub Copilot: 这里用 extern 是因为你只是在头文件里声明这两个变量(函数指针),而不是定义它们

  • extern SDF_OpenDevice OpenDevice; 表示“OpenDevice 是一个在别的源文件(比如 sdf_bind.c)里定义的全局变量,这里只是声明一下,方便其它文件引用。”
  • 如果没有 extern,直接写 SDF_OpenDevice OpenDevice;,每个包含这个头文件的源文件都会生成一个自己的副本,导致链接时“重复定义”错误。

总结:
extern 用于声明全局变量或函数,让编译器知道它们在别处定义,避免重复定义和链接错误。

CMoments: 个包含这个头文件的源文件都会生成一个自己的副本,导致链接时“重复定义”错误。
为什么?

GitHub Copilot: 这是因为没有使用 extern 时,头文件里的变量声明会被当作“定义”,每个包含这个头文件的源文件都会生成一份独立的全局变量。

举例说明:

1
2
// sdf_bind.h
SDF_OpenDevice OpenDevice; // 没有 extern

如果你有两个源文件都 #include "sdf_bind.h",编译器会在每个源文件里都生成一个 OpenDevice 变量。
链接时,链接器会发现有多个同名的全局变量(比如 OpenDevice),就会报“multiple definition of OpenDevice”的错误。

正确做法:

  • 在头文件里用 extern 只是声明变量,不分配空间。
  • 在某一个 .c 文件里(如 sdf_bind.c)写一次真正的定义(不加 extern)。

这样,所有源文件都能访问同一个全局变量,且不会重复定义。

总结:
没有 extern,每个包含头文件的源文件都会生成自己的变量副本,导致链接时“重复定义”错误。

CMoments: 声明和定义的区别?那我都在头文件定义可以吗?

GitHub Copilot: 声明(declaration)告诉编译器“有这样一个变量/函数”,但不分配内存,不生成实际内容。
定义(definition)不仅告诉编译器“有这样一个变量/函数”,还分配内存或生成实际内容。


举例

1
2
3
4
5
// 声明
extern int a; // 只是声明,没有分配内存

// 定义
int a = 0; // 这是定义,分配了内存

为什么不能都在头文件定义?

如果你在头文件里直接写 int a = 0;,每个包含这个头文件的源文件都会生成一份 a,链接时就会报“multiple definition of a”错误。

正确做法:

  • 在头文件里用 extern 声明变量或函数。
  • 在某一个 .c 文件里写一次定义。

你的项目应该这样做

  • 头文件(如 sdf_bind.h)里只写 extern SDF_OpenDevice OpenDevice;(声明)。
  • 源文件(如 sdf_bind.c)里写 SDF_OpenDevice OpenDevice = NULL;(定义)。

这样所有源文件都能访问同一个全局变量,不会重复定义。


总结:
头文件只能写声明(用 extern),定义只能写在一个 .c 文件里,否则会链接错误。

CMoments: int sdf_bind_init(void *handle);
那这是定义还是声明

GitHub Copilot: int sdf_bind_init(void *handle);
这是声明,不是定义。

  • 声明:告诉编译器有这样一个函数,参数和返回值是什么,但没有实现(没有函数体)。
  • 定义:要有函数体,比如
    1
    2
    3
    int sdf_bind_init(void *handle) {
    // ...函数实现...
    }

总结:
你在头文件里写的是声明,真正的定义应该在 .c 文件里。

CMoments: 所以说,头文件负责声明。源文件负责定义。编译链接后,对应关系会被自动找到

GitHub Copilot: 完全正确!

  • 头文件(.h)负责声明:告诉编译器有哪些变量、函数、类型可以用,但不分配内存、不实现逻辑。
  • 源文件(.c)负责定义:真正分配内存、实现函数逻辑。
  • 编译和链接阶段:编译器会检查声明,链接器会把所有定义和声明自动对应起来,保证每个全局变量和函数只有唯一的实现。

这样可以让多个源文件共享同一个变量或函数,实现模块化和代码复用。


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