3. Dilithium签名接口封装与验签测试

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;
}
liboqs 的 OQS_SIG_sign 内部使用确定性或随机化签名取决于算法实现,Dilithium 默认采用随机化签名(带随机数 rho'),因此同一消息两次签名结果不同,但验签都应通过。测试时不要用“两次签名结果相同”作为正确性判据。

验签失败路径的构造与判读

验签接口的测试重点不是“正常签名能通过”,而是“异常输入必须被拒绝”。需要覆盖的失败场景至少包括:消息被篡改、公钥被替换、签名被篡改、签名长度错误、公钥长度错误。下面给出一个测试用例表,明确每个场景的构造方式与预期返回码。

用例编号场景构造方式预期返回
T1正常验签原始 msg + 原始 pk + 原始 sig0(通过)
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;
}
T3 中“错误公钥”必须来自另一次独立的密钥生成,不能简单地把原公钥某字节取反。取反后的公钥可能不满足 Dilithium 公钥的编码约束,验签可能在解析阶段就失败,返回码与“合法公钥但密钥不匹配”不同。用独立生成的合法公钥才能准确测试“密钥不匹配”这一路径。

三参数集长度对照与缓冲区规划

封装层的一个实际问题是调用者如何分配缓冲区。Dilithium2/3/5 的公钥、私钥、签名长度差异明显,若统一按最大长度分配会浪费内存,若按最小长度分配则高等级参数集会溢出。下表给出 liboqs 中三种参数集的典型长度(字节),用于规划缓冲区。

参数集公钥长度私钥长度签名长度NIST 等级
Dilithium21312252824202
Dilithium31952400032933
Dilithium52592486445955

推荐做法是调用 dilithium_lengths() 动态查询后再分配,而不是硬编码上表数值。上表仅用于快速估算和静态数组声明时的上限参考。若使用静态数组,按 Dilithium5 的长度声明即可覆盖三种参数集。

Dilithium 验签测试路径与返回码判读 输入三元组 pk(公钥) msg(消息) sig(签名) + 各自长度参数 长度校验 pk_len == 期望? sig_len == 期望? 不匹配 → 返回 -3 / -4 OQS_SIG_verify 哈希 + 多项式运算 与 pk 中承诺比对 通过 → 0,拒绝 → -5 六条测试路径与预期返回码 T1 正常 原始 pk/msg/sig 返回 0 T2 消息篡改 msg 末字节取反 返回 -5 T3 错误公钥 另一对密钥的 pk 返回 -5 T4 签名篡改 sig 中间字节取反 返回 -5 T5 签名长度 sig_len 减 1 返回 -4 T6 公钥长度 pk_len 减 1 返回 -3 判读要点: • 返回 0 仅表示验签通过,不表示签名由指定私钥产生(需结合密钥管理确认)。 • 返回 -3/-4 属于参数校验失败,未进入密码学运算;返回 -5 属于验签拒绝,已执行完整验签流程。

测试执行与结果判读

编译测试程序时需要链接 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。验签失败路径的测试则确保封装层在异常输入下不会误报通过,这是签名接口安全性的底线。