要用 NIST KAT 驗證 sign,有三個 function 要自己寫,PQCgenKAT_sign
會去 call 他們,然後產生 PQCsignKAT_64.req 和 PQCsignKAT_64.rsp 供比對。
int
crypto_sign_keypair(unsigned char *pk, unsigned char *sk);
int
crypto_sign(unsigned char *sm, unsigned long long *smlen,
const unsigned char *m, unsigned long long mlen,
const unsigned char *sk);
int
crypto_sign_open(unsigned char *m, unsigned long long *mlen,
const unsigned char *sm, unsigned long long smlen,
const unsigned char *pk);
SPHINCS+ 官方的 crypto_sign_seed_keypair
int crypto_sign_seed_keypair(unsigned char *pk, unsigned char *sk,
const unsigned char *seed)
{
spx_ctx ctx;
/* Initialize SK_SEED, SK_PRF and PUB_SEED from seed. */
memcpy(sk, seed, CRYPTO_SEEDBYTES);
memcpy(pk, sk + 2*SPX_N, SPX_N);
memcpy(ctx.pub_seed, pk, SPX_N);
memcpy(ctx.sk_seed, sk, SPX_N);
...
memcpy(pk + SPX_N, sk + 3*SPX_N, SPX_N);
}
SPHINCS+ 的 reference code 是先產生一個 3*n bytes 的亂數,然後 memcpy 給 sk,此即為 SK.seed, SK.prf 和 PK.seed。
第 3 個 n bytes 是 PK.seed,所以再 call 一次 memcpy,就是 pk 的第一個 byte。
第 1 個 n bytes 第 2 個 n bytes 第 3 個 n bytes 第 4 個 n bytes
sk: SK.seed || SK.prf || pk.seed || pk.root
pk: pk.seed || pk.root
計算完 pk.root 之後,將 pk.root memcpy 到 sk 的第 4 個 n bytes 和 pk 的第 2 個 n bytes,crypto_sign_keypair 就完成了。
關鍵在於 xmss_node,因為從這裡開始就要自己寫 code 去計算 pk.root
因為要用 PQCgenKAT_sign.c 來驗證我的 implementation,所以在產生 sk, pk 的時候也要照著上述的順序。這時候就不需要key id了,可以直接傳 sk, pk。
crypto_sign_keypair
的 source code 大概是這樣
int crypto_sign_keypair(unsigned char *pk, unsigned char *sk)
{
uint8_t rand[3*SPX_N];
randombytes(rand, sizeof(rand));
memcpy(sk, rand, sizeof(rand));
// 第 3 個 n bytes 是 pk.seed
memcpy(pk, rand[2*SPX_N], SPX_N);
// 計算 pk.root
ADRS adrs;
memset(adrs, 0, 32);
int d = 7; // SLH-DSA-SHA2-128s, d is 7
set_layer_addr(adrs, d-1);
unsigned int h_prime = 9;
uint8_t pk_root[SPX_N] = {0};
// PK.root ← xmss_node(SK.seed, 0, ℎ′, PK.seed, ADRS)
xmss_node(pk_root, sk_seed, 0, h_prime, pk_seed, adrs);
memcpy(pk + SPX_N, pk_root, SPX_N);
memcpy(pk + SPX_N, pk_root, SPX_N);
}
在 crypto_sign
裡放自己的 implementation, 因為 unsigned char *sk 指向一個 4*n bytes 的陣列,所以把 pointer 一次加 n 個 bytes 就能陸續取得 sk_seed、sk_prf、pk_seed 和 pk_root,然後 call slh_dsa_sign
就可以了。
int crypto_sign(unsigned char *sm, unsigned long long *smlen,
const unsigned char *m, unsigned long long mlen,
const unsigned char *sk)
{
// sk: SK.seed || SK.prf || pk.seed || pk.root
unsigned char *p = sk;
memcpy(sk_seed, p, SPX_N); p += SPX_N;
memcpy(sk_prf, p, SPX_N); p += SPX_N;
memcpy(pk_seed, p, SPX_N); p += SPX_N;
memcpy(pk_root, p, SPX_N);
psa_key_id_t sk_key_id = 1;
psa_key_id_t sk_prf_key_id = 2;
psa_key_id_t pk_key_id = 3;
uint8_t sig_out[SPX_BYTES];
uint8_t optrand[SPX_N] = {0};
slh_dsa_sign(sig_out, sk_key_id, sk_prf_key_id, pk_key_id, m, mlen, optrand);
smlen = SPX_BYTES;
memcpy(sm, sig_out, SPX_BYTES);
return 0;
}
最後一個要實作的就是 crypto_sign_open
,我發現這就是要寫驗章,所以我打算跳過這一步,直接用 PQCgenKAT_sign.c 產出的 PQCsignKAT_64.rsp 去跟 Download 3rd round NIST submission package (zip) 的 PQCsignKAT_64.rsp 比對,所以讓 crypto_sign_open
直接 return 成功,不做驗章。
int crypto_sign_open(unsigned char *m, unsigned long long *mlen,
const unsigned char *sm, unsigned long long smlen,
const unsigned char *pk)
{
(void)pk;
if (smlen < (unsigned long long)CRYPTO_BYTES) {
return -1; // malformed input
}
unsigned long long msglen = smlen - (unsigned long long)CRYPTO_BYTES;
memmove(m, sm + CRYPTO_BYTES, (size_t)msglen);
*mlen = msglen;
return 0; // always "valid" (KAT stub)
}
PQCsignKAT_64.rsp 這個 64 不是 x86/x64 的那個 64,他是代表 private key (sk) 的大小,由於在 sha256-128s-simple 裡 sk.seed, sk.prf, pk.seed, pk.root 都是 16 bytes,他們合併就是 sk,加起來的大小就是 4*16=64。所以,api.h 有3個 define 要改
#define CRYPTO_SECRETKEYBYTES 64
#define CRYPTO_PUBLICKEYBYTES 32
#define CRYPTO_BYTES 7856
CRYPTO_BYTES 就是 SLH-DSA signature 的大小,R || FORS signature || HT signature
的 size 就是 16 + 2912 + 4928 = 7856
SPX_N + (FORS sig length) + (HT sig length) = 16 + 2912 + 4928 = 7856
FORS signature length = k * (a + 1) * n = 14 * (12 + 1) * 16 = 2912
下一步就是改 Makefile 和 ci.yml。
PQC – Known Answer Tests and Test Vectors 有提到 KAT 依賴 OpenSSL,那麼 bare-metal 就不合適,所以就直接在 x86 上執行 elf,不使用 Renode。
In addition, you will need to have OpenSSL Version 1.10f
因為在 x86 環境下 access UART 會直接 segmentation fault (core dumped)
,所以 Makefile 要加 -DX86
截錄部份的 Makefile
kat:
@mkdir -p $(KAT_BUILD)
$(HOSTCC) $(HOSTCFLAGS) $(KAT_INCS) $(KAT_SRCS) -DX86 $(SRCS) -o $(KAT_BIN) $(HOSTLDFLAGS)
@echo "[KAT] running in $(KAT_BUILD)"
@cd $(KAT_BUILD) && ./PQCgenKAT_sign
@ls -l $(KAT_BUILD)/PQCsignKAT_*.req $(KAT_BUILD)/PQCsignKAT_*.rsp 2>/dev/null || true
ci.yml
加這一段
- name: KAT
run: |
set -e
make clean
make TARGET=x86 kat
在 uart_min.c 加這個
#ifdef X86
#define RETURN_IF_X86 return
#else
#define RETURN_IF_X86 ((void)0)
#endif
然後在 uart_min.c 的每一個 function 加 RETURN_IF_X86;
例如
void uarte0_init(void)
{
RETURN_IF_X86;
...
}
執行的時候大概會是這個結果
因為 xmss_node
一旦執行就會 segmentation fault (core dumped)
,所以我暫時跳過 xmss_node
,先讓流程走到 print PQCsignKAT_64.rsp
。
xmss_node(pk_root, sk_seed, 0, h_prime, pk_seed, adrs);
經過了二十幾天的實驗和實作,我們己經走到了倒數第二步,就是 產生 PQCsignKAT_64.rsp。這一步的重點就是跟官方 [3
] 的 PQCsignKAT_64.rsp 一致,但倒數第二步還沒有成功,我希望能在明天 Day 29 把這問題解決,然後就能走到了最後一步,將軟體的 SHA256, HMAC-SHA256 換成硬體。