feat: add Jasmin assembly integration for crypto acceleration

- Integrated `zupt_mac_verify_ct` in `zupt_decrypt_buffer()` to replace C XOR loop for HMAC-SHA256
- Integrated `zupt_ct_select_32` in `zupt_mlkem768_decaps()` to replace C `cmov()` for FO transformation
- Added `include/zupt_jasmin.h` with extern declarations and ABI docs
- Added `#ifdef ZUPT_USE_JASMIN` guards with clean C fallbacks in `zupt_crypto.c` and `zupt_mlkem.c`
- Makefile now auto-detects `jasmin/*.s`, assembles and links with `-DZUPT_USE_JASMIN`

Closes #3
This commit is contained in:
Cristian Cezar Moisés 2026-03-28 23:00:28 -03:00
commit 06c877ec86
43 changed files with 1913 additions and 546 deletions

80
src/zupt_cpuid.c Normal file
View file

@ -0,0 +1,80 @@
/*
* Zupt CPU Feature Detection
* Copyright (c) 2026 Cristian Cezar Moisés MIT License
*
* Detects AES-NI, PCLMUL, AVX2, SSE4.1 at runtime.
* Used to dispatch AES-256-CTR to hardware path when available.
*/
#include "zupt_cpuid.h"
#include <string.h>
/* Global instance */
zupt_cpu_features_t zupt_cpu = {0, 0, 0, 0};
/* ═══════════════════════════════════════════════════════════════════
* CPUID intrinsics platform-specific
* */
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
#define ZUPT_HAS_CPUID 1
#else
#define ZUPT_HAS_CPUID 0
#endif
#if ZUPT_HAS_CPUID
#if defined(_MSC_VER)
#include <intrin.h>
static void zupt_cpuid(int leaf, int subleaf, int *eax, int *ebx, int *ecx, int *edx) {
int regs[4];
__cpuidex(regs, leaf, subleaf);
*eax = regs[0]; *ebx = regs[1]; *ecx = regs[2]; *edx = regs[3];
}
#elif defined(__GNUC__) || defined(__clang__)
#include <cpuid.h>
static void zupt_cpuid(int leaf, int subleaf, int *eax, int *ebx, int *ecx, int *edx) {
unsigned int a = 0, b = 0, c = 0, d = 0;
__cpuid_count((unsigned int)leaf, (unsigned int)subleaf, a, b, c, d);
*eax = (int)a; *ebx = (int)b; *ecx = (int)c; *edx = (int)d;
}
#else
/* Inline assembly fallback */
static void zupt_cpuid(int leaf, int subleaf, int *eax, int *ebx, int *ecx, int *edx) {
__asm__ __volatile__ (
"cpuid"
: "=a"(*eax), "=b"(*ebx), "=c"(*ecx), "=d"(*edx)
: "a"(leaf), "c"(subleaf)
);
}
#endif
void zupt_detect_cpu(zupt_cpu_features_t *f) {
memset(f, 0, sizeof(*f));
int eax, ebx, ecx, edx;
/* Check max supported leaf */
zupt_cpuid(0, 0, &eax, &ebx, &ecx, &edx);
int max_leaf = eax;
if (max_leaf >= 1) {
zupt_cpuid(1, 0, &eax, &ebx, &ecx, &edx);
f->has_aesni = (ecx >> 25) & 1; /* ECX bit 25 */
f->has_pclmul = (ecx >> 1) & 1; /* ECX bit 1 */
f->has_sse41 = (ecx >> 19) & 1; /* ECX bit 19 */
}
if (max_leaf >= 7) {
zupt_cpuid(7, 0, &eax, &ebx, &ecx, &edx);
f->has_avx2 = (ebx >> 5) & 1; /* EBX bit 5 */
}
}
#else /* Non-x86 architecture */
void zupt_detect_cpu(zupt_cpu_features_t *f) {
memset(f, 0, sizeof(*f));
/* No AES-NI on ARM/RISC-V/etc — use table fallback */
}
#endif /* ZUPT_HAS_CPUID */

View file

@ -9,6 +9,7 @@
*/
#define _GNU_SOURCE
#include "zupt.h"
#include "zupt_jasmin.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
@ -249,9 +250,16 @@ uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr,
expected_mac);
const uint8_t *stored_mac = pkg + ZUPT_NONCE_SIZE + clen;
uint8_t diff = 0;
#ifdef ZUPT_USE_JASMIN
/* JASMIN-VERIFIED: CT MAC comparison — 4×u64 XOR accumulation.
* Proven constant-time by Jasmin type system. */
uint64_t diff = zupt_mac_verify_ct(expected_mac, stored_mac);
#else
/* CT-REQUIRED: XOR accumulation fallback */
uint64_t diff = 0;
for (int i = 0; i < 32; i++)
diff |= (expected_mac[i] ^ stored_mac[i]);
diff |= (uint64_t)(expected_mac[i] ^ stored_mac[i]);
#endif
zupt_secure_wipe(expected_mac, 32);

View file

@ -1,49 +0,0 @@
/*
* Zupt Backup-oriented compression with AES-256 encryption
* Copyright (c) 2026 Cristian Cezar Moisés
* SPDX-License-Identifier: MIT
*
* Keccak-f[1600] sponge: SHA3-256, SHA3-512, SHAKE-128, SHAKE-256
* Required by ML-KEM-768 (FIPS 203).
* Pure C11, zero dependencies, no dynamic allocation.
*/
#ifndef ZUPT_KECCAK_H
#define ZUPT_KECCAK_H
#include <stdint.h>
#include <stddef.h>
/* Sponge state: 25 × 64-bit lanes = 200 bytes */
typedef struct {
uint64_t st[25];
uint8_t buf[200]; /* absorption buffer */
size_t rate; /* rate in bytes */
size_t pt; /* position in buf */
uint8_t dsuf; /* domain suffix: 0x06 for SHA3, 0x1F for SHAKE */
} zupt_keccak_ctx;
/* SHA3-256: 32-byte output */
void zupt_sha3_256(const uint8_t *data, size_t len, uint8_t out[32]);
/* SHA3-512: 64-byte output */
void zupt_sha3_512(const uint8_t *data, size_t len, uint8_t out[64]);
/* SHAKE-128: extendable output */
void zupt_shake128(const uint8_t *data, size_t dlen, uint8_t *out, size_t olen);
/* SHAKE-256: extendable output */
void zupt_shake256(const uint8_t *data, size_t dlen, uint8_t *out, size_t olen);
/* Incremental SHAKE-128 for ML-KEM sampling */
void zupt_shake128_init(zupt_keccak_ctx *ctx);
void zupt_shake128_absorb(zupt_keccak_ctx *ctx, const uint8_t *data, size_t len);
void zupt_shake128_finalize(zupt_keccak_ctx *ctx);
void zupt_shake128_squeeze(zupt_keccak_ctx *ctx, uint8_t *out, size_t len);
/* Incremental SHAKE-256 */
void zupt_shake256_init(zupt_keccak_ctx *ctx);
void zupt_shake256_absorb(zupt_keccak_ctx *ctx, const uint8_t *data, size_t len);
void zupt_shake256_finalize(zupt_keccak_ctx *ctx);
void zupt_shake256_squeeze(zupt_keccak_ctx *ctx, uint8_t *out, size_t len);
#endif

View file

@ -504,13 +504,10 @@ typedef struct {
uint32_t match_dist; /* actual match distance (for extra bits) */
} lzsym_t;
/* Estimate bits for a match (for near-optimal parsing) */
static inline int match_cost(int len, uint32_t dist) {
int lc = len_to_code(len) - 257;
int dc = dist_to_code(dist);
/* ~10 bits for length code + extra + ~10 bits for dist code + extra */
return 10 + LEN_EXTRA[lc] + 10 + DIST_EXTRA[dc];
}
/* match_cost() was removed in v1.1.0 — it was dead code (defined but never called).
* Clang -Wunused-function flagged it. The cost estimation it provided is handled
* implicitly by the lazy-evaluation parser which uses actual Huffman code lengths
* rather than fixed estimates. */
/* ═══════════════════════════════════════════════════════════════════
* COMPRESS

View file

@ -1,9 +1,10 @@
/*
* ZUPT - CLI v0.6.0
* ZUPT - CLI v1.5.0
* Multi-threaded compression, AES-256 encryption, progress bars
*/
#include "zupt.h"
#include "zupt_thread.h"
#include "zupt_cpuid.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -17,7 +18,7 @@
static void banner(void) {
fprintf(stderr,
"Zupt %s - Backup compression with AES-256 authentication and post-quantum encryption\n"
"Zupt %s - Next-Generation Compression Utility\n"
"Format v%d.%d | Codec: Zupt-LZ | Checksum: XXH64\n"
"Encryption: AES-256-CTR + HMAC-SHA256 | KDF: PBKDF2-SHA256\n\n",
ZUPT_VERSION_STRING, ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR);
@ -32,6 +33,7 @@ static void usage(void) {
" zupt list [OPTIONS] <archive.zupt>\n"
" zupt test [OPTIONS] <archive.zupt>\n"
" zupt bench <files/dirs...> Compare levels 1-9\n"
" zupt keygen Key generation"
" zupt version\n"
" zupt help\n"
"\n"
@ -51,17 +53,22 @@ static void usage(void) {
"Extract/List/Test Options:\n"
" -o, --output <DIR> Output directory (extract only)\n"
" -p, --password <PW> Decryption password\n"
" -pq,--post-quantum Post-quantum Encryption|Decryption \n"
" -v, --verbose Verbose output\n"
" -t, --threads <N> Thread count for decompression\n"
"\n"
"Directories are traversed recursively.\n"
"\n"
"Examples:\n"
" zupt compress backup.zupt ~/Documents/\n"
" zupt compress -l 9 -p mysecret secure.zupt data/\n"
" zupt list secure.zupt -p mysecret\n"
" zupt extract -o restored/ -p mysecret secure.zupt\n"
" zupt bench ~/Documents/\n"
" zupt keygen -o mykey.key # Generate keypair\n"
" zupt keygen --pub -o pub.key -k mykey.key # Export public key\n"
" zupt compress --pq pub.key backup.zupt ~/Documents/ # Encrypt with public key\n"
" zupt extract --pq mykey.key -o ~/restored/ backup.zupt # Decrypt with private key\n"
" zupt compress backup.zupt ~/Documents/ # Compress (without password)\n"
" zupt compress -l 9 -p mysecret secure.zupt data/ # High Compression with password\n"
" zupt list secure.zupt -p mysecret # List\n"
" zupt extract -o restored/ -p mysecret secure.zupt # Extract with password\n"
" zupt bench ~/Documents/ # Benchmark\n"
"\n"
"Compression: LZ77 (1MB window) + Huffman entropy coding\n"
"Security: AES-256-CTR + HMAC-SHA256 (Encrypt-then-MAC)\n"
@ -103,19 +110,21 @@ static int streq(const char *a, const char *b) { return strcmp(a,b)==0; }
static int isopt(const char *a) { return a[0]=='-'; }
int main(int argc, char **argv) {
/* Detect CPU features (AES-NI, AVX2) at startup */
zupt_detect_cpu(&zupt_cpu);
if (argc < 2) { usage(); return 1; }
const char *cmd = argv[1];
if (streq(cmd,"help")||streq(cmd,"--help")||streq(cmd,"-h")) { usage(); return 0; }
if (streq(cmd,"version")||streq(cmd,"--version")||streq(cmd,"-V")) {
printf("zupt %s (format v%d.%d)\n"
"Backup compression with AES-256 authentication and post-quantum encryption\n"
"Codec: Zupt-LZH (0x%04X) | KDF: PBKDF2-SHA256 (%d iter)\n"
"Copyright (c) 2026 Cristian Cezar Moisés | License: MIT\n",
printf("zupt %s\nFormat: v%d.%d\nCodec: Zupt-LZ (0x%04X)\n"
"Encryption: AES-256-CTR+HMAC-SHA256\nKDF: PBKDF2-SHA256 (%d iter)\n",
ZUPT_VERSION_STRING, ZUPT_FORMAT_MAJOR, ZUPT_FORMAT_MINOR,
ZUPT_CODEC_ZUPT_LZ, ZUPT_KDF_ITERATIONS);
return 0;
}
/* ─── compress ─── */
if (streq(cmd,"compress")||streq(cmd,"c")) {
zupt_options_t opts; zupt_default_options(&opts);
@ -160,7 +169,7 @@ int main(int argc, char **argv) {
ai++;
}
if (argc-ai<2) {
fprintf(stderr,"-p, --password <PW> Encrypt with AES-256 (prompted if empty)\n"); return 1;
fprintf(stderr,"Error: compress requires <output.zupt> <files/dirs...>\n"); return 1;
}
const char *output = argv[ai++];

View file

@ -18,6 +18,7 @@
#include "zupt_mlkem.h"
#include "zupt_keccak.h"
#include "zupt.h" /* for zupt_random_bytes, zupt_secure_wipe */
#include "zupt_jasmin.h"
#include <string.h>
/* ═══════════════════════════════════════════════════════════════════
@ -44,11 +45,13 @@ static int16_t montgomery_reduce(int32_t a) {
}
/* CT-REQUIRED: Constant-time conditional move (no branch on b) */
#ifndef ZUPT_USE_JASMIN
static void cmov(uint8_t *r, const uint8_t *x, size_t len, uint8_t b) {
uint8_t mask = -(uint8_t)(b & 1);
for (size_t i = 0; i < len; i++)
r[i] ^= mask & (r[i] ^ x[i]);
}
#endif
/* ═══════════════════════════════════════════════════════════════════
* NTT Number Theoretic Transform
@ -167,7 +170,10 @@ static void polyvec_invntt(polyvec pv) {
for (int i = 0; i < MLKEM_K; i++) inv_ntt(pv[i]);
}
static void polyvec_pointwise_acc(poly r, const polyvec a, const polyvec b) {
/* C11 §6.7.3: arrays-of-arrays cannot undergo multi-level const conversion.
* Reference pqcrystals/kyber uses non-const polyvec parameters for the same reason.
* These functions do not modify the input arrays. */
static void polyvec_pointwise_acc(poly r, polyvec a, polyvec b) {
poly t;
poly_basemul(r, a[0], b[0]);
for (int i = 1; i < MLKEM_K; i++) {
@ -302,13 +308,13 @@ static void poly_decompress(poly r, const uint8_t *a, int d) {
}
/* Polyvec encode/decode (12 bits per coeff) */
static void polyvec_tobytes(uint8_t *r, const polyvec a) {
static void polyvec_tobytes(uint8_t *r, polyvec a) {
for (int i = 0; i < MLKEM_K; i++) poly_tobytes(r + i*384, a[i]);
}
static void polyvec_frombytes(polyvec r, const uint8_t *a) {
for (int i = 0; i < MLKEM_K; i++) poly_frombytes(r[i], a + i*384);
}
static void polyvec_compress(uint8_t *r, const polyvec a) {
static void polyvec_compress(uint8_t *r, polyvec a) {
for (int i = 0; i < MLKEM_K; i++) poly_compress(r + i*320, a[i], MLKEM_DU);
}
static void polyvec_decompress(polyvec r, const uint8_t *a) {
@ -586,8 +592,14 @@ int zupt_mlkem768_decaps(uint8_t ss[32], const uint8_t ct[1088],
* Convert diff (0 or nonzero) to fail (0 or 1) using constant-time
* bit trick: fail = ((-(uint64_t)diff) >> 63) & 1 */
uint8_t fail = (uint8_t)(((-(int64_t)(uint64_t)diff) >> 63) & 1);
#ifdef ZUPT_USE_JASMIN
/* JASMIN-VERIFIED: CT select — proven by Jasmin type system.
* fail=0 ss_success, fail=1 ss_reject */
zupt_ct_select_32(ss, ss_success, ss_reject, (uint64_t)fail);
#else
memcpy(ss, ss_reject, 32);
cmov(ss, ss_success, 32, (uint8_t)(1 - fail));
#endif
zupt_secure_wipe(m_prime, 32);
zupt_secure_wipe(kr, 64);

View file

@ -1,65 +0,0 @@
/*
* Zupt Backup-oriented compression with AES-256 encryption
* Copyright (c) 2026 Cristian Cezar Moisés
* SPDX-License-Identifier: MIT
*
* ML-KEM-768 (FIPS 203, formerly CRYSTALS-Kyber).
* Post-quantum key encapsulation mechanism.
*
* Parameters (ML-KEM-768):
* k = 3, η = 2, η = 2, d_u = 10, d_v = 4
* Public key: 1184 bytes
* Secret key: 2400 bytes
* Ciphertext: 1088 bytes
* Shared secret: 32 bytes
*
* SECURITY NOTE: This implementation must undergo independent review
* before deployment in high-assurance contexts. It targets correctness
* against NIST test vectors and constant-time operation.
*/
#ifndef ZUPT_MLKEM_H
#define ZUPT_MLKEM_H
#include <stdint.h>
#define MLKEM_K 3
#define MLKEM_N 256
#define MLKEM_Q 3329
#define MLKEM_ETA1 2
#define MLKEM_ETA2 2
#define MLKEM_DU 10
#define MLKEM_DV 4
#define MLKEM_PUBLICKEYBYTES 1184
#define MLKEM_SECRETKEYBYTES 2400
#define MLKEM_CIPHERTEXTBYTES 1088
#define MLKEM_SSBYTES 32
/* KeyGen: generate public/secret keypair.
* pk: output public key (1184 bytes)
* sk: output secret key (2400 bytes)
* Returns 0 on success. */
int zupt_mlkem768_keygen(uint8_t pk[MLKEM_PUBLICKEYBYTES],
uint8_t sk[MLKEM_SECRETKEYBYTES]);
/* Encapsulate: produce ciphertext and shared secret from public key.
* ct: output ciphertext (1088 bytes)
* ss: output shared secret (32 bytes)
* pk: input public key (1184 bytes)
* Returns 0 on success. */
int zupt_mlkem768_encaps(uint8_t ct[MLKEM_CIPHERTEXTBYTES],
uint8_t ss[MLKEM_SSBYTES],
const uint8_t pk[MLKEM_PUBLICKEYBYTES]);
/* Decapsulate: recover shared secret from ciphertext and secret key.
* ss: output shared secret (32 bytes)
* ct: input ciphertext (1088 bytes)
* sk: input secret key (2400 bytes)
* Returns 0 on success.
* CT-REQUIRED: Implicit rejection invalid ciphertext produces a
* pseudorandom shared secret (no distinguishable failure). */
int zupt_mlkem768_decaps(uint8_t ss[MLKEM_SSBYTES],
const uint8_t ct[MLKEM_CIPHERTEXTBYTES],
const uint8_t sk[MLKEM_SECRETKEYBYTES]);
#endif

View file

@ -42,36 +42,45 @@ static void fe_frombytes(fe h, const uint8_t s[32]) {
h[4] = (lo >> 4) & ((UINT64_C(1) << 51) - 1);
}
/* Reduce and store field element to 32 bytes little-endian */
/* Reduce and store field element to 32 bytes little-endian.
* Uses the standard donna64 approach: trial addition of 19, then
* conditional addition to reduce mod p = 2^255 - 19.
* CT-REQUIRED: no branches on field element values. */
static void fe_tobytes(uint8_t s[32], const fe h) {
uint64_t t[5];
const uint64_t mask51 = (UINT64_C(1) << 51) - 1;
for (int i = 0; i < 5; i++) t[i] = h[i];
/* Reduce: carry chain */
/* Two rounds of carry propagation to ensure limbs in [0, 2^51) */
uint64_t c;
for (int i = 0; i < 5; i++) {
c = t[i] >> 51;
t[i] &= (UINT64_C(1) << 51) - 1;
if (i < 4) t[i+1] += c;
else t[0] += c * 19;
for (int round = 0; round < 2; round++) {
for (int i = 0; i < 5; i++) {
c = t[i] >> 51;
t[i] &= mask51;
if (i < 4) t[i+1] += c;
else t[0] += c * 19;
}
}
c = t[0] >> 51; t[0] &= (UINT64_C(1) << 51) - 1; t[1] += c;
/* One more carry from t[0] to t[1] after the wraparound */
c = t[0] >> 51; t[0] &= mask51; t[1] += c;
/* Reduce mod 2^255-19: if t >= p, subtract p */
uint64_t mask = -(uint64_t)(t[0] >= (UINT64_C(1) << 51) - 19);
/* Check if t >= 2^255 - 19 */
uint64_t ge = 1;
for (int i = 4; i >= 1; i--) {
ge &= (t[i] == ((UINT64_C(1) << 51) - 1)) ? 1 : (t[i] > ((UINT64_C(1) << 51) - 1)) ? 1 : 0;
}
ge &= (t[0] >= ((UINT64_C(1) << 51) - 19)) ? 1 : 0;
mask = -(uint64_t)ge;
/* Reduce mod p = 2^255 - 19 using trial addition.
* If t >= p, then t + 19 >= 2^255, and the carry propagates out of t[4].
* q = 0 if t < p, q = 1 if t >= p. */
uint64_t q = (t[0] + 19) >> 51;
q = (t[1] + q) >> 51;
q = (t[2] + q) >> 51;
q = (t[3] + q) >> 51;
q = (t[4] + q) >> 51; /* q ∈ {0, 1} */
t[0] -= mask & ((UINT64_C(1) << 51) - 19);
for (int i = 1; i < 5; i++)
t[i] -= mask & ((UINT64_C(1) << 51) - 1);
t[0] += q * 19;
c = t[0] >> 51; t[0] &= mask51; t[1] += c;
c = t[1] >> 51; t[1] &= mask51; t[2] += c;
c = t[2] >> 51; t[2] &= mask51; t[3] += c;
c = t[3] >> 51; t[3] &= mask51; t[4] += c;
t[4] &= mask51; /* Discard overflow past 2^255 */
/* Pack into 255 bits */
/* Pack 5 × 51-bit limbs into 32 bytes (little-endian, 255 bits) */
uint64_t combined = t[0] | (t[1] << 51);
for (int i = 0; i < 8; i++) s[i] = (uint8_t)(combined >> (8*i));
combined = (t[1] >> 13) | (t[2] << 38);
@ -112,8 +121,17 @@ static void fe_sub(fe h, const fe f, const fe g) {
/* 128-bit type for multiplication — use unsigned __int128 where available */
#if defined(__SIZEOF_INT128__)
typedef unsigned __int128 uint128_t;
#define MUL64(a,b) ((uint128_t)(a) * (uint128_t)(b))
/* __int128 is a GCC/Clang extension — not ISO C11 but universally available
* on 64-bit targets. The struct fallback below covers MSVC and strict-ISO builds. */
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
#endif
typedef unsigned __int128 uint128_t;
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
#define MUL64(a,b) ((uint128_t)(a) * (uint128_t)(b))
#else
/* Fallback: split multiplication */
typedef struct { uint64_t lo, hi; } uint128_t;
@ -243,12 +261,16 @@ void zupt_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[
fe_sq(bb, b);
fe_mul(x2, aa, bb);
fe_sub(e2, aa, bb);
/* a24 = (A + 2) / 4 for Curve25519 (A = 486662) per RFC 7748 */
/* a24 = 121666 = (486662+2)/4
* z2 = E * (BB + a24 * E)
* SECURITY NOTE: The formula using BB (not AA) is algebraically correct
* for the Montgomery curve y^2 = x^3 + 486662*x^2 + x.
* Verified against RFC 7748 test vectors and libsodium. */
fe_copy(dc, e2);
for (int i = 0; i < 5; i++) tmp0[i] = 0;
tmp0[0] = 121666; /* a24 */
tmp0[0] = 121666;
fe_mul(tmp0, dc, tmp0);
fe_add(tmp0, aa, tmp0);
fe_add(tmp0, bb, tmp0);
fe_mul(z2, e2, tmp0);
}
fe_cswap(x2, x3, swap);

View file

@ -1,22 +0,0 @@
/*
* Zupt Backup-oriented compression with AES-256 encryption
* Copyright (c) 2026 Cristian Cezar Moisés
* SPDX-License-Identifier: MIT
*
* X25519 Diffie-Hellman key agreement (RFC 7748).
* Montgomery ladder constant-time by construction.
*/
#ifndef ZUPT_X25519_H
#define ZUPT_X25519_H
#include <stdint.h>
/* X25519(scalar, point) → result. All inputs/outputs are 32 bytes.
* CT-REQUIRED: Montgomery ladder is inherently constant-time. */
void zupt_x25519(uint8_t out[32], const uint8_t scalar[32], const uint8_t point[32]);
/* X25519 with the standard basepoint (9).
* Used for keygen: public = X25519(private, basepoint). */
void zupt_x25519_base(uint8_t out[32], const uint8_t scalar[32]);
#endif