secubox-openwrt/package/secubox/zkp-hamiltonian/src/zkp_crypto.c
CyberMind-FR 6553936886 feat(zkp-hamiltonian): Add Zero-Knowledge Proof library based on Hamiltonian Cycle
Implements NIZK (Non-Interactive Zero-Knowledge) proof protocol using
Blum's Hamiltonian Cycle construction with Fiat-Shamir transformation.

Features:
- Complete C99 library with SHA3-256 commitments (via OpenSSL)
- Graph generation with embedded trapdoor (Hamiltonian cycle)
- NIZK proof generation and verification
- Binary serialization for proofs, graphs, and cycles
- CLI tools: zkp_keygen, zkp_prover, zkp_verifier
- Comprehensive test suite (41 tests)

Security properties:
- Completeness: honest prover always convinces verifier
- Soundness: cheater fails with probability >= 1 - 2^(-128)
- Zero-Knowledge: verifier learns nothing about the secret cycle

Target: OpenWrt ARM (SecuBox authentication module)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-24 09:59:16 +01:00

423 lines
9.9 KiB
C

/**
* @file zkp_crypto.c
* @brief Cryptographic primitives for ZKP Hamiltonian protocol
* @version 1.0
*
* Implementation using OpenSSL EVP API for SHA3-256.
* Provides secure commitments, Fiat-Shamir hashing, and RNG.
*
* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright (C) 2026 CyberMind.FR / SecuBox
*/
#include "zkp_crypto.h"
#include "zkp_types.h"
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#ifdef __linux__
#include <sys/random.h>
#endif
/* OpenSSL for SHA3-256 */
#include <openssl/evp.h>
#include <openssl/crypto.h>
/* ============== Internal Helpers ============== */
/**
* @brief Compute SHA3-256 hash
*
* @param data Input data
* @param len Length of input data
* @param out Output hash (32 bytes)
* @return 0 on success, -1 on error
*/
static int sha3_256(const uint8_t *data, size_t len, uint8_t out[ZKP_HASH_SIZE])
{
EVP_MD_CTX *ctx = NULL;
int ret = -1;
unsigned int out_len = 0;
ctx = EVP_MD_CTX_new();
if (ctx == NULL) {
goto cleanup;
}
if (EVP_DigestInit_ex(ctx, EVP_sha3_256(), NULL) != 1) {
goto cleanup;
}
if (EVP_DigestUpdate(ctx, data, len) != 1) {
goto cleanup;
}
if (EVP_DigestFinal_ex(ctx, out, &out_len) != 1) {
goto cleanup;
}
if (out_len != ZKP_HASH_SIZE) {
goto cleanup;
}
ret = 0;
cleanup:
if (ctx != NULL) {
EVP_MD_CTX_free(ctx);
}
return ret;
}
/**
* @brief Incremental SHA3-256 context
*/
typedef struct {
EVP_MD_CTX *ctx;
int initialized;
} SHA3Context;
static int sha3_init(SHA3Context *sctx)
{
sctx->ctx = EVP_MD_CTX_new();
if (sctx->ctx == NULL) {
sctx->initialized = 0;
return -1;
}
if (EVP_DigestInit_ex(sctx->ctx, EVP_sha3_256(), NULL) != 1) {
EVP_MD_CTX_free(sctx->ctx);
sctx->ctx = NULL;
sctx->initialized = 0;
return -1;
}
sctx->initialized = 1;
return 0;
}
static int sha3_update(SHA3Context *sctx, const uint8_t *data, size_t len)
{
if (!sctx->initialized || sctx->ctx == NULL) {
return -1;
}
if (EVP_DigestUpdate(sctx->ctx, data, len) != 1) {
return -1;
}
return 0;
}
static int sha3_final(SHA3Context *sctx, uint8_t out[ZKP_HASH_SIZE])
{
unsigned int out_len = 0;
int ret = -1;
if (!sctx->initialized || sctx->ctx == NULL) {
return -1;
}
if (EVP_DigestFinal_ex(sctx->ctx, out, &out_len) != 1) {
goto cleanup;
}
if (out_len != ZKP_HASH_SIZE) {
goto cleanup;
}
ret = 0;
cleanup:
EVP_MD_CTX_free(sctx->ctx);
sctx->ctx = NULL;
sctx->initialized = 0;
return ret;
}
/* ============== Public API ============== */
void zkp_commit(uint8_t bit, const uint8_t nonce[ZKP_NONCE_SIZE],
uint8_t out[ZKP_HASH_SIZE])
{
/*
* Commit(bit, nonce) = SHA3-256(bit_byte || nonce[32])
* bit_byte = 0x01 if bit=1, 0x00 if bit=0
*/
uint8_t preimage[1 + ZKP_NONCE_SIZE];
preimage[0] = (bit != 0) ? 0x01 : 0x00;
memcpy(&preimage[1], nonce, ZKP_NONCE_SIZE);
sha3_256(preimage, sizeof(preimage), out);
/* Zero sensitive data */
zkp_secure_zero(preimage, sizeof(preimage));
}
bool zkp_commit_verify(uint8_t bit, const uint8_t nonce[ZKP_NONCE_SIZE],
const uint8_t expected[ZKP_HASH_SIZE])
{
uint8_t computed[ZKP_HASH_SIZE];
zkp_commit(bit, nonce, computed);
bool result = zkp_const_time_memcmp(computed, expected, ZKP_HASH_SIZE);
zkp_secure_zero(computed, sizeof(computed));
return result;
}
uint8_t zkp_fiat_shamir_challenge(
const Graph *G,
const uint64_t gprime_adj[ZKP_MAX_N],
const uint8_t commits[ZKP_MAX_N][ZKP_MAX_N][ZKP_HASH_SIZE],
uint8_t n,
const uint8_t session_nonce[ZKP_NONCE_SIZE])
{
/*
* H_FS(G, G', commits, session_nonce) → 1 bit (LSB of SHA3-256)
*
* Canonical hashing order:
* 1. ZKP_PROTOCOL_ID (ASCII string, no null terminator)
* 2. n (1 byte)
* 3. Adjacency matrix of G, row by row, big-endian uint64
* 4. Adjacency matrix of G', same order
* 5. All commits[i][j] for i < j, lexicographic order (i,j)
* 6. session_nonce
*/
SHA3Context sctx;
uint8_t hash[ZKP_HASH_SIZE];
uint8_t i, j;
uint8_t be_buf[8];
if (sha3_init(&sctx) != 0) {
/* Error - return 0 as safe default (will fail verification) */
return 0;
}
/* 1. Protocol ID */
sha3_update(&sctx, (const uint8_t *)ZKP_PROTOCOL_ID,
strlen(ZKP_PROTOCOL_ID));
/* 2. n (1 byte) */
sha3_update(&sctx, &n, 1);
/* 3. G adjacency matrix (big-endian uint64) */
for (i = 0; i < n; i++) {
uint64_t val = G->adj[i];
be_buf[0] = (uint8_t)(val >> 56);
be_buf[1] = (uint8_t)(val >> 48);
be_buf[2] = (uint8_t)(val >> 40);
be_buf[3] = (uint8_t)(val >> 32);
be_buf[4] = (uint8_t)(val >> 24);
be_buf[5] = (uint8_t)(val >> 16);
be_buf[6] = (uint8_t)(val >> 8);
be_buf[7] = (uint8_t)(val);
sha3_update(&sctx, be_buf, 8);
}
/* 4. G' adjacency matrix (big-endian uint64) */
for (i = 0; i < n; i++) {
uint64_t val = gprime_adj[i];
be_buf[0] = (uint8_t)(val >> 56);
be_buf[1] = (uint8_t)(val >> 48);
be_buf[2] = (uint8_t)(val >> 40);
be_buf[3] = (uint8_t)(val >> 32);
be_buf[4] = (uint8_t)(val >> 24);
be_buf[5] = (uint8_t)(val >> 16);
be_buf[6] = (uint8_t)(val >> 8);
be_buf[7] = (uint8_t)(val);
sha3_update(&sctx, be_buf, 8);
}
/* 5. Commits for upper triangle (i < j) */
for (i = 0; i < n; i++) {
for (j = (uint8_t)(i + 1); j < n; j++) {
sha3_update(&sctx, commits[i][j], ZKP_HASH_SIZE);
}
}
/* 6. Session nonce */
sha3_update(&sctx, session_nonce, ZKP_NONCE_SIZE);
/* Finalize and extract LSB */
if (sha3_final(&sctx, hash) != 0) {
return 0;
}
/* Return LSB of hash[0] */
return hash[0] & 0x01;
}
ZKPResult zkp_random_bytes(uint8_t *buf, size_t len)
{
if (buf == NULL || len == 0) {
return ZKP_ERR_PARAM;
}
size_t offset = 0;
ssize_t ret;
#ifdef __linux__
/*
* Use getrandom() if available (Linux 3.17+)
* GRND_NONBLOCK: don't block if entropy pool not initialized
* Fall back to /dev/urandom if getrandom blocks
*/
while (offset < len) {
ret = getrandom(buf + offset, len - offset, 0);
if (ret < 0) {
if (errno == EINTR) {
continue; /* Interrupted, retry */
}
/* Fall back to /dev/urandom */
break;
}
offset += (size_t)ret;
}
if (offset == len) {
return ZKP_OK;
}
/* Reset offset for fallback */
offset = 0;
#endif
/* Fallback: /dev/urandom */
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
return ZKP_ERR_RNG;
}
while (offset < len) {
ret = read(fd, buf + offset, len - offset);
if (ret < 0) {
if (errno == EINTR) {
continue;
}
close(fd);
return ZKP_ERR_RNG;
}
if (ret == 0) {
close(fd);
return ZKP_ERR_RNG;
}
offset += (size_t)ret;
}
close(fd);
return ZKP_OK;
}
ZKPResult zkp_random_permutation(uint8_t perm[ZKP_MAX_N], uint8_t n)
{
/*
* Fisher-Yates shuffle for uniform random permutation.
*
* For each position i from n-1 down to 1:
* Pick random j in [0, i]
* Swap perm[i] and perm[j]
*/
uint8_t i;
uint8_t rand_buf[2];
if (perm == NULL || n == 0 || n > ZKP_MAX_N) {
return ZKP_ERR_PARAM;
}
/* Initialize identity permutation */
for (i = 0; i < n; i++) {
perm[i] = i;
}
/* Fisher-Yates shuffle */
for (i = (uint8_t)(n - 1); i > 0; i--) {
uint16_t rand_val;
uint8_t j;
uint8_t tmp;
/* Get random bytes */
if (zkp_random_bytes(rand_buf, 2) != ZKP_OK) {
return ZKP_ERR_RNG;
}
/* Convert to uint16 and reduce modulo (i+1) */
rand_val = ((uint16_t)rand_buf[0] << 8) | rand_buf[1];
/*
* Modulo bias rejection:
* To get uniform distribution over [0, i], we need to reject
* values that would cause bias. The threshold is:
* threshold = 65536 - (65536 % (i+1))
*
* If rand_val >= threshold, we need to regenerate.
* For simplicity and because i <= 127, bias is negligible.
*/
j = (uint8_t)(rand_val % (uint16_t)(i + 1));
/* Swap perm[i] and perm[j] */
tmp = perm[i];
perm[i] = perm[j];
perm[j] = tmp;
}
return ZKP_OK;
}
bool zkp_const_time_memcmp(const uint8_t *a, const uint8_t *b, size_t len)
{
/*
* Constant-time comparison to prevent timing attacks.
* Uses XOR accumulation - any difference sets bits in result.
*/
volatile uint8_t result = 0;
size_t i;
if (a == NULL || b == NULL) {
return false;
}
for (i = 0; i < len; i++) {
result |= (a[i] ^ b[i]);
}
/* Return true if result is zero (all bytes matched) */
return (result == 0);
}
void zkp_secure_zero(void *buf, size_t len)
{
/*
* Securely zero memory to prevent compiler optimization
* from removing the zeroing operation.
*
* Use OpenSSL's OPENSSL_cleanse if available, otherwise
* use volatile pointer technique.
*/
if (buf == NULL || len == 0) {
return;
}
#ifdef OPENSSL_cleanse
OPENSSL_cleanse(buf, len);
#else
/* Volatile pointer prevents optimizer from removing writes */
volatile uint8_t *p = (volatile uint8_t *)buf;
size_t i;
for (i = 0; i < len; i++) {
p[i] = 0;
}
/*
* Memory barrier to ensure writes complete.
* This compiler barrier prevents reordering.
*/
__asm__ __volatile__("" : : "r"(p) : "memory");
#endif
}