Skip to content

Commit 4ad616a

Browse files
Merge pull request #169 from boffin-dmytro/main
: add SHA256 integrity verification for Linux GGUF downloads
2 parents a2526ce + 0db197e commit 4ad616a

4 files changed

Lines changed: 333 additions & 28 deletions

File tree

dream-server/SECURITY.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,41 @@ tar -cz data/ | gpg -c > dream-backup-$(date +%Y%m%d).tar.gz.gpg
159159
gpg -d dream-backup-YYYYMMDD.tar.gz.gpg | tar -xz
160160
```
161161

162+
### Model Download Integrity
163+
164+
The installer verifies GGUF model downloads using SHA256 checksums to prevent:
165+
- Corrupted downloads from network issues
166+
- Truncated files from interrupted transfers
167+
- Potential supply chain attacks
168+
169+
**How it works:**
170+
1. Before installation: checks existing model files against known checksums
171+
2. After download: verifies freshly downloaded models
172+
3. On mismatch: removes corrupt file and prompts for re-download
173+
174+
**Verification happens automatically** during installation. If a model fails verification:
175+
```bash
176+
# The installer will show:
177+
# ✗ Downloaded file is corrupt (SHA256 mismatch)
178+
# Expected: 9f1a24700a339b09c06009b729b5c809e0b64c213b8af5b711b3dbdfd0c5ba48
179+
# Got: [actual hash]
180+
# Corrupt file removed. Re-run installer to download again.
181+
182+
# Simply re-run the installer:
183+
./install.sh
184+
```
185+
186+
**Manual verification:**
187+
```bash
188+
# Check a model file manually
189+
sha256sum data/models/Qwen3-8B-Q4_K_M.gguf
190+
191+
# Compare against expected hash in installers/lib/tier-map.sh
192+
grep -A 2 "Qwen3-8B" installers/lib/tier-map.sh | grep GGUF_SHA256
193+
```
194+
195+
**Note:** Some models (like Qwen3-14B and qwen3-coder-next) don't have checksums yet. The installer will skip verification for these but still download them successfully.
196+
162197
---
163198

164199
## API Security

dream-server/installers/lib/tier-map.sh

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,55 +21,63 @@ resolve_tier_config() {
2121
LLM_MODEL="anthropic/claude-sonnet-4-5-20250514"
2222
GGUF_FILE=""
2323
GGUF_URL=""
24+
GGUF_SHA256=""
2425
MAX_CONTEXT=200000
2526
;;
2627
NV_ULTRA)
2728
TIER_NAME="NVIDIA Ultra (90GB+)"
2829
LLM_MODEL="qwen3-coder-next"
2930
GGUF_FILE="qwen3-coder-next-Q4_K_M.gguf"
3031
GGUF_URL="https://huggingface.co/unsloth/Qwen3-Coder-Next-GGUF/resolve/main/Qwen3-Coder-Next-Q4_K_M.gguf"
32+
GGUF_SHA256=""
3133
MAX_CONTEXT=131072
3234
;;
3335
SH_LARGE)
3436
TIER_NAME="Strix Halo 90+"
3537
LLM_MODEL="qwen3-coder-next"
3638
GGUF_FILE="qwen3-coder-next-Q4_K_M.gguf"
3739
GGUF_URL="https://huggingface.co/unsloth/Qwen3-Coder-Next-GGUF/resolve/main/Qwen3-Coder-Next-Q4_K_M.gguf"
40+
GGUF_SHA256=""
3841
MAX_CONTEXT=131072
3942
;;
4043
SH_COMPACT)
4144
TIER_NAME="Strix Halo Compact"
4245
LLM_MODEL="qwen3-30b-a3b"
43-
GGUF_FILE="qwen3-30b-a3b-Q4_K_M.gguf"
46+
GGUF_FILE="Qwen3-30B-A3B-Q4_K_M.gguf"
4447
GGUF_URL="https://huggingface.co/unsloth/Qwen3-30B-A3B-GGUF/resolve/main/Qwen3-30B-A3B-Q4_K_M.gguf"
48+
GGUF_SHA256="9f1a24700a339b09c06009b729b5c809e0b64c213b8af5b711b3dbdfd0c5ba48"
4549
MAX_CONTEXT=131072
4650
;;
4751
1)
4852
TIER_NAME="Entry Level"
4953
LLM_MODEL="qwen3-8b"
5054
GGUF_FILE="Qwen3-8B-Q4_K_M.gguf"
5155
GGUF_URL="https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf"
56+
GGUF_SHA256="120307ba529eb2439d6c430d94104dabd578497bc7bfe7e322b5d9933b449bd4"
5257
MAX_CONTEXT=16384
5358
;;
5459
2)
5560
TIER_NAME="Prosumer"
5661
LLM_MODEL="qwen3-8b"
5762
GGUF_FILE="Qwen3-8B-Q4_K_M.gguf"
5863
GGUF_URL="https://huggingface.co/unsloth/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.gguf"
64+
GGUF_SHA256="120307ba529eb2439d6c430d94104dabd578497bc7bfe7e322b5d9933b449bd4"
5965
MAX_CONTEXT=32768
6066
;;
6167
3)
6268
TIER_NAME="Pro"
6369
LLM_MODEL="qwen3-14b"
6470
GGUF_FILE="Qwen3-14B-Q4_K_M.gguf"
6571
GGUF_URL="https://huggingface.co/unsloth/Qwen3-14B-GGUF/resolve/main/Qwen3-14B-Q4_K_M.gguf"
72+
GGUF_SHA256=""
6673
MAX_CONTEXT=32768
6774
;;
6875
4)
6976
TIER_NAME="Enterprise"
7077
LLM_MODEL="qwen3-30b-a3b"
71-
GGUF_FILE="qwen3-30b-a3b-Q4_K_M.gguf"
78+
GGUF_FILE="Qwen3-30B-A3B-Q4_K_M.gguf"
7279
GGUF_URL="https://huggingface.co/unsloth/Qwen3-30B-A3B-GGUF/resolve/main/Qwen3-30B-A3B-Q4_K_M.gguf"
80+
GGUF_SHA256="9f1a24700a339b09c06009b729b5c809e0b64c213b8af5b711b3dbdfd0c5ba48"
7381
MAX_CONTEXT=131072
7482
;;
7583
*)

dream-server/installers/phases/11-services.sh

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,37 +40,96 @@ else
4040
# Ensure model directory exists
4141
mkdir -p "$INSTALL_DIR/data/models"
4242

43-
# Download GGUF model if not already present (with retry)
43+
# Download GGUF model if not already present (with retry and integrity verification)
4444
GGUF_DIR="$INSTALL_DIR/data/models"
45-
if [[ "${DREAM_MODE:-local}" != "cloud" && ! -f "$GGUF_DIR/$GGUF_FILE" && -n "$GGUF_URL" ]]; then
46-
ai "Downloading GGUF model: $GGUF_FILE"
47-
signal "This is the big one. I've got it — sit back."
48-
echo ""
45+
if [[ "${DREAM_MODE:-local}" != "cloud" && -n "$GGUF_URL" ]]; then
46+
# Check if model exists and verify integrity
47+
if [[ -f "$GGUF_DIR/$GGUF_FILE" ]]; then
48+
if [[ -n "$GGUF_SHA256" ]]; then
49+
if command -v sha256sum &>/dev/null; then
50+
ai "Verifying model integrity (SHA256)..."
51+
ACTUAL_HASH=$(sha256sum "$GGUF_DIR/$GGUF_FILE" 2>/dev/null | awk '{print $1}')
52+
if [[ -n "$ACTUAL_HASH" && "$ACTUAL_HASH" == "$GGUF_SHA256" ]]; then
53+
ai_ok "Model verified: $GGUF_FILE"
54+
elif [[ -z "$ACTUAL_HASH" ]]; then
55+
ai_warn "Could not compute checksum for existing model file"
56+
ai_ok "GGUF model already present: $GGUF_FILE (verification skipped)"
57+
else
58+
ai_warn "Model file is corrupt (SHA256 mismatch)."
59+
ai " Expected: $GGUF_SHA256"
60+
ai " Got: $ACTUAL_HASH"
61+
ai "Removing corrupt file and re-downloading..."
62+
rm -f "$GGUF_DIR/$GGUF_FILE"
63+
fi
64+
else
65+
ai_warn "sha256sum not available, skipping integrity check"
66+
ai_ok "GGUF model already present: $GGUF_FILE (verification skipped)"
67+
fi
68+
else
69+
ai_ok "GGUF model already present: $GGUF_FILE"
70+
fi
71+
fi
72+
73+
# Download if not present or was removed due to corruption
74+
if [[ ! -f "$GGUF_DIR/$GGUF_FILE" ]]; then
75+
ai "Downloading GGUF model: $GGUF_FILE"
76+
signal "This is the big one. I've got it — sit back."
77+
echo ""
4978

50-
# Retry loop: up to 3 attempts with resume support (-c flag)
51-
_dl_success=false
52-
for _attempt in 1 2 3; do
53-
[[ $_attempt -gt 1 ]] && ai "Retry attempt $_attempt of 3..."
54-
wget -c -q -O "$GGUF_DIR/$GGUF_FILE.part" "$GGUF_URL" \
55-
>> "$INSTALL_DIR/logs/model-download.log" 2>&1 &
56-
dl_pid=$!
57-
58-
if spin_task $dl_pid "Downloading $GGUF_FILE"; then
59-
mv "$GGUF_DIR/$GGUF_FILE.part" "$GGUF_DIR/$GGUF_FILE"
60-
printf "\r ${BGRN}${NC} %-60s\n" "Model downloaded: $GGUF_FILE"
61-
_dl_success=true
62-
break
79+
# Retry loop: up to 3 attempts with resume support (-c flag)
80+
_dl_success=false
81+
for _attempt in 1 2 3; do
82+
[[ $_attempt -gt 1 ]] && ai "Retry attempt $_attempt of 3..."
83+
wget -c -q -O "$GGUF_DIR/$GGUF_FILE.part" "$GGUF_URL" \
84+
>> "$INSTALL_DIR/logs/model-download.log" 2>&1 &
85+
dl_pid=$!
86+
87+
if spin_task $dl_pid "Downloading $GGUF_FILE"; then
88+
mv "$GGUF_DIR/$GGUF_FILE.part" "$GGUF_DIR/$GGUF_FILE"
89+
printf "\r ${BGRN}${NC} %-60s\n" "Model downloaded: $GGUF_FILE"
90+
_dl_success=true
91+
break
92+
fi
93+
printf "\r ${AMB}${NC} %-60s\n" "Download attempt $_attempt failed"
94+
sleep 3
95+
done
96+
97+
if [[ "$_dl_success" != "true" ]]; then
98+
printf "\r ${RED}${NC} %-60s\n" "Download failed after 3 attempts: $GGUF_FILE"
99+
ai "Manual retry: wget -c -O '$GGUF_DIR/$GGUF_FILE.part' '$GGUF_URL' && mv '$GGUF_DIR/$GGUF_FILE.part' '$GGUF_DIR/$GGUF_FILE'"
100+
else
101+
# Verify freshly downloaded file
102+
if [[ -n "$GGUF_SHA256" ]]; then
103+
if command -v sha256sum &>/dev/null; then
104+
ai "Verifying download integrity (SHA256)..."
105+
ACTUAL_HASH=$(sha256sum "$GGUF_DIR/$GGUF_FILE" 2>/dev/null | awk '{print $1}')
106+
if [[ -n "$ACTUAL_HASH" && "$ACTUAL_HASH" == "$GGUF_SHA256" ]]; then
107+
ai_ok "Download verified OK"
108+
elif [[ -z "$ACTUAL_HASH" ]]; then
109+
ai_warn "Could not compute checksum for downloaded file"
110+
ai_warn "Proceeding without verification (file may be corrupt)"
111+
else
112+
printf "\r ${RED}${NC} %-60s\n" "Downloaded file is corrupt (SHA256 mismatch)"
113+
ai " Expected: $GGUF_SHA256"
114+
ai " Got: $ACTUAL_HASH"
115+
rm -f "$GGUF_DIR/$GGUF_FILE"
116+
ai_warn "Corrupt file removed. Re-run installer to download again."
117+
_dl_success=false
118+
fi
119+
else
120+
ai_warn "sha256sum not available, skipping integrity check"
121+
ai_warn "Proceeding without verification (file may be corrupt)"
122+
fi
123+
fi
63124
fi
64-
printf "\r ${AMB}${NC} %-60s\n" "Download attempt $_attempt failed"
65-
sleep 3
66-
done
125+
fi
67126

68-
if [[ "$_dl_success" != "true" ]]; then
69-
printf "\r ${RED}${NC} %-60s\n" "Download failed after 3 attempts: $GGUF_FILE"
70-
ai "Manual retry: wget -c -O '$GGUF_DIR/$GGUF_FILE.part' '$GGUF_URL' && mv '$GGUF_DIR/$GGUF_FILE.part' '$GGUF_DIR/$GGUF_FILE'"
127+
# Abort if model download/verification failed
128+
if [[ "${DREAM_MODE:-local}" != "cloud" && -n "$GGUF_URL" && ! -f "$GGUF_DIR/$GGUF_FILE" ]]; then
129+
ai_bad "Model file missing or verification failed. Cannot proceed without a valid model."
130+
ai "Re-run the installer to retry the download."
131+
exit 1
71132
fi
72-
elif [[ -f "$GGUF_DIR/$GGUF_FILE" ]]; then
73-
ai_ok "GGUF model already present: $GGUF_FILE"
74133
fi
75134

76135
# ── FLUX.1-schnell model download (ComfyUI image generation) ──

0 commit comments

Comments
 (0)