- Add inventory.sh for hardware inventory collection (MAC, serial, model, CPU, RAM, storage) - Add profiles.sh for profile management and device matching - Add default.json profile template for auto-provisioned peers - Add discovery mode to master-link.sh with pending queue and approval workflow - Add bulk token generation (up to 100 tokens per batch) - Enhance 50-secubox-clone-provision with inventory collection and discovery join - Add 9 new RPCD methods to luci.cloner for factory provisioning - Fix p2p-mesh.sh to be silent when sourced as library - Add UCI options: discovery_mode, auto_approve_known, discovery_window, default_profile Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1809 lines
52 KiB
Bash
1809 lines
52 KiB
Bash
#!/bin/sh
|
|
# SecuBox Master-Link - Secure Mesh Propagation
|
|
# Manages join tokens, peer onboarding, and gigogne hierarchy
|
|
# Copyright 2026 CyberMind - Licensed under Apache-2.0
|
|
|
|
# Source dependencies
|
|
. /usr/lib/secubox/p2p-mesh.sh 2>/dev/null
|
|
. /usr/lib/secubox/factory.sh 2>/dev/null
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
ML_DIR="/var/lib/secubox-master-link"
|
|
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
|
|
}
|
|
|
|
# ============================================================================
|
|
# Token Management
|
|
# ============================================================================
|
|
|
|
# Generate HMAC-based one-time join token
|
|
ml_token_generate() {
|
|
ml_init
|
|
|
|
local ttl=$(uci -q get master-link.main.token_ttl)
|
|
[ -z "$ttl" ] && ttl=3600
|
|
|
|
local now=$(date +%s)
|
|
local expires=$((now + ttl))
|
|
local rand=$(head -c 32 /dev/urandom 2>/dev/null | sha256sum | cut -d' ' -f1)
|
|
[ -z "$rand" ] && rand=$(date +%s%N | sha256sum | cut -d' ' -f1)
|
|
|
|
# HMAC token using master key
|
|
local key_data=$(cat "$KEYFILE" 2>/dev/null)
|
|
local token=$(echo "${key_data}:${rand}:${now}" | sha256sum | cut -d' ' -f1)
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
|
|
# Store token in UCI
|
|
local section_id="token_$(echo "$token_hash" | cut -c1-8)"
|
|
uci -q batch <<-EOF
|
|
set master-link.${section_id}=token
|
|
set master-link.${section_id}.hash='${token_hash}'
|
|
set master-link.${section_id}.created='${now}'
|
|
set master-link.${section_id}.expires='${expires}'
|
|
set master-link.${section_id}.peer_fp=''
|
|
set master-link.${section_id}.status='active'
|
|
EOF
|
|
uci commit master-link
|
|
|
|
# Also store full token locally for validation
|
|
echo "$token" > "$ML_TOKENS_DIR/${token_hash}"
|
|
|
|
# Record in blockchain
|
|
local fp=$(factory_fingerprint 2>/dev/null)
|
|
chain_add_block "token_generated" \
|
|
"{\"token_hash\":\"$token_hash\",\"expires\":$expires,\"created_by\":\"$fp\"}" \
|
|
"$(echo "token_generated:${token_hash}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1
|
|
|
|
# Build join URL
|
|
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)
|
|
[ -z "$my_addr" ] && my_addr="$(hostname -i 2>/dev/null | awk '{print $1}')"
|
|
|
|
logger -t master-link "Token generated: ${token_hash} (expires: $(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
|
|
|
|
cat <<-EOF
|
|
{
|
|
"token": "$token",
|
|
"token_hash": "$token_hash",
|
|
"expires": $expires,
|
|
"ttl": $ttl,
|
|
"url": "http://${my_addr}:${MESH_PORT}/master-link/?token=${token}"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Validate a token
|
|
ml_token_validate() {
|
|
local token="$1"
|
|
[ -z "$token" ] && { echo '{"valid":false,"error":"missing_token"}'; return 1; }
|
|
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
local now=$(date +%s)
|
|
|
|
# Check token file exists
|
|
if [ ! -f "$ML_TOKENS_DIR/${token_hash}" ]; then
|
|
echo '{"valid":false,"error":"unknown_token"}'
|
|
return 1
|
|
fi
|
|
|
|
# Find token in UCI
|
|
local status=""
|
|
local expires=""
|
|
local found=0
|
|
|
|
local sections=$(uci -q show master-link 2>/dev/null | grep "\.hash=" | sed "s/master-link\.\(.*\)\.hash=.*/\1/")
|
|
for sec in $sections; do
|
|
local sec_hash=$(uci -q get "master-link.${sec}.hash")
|
|
if [ "$sec_hash" = "$token_hash" ]; then
|
|
status=$(uci -q get "master-link.${sec}.status")
|
|
expires=$(uci -q get "master-link.${sec}.expires")
|
|
found=1
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$found" -eq 0 ]; then
|
|
echo '{"valid":false,"error":"token_not_found"}'
|
|
return 1
|
|
fi
|
|
|
|
# Check status
|
|
if [ "$status" = "used" ]; then
|
|
echo '{"valid":false,"error":"token_already_used"}'
|
|
return 1
|
|
fi
|
|
|
|
if [ "$status" = "expired" ]; then
|
|
echo '{"valid":false,"error":"token_expired"}'
|
|
return 1
|
|
fi
|
|
|
|
# Check expiry
|
|
if [ "$now" -gt "$expires" ]; then
|
|
# Mark as expired in UCI
|
|
for sec in $sections; do
|
|
local sec_hash=$(uci -q get "master-link.${sec}.hash")
|
|
if [ "$sec_hash" = "$token_hash" ]; then
|
|
uci -q set "master-link.${sec}.status=expired"
|
|
uci commit master-link
|
|
break
|
|
fi
|
|
done
|
|
echo '{"valid":false,"error":"token_expired"}'
|
|
return 1
|
|
fi
|
|
|
|
echo "{\"valid\":true,\"token_hash\":\"$token_hash\",\"expires\":$expires}"
|
|
return 0
|
|
}
|
|
|
|
# Revoke a token
|
|
ml_token_revoke() {
|
|
local token="$1"
|
|
[ -z "$token" ] && return 1
|
|
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
|
|
local sections=$(uci -q show master-link 2>/dev/null | grep "\.hash=" | sed "s/master-link\.\(.*\)\.hash=.*/\1/")
|
|
for sec in $sections; do
|
|
local sec_hash=$(uci -q get "master-link.${sec}.hash")
|
|
if [ "$sec_hash" = "$token_hash" ]; then
|
|
uci -q set "master-link.${sec}.status=expired"
|
|
uci commit master-link
|
|
rm -f "$ML_TOKENS_DIR/${token_hash}"
|
|
logger -t master-link "Token revoked: $token_hash"
|
|
echo '{"success":true}'
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
echo '{"success":false,"error":"token_not_found"}'
|
|
return 1
|
|
}
|
|
|
|
# Cleanup expired tokens
|
|
ml_token_cleanup() {
|
|
local now=$(date +%s)
|
|
local cleaned=0
|
|
|
|
local sections=$(uci -q show master-link 2>/dev/null | grep "=token$" | sed "s/master-link\.\(.*\)=token/\1/")
|
|
for sec in $sections; do
|
|
local expires=$(uci -q get "master-link.${sec}.expires")
|
|
local status=$(uci -q get "master-link.${sec}.status")
|
|
[ -z "$expires" ] && continue
|
|
|
|
if [ "$now" -gt "$expires" ] || [ "$status" = "used" ] || [ "$status" = "expired" ]; then
|
|
local hash=$(uci -q get "master-link.${sec}.hash")
|
|
uci -q delete "master-link.${sec}"
|
|
rm -f "$ML_TOKENS_DIR/${hash}"
|
|
cleaned=$((cleaned + 1))
|
|
fi
|
|
done
|
|
|
|
[ "$cleaned" -gt 0 ] && uci commit master-link
|
|
logger -t master-link "Token cleanup: removed $cleaned expired tokens"
|
|
echo "{\"cleaned\":$cleaned}"
|
|
}
|
|
|
|
# Generate clone-specific token with auto-approve
|
|
# Used by secubox-cloner for station cloning
|
|
ml_clone_token_generate() {
|
|
ml_init
|
|
|
|
local ttl="${1:-86400}" # 24 hours default for clones
|
|
local token_type="${2:-clone}"
|
|
|
|
local now=$(date +%s)
|
|
local expires=$((now + ttl))
|
|
local rand=$(head -c 32 /dev/urandom 2>/dev/null | sha256sum | cut -d' ' -f1)
|
|
[ -z "$rand" ] && rand=$(date +%s%N | sha256sum | cut -d' ' -f1)
|
|
|
|
# HMAC token using master key
|
|
local key_data=$(cat "$KEYFILE" 2>/dev/null)
|
|
local token=$(echo "${key_data}:clone:${rand}:${now}" | sha256sum | cut -d' ' -f1)
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
|
|
# Store token in UCI with auto_approve flag
|
|
local section_id="clone_$(echo "$token_hash" | cut -c1-8)"
|
|
uci -q batch <<-EOF
|
|
set master-link.${section_id}=token
|
|
set master-link.${section_id}.hash='${token_hash}'
|
|
set master-link.${section_id}.created='${now}'
|
|
set master-link.${section_id}.expires='${expires}'
|
|
set master-link.${section_id}.peer_fp=''
|
|
set master-link.${section_id}.status='active'
|
|
set master-link.${section_id}.type='${token_type}'
|
|
set master-link.${section_id}.auto_approve='1'
|
|
EOF
|
|
uci commit master-link
|
|
|
|
# Store full token locally for validation
|
|
echo "$token" > "$ML_TOKENS_DIR/${token_hash}"
|
|
|
|
# Record in blockchain
|
|
local fp=$(factory_fingerprint 2>/dev/null)
|
|
chain_add_block "clone_token_generated" \
|
|
"{\"token_hash\":\"$token_hash\",\"type\":\"$token_type\",\"expires\":$expires,\"created_by\":\"$fp\"}" \
|
|
"$(echo "clone_token:${token_hash}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1
|
|
|
|
# Build join URL
|
|
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)
|
|
|
|
logger -t master-link "Clone token generated: ${token_hash} (auto-approve, expires: $(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
|
|
|
|
cat <<-EOF
|
|
{
|
|
"token": "$token",
|
|
"token_hash": "$token_hash",
|
|
"type": "$token_type",
|
|
"auto_approve": true,
|
|
"expires": $expires,
|
|
"ttl": $ttl,
|
|
"master_ip": "$my_addr",
|
|
"url": "http://${my_addr}:${MESH_PORT}/master-link/?token=${token}&type=clone"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Check if token is a clone token with auto-approve
|
|
ml_token_is_auto_approve() {
|
|
local token="$1"
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
|
|
# Find token in UCI
|
|
local sections=$(uci -q show master-link 2>/dev/null | grep "=token$" | sed "s/master-link\.\(.*\)=token/\1/")
|
|
for sec in $sections; do
|
|
local hash=$(uci -q get "master-link.${sec}.hash")
|
|
if [ "$hash" = "$token_hash" ]; then
|
|
local auto=$(uci -q get "master-link.${sec}.auto_approve")
|
|
if [ "$auto" = "1" ]; then
|
|
echo "true"
|
|
return 0
|
|
fi
|
|
break
|
|
fi
|
|
done
|
|
echo "false"
|
|
return 1
|
|
}
|
|
|
|
# ============================================================================
|
|
# Join Protocol
|
|
# ============================================================================
|
|
|
|
# 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")
|
|
local valid=$(echo "$validation" | jsonfilter -e '@.valid' 2>/dev/null)
|
|
|
|
if [ "$valid" != "true" ]; then
|
|
echo "$validation"
|
|
return 1
|
|
fi
|
|
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
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",
|
|
"address": "$peer_addr",
|
|
"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 (with ZKP status)
|
|
chain_add_block "join_request" \
|
|
"{\"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 [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)
|
|
local token_auto=$(ml_token_is_auto_approve "$token")
|
|
|
|
if [ "$auto_approve" = "1" ] || [ "$token_auto" = "true" ]; then
|
|
logger -t master-link "Auto-approving join for $peer_fp (global=$auto_approve, token=$token_auto)"
|
|
ml_join_approve "$peer_fp"
|
|
return $?
|
|
fi
|
|
|
|
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"
|
|
|
|
[ -z "$peer_fp" ] && {
|
|
echo '{"error":"missing_fingerprint"}'
|
|
return 1
|
|
}
|
|
|
|
local request_file="$ML_REQUESTS_DIR/${peer_fp}.json"
|
|
[ -f "$request_file" ] || {
|
|
echo '{"error":"no_pending_request"}'
|
|
return 1
|
|
}
|
|
|
|
local peer_addr=$(jsonfilter -i "$request_file" -e '@.address' 2>/dev/null)
|
|
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)
|
|
[ -z "$my_depth" ] && my_depth=0
|
|
local peer_depth=$((my_depth + 1))
|
|
|
|
# Trust peer via factory TOFU
|
|
factory_trust_peer "$peer_fp" "$peer_addr" >/dev/null 2>&1
|
|
|
|
# Add peer to mesh
|
|
peer_add "$peer_addr" "$MESH_PORT" "$peer_fp" >/dev/null 2>&1
|
|
|
|
# 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",
|
|
"address": "$peer_addr",
|
|
"hostname": "$peer_hostname",
|
|
"token_hash": "$token_hash",
|
|
"timestamp": $orig_ts,
|
|
"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
|
|
|
|
# Mark token as used
|
|
local sections=$(uci -q show master-link 2>/dev/null | grep "\.hash=" | sed "s/master-link\.\(.*\)\.hash=.*/\1/")
|
|
for sec in $sections; do
|
|
local sec_hash=$(uci -q get "master-link.${sec}.hash")
|
|
if [ "$sec_hash" = "$token_hash" ]; then
|
|
uci -q set "master-link.${sec}.status=used"
|
|
uci -q set "master-link.${sec}.peer_fp=$peer_fp"
|
|
uci commit master-link
|
|
break
|
|
fi
|
|
done
|
|
|
|
# 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\",\"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 [zkp_verified=$zkp_verified]"
|
|
|
|
cat <<-EOF
|
|
{
|
|
"success": true,
|
|
"fingerprint": "$peer_fp",
|
|
"address": "$peer_addr",
|
|
"hostname": "$peer_hostname",
|
|
"depth": $peer_depth,
|
|
"zkp_verified": $zkp_verified,
|
|
"zkp_graph_stored": $zkp_graph_stored,
|
|
"status": "approved"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Reject a peer join request
|
|
ml_join_reject() {
|
|
local peer_fp="$1"
|
|
local reason="${2:-rejected by admin}"
|
|
|
|
[ -z "$peer_fp" ] && {
|
|
echo '{"error":"missing_fingerprint"}'
|
|
return 1
|
|
}
|
|
|
|
local request_file="$ML_REQUESTS_DIR/${peer_fp}.json"
|
|
[ -f "$request_file" ] || {
|
|
echo '{"error":"no_pending_request"}'
|
|
return 1
|
|
}
|
|
|
|
local my_fp=$(factory_fingerprint 2>/dev/null)
|
|
local now=$(date +%s)
|
|
|
|
# Read all fields before overwriting
|
|
local peer_addr=$(jsonfilter -i "$request_file" -e '@.address' 2>/dev/null)
|
|
local peer_hostname=$(jsonfilter -i "$request_file" -e '@.hostname' 2>/dev/null)
|
|
local orig_ts=$(jsonfilter -i "$request_file" -e '@.timestamp' 2>/dev/null)
|
|
[ -z "$orig_ts" ] && orig_ts=0
|
|
|
|
cat > "$request_file" <<-EOF
|
|
{
|
|
"fingerprint": "$peer_fp",
|
|
"address": "$peer_addr",
|
|
"hostname": "$peer_hostname",
|
|
"timestamp": $orig_ts,
|
|
"rejected_at": $now,
|
|
"rejected_by": "$my_fp",
|
|
"reason": "$reason",
|
|
"status": "rejected"
|
|
}
|
|
EOF
|
|
|
|
# Add peer_rejected block to chain
|
|
chain_add_block "peer_rejected" \
|
|
"{\"fp\":\"$peer_fp\",\"reason\":\"$reason\",\"rejected_by\":\"$my_fp\"}" \
|
|
"$(echo "peer_rejected:${peer_fp}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1
|
|
|
|
logger -t master-link "Peer rejected: $peer_fp - $reason"
|
|
|
|
echo "{\"success\":true,\"fingerprint\":\"$peer_fp\",\"status\":\"rejected\"}"
|
|
}
|
|
|
|
# ============================================================================
|
|
# IPK Generation & Serving
|
|
# ============================================================================
|
|
|
|
# Generate a minimal join IPK on-the-fly
|
|
# OpenWrt IPK format: tar.gz containing debian-binary, control.tar.gz, data.tar.gz
|
|
# Args: $1=master_ip $2=master_depth $3=master_hostname $4=token_prefix
|
|
# Output: path to generated IPK file on stdout
|
|
ml_ipk_generate() {
|
|
local master_ip="$1"
|
|
local master_depth="${2:-0}"
|
|
local master_hostname="${3:-secubox}"
|
|
local token_prefix="${4:-0000}"
|
|
local peer_depth=$((master_depth + 1))
|
|
local version="1.0.0-${token_prefix}"
|
|
|
|
local work="/tmp/ml-ipk-$$"
|
|
local ipk_file="/tmp/secubox-mesh-join-$$.ipk"
|
|
|
|
rm -rf "$work"
|
|
mkdir -p "$work/ipk" "$work/control" "$work/data"
|
|
|
|
# --- debian-binary ---
|
|
printf '2.0\n' > "$work/ipk/debian-binary"
|
|
|
|
# --- control file ---
|
|
cat > "$work/control/control" <<-CTRL
|
|
Package: secubox-mesh-join
|
|
Version: ${version}
|
|
Architecture: all
|
|
Installed-Size: 1
|
|
Description: SecuBox mesh join - peer of ${master_hostname} (${master_ip})
|
|
Maintainer: SecuBox
|
|
Section: admin
|
|
CTRL
|
|
|
|
# --- postinst script ---
|
|
# Configure master-link via uci instead of shipping config file
|
|
# (avoids file conflict with secubox-master-link package)
|
|
cat > "$work/control/postinst" <<-POSTINST
|
|
#!/bin/sh
|
|
[ -n "\${IPKG_INSTROOT}" ] || {
|
|
uci -q set master-link.main=master-link
|
|
uci -q set master-link.main.enabled='1'
|
|
uci -q set master-link.main.role='peer'
|
|
uci -q set master-link.main.upstream='${master_ip}'
|
|
uci -q set master-link.main.depth='${peer_depth}'
|
|
uci -q set master-link.main.max_depth='3'
|
|
uci -q set master-link.main.token_ttl='3600'
|
|
uci -q set master-link.main.auto_approve='0'
|
|
uci commit master-link
|
|
/etc/init.d/master-link enable 2>/dev/null
|
|
/etc/init.d/master-link start 2>/dev/null
|
|
}
|
|
exit 0
|
|
POSTINST
|
|
chmod 755 "$work/control/postinst"
|
|
|
|
# --- data: empty (config is applied by postinst via uci) ---
|
|
|
|
# --- Build inner tar.gz archives ---
|
|
tar czf "$work/ipk/control.tar.gz" -C "$work/control" . 2>/dev/null
|
|
tar czf "$work/ipk/data.tar.gz" -C "$work/data" . 2>/dev/null
|
|
|
|
# --- Assemble IPK (outer tar.gz) ---
|
|
tar czf "$ipk_file" -C "$work/ipk" \
|
|
debian-binary control.tar.gz data.tar.gz 2>/dev/null
|
|
|
|
rm -rf "$work"
|
|
echo "$ipk_file"
|
|
}
|
|
|
|
# Detect master IP from CGI environment or UCI
|
|
_ml_detect_master_ip() {
|
|
# Try HTTP_HOST (CGI environment, set by uhttpd)
|
|
local ip=$(echo "${HTTP_HOST:-}" | cut -d: -f1)
|
|
[ -n "$ip" ] && { echo "$ip"; return; }
|
|
|
|
# Try UCI network config
|
|
ip=$(uci -q get network.lan.ipaddr)
|
|
[ -n "$ip" ] && { echo "$ip"; return; }
|
|
|
|
# Fallback: parse ip addr
|
|
ip addr show br-lan 2>/dev/null | grep -o 'inet [0-9.]*' | head -1 | cut -d' ' -f2
|
|
}
|
|
|
|
# Validate token and serve IPK file (pre-built or generated)
|
|
ml_ipk_serve() {
|
|
local token="$1"
|
|
|
|
# Validate token
|
|
local validation=$(ml_token_validate "$token")
|
|
local valid=$(echo "$validation" | jsonfilter -e '@.valid' 2>/dev/null)
|
|
|
|
if [ "$valid" != "true" ]; then
|
|
echo "Status: 403 Forbidden"
|
|
echo "Content-Type: application/json"
|
|
echo "Access-Control-Allow-Origin: *"
|
|
echo ""
|
|
echo "$validation"
|
|
return 1
|
|
fi
|
|
|
|
# Try pre-built IPK first
|
|
local ipk_path=$(uci -q get master-link.main.ipk_path)
|
|
[ -z "$ipk_path" ] && ipk_path="/www/secubox-feed/secubox-master-link_*.ipk"
|
|
|
|
local ipk_file=""
|
|
for f in $ipk_path; do
|
|
[ -f "$f" ] && ipk_file="$f"
|
|
done
|
|
|
|
# Fallback: generate minimal join IPK on-the-fly
|
|
local generated=0
|
|
if [ -z "$ipk_file" ]; then
|
|
local master_ip=$(_ml_detect_master_ip)
|
|
local master_depth=$(uci -q get master-link.main.depth)
|
|
[ -z "$master_depth" ] && master_depth=0
|
|
local master_hostname=$(uci -q get system.@system[0].hostname 2>/dev/null)
|
|
[ -z "$master_hostname" ] && master_hostname="secubox"
|
|
local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1)
|
|
local token_prefix=$(echo "$token_hash" | cut -c1-8)
|
|
|
|
ipk_file=$(ml_ipk_generate "$master_ip" "$master_depth" "$master_hostname" "$token_prefix")
|
|
generated=1
|
|
|
|
if [ -z "$ipk_file" ] || [ ! -f "$ipk_file" ]; then
|
|
echo "Status: 500 Internal Server Error"
|
|
echo "Content-Type: application/json"
|
|
echo "Access-Control-Allow-Origin: *"
|
|
echo ""
|
|
echo '{"error":"ipk_generation_failed"}'
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
local filename="secubox-mesh-join.ipk"
|
|
[ "$generated" = "0" ] && filename=$(basename "$ipk_file")
|
|
local filesize=$(wc -c < "$ipk_file")
|
|
|
|
echo "Content-Type: application/octet-stream"
|
|
echo "Content-Disposition: attachment; filename=\"$filename\""
|
|
echo "Content-Length: $filesize"
|
|
echo "Access-Control-Allow-Origin: *"
|
|
echo ""
|
|
cat "$ipk_file"
|
|
|
|
# Clean up generated IPK
|
|
[ "$generated" = "1" ] && rm -f "$ipk_file"
|
|
}
|
|
|
|
# Return IPK metadata
|
|
ml_ipk_bundle_info() {
|
|
local ipk_path=$(uci -q get master-link.main.ipk_path)
|
|
[ -z "$ipk_path" ] && ipk_path="/www/secubox-feed/secubox-master-link_*.ipk"
|
|
|
|
local ipk_file=""
|
|
for f in $ipk_path; do
|
|
[ -f "$f" ] && ipk_file="$f"
|
|
done
|
|
|
|
if [ -n "$ipk_file" ]; then
|
|
local filename=$(basename "$ipk_file")
|
|
local filesize=$(wc -c < "$ipk_file")
|
|
local sha256=$(sha256sum "$ipk_file" | cut -d' ' -f1)
|
|
|
|
cat <<-EOF
|
|
{
|
|
"available": true,
|
|
"type": "prebuilt",
|
|
"filename": "$filename",
|
|
"size": $filesize,
|
|
"sha256": "$sha256"
|
|
}
|
|
EOF
|
|
else
|
|
# Dynamic generation always available
|
|
cat <<-EOF
|
|
{
|
|
"available": true,
|
|
"type": "dynamic",
|
|
"filename": "secubox-mesh-join.ipk",
|
|
"description": "Minimal join package (generated on-the-fly)"
|
|
}
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Gigogne (Nested Hierarchy)
|
|
# ============================================================================
|
|
|
|
# Promote an approved peer to sub-master
|
|
ml_promote_to_submaster() {
|
|
local peer_fp="$1"
|
|
|
|
[ -z "$peer_fp" ] && {
|
|
echo '{"error":"missing_fingerprint"}'
|
|
return 1
|
|
}
|
|
|
|
# Check depth limit
|
|
local depth_ok=$(ml_check_depth)
|
|
local can_promote=$(echo "$depth_ok" | jsonfilter -e '@.can_promote' 2>/dev/null)
|
|
|
|
if [ "$can_promote" != "true" ]; then
|
|
echo "$depth_ok"
|
|
return 1
|
|
fi
|
|
|
|
local request_file="$ML_REQUESTS_DIR/${peer_fp}.json"
|
|
[ -f "$request_file" ] || {
|
|
echo '{"error":"peer_not_found"}'
|
|
return 1
|
|
}
|
|
|
|
local peer_status=$(jsonfilter -i "$request_file" -e '@.status' 2>/dev/null)
|
|
if [ "$peer_status" != "approved" ]; then
|
|
echo '{"error":"peer_not_approved"}'
|
|
return 1
|
|
fi
|
|
|
|
local peer_addr=$(jsonfilter -i "$request_file" -e '@.address' 2>/dev/null)
|
|
local my_depth=$(uci -q get master-link.main.depth)
|
|
[ -z "$my_depth" ] && my_depth=0
|
|
local new_depth=$((my_depth + 1))
|
|
local now=$(date +%s)
|
|
|
|
# Read all fields before truncating the file
|
|
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 orig_approved=$(jsonfilter -i "$request_file" -e '@.approved_at' 2>/dev/null)
|
|
|
|
cat > "$request_file" <<-EOF
|
|
{
|
|
"fingerprint": "$peer_fp",
|
|
"address": "$peer_addr",
|
|
"hostname": "$peer_hostname",
|
|
"token_hash": "$token_hash",
|
|
"timestamp": $orig_ts,
|
|
"approved_at": $orig_approved,
|
|
"promoted_at": $now,
|
|
"depth": $new_depth,
|
|
"role": "sub-master",
|
|
"status": "approved"
|
|
}
|
|
EOF
|
|
|
|
# Add peer_promoted block to chain
|
|
chain_add_block "peer_promoted" \
|
|
"{\"fp\":\"$peer_fp\",\"new_role\":\"sub-master\",\"new_depth\":$new_depth}" \
|
|
"$(echo "peer_promoted:${peer_fp}:${now}" | sha256sum | cut -d' ' -f1)" >/dev/null 2>&1
|
|
|
|
# Notify the peer to update its role (via mesh API)
|
|
curl -s --connect-timeout 5 -X POST \
|
|
"http://$peer_addr:$MESH_PORT/api/master-link/status" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"action\":\"promote\",\"role\":\"sub-master\",\"depth\":$new_depth}" 2>/dev/null &
|
|
|
|
logger -t master-link "Peer promoted to sub-master: $peer_fp at depth $new_depth"
|
|
|
|
cat <<-EOF
|
|
{
|
|
"success": true,
|
|
"fingerprint": "$peer_fp",
|
|
"new_role": "sub-master",
|
|
"new_depth": $new_depth
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Forward blocks to upstream master
|
|
ml_propagate_block_upstream() {
|
|
local block_data="$1"
|
|
local upstream=$(uci -q get master-link.main.upstream)
|
|
[ -z "$upstream" ] && return 0
|
|
|
|
curl -s --connect-timeout 5 -X POST \
|
|
"http://$upstream:$MESH_PORT/api/chain" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$block_data" 2>/dev/null
|
|
|
|
return $?
|
|
}
|
|
|
|
# Check if depth allows sub-master promotion
|
|
ml_check_depth() {
|
|
local depth=$(uci -q get master-link.main.depth)
|
|
local max_depth=$(uci -q get master-link.main.max_depth)
|
|
[ -z "$depth" ] && depth=0
|
|
[ -z "$max_depth" ] && max_depth=3
|
|
|
|
local next_depth=$((depth + 1))
|
|
|
|
if [ "$next_depth" -ge "$max_depth" ]; then
|
|
echo "{\"can_promote\":false,\"depth\":$depth,\"max_depth\":$max_depth,\"error\":\"max_depth_reached\"}"
|
|
return 1
|
|
fi
|
|
|
|
echo "{\"can_promote\":true,\"depth\":$depth,\"max_depth\":$max_depth,\"next_depth\":$next_depth}"
|
|
return 0
|
|
}
|
|
|
|
# ============================================================================
|
|
# Status & Peer Listing
|
|
# ============================================================================
|
|
|
|
# Return mesh status
|
|
ml_status() {
|
|
ml_init 2>/dev/null
|
|
|
|
local role=$(uci -q get master-link.main.role)
|
|
local depth=$(uci -q get master-link.main.depth)
|
|
local upstream=$(uci -q get master-link.main.upstream)
|
|
local max_depth=$(uci -q get master-link.main.max_depth)
|
|
local enabled=$(uci -q get master-link.main.enabled)
|
|
local auto_approve=$(uci -q get master-link.main.auto_approve)
|
|
local fp=$(factory_fingerprint 2>/dev/null)
|
|
|
|
[ -z "$role" ] && role="master"
|
|
[ -z "$depth" ] && depth=0
|
|
[ -z "$max_depth" ] && max_depth=3
|
|
[ -z "$enabled" ] && enabled=1
|
|
|
|
# Count peers by status
|
|
local pending=0
|
|
local approved=0
|
|
local rejected=0
|
|
for req in "$ML_REQUESTS_DIR"/*.json; do
|
|
[ -f "$req" ] || continue
|
|
local st=$(jsonfilter -i "$req" -e '@.status' 2>/dev/null)
|
|
case "$st" in
|
|
pending) pending=$((pending + 1)) ;;
|
|
approved) approved=$((approved + 1)) ;;
|
|
rejected) rejected=$((rejected + 1)) ;;
|
|
esac
|
|
done
|
|
|
|
# Count active tokens
|
|
local active_tokens=0
|
|
local now=$(date +%s)
|
|
local sections=$(uci -q show master-link 2>/dev/null | grep "=token$" | sed "s/master-link\.\(.*\)=token/\1/")
|
|
for sec in $sections; do
|
|
local status=$(uci -q get "master-link.${sec}.status")
|
|
local expires=$(uci -q get "master-link.${sec}.expires")
|
|
if [ "$status" = "active" ] && [ -n "$expires" ] && [ "$now" -lt "$expires" ]; then
|
|
active_tokens=$((active_tokens + 1))
|
|
fi
|
|
done
|
|
|
|
# Chain height
|
|
local chain_height=0
|
|
[ -f "$CHAIN_FILE" ] && chain_height=$(jsonfilter -i "$CHAIN_FILE" -e '@.blocks[*]' 2>/dev/null | wc -l)
|
|
|
|
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,
|
|
"role": "$role",
|
|
"depth": $depth,
|
|
"max_depth": $max_depth,
|
|
"upstream": "$upstream",
|
|
"fingerprint": "$fp",
|
|
"hostname": "$hostname",
|
|
"auto_approve": $auto_approve,
|
|
"peers": {
|
|
"pending": $pending,
|
|
"approved": $approved,
|
|
"rejected": $rejected,
|
|
"total": $((pending + approved + rejected))
|
|
},
|
|
"active_tokens": $active_tokens,
|
|
"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
|
|
}
|
|
|
|
# List all peers with details
|
|
ml_peer_list() {
|
|
local first=1
|
|
echo '{"peers":['
|
|
|
|
for req in "$ML_REQUESTS_DIR"/*.json; do
|
|
[ -f "$req" ] || continue
|
|
[ $first -eq 0 ] && echo ","
|
|
first=0
|
|
cat "$req" | tr '\n' ' ' | tr '\t' ' '
|
|
done
|
|
|
|
echo ']}'
|
|
}
|
|
|
|
# Build mesh tree from chain blocks
|
|
ml_tree() {
|
|
local fp=$(factory_fingerprint 2>/dev/null)
|
|
local hostname=$(uci -q get system.@system[0].hostname 2>/dev/null || hostname)
|
|
local role=$(uci -q get master-link.main.role)
|
|
local depth=$(uci -q get master-link.main.depth)
|
|
[ -z "$role" ] && role="master"
|
|
[ -z "$depth" ] && depth=0
|
|
|
|
echo '{"tree":{'
|
|
echo "\"fingerprint\":\"$fp\","
|
|
echo "\"hostname\":\"$hostname\","
|
|
echo "\"role\":\"$role\","
|
|
echo "\"depth\":$depth,"
|
|
echo '"children":['
|
|
|
|
# Build children from approved peers
|
|
local first=1
|
|
for req in "$ML_REQUESTS_DIR"/*.json; do
|
|
[ -f "$req" ] || continue
|
|
local st=$(jsonfilter -i "$req" -e '@.status' 2>/dev/null)
|
|
[ "$st" != "approved" ] && continue
|
|
|
|
local child_fp=$(jsonfilter -i "$req" -e '@.fingerprint' 2>/dev/null)
|
|
local child_hostname=$(jsonfilter -i "$req" -e '@.hostname' 2>/dev/null)
|
|
local child_addr=$(jsonfilter -i "$req" -e '@.address' 2>/dev/null)
|
|
local child_depth=$(jsonfilter -i "$req" -e '@.depth' 2>/dev/null)
|
|
local child_role=$(jsonfilter -i "$req" -e '@.role' 2>/dev/null)
|
|
[ -z "$child_depth" ] && child_depth=$((depth + 1))
|
|
[ -z "$child_role" ] && child_role="peer"
|
|
|
|
# Check if peer is online
|
|
local online="false"
|
|
curl -s --connect-timeout 2 "http://$child_addr:$MESH_PORT/api/status" >/dev/null 2>&1 && online="true"
|
|
|
|
[ $first -eq 0 ] && echo ","
|
|
first=0
|
|
|
|
cat <<-CHILD
|
|
{
|
|
"fingerprint": "$child_fp",
|
|
"hostname": "$child_hostname",
|
|
"address": "$child_addr",
|
|
"role": "$child_role",
|
|
"depth": $child_depth,
|
|
"online": $online
|
|
}
|
|
CHILD
|
|
done
|
|
|
|
echo ']}}'
|
|
}
|
|
|
|
# ============================================================================
|
|
# Factory Auto-Provisioning (Discovery Mode)
|
|
# ============================================================================
|
|
|
|
ML_PENDING_DIR="$ML_DIR/pending"
|
|
ML_APPROVED_DIR="$ML_DIR/approved"
|
|
ML_BULK_DIR="$ML_DIR/bulk"
|
|
|
|
# Check if discovery mode is enabled
|
|
ml_discovery_enabled() {
|
|
local enabled
|
|
enabled=$(uci -q get master-link.main.discovery_mode || echo "0")
|
|
[ "$enabled" = "1" ]
|
|
}
|
|
|
|
# Handle discovery request from new device (no token required)
|
|
ml_discovery_request() {
|
|
local inventory_json="$1"
|
|
local peer_addr="$2"
|
|
|
|
# Check if discovery mode is enabled
|
|
ml_discovery_enabled || {
|
|
echo '{"error":"discovery_disabled","message":"Discovery mode is not enabled on this master"}'
|
|
return 1
|
|
}
|
|
|
|
# Extract device identifiers from inventory
|
|
local mac serial model
|
|
mac=$(echo "$inventory_json" | jsonfilter -e '@.mac' 2>/dev/null)
|
|
serial=$(echo "$inventory_json" | jsonfilter -e '@.serial' 2>/dev/null)
|
|
model=$(echo "$inventory_json" | jsonfilter -e '@.model' 2>/dev/null)
|
|
|
|
# Generate device ID from MAC (lowercase, no colons)
|
|
local device_id
|
|
device_id=$(echo "$mac" | tr -d ':' | tr '[:upper:]' '[:lower:]')
|
|
|
|
[ -z "$device_id" ] && {
|
|
echo '{"error":"invalid_inventory","message":"Missing MAC address in inventory"}'
|
|
return 1
|
|
}
|
|
|
|
# Store inventory
|
|
if [ -f /usr/lib/secubox/inventory.sh ]; then
|
|
. /usr/lib/secubox/inventory.sh
|
|
inventory_store "$device_id" "$inventory_json"
|
|
fi
|
|
|
|
# Check if pre-registered for auto-approval
|
|
local auto_approve
|
|
auto_approve=$(uci -q get master-link.main.auto_approve_known || echo "0")
|
|
|
|
if [ "$auto_approve" = "1" ] && [ -f /usr/lib/secubox/inventory.sh ]; then
|
|
local match
|
|
match=$(inventory_match "$mac" "$serial")
|
|
if [ -n "$match" ]; then
|
|
# Auto-approve: determine profile and generate token
|
|
local profile
|
|
profile=$(echo "$match" | jsonfilter -e '@.profile' 2>/dev/null)
|
|
[ -z "$profile" ] && profile="default"
|
|
|
|
local token
|
|
token=$(ml_clone_token_generate 3600 | jsonfilter -e '@.token' 2>/dev/null)
|
|
|
|
logger -t master-link "Auto-approved device $device_id (MAC: $mac)"
|
|
|
|
local my_addr
|
|
my_addr=$(uci -q get network.lan.ipaddr || echo "192.168.255.1")
|
|
|
|
cat <<-EOF
|
|
{
|
|
"status": "approved",
|
|
"device_id": "$device_id",
|
|
"token": "$token",
|
|
"profile": "$profile",
|
|
"master": "$my_addr"
|
|
}
|
|
EOF
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Queue for manual approval
|
|
mkdir -p "$ML_PENDING_DIR"
|
|
|
|
local now
|
|
now=$(date +%s)
|
|
|
|
cat > "$ML_PENDING_DIR/${device_id}.json" <<-EOF
|
|
{
|
|
"device_id": "$device_id",
|
|
"mac": "$mac",
|
|
"serial": "$serial",
|
|
"model": "$model",
|
|
"address": "$peer_addr",
|
|
"inventory": $inventory_json,
|
|
"requested_at": $now,
|
|
"status": "pending"
|
|
}
|
|
EOF
|
|
|
|
logger -t master-link "Discovery request queued: $device_id (MAC: $mac)"
|
|
|
|
cat <<-EOF
|
|
{
|
|
"status": "pending",
|
|
"device_id": "$device_id",
|
|
"message": "Awaiting approval from master"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# Approve pending discovery device
|
|
ml_discovery_approve() {
|
|
local device_id="$1"
|
|
local profile="${2:-default}"
|
|
|
|
local pending_file="$ML_PENDING_DIR/${device_id}.json"
|
|
[ -f "$pending_file" ] || {
|
|
echo '{"error":"device_not_found","message":"No pending device with this ID"}'
|
|
return 1
|
|
}
|
|
|
|
local device_info
|
|
device_info=$(cat "$pending_file")
|
|
|
|
local mac peer_addr
|
|
mac=$(echo "$device_info" | jsonfilter -e '@.mac' 2>/dev/null)
|
|
peer_addr=$(echo "$device_info" | jsonfilter -e '@.address' 2>/dev/null)
|
|
|
|
# Generate token for the device
|
|
local token
|
|
token=$(ml_clone_token_generate 3600 | jsonfilter -e '@.token' 2>/dev/null)
|
|
|
|
local now
|
|
now=$(date +%s)
|
|
|
|
# Store in approved directory
|
|
mkdir -p "$ML_APPROVED_DIR"
|
|
|
|
cat > "$ML_APPROVED_DIR/${device_id}.json" <<-EOF
|
|
{
|
|
"device_id": "$device_id",
|
|
"mac": "$mac",
|
|
"profile": "$profile",
|
|
"token": "$token",
|
|
"approved_at": $now,
|
|
"approved_by": "$(factory_fingerprint 2>/dev/null || echo 'master')"
|
|
}
|
|
EOF
|
|
|
|
# Remove from pending
|
|
rm -f "$pending_file"
|
|
|
|
# Notify device if online (fire-and-forget)
|
|
if [ -n "$peer_addr" ]; then
|
|
curl -s -X POST "http://$peer_addr:7331/api/discovery/approved" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"token\":\"$token\",\"profile\":\"$profile\"}" \
|
|
--connect-timeout 2 &
|
|
fi
|
|
|
|
logger -t master-link "Approved discovery device: $device_id (profile: $profile)"
|
|
|
|
echo "{\"status\":\"approved\",\"device_id\":\"$device_id\",\"profile\":\"$profile\"}"
|
|
}
|
|
|
|
# Reject pending discovery device
|
|
ml_discovery_reject() {
|
|
local device_id="$1"
|
|
local reason="${2:-rejected by admin}"
|
|
|
|
local pending_file="$ML_PENDING_DIR/${device_id}.json"
|
|
[ -f "$pending_file" ] || {
|
|
echo '{"error":"device_not_found"}'
|
|
return 1
|
|
}
|
|
|
|
rm -f "$pending_file"
|
|
|
|
logger -t master-link "Rejected discovery device: $device_id - $reason"
|
|
|
|
echo "{\"status\":\"rejected\",\"device_id\":\"$device_id\",\"reason\":\"$reason\"}"
|
|
}
|
|
|
|
# List pending discovery devices
|
|
ml_discovery_pending() {
|
|
mkdir -p "$ML_PENDING_DIR"
|
|
|
|
echo "["
|
|
local first=1
|
|
for f in "$ML_PENDING_DIR"/*.json; do
|
|
[ -f "$f" ] || continue
|
|
[ "$first" = "1" ] || echo ","
|
|
cat "$f"
|
|
first=0
|
|
done
|
|
echo "]"
|
|
}
|
|
|
|
# Get discovery status for a device
|
|
ml_discovery_status() {
|
|
local device_id="$1"
|
|
|
|
# Check approved
|
|
if [ -f "$ML_APPROVED_DIR/${device_id}.json" ]; then
|
|
cat "$ML_APPROVED_DIR/${device_id}.json"
|
|
return 0
|
|
fi
|
|
|
|
# Check pending
|
|
if [ -f "$ML_PENDING_DIR/${device_id}.json" ]; then
|
|
cat "$ML_PENDING_DIR/${device_id}.json"
|
|
return 0
|
|
fi
|
|
|
|
echo '{"status":"unknown","device_id":"'"$device_id"'"}'
|
|
return 1
|
|
}
|
|
|
|
# Generate bulk tokens for mass provisioning
|
|
ml_bulk_tokens() {
|
|
local count="$1"
|
|
local profile="${2:-default}"
|
|
local ttl="${3:-86400}"
|
|
|
|
# Validate count
|
|
[ "$count" -gt 0 ] 2>/dev/null || {
|
|
echo '{"error":"invalid_count","message":"Count must be a positive number"}'
|
|
return 1
|
|
}
|
|
[ "$count" -le 100 ] || {
|
|
echo '{"error":"max_100","message":"Maximum 100 tokens per batch"}'
|
|
return 1
|
|
}
|
|
|
|
local batch_id
|
|
batch_id=$(date +%s)
|
|
mkdir -p "$ML_BULK_DIR/${batch_id}"
|
|
|
|
echo "["
|
|
local i=1
|
|
while [ "$i" -le "$count" ]; do
|
|
[ "$i" -gt 1 ] && echo ","
|
|
|
|
local token_json
|
|
token_json=$(ml_clone_token_generate "$ttl" "bulk")
|
|
local token
|
|
token=$(echo "$token_json" | jsonfilter -e '@.token' 2>/dev/null)
|
|
|
|
# Store with profile association
|
|
echo "{\"profile\":\"$profile\",\"batch\":\"$batch_id\"}" > "$ML_BULK_DIR/${batch_id}/${token}.meta"
|
|
|
|
printf '{"index":%d,"token":"%s","profile":"%s"}' "$i" "$token" "$profile"
|
|
i=$((i + 1))
|
|
done
|
|
echo "]"
|
|
|
|
logger -t master-link "Generated bulk tokens: batch=$batch_id count=$count profile=$profile"
|
|
}
|
|
|
|
# List bulk token batches
|
|
ml_bulk_list() {
|
|
mkdir -p "$ML_BULK_DIR"
|
|
|
|
echo "["
|
|
local first=1
|
|
for d in "$ML_BULK_DIR"/*/; do
|
|
[ -d "$d" ] || continue
|
|
[ "$first" = "1" ] || echo ","
|
|
local batch_id=$(basename "$d")
|
|
local token_count=$(ls -1 "$d"/*.meta 2>/dev/null | wc -l)
|
|
printf '{"batch_id":"%s","token_count":%d}' "$batch_id" "$token_count"
|
|
first=0
|
|
done
|
|
echo "]"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Auth Helpers
|
|
# ============================================================================
|
|
|
|
# Check if request is from local origin (127.0.0.1 or LAN)
|
|
ml_check_local_auth() {
|
|
local remote_addr="${REMOTE_ADDR:-}"
|
|
|
|
case "$remote_addr" in
|
|
127.0.0.1|::1|"")
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# Check if from LAN subnet
|
|
local lan_addr=$(uci -q get network.lan.ipaddr)
|
|
local lan_mask=$(uci -q get network.lan.netmask)
|
|
|
|
if [ -n "$lan_addr" ]; then
|
|
local lan_prefix=$(echo "$lan_addr" | cut -d. -f1-3)
|
|
local remote_prefix=$(echo "$remote_addr" | cut -d. -f1-3)
|
|
[ "$lan_prefix" = "$remote_prefix" ] && return 0
|
|
fi
|
|
|
|
# Check for LuCI session cookie
|
|
local cookie="${HTTP_COOKIE:-}"
|
|
if echo "$cookie" | grep -q "sysauth="; then
|
|
return 0
|
|
fi
|
|
|
|
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 <master_ip> <token>"}'
|
|
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
|
|
# ============================================================================
|
|
case "${1:-}" in
|
|
token-generate)
|
|
ml_token_generate
|
|
;;
|
|
token-validate)
|
|
ml_token_validate "$2"
|
|
;;
|
|
token-revoke)
|
|
ml_token_revoke "$2"
|
|
;;
|
|
token-cleanup)
|
|
ml_token_cleanup
|
|
;;
|
|
clone-token|generate-clone-token)
|
|
# Generate clone-specific auto-approve token
|
|
# Usage: master-link.sh clone-token [ttl]
|
|
ml_clone_token_generate "${2:-86400}" "clone"
|
|
;;
|
|
register-token)
|
|
# Register external token (for secubox-cloner integration)
|
|
# Usage: master-link.sh register-token <token> <ttl> <type>
|
|
local ext_token="$2"
|
|
local ext_ttl="${3:-86400}"
|
|
local ext_type="${4:-clone}"
|
|
local token_hash=$(echo "$ext_token" | sha256sum | cut -d' ' -f1)
|
|
local now=$(date +%s)
|
|
local expires=$((now + ext_ttl))
|
|
local section_id="ext_$(echo "$token_hash" | cut -c1-8)"
|
|
uci -q batch <<-EOF
|
|
set master-link.${section_id}=token
|
|
set master-link.${section_id}.hash='${token_hash}'
|
|
set master-link.${section_id}.created='${now}'
|
|
set master-link.${section_id}.expires='${expires}'
|
|
set master-link.${section_id}.peer_fp=''
|
|
set master-link.${section_id}.status='active'
|
|
set master-link.${section_id}.type='${ext_type}'
|
|
set master-link.${section_id}.auto_approve='1'
|
|
EOF
|
|
uci commit master-link
|
|
mkdir -p "$ML_TOKENS_DIR"
|
|
echo "$ext_token" > "$ML_TOKENS_DIR/${token_hash}"
|
|
logger -t master-link "External token registered: ${token_hash} (type=$ext_type, expires=$(date -d @$expires -Iseconds 2>/dev/null || echo $expires))"
|
|
echo "{\"registered\":true,\"token_hash\":\"$token_hash\",\"expires\":$expires}"
|
|
;;
|
|
join-request)
|
|
# Usage: join-request <token> <fingerprint> <addr> [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 <master_ip> <token>
|
|
ml_join_with_zkp "$2" "$3"
|
|
;;
|
|
join-approve)
|
|
ml_join_approve "$2"
|
|
;;
|
|
join-reject)
|
|
ml_join_reject "$2" "$3"
|
|
;;
|
|
promote)
|
|
ml_promote_to_submaster "$2"
|
|
;;
|
|
status)
|
|
ml_status
|
|
;;
|
|
peers)
|
|
ml_peer_list
|
|
;;
|
|
tree)
|
|
ml_tree
|
|
;;
|
|
ipk-info)
|
|
ml_ipk_bundle_info
|
|
;;
|
|
ipk-generate)
|
|
ml_ipk_generate "$2" "$3" "$4" "$5"
|
|
;;
|
|
init)
|
|
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"
|
|
;;
|
|
# Discovery mode commands
|
|
discovery-request)
|
|
# Usage: discovery-request <inventory_json> <peer_addr>
|
|
ml_discovery_request "$2" "$3"
|
|
;;
|
|
discovery-approve)
|
|
# Usage: discovery-approve <device_id> [profile]
|
|
ml_discovery_approve "$2" "$3"
|
|
;;
|
|
discovery-reject)
|
|
# Usage: discovery-reject <device_id> [reason]
|
|
ml_discovery_reject "$2" "$3"
|
|
;;
|
|
discovery-pending)
|
|
ml_discovery_pending
|
|
;;
|
|
discovery-status)
|
|
ml_discovery_status "$2"
|
|
;;
|
|
bulk-tokens)
|
|
# Usage: bulk-tokens <count> [profile] [ttl]
|
|
ml_bulk_tokens "$2" "$3" "$4"
|
|
;;
|
|
bulk-list)
|
|
ml_bulk_list
|
|
;;
|
|
*)
|
|
# Sourced as library - do nothing
|
|
:
|
|
;;
|
|
esac
|