v4.0.0: codec 2.60.4 security release, --pq-box sealed-box mode, F-16 fix
Some checks failed
CI / build-and-test (clang) (push) Has been cancelled
CI / build-and-test (gcc) (push) Has been cancelled
CI / strict-warnings (clang, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -O2 -std=c11 -Werror) (push) Has been cancelled
CI / strict-warnings (gcc, -Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -Wformat-security -Wlogical-op -Wjump-misses-init -Wdouble-promotion -O2 -std=c11 -Werror) (push) Has been cancelled
CI / sanitizers (push) Has been cancelled
CI / pie-hardening (push) Has been cancelled
CI / cross-aarch64 (push) Has been cancelled
CI / dist-reproducibility (push) Has been cancelled
CI / packaging-syntax (push) Has been cancelled
CI / release (push) Has been cancelled

Major release. Highlights:

- Codec: vendored VaptVupt codec moves to canonical 2.60.4 security
  release. Fixes a high-severity OOB heap write in the AVX2 decode fast
  path (reachable on a valid stream sized to exactly content_size, both
  tail variants). Brings CBMC-formally-verified BCJ filters with
  automatic ELF/PE/Mach-O detection. Compressed output stays
  byte-identical (ratio gate Δ 0.00%); wire format unchanged at v1.6.
- New --pq-box sealed-box recipient mode (vendored libpqvaptvupt 0.6.0):
  ML-KEM-768 + X25519 combined via HKDF-SHA256 with domain separation,
  AES-256-CTR + HMAC-SHA256 EtM. Legacy --pq and --pq-sdk stay readable.
- F-16: discloses and fixes a pre-existing data-loss defect in the
  <= 3.8.0 in-tree BCJ encoder. Full back-compat matrix decodes
  byte-exact under 4.0.0; every readable pre-4.0 archive remains readable.

Repository hygiene:
- Sync full 4.0.0 source tree (codec, crypto, SDK, GUI, packaging, tests).
- Remove internal scratch files (PROMPT.md, FORMAL_AUDIT_PROMPT.md)
  and superseded version-specific docs (INTEGRATION_PROTOCOL_2.60.4.md,
  docs/FINDINGS-2.x.md) and a stray test binary.
- Refresh README download/install section to real 4.0.0 release assets;
  bump version badge to 4.0.0.
- Add .gitignore for build outputs (keeps vendored prebuilt libraries).
This commit is contained in:
Cristian Cezar Moisés 2026-06-10 18:48:58 -03:00
commit 544a2cd647
98 changed files with 15615 additions and 1397 deletions

181
src/zupt_crypto_pqbox.c Normal file
View file

@ -0,0 +1,181 @@
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
* Copyright (c) 2026 Cristian Cezar Moisés
*
* zupt_crypto_pqbox.c ZUPT_ENC_PQ_BOX_V1 (0x05): hybrid PQ sealed-box
* recipient encryption backed by vendored libpqvaptvupt (v0.6.0).
*
* Why a third PQ mode:
* - legacy --pq (0x02) combines the ML-KEM and X25519 shared secrets
* with XOR+SHA3 functional, but not the modern recommendation;
* - --pq-sdk (0x03) is libzuptsdk's v2 envelope (kept for back-compat);
* - --pq-box (0x05) uses libpqvaptvupt's sealed box, which combines the
* two KEM secrets through HKDF-SHA256 Extract/Expand with a
* domain-separating info string ("pqvv-seal-v1") the construction
* this project's own crypto standing orders prescribe. AES-256-CTR +
* HMAC-SHA256 Encrypt-then-MAC inside the box; if either KEM is
* broken later, the other still protects the session key.
*
* Envelope layout inside the ENC_HEADER block payload:
* [1B] enc_type = ZUPT_ENC_PQ_BOX_V1 (0x05)
* [4B] sealed_len (LE)
* [..] pqvv_seal(recipient_pk, session_key[32]) 32 + PQVV_OVERHEAD
*
* The 32-byte random session key is split into the archive's enc/mac keys
* with domain-separated SHA3-256, mirroring the SDK path exactly so the
* per-block AEAD machinery is shared and already regression-tested.
*
* Key files (this module owns the format; magic prevents cross-mode
* key-type confusion at the file level):
* [8B] "PQVVBOX1"
* [1B] role: 'P' (public) | 'S' (secret)
* [..] raw key bytes (PQVV_PUBLICKEYBYTES / PQVV_SECRETKEYBYTES)
*/
#include "zupt.h"
#include "zupt_keccak.h"
#include "pqvaptvupt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PQBOX_MAGIC "PQVVBOX1"
#define PQBOX_MAGIC_LEN 8
#define PQBOX_HDR_LEN (PQBOX_MAGIC_LEN + 1)
#define PQBOX_SEALED_SESSION (32u + PQVV_OVERHEAD)
static int pqbox_write_keyfile(const char *path, char role,
const uint8_t *key, size_t klen) {
FILE *f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(PQBOX_MAGIC, 1, PQBOX_MAGIC_LEN, f) == PQBOX_MAGIC_LEN
&& fputc(role, f) != EOF
&& fwrite(key, 1, klen, f) == klen;
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
/* Reads and validates a key file. Returns 0 and fills `key` on success. */
static int pqbox_read_keyfile(const char *path, char role,
uint8_t *key, size_t klen) {
FILE *f = fopen(path, "rb");
if (!f) return -1;
uint8_t hdr[PQBOX_HDR_LEN];
int ok = fread(hdr, 1, PQBOX_HDR_LEN, f) == PQBOX_HDR_LEN
&& memcmp(hdr, PQBOX_MAGIC, PQBOX_MAGIC_LEN) == 0
&& hdr[PQBOX_MAGIC_LEN] == (uint8_t)role
&& fread(key, 1, klen, f) == klen
&& fgetc(f) == EOF; /* exact size — no trailing bytes */
fclose(f);
return ok ? 0 : -1;
}
int zupt_pqbox_keygen(const char *privkeyfile, const char *pubkeyfile) {
uint8_t pk[PQVV_PUBLICKEYBYTES];
uint8_t sk[PQVV_SECRETKEYBYTES];
if (pqvv_keygen(pk, sk) != PQVV_OK) return -1;
int rc = 0;
if (pqbox_write_keyfile(privkeyfile, 'S', sk, sizeof(sk)) != 0) rc = -1;
if (rc == 0 && pqbox_write_keyfile(pubkeyfile, 'P', pk, sizeof(pk)) != 0) rc = -1;
zupt_secure_wipe(sk, sizeof(sk));
return rc;
}
/* Encrypt-init: seal a fresh 32-byte session key to the recipient and
* emit the ENC_HEADER payload. Mirrors zupt_sdk_hybrid_encrypt_init. */
int zupt_pqbox_encrypt_init(zupt_keyring_t *kr, const char *pubkeyfile,
uint8_t *enc_hdr, size_t *enc_hdr_len) {
uint8_t pk[PQVV_PUBLICKEYBYTES];
if (pqbox_read_keyfile(pubkeyfile, 'P', pk, sizeof(pk)) != 0) {
fprintf(stderr, "Error: '%s' is not a pq-box PUBLIC key file.\n", pubkeyfile);
return -1;
}
uint8_t session_key[32];
zupt_random_bytes(session_key, 32);
uint8_t *sealed = NULL;
size_t sealed_len = 0;
if (pqvv_seal(pk, session_key, 32, &sealed, &sealed_len) != PQVV_OK
|| sealed_len != PQBOX_SEALED_SESSION) {
free(sealed);
zupt_secure_wipe(session_key, sizeof(session_key));
return -1;
}
enc_hdr[0] = ZUPT_ENC_PQ_BOX_V1;
enc_hdr[1] = (uint8_t)(sealed_len & 0xff);
enc_hdr[2] = (uint8_t)((sealed_len >> 8) & 0xff);
enc_hdr[3] = (uint8_t)((sealed_len >> 16) & 0xff);
enc_hdr[4] = (uint8_t)((sealed_len >> 24) & 0xff);
memcpy(enc_hdr + 5, sealed, sealed_len);
*enc_hdr_len = 5 + sealed_len;
free(sealed);
/* Session key → enc/mac keys, domain-separated SHA3 (identical shape
* to the SDK path so all per-block machinery is shared). */
uint8_t kdf_buf[32 + 16];
memcpy(kdf_buf, session_key, 32);
memcpy(kdf_buf + 32, "ZUPT-BOX-ENC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key);
memcpy(kdf_buf + 32, "ZUPT-BOX-MAC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key);
zupt_secure_wipe(kdf_buf, sizeof(kdf_buf));
kr->canary_head = ZUPT_CANARY;
zupt_random_bytes(kr->base_nonce, ZUPT_NONCE_SIZE);
kr->iterations = 0;
kr->active = 1;
kr->canary_tail = ZUPT_CANARY;
zupt_secure_wipe(session_key, sizeof(session_key));
return 0;
}
/* Decrypt-init: parse the 0x05 envelope, open with the recipient secret
* key, rebuild the keyring. Fail-closed on any mismatch. */
int zupt_pqbox_decrypt_init(zupt_keyring_t *kr, const char *privkeyfile,
const uint8_t *payload, size_t payload_len) {
if (payload_len < 5 || payload[0] != ZUPT_ENC_PQ_BOX_V1) return -1;
uint32_t sealed_len = (uint32_t)payload[1]
| ((uint32_t)payload[2] << 8)
| ((uint32_t)payload[3] << 16)
| ((uint32_t)payload[4] << 24);
if (sealed_len != PQBOX_SEALED_SESSION || payload_len < 5 + (size_t)sealed_len)
return -1;
uint8_t sk[PQVV_SECRETKEYBYTES];
if (pqbox_read_keyfile(privkeyfile, 'S', sk, sizeof(sk)) != 0) {
fprintf(stderr, "Error: '%s' is not a pq-box SECRET key file.\n", privkeyfile);
return -1;
}
uint8_t *pt = NULL;
size_t pt_len = 0;
int rc = pqvv_open(sk, payload + 5, sealed_len, &pt, &pt_len);
zupt_secure_wipe(sk, sizeof(sk));
if (rc != PQVV_OK || pt_len != 32 || !pt) {
if (pt) { zupt_secure_wipe(pt, pt_len); free(pt); }
return -1; /* wrong key, tampered envelope — generic at call site */
}
uint8_t session_key[32];
memcpy(session_key, pt, 32);
zupt_secure_wipe(pt, pt_len);
free(pt);
uint8_t kdf_buf[32 + 16];
memcpy(kdf_buf, session_key, 32);
memcpy(kdf_buf + 32, "ZUPT-BOX-ENC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->enc_key);
memcpy(kdf_buf + 32, "ZUPT-BOX-MAC-KEY", 16);
zupt_sha3_256(kdf_buf, sizeof(kdf_buf), kr->mac_key);
zupt_secure_wipe(kdf_buf, sizeof(kdf_buf));
kr->canary_head = ZUPT_CANARY;
kr->iterations = 0;
kr->active = 1;
kr->canary_tail = ZUPT_CANARY;
zupt_secure_wipe(session_key, sizeof(session_key));
return 0;
}