From 6b7aa62a0e77d679b277684b9a20499db3a50946 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 24 Feb 2026 16:45:42 +0100 Subject: [PATCH] feat(mesh): ZKP authentication and blockchain sync - ZKP Mesh Authentication: Zero-Knowledge Proof identity for mesh nodes - New API endpoints: zkp-challenge, zkp-verify, zkp/graph - Shell functions: ml_zkp_init, ml_zkp_challenge, ml_zkp_verify - Enhanced join flow with optional ZKP proof requirement - Blockchain acknowledgment via peer_zkp_verified blocks - LuCI dashboard with ZKP status section and peer badges - MirrorNet Ash Compatibility: Fixed BusyBox shell incompatibilities - Replaced process substitution with pipe-based patterns - Fixed mirror.sh, gossip.sh, health.sh, identity.sh - Mesh Blockchain Sync: Fixed chain synchronization between nodes - Fixed /api/chain/since endpoint to return only new blocks - chain_add_block/chain_merge_block use awk for safe JSON insertion - Handles varying JSON formatting (whitespace, newlines) - Tested bidirectional sync: Master <-> Clone at height 70 Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 124 +++++ .claude/WIP.md | 48 +- .claude/settings.local.json | 10 +- .../resources/view/secubox/master-link.js | 88 +++ .../root/usr/lib/secubox/p2p-mesh.sh | 102 +++- .../secubox/secubox-core/root/www/api/chain | 34 +- .../files/etc/config/master-link | 5 + .../files/usr/lib/secubox/master-link.sh | 503 +++++++++++++++++- .../files/www/api/master-link/join | 6 +- .../files/www/api/master-link/zkp-challenge | 28 + .../files/www/api/master-link/zkp-verify | 49 ++ .../files/www/api/zkp/graph | 21 + .../files/usr/lib/mirrornet/gossip.sh | 39 +- .../files/usr/lib/mirrornet/health.sh | 25 +- .../files/usr/lib/mirrornet/identity.sh | 2 +- .../files/usr/lib/mirrornet/mirror.sh | 63 ++- 16 files changed, 1069 insertions(+), 78 deletions(-) create mode 100644 package/secubox/secubox-master-link/files/www/api/master-link/zkp-challenge create mode 100644 package/secubox/secubox-master-link/files/www/api/master-link/zkp-verify create mode 100644 package/secubox/secubox-master-link/files/www/api/zkp/graph diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 5aab127e..1e25924f 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -3232,6 +3232,43 @@ git checkout HEAD -- index.html - `zkp-hamiltonian/CMakeLists.txt` - **Commit:** `65539368 feat(zkp-hamiltonian): Add Zero-Knowledge Proof library based on Hamiltonian Cycle` +41. **ZKP Mesh Authentication Integration (2026-02-24)** + - Integrated Zero-Knowledge Proofs into SecuBox master-link mesh authentication system. + - **Architecture:** + - Each node has ZKP identity (public graph + secret Hamiltonian cycle) + - Challenge-response authentication between mesh peers + - Blockchain acknowledgment of successful verifications + - **New API Endpoints:** + - `GET /api/master-link/zkp-challenge` — Generate authentication challenge with TTL + - `POST /api/master-link/zkp-verify` — Verify ZKP proof, record to blockchain + - `GET /api/zkp/graph` — Serve node's public ZKP graph (base64) + - **New Shell Functions in master-link.sh:** + - `ml_zkp_init()` — Initialize ZKP identity on first boot + - `ml_zkp_status()` — Return ZKP configuration status + - `ml_zkp_challenge()` — Generate challenge with UUID and expiry + - `ml_zkp_prove()` — Generate proof for given challenge + - `ml_zkp_verify()` — Verify peer's proof against trusted graph + - `ml_zkp_trust_peer()` — Store peer's public graph for future verification + - `ml_zkp_get_graph()` — Return base64-encoded public graph + - **Blockchain Acknowledgment:** + - New block type: `peer_zkp_verified` + - Records: peer_fp, proof_hash, challenge_id, result, verified_by + - **UCI Configuration:** + - `zkp_enabled` — Toggle ZKP authentication + - `zkp_fingerprint` — Auto-derived from graph hash (SHA256[0:16]) + - `zkp_require_on_join` — Require ZKP proof for new peers + - `zkp_challenge_ttl` — Challenge validity in seconds (default 30) + - **Verification Test Results:** + - Master (192.168.255.1): ZKP identity initialized, fingerprint `7c5ead2b4e4b0106` + - API verification flow tested: challenge → proof → verify → blockchain record + - `peer_zkp_verified` block successfully recorded to chain + - **Files:** + - `secubox-master-link/files/usr/lib/secubox/master-link.sh` (ZKP functions) + - `secubox-master-link/files/www/api/zkp/graph` (new) + - `secubox-master-link/files/www/api/master-link/zkp-challenge` (new) + - `secubox-master-link/files/www/api/master-link/zkp-verify` (new) + - `secubox-master-link/files/etc/config/master-link` (ZKP options) + 41. **MetaBlogizer Upload Workflow Fix (2026-02-24)** - Sites now work immediately after upload without needing unpublish + expose. - **Root cause:** Upload created HAProxy vhost and mitmproxy route file entry, but mitmproxy never received a reload signal to activate the route. @@ -3312,3 +3349,90 @@ git checkout HEAD -- index.html - **Verification:** `rcve.gk2.secubox.in` now returns HTTP 200 with correct content. - **Files Modified:** - `luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer` + + +46. **ZKP Join Flow Integration (2026-02-24)** + - Enhanced mesh join protocol to support ZKP (Zero-Knowledge Proof) authentication. + - **Join Request Enhancement** (`ml_join_request()`): + - Now accepts `zkp_proof` (base64) and `zkp_graph` (base64) parameters + - Verifies proof against provided graph using `zkp_verifier` + - Validates fingerprint matches SHA256(graph)[0:16] + - Auto-stores peer's graph in `/etc/secubox/zkp/peers/` on successful verification + - Records `zkp_verified` and `zkp_proof_hash` in request file + - **Join Approval Enhancement** (`ml_join_approve()`): + - Auto-fetches peer's ZKP graph if not already stored during join + - Records `zkp_graph_stored` status in approval response + - Blockchain `peer_approved` blocks now include `zkp_verified` field + - **Peer-side Join** (`ml_join_with_zkp()`): + - New function for ZKP-authenticated mesh joining + - Generates ZKP proof using local identity keypair + - Uses ZKP fingerprint (from graph hash) instead of factory fingerprint + - Auto-stores master's graph for mutual authentication + - **API Update** (`/api/master-link/join`): + - Accepts `zkp_proof` and `zkp_graph` fields in POST body + - **Configuration**: + - `zkp_require_on_join`: When set to 1, rejects joins without valid ZKP proof + - **Verification:** Clone joined with `zkp_verified: true`, graphs exchanged bidirectionally + - **Files Modified:** + - `secubox-master-link/files/usr/lib/secubox/master-link.sh` + - `secubox-master-link/files/www/api/master-link/join` + + +47. **LuCI ZKP Dashboard (2026-02-24)** + - Enhanced `luci-app-master-link` with ZKP authentication status visualization. + - **Overview Tab - ZKP Status Section:** + - ZKP Identity card: fingerprint display, copy button, generation status + - ZKP Tools card: installation status for zkp_keygen/prover/verifier + - Trusted Peers card: count of stored peer graphs + - Purple theme (violet gradient) for ZKP elements + - Enabled/Disabled badge next to section title + - **Peer Table Enhancement:** + - New "Auth" column showing authentication method + - `zkpBadge()` helper function for visual indicators: + - 🔐 ZKP badge (purple) for ZKP-verified peers + - TOKEN badge (gray) for token-only authentication + - **Design:** + - Purple accent colors (#8b5cf6, #a855f7, #c084fc) for ZKP elements + - Consistent with SecuBox KISS theme guidelines + - **Files Modified:** + - `luci-app-master-link/htdocs/luci-static/resources/view/secubox/master-link.js` + + +48. **MirrorNet Ash Compatibility Fix (2026-02-24)** + - Fixed process substitution (`< <(cmd)`) incompatibility with BusyBox ash shell. + - **Pattern replaced:** `while read; do ... done < <(jsonfilter ...)` + - **Ash-compatible pattern:** `jsonfilter ... | while read; do ... done` with temp files for variable persistence + - **Files fixed:** + - `secubox-mirrornet/files/usr/lib/mirrornet/mirror.sh` (3 instances) + - `secubox-mirrornet/files/usr/lib/mirrornet/gossip.sh` (3 instances) + - `secubox-mirrornet/files/usr/lib/mirrornet/health.sh` (1 instance) + - `secubox-mirrornet/files/usr/lib/mirrornet/identity.sh` (1 instance - for loop fix) + - **Tested:** `mirrorctl status`, `mirror-add`, `mirror-upstream`, `mirror-check`, `mirror-haproxy` all working + - **Deployed:** Both master (192.168.255.1) and clone (192.168.255.156) routers + + +49. **Mesh Blockchain Sync (2026-02-24)** + - Fixed blockchain chain synchronization between mesh nodes. + - **Chain Append Fix:** + - `chain_add_block()`: Uses awk to safely insert new blocks before `] }` ending + - Handles JSON with/without trailing newlines and varying whitespace + - Compacts multi-line blocks to single line for clean insertion + - **Chain Merge Fix:** + - `chain_merge_block()`: Same awk-based approach for remote block merging + - Validates block structure and prev_hash linkage before merging + - **Sync Endpoint Fix:** + - `/api/chain/since/`: Now properly returns only blocks after given hash + - Returns JSON array of blocks (not full chain) + - Supports partial hash matching + - **Sync Function Fix:** + - `sync_with_peer()`: Properly fetches and merges missing blocks + - Uses `chain_merge_block()` for each received block + - Stores block data in blocks directory + - **Verification:** + - Master→Clone sync: Block 70 synced successfully + - Clone→Master sync: Block 69 synced successfully + - Both nodes at height 70 with matching hash + - JSON validity confirmed via Python parser + - **Files Modified:** + - `secubox-core/root/usr/lib/secubox/p2p-mesh.sh` + - `secubox-core/root/www/api/chain` diff --git a/.claude/WIP.md b/.claude/WIP.md index 14751407..4b12a0e3 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -1,6 +1,6 @@ # Work In Progress (Claude) -_Last updated: 2026-02-24 (Service Stability Fixes)_ +_Last updated: 2026-02-24 (ZKP Mesh Authentication)_ > **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches @@ -62,6 +62,52 @@ _Last updated: 2026-02-24 (Service Stability Fixes)_ - Gossip-based exposure config sync via secubox-p2p - Created `luci-app-vortex-dns` dashboard +### Just Completed (2026-02-24) + +- **ZKP Mesh Authentication** — DONE (2026-02-24) + - Zero-Knowledge Proof integration for cryptographic mesh authentication + - Each node has ZKP identity (public graph + secret Hamiltonian cycle) + - New API endpoints: `/api/master-link/zkp-challenge`, `/api/master-link/zkp-verify`, `/api/zkp/graph` + - Shell functions: `ml_zkp_init()`, `ml_zkp_challenge()`, `ml_zkp_verify()`, `ml_zkp_trust_peer()` + - Blockchain acknowledgment via `peer_zkp_verified` block type + - UCI config options: `zkp_enabled`, `zkp_fingerprint`, `zkp_require_on_join`, `zkp_challenge_ttl` + - Tested on master (fingerprint: `7c5ead2b4e4b0106`) + - Files: `master-link.sh` (ZKP functions), 3 new API endpoints + +- **ZKP Join Flow Integration** — DONE (2026-02-24) + - Enhanced `ml_join_request()` to accept and verify ZKP proofs during join + - Enhanced `ml_join_approve()` to auto-fetch and store peer's ZKP graph + - New peer-side `ml_join_with_zkp()` function for ZKP-authenticated joining + - `/api/master-link/join` now accepts `zkp_proof` and `zkp_graph` fields + - When ZKP proof provided: fingerprint = SHA256(graph)[0:16] (ZKP fingerprint) + - Option `zkp_require_on_join` to mandate ZKP for all new joins + - Join requests now store `zkp_verified` and `zkp_proof_hash` fields + - Tested: Clone joined with `zkp_verified: true`, graph auto-stored on approval + +- **LuCI ZKP Dashboard** — DONE (2026-02-24) + - Added ZKP Status section to `luci-app-master-link` Overview tab + - Cards: ZKP Identity (fingerprint), ZKP Tools status, Trusted Peers count + - Color theme: purple gradient for ZKP elements + - Added ZKP badge column to peer table (🔐ZKP vs TOKEN) + - Helper function `zkpBadge()` for visual auth type indicator + +- **MirrorNet Ash Compatibility Fix** — DONE (2026-02-24) + - Fixed process substitution `< <(cmd)` incompatibility with BusyBox ash + - Converted to pipe-based patterns with temp files for variable persistence + - Files fixed: mirror.sh (3), gossip.sh (3), health.sh (1), identity.sh (1) + - Tested: `mirrorctl` CLI fully functional on both routers + - Mirror features working: add service, add upstream, health check, HAProxy config generation + +- **Mesh Blockchain Sync** — DONE (2026-02-24) + - Fixed chain.json append logic for proper JSON structure preservation + - Fixed `/api/chain/since/` endpoint to return only new blocks as array + - `chain_add_block()`: Uses awk to safely insert before closing `] }` + - `chain_merge_block()`: Same awk-based approach for remote block merging + - `sync_with_peer()`: Properly merges blocks into local chain + - Handles JSON with/without trailing newlines and varying whitespace + - Tested bidirectional sync: Master ↔ Clone both at height 70, matching hash + - Files: `p2p-mesh.sh` (chain functions), `/www/api/chain` (endpoint) + ### Just Completed (2026-02-20) - **LuCI VM Manager** — DONE (2026-02-20) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index cde3b8fe..d43961ca 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -421,7 +421,15 @@ "Bash(# Check if OpenWrt toolchain is available ls -la /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/openwrt/)", "Bash(# Create symlink in SDK feeds cd /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/sdk ln -sf ../local-feed/zkp-hamiltonian/openwrt feeds/local/zkp-hamiltonian || true ls -la feeds/local/)", "WebFetch(domain:www.linkedin.com)", - "WebFetch(domain:www.crowdsec.net)" + "WebFetch(domain:www.crowdsec.net)", + "Bash(apt-cache search:*)", + "Bash(musl-gcc:*)", + "Bash(docker run:*)", + "Bash(PATH=/tmp/sdk-x86/openwrt-sdk-24.10.5-x86-64_gcc-13.3.0_musl.Linux-x86_64/staging_dir/toolchain-x86_64_gcc-13.3.0_musl/bin:$PATH STAGING_DIR=/tmp/sdk-x86/openwrt-sdk-24.10.5-x86-64_gcc-13.3.0_musl.Linux-x86_64/staging_dir make:*)", + "Bash(/usr/bin/tail:*)", + "Bash(/usr/bin/make:*)", + "Bash(/tmp/build-zkp-x86.sh:*)", + "Bash(__NEW_LINE_a9089175728efc91__ echo \"\")" ] } } diff --git a/package/secubox/luci-app-master-link/htdocs/luci-static/resources/view/secubox/master-link.js b/package/secubox/luci-app-master-link/htdocs/luci-static/resources/view/secubox/master-link.js index c92109d0..cc92d4b0 100644 --- a/package/secubox/luci-app-master-link/htdocs/luci-static/resources/view/secubox/master-link.js +++ b/package/secubox/luci-app-master-link/htdocs/luci-static/resources/view/secubox/master-link.js @@ -75,6 +75,22 @@ function statusBadge(status) { }, status || 'unknown'); } +function zkpBadge(verified) { + if (verified === true || verified === 'true') { + return E('span', { + 'style': 'display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:9999px;font-size:10px;font-weight:600;color:#fff;background:#8b5cf6;', + 'title': _('Zero-Knowledge Proof verified') + }, [ + E('span', { 'style': 'font-size:12px;' }, '🔐'), + 'ZKP' + ]); + } + return E('span', { + 'style': 'display:inline-block;padding:2px 8px;border-radius:9999px;font-size:10px;font-weight:500;color:#94a3b8;background:#f1f5f9;', + 'title': _('Token-based authentication') + }, 'TOKEN'); +} + function copyText(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(function() { @@ -230,6 +246,76 @@ return view.extend({ statusSection.appendChild(statusGrid); overviewPanel.appendChild(statusSection); + // ZKP Status Section + var zkp = status.zkp || {}; + var zkpSection = E('div', { 'class': 'cbi-section' }, [ + E('h3', { 'class': 'cbi-section-title' }, [ + E('span', {}, _('Zero-Knowledge Proof Authentication')), + zkp.enabled == 1 ? + E('span', { + 'style': 'margin-left:10px;font-size:11px;padding:2px 8px;border-radius:9999px;background:#22c55e;color:#fff;font-weight:600;' + }, _('ENABLED')) : + E('span', { + 'style': 'margin-left:10px;font-size:11px;padding:2px 8px;border-radius:9999px;background:#94a3b8;color:#fff;font-weight:600;' + }, _('DISABLED')) + ]) + ]); + + var zkpGrid = E('div', { + 'style': 'display:flex;gap:20px;flex-wrap:wrap;' + }); + + // ZKP Fingerprint card + zkpGrid.appendChild(E('div', { + 'style': 'flex:1;min-width:200px;background:#faf5ff;padding:15px;border-radius:8px;border-left:4px solid #8b5cf6;' + }, [ + E('div', { 'style': 'font-size:12px;color:#666;margin-bottom:4px;' }, _('ZKP Identity')), + E('div', { 'style': 'display:flex;align-items:center;gap:8px;' }, [ + zkp.has_identity ? + E('code', { 'style': 'font-size:14px;font-weight:600;letter-spacing:0.05em;color:#8b5cf6;' }, + zkp.fingerprint || '-') : + E('span', { 'style': 'color:#94a3b8;font-style:italic;' }, _('Not generated')), + zkp.fingerprint ? E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'padding:2px 8px;font-size:11px;', + 'click': function() { copyText(zkp.fingerprint); } + }, _('Copy')) : E('span') + ]), + E('div', { 'style': 'font-size:11px;color:#94a3b8;margin-top:6px;' }, + zkp.has_identity ? _('Cryptographic identity based on Hamiltonian cycle') : _('Run zkp-init to generate')) + ])); + + // ZKP Tools status + zkpGrid.appendChild(E('div', { + 'style': 'flex:1;min-width:200px;background:#faf5ff;padding:15px;border-radius:8px;border-left:4px solid #a855f7;' + }, [ + E('div', { 'style': 'font-size:12px;color:#666;margin-bottom:4px;' }, _('ZKP Tools')), + E('div', { 'style': 'display:flex;align-items:center;gap:8px;' }, [ + zkp.tools_available ? + E('span', { 'style': 'color:#22c55e;font-weight:600;' }, '✓ ' + _('Installed')) : + E('span', { 'style': 'color:#ef4444;font-weight:600;' }, '✗ ' + _('Not installed')) + ]), + E('div', { 'style': 'font-size:11px;color:#94a3b8;margin-top:6px;' }, + _('zkp_keygen, zkp_prover, zkp_verifier')) + ])); + + // Trusted Peers + zkpGrid.appendChild(E('div', { + 'style': 'flex:1;min-width:200px;background:#faf5ff;padding:15px;border-radius:8px;border-left:4px solid #c084fc;' + }, [ + E('div', { 'style': 'font-size:12px;color:#666;margin-bottom:4px;' }, _('Trusted Peers')), + E('div', {}, [ + E('span', { 'style': 'font-size:20px;font-weight:700;color:#8b5cf6;' }, + String(zkp.trusted_peers || 0)), + E('span', { 'style': 'font-size:11px;color:#666;margin-left:4px;' }, _('peer graphs stored')) + ]), + E('div', { 'style': 'font-size:11px;color:#94a3b8;margin-top:6px;' }, + _('For challenge-response authentication')) + ])); + + zkpSection.appendChild(zkpGrid); + overviewPanel.appendChild(zkpSection); + // Upstream info (for peers/sub-masters) if (status.upstream) { overviewPanel.appendChild(E('div', { 'class': 'cbi-section' }, [ @@ -347,6 +433,7 @@ return view.extend({ E('th', { 'class': 'th' }, _('Hostname')), E('th', { 'class': 'th' }, _('Address')), E('th', { 'class': 'th' }, _('Fingerprint')), + E('th', { 'class': 'th' }, _('Auth')), E('th', { 'class': 'th' }, _('Requested')), E('th', { 'class': 'th' }, _('Status')), E('th', { 'class': 'th' }, _('Actions')) @@ -430,6 +517,7 @@ return view.extend({ E('td', { 'class': 'td' }, peer.hostname || '-'), E('td', { 'class': 'td' }, E('code', { 'style': 'font-size:12px;' }, peer.address || '-')), E('td', { 'class': 'td' }, E('code', { 'style': 'font-size:11px;' }, (peer.fingerprint || '').substring(0, 12) + '...')), + E('td', { 'class': 'td' }, zkpBadge(peer.zkp_verified)), E('td', { 'class': 'td', 'style': 'font-size:12px;' }, formatTime(peer.timestamp)), E('td', { 'class': 'td' }, statusBadge(peer.status)), actionCell diff --git a/package/secubox/secubox-core/root/usr/lib/secubox/p2p-mesh.sh b/package/secubox/secubox-core/root/usr/lib/secubox/p2p-mesh.sh index bfe03344..e52bcafa 100644 --- a/package/secubox/secubox-core/root/usr/lib/secubox/p2p-mesh.sh +++ b/package/secubox/secubox-core/root/usr/lib/secubox/p2p-mesh.sh @@ -138,14 +138,74 @@ EOF ) # Append to chain (using temp file for atomic update) + # JSON structure ends with: ...} ] } (with possible whitespace) + # Use awk to replace the last ] } with ,newblock ] } local tmp_chain="$MESH_DIR/tmp/chain_$$.json" - jsonfilter -i "$CHAIN_FILE" -e '@' | sed 's/\]$//' > "$tmp_chain" - echo ",$block_record]}" >> "$tmp_chain" - mv "$tmp_chain" "$CHAIN_FILE" + # Compact the block to single line (escape special chars for awk) + local compact_block=$(echo "$block_record" | tr '\n' ' ' | tr -s ' ' | sed 's/&/\\&/g') + + # Use awk to replace last occurrence of ] followed by whitespace and } + # RS="" reads entire file, gsub replaces pattern + awk -v block="$compact_block" ' + BEGIN { RS=""; ORS="" } + { + # Find last ] } pattern and insert block before it + n = match($0, /\][ \t\n]*\}[ \t\n]*$/) + if (n > 0) { + print substr($0, 1, n-1) "," block "]}\n" + } else { + print $0 + } + } + ' "$CHAIN_FILE" > "$tmp_chain" + + mv "$tmp_chain" "$CHAIN_FILE" echo "$block_hash" } +# Merge a remote block into the local chain (for sync) +chain_merge_block() { + local block_json="$1" + + # Validate block structure + local block_index=$(echo "$block_json" | jsonfilter -e '@.index' 2>/dev/null) + local block_hash=$(echo "$block_json" | jsonfilter -e '@.hash' 2>/dev/null) + local block_prev=$(echo "$block_json" | jsonfilter -e '@.prev_hash' 2>/dev/null) + + [ -z "$block_index" ] && return 1 + [ -z "$block_hash" ] && return 1 + + # Check prev_hash matches our tip + local local_hash=$(chain_get_hash) + if [ "$block_prev" != "$local_hash" ]; then + echo "Block prev_hash mismatch: expected=$local_hash got=$block_prev" >&2 + return 1 + fi + + # Append block to chain + local tmp_chain="$MESH_DIR/tmp/chain_merge_$$.json" + + # Compact block to single line (escape special chars for awk) + local compact_block=$(echo "$block_json" | tr '\n' ' ' | tr -s ' ' | sed 's/&/\\&/g') + + # Use awk to insert block before last ] } + awk -v block="$compact_block" ' + BEGIN { RS=""; ORS="" } + { + n = match($0, /\][ \t\n]*\}[ \t\n]*$/) + if (n > 0) { + print substr($0, 1, n-1) "," block "]}\n" + } else { + print $0 + } + } + ' "$CHAIN_FILE" > "$tmp_chain" + + mv "$tmp_chain" "$CHAIN_FILE" + echo "Merged block $block_index ($block_hash)" +} + chain_verify() { # Verify chain integrity local prev_hash="0000000000000000000000000000000000000000000000000000000000000000" @@ -326,22 +386,34 @@ sync_with_peer() { # Get missing blocks from peer local missing=$(curl -s "http://$peer_addr:$peer_port/api/chain/since/$local_hash" 2>/dev/null) - if [ -n "$missing" ]; then - echo "$missing" | jsonfilter -e '@[*]' | while read block; do - local block_hash=$(echo "$block" | jsonfilter -e '@.hash') - local block_type=$(echo "$block" | jsonfilter -e '@.type') + if [ -n "$missing" ] && [ "$missing" != "[]" ]; then + # Store blocks in temp file for ordered processing + local tmp_blocks="$MESH_DIR/tmp/sync_blocks_$$.tmp" + echo "$missing" | jsonfilter -e '@[*]' > "$tmp_blocks" 2>/dev/null - # Fetch and store block data - if ! block_exists "$block_hash"; then - curl -s "http://$peer_addr:$peer_port/api/block/$block_hash" -o "$MESH_DIR/tmp/$block_hash" - if [ -f "$MESH_DIR/tmp/$block_hash" ]; then - block_store_file "$MESH_DIR/tmp/$block_hash" - rm "$MESH_DIR/tmp/$block_hash" + local synced_count=0 + while read -r block; do + [ -z "$block" ] && continue + local block_hash=$(echo "$block" | jsonfilter -e '@.hash' 2>/dev/null) + + # Merge block into local chain + if chain_merge_block "$block"; then + synced_count=$((synced_count + 1)) + + # Also store block data if present + if [ -n "$block_hash" ] && ! block_exists "$block_hash"; then + local block_data=$(echo "$block" | jsonfilter -e '@.data' 2>/dev/null) + if [ -n "$block_data" ]; then + echo "$block_data" | block_store "$block_hash" + fi fi fi - done + done < "$tmp_blocks" + rm -f "$tmp_blocks" - echo "Synced $(echo "$missing" | jsonfilter -e '@[*]' | wc -l) blocks from $peer_addr" + echo "Synced $synced_count blocks from $peer_addr" + else + echo "No new blocks from $peer_addr" fi } diff --git a/package/secubox/secubox-core/root/www/api/chain b/package/secubox/secubox-core/root/www/api/chain index 4588c201..34d887f0 100755 --- a/package/secubox/secubox-core/root/www/api/chain +++ b/package/secubox/secubox-core/root/www/api/chain @@ -25,9 +25,39 @@ case "$PATH_INFO" in SINCE_HASH=${PATH_INFO#/since/} # Return blocks since given hash (for sync) if [ -f "$CHAIN_FILE" ]; then - cat "$CHAIN_FILE" + # Find index of the hash and return subsequent blocks as JSON array + FOUND_INDEX="" + TOTAL_BLOCKS=$(jsonfilter -i "$CHAIN_FILE" -e '@.blocks[*]' 2>/dev/null | wc -l) + + # Find the block with matching hash + for i in $(seq 0 $((TOTAL_BLOCKS - 1))); do + BLOCK_HASH=$(jsonfilter -i "$CHAIN_FILE" -e "@.blocks[$i].hash" 2>/dev/null) + if [ "$BLOCK_HASH" = "$SINCE_HASH" ] || echo "$BLOCK_HASH" | grep -q "^$SINCE_HASH"; then + FOUND_INDEX=$i + break + fi + done + + if [ -n "$FOUND_INDEX" ]; then + # Return blocks after the found index as array + START_INDEX=$((FOUND_INDEX + 1)) + echo "[" + FIRST=1 + for i in $(seq $START_INDEX $((TOTAL_BLOCKS - 1))); do + BLOCK=$(jsonfilter -i "$CHAIN_FILE" -e "@.blocks[$i]" 2>/dev/null) + if [ -n "$BLOCK" ]; then + [ "$FIRST" = "1" ] || echo "," + echo "$BLOCK" + FIRST=0 + fi + done + echo "]" + else + # Hash not found, return empty array + echo "[]" + fi else - echo "{\"blocks\":[]}" + echo "[]" fi ;; *) diff --git a/package/secubox/secubox-master-link/files/etc/config/master-link b/package/secubox/secubox-master-link/files/etc/config/master-link index 85df49d2..d950711c 100644 --- a/package/secubox/secubox-master-link/files/etc/config/master-link +++ b/package/secubox/secubox-master-link/files/etc/config/master-link @@ -9,3 +9,8 @@ config master-link 'main' option token_ttl '3600' option auto_approve '0' option ipk_path '/www/secubox-feed/secubox-master-link_*.ipk' + # ZKP Authentication + option zkp_enabled '1' + option zkp_fingerprint '' + option zkp_require_on_join '0' + option zkp_challenge_ttl '30' diff --git a/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh b/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh index d07c47cb..bf5f2677 100644 --- a/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh +++ b/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh @@ -15,10 +15,269 @@ ML_TOKENS_DIR="$ML_DIR/tokens" ML_REQUESTS_DIR="$ML_DIR/requests" MESH_PORT="${MESH_PORT:-7331}" +# ZKP Configuration +ZKP_DIR="/etc/secubox/zkp" +ZKP_IDENTITY_GRAPH="$ZKP_DIR/identity.graph" +ZKP_IDENTITY_KEY="$ZKP_DIR/identity.key" +ZKP_PEERS_DIR="$ZKP_DIR/peers" +ZKP_CHALLENGES_DIR="/tmp/zkp_challenges" +ZKP_CHALLENGE_TTL="${ZKP_CHALLENGE_TTL:-30}" + ml_init() { mkdir -p "$ML_DIR" "$ML_TOKENS_DIR" "$ML_REQUESTS_DIR" factory_init_keys >/dev/null 2>&1 mesh_init >/dev/null 2>&1 + ml_zkp_init >/dev/null 2>&1 +} + +# ============================================================================ +# ZKP Identity Management +# ============================================================================ + +# Initialize ZKP identity (generate keypair if not exists) +ml_zkp_init() { + # Check if ZKP tools are available + command -v zkp_keygen >/dev/null 2>&1 || return 0 + + mkdir -p "$ZKP_DIR" "$ZKP_PEERS_DIR" "$ZKP_CHALLENGES_DIR" + + # Generate identity if not exists + if [ ! -f "$ZKP_IDENTITY_GRAPH" ] || [ ! -f "$ZKP_IDENTITY_KEY" ]; then + logger -t master-link "Generating ZKP identity keypair..." + local tmpprefix="/tmp/zkp_init_$$" + if zkp_keygen -n 50 -r 1.0 -o "$tmpprefix" >/dev/null 2>&1; then + mv "${tmpprefix}.graph" "$ZKP_IDENTITY_GRAPH" + mv "${tmpprefix}.key" "$ZKP_IDENTITY_KEY" + chmod 644 "$ZKP_IDENTITY_GRAPH" + chmod 600 "$ZKP_IDENTITY_KEY" + logger -t master-link "ZKP identity generated" + else + logger -t master-link "ZKP keygen failed" + rm -f "${tmpprefix}.graph" "${tmpprefix}.key" + return 1 + fi + fi + + # Derive and store ZKP fingerprint + if [ -f "$ZKP_IDENTITY_GRAPH" ]; then + local zkp_fp=$(sha256sum "$ZKP_IDENTITY_GRAPH" | cut -c1-16) + uci -q set master-link.main.zkp_fingerprint="$zkp_fp" + uci -q set master-link.main.zkp_enabled="1" + uci commit master-link + fi + + return 0 +} + +# Get ZKP status +ml_zkp_status() { + local zkp_enabled=$(uci -q get master-link.main.zkp_enabled) + local zkp_fp=$(uci -q get master-link.main.zkp_fingerprint) + local has_tools="false" + local has_identity="false" + local peer_count=0 + + command -v zkp_keygen >/dev/null 2>&1 && has_tools="true" + [ -f "$ZKP_IDENTITY_GRAPH" ] && [ -f "$ZKP_IDENTITY_KEY" ] && has_identity="true" + [ -d "$ZKP_PEERS_DIR" ] && peer_count=$(ls -1 "$ZKP_PEERS_DIR"/*.graph 2>/dev/null | wc -l) + + cat <<-EOF + { + "enabled": ${zkp_enabled:-0}, + "tools_available": $has_tools, + "has_identity": $has_identity, + "fingerprint": "${zkp_fp:-}", + "trusted_peers": $peer_count + } + EOF +} + +# Generate ZKP challenge for authentication +ml_zkp_challenge() { + mkdir -p "$ZKP_CHALLENGES_DIR" + + local challenge_id=$(head -c 16 /dev/urandom 2>/dev/null | sha256sum | cut -c1-32) + local timestamp=$(date +%s) + local expires=$((timestamp + ZKP_CHALLENGE_TTL)) + + # Store challenge + echo "{\"id\":\"$challenge_id\",\"timestamp\":$timestamp,\"expires\":$expires}" > "$ZKP_CHALLENGES_DIR/${challenge_id}.json" + + # Cleanup old challenges + find "$ZKP_CHALLENGES_DIR" -name "*.json" -mmin +5 -delete 2>/dev/null + + cat <<-EOF + { + "challenge_id": "$challenge_id", + "timestamp": $timestamp, + "expires": $expires, + "ttl": $ZKP_CHALLENGE_TTL + } + EOF +} + +# Generate ZKP proof for authentication +ml_zkp_prove() { + local challenge_id="$1" + + # Check identity exists + if [ ! -f "$ZKP_IDENTITY_GRAPH" ] || [ ! -f "$ZKP_IDENTITY_KEY" ]; then + echo '{"success":false,"error":"no_identity"}' + return 1 + fi + + # Check tools available + command -v zkp_prover >/dev/null 2>&1 || { + echo '{"success":false,"error":"no_zkp_tools"}' + return 1 + } + + local proof_file="/tmp/zkp_proof_$$.proof" + + # Generate proof + if zkp_prover -g "$ZKP_IDENTITY_GRAPH" -k "$ZKP_IDENTITY_KEY" -o "$proof_file" >/dev/null 2>&1; then + local proof_b64=$(base64 -w 0 "$proof_file") + local proof_hash=$(sha256sum "$proof_file" | cut -c1-16) + local proof_size=$(stat -c %s "$proof_file" 2>/dev/null || echo 0) + rm -f "$proof_file" + + local zkp_fp=$(uci -q get master-link.main.zkp_fingerprint) + + cat <<-EOF + { + "success": true, + "fingerprint": "$zkp_fp", + "challenge_id": "$challenge_id", + "proof": "$proof_b64", + "proof_hash": "$proof_hash", + "proof_size": $proof_size + } + EOF + else + rm -f "$proof_file" + echo '{"success":false,"error":"proof_generation_failed"}' + return 1 + fi +} + +# Verify ZKP proof from peer +ml_zkp_verify() { + local peer_fp="$1" + local proof_b64="$2" + local challenge_id="$3" + + # Validate challenge + local challenge_file="$ZKP_CHALLENGES_DIR/${challenge_id}.json" + if [ -n "$challenge_id" ] && [ -f "$challenge_file" ]; then + local expires=$(jsonfilter -i "$challenge_file" -e '@.expires' 2>/dev/null) + local now=$(date +%s) + if [ -n "$expires" ] && [ "$now" -gt "$expires" ]; then + rm -f "$challenge_file" + echo '{"success":false,"result":"REJECT","error":"challenge_expired"}' + return 1 + fi + fi + + # Check peer graph exists + local graph_file="$ZKP_PEERS_DIR/${peer_fp}.graph" + if [ ! -f "$graph_file" ]; then + echo '{"success":false,"result":"REJECT","error":"unknown_peer"}' + return 1 + fi + + # Check tools available + command -v zkp_verifier >/dev/null 2>&1 || { + echo '{"success":false,"result":"REJECT","error":"no_zkp_tools"}' + return 1 + } + + # Decode and verify proof + local proof_file="/tmp/zkp_verify_$$.proof" + echo "$proof_b64" | base64 -d > "$proof_file" 2>/dev/null + + local result=$(zkp_verifier -g "$graph_file" -p "$proof_file" 2>&1) + local rc=$? + local proof_hash=$(sha256sum "$proof_file" 2>/dev/null | cut -c1-16) + rm -f "$proof_file" + + # Clean up challenge after use + [ -f "$challenge_file" ] && rm -f "$challenge_file" + + local my_fp=$(factory_fingerprint 2>/dev/null) + local timestamp=$(date +%s) + + if [ "$result" = "ACCEPT" ]; then + # Record to blockchain + chain_add_block "peer_zkp_verified" \ + "{\"peer_fp\":\"$peer_fp\",\"proof_hash\":\"$proof_hash\",\"challenge_id\":\"$challenge_id\",\"result\":\"ACCEPT\",\"verified_by\":\"$my_fp\"}" \ + "$(echo "zkp_verify:${peer_fp}:${proof_hash}:${timestamp}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1 + + logger -t master-link "ZKP verified: peer=$peer_fp result=ACCEPT" + + cat <<-EOF + { + "success": true, + "result": "ACCEPT", + "peer_fp": "$peer_fp", + "proof_hash": "$proof_hash", + "verified_at": $timestamp, + "verified_by": "$my_fp" + } + EOF + else + logger -t master-link "ZKP verification failed: peer=$peer_fp result=$result" + cat <<-EOF + { + "success": true, + "result": "REJECT", + "peer_fp": "$peer_fp", + "error": "verification_failed" + } + EOF + return 1 + fi +} + +# Store peer's ZKP graph (called during approval) +ml_zkp_trust_peer() { + local peer_fp="$1" + local peer_addr="$2" + + mkdir -p "$ZKP_PEERS_DIR" + + # Fetch peer's graph + local graph_b64=$(curl -s --connect-timeout 5 "http://${peer_addr}:${MESH_PORT}/api/zkp/graph" 2>/dev/null) + if [ -z "$graph_b64" ]; then + logger -t master-link "Failed to fetch ZKP graph from $peer_addr" + return 1 + fi + + # Decode and verify fingerprint + local tmp_graph="/tmp/zkp_peer_$$.graph" + echo "$graph_b64" | base64 -d > "$tmp_graph" 2>/dev/null + + local fetched_fp=$(sha256sum "$tmp_graph" | cut -c1-16) + if [ "$fetched_fp" != "$peer_fp" ]; then + logger -t master-link "ZKP fingerprint mismatch: expected=$peer_fp got=$fetched_fp" + rm -f "$tmp_graph" + return 1 + fi + + # Store trusted peer graph + mv "$tmp_graph" "$ZKP_PEERS_DIR/${peer_fp}.graph" + chmod 644 "$ZKP_PEERS_DIR/${peer_fp}.graph" + + logger -t master-link "Trusted ZKP peer: $peer_fp" + return 0 +} + +# Get own public graph (base64 encoded) +ml_zkp_get_graph() { + if [ -f "$ZKP_IDENTITY_GRAPH" ]; then + base64 -w 0 "$ZKP_IDENTITY_GRAPH" + else + echo "" + fi } # ============================================================================ @@ -282,11 +541,14 @@ ml_token_is_auto_approve() { # ============================================================================ # Handle join request from new node +# Enhanced with ZKP authentication support ml_join_request() { local token="$1" local peer_fp="$2" local peer_addr="$3" local peer_hostname="${4:-unknown}" + local zkp_proof_b64="$5" + local zkp_graph_b64="$6" # Validate token local validation=$(ml_token_validate "$token") @@ -298,9 +560,62 @@ ml_join_request() { fi local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1) - - # Store join request local now=$(date +%s) + local zkp_verified="false" + local zkp_proof_hash="" + + # Check if ZKP is required for join + local zkp_require=$(uci -q get master-link.main.zkp_require_on_join) + local zkp_enabled=$(uci -q get master-link.main.zkp_enabled) + + # ZKP verification if proof provided + if [ -n "$zkp_proof_b64" ] && [ -n "$zkp_graph_b64" ]; then + # Check ZKP tools available + if command -v zkp_verifier >/dev/null 2>&1; then + # First, verify peer fingerprint matches graph hash + local tmp_graph="/tmp/zkp_join_$$.graph" + local tmp_proof="/tmp/zkp_join_$$.proof" + + echo "$zkp_graph_b64" | base64 -d > "$tmp_graph" 2>/dev/null + echo "$zkp_proof_b64" | base64 -d > "$tmp_proof" 2>/dev/null + + local graph_fp=$(sha256sum "$tmp_graph" 2>/dev/null | cut -c1-16) + zkp_proof_hash=$(sha256sum "$tmp_proof" 2>/dev/null | cut -c1-16) + + if [ "$graph_fp" = "$peer_fp" ]; then + # Fingerprint matches - verify proof + local verify_result=$(zkp_verifier -g "$tmp_graph" -p "$tmp_proof" 2>&1) + if [ "$verify_result" = "ACCEPT" ]; then + zkp_verified="true" + logger -t master-link "ZKP join proof verified for $peer_fp" + + # Store peer graph for future verifications + mkdir -p "$ZKP_PEERS_DIR" + mv "$tmp_graph" "$ZKP_PEERS_DIR/${peer_fp}.graph" + chmod 644 "$ZKP_PEERS_DIR/${peer_fp}.graph" + else + logger -t master-link "ZKP join proof REJECTED for $peer_fp: $verify_result" + rm -f "$tmp_graph" + fi + else + logger -t master-link "ZKP fingerprint mismatch: expected=$peer_fp got=$graph_fp" + rm -f "$tmp_graph" + fi + + rm -f "$tmp_proof" + else + logger -t master-link "ZKP tools not available, skipping proof verification" + fi + fi + + # Reject if ZKP required but not verified + if [ "$zkp_require" = "1" ] && [ "$zkp_enabled" = "1" ] && [ "$zkp_verified" != "true" ]; then + logger -t master-link "Rejecting join: ZKP required but not verified ($peer_fp)" + echo '{"success":false,"error":"zkp_required","message":"ZKP proof required for join"}' + return 1 + fi + + # Store join request with ZKP status cat > "$ML_REQUESTS_DIR/${peer_fp}.json" <<-EOF { "fingerprint": "$peer_fp", @@ -308,16 +623,18 @@ ml_join_request() { "hostname": "$peer_hostname", "token_hash": "$token_hash", "timestamp": $now, + "zkp_verified": $zkp_verified, + "zkp_proof_hash": "$zkp_proof_hash", "status": "pending" } EOF - # Add join_request block to chain + # Add join_request block to chain (with ZKP status) chain_add_block "join_request" \ - "{\"fp\":\"$peer_fp\",\"addr\":\"$peer_addr\",\"hostname\":\"$peer_hostname\",\"token_hash\":\"$token_hash\"}" \ + "{\"fp\":\"$peer_fp\",\"addr\":\"$peer_addr\",\"hostname\":\"$peer_hostname\",\"token_hash\":\"$token_hash\",\"zkp_verified\":$zkp_verified}" \ "$(echo "join_request:${peer_fp}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1 - logger -t master-link "Join request from $peer_hostname ($peer_fp) at $peer_addr" + logger -t master-link "Join request from $peer_hostname ($peer_fp) at $peer_addr [zkp=$zkp_verified]" # Check auto-approve: either global setting or token-specific (clone tokens) local auto_approve=$(uci -q get master-link.main.auto_approve) @@ -329,10 +646,11 @@ ml_join_request() { return $? fi - echo "{\"success\":true,\"status\":\"pending\",\"message\":\"Join request queued for approval\"}" + echo "{\"success\":true,\"status\":\"pending\",\"zkp_verified\":$zkp_verified,\"message\":\"Join request queued for approval\"}" } # Approve a peer join request +# Enhanced with ZKP graph fetching on approval ml_join_approve() { local peer_fp="$1" @@ -351,7 +669,10 @@ ml_join_approve() { local peer_hostname=$(jsonfilter -i "$request_file" -e '@.hostname' 2>/dev/null) local token_hash=$(jsonfilter -i "$request_file" -e '@.token_hash' 2>/dev/null) local orig_ts=$(jsonfilter -i "$request_file" -e '@.timestamp' 2>/dev/null) + local zkp_verified=$(jsonfilter -i "$request_file" -e '@.zkp_verified' 2>/dev/null) + local zkp_proof_hash=$(jsonfilter -i "$request_file" -e '@.zkp_proof_hash' 2>/dev/null) [ -z "$orig_ts" ] && orig_ts=0 + [ -z "$zkp_verified" ] && zkp_verified="false" local now=$(date +%s) local my_fp=$(factory_fingerprint 2>/dev/null) local my_depth=$(uci -q get master-link.main.depth) @@ -364,7 +685,22 @@ ml_join_approve() { # Add peer to mesh peer_add "$peer_addr" "$MESH_PORT" "$peer_fp" >/dev/null 2>&1 - # Update request status + # Fetch peer's ZKP graph if not already stored (from join verification) + local zkp_enabled=$(uci -q get master-link.main.zkp_enabled) + local zkp_graph_stored="false" + if [ "$zkp_enabled" = "1" ] && [ ! -f "$ZKP_PEERS_DIR/${peer_fp}.graph" ]; then + logger -t master-link "Fetching ZKP graph from approved peer $peer_fp" + if ml_zkp_trust_peer "$peer_fp" "$peer_addr" >/dev/null 2>&1; then + zkp_graph_stored="true" + logger -t master-link "Stored ZKP graph for peer $peer_fp" + else + logger -t master-link "Failed to fetch ZKP graph from $peer_fp (ZKP auth won't work for this peer)" + fi + elif [ -f "$ZKP_PEERS_DIR/${peer_fp}.graph" ]; then + zkp_graph_stored="true" + fi + + # Update request status with ZKP info cat > "$request_file" <<-EOF { "fingerprint": "$peer_fp", @@ -375,6 +711,9 @@ ml_join_approve() { "approved_at": $now, "approved_by": "$my_fp", "depth": $peer_depth, + "zkp_verified": $zkp_verified, + "zkp_proof_hash": "$zkp_proof_hash", + "zkp_graph_stored": $zkp_graph_stored, "status": "approved" } EOF @@ -391,15 +730,15 @@ ml_join_approve() { fi done - # Add peer_approved block to chain + # Add peer_approved block to chain (with ZKP status) chain_add_block "peer_approved" \ - "{\"fp\":\"$peer_fp\",\"addr\":\"$peer_addr\",\"depth\":$peer_depth,\"approved_by\":\"$my_fp\"}" \ + "{\"fp\":\"$peer_fp\",\"addr\":\"$peer_addr\",\"depth\":$peer_depth,\"approved_by\":\"$my_fp\",\"zkp_verified\":$zkp_verified}" \ "$(echo "peer_approved:${peer_fp}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1 # Sync chain with new peer gossip_sync >/dev/null 2>&1 & - logger -t master-link "Peer approved: $peer_hostname ($peer_fp) at depth $peer_depth" + logger -t master-link "Peer approved: $peer_hostname ($peer_fp) at depth $peer_depth [zkp_verified=$zkp_verified]" cat <<-EOF { @@ -408,6 +747,8 @@ ml_join_approve() { "address": "$peer_addr", "hostname": "$peer_hostname", "depth": $peer_depth, + "zkp_verified": $zkp_verified, + "zkp_graph_stored": $zkp_graph_stored, "status": "approved" } EOF @@ -818,6 +1159,16 @@ ml_status() { local hostname=$(uci -q get system.@system[0].hostname 2>/dev/null || hostname) + # ZKP status + local zkp_enabled=$(uci -q get master-link.main.zkp_enabled) + local zkp_fp=$(uci -q get master-link.main.zkp_fingerprint) + local zkp_tools="false" + local zkp_identity="false" + local zkp_peers=0 + command -v zkp_keygen >/dev/null 2>&1 && zkp_tools="true" + [ -f "$ZKP_IDENTITY_GRAPH" ] && [ -f "$ZKP_IDENTITY_KEY" ] && zkp_identity="true" + [ -d "$ZKP_PEERS_DIR" ] && zkp_peers=$(ls -1 "$ZKP_PEERS_DIR"/*.graph 2>/dev/null | wc -l) + cat <<-EOF { "enabled": $enabled, @@ -835,7 +1186,14 @@ ml_status() { "total": $((pending + approved + rejected)) }, "active_tokens": $active_tokens, - "chain_height": $chain_height + "chain_height": $chain_height, + "zkp": { + "enabled": ${zkp_enabled:-0}, + "fingerprint": "${zkp_fp:-}", + "tools_available": $zkp_tools, + "has_identity": $zkp_identity, + "trusted_peers": $zkp_peers + } } EOF } @@ -941,6 +1299,98 @@ ml_check_local_auth() { return 1 } +# ============================================================================ +# Peer-side Join with ZKP +# ============================================================================ + +# Send a ZKP-authenticated join request to master +# Called by a node wanting to join a mesh +ml_join_with_zkp() { + local master_addr="$1" + local token="$2" + + [ -z "$master_addr" ] || [ -z "$token" ] && { + echo '{"success":false,"error":"missing_args","usage":"ml_join_with_zkp "}' + return 1 + } + + # Initialize ZKP if not already done + ml_zkp_init >/dev/null 2>&1 + + local my_hostname=$(uci -q get system.@system[0].hostname 2>/dev/null || hostname) + local my_addr=$(uci -q get network.lan.ipaddr) + [ -z "$my_addr" ] && my_addr=$(ip -4 addr show br-lan 2>/dev/null | grep -oP 'inet \K[0-9.]+' | head -1) + + # Prepare ZKP proof if available + local zkp_proof_b64="" + local zkp_graph_b64="" + local my_fp="" + + if [ -f "$ZKP_IDENTITY_GRAPH" ] && [ -f "$ZKP_IDENTITY_KEY" ] && command -v zkp_prover >/dev/null 2>&1; then + local proof_file="/tmp/zkp_join_proof_$$.proof" + if zkp_prover -g "$ZKP_IDENTITY_GRAPH" -k "$ZKP_IDENTITY_KEY" -o "$proof_file" >/dev/null 2>&1; then + zkp_proof_b64=$(base64 -w 0 "$proof_file") + zkp_graph_b64=$(base64 -w 0 "$ZKP_IDENTITY_GRAPH") + # Use ZKP fingerprint (graph hash) when ZKP is available + my_fp=$(sha256sum "$ZKP_IDENTITY_GRAPH" | cut -c1-16) + rm -f "$proof_file" + logger -t master-link "Generated ZKP proof for join request (zkp_fp=$my_fp)" + else + logger -t master-link "ZKP proof generation failed, joining without ZKP" + rm -f "$proof_file" + my_fp=$(factory_fingerprint 2>/dev/null) + fi + else + logger -t master-link "No ZKP identity or tools, joining without ZKP" + my_fp=$(factory_fingerprint 2>/dev/null) + fi + + # Build JSON request body + local body="{\"token\":\"$token\",\"fingerprint\":\"$my_fp\",\"hostname\":\"$my_hostname\",\"address\":\"$my_addr\"" + + if [ -n "$zkp_proof_b64" ]; then + body="${body},\"zkp_proof\":\"$zkp_proof_b64\",\"zkp_graph\":\"$zkp_graph_b64\"" + fi + + body="${body}}" + + # Send join request + local response=$(curl -s --connect-timeout 10 -X POST \ + "http://${master_addr}:${MESH_PORT}/api/master-link/join" \ + -H "Content-Type: application/json" \ + -d "$body" 2>/dev/null) + + if [ -z "$response" ]; then + echo '{"success":false,"error":"connection_failed"}' + return 1 + fi + + # Check if approved, store upstream + local status=$(echo "$response" | jsonfilter -e '@.status' 2>/dev/null) + local success=$(echo "$response" | jsonfilter -e '@.success' 2>/dev/null) + local zkp_verified=$(echo "$response" | jsonfilter -e '@.zkp_verified' 2>/dev/null) + + if [ "$success" = "true" ]; then + if [ "$status" = "approved" ]; then + # Auto-approved - configure as peer + uci -q set master-link.main.role='peer' + uci -q set master-link.main.upstream="$master_addr" + local depth=$(echo "$response" | jsonfilter -e '@.depth' 2>/dev/null) + [ -n "$depth" ] && uci -q set master-link.main.depth="$depth" + uci commit master-link + + # Fetch master's ZKP graph for mutual authentication + ml_zkp_trust_peer "$(echo "$response" | jsonfilter -e '@.approved_by' 2>/dev/null || echo 'master')" "$master_addr" >/dev/null 2>&1 + + logger -t master-link "Joined mesh as peer of $master_addr [zkp=$zkp_verified]" + else + logger -t master-link "Join request pending approval at $master_addr" + fi + fi + + echo "$response" +} + # ============================================================================ # Main CLI # ============================================================================ @@ -989,7 +1439,13 @@ case "${1:-}" in echo "{\"registered\":true,\"token_hash\":\"$token_hash\",\"expires\":$expires}" ;; join-request) - ml_join_request "$2" "$3" "$4" "$5" + # Usage: join-request [hostname] [zkp_proof] [zkp_graph] + ml_join_request "$2" "$3" "$4" "$5" "$6" "$7" + ;; + join-with-zkp) + # Join a mesh with ZKP authentication (peer-side command) + # Usage: master-link.sh join-with-zkp + ml_join_with_zkp "$2" "$3" ;; join-approve) ml_join_approve "$2" @@ -1019,6 +1475,29 @@ case "${1:-}" in ml_init echo "Master-link initialized" ;; + # ZKP commands + zkp-init) + ml_zkp_init + echo "ZKP identity initialized" + ;; + zkp-status) + ml_zkp_status + ;; + zkp-challenge) + ml_zkp_challenge + ;; + zkp-prove) + ml_zkp_prove "$2" + ;; + zkp-verify) + ml_zkp_verify "$2" "$3" "$4" + ;; + zkp-graph) + ml_zkp_get_graph + ;; + zkp-trust-peer) + ml_zkp_trust_peer "$2" "$3" + ;; *) # Sourced as library - do nothing : diff --git a/package/secubox/secubox-master-link/files/www/api/master-link/join b/package/secubox/secubox-master-link/files/www/api/master-link/join index d996c38f..d1902ca1 100644 --- a/package/secubox/secubox-master-link/files/www/api/master-link/join +++ b/package/secubox/secubox-master-link/files/www/api/master-link/join @@ -30,6 +30,10 @@ fingerprint=$(echo "$input" | jsonfilter -e '@.fingerprint' 2>/dev/null) address=$(echo "$input" | jsonfilter -e '@.address' 2>/dev/null) peer_hostname=$(echo "$input" | jsonfilter -e '@.hostname' 2>/dev/null) +# ZKP fields (optional unless zkp_require_on_join=1) +zkp_proof=$(echo "$input" | jsonfilter -e '@.zkp_proof' 2>/dev/null) +zkp_graph=$(echo "$input" | jsonfilter -e '@.zkp_graph' 2>/dev/null) + # Use REMOTE_ADDR as fallback for address [ -z "$address" ] && address="$REMOTE_ADDR" @@ -38,4 +42,4 @@ if [ -z "$token" ] || [ -z "$fingerprint" ]; then exit 0 fi -ml_join_request "$token" "$fingerprint" "$address" "$peer_hostname" +ml_join_request "$token" "$fingerprint" "$address" "$peer_hostname" "$zkp_proof" "$zkp_graph" diff --git a/package/secubox/secubox-master-link/files/www/api/master-link/zkp-challenge b/package/secubox/secubox-master-link/files/www/api/master-link/zkp-challenge new file mode 100644 index 00000000..12b5ae7e --- /dev/null +++ b/package/secubox/secubox-master-link/files/www/api/master-link/zkp-challenge @@ -0,0 +1,28 @@ +#!/bin/sh +# Master-Link API - ZKP Challenge Generation +# GET /api/master-link/zkp-challenge +# Returns: challenge_id and timestamp for ZKP authentication + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "Access-Control-Allow-Methods: GET, OPTIONS" +echo "Access-Control-Allow-Headers: Content-Type" +echo "" + +# Handle CORS preflight +if [ "$REQUEST_METHOD" = "OPTIONS" ]; then + exit 0 +fi + +# Load library +. /usr/lib/secubox/master-link.sh >/dev/null 2>&1 + +# Check if ZKP is enabled +zkp_enabled=$(uci -q get master-link.main.zkp_enabled) +if [ "$zkp_enabled" != "1" ]; then + echo '{"error":"zkp_disabled"}' + exit 0 +fi + +# Generate challenge +ml_zkp_challenge diff --git a/package/secubox/secubox-master-link/files/www/api/master-link/zkp-verify b/package/secubox/secubox-master-link/files/www/api/master-link/zkp-verify new file mode 100644 index 00000000..913352b7 --- /dev/null +++ b/package/secubox/secubox-master-link/files/www/api/master-link/zkp-verify @@ -0,0 +1,49 @@ +#!/bin/sh +# Master-Link API - ZKP Proof Verification +# POST /api/master-link/zkp-verify +# Body: {"fingerprint": "", "challenge_id": "", "proof": ""} +# Returns: {"result": "ACCEPT|REJECT", "verified_at": } + +echo "Content-Type: application/json" +echo "Access-Control-Allow-Origin: *" +echo "Access-Control-Allow-Methods: POST, OPTIONS" +echo "Access-Control-Allow-Headers: Content-Type" +echo "" + +# Handle CORS preflight +if [ "$REQUEST_METHOD" = "OPTIONS" ]; then + exit 0 +fi + +# Load library +. /usr/lib/secubox/master-link.sh >/dev/null 2>&1 + +# Check if ZKP is enabled +zkp_enabled=$(uci -q get master-link.main.zkp_enabled) +if [ "$zkp_enabled" != "1" ]; then + echo '{"error":"zkp_disabled"}' + exit 0 +fi + +# Only accept POST +if [ "$REQUEST_METHOD" != "POST" ]; then + echo '{"error":"method_not_allowed"}' + exit 0 +fi + +# Read request body +read -r input + +# Parse fields +fingerprint=$(echo "$input" | jsonfilter -e '@.fingerprint' 2>/dev/null) +challenge_id=$(echo "$input" | jsonfilter -e '@.challenge_id' 2>/dev/null) +proof=$(echo "$input" | jsonfilter -e '@.proof' 2>/dev/null) + +# Validate required fields +if [ -z "$fingerprint" ] || [ -z "$proof" ]; then + echo '{"error":"missing_required_fields","required":["fingerprint","proof"]}' + exit 0 +fi + +# Verify proof +ml_zkp_verify "$fingerprint" "$proof" "$challenge_id" diff --git a/package/secubox/secubox-master-link/files/www/api/zkp/graph b/package/secubox/secubox-master-link/files/www/api/zkp/graph new file mode 100644 index 00000000..76b3d5c6 --- /dev/null +++ b/package/secubox/secubox-master-link/files/www/api/zkp/graph @@ -0,0 +1,21 @@ +#!/bin/sh +# ZKP API - Get node's public graph +# GET /api/zkp/graph +# Returns: base64-encoded graph for ZKP authentication + +echo "Content-Type: text/plain" +echo "Access-Control-Allow-Origin: *" +echo "Access-Control-Allow-Methods: GET, OPTIONS" +echo "Access-Control-Allow-Headers: Content-Type" +echo "" + +# Handle CORS preflight +if [ "$REQUEST_METHOD" = "OPTIONS" ]; then + exit 0 +fi + +# Load library +. /usr/lib/secubox/master-link.sh >/dev/null 2>&1 + +# Return public graph (base64 encoded) +ml_zkp_get_graph diff --git a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/gossip.sh b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/gossip.sh index b7c60af7..8e2bb345 100644 --- a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/gossip.sh +++ b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/gossip.sh @@ -282,8 +282,8 @@ gossip_forward() { local msg_path msg_path=$(echo "$message" | jsonfilter -e '@.path[*]' 2>/dev/null) - # Forward to each peer not in path - while read -r peer_line; do + # Forward to each peer not in path (ash-compatible) + jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null | while read -r peer_line; do local peer_addr peer_id peer_addr=$(echo "$peer_line" | jsonfilter -e '@.address' 2>/dev/null) peer_id=$(echo "$peer_line" | jsonfilter -e '@.id' 2>/dev/null) @@ -302,7 +302,7 @@ gossip_forward() { --connect-timeout 2 & _update_stat "forwarded" - done < <(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null) + done wait # Wait for all forwards to complete } @@ -328,8 +328,11 @@ gossip_broadcast() { return 1 fi - local sent_count=0 - while read -r peer_line; do + # Send to all peers (ash-compatible) + local sent_count_file="/tmp/gossip_sent_$$.tmp" + echo "0" > "$sent_count_file" + + jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null | while read -r peer_line; do local peer_addr peer_addr=$(echo "$peer_line" | jsonfilter -e '@.address' 2>/dev/null) @@ -340,11 +343,14 @@ gossip_broadcast() { "http://$peer_addr:7332/api/gossip" \ --connect-timeout 2 & - sent_count=$((sent_count + 1)) + local cnt=$(cat "$sent_count_file") + echo $((cnt + 1)) > "$sent_count_file" _update_stat "sent" - done < <(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null) + done wait + local sent_count=$(cat "$sent_count_file") + rm -f "$sent_count_file" logger -t mirrornet "Gossip: broadcast $type to $sent_count peers" echo "$msg_id" @@ -398,15 +404,22 @@ gossip_process_queue() { local batch_size batch_size=$(_get_batch_size) - local count=0 - while read -r message; do + # Process queue (ash-compatible) + local count_file="/tmp/gossip_proc_$$.tmp" + echo "0" > "$count_file" + + jsonfilter -i "$GOSSIP_QUEUE" -e '@[*]' 2>/dev/null | while read -r message; do [ -z "$message" ] && continue - gossip_forward "$message" - count=$((count + 1)) + local cnt=$(cat "$count_file") + [ "$cnt" -ge "$batch_size" ] && break - [ "$count" -ge "$batch_size" ] && break - done < <(jsonfilter -i "$GOSSIP_QUEUE" -e '@[*]' 2>/dev/null) + gossip_forward "$message" + echo $((cnt + 1)) > "$count_file" + done + + local count=$(cat "$count_file") + rm -f "$count_file" # Clear processed messages if [ "$count" -gt 0 ]; then diff --git a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/health.sh b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/health.sh index 93a9725f..6754d4bd 100644 --- a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/health.sh +++ b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/health.sh @@ -289,16 +289,15 @@ health_check_all_peers() { echo " \"timestamp\": $(date +%s)," echo " \"peers\": [" - local first=1 - while read -r peer_line; do + # Collect peer check results (ash-compatible) + local tmp_results="/tmp/health_check_$$.tmp" + jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null | while read -r peer_line; do local peer_id peer_addr peer_id=$(echo "$peer_line" | jsonfilter -e '@.id' 2>/dev/null) peer_addr=$(echo "$peer_line" | jsonfilter -e '@.address' 2>/dev/null) [ -z "$peer_addr" ] && continue - [ "$first" = "1" ] || echo "," - # Run health checks local ping_result http_result ping_result=$(health_ping "$peer_addr") @@ -318,20 +317,22 @@ health_check_all_peers() { combined_status="unhealthy" fi - echo " {" - echo " \"peer_id\": \"$peer_id\"," - echo " \"address\": \"$peer_addr\"," - echo " \"status\": \"$combined_status\"," - echo " \"ping\": $ping_result," - echo " \"http\": $http_result" - echo " }" + # Output peer result as single line JSON + echo "{\"peer_id\":\"$peer_id\",\"address\":\"$peer_addr\",\"status\":\"$combined_status\",\"ping\":$ping_result,\"http\":$http_result}" # Record metrics local metrics="{\"latency_ms\":$(echo "$ping_result" | jsonfilter -e '@.latency_ms' 2>/dev/null || echo null),\"packet_loss\":$(echo "$ping_result" | jsonfilter -e '@.packet_loss' 2>/dev/null || echo 0),\"http_code\":$(echo "$http_result" | jsonfilter -e '@.http_code' 2>/dev/null || echo 0)}" health_record_metrics "$peer_id" "$metrics" + done > "$tmp_results" + # Output collected results with proper formatting + local first=1 + while read -r result; do + [ "$first" = "1" ] || echo "," + echo " $result" first=0 - done < <(jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null) + done < "$tmp_results" + rm -f "$tmp_results" echo " ]" echo "}" diff --git a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/identity.sh b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/identity.sh index a28f861e..3b2b9cc6 100644 --- a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/identity.sh +++ b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/identity.sh @@ -240,7 +240,7 @@ identity_list_peers() { echo "[" local first=1 - for peer_file in "$peer_dir"/*.json 2>/dev/null; do + for peer_file in "$peer_dir"/*.json; do [ -f "$peer_file" ] || continue [ "$first" = "1" ] || echo "," cat "$peer_file" diff --git a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/mirror.sh b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/mirror.sh index 5fa89900..253ec856 100644 --- a/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/mirror.sh +++ b/package/secubox/secubox-mirrornet/files/usr/lib/mirrornet/mirror.sh @@ -145,9 +145,10 @@ mirror_check_service() { # Parse and check each upstream local first=1 local count=0 + local tmp_output="/tmp/mirror_check_$$.tmp" - # Simple line-by-line parsing - while read -r line; do + # Simple line-by-line parsing (ash-compatible) + jsonfilter -i "$upstreams_file" -e '@[*]' 2>/dev/null | while read -r line; do local address port peer_id address=$(echo "$line" | jsonfilter -e '@.address' 2>/dev/null) port=$(echo "$line" | jsonfilter -e '@.port' 2>/dev/null) @@ -158,11 +159,19 @@ mirror_check_service() { local status status=$(mirror_check_upstream "$address" "$port") + echo "{\"peer_id\":\"$peer_id\",\"address\":\"$address\",\"port\":$port,\"status\":\"$status\"}" + done > "$tmp_output" + + # Output collected results + local count=0 + local first=1 + while read -r item; do [ "$first" = "1" ] || echo "," - echo " {\"peer_id\":\"$peer_id\",\"address\":\"$address\",\"port\":$port,\"status\":\"$status\"}" + echo " $item" first=0 count=$((count + 1)) - done < <(jsonfilter -i "$upstreams_file" -e '@[*]' 2>/dev/null) + done < "$tmp_output" + rm -f "$tmp_output" echo " ]," echo " \"total\": $count" @@ -178,30 +187,34 @@ mirror_get_best_upstream() { return 1 fi - # Find highest priority healthy upstream - local best_address="" - local best_port="" - local best_priority=0 + # Find highest priority healthy upstream (ash-compatible) + local best_file="/tmp/mirror_best_$$.tmp" + echo "0" > "$best_file" - while read -r line; do + jsonfilter -i "$upstreams_file" -e '@[*]' 2>/dev/null | while read -r line; do local address port priority status address=$(echo "$line" | jsonfilter -e '@.address' 2>/dev/null) port=$(echo "$line" | jsonfilter -e '@.port' 2>/dev/null) priority=$(echo "$line" | jsonfilter -e '@.priority' 2>/dev/null) [ -z "$address" ] && continue + [ -z "$priority" ] && priority=50 status=$(mirror_check_upstream "$address" "$port") - if [ "$status" = "ok" ] && [ "$priority" -gt "$best_priority" ]; then - best_address="$address" - best_port="$port" - best_priority="$priority" + if [ "$status" = "ok" ]; then + local current_best=$(cat "$best_file" | cut -d: -f1) + if [ "$priority" -gt "$current_best" ]; then + echo "$priority:$address:$port" > "$best_file" + fi fi - done < <(jsonfilter -i "$upstreams_file" -e '@[*]' 2>/dev/null) + done - if [ -n "$best_address" ]; then - echo "$best_address:$best_port" + local result=$(cat "$best_file") + rm -f "$best_file" + + if [ "$result" != "0" ]; then + echo "$result" | cut -d: -f2- else return 1 fi @@ -250,21 +263,31 @@ mirror_generate_haproxy_backend() { echo " option httpchk GET /health" echo " http-check expect status 200" - local server_num=1 - while read -r line; do + # Generate server lines (ash-compatible) + local tmp_servers="/tmp/mirror_servers_$$.tmp" + jsonfilter -i "$upstreams_file" -e '@[*]' 2>/dev/null | while read -r line; do local address port priority address=$(echo "$line" | jsonfilter -e '@.address' 2>/dev/null) port=$(echo "$line" | jsonfilter -e '@.port' 2>/dev/null) priority=$(echo "$line" | jsonfilter -e '@.priority' 2>/dev/null) [ -z "$address" ] && continue + [ -z "$priority" ] && priority=50 local weight=$((priority / 10)) [ "$weight" -lt 1 ] && weight=1 - echo " server srv$server_num $address:$port weight $weight check inter 10s fall 3 rise 2" + echo "$address:$port:$weight" + done > "$tmp_servers" + + local server_num=1 + while read -r srv_line; do + local addr_port=$(echo "$srv_line" | cut -d: -f1-2) + local weight=$(echo "$srv_line" | cut -d: -f3) + echo " server srv$server_num $addr_port weight $weight check inter 10s fall 3 rise 2" server_num=$((server_num + 1)) - done < <(jsonfilter -i "$upstreams_file" -e '@[*]' 2>/dev/null) + done < "$tmp_servers" + rm -f "$tmp_servers" echo "" }