2. Kyber密钥封装接口封装与单元测试
统一KEM接口的设计边界
Kyber512/768/1024 的差异集中在参数集(k、η1、η2、du、dv)与密文/公钥长度,而调用流程完全一致:keypair → encaps → decaps。因此封装层不应为每个参数集写三套逻辑,而应把“参数集”抽象为运行时选择项,把“算法调用”收敛为三个函数指针或统一入口。
以 liboqs 为例,其 OQS_KEM 结构体已经提供了 length_public_key、length_secret_key、length_ciphertext、length_shared_secret 以及 keypair/encaps/decaps 函数指针。封装层要做的是:
- 用枚举
kyber_level映射到OQS_KEM_alg_kyber_512/768/1024; - 暴露统一的
kyber_kem_keypair/encaps/decaps,内部按 level 取算法对象; - 把长度查询做成
kyber_kem_lengths(level, &pk,&sk,&ct,&ss),避免调用方硬编码 800/1632/1568 等数字; - 所有函数返回
int(0 成功,负值失败),不把库的OQS_STATUS泄漏到业务层。
下面给出可直接编译的封装头与实现(依赖 liboqs,编译时链接 -loqs)。
/* kyber_kem.h */
#ifndef KYBER_KEM_H
#define KYBER_KEM_H
#include <stddef.h>
#include <stdint.h>
typedef enum {
KYBER_512 = 0,
KYBER_768 = 1,
KYBER_1024 = 2
} kyber_level;
typedef struct {
size_t pk_len;
size_t sk_len;
size_t ct_len;
size_t ss_len;
} kyber_lengths;
/* 返回 0 成功,负值失败 */
int kyber_kem_lengths(kyber_level lv, kyber_lengths *out);
int kyber_kem_keypair(kyber_level lv, uint8_t *pk, uint8_t *sk);
int kyber_kem_encaps(kyber_level lv,
const uint8_t *pk,
uint8_t *ct, uint8_t *ss);
int kyber_kem_decaps(kyber_level lv,
const uint8_t *ct, const uint8_t *sk,
uint8_t *ss);
#endif
/* kyber_kem.c */
#include "kyber_kem.h"
#include <oqs/oqs.h>
#include <string.h>
static const char *alg_name(kyber_level lv) {
switch (lv) {
case KYBER_512: return OQS_KEM_alg_kyber_512;
case KYBER_768: return OQS_KEM_alg_kyber_768;
case KYBER_1024: return OQS_KEM_alg_kyber_1024;
default: return NULL;
}
}
static OQS_KEM *get_kem(kyber_level lv) {
const char *name = alg_name(lv);
if (!name) return NULL;
return OQS_KEM_new(name);
}
int kyber_kem_lengths(kyber_level lv, kyber_lengths *out) {
if (!out) return -1;
OQS_KEM *kem = get_kem(lv);
if (!kem) return -2;
out->pk_len = kem->length_public_key;
out->sk_len = kem->length_secret_key;
out->ct_len = kem->length_ciphertext;
out->ss_len = kem->length_shared_secret;
OQS_KEM_free(kem);
return 0;
}
int kyber_kem_keypair(kyber_level lv, uint8_t *pk, uint8_t *sk) {
if (!pk || !sk) return -1;
OQS_KEM *kem = get_kem(lv);
if (!kem) return -2;
OQS_STATUS rc = OQS_KEM_keypair(kem, pk, sk);
OQS_KEM_free(kem);
return (rc == OQS_SUCCESS) ? 0 : -3;
}
int kyber_kem_encaps(kyber_level lv,
const uint8_t *pk,
uint8_t *ct, uint8_t *ss) {
if (!pk || !ct || !ss) return -1;
OQS_KEM *kem = get_kem(lv);
if (!kem) return -2;
OQS_STATUS rc = OQS_KEM_encaps(kem, ct, ss, pk);
OQS_KEM_free(kem);
return (rc == OQS_SUCCESS) ? 0 : -3;
}
int kyber_kem_decaps(kyber_level lv,
const uint8_t *ct, const uint8_t *sk,
uint8_t *ss) {
if (!ct || !sk || !ss) return -1;
OQS_KEM *kem = get_kem(lv);
if (!kem) return -2;
OQS_STATUS rc = OQS_KEM_decaps(kem, ss, ct, sk);
OQS_KEM_free(kem);
return (rc == OQS_SUCCESS) ? 0 : -3;
}
每次调用都
OQS_KEM_new/free 会带来少量开销。若在握手热路径上频繁调用,可改为在初始化阶段缓存三个 OQS_KEM*,但需保证线程安全或每线程独立实例。本例为清晰起见采用即用即建。接口调用与数据流
封装层把“参数集选择”和“算法调用”解耦后,调用方只需持有 kyber_level 与四段缓冲区。下图给出一次完整 KEM 交互中,公钥、密文、共享密钥在 Alice/Bob 两侧的流向,以及封装层在其中的位置。
单元测试:正常路径与边界输入
测试目标不是“跑通一次”,而是验证三件事:长度正确、共享密钥一致、错误输入被拒绝。下面用 C 语言配合 assert 编写,不引入额外测试框架,便于直接嵌入 CI。
/* test_kyber_kem.c */
#include "kyber_kem.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static void test_lengths(void) {
kyber_lengths L;
assert(kyber_kem_lengths(KYBER_512, &L) == 0);
assert(L.pk_len == 800 && L.sk_len == 1632);
assert(L.ct_len == 768 && L.ss_len == 32);
assert(kyber_kem_lengths(KYBER_768, &L) == 0);
assert(L.pk_len == 1184 && L.sk_len == 2400);
assert(L.ct_len == 1088 && L.ss_len == 32);
assert(kyber_kem_lengths(KYBER_1024, &L) == 0);
assert(L.pk_len == 1568 && L.sk_len == 3168);
assert(L.ct_len == 1568 && L.ss_len == 32);
}
static void test_roundtrip(kyber_level lv) {
kyber_lengths L;
assert(kyber_kem_lengths(lv, &L) == 0);
uint8_t *pk = malloc(L.pk_len);
uint8_t *sk = malloc(L.sk_len);
uint8_t *ct = malloc(L.ct_len);
uint8_t *ss_a = malloc(L.ss_len);
uint8_t *ss_b = malloc(L.ss_len);
assert(pk && sk && ct && ss_a && ss_b);
assert(kyber_kem_keypair(lv, pk, sk) == 0);
assert(kyber_kem_encaps(lv, pk, ct, ss_b) == 0);
assert(kyber_kem_decaps(lv, ct, sk, ss_a) == 0);
/* 核心断言:双方共享密钥必须一致 */
assert(memcmp(ss_a, ss_b, L.ss_len) == 0);
free(pk); free(sk); free(ct); free(ss_a); free(ss_b);
}
static void test_bad_inputs(void) {
kyber_lengths L;
assert(kyber_kem_lengths(KYBER_512, &L) == 0);
uint8_t *pk = malloc(L.pk_len);
uint8_t *sk = malloc(L.sk_len);
uint8_t *ct = malloc(L.ct_len);
uint8_t *ss = malloc(L.ss_len);
assert(pk && sk && ct && ss);
/* 空指针必须被拒绝 */
assert(kyber_kem_keypair(KYBER_512, NULL, sk) < 0);
assert(kyber_kem_keypair(KYBER_512, pk, NULL) < 0);
assert(kyber_kem_encaps(KYBER_512, NULL, ct, ss) < 0);
assert(kyber_kem_decaps(KYBER_512, NULL, sk, ss) < 0);
assert(kyber_kem_lengths(KYBER_512, NULL) < 0);
/* 非法 level 必须被拒绝 */
assert(kyber_kem_keypair((kyber_level)99, pk, sk) < 0);
assert(kyber_kem_encaps((kyber_level)99, pk, ct, ss) < 0);
free(pk); free(sk); free(ct); free(ss);
}
static void test_tampered_ciphertext(void) {
kyber_lengths L;
assert(kyber_kem_lengths(KYBER_512, &L) == 0);
uint8_t *pk = malloc(L.pk_len);
uint8_t *sk = malloc(L.sk_len);
uint8_t *ct = malloc(L.ct_len);
uint8_t *ss_a = malloc(L.ss_len);
uint8_t *ss_b = malloc(L.ss_len);
assert(pk && sk && ct && ss_a && ss_b);
assert(kyber_kem_keypair(KYBER_512, pk, sk) == 0);
assert(kyber_kem_encaps(KYBER_512, pk, ct, ss_b) == 0);
/* 篡改密文一个字节 */
ct[0] ^= 0x01;
assert(kyber_kem_decaps(KYBER_512, ct, sk, ss_a) == 0);
/* Kyber 的隐式拒绝:解封装成功但共享密钥不同 */
assert(memcmp(ss_a, ss_b, L.ss_len) != 0);
free(pk); free(sk); free(ct); free(ss_a); free(ss_b);
}
int main(void) {
test_lengths();
test_roundtrip(KYBER_512);
test_roundtrip(KYBER_768);
test_roundtrip(KYBER_1024);
test_bad_inputs();
test_tampered_ciphertext();
printf("all kyber kem tests passed\n");
return 0;
}
Kyber 采用隐式拒绝(implicit rejection):密文被篡改时
decaps 仍返回成功,但输出的共享密钥与发送方不同。因此测试不能只断言返回值,必须比较 ss_a 与 ss_b。若业务层需要显式失败信号,应在 KEM 之上叠加认证(如用长期签名密钥签名密文)。编译与运行
# 假设 liboqs 已安装到 /usr/local
gcc -O2 -Wall -Wextra \
kyber_kem.c test_kyber_kem.c \
-I/usr/local/include -L/usr/local/lib -loqs \
-o test_kyber_kem
./test_kyber_kem
# 预期输出:all kyber kem tests passed
测试覆盖对照
| 测试函数 | 覆盖点 | 关键断言 |
|---|---|---|
test_lengths | 三档参数集长度查询 | pk/sk/ct/ss 长度与规范一致 |
test_roundtrip | 512/768/1024 正常封装解封装 | ss_a == ss_b |
test_bad_inputs | 空指针、非法 level | 返回值 < 0 |
test_tampered_ciphertext | 密文篡改 | 返回值 0 但 ss_a != ss_b |
封装层的价值在于:业务代码不再出现
OQS_KEM_alg_kyber_768 这类库内符号,也不再硬编码 1184/2400/1088 等长度。切换参数集只需改一个 kyber_level 变量,单元测试可对三档参数集复用同一套断言逻辑。