3. Dilithium签名接口封装与验签测试
接口封装目标与统一抽象
Dilithium(ML-DSA)在 FIPS 204 中定义了三种参数集:Dilithium2(NIST 安全等级 2)、Dilithium3(等级 3)、Dilithium5(等级 5)。三者的密钥长度、签名长度和内部矩阵维度不同,但对外接口语义一致。封装的核心是提供一组与参数集无关的统一函数,让上层调用者通过枚举选择参数集,而不必关心底层结构体差异。
以 liboqs 为例,其 OQS_SIG 对象在创建时绑定具体算法名,因此封装层需要维护一个算法名到 OQS_SIG 实例的映射表。下面给出一个最小可用的封装头文件与实现骨架,覆盖密钥生成、签名、验签三个操作,并统一返回码语义。
/* dilithium_wrap.h */
#ifndef DILITHIUM_WRAP_H
#define DILITHIUM_WRAP_H
#include <stddef.h>
#include <stdint.h>
typedef enum {
DILITHIUM_LEVEL2 = 2,
DILITHIUM_LEVEL3 = 3,
DILITHIUM_LEVEL5 = 5
} dilithium_level_t;
/* 返回 0 成功,负值失败 */
int dilithium_keypair(dilithium_level_t lvl,
uint8_t *pk, size_t *pk_len,
uint8_t *sk, size_t *sk_len);
int dilithium_sign(dilithium_level_t lvl,
const uint8_t *sk, size_t sk_len,
const uint8_t *msg, size_t msg_len,
uint8_t *sig, size_t *sig_len);
int dilithium_verify(dilithium_level_t lvl,
const uint8_t *pk, size_t pk_len,
const uint8_t *msg, size_t msg_len,
const uint8_t *sig, size_t sig_len);
/* 查询某参数集的密钥/签名长度,便于调用者分配缓冲区 */
int dilithium_lengths(dilithium_level_t lvl,
size_t *pk_len, size_t *sk_len, size_t *sig_len);
#endif
实现层的关键点有三个:一是算法名映射,liboqs 中 Dilithium2/3/5 对应的算法标识分别为 OQS_SIG_alg_dilithium_2、OQS_SIG_alg_dilithium_3、OQS_SIG_alg_dilithium_5;二是缓冲区长度校验,调用者传入的 *pk_len 等既作为输入容量又作为输出实际长度,必须在写入前检查容量是否足够;三是错误路径统一,任何一步失败都返回负值并保持输出缓冲区不被部分写入。
/* dilithium_wrap.c */
#include "dilithium_wrap.h"
#include <oqs/oqs.h>
#include <string.h>
static const char *alg_name(dilithium_level_t lvl) {
switch (lvl) {
case DILITHIUM_LEVEL2: return OQS_SIG_alg_dilithium_2;
case DILITHIUM_LEVEL3: return OQS_SIG_alg_dilithium_3;
case DILITHIUM_LEVEL5: return OQS_SIG_alg_dilithium_5;
default: return NULL;
}
}
int dilithium_lengths(dilithium_level_t lvl,
size_t *pk_len, size_t *sk_len, size_t *sig_len) {
const char *name = alg_name(lvl);
if (!name) return -1;
OQS_SIG *sig = OQS_SIG_new(name);
if (!sig) return -2;
if (pk_len) *pk_len = sig->length_public_key;
if (sk_len) *sk_len = sig->length_secret_key;
if (sig_len) *sig_len = sig->length_signature;
OQS_SIG_free(sig);
return 0;
}
int dilithium_keypair(dilithium_level_t lvl,
uint8_t *pk, size_t *pk_len,
uint8_t *sk, size_t *sk_len) {
const char *name = alg_name(lvl);
if (!name || !pk || !sk || !pk_len || !sk_len) return -1;
OQS_SIG *sig = OQS_SIG_new(name);
if (!sig) return -2;
if (*pk_len < sig->length_public_key ||
*sk_len < sig->length_secret_key) {
OQS_SIG_free(sig);
return -3; /* 缓冲区不足 */
}
OQS_STATUS rc = OQS_SIG_keypair(sig, pk, sk);
if (rc != OQS_SUCCESS) { OQS_SIG_free(sig); return -4; }
*pk_len = sig->length_public_key;
*sk_len = sig->length_secret_key;
OQS_SIG_free(sig);
return 0;
}
int dilithium_sign(dilithium_level_t lvl,
const uint8_t *sk, size_t sk_len,
const uint8_t *msg, size_t msg_len,
uint8_t *sig_out, size_t *sig_len) {
const char *name = alg_name(lvl);
if (!name || !sk || !msg || !sig_out || !sig_len) return -1;
OQS_SIG *sig = OQS_SIG_new(name);
if (!sig) return -2;
if (sk_len != sig->length_secret_key) { OQS_SIG_free(sig); return -3; }
if (*sig_len < sig->length_signature) { OQS_SIG_free(sig); return -4; }
OQS_STATUS rc = OQS_SIG_sign(sig, sig_out, sig_len, msg, msg_len, sk);
OQS_SIG_free(sig);
return (rc == OQS_SUCCESS) ? 0 : -5;
}
int dilithium_verify(dilithium_level_t lvl,
const uint8_t *pk, size_t pk_len,
const uint8_t *msg, size_t msg_len,
const uint8_t *sig_in, size_t sig_len) {
const char *name = alg_name(lvl);
if (!name || !pk || !msg || !sig_in) return -1;
OQS_SIG *sig = OQS_SIG_new(name);
if (!sig) return -2;
if (pk_len != sig->length_public_key) { OQS_SIG_free(sig); return -3; }
if (sig_len != sig->length_signature) { OQS_SIG_free(sig); return -4; }
OQS_STATUS rc = OQS_SIG_verify(sig, msg, msg_len, sig_in, sig_len, pk);
OQS_SIG_free(sig);
return (rc == OQS_SUCCESS) ? 0 : -5;
}
OQS_SIG_sign 内部使用确定性或随机化签名取决于算法实现,Dilithium 默认采用随机化签名(带随机数 rho'),因此同一消息两次签名结果不同,但验签都应通过。测试时不要用“两次签名结果相同”作为正确性判据。验签失败路径的构造与判读
验签接口的测试重点不是“正常签名能通过”,而是“异常输入必须被拒绝”。需要覆盖的失败场景至少包括:消息被篡改、公钥被替换、签名被篡改、签名长度错误、公钥长度错误。下面给出一个测试用例表,明确每个场景的构造方式与预期返回码。
| 用例编号 | 场景 | 构造方式 | 预期返回 |
|---|---|---|---|
| T1 | 正常验签 | 原始 msg + 原始 pk + 原始 sig | 0(通过) |
| T2 | 消息篡改 | msg 最后一字节取反,其余不变 | -5(拒绝) |
| T3 | 错误公钥 | 用另一对密钥的 pk 验签 | -5(拒绝) |
| T4 | 签名篡改 | sig 中间一字节取反 | -5(拒绝) |
| T5 | 签名长度错误 | sig_len 减 1 | -4(长度校验失败) |
| T6 | 公钥长度错误 | pk_len 减 1 | -3(长度校验失败) |
测试代码骨架如下,使用同一消息分别跑通 T1 到 T6,并断言返回码与预期一致。
/* test_dilithium_verify.c */
#include "dilithium_wrap.h"
#include <stdio.h>
#include <string.h>
#include <assert.h>
static void test_level(dilithium_level_t lvl) {
size_t pk_len, sk_len, sig_len;
assert(dilithium_lengths(lvl, &pk_len, &sk_len, &sig_len) == 0);
uint8_t pk[4096], sk[8192], sig[8192];
size_t pk_l = sizeof(pk), sk_l = sizeof(sk), sig_l = sizeof(sig);
assert(dilithium_keypair(lvl, pk, &pk_l, sk, &sk_l) == 0);
const char *msg = "PQC test message for Dilithium verify";
size_t msg_len = strlen(msg);
sig_l = sizeof(sig);
assert(dilithium_sign(lvl, sk, sk_l, (const uint8_t*)msg, msg_len,
sig, &sig_l) == 0);
/* T1 正常验签 */
assert(dilithium_verify(lvl, pk, pk_l, (const uint8_t*)msg, msg_len,
sig, sig_l) == 0);
/* T2 消息篡改 */
uint8_t bad_msg[64];
memcpy(bad_msg, msg, msg_len);
bad_msg[msg_len - 1] ^= 0x01;
assert(dilithium_verify(lvl, pk, pk_l, bad_msg, msg_len,
sig, sig_l) == -5);
/* T3 错误公钥:重新生成一对密钥 */
uint8_t pk2[4096], sk2[8192];
size_t pk2_l = sizeof(pk2), sk2_l = sizeof(sk2);
assert(dilithium_keypair(lvl, pk2, &pk2_l, sk2, &sk2_l) == 0);
assert(dilithium_verify(lvl, pk2, pk2_l, (const uint8_t*)msg, msg_len,
sig, sig_l) == -5);
/* T4 签名篡改 */
uint8_t bad_sig[8192];
memcpy(bad_sig, sig, sig_l);
bad_sig[sig_l / 2] ^= 0x01;
assert(dilithium_verify(lvl, pk, pk_l, (const uint8_t*)msg, msg_len,
bad_sig, sig_l) == -5);
/* T5 签名长度错误 */
assert(dilithium_verify(lvl, pk, pk_l, (const uint8_t*)msg, msg_len,
sig, sig_l - 1) == -4);
/* T6 公钥长度错误 */
assert(dilithium_verify(lvl, pk, pk_l - 1, (const uint8_t*)msg, msg_len,
sig, sig_l) == -3);
printf("level %d: all verify tests passed\n", (int)lvl);
}
int main(void) {
test_level(DILITHIUM_LEVEL2);
test_level(DILITHIUM_LEVEL3);
test_level(DILITHIUM_LEVEL5);
return 0;
}
三参数集长度对照与缓冲区规划
封装层的一个实际问题是调用者如何分配缓冲区。Dilithium2/3/5 的公钥、私钥、签名长度差异明显,若统一按最大长度分配会浪费内存,若按最小长度分配则高等级参数集会溢出。下表给出 liboqs 中三种参数集的典型长度(字节),用于规划缓冲区。
| 参数集 | 公钥长度 | 私钥长度 | 签名长度 | NIST 等级 |
|---|---|---|---|---|
| Dilithium2 | 1312 | 2528 | 2420 | 2 |
| Dilithium3 | 1952 | 4000 | 3293 | 3 |
| Dilithium5 | 2592 | 4864 | 4595 | 5 |
推荐做法是调用 dilithium_lengths() 动态查询后再分配,而不是硬编码上表数值。上表仅用于快速估算和静态数组声明时的上限参考。若使用静态数组,按 Dilithium5 的长度声明即可覆盖三种参数集。
测试执行与结果判读
编译测试程序时需要链接 liboqs。若 liboqs 以静态库安装,典型编译命令为:
gcc -o test_dilithium test_dilithium_verify.c dilithium_wrap.c \
-I/usr/local/include -L/usr/local/lib -loqs -lcrypto -lpthread
运行后预期输出三行,分别对应三个参数集:
level 2: all verify tests passed
level 3: all verify tests passed
level 5: all verify tests passed
若某个断言失败,程序会因 assert 中止并打印失败位置。此时应优先检查:返回码是否与预期一致、缓冲区长度是否足够、算法名映射是否正确。特别注意 T3 的错误公钥必须来自独立密钥生成,否则可能因公钥编码非法而提前失败,导致返回码与预期不符。
DILITHIUM_LEVEL3 和缓冲区,不必关心底层是 Dilithium3 还是 ML-DSA-65。验签失败路径的测试则确保封装层在异常输入下不会误报通过,这是签名接口安全性的底线。