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 \ 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..d0610e5b2 --- /dev/null +++ b/lib/pqclean/kyber512/kyber_larkg.c @@ -0,0 +1,230 @@ +#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 +#include // REMOVE AFTER TESTING + +// Imported from zenroom +extern int randombytes(void *buf, size_t n); + +// --- 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; +} + +// Rejection sampling for LARKG (Lyubashevsky-style). +// +// 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 *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 PER_ZERO = 901636; + static const int64_t LN_M = 1429059; + + int64_t log_chi_K = 0; + int64_t log_chi_S = 0; + + for (int i = 0; i < KYBER_K; i++) { + for (int j = 0; j < KYBER_N; j++) { + 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. + // Each trailing zero bit of a random uint64_t contributes PER_ZERO. + 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); + + // 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; +} + +// ------------------------------------------------- + +/************************************************* +* 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_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]; + polyvec S_poly, K_poly, K_raw, 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_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 5 + if (memcmp(mu_star, cred_in->mu, 32) != 0) { + return -2; + } + + // 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-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 new file mode 100644 index 000000000..42f717ed0 --- /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_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 new file mode 100644 index 000000000..b6c3f8eb5 --- /dev/null +++ b/lib/pqclean/kyber512/skem.c @@ -0,0 +1,261 @@ +// 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_LARKG_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); + memcpy(sk + KYBER_POLYVECBYTES, buf, KYBER_SYMBYTES); // Append s_seed to the secret key for rejection sampling + 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_LARKG_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..e54c280d7 --- /dev/null +++ b/lib/pqclean/kyber512/skem.h @@ -0,0 +1,47 @@ +// 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; + +// 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_LARKG_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_LARKG_SECRETKEYBYTES], + const uint8_t c_in[KYBER_POLYCOMPRESSEDBYTES], + const uint8_t pkp[KYBER_POLYVECBYTES]); + +#endif 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..5611dc025 --- /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_LARKG_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 <