zupt/src/zupt_crypto.c

701 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Zupt — Backup-oriented compression with AES-256 encryption
* Copyright (c) 2026 Cristian Cezar Moisés
* SPDX-License-Identifier: MIT
*
* Cryptographic operations:
* - HMAC-SHA256, PBKDF2, AES-256-CTR, Encrypt-then-MAC (v0.2+)
* - Hybrid PQ KEM: ML-KEM-768 + X25519 (v0.7.0)
*
* FRAMA-C: ACSL-annotated (v2.0.0)
*/
#define _GNU_SOURCE
#include "zupt.h"
#include "zupt_acsl.h"
#include "zupt_jasmin.h"
#include "zupt_cpuid.h" /* JASMIN-VERIFIED: AES-NI dispatch */
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* ═══════════════════════════════════════════════════════════════════
* RANDOM BYTES (OS-native CSPRNG — NO FALLBACK)
*
* If the OS CSPRNG is unavailable, this aborts. Using rand() would
* make salt/nonce predictable and destroy all security guarantees.
* ═══════════════════════════════════════════════════════════════════ */
void zupt_random_bytes(uint8_t *buf, size_t len) {
#ifdef _WIN32
/* Windows: RtlGenRandom (SystemFunction036) */
HMODULE lib = LoadLibraryA("advapi32.dll");
if (lib) {
typedef BOOLEAN(WINAPI *RtlGenRandomFunc)(PVOID, ULONG);
RtlGenRandomFunc fn = (RtlGenRandomFunc)(void(*)(void))GetProcAddress(lib, "SystemFunction036");
if (fn && fn(buf, (ULONG)len)) { FreeLibrary(lib); return; }
FreeLibrary(lib);
}
fprintf(stderr, "FATAL: Windows CSPRNG (RtlGenRandom) unavailable.\n");
exit(1);
#else
/* Linux/macOS/BSD: try getrandom(2) first, then /dev/urandom */
#if defined(__linux__) && defined(SYS_getrandom)
#include <sys/syscall.h>
ssize_t r = syscall(SYS_getrandom, buf, len, 0);
if (r == (ssize_t)len) return;
#endif
FILE *f = fopen("/dev/urandom", "rb");
if (f) {
size_t r = fread(buf, 1, len, f);
fclose(f);
if (r == len) return;
}
fprintf(stderr, "FATAL: /dev/urandom unavailable. Cannot generate secure random bytes.\n");
exit(1);
#endif
}
/* ═══════════════════════════════════════════════════════════════════
* HMAC-SHA256 (RFC 2104)
* ═══════════════════════════════════════════════════════════════════ */
/* FRAMA-C: HMAC-SHA256 (RFC 2104) */
/*@ requires klen <= 256;
@ requires \valid_read(key + (0..klen-1));
@ requires \valid_read(data + (0..dlen-1));
@ requires \valid(mac + (0..31));
@ requires \separated(key + (0..klen-1), mac + (0..31));
@ requires \separated(data + (0..dlen-1), mac + (0..31));
@ assigns mac[0..31];
@ ensures \initialized(mac + (0..31));
*/
void zupt_hmac_sha256(const uint8_t *key, size_t klen,
const uint8_t *data, size_t dlen,
uint8_t mac[32]) {
uint8_t k_pad[64];
uint8_t k_hash[32];
/* If key > 64 bytes, hash it first */
if (klen > 64) {
zupt_sha256(key, klen, k_hash);
key = k_hash; klen = 32;
}
/* ipad = key XOR 0x36 */
memset(k_pad, 0x36, 64);
for (size_t i = 0; i < klen; i++) k_pad[i] ^= key[i];
/* inner = SHA256(ipad || data) */
zupt_sha256_ctx ctx;
zupt_sha256_init(&ctx);
zupt_sha256_update(&ctx, k_pad, 64);
zupt_sha256_update(&ctx, data, dlen);
uint8_t inner[32];
zupt_sha256_final(&ctx, inner);
/* opad = key XOR 0x5c */
memset(k_pad, 0x5c, 64);
for (size_t i = 0; i < klen; i++) k_pad[i] ^= key[i];
/* mac = SHA256(opad || inner) */
zupt_sha256_init(&ctx);
zupt_sha256_update(&ctx, k_pad, 64);
zupt_sha256_update(&ctx, inner, 32);
zupt_sha256_final(&ctx, mac);
/* Wipe sensitive data */
zupt_secure_wipe(k_pad, 64);
zupt_secure_wipe(inner, 32);
zupt_secure_wipe(k_hash, 32);
}
/* ═══════════════════════════════════════════════════════════════════
* PBKDF2-HMAC-SHA256 (RFC 8018)
* ═══════════════════════════════════════════════════════════════════ */
/* FRAMA-C: PBKDF2-HMAC-SHA256 (RFC 8018) */
/*@ requires pwlen <= 256;
@ requires slen <= 252;
@ requires olen > 0 && olen <= 64;
@ requires iterations >= 1;
@ requires \valid_read(pw + (0..pwlen-1));
@ requires \valid_read(salt + (0..slen-1));
@ requires \valid(output + (0..olen-1));
@ assigns output[0..olen-1];
@ ensures \initialized(output + (0..olen-1));
*/
void zupt_pbkdf2_sha256(const uint8_t *pw, size_t pwlen,
const uint8_t *salt, size_t slen,
uint32_t iterations,
uint8_t *output, size_t olen) {
/* Clamp salt length to fit in the stack buffer.
* ZUPT always passes ZUPT_SALT_SIZE (32) so this is a safety net. */
size_t effective_slen = slen;
if (effective_slen > 252) effective_slen = 252;
uint32_t block_num = 1;
size_t pos = 0;
while (pos < olen) {
/* U_1 = HMAC(pw, salt || INT_32_BE(block_num)) */
uint8_t salt_block[256];
memcpy(salt_block, salt, effective_slen);
salt_block[effective_slen+0] = (uint8_t)(block_num >> 24);
salt_block[effective_slen+1] = (uint8_t)(block_num >> 16);
salt_block[effective_slen+2] = (uint8_t)(block_num >> 8);
salt_block[effective_slen+3] = (uint8_t)(block_num);
uint8_t u[32], t[32];
zupt_hmac_sha256(pw, pwlen, salt_block, effective_slen + 4, u);
memcpy(t, u, 32);
/* U_2 .. U_c: XOR chain */
for (uint32_t i = 1; i < iterations; i++) {
zupt_hmac_sha256(pw, pwlen, u, 32, u);
for (int j = 0; j < 32; j++) t[j] ^= u[j];
}
/* Copy to output */
size_t chunk = olen - pos;
if (chunk > 32) chunk = 32;
memcpy(output + pos, t, chunk);
pos += chunk;
block_num++;
/* Wipe per-block intermediates */
zupt_secure_wipe(u, 32);
zupt_secure_wipe(t, 32);
zupt_secure_wipe(salt_block, sizeof(salt_block));
}
}
/* ═══════════════════════════════════════════════════════════════════
* AES-256-CTR MODE
* ═══════════════════════════════════════════════════════════════════ */
/* FRAMA-C: AES-256-CTR stream cipher */
/*@ requires \valid_read(key + (0..31));
@ requires \valid_read(nonce + (0..15));
@ requires \valid_read(in + (0..len-1));
@ requires \valid(out + (0..len-1));
@ requires \separated(in + (0..len-1), out + (0..len-1));
@ assigns out[0..len-1];
@ ensures \initialized(out + (0..len-1));
*/
void zupt_aes256_ctr(const uint8_t key[32], const uint8_t nonce[16],
const uint8_t *in, uint8_t *out, size_t len) {
uint8_t counter[16], keystream[16];
memcpy(counter, nonce, 16);
#ifdef ZUPT_USE_JASMIN
/* JASMIN-VERIFIED: AES-NI path — constant-time, no T-table leakage.
* The Jasmin-generated assembly uses VEX-encoded instructions (vaesenc,
* vmovdqu, vpxor, etc.) which require BOTH AES-NI AND AVX support.
* Checking only has_aesni would SIGILL on CPUs with AES-NI but no AVX,
* or where the OS hasn't enabled XSAVE for YMM state. */
if (zupt_cpu.has_aesni && zupt_cpu.has_avx) {
size_t full_blocks = len / 16;
size_t tail_bytes = len % 16;
if (full_blocks >= 4) {
/* 4-block pipeline: processes 4 blocks per iteration */
size_t pipe_blocks = (full_blocks / 4) * 4;
zupt_aes256_ctr4(out, in, key, counter, pipe_blocks);
size_t pipe_bytes = pipe_blocks * 16;
in += pipe_bytes;
out += pipe_bytes;
full_blocks -= pipe_blocks;
}
/* Remaining 0-3 full blocks: single-block path */
size_t pos = 0;
for (size_t b = 0; b < full_blocks; b++) {
zupt_aes256_blk(out + pos, in + pos, key, counter);
pos += 16;
/* Increment counter (big-endian, last 8 bytes) */
for (int i = 15; i >= 8; i--) {
if (++counter[i] != 0) break;
}
}
in += pos;
out += pos;
/* Tail: partial last block */
if (tail_bytes > 0) {
uint8_t tmp_in[16], tmp_out[16];
memset(tmp_in, 0, 16);
memcpy(tmp_in, in, tail_bytes);
zupt_aes256_blk(tmp_out, tmp_in, key, counter);
memcpy(out, tmp_out, tail_bytes);
zupt_secure_wipe(tmp_in, 16);
zupt_secure_wipe(tmp_out, 16);
}
zupt_secure_wipe(counter, 16);
zupt_secure_wipe(keystream, 16);
return;
}
#endif
/* C table-based fallback */
zupt_aes256_ctx ctx;
zupt_aes256_init(&ctx, key);
size_t pos = 0;
while (pos < len) {
zupt_aes256_encrypt_block(&ctx, counter, keystream);
size_t chunk = len - pos;
if (chunk > 16) chunk = 16;
for (size_t i = 0; i < chunk; i++)
out[pos + i] = in[pos + i] ^ keystream[i];
pos += chunk;
/* Increment counter (big-endian, last 8 bytes) */
for (int i = 15; i >= 8; i--) {
if (++counter[i] != 0) break;
}
}
zupt_secure_wipe(&ctx, sizeof(ctx));
zupt_secure_wipe(keystream, 16);
}
/* ═══════════════════════════════════════════════════════════════════
* KEY DERIVATION
* ═══════════════════════════════════════════════════════════════════ */
/* FRAMA-C: Key derivation from password + salt */
/*@ requires \valid(kr);
@ requires \valid_read(salt + (0..31));
@ requires \valid_read(nonce + (0..15));
@ requires strlen(pw) <= 255;
@ requires iterations >= 1;
@ assigns kr->enc_key[0..31], kr->mac_key[0..31], kr->salt[0..31],
@ kr->base_nonce[0..15], kr->iterations, kr->active;
@ ensures kr->active == 1;
*/
void zupt_derive_keys(zupt_keyring_t *kr, const char *pw,
const uint8_t salt[32], const uint8_t nonce[16],
uint32_t iterations) {
/* Init canaries if not already set */
kr->canary_head = ZUPT_CANARY;
kr->canary_tail = ZUPT_CANARY;
memcpy(kr->salt, salt, ZUPT_SALT_SIZE);
memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE);
kr->iterations = iterations;
kr->active = 1;
/* Derive 64 bytes: 32 enc_key + 32 mac_key */
uint8_t material[64];
zupt_pbkdf2_sha256((const uint8_t *)pw, strlen(pw),
salt, ZUPT_SALT_SIZE,
iterations, material, 64);
memcpy(kr->enc_key, material, 32);
memcpy(kr->mac_key, material + 32, 32);
zupt_secure_wipe(material, 64);
/* Lock key material in RAM — prevent swap to disk */
zupt_mlock_keys(kr->enc_key, ZUPT_AES_KEY_SIZE);
zupt_mlock_keys(kr->mac_key, ZUPT_HMAC_SIZE);
}
/* ═══════════════════════════════════════════════════════════════════
* ENCRYPT-THEN-MAC
*
* Output format: [16-byte per-block nonce] [ciphertext] [32-byte HMAC]
* The HMAC covers the nonce and ciphertext.
* Per-block nonce = base_nonce XOR (block_seq as LE 8 bytes in low half)
* ═══════════════════════════════════════════════════════════════════ */
/* FRAMA-C: Encrypt-then-MAC: produces [nonce][ciphertext][HMAC] */
/*@ requires \valid_read(&kr->enc_key[0..31]);
@ requires \valid_read(&kr->mac_key[0..31]);
@ requires \valid_read(&kr->base_nonce[0..15]);
@ requires kr->active == 1;
@ requires \valid_read(plain + (0..plen-1));
@ requires \valid(olen);
@ assigns *olen;
@ ensures *olen == 16 + plen + 32;
*/
uint8_t *zupt_encrypt_buffer(const zupt_keyring_t *kr,
const uint8_t *plain, size_t plen,
uint64_t block_seq, size_t *olen) {
*olen = ZUPT_NONCE_SIZE + plen + ZUPT_HMAC_SIZE;
uint8_t *pkg = (uint8_t *)malloc(*olen);
if (!pkg) return NULL;
/* Derive per-block nonce */
uint8_t nonce[16];
memcpy(nonce, kr->base_nonce, 16);
for (int i = 0; i < 8; i++)
nonce[i] ^= (uint8_t)(block_seq >> (i * 8));
/* Store nonce */
memcpy(pkg, nonce, 16);
/* Encrypt */
zupt_aes256_ctr(kr->enc_key, nonce, plain, pkg + 16, plen);
/* MAC over nonce + ciphertext */
zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE,
pkg, 16 + plen,
pkg + 16 + plen);
return pkg;
}
/* FRAMA-C: Decrypt with MAC verification (Encrypt-then-MAC) */
/*@ requires \valid_read(&kr->enc_key[0..31]);
@ requires \valid_read(&kr->mac_key[0..31]);
@ requires kr->active == 1;
@ requires pkglen >= 48;
@ requires \valid_read(pkg + (0..pkglen-1));
@ requires \valid(olen);
@ assigns *olen;
@ behavior auth_ok:
@ ensures \result != \null ==> *olen == pkglen - 48;
@ behavior auth_fail:
@ ensures \result == \null ==> *olen == pkglen - 48;
*/
uint8_t *zupt_decrypt_buffer(const zupt_keyring_t *kr,
const uint8_t *pkg, size_t pkglen,
uint64_t block_seq, size_t *olen) {
(void)block_seq;
if (pkglen < ZUPT_NONCE_SIZE + ZUPT_HMAC_SIZE) return NULL;
size_t clen = pkglen - ZUPT_NONCE_SIZE - ZUPT_HMAC_SIZE;
*olen = clen;
/* Verify HMAC — constant-time comparison via XOR accumulation */
uint8_t expected_mac[32];
zupt_hmac_sha256(kr->mac_key, ZUPT_HMAC_SIZE,
pkg, ZUPT_NONCE_SIZE + clen,
expected_mac);
const uint8_t *stored_mac = pkg + ZUPT_NONCE_SIZE + clen;
#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 |= (uint64_t)(expected_mac[i] ^ stored_mac[i]);
#endif
zupt_secure_wipe(expected_mac, 32);
/* CT-REQUIRED: Always decrypt even on MAC failure to prevent timing oracle.
* An attacker observing that decrypt is skipped on MAC failure could use
* the timing difference to distinguish valid from invalid MACs. */
uint8_t *plain = (uint8_t *)malloc(clen);
if (!plain) return NULL;
const uint8_t *nonce = pkg;
zupt_aes256_ctr(kr->enc_key, nonce, pkg + 16, plain, clen);
if (diff != 0) {
/* Authentication failed — wipe and discard decrypted data */
zupt_secure_wipe(plain, clen);
free(plain);
return NULL;
}
return plain;
}
/* ═══════════════════════════════════════════════════════════════════
* HYBRID POST-QUANTUM KEM: ML-KEM-768 + X25519 (v0.7.0)
*
* Security model: Secure if EITHER ML-KEM-768 OR X25519 is secure.
* Same approach as Signal (PQXDH), iMessage (PQ3), OpenSSH 9.0+.
*
* Key file format (.zupt-key):
* [4B] magic "ZKEY"
* [1B] version 0x01
* [1B] flags: bit0=has_private
* [2B] reserved
* [1184B] ml_kem_pk
* [32B] x25519_pk
* [2400B] ml_kem_sk (only if has_private)
* [32B] x25519_sk (only if has_private)
* [8B] xxh64 checksum of all above
* ═══════════════════════════════════════════════════════════════════ */
#include "zupt_mlkem.h"
#include "zupt_x25519.h"
#include "zupt_keccak.h"
#define ZKEY_MAGIC "ZKEY"
#define ZKEY_VERSION 0x01
#define ZKEY_FLAG_PRIVATE 0x01
#define ZKEY_PUB_SIZE (8 + 1184 + 32) /* header + ml_kem_pk + x25519_pk */
#define ZKEY_PRIV_SIZE (8 + 1184 + 32 + 2400 + 32) /* + ml_kem_sk + x25519_sk */
int zupt_hybrid_keygen(const char *keyfile) {
uint8_t ml_pk[MLKEM_PUBLICKEYBYTES], ml_sk[MLKEM_SECRETKEYBYTES];
uint8_t x_sk[32], x_pk[32];
/* Generate ML-KEM-768 keypair */
if (zupt_mlkem768_keygen(ml_pk, ml_sk) != 0) return -1;
/* Generate X25519 keypair */
zupt_random_bytes(x_sk, 32);
zupt_x25519_base(x_pk, x_sk);
/* Write private key file */
FILE *f = fopen(keyfile, "wb");
if (!f) return -1;
size_t total = ZKEY_PRIV_SIZE;
uint8_t *buf = (uint8_t *)calloc(total + 8, 1); /* +8 for checksum */
if (!buf) { fclose(f); return -1; }
memcpy(buf, ZKEY_MAGIC, 4);
buf[4] = ZKEY_VERSION;
buf[5] = ZKEY_FLAG_PRIVATE;
buf[6] = buf[7] = 0; /* reserved */
memcpy(buf + 8, ml_pk, 1184);
memcpy(buf + 8 + 1184, x_pk, 32);
memcpy(buf + 8 + 1184 + 32, ml_sk, 2400);
memcpy(buf + 8 + 1184 + 32 + 2400, x_sk, 32);
/* Checksum */
uint64_t ck = zupt_xxh64(buf, total, 0);
zupt_le64_put(buf + total, ck);
size_t written = fwrite(buf, 1, total + 8, f);
fclose(f);
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
zupt_secure_wipe(x_sk, 32);
zupt_secure_wipe(buf, total + 8);
free(buf);
return (written == total + 8) ? 0 : -1;
}
int zupt_hybrid_export_pubkey(const char *privfile, const char *pubfile) {
FILE *f = fopen(privfile, "rb");
if (!f) return -1;
uint8_t hdr[8];
if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, ZKEY_MAGIC, 4) != 0 ||
!(hdr[5] & ZKEY_FLAG_PRIVATE)) {
fclose(f); return -1;
}
uint8_t pk_data[1184 + 32];
if (fread(pk_data, 1, 1216, f) != 1216) { fclose(f); return -1; }
fclose(f);
/* Write public key file */
FILE *out = fopen(pubfile, "wb");
if (!out) return -1;
size_t total = ZKEY_PUB_SIZE;
uint8_t buf[ZKEY_PUB_SIZE + 8];
memcpy(buf, ZKEY_MAGIC, 4);
buf[4] = ZKEY_VERSION;
buf[5] = 0; /* no private key */
buf[6] = buf[7] = 0;
memcpy(buf + 8, pk_data, 1216);
uint64_t ck = zupt_xxh64(buf, total, 0);
zupt_le64_put(buf + total, ck);
size_t written = fwrite(buf, 1, total + 8, out);
fclose(out);
return (written == total + 8) ? 0 : -1;
}
/* Read public key from a .zupt-key file (works for both pub and priv files) */
static int read_pubkey(const char *path, uint8_t ml_pk[1184], uint8_t x_pk[32]) {
FILE *f = fopen(path, "rb");
if (!f) return -1;
uint8_t hdr[8];
if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, ZKEY_MAGIC, 4) != 0) {
fclose(f); return -1;
}
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
if (fread(x_pk, 1, 32, f) != 32) { fclose(f); return -1; }
fclose(f);
return 0;
}
/* Read private key from a .zupt-key file */
static int read_privkey(const char *path, uint8_t ml_pk[1184], uint8_t x_pk[32],
uint8_t ml_sk[2400], uint8_t x_sk[32]) {
FILE *f = fopen(path, "rb");
if (!f) return -1;
uint8_t hdr[8];
if (fread(hdr, 1, 8, f) != 8 || memcmp(hdr, ZKEY_MAGIC, 4) != 0 ||
!(hdr[5] & ZKEY_FLAG_PRIVATE)) {
fclose(f); return -1;
}
if (fread(ml_pk, 1, 1184, f) != 1184) { fclose(f); return -1; }
if (fread(x_pk, 1, 32, f) != 32) { fclose(f); return -1; }
if (fread(ml_sk, 1, 2400, f) != 2400) { fclose(f); return -1; }
if (fread(x_sk, 1, 32, f) != 32) { fclose(f); return -1; }
fclose(f);
return 0;
}
/*
* HYBRID ENCRYPT INIT: Encapsulate with ML-KEM + X25519, derive archive keys.
*
* enc_hdr output (1121 bytes):
* [1B] enc_type = 0x02 (PQ-Hybrid)
* [1088B] ml_kem_ciphertext
* [32B] ephemeral_x25519_pubkey
*
* Key derivation:
* hybrid_ikm = ml_kem_ss XOR x25519_ss
* archive_key[64] = SHA-256(hybrid_ikm ‖ ml_kem_ct ‖ ephemeral_pk ‖ "ZUPT-HYBRID-v1")
* enc_key = archive_key[0:32], mac_key = archive_key[32:64]
*/
/* FRAMA-C: Hybrid PQ encrypt init — ML-KEM-768 + X25519 KEM */
/*@ requires \valid(kr);
@ requires \valid_read(pubkeyfile);
@ requires \valid(enc_hdr + (0..1199));
@ requires \valid(enc_hdr_len);
@ assigns kr->enc_key[0..31], kr->mac_key[0..31], kr->base_nonce[0..15],
@ kr->iterations, kr->active;
@ assigns enc_hdr[0..1199], *enc_hdr_len;
@ ensures \result == 0 ==> kr->active == 1;
@ ensures \result == 0 ==> *enc_hdr_len == 1137;
*/
int zupt_hybrid_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
uint8_t *enc_hdr, size_t *enc_hdr_len) {
uint8_t ml_pk[1184], x_pk[32];
if (read_pubkey(pubkeyfile, ml_pk, x_pk) != 0) return -1;
/* ML-KEM-768 encapsulation */
uint8_t ml_ct[1088], ml_ss[32];
if (zupt_mlkem768_encaps(ml_ct, ml_ss, ml_pk) != 0) return -1;
/* X25519 ECDH */
uint8_t eph_sk[32], eph_pk[32], x_ss[32];
zupt_random_bytes(eph_sk, 32);
zupt_x25519_base(eph_pk, eph_sk);
zupt_x25519(x_ss, eph_sk, x_pk);
/* Hybrid shared secret: XOR then hash with transcript */
uint8_t hybrid_ikm[32];
for (int i = 0; i < 32; i++) hybrid_ikm[i] = ml_ss[i] ^ x_ss[i];
/* archive_key = SHA-256(hybrid_ikm ‖ ml_ct ‖ eph_pk ‖ "ZUPT-HYBRID-v1") */
/* We need 64 bytes, so use SHA3-512 instead of SHA-256 */
uint8_t kdf_input[32 + 1088 + 32 + 15];
memcpy(kdf_input, hybrid_ikm, 32);
memcpy(kdf_input + 32, ml_ct, 1088);
memcpy(kdf_input + 32 + 1088, eph_pk, 32);
memcpy(kdf_input + 32 + 1088 + 32, "ZUPT-HYBRID-v1", 15);
uint8_t archive_key[64];
zupt_sha3_512(kdf_input, sizeof(kdf_input), archive_key);
/* Set up keyring */
kr->canary_head = ZUPT_CANARY;
memcpy(kr->enc_key, archive_key, 32);
memcpy(kr->mac_key, archive_key + 32, 32);
zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE);
kr->iterations = 0;
kr->active = 1;
kr->canary_tail = ZUPT_CANARY;
/* Lock key material in RAM */
zupt_mlock_keys(kr->enc_key, ZUPT_AES_KEY_SIZE);
zupt_mlock_keys(kr->mac_key, ZUPT_HMAC_SIZE);
/* Build encryption header: enc_type(1) + ml_ct(1088) + eph_pk(32) + base_nonce(16) */
enc_hdr[0] = ZUPT_ENC_PQ_HYBRID;
memcpy(enc_hdr + 1, ml_ct, 1088);
memcpy(enc_hdr + 1 + 1088, eph_pk, 32);
memcpy(enc_hdr + 1 + 1088 + 32, kr->base_nonce, 16);
*enc_hdr_len = 1 + 1088 + 32 + 16; /* 1137 bytes */
/* Wipe all intermediates */
zupt_secure_wipe(ml_ss, 32);
zupt_secure_wipe(x_ss, 32);
zupt_secure_wipe(eph_sk, 32);
zupt_secure_wipe(hybrid_ikm, 32);
zupt_secure_wipe(kdf_input, sizeof(kdf_input));
zupt_secure_wipe(archive_key, 64);
return 0;
}
/*
* HYBRID DECRYPT INIT: Decapsulate with ML-KEM + X25519, derive archive keys.
*/
/* FRAMA-C: Hybrid PQ decrypt init — ML-KEM-768 + X25519 decaps */
/*@ requires \valid(kr);
@ requires \valid_read(privkeyfile);
@ requires enc_hdr_len >= 1137;
@ requires \valid_read(enc_hdr + (0..enc_hdr_len-1));
@ assigns kr->enc_key[0..31], kr->mac_key[0..31], kr->base_nonce[0..15],
@ kr->iterations, kr->active;
@ ensures \result == 0 ==> kr->active == 1;
*/
int zupt_hybrid_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *enc_hdr, size_t enc_hdr_len) {
if (enc_hdr_len < 1 + 1088 + 32 + 16) return -1; /* enc_type + ct + eph_pk + nonce */
if (enc_hdr[0] != ZUPT_ENC_PQ_HYBRID) return -1;
const uint8_t *ml_ct = enc_hdr + 1;
const uint8_t *eph_pk = enc_hdr + 1 + 1088;
const uint8_t *nonce = enc_hdr + 1 + 1088 + 32;
uint8_t ml_pk[1184], x_pk[32], ml_sk[2400], x_sk[32];
if (read_privkey(privkeyfile, ml_pk, x_pk, ml_sk, x_sk) != 0) return -1;
/* ML-KEM-768 decapsulation */
uint8_t ml_ss[32];
zupt_mlkem768_decaps(ml_ss, ml_ct, ml_sk);
/* X25519 ECDH with ephemeral pubkey */
uint8_t x_ss[32];
zupt_x25519(x_ss, x_sk, eph_pk);
/* Same key derivation as encrypt */
uint8_t hybrid_ikm[32];
for (int i = 0; i < 32; i++) hybrid_ikm[i] = ml_ss[i] ^ x_ss[i];
uint8_t kdf_input[32 + 1088 + 32 + 15];
memcpy(kdf_input, hybrid_ikm, 32);
memcpy(kdf_input + 32, ml_ct, 1088);
memcpy(kdf_input + 32 + 1088, eph_pk, 32);
memcpy(kdf_input + 32 + 1088 + 32, "ZUPT-HYBRID-v1", 15);
uint8_t archive_key[64];
zupt_sha3_512(kdf_input, sizeof(kdf_input), archive_key);
kr->canary_head = ZUPT_CANARY;
memcpy(kr->enc_key, archive_key, 32);
memcpy(kr->mac_key, archive_key + 32, 32);
memcpy(kr->base_nonce, nonce, ZUPT_NONCE_SIZE); /* Read from enc_hdr, NOT random */
kr->iterations = 0;
kr->active = 1;
kr->canary_tail = ZUPT_CANARY;
/* Lock key material in RAM */
zupt_mlock_keys(kr->enc_key, ZUPT_AES_KEY_SIZE);
zupt_mlock_keys(kr->mac_key, ZUPT_HMAC_SIZE);
zupt_secure_wipe(ml_sk, sizeof(ml_sk));
zupt_secure_wipe(x_sk, 32);
zupt_secure_wipe(ml_ss, 32);
zupt_secure_wipe(x_ss, 32);
zupt_secure_wipe(hybrid_ikm, 32);
zupt_secure_wipe(kdf_input, sizeof(kdf_input));
zupt_secure_wipe(archive_key, 64);
return 0;
}