From 323f92f364937a6efdf5a14d640194e2482a3d0e Mon Sep 17 00:00:00 2001 From: ms8421ms Date: Wed, 6 May 2026 15:33:40 +0200 Subject: [PATCH 1/5] feat: LARKG from kyber512 --- lib/pqclean/Makefile | 2 +- lib/pqclean/kyber512/Makefile | 15 +- lib/pqclean/kyber512/kyber_larkg.c | 239 ++++++++++++++++++++++++++ lib/pqclean/kyber512/kyber_larkg.h | 23 +++ lib/pqclean/kyber512/skem.c | 260 +++++++++++++++++++++++++++++ lib/pqclean/kyber512/skem.h | 44 +++++ 6 files changed, 580 insertions(+), 3 deletions(-) create mode 100644 lib/pqclean/kyber512/kyber_larkg.c create mode 100644 lib/pqclean/kyber512/kyber_larkg.h create mode 100644 lib/pqclean/kyber512/skem.c create mode 100644 lib/pqclean/kyber512/skem.h diff --git a/lib/pqclean/Makefile b/lib/pqclean/Makefile index 9f744429b..8adb6b936 100644 --- a/lib/pqclean/Makefile +++ b/lib/pqclean/Makefile @@ -7,7 +7,7 @@ CC ?= gcc COMMON=sha2.o fips202.o # LIB=libkyber512_clean.a -KIBER512=kyber512/cbd.o kyber512/indcpa.o kyber512/kem.o kyber512/ntt.o kyber512/poly.o kyber512/polyvec.o kyber512/reduce.o kyber512/symmetric-shake.o kyber512/verify.o +KIBER512=kyber512/cbd.o kyber512/indcpa.o kyber512/kem.o kyber512/ntt.o kyber512/poly.o kyber512/polyvec.o kyber512/reduce.o kyber512/symmetric-shake.o kyber512/verify.o kyber512/skem.o kyber512/kyber_larkg.o # LIB=libdilithium2_clean.a DILITHIUM2=dilithium2/ntt.o dilithium2/packing.o dilithium2/poly.o dilithium2/polyvec.o dilithium2/reduce.o dilithium2/rounding.o dilithium2/sign.o dilithium2/symmetric-shake.o diff --git a/lib/pqclean/kyber512/Makefile b/lib/pqclean/kyber512/Makefile index 7a5c9468f..7c957b5c9 100644 --- a/lib/pqclean/kyber512/Makefile +++ b/lib/pqclean/kyber512/Makefile @@ -1,8 +1,8 @@ # This Makefile can be used with GNU Make or BSD Make LIB=libkyber512_clean.a -HEADERS=api.h cbd.h indcpa.h kem.h ntt.h params.h poly.h polyvec.h reduce.h symmetric.h verify.h -OBJECTS=cbd.o indcpa.o kem.o ntt.o poly.o polyvec.o reduce.o symmetric-shake.o verify.o ../fips202.o +HEADERS=api.h cbd.h indcpa.h kem.h ntt.h params.h poly.h polyvec.h reduce.h symmetric.h verify.h skem.h kyber_larkg.h +OBJECTS=cbd.o indcpa.o kem.o ntt.o poly.o polyvec.o reduce.o symmetric-shake.o verify.o skem.o kyber_larkg.o ../fips202.o # CFLAGS=-O3 -Wall -Wextra -Wpedantic -Werror -Wmissing-prototypes -Wredundant-decls -std=c99 -I.. $(EXTRAFLAGS) @@ -14,6 +14,17 @@ all: $(LIB) $(LIB): $(OBJECTS) $(AR) -r $@ $(OBJECTS) +RANDOMBYTES=../../../src/randombytes.c + +test_larkg: test_larkg.o $(OBJECTS) $(RANDOMBYTES:.c=.o) + $(CC) $(CFLAGS) -o $@ test_larkg.o $(OBJECTS) $(RANDOMBYTES:.c=.o) -lm + +../../../src/randombytes.o: $(RANDOMBYTES) + $(CC) $(CFLAGS) -fPIC -c -o $@ $(RANDOMBYTES) + +test_larkg.o: test_larkg.c $(HEADERS) + $(CC) $(CFLAGS) -fPIC -c -o $@ test_larkg.c + clean: $(RM) $(OBJECTS) $(RM) $(LIB) diff --git a/lib/pqclean/kyber512/kyber_larkg.c b/lib/pqclean/kyber512/kyber_larkg.c new file mode 100644 index 000000000..6a5d44db9 --- /dev/null +++ b/lib/pqclean/kyber512/kyber_larkg.c @@ -0,0 +1,239 @@ +#include "kyber_larkg.h" +#include "skem.h" +#include "indcpa.h" +#include "poly.h" +#include "polyvec.h" +#include "symmetric.h" +#include +#include +#include +#include // REMOVE AFTER TESTING + +// Imported from zenroom +extern int randombytes(void *buf, size_t n); + +// --- Helpers for rejection sampling in LARKG --- + +// Parameters for LARKG +#define LARKG_ETA KYBER_ETA1 +#define KYBER_MONT_R_INV 169 // Montgomery constant R^-1 mod q, where R = 2^16 and q = 3329. Used for converting to/from Montgomery form. + +// Function to convert from Montgomery form to standard representation and center around 0 +static inline int16_t demont_center(int16_t coeff) { + int32_t v = ((int32_t)coeff % KYBER_Q + KYBER_Q) % KYBER_Q; // Ensure coeff is in [0, q-1] + v = (v * KYBER_MONT_R_INV) % KYBER_Q; // Convert from Montgomery form to standard representation + if (v >= KYBER_Q - 2 * KYBER_ETA1) { + v -= KYBER_Q; // Center around 0 + } + return (int16_t)v; +} + +// Function to compute gcd, to reduce numerator and denominator in the rejection sampling step +static uint64_t gcd64(uint64_t a, uint64_t b) { + while (b != 0) { + uint64_t t = b; + b = a % b; + a = t; + } + return a; +} + +// Function to generate a 64-bit number +static uint64_t random64(void) { + uint8_t buf[8]; + + randombytes(buf, 8); + + return ((uint64_t)buf[0] << 56) | + ((uint64_t)buf[1] << 48) | + ((uint64_t)buf[2] << 40) | + ((uint64_t)buf[3] << 32) | + ((uint64_t)buf[4] << 24) | + ((uint64_t)buf[5] << 16) | + ((uint64_t)buf[6] << 8) | + ((uint64_t)buf[7]); +} + +// LARKG rejection sampling in integer-only arithmetic +// Returns 0 on success, 1 on rejection +static int larkg_rej_sampling(const polyvec *S_flat, + const polyvec *S_pp_flat) { + static const int64_t LOG2_RSP[4] = {283261, 321617, 234953, 65536}; + static const int64_t LOG2_M_Q16 = 1182310LL; + + int64_t log2_num = LOG2_M_Q16; + + for (int i = 0; i < KYBER_K; i++) { + for (int j = 0; j < KYBER_N; j++) { + int16_t s_ij = demont_center(S_flat->vec[i].coeffs[j]); + int16_t s_pp_ij = demont_center(S_pp_flat->vec[i].coeffs[j]); + + if (abs(s_ij) > LARKG_ETA || abs(s_pp_ij - s_ij) > 2 * LARKG_ETA) + return 1; + if (abs(s_ij) > 3 || abs(s_pp_ij - s_ij) > 3) + return 1; + + log2_num += LOG2_RSP[abs(s_pp_ij - s_ij)] - LOG2_RSP[abs(s_ij)]; + } + } + + static int debug_count = 0; + debug_count++; + if (debug_count <= 10) { + printf(" [SAMPLE %d] log2_num=%.4f neg=%lld int_part=%lld\n", + debug_count, + (double)log2_num / 65536.0, + (long long)(-log2_num), + (long long)((-log2_num) >> 16)); + } + + // Reject if reject_prob >= 1 + if (log2_num >= 0) + return 1; + + // threshold = 2^(64 + log2_num/65536), with log2_num < 0 + int64_t neg = -log2_num; + int64_t int_part = neg >> 16; + int64_t frac_part = neg & 0xFFFF; + + if (int_part >= 64) + return 1; + + // base = 2^(64 - int_part) as uint64_t + uint64_t base = (int_part == 0) ? UINT64_MAX : (UINT64_MAX >> int_part); + + // Correct for fractional part using ln2 approximation: + // 2^(-frac/65536) ≈ 1 - frac/65536 * ln2 + // ln2 * 65536 = 45426 */ + uint64_t threshold = base - ((__uint128_t)base * frac_part * 45426ULL >> 32); + + uint64_t u64 = random64(); + return (u64 < threshold) ? 1 : 0; +} +// ------------------------------------------------- + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_larkg_derive_pk +* +* Description: Derives the next public key and encapsulating key for the sender. +* +* Arguments: uint8_t *next_pk: pointer to output next public key +* larkg_cred_t *cred_out: pointer to output credentials (encapsulating key and authentication tag) +* const uint8_t *current_pk: pointer to input current public key +* const skem_context *ctx: pointer to the global context +* +* Returns: 0 on success +**************************************************/ +int PQCLEAN_KYBER512_CLEAN_larkg_derive_pk(uint8_t next_pk[KYBER_INDCPA_PUBLICKEYBYTES], + larkg_cred_t *cred_out, + const uint8_t current_pk[KYBER_INDCPA_PUBLICKEYBYTES], + const skem_context *ctx) { + uint8_t S_prime_bytes[KYBER_INDCPA_SECRETKEYBYTES]; + uint8_t k_seed[KYBER_SSBYTES]; + uint8_t rand_buf[KYBER_SYMBYTES]; + + polyvec B_poly, K_poly, E_prime_poly, P_poly; + polyvec matrix_A[KYBER_K]; + + PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(&B_poly, current_pk); + + // Ln 1 + PQCLEAN_KYBER512_CLEAN_skem_keygen_enc(cred_out->B_prime, S_prime_bytes, ctx); + + // Ln 2 + PQCLEAN_KYBER512_CLEAN_skem_encaps(cred_out->c, k_seed, S_prime_bytes, current_pk); + + // Ln 3 + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&K_poly.vec[i], k_seed, i); + PQCLEAN_KYBER512_CLEAN_poly_ntt(&K_poly.vec[i]); + } + + // Ln 4 + hash_h(cred_out->mu, k_seed, KYBER_SSBYTES); + + // Ln 5 + randombytes(rand_buf, KYBER_SYMBYTES); + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&E_prime_poly.vec[i], rand_buf, i); + PQCLEAN_KYBER512_CLEAN_poly_ntt(&E_prime_poly.vec[i]); + } + + // Ln 6 + const uint8_t *rho = current_pk + KYBER_POLYVECBYTES; + PQCLEAN_KYBER512_CLEAN_gen_matrix(matrix_A, rho, 0); + + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_polyvec_basemul_acc_montgomery(&P_poly.vec[i], &matrix_A[i], &K_poly); + PQCLEAN_KYBER512_CLEAN_poly_tomont(&P_poly.vec[i]); + + PQCLEAN_KYBER512_CLEAN_poly_add(&P_poly.vec[i], &P_poly.vec[i], &E_prime_poly.vec[i]); + PQCLEAN_KYBER512_CLEAN_poly_add(&P_poly.vec[i], &P_poly.vec[i], &B_poly.vec[i]); + PQCLEAN_KYBER512_CLEAN_poly_reduce(&P_poly.vec[i]); + } + + // Ln 7 + PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(next_pk, &P_poly); + memcpy(next_pk + KYBER_POLYVECBYTES, rho, KYBER_SYMBYTES); + + return 0; +} + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_larkg_derive_sk +* +* Description: Derives the next secret key for the receiver. +* +* Arguments: uint8_t *next_sk: pointer to output next secret key +* const uint8_t *current_sk: pointer to input current secret key +* const larkg_cred_t *cred_in: pointer to input credentials (encapsulating key and authentication tag) +* +* Returns: 0 on success, -1 on rejection, -2 on failed authentication +**************************************************/ +int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t current_sk[KYBER_INDCPA_SECRETKEYBYTES], + const larkg_cred_t *cred_in) { + uint8_t k_seed[KYBER_SSBYTES]; + uint8_t mu_star[32]; + polyvec S_poly, K_poly, S_prime_prime; + + // Ln 1 + PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(&S_poly, current_sk); + + // Ln 2 + PQCLEAN_KYBER512_CLEAN_skem_decaps(k_seed, current_sk, cred_in->c, cred_in->B_prime); + + // Ln 3 + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&K_poly.vec[i], k_seed, (uint8_t)i); + PQCLEAN_KYBER512_CLEAN_poly_ntt(&K_poly.vec[i]); + } + + // Ln 4 + hash_h(mu_star, k_seed, KYBER_SSBYTES); + + // Ln 5 + if (memcmp(mu_star, cred_in->mu, 32) != 0) { + return -2; // Authentication failed (mu_star != mu) + } + + // Ln 6 + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_add(&S_prime_prime.vec[i], &S_poly.vec[i], &K_poly.vec[i]); + PQCLEAN_KYBER512_CLEAN_poly_reduce(&S_prime_prime.vec[i]); + } + + // Ln 7 and 8 (rejection sampling) + polyvec S_flat = S_poly; + polyvec S_pp_flat = S_prime_prime; + PQCLEAN_KYBER512_CLEAN_polyvec_invntt_tomont(&S_flat); + PQCLEAN_KYBER512_CLEAN_polyvec_invntt_tomont(&S_pp_flat); + + int rej = larkg_rej_sampling(&S_flat, &S_pp_flat); + if (rej != 0) return -1; // Rejection + + // Ln 9 + PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(next_sk, &S_prime_prime); + + return 0; +} diff --git a/lib/pqclean/kyber512/kyber_larkg.h b/lib/pqclean/kyber512/kyber_larkg.h new file mode 100644 index 000000000..aa0aac4c1 --- /dev/null +++ b/lib/pqclean/kyber512/kyber_larkg.h @@ -0,0 +1,23 @@ +#ifndef KYBER_LARKG_H +#define KYBER_LARKG_H + +#include "params.h" +#include "skem.h" +#include + +typedef struct { + uint8_t B_prime[KYBER_POLYVECBYTES]; // Encapsulating key + uint8_t c[KYBER_POLYCOMPRESSEDBYTES]; // SKEM ciphertext + uint8_t mu[32]; // Authentication tag (hash of shared secret) +} larkg_cred_t; + +int PQCLEAN_KYBER512_CLEAN_larkg_derive_pk(uint8_t next_pk[KYBER_INDCPA_PUBLICKEYBYTES], + larkg_cred_t *cred_out, + const uint8_t current_pk[KYBER_INDCPA_PUBLICKEYBYTES], + const skem_context *ctx); + +int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t current_sk[KYBER_INDCPA_SECRETKEYBYTES], + const larkg_cred_t *cred_in); + +#endif diff --git a/lib/pqclean/kyber512/skem.c b/lib/pqclean/kyber512/skem.c new file mode 100644 index 000000000..e744fdfac --- /dev/null +++ b/lib/pqclean/kyber512/skem.c @@ -0,0 +1,260 @@ +// Split KEM implementation for Kyber512 (see https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=10190483) +// To be used in LARKG instantiated from Kyber + +#include "skem.h" +#include "indcpa.h" +#include "ntt.h" +#include "params.h" +#include "poly.h" +#include "polyvec.h" +#include "symmetric.h" +#include +#include +#include + +// Imported from zenroom +extern int randombytes(void *buf, size_t n); + +extern void PQCLEAN_KYBER512_CLEAN_gen_matrix(polyvec *a, const uint8_t seed[KYBER_SYMBYTES], int transposed); + +/************************************************* +* Name: skem_pack_pk +* +* Description: Serialize the public key as concatenation of the +* serialized vector of polynomials pk and the global seed rho. +* +* Arguments: uint8_t *r: pointer to output serialized public key +* polyvec *pk: pointer to input vector of polynomials (public key) +* const uint8_t *seed: pointer to input public seed (rho) +**************************************************/ +static void skem_pack_pk(uint8_t r[KYBER_INDCPA_PUBLICKEYBYTES], + polyvec *pk, + const uint8_t seed[KYBER_SYMBYTES]) { + PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(r, pk); + for (size_t i = 0; i < KYBER_SYMBYTES; i++) { + r[i + KYBER_POLYVECBYTES] = seed[i]; + } +} + +/************************************************* +* Name: skem_unpack_pk +* +* Description: De-serialize public key from a byte array. Note that +* the global seed rho is ignored here since it's part of the context. +* +* Arguments: polyvec *pk: pointer to output vector of polynomials (public key) +* const uint8_t *packedpk: pointer to input serialized public key +**************************************************/ +static void skem_unpack_pk(polyvec *pk, + const uint8_t packedpk[KYBER_INDCPA_PUBLICKEYBYTES]) { + PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(pk, packedpk); +} + +/************************************************* +* Name: skem_pack_sk +* +* Description: Serialize the secret key. +* +* Arguments: uint8_t *r: pointer to output serialized secret key +* polyvec *sk: pointer to input vector of polynomials (secret key) +**************************************************/ +static void skem_pack_sk(uint8_t r[KYBER_INDCPA_SECRETKEYBYTES], + polyvec *sk) { + PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(r, sk); +} + +/************************************************* +* Name: skem_unpack_sk +* +* Description: De-serialize the secret key; inverse of skem_pack_sk. +* +* Arguments: polyvec *sk: pointer to output vector of polynomials (secret key) +* const uint8_t *packedsk: pointer to input serialized secret key +**************************************************/ +static void skem_unpack_sk(polyvec *sk, + const uint8_t packedsk[KYBER_INDCPA_SECRETKEYBYTES]) { + PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(sk, packedsk); +} + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_skem_init +* +* Description: Initializes the global Split-KEM context. +* +* Arguments: skem_context *ctx: pointer to the context to be initialized +* const uint8_t *rho: pointer to the shared 32-byte seed +**************************************************/ +void PQCLEAN_KYBER512_CLEAN_skem_init(skem_context *ctx, + const uint8_t rho[KYBER_SYMBYTES]) { + + // Store rho in the context + memcpy(ctx->rho, rho, KYBER_SYMBYTES); + + // Generate matrices A and A^T from rho + PQCLEAN_KYBER512_CLEAN_gen_matrix(ctx->a, ctx->rho, 0); // Generate A + PQCLEAN_KYBER512_CLEAN_gen_matrix(ctx->at, ctx->rho, 1); // Generate A^T +} + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_skem_keygen_dec +* +* Description: Generates public and private key for the receiver. +* +* Arguments: uint8_t *pk: pointer to output public key +* uint8_t *sk: pointer to output private key +* const skem_context *ctx: pointer to the global context +**************************************************/ +void PQCLEAN_KYBER512_CLEAN_skem_keygen(uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES], + uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + const skem_context *ctx) { + uint8_t buf[2 * KYBER_SYMBYTES]; + uint8_t nonce = 0; + polyvec e, skpv, pkpv; + + randombytes(buf, KYBER_SYMBYTES); + hash_g(buf, buf, KYBER_SYMBYTES); + + // Generate the error vector s ∈ R^k + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&skpv.vec[i], buf, nonce++); + } + + // Generate the error vector e ∈ R^k + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&e.vec[i], buf, nonce++); + } + + PQCLEAN_KYBER512_CLEAN_polyvec_ntt(&skpv); // No need to reduce coefficients of skpv since they are already small + PQCLEAN_KYBER512_CLEAN_polyvec_ntt(&e); + + // Compute the public key pk = A * s + e and reduce coefficients + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_polyvec_basemul_acc_montgomery(&pkpv.vec[i], &ctx->a[i], &skpv); + PQCLEAN_KYBER512_CLEAN_poly_tomont(&pkpv.vec[i]); + } + PQCLEAN_KYBER512_CLEAN_polyvec_add(&pkpv, &pkpv, &e); + PQCLEAN_KYBER512_CLEAN_polyvec_reduce(&pkpv); + + // Encode pk and sk to byte arrays + skem_pack_sk(sk, &skpv); + skem_pack_pk(pk, &pkpv, ctx->rho); +} + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_skem_keygen_enc +* +* Description: Generates the encapsulation key pair (pkp, skp) for the sender. +* +* Arguments: uint8_t *pkp: pointer to output public key for encapsulation +* uint8_t *skp: pointer to output secret key for encapsulation +* const skem_context *ctx: pointer to the global context +**************************************************/ +void PQCLEAN_KYBER512_CLEAN_skem_keygen_enc(uint8_t pkp[KYBER_POLYVECBYTES], + uint8_t skp[KYBER_INDCPA_SECRETKEYBYTES], + const skem_context *ctx) { + uint8_t coins[KYBER_SYMBYTES]; + uint8_t nonce = 0; + polyvec r, e1, u; + + randombytes(coins, KYBER_SYMBYTES); + + // Generate the error vector r ∈ R^k + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&r.vec[i], coins, nonce++); + } + // Generate the error vector e1 ∈ R^k + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta2(&e1.vec[i], coins, nonce++); + } + + PQCLEAN_KYBER512_CLEAN_polyvec_ntt(&r); + + // Compute u = A^t * skp + e1 and reduce coefficients + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_polyvec_basemul_acc_montgomery(&u.vec[i], &ctx->at[i], &r); + } + PQCLEAN_KYBER512_CLEAN_polyvec_invntt_tomont(&u); + PQCLEAN_KYBER512_CLEAN_polyvec_add(&u, &u, &e1); + PQCLEAN_KYBER512_CLEAN_polyvec_reduce(&u); + + // Encode pkp and skp to byte arrays + PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(pkp, &u); + skem_pack_sk(skp, &r); +} + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_skem_encaps +* +* Description: Generates cipher text and shared secret for given public key. +* +* Arguments: uint8_t *c_out: pointer to output cipher text +* uint8_t *K: pointer to output shared secret +* const uint8_t *skp: pointer to input secret key for encapsulation +* const uint8_t *pk: pointer to input public key for encapsulation +**************************************************/ +void PQCLEAN_KYBER512_CLEAN_skem_encaps(uint8_t c_out[KYBER_POLYCOMPRESSEDBYTES], + uint8_t K[KYBER_SSBYTES], + const uint8_t skp[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES]) { + uint8_t coins[KYBER_SYMBYTES]; + uint8_t buf[KYBER_SYMBYTES]; + uint8_t nonce = 0; + polyvec sp, pkpv; + poly v, e2, m_poly; + + randombytes(buf, KYBER_SSBYTES); + hash_h(K, buf, KYBER_SSBYTES); + + // Encode message as polynomial + PQCLEAN_KYBER512_CLEAN_poly_frommsg(&m_poly, K); + skem_unpack_pk(&pkpv, pk); + skem_unpack_sk(&sp, skp); + + randombytes(coins, KYBER_SYMBYTES); + + // Generate the error polynomial e2 ∈ R + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta2(&e2, coins, nonce++); + + // Compute v = pk * sp + e2 + m_poly and reduce coefficients + PQCLEAN_KYBER512_CLEAN_polyvec_basemul_acc_montgomery(&v, &pkpv, &sp); + PQCLEAN_KYBER512_CLEAN_poly_invntt_tomont(&v); + PQCLEAN_KYBER512_CLEAN_poly_add(&v, &v, &e2); + PQCLEAN_KYBER512_CLEAN_poly_add(&v, &v, &m_poly); + PQCLEAN_KYBER512_CLEAN_poly_reduce(&v); + + // Ciphertext to bytes + PQCLEAN_KYBER512_CLEAN_poly_compress(c_out, &v); +} + +/************************************************* +* Name: PQCLEAN_KYBER512_CLEAN_skem_decaps +* +* Description: Generates shared secret for given cipher text and private key. +* +* Arguments: uint8_t *m: pointer to output shared secret +* const uint8_t *sk: pointer to input private key +* const uint8_t *c_in: pointer to input cipher text +* const uint8_t *pkp: pointer to input public key for encapsulation +**************************************************/ +void PQCLEAN_KYBER512_CLEAN_skem_decaps(uint8_t m[KYBER_INDCPA_MSGBYTES], + const uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t c_in[KYBER_POLYCOMPRESSEDBYTES], + const uint8_t pkp[KYBER_POLYVECBYTES]) { + polyvec u, skpv; + poly v, mp; + + PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(&u, pkp); + PQCLEAN_KYBER512_CLEAN_poly_decompress(&v, c_in); + skem_unpack_sk(&skpv, sk); + + PQCLEAN_KYBER512_CLEAN_polyvec_ntt(&u); + + // Recover message polynomial mp = v - u * skpv and reduce coefficients + PQCLEAN_KYBER512_CLEAN_polyvec_basemul_acc_montgomery(&mp, &skpv, &u); + PQCLEAN_KYBER512_CLEAN_poly_invntt_tomont(&mp); + PQCLEAN_KYBER512_CLEAN_poly_sub(&mp, &v, &mp); + PQCLEAN_KYBER512_CLEAN_poly_reduce(&mp); + + // Message to bytes + PQCLEAN_KYBER512_CLEAN_poly_tomsg(m, &mp); +} diff --git a/lib/pqclean/kyber512/skem.h b/lib/pqclean/kyber512/skem.h new file mode 100644 index 000000000..9997bdf97 --- /dev/null +++ b/lib/pqclean/kyber512/skem.h @@ -0,0 +1,44 @@ +// Split KEM implementation for Kyber512 (see https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=10190483) +// To be used in LARKG instantiated from Kyber + +#ifndef PQCLEAN_KYBER512_CLEAN_SKEM_H +#define PQCLEAN_KYBER512_CLEAN_SKEM_H + +#include "params.h" +#include "polyvec.h" +#include + +/************************************************* +* Name: skem_context +* +* Description: Global context for the Split-KEM scheme. +* Contains the globally shared seed rho and the pre-computed +* matrices A and A^T. +**************************************************/ +typedef struct { + uint8_t rho[KYBER_SYMBYTES]; + polyvec a[KYBER_K]; + polyvec at[KYBER_K]; +} skem_context; + +void PQCLEAN_KYBER512_CLEAN_skem_init(skem_context *ctx, const uint8_t rho[KYBER_SYMBYTES]); + +void PQCLEAN_KYBER512_CLEAN_skem_keygen(uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES], + uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + const skem_context *ctx); + +void PQCLEAN_KYBER512_CLEAN_skem_keygen_enc(uint8_t pkp[KYBER_POLYVECBYTES], + uint8_t skp[KYBER_INDCPA_SECRETKEYBYTES], + const skem_context *ctx); + +void PQCLEAN_KYBER512_CLEAN_skem_encaps(uint8_t c_out[KYBER_POLYCOMPRESSEDBYTES], + uint8_t K[KYBER_SSBYTES], + const uint8_t skp[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES]); + +void PQCLEAN_KYBER512_CLEAN_skem_decaps(uint8_t m[KYBER_INDCPA_MSGBYTES], + const uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t c_in[KYBER_POLYCOMPRESSEDBYTES], + const uint8_t pkp[KYBER_POLYVECBYTES]); + +#endif From 5562c37188dcf34c2d26f76f97b973468dfc2d8b Mon Sep 17 00:00:00 2001 From: ms8421ms Date: Mon, 18 May 2026 11:12:02 +0200 Subject: [PATCH 2/5] chore: larkg zencode syntax + test --- src/lua/zencode_keyring.lua | 4 + src/lua/zencode_larkg.lua | 82 ++++++++++++++ src/lua_modules.c | 3 + src/zen_larkg.c | 214 ++++++++++++++++++++++++++++++++++++ test/zencode/larkg.bats | 138 +++++++++++++++++++++++ 5 files changed, 441 insertions(+) create mode 100644 src/lua/zencode_larkg.lua create mode 100644 src/zen_larkg.c create mode 100644 test/zencode/larkg.bats diff --git a/src/lua/zencode_keyring.lua b/src/lua/zencode_keyring.lua index 02e95ec10..9f793d990 100644 --- a/src/lua/zencode_keyring.lua +++ b/src/lua/zencode_keyring.lua @@ -152,6 +152,10 @@ local keytypes = { fsp = { import = function(obj) return schema_get(obj, 'fsp', nop, O.from_base64) end, export = function(obj) return obj.fsp:octet():base64() end + }, + larkg = { + import = function(obj) return schema_get(obj, 'larkg', nop, O.from_base64) end, + export = function(obj) return obj.larkg:octet():base64() end } } diff --git a/src/lua/zencode_larkg.lua b/src/lua/zencode_larkg.lua new file mode 100644 index 000000000..2a417e581 --- /dev/null +++ b/src/lua/zencode_larkg.lua @@ -0,0 +1,82 @@ +--[[ +--This file is part of zenroom +-- +--Copyright (C) 2018-2026 Dyne.org foundation +--designed, written and maintained by Denis Roio +-- +--This program is free software: you can redistribute it and/or modify +--it under the terms of the GNU Affero General Public License as +--published by the Free Software Foundation, either version 3 of the +--License, or (at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Affero General Public License for more details. +-- +--You should have received a copy of the GNU Affero General Public License +--along with this program. If not, see . +-- +--Last modified by Matteo Sangalli +--on Tuesday, 12th May 2026 +--]] + +LARKG = require_once'larkg' + +local function larkg_public_key_f(obj) + local res = schema_get(obj, '.') + zencode_assert( + LARKG.pubcheck(res), + 'Public key is not valid' + ) + return res +end + +local function larkg_cred_f(obj) + local res = schema_get(obj, '.') + zencode_assert( + LARKG.credcheck(res), + 'Credential is not valid' + ) + return res +end + +ZEN:add_schema( + { + larkg_public_key = {import=larkg_public_key_f}, + larkg_derived_public_key = {import=larkg_public_key_f}, + larkg_cred = {import=larkg_cred_f}, + larkg_rho = {import=function(obj) return schema_get(obj, '.') end} + } +) + +-- Generate initial keypair and public parameters for larkg +When("create larkg key", function() + initkeyring'larkg' + local kp = LARKG.keygen() + ACK.keyring.larkg = kp.private + ACK.larkg_public_key = kp.public + ACK.larkg_rho = kp.rho -- part of public parameters + new_codec('larkg public key') + new_codec('larkg rho') +end) + +-- Derive next public key (sender side) +When("derive next larkg public key from '' with rho ''", function(pub, rho_in) + local pk = have(pub) + local rho = have(rho_in) + local res = LARKG.derive_pk(pk, rho) + ACK.larkg_derived_public_key = res.next_public + ACK.larkg_credential = res.credential + if not CODEC.larkg_derived_public_key then + new_codec('larkg derived public key') + end + new_codec('larkg credential') +end) + +-- Derive next secret key (receiver side) +When("derive next larkg secret key with credential ''", function(cred_in) + local sk = havekey'larkg' + local cred = have(cred_in) + ACK.keyring.larkg = LARKG.derive_sk(sk, cred) +end) diff --git a/src/lua_modules.c b/src/lua_modules.c index b15d5ef2f..c14c2ae82 100644 --- a/src/lua_modules.c +++ b/src/lua_modules.c @@ -62,6 +62,7 @@ extern int luaopen_p256(lua_State *L); extern int luaopen_x509(lua_State *L); extern int luaopen_varint(lua_State *L); extern int luaopen_longfellow(lua_State *L); +extern int luaopen_larkg(lua_State *L); #ifdef ZEN_ENABLE_ZKCC extern int luaopen_zkcore(lua_State *L); #endif @@ -205,6 +206,8 @@ int zen_require(lua_State *L) { luaL_requiref(L, s, luaopen_varint, 1); } else if(strcasecmp(s, "longfellow") ==0) { luaL_requiref(L, s, luaopen_longfellow, 1); } + else if(strcasecmp(s, "larkg") ==0) { + luaL_requiref(L, s, luaopen_larkg, 1); } #ifdef ZEN_ENABLE_ZKCC else if(strcasecmp(s, "zkcore") ==0) { luaL_requiref(L, s, luaopen_zkcore, 1); } diff --git a/src/zen_larkg.c b/src/zen_larkg.c new file mode 100644 index 000000000..38329d6ac --- /dev/null +++ b/src/zen_larkg.c @@ -0,0 +1,214 @@ +#include +#include +#include +#include + +#include "../lib/pqclean/kyber512/kyber_larkg.h" +#include "../lib/pqclean/kyber512/skem.h" +#include "../lib/pqclean/kyber512/params.h" + +#define LARKG_SK_BYTES KYBER_INDCPA_SECRETKEYBYTES +#define LARKG_PK_BYTES KYBER_INDCPA_PUBLICKEYBYTES +#define LARKG_CRED_BYTES (sizeof(larkg_cred_t)) + +// Imported from Zenroom +extern int randombytes(void *buf, size_t n); + +// skem_context is shared global state +// Here it is serialised as the 32 byte seed rho and is rebuilt on demand since gen_matrix is deterministic + +// --- Internal functions --- + +// Rebuild the skem_context from the seed rho +static void _ctx_from_rho(skem_context *ctx, const octet *rho) { + PQCLEAN_KYBER512_CLEAN_skem_init(ctx, (const uint8_t *)rho->val); +} + +// Serialise larkg_cred_t to a octet: B_prime || c || mu +static void _cred_to_octet(octet *oct, const larkg_cred_t *cred) { + size_t offset = 0; + memcpy(oct->val + offset, cred->B_prime, KYBER_POLYVECBYTES); + offset += KYBER_POLYVECBYTES; + memcpy(oct->val + offset, cred->c, KYBER_POLYCOMPRESSEDBYTES); + offset += KYBER_POLYCOMPRESSEDBYTES; + memcpy(oct->val + offset, cred->mu, KYBER_SSBYTES); + oct->len = offset + KYBER_SSBYTES; +} + +// Deserialise larkg_cred_t from a octet: B_prime || c || mu +static int _octet_to_cred(larkg_cred_t *cred, const octet *oct) { + if(oct->len != LARKG_CRED_BYTES) { + return 0; + } + size_t offset = 0; + memcpy(cred->B_prime, oct->val + offset, KYBER_POLYVECBYTES); + offset += KYBER_POLYVECBYTES; + memcpy(cred->c, oct->val + offset, KYBER_POLYCOMPRESSEDBYTES); + offset += KYBER_POLYCOMPRESSEDBYTES; + memcpy(cred->mu, oct->val + offset, KYBER_SSBYTES); + return 1; +} + +// --- Lua bindings --- + +// Generate initial LARKG keypair (sk, pk) and the shared rho seed +static int larkg_keygen(lua_State *L) { + + BEGIN(); + char *failed_msg = NULL; + + lua_createtable(L, 0, 3); + + octet *sk = o_new(L, LARKG_SK_BYTES); SAFE_GOTO(sk, "Could not allocate LARKG secret key"); + lua_setfield(L, -2, "private"); + + octet *pk = o_new(L, LARKG_PK_BYTES); SAFE_GOTO(pk, "Could not allocate LARKG public key"); + lua_setfield(L, -2, "public"); + + octet *rho = o_new(L, KYBER_SYMBYTES); SAFE_GOTO(rho, "Could not allocate LARKG rho seed"); + lua_setfield(L, -2, "rho"); + + randombytes((uint8_t *)rho->val, KYBER_SYMBYTES); + rho->len = KYBER_SYMBYTES; + + skem_context ctx; + _ctx_from_rho(&ctx, rho); + PQCLEAN_KYBER512_CLEAN_skem_keygen((uint8_t *)pk->val, (uint8_t *)sk->val, &ctx); + + pk->len = LARKG_PK_BYTES; + sk->len = LARKG_SK_BYTES; + +end: + if(failed_msg) { + THROW(failed_msg); + } + + END(1); +} + +// Derive the next public key (sender side) +static int larkg_derive_pk(lua_State *L) { + + BEGIN(); + char *failed_msg = NULL; + const octet *pk = NULL; + const octet *rho = NULL; + + pk = o_arg(L, 1); SAFE_GOTO(pk, "Could not allocate LARKG current public key"); + SAFE_GOTO(pk->len == LARKG_PK_BYTES, "Invalid LARKG public key length"); + + rho = o_arg(L, 2); SAFE_GOTO(rho, "Could not allocate LARKG rho seed"); + SAFE_GOTO(rho->len == KYBER_SYMBYTES, "Invalid LARKG rho seed length"); + + lua_createtable(L, 0, 1); + + octet *next_pk = o_new(L, LARKG_PK_BYTES); SAFE_GOTO(next_pk, "Could not allocate LARKG next public key"); + lua_setfield(L, -2, "next_public"); + + octet *cred_oct = o_new(L, LARKG_CRED_BYTES); SAFE_GOTO(cred_oct, "Could not allocate LARKG credential octet"); + lua_setfield(L, -2, "credential"); + + skem_context ctx; + _ctx_from_rho(&ctx, rho); + + larkg_cred_t cred; + int ret = PQCLEAN_KYBER512_CLEAN_larkg_derive_pk((uint8_t *)next_pk->val, &cred, (const uint8_t *)pk->val, &ctx); + SAFE_GOTO(ret == 0, "LARKG derive_pk failed"); + + next_pk->len = LARKG_PK_BYTES; + _cred_to_octet(cred_oct, &cred); + +end: + o_free(L, rho); + o_free(L, pk); + if(failed_msg) { + THROW(failed_msg); + } + + END(1); +} + +// Derive the next secret key (receiver side) +static int larkg_derive_sk(lua_State *L) { + + BEGIN(); + char *failed_msg = NULL; + const octet *sk = NULL; + const octet *cred_oct = NULL; + + sk = o_arg(L, 1); SAFE_GOTO(sk, "Could not allocate LARKG current secret key"); + SAFE_GOTO(sk->len == LARKG_SK_BYTES, "Invalid LARKG secret key length"); + + cred_oct = o_arg(L, 2); SAFE_GOTO(cred_oct, "Could not allocate LARKG credential octet"); + SAFE_GOTO(cred_oct->len == LARKG_CRED_BYTES, "Invalid LARKG credential octet length"); + + larkg_cred_t cred; + SAFE_GOTO(_octet_to_cred(&cred, cred_oct), "Failed to deserialise LARKG credential"); + + octet *next_sk = o_new(L, LARKG_SK_BYTES); SAFE_GOTO(next_sk, "Could not allocate LARKG next secret key"); + + int ret; + // Retry on rejection (-1) abort on auth failure (-2) + do { + ret = PQCLEAN_KYBER512_CLEAN_larkg_derive_sk((uint8_t *)next_sk->val, (const uint8_t *)sk->val, &cred); + } while (ret == -1); + + SAFE_GOTO(ret == 0, "LARKG authentication failed"); + next_sk->len = LARKG_SK_BYTES; + +end: + o_free(L, cred_oct); + o_free(L, sk); + if(failed_msg) { + THROW(failed_msg); + } + + END(1); +} + +// --- Size checks --- + +static int larkg_sk_check(lua_State *L) { + BEGIN(); + const octet *sk = o_arg(L, 1); SAFE(sk, "Could not allocate LARKG secret key"); + lua_pushboolean(L, sk->len == LARKG_SK_BYTES); + o_free(L, sk); + END(1); +} + +static int larkg_pk_check(lua_State *L) { + BEGIN(); + const octet *pk = o_arg(L, 1); SAFE(pk, "Could not allocate LARKG public key"); + lua_pushboolean(L, pk->len == LARKG_PK_BYTES); + o_free(L, pk); + END(1); +} + +static int larkg_cred_check(lua_State *L) { + BEGIN(); + const octet *cred_oct = o_arg(L, 1); SAFE(cred_oct, "Could not allocate LARKG credential octet"); + lua_pushboolean(L, cred_oct->len == LARKG_CRED_BYTES); + o_free(L, cred_oct); + END(1); +} + + +int luaopen_larkg(lua_State *L) { + (void)L; + const struct luaL_Reg larkg_class[] = { + {"keygen", larkg_keygen}, + {"derive_pk", larkg_derive_pk}, + {"derive_sk", larkg_derive_sk}, + {"seccheck", larkg_sk_check}, + {"pubcheck", larkg_pk_check}, + {"credcheck", larkg_cred_check}, + {NULL, NULL} + }; + + const struct luaL_Reg larkg_methods[] = { + {NULL, NULL} + }; + + zen_add_class(L, "larkg", larkg_class, larkg_methods); + return 1; +} diff --git a/test/zencode/larkg.bats b/test/zencode/larkg.bats new file mode 100644 index 000000000..369142e88 --- /dev/null +++ b/test/zencode/larkg.bats @@ -0,0 +1,138 @@ +load ../bats_setup +load ../bats_zencode +SUBDOC=larkg + +@test "Generate LARKG keypair for Alice" { + cat < fail_derive_sk.zen +Scenario larkg +Given I am known as 'Alice' +and I have my 'keyring' +and I have a 'larkg credential' +When I derive next larkg secret key with credential 'larkg credential' +Then print the 'larkg secret key' +EOF + + run $ZENROOM_EXECUTABLE -z -k alice_keyring.json -a corrupted_cred.json fail_derive_sk.zen + assert_line --partial 'LARKG authentication failed' +} + +@test "Ratchet: two consecutive key derivations" { + cat < Date: Mon, 18 May 2026 11:12:36 +0200 Subject: [PATCH 3/5] fix: correct rejection sampling rate --- lib/pqclean/kyber512/kyber_larkg.c | 210 ++++++++++++----------------- 1 file changed, 84 insertions(+), 126 deletions(-) diff --git a/lib/pqclean/kyber512/kyber_larkg.c b/lib/pqclean/kyber512/kyber_larkg.c index 6a5d44db9..42c947ddc 100644 --- a/lib/pqclean/kyber512/kyber_larkg.c +++ b/lib/pqclean/kyber512/kyber_larkg.c @@ -12,104 +12,63 @@ // Imported from zenroom extern int randombytes(void *buf, size_t n); -// --- Helpers for rejection sampling in LARKG --- - -// Parameters for LARKG -#define LARKG_ETA KYBER_ETA1 -#define KYBER_MONT_R_INV 169 // Montgomery constant R^-1 mod q, where R = 2^16 and q = 3329. Used for converting to/from Montgomery form. - -// Function to convert from Montgomery form to standard representation and center around 0 -static inline int16_t demont_center(int16_t coeff) { - int32_t v = ((int32_t)coeff % KYBER_Q + KYBER_Q) % KYBER_Q; // Ensure coeff is in [0, q-1] - v = (v * KYBER_MONT_R_INV) % KYBER_Q; // Convert from Montgomery form to standard representation - if (v >= KYBER_Q - 2 * KYBER_ETA1) { - v -= KYBER_Q; // Center around 0 - } - return (int16_t)v; -} - -// Function to compute gcd, to reduce numerator and denominator in the rejection sampling step -static uint64_t gcd64(uint64_t a, uint64_t b) { - while (b != 0) { - uint64_t t = b; - b = a % b; - a = t; - } - return a; -} - -// Function to generate a 64-bit number -static uint64_t random64(void) { - uint8_t buf[8]; - - randombytes(buf, 8); - - return ((uint64_t)buf[0] << 56) | - ((uint64_t)buf[1] << 48) | - ((uint64_t)buf[2] << 40) | - ((uint64_t)buf[3] << 32) | - ((uint64_t)buf[4] << 24) | - ((uint64_t)buf[5] << 16) | - ((uint64_t)buf[6] << 8) | - ((uint64_t)buf[7]); +// --- Rejection sampling for LARKG --- + +// Portable count of trailing zero bits, used to sample G ~ Geom(1/2). +static int count_trailing_zeros(uint64_t x) { + if (x == 0) return 64; + int n = 0; + while ((x & 1) == 0) { n++; x >>= 1; } + return n; } -// LARKG rejection sampling in integer-only arithmetic -// Returns 0 on success, 1 on rejection -static int larkg_rej_sampling(const polyvec *S_flat, - const polyvec *S_pp_flat) { - static const int64_t LOG2_RSP[4] = {283261, 321617, 234953, 65536}; - static const int64_t LOG2_M_Q16 = 1182310LL; - - int64_t log2_num = LOG2_M_Q16; - +// Rejection sampling for LARKG (Lyubashevsky-style). +// Accepts candidate key S'' with probability chi(K) / (M * chi_max), +// where K = S'' - S and chi = CBD(eta=3) for Kyber512. +// Equivalent log-domain condition: centred_log_ratio + exp_sample > ln(M)*stddev +// Constants derived analytically from CBD(eta=3) over KYBER_K*KYBER_N = 512 coefficients: +// MEAN = 512 * E[log_cbd_rel] = 512 * (-29910) = -15240843 +// PER_ZERO = ln(2) * stddev(log_ratio) ~ 637553 +// LN_M = ln(3) * stddev(log_ratio) ~ 1010498 (M=3, acceptance rate ~0.33) +// Returns 0 (accept) or 1 (reject). +static int larkg_rej_sampling(const polyvec *K_raw) { + + // log P(|k|) - log P(0), scaled by 2^16, for CBD(eta=3) + static const int64_t log_cbd_rel[4] = { + 0, // |k|=0: ln(20/64) - ln(20/64) + -18854, // |k|=1: ln(15/20) * 65536 + -78904, // |k|=2: ln(6/20) * 65536 + -196328 // |k|=3: ln(1/20) * 65536 + }; + + static const int64_t MEAN = -15240843; + static const int64_t PER_ZERO = 637553; + static const int64_t LN_M = 1010498; + + int64_t log_ratio = 0; for (int i = 0; i < KYBER_K; i++) { for (int j = 0; j < KYBER_N; j++) { - int16_t s_ij = demont_center(S_flat->vec[i].coeffs[j]); - int16_t s_pp_ij = demont_center(S_pp_flat->vec[i].coeffs[j]); - - if (abs(s_ij) > LARKG_ETA || abs(s_pp_ij - s_ij) > 2 * LARKG_ETA) - return 1; - if (abs(s_ij) > 3 || abs(s_pp_ij - s_ij) > 3) - return 1; - - log2_num += LOG2_RSP[abs(s_pp_ij - s_ij)] - LOG2_RSP[abs(s_ij)]; + int32_t k = (int32_t)K_raw->vec[i].coeffs[j]; + if (k < 0) k = -k; + if (k > 3) return 1; + log_ratio += log_cbd_rel[k]; } } - static int debug_count = 0; - debug_count++; - if (debug_count <= 10) { - printf(" [SAMPLE %d] log2_num=%.4f neg=%lld int_part=%lld\n", - debug_count, - (double)log2_num / 65536.0, - (long long)(-log2_num), - (long long)((-log2_num) >> 16)); - } - - // Reject if reject_prob >= 1 - if (log2_num >= 0) - return 1; - - // threshold = 2^(64 + log2_num/65536), with log2_num < 0 - int64_t neg = -log2_num; - int64_t int_part = neg >> 16; - int64_t frac_part = neg & 0xFFFF; - - if (int_part >= 64) - return 1; - - // base = 2^(64 - int_part) as uint64_t - uint64_t base = (int_part == 0) ? UINT64_MAX : (UINT64_MAX >> int_part); - - // Correct for fractional part using ln2 approximation: - // 2^(-frac/65536) ≈ 1 - frac/65536 * ln2 - // ln2 * 65536 = 45426 */ - uint64_t threshold = base - ((__uint128_t)base * frac_part * 45426ULL >> 32); - - uint64_t u64 = random64(); - return (u64 < threshold) ? 1 : 0; + // Sample exp_sample ~ Exp(1) * stddev via geometric bit counting + int64_t exp_sample = 0; + uint64_t bits; + randombytes(&bits, sizeof(bits)); + do { + int z = count_trailing_zeros(bits); + exp_sample += (int64_t)z * PER_ZERO; + if (bits != 0) break; + randombytes(&bits, sizeof(bits)); + } while (1); + + return ((log_ratio - MEAN) > LN_M - exp_sample) ? 0 : 1; } + // ------------------------------------------------- /************************************************* @@ -191,49 +150,48 @@ int PQCLEAN_KYBER512_CLEAN_larkg_derive_pk(uint8_t next_pk[KYBER_INDCPA_PUBLICKE * Returns: 0 on success, -1 on rejection, -2 on failed authentication **************************************************/ int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_INDCPA_SECRETKEYBYTES], - const uint8_t current_sk[KYBER_INDCPA_SECRETKEYBYTES], - const larkg_cred_t *cred_in) { - uint8_t k_seed[KYBER_SSBYTES]; - uint8_t mu_star[32]; - polyvec S_poly, K_poly, S_prime_prime; + const uint8_t current_sk[KYBER_INDCPA_SECRETKEYBYTES], + const larkg_cred_t *cred_in) { + uint8_t k_seed[KYBER_SSBYTES]; + uint8_t mu_star[32]; + polyvec S_poly, K_poly, K_raw, S_prime_prime; - // Ln 1 - PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(&S_poly, current_sk); + // Ln 1 + PQCLEAN_KYBER512_CLEAN_polyvec_frombytes(&S_poly, current_sk); - // Ln 2 - PQCLEAN_KYBER512_CLEAN_skem_decaps(k_seed, current_sk, cred_in->c, cred_in->B_prime); + // Ln 2 + PQCLEAN_KYBER512_CLEAN_skem_decaps(k_seed, current_sk, cred_in->c, cred_in->B_prime); - // Ln 3 - for (int i = 0; i < KYBER_K; i++) { - PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&K_poly.vec[i], k_seed, (uint8_t)i); - PQCLEAN_KYBER512_CLEAN_poly_ntt(&K_poly.vec[i]); - } + // Ln 3 + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&K_raw.vec[i], k_seed, (uint8_t)i); + K_poly.vec[i] = K_raw.vec[i]; + PQCLEAN_KYBER512_CLEAN_poly_ntt(&K_poly.vec[i]); + } - // Ln 4 - hash_h(mu_star, k_seed, KYBER_SSBYTES); + // Ln 4 + hash_h(mu_star, k_seed, KYBER_SSBYTES); - // Ln 5 - if (memcmp(mu_star, cred_in->mu, 32) != 0) { - return -2; // Authentication failed (mu_star != mu) - } - - // Ln 6 - for (int i = 0; i < KYBER_K; i++) { - PQCLEAN_KYBER512_CLEAN_poly_add(&S_prime_prime.vec[i], &S_poly.vec[i], &K_poly.vec[i]); - PQCLEAN_KYBER512_CLEAN_poly_reduce(&S_prime_prime.vec[i]); - } + // Ln 5 + if (memcmp(mu_star, cred_in->mu, 32) != 0) { + return -2; + } - // Ln 7 and 8 (rejection sampling) - polyvec S_flat = S_poly; - polyvec S_pp_flat = S_prime_prime; - PQCLEAN_KYBER512_CLEAN_polyvec_invntt_tomont(&S_flat); - PQCLEAN_KYBER512_CLEAN_polyvec_invntt_tomont(&S_pp_flat); + // Ln 6 + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_add(&S_prime_prime.vec[i], + &S_poly.vec[i], + &K_poly.vec[i]); + PQCLEAN_KYBER512_CLEAN_poly_reduce(&S_prime_prime.vec[i]); + } - int rej = larkg_rej_sampling(&S_flat, &S_pp_flat); - if (rej != 0) return -1; // Rejection + // Ln 7-8: rejection sampling on K in normal domain + if (larkg_rej_sampling(&K_raw) != 0) { + return -1; + } - // Ln 9 - PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(next_sk, &S_prime_prime); + // Ln 9 + PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(next_sk, &S_prime_prime); - return 0; + return 0; } From 0577504da78ea1a340a26f8d5c5b80ac3ac253a1 Mon Sep 17 00:00:00 2001 From: ms8421ms Date: Mon, 18 May 2026 11:19:47 +0200 Subject: [PATCH 4/5] chore: modified init.mk --- build/init.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/init.mk b/build/init.mk index 953209bd9..e9e7b201b 100644 --- a/build/init.mk +++ b/build/init.mk @@ -14,7 +14,7 @@ ZEN_SOURCES := src/zenroom.o src/zen_error.o src/lua_functions.o \ src/zen_ed.o src/zen_float.o src/zen_time.o src/api_hash.o \ src/api_sign.o src/randombytes.o src/zen_fuzzer.o src/cortex_m.o \ src/p256-m.o src/zen_p256.o src/zen_rsa.o src/zen_bbs.o \ - src/zen_longfellow.o + src/zen_longfellow.o src/zen_larkg.o ZEN_INCLUDES += -Isrc -Ilib/lua54/src -Ilib -I/usr/local/include \ -Ilib/milagro-crypto-c/build/include -Ilib/milagro-crypto-c/include \ From 628b9a86d6e2fe09238cf3baad9ac922bf403e24 Mon Sep 17 00:00:00 2001 From: ms8421ms Date: Tue, 26 May 2026 11:20:15 +0200 Subject: [PATCH 5/5] fix: correct rejection sampling --- lib/pqclean/kyber512/kyber_larkg.c | 91 ++++++++++++++++++++---------- lib/pqclean/kyber512/kyber_larkg.h | 4 +- lib/pqclean/kyber512/skem.c | 5 +- lib/pqclean/kyber512/skem.h | 7 ++- src/zen_larkg.c | 2 +- 5 files changed, 73 insertions(+), 36 deletions(-) diff --git a/lib/pqclean/kyber512/kyber_larkg.c b/lib/pqclean/kyber512/kyber_larkg.c index 42c947ddc..d0610e5b2 100644 --- a/lib/pqclean/kyber512/kyber_larkg.c +++ b/lib/pqclean/kyber512/kyber_larkg.c @@ -8,6 +8,7 @@ #include #include #include // REMOVE AFTER TESTING +#include // REMOVE AFTER TESTING // Imported from zenroom extern int randombytes(void *buf, size_t n); @@ -23,39 +24,62 @@ static int count_trailing_zeros(uint64_t x) { } // Rejection sampling for LARKG (Lyubashevsky-style). -// Accepts candidate key S'' with probability chi(K) / (M * chi_max), -// where K = S'' - S and chi = CBD(eta=3) for Kyber512. -// Equivalent log-domain condition: centred_log_ratio + exp_sample > ln(M)*stddev -// Constants derived analytically from CBD(eta=3) over KYBER_K*KYBER_N = 512 coefficients: -// MEAN = 512 * E[log_cbd_rel] = 512 * (-29910) = -15240843 -// PER_ZERO = ln(2) * stddev(log_ratio) ~ 637553 -// LN_M = ln(3) * stddev(log_ratio) ~ 1010498 (M=3, acceptance rate ~0.33) +// +// Implements lines 7-8 of DeriveSK (Figure 5 of the paper): +// u <- U[0,1] +// accept if u < chi_a(K) / (M * chi_a(S)) +// +// where chi_a = CBD(eta=3), K = S'' - S is the update vector, +// and S is the current secret key. Both K and S have coefficients +// in [-3, 3] (K from getnoise_eta1 before NTT, S from the appended seed). +// +// In log domain the condition becomes: +// log_chi_K - log_chi_S > ln(M) - exp_sample +// +// where: +// log_chi_K = sum_i log P(k_i) (absolute log-prob under CBD(eta=3)) +// log_chi_S = sum_i log P(s_i) +// log_ratio = log_chi_K - log_chi_S has mean 0 (K and S i.i.d.) +// and stddev = sqrt(2 * N * Var[log P(single coeff)]) +// exp_sample ~ Exp(1) * stddev, sampled via geometric bit counting +// +// log_cbd[|k|] = log P(k) * 2^16, for CBD(eta=3): +// P(0)=20/64, P(1)=P(-1)=15/64, P(2)=P(-2)=6/64, P(3)=P(-3)=1/64 +// +// Constants +// MEAN = 0 (exact by symmetry) +// PER_ZERO = ln(2) * stddev +// LN_M = ln(3) * stddev (M=3, acceptance rate ~1/3) +// // Returns 0 (accept) or 1 (reject). -static int larkg_rej_sampling(const polyvec *K_raw) { - - // log P(|k|) - log P(0), scaled by 2^16, for CBD(eta=3) - static const int64_t log_cbd_rel[4] = { - 0, // |k|=0: ln(20/64) - ln(20/64) - -18854, // |k|=1: ln(15/20) * 65536 - -78904, // |k|=2: ln(6/20) * 65536 - -196328 // |k|=3: ln(1/20) * 65536 +static int larkg_rej_sampling(const polyvec *S_raw, + const polyvec *K_raw) { + // log P(|k|) * 2^16 for CBD(eta=3), absolute log-probabilities + static const int64_t log_cbd[4] = { + -76228, // |k|=0: log(20/64) * 65536 + -95082, // |k|=1: log(15/64) * 65536 + -155132, // |k|=2: log(6/64) * 65536 + -272557 // |k|=3: log(1/64) * 65536 }; - static const int64_t MEAN = -15240843; - static const int64_t PER_ZERO = 637553; - static const int64_t LN_M = 1010498; + static const int64_t PER_ZERO = 901636; + static const int64_t LN_M = 1429059; + + int64_t log_chi_K = 0; + int64_t log_chi_S = 0; - int64_t log_ratio = 0; for (int i = 0; i < KYBER_K; i++) { for (int j = 0; j < KYBER_N; j++) { - int32_t k = (int32_t)K_raw->vec[i].coeffs[j]; - if (k < 0) k = -k; - if (k > 3) return 1; - log_ratio += log_cbd_rel[k]; + int32_t k = abs((int32_t)K_raw->vec[i].coeffs[j]); + int32_t s = abs((int32_t)S_raw->vec[i].coeffs[j]); + if (k > 3 || s > 3) return 1; + log_chi_K += log_cbd[k]; + log_chi_S += log_cbd[s]; } } - // Sample exp_sample ~ Exp(1) * stddev via geometric bit counting + // Sample exp_sample ~ Exp(1) * stddev via geometric bit counting. + // Each trailing zero bit of a random uint64_t contributes PER_ZERO. int64_t exp_sample = 0; uint64_t bits; randombytes(&bits, sizeof(bits)); @@ -66,7 +90,9 @@ static int larkg_rej_sampling(const polyvec *K_raw) { randombytes(&bits, sizeof(bits)); } while (1); - return ((log_ratio - MEAN) > LN_M - exp_sample) ? 0 : 1; + // Accept if log_chi_K - log_chi_S > LN_M - exp_sample. + // MEAN = 0 by symmetry, so no centring needed. + return ((log_chi_K - log_chi_S) > LN_M - exp_sample) ? 0 : 1; } // ------------------------------------------------- @@ -149,8 +175,8 @@ int PQCLEAN_KYBER512_CLEAN_larkg_derive_pk(uint8_t next_pk[KYBER_INDCPA_PUBLICKE * * Returns: 0 on success, -1 on rejection, -2 on failed authentication **************************************************/ -int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_INDCPA_SECRETKEYBYTES], - const uint8_t current_sk[KYBER_INDCPA_SECRETKEYBYTES], +int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_LARKG_SECRETKEYBYTES], + const uint8_t current_sk[KYBER_LARKG_SECRETKEYBYTES], const larkg_cred_t *cred_in) { uint8_t k_seed[KYBER_SSBYTES]; uint8_t mu_star[32]; @@ -185,13 +211,20 @@ int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_INDCPA_SECRETKE PQCLEAN_KYBER512_CLEAN_poly_reduce(&S_prime_prime.vec[i]); } - // Ln 7-8: rejection sampling on K in normal domain - if (larkg_rej_sampling(&K_raw) != 0) { + // Ln 7-8: rejection sampling + // Recover S in normal domain from appended seed in the secret key, and use it for rejection sampling + const uint8_t *s_seed = current_sk + KYBER_POLYVECBYTES; + polyvec S_raw; + for (int i = 0; i < KYBER_K; i++) { + PQCLEAN_KYBER512_CLEAN_poly_getnoise_eta1(&S_raw.vec[i], s_seed, (uint8_t)i); + } + if (larkg_rej_sampling(&S_raw, &K_raw) != 0) { return -1; } // Ln 9 PQCLEAN_KYBER512_CLEAN_polyvec_tobytes(next_sk, &S_prime_prime); + memcpy(next_sk + KYBER_POLYVECBYTES, k_seed, KYBER_SYMBYTES); // Append seed to the secret key for the next round of rejection sampling return 0; } diff --git a/lib/pqclean/kyber512/kyber_larkg.h b/lib/pqclean/kyber512/kyber_larkg.h index aa0aac4c1..42f717ed0 100644 --- a/lib/pqclean/kyber512/kyber_larkg.h +++ b/lib/pqclean/kyber512/kyber_larkg.h @@ -16,8 +16,8 @@ int PQCLEAN_KYBER512_CLEAN_larkg_derive_pk(uint8_t next_pk[KYBER_INDCPA_PUBLICKE const uint8_t current_pk[KYBER_INDCPA_PUBLICKEYBYTES], const skem_context *ctx); -int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_INDCPA_SECRETKEYBYTES], - const uint8_t current_sk[KYBER_INDCPA_SECRETKEYBYTES], +int PQCLEAN_KYBER512_CLEAN_larkg_derive_sk(uint8_t next_sk[KYBER_LARKG_SECRETKEYBYTES], + const uint8_t current_sk[KYBER_LARKG_SECRETKEYBYTES], const larkg_cred_t *cred_in); #endif diff --git a/lib/pqclean/kyber512/skem.c b/lib/pqclean/kyber512/skem.c index e744fdfac..b6c3f8eb5 100644 --- a/lib/pqclean/kyber512/skem.c +++ b/lib/pqclean/kyber512/skem.c @@ -105,7 +105,7 @@ void PQCLEAN_KYBER512_CLEAN_skem_init(skem_context *ctx, * const skem_context *ctx: pointer to the global context **************************************************/ void PQCLEAN_KYBER512_CLEAN_skem_keygen(uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES], - uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + uint8_t sk[KYBER_LARKG_SECRETKEYBYTES], const skem_context *ctx) { uint8_t buf[2 * KYBER_SYMBYTES]; uint8_t nonce = 0; @@ -137,6 +137,7 @@ void PQCLEAN_KYBER512_CLEAN_skem_keygen(uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES], // Encode pk and sk to byte arrays skem_pack_sk(sk, &skpv); + memcpy(sk + KYBER_POLYVECBYTES, buf, KYBER_SYMBYTES); // Append s_seed to the secret key for rejection sampling skem_pack_pk(pk, &pkpv, ctx->rho); } @@ -237,7 +238,7 @@ void PQCLEAN_KYBER512_CLEAN_skem_encaps(uint8_t c_out[KYBER_POLYCOMPRESSEDBYTES] * const uint8_t *pkp: pointer to input public key for encapsulation **************************************************/ void PQCLEAN_KYBER512_CLEAN_skem_decaps(uint8_t m[KYBER_INDCPA_MSGBYTES], - const uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t sk[KYBER_LARKG_SECRETKEYBYTES], const uint8_t c_in[KYBER_POLYCOMPRESSEDBYTES], const uint8_t pkp[KYBER_POLYVECBYTES]) { polyvec u, skpv; diff --git a/lib/pqclean/kyber512/skem.h b/lib/pqclean/kyber512/skem.h index 9997bdf97..e54c280d7 100644 --- a/lib/pqclean/kyber512/skem.h +++ b/lib/pqclean/kyber512/skem.h @@ -21,10 +21,13 @@ typedef struct { polyvec at[KYBER_K]; } skem_context; +// SK size for the receiver (includes seed for rejection sampling) +#define KYBER_LARKG_SECRETKEYBYTES (KYBER_POLYVECBYTES + KYBER_SYMBYTES) + void PQCLEAN_KYBER512_CLEAN_skem_init(skem_context *ctx, const uint8_t rho[KYBER_SYMBYTES]); void PQCLEAN_KYBER512_CLEAN_skem_keygen(uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES], - uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + uint8_t sk[KYBER_LARKG_SECRETKEYBYTES], const skem_context *ctx); void PQCLEAN_KYBER512_CLEAN_skem_keygen_enc(uint8_t pkp[KYBER_POLYVECBYTES], @@ -37,7 +40,7 @@ void PQCLEAN_KYBER512_CLEAN_skem_encaps(uint8_t c_out[KYBER_POLYCOMPRESSEDBYTES] const uint8_t pk[KYBER_INDCPA_PUBLICKEYBYTES]); void PQCLEAN_KYBER512_CLEAN_skem_decaps(uint8_t m[KYBER_INDCPA_MSGBYTES], - const uint8_t sk[KYBER_INDCPA_SECRETKEYBYTES], + const uint8_t sk[KYBER_LARKG_SECRETKEYBYTES], const uint8_t c_in[KYBER_POLYCOMPRESSEDBYTES], const uint8_t pkp[KYBER_POLYVECBYTES]); diff --git a/src/zen_larkg.c b/src/zen_larkg.c index 38329d6ac..5611dc025 100644 --- a/src/zen_larkg.c +++ b/src/zen_larkg.c @@ -7,7 +7,7 @@ #include "../lib/pqclean/kyber512/skem.h" #include "../lib/pqclean/kyber512/params.h" -#define LARKG_SK_BYTES KYBER_INDCPA_SECRETKEYBYTES +#define LARKG_SK_BYTES KYBER_LARKG_SECRETKEYBYTES #define LARKG_PK_BYTES KYBER_INDCPA_PUBLICKEYBYTES #define LARKG_CRED_BYTES (sizeof(larkg_cred_t))