Skip to content

Commit c3b589e

Browse files
aamirrasheedclaude
andauthored
Add SEV-SNP hardware attestation to verify command (#1)
Expand verify from 3 checks to 5: (1) SEV-SNP Hardware — gets a fresh SNP report via attestation-cli and validates the VCEK certificate chain (AMD root CA -> VCEK -> SNP report), displaying guest SVN, policy, measurement, host_data, and report_data fields; (2) TPM Attestation — existing HCL report check; (3) Host Key Binding — split out as its own check; (4) Inference Provider; (5) Access Lockout. Also makes attestation-cli path configurable via ATTESTATION_CLI_PATH env var, with auto-detection at the standard install locations. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4fbe379 commit c3b589e

1 file changed

Lines changed: 199 additions & 67 deletions

File tree

privateclaw

Lines changed: 199 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@ set -e
77

88
ATTEST_DIR="/etc/privateclaw"
99
EVIDENCE_FILE="$ATTEST_DIR/evidence.json"
10+
SNP_EVIDENCE_FILE="$ATTEST_DIR/snp_evidence.json"
1011
HOST_KEY="/etc/ssh/ssh_host_ed25519_key.pub"
1112
HOST_KEY_HASH_FILE="$ATTEST_DIR/host_key_hash.txt"
1213
INFERENCE_CONF="$ATTEST_DIR/inference.conf"
1314
ASSIGN_LOCK="$ATTEST_DIR/.assigned"
1415

16+
# attestation-cli binary path (auto-detect or override via env)
17+
ATTESTATION_CLI="${ATTESTATION_CLI_PATH:-}"
18+
if [ -z "$ATTESTATION_CLI" ]; then
19+
if command -v attestation-cli &>/dev/null; then
20+
ATTESTATION_CLI="attestation-cli"
21+
elif [ -x /opt/lunal-services/attestation-cli ]; then
22+
ATTESTATION_CLI="/opt/lunal-services/attestation-cli"
23+
fi
24+
fi
25+
1526
# Azure CVM vTPM NV indices
1627
HCL_REPORT_NV="0x01400001"
1728

@@ -38,8 +49,8 @@ cmd_attest() {
3849
echo "$HOST_KEY_HASH" > "$HOST_KEY_HASH_FILE"
3950

4051
# Try attestation-cli first (produces full JSON with cert chain + SNP report)
41-
if command -v attestation-cli &>/dev/null; then
42-
if attestation-cli attest --report-data-hex "$REPORT_DATA" -o "$EVIDENCE_FILE" 2>/dev/null; then
52+
if [ -n "$ATTESTATION_CLI" ]; then
53+
if $ATTESTATION_CLI attest --report-data-hex "$REPORT_DATA" -o "$EVIDENCE_FILE" 2>/dev/null; then
4354
echo "Attestation evidence generated via attestation-cli: $EVIDENCE_FILE"
4455
return 0
4556
fi
@@ -86,12 +97,20 @@ EOFEVIDENCE
8697
# privateclaw verify
8798
# User-facing command. Verifies TEE attestation, inference provider, and
8899
# SSH access lockout. Handles everything automatically — no manual steps.
100+
#
101+
# Checks (in order):
102+
# 1. SEV-SNP Hardware — fresh SNP report + VCEK certificate chain
103+
# 2. TPM Attestation — HCL report from vTPM NV index
104+
# 3. Host Key Binding — SSH host key hash matches boot-time record
105+
# 4. Inference Provider — endpoint reachable + attestation header
106+
# 5. Access Lockout — SSH keys + firewall
89107
# ---------------------------------------------------------------------------
90108
cmd_verify() {
91109
echo ""
92110
echo "=== PrivateClaw TEE Verification ==="
93111
echo ""
94112

113+
TOTAL_CHECKS=5
95114
PASS_COUNT=0
96115
FAIL_COUNT=0
97116

@@ -104,94 +123,205 @@ cmd_verify() {
104123
fi
105124
fi
106125

107-
# -- Check 1: TEE Attestation --
108-
echo "[1/3] TEE Attestation"
109-
110-
# If no evidence file, try to generate it now
126+
# Ensure attestation evidence exists (needed by checks 2 and 3)
111127
if [ ! -f "$EVIDENCE_FILE" ]; then
112-
echo " Generating attestation evidence..."
113-
if cmd_attest >/dev/null 2>&1; then
114-
echo " Evidence generated."
128+
cmd_attest >/dev/null 2>&1 || true
129+
fi
130+
131+
# Compute current host key hash (used by checks 1 and 3)
132+
CURRENT_HOST_KEY_HASH=""
133+
if [ -f "$HOST_KEY" ]; then
134+
CURRENT_HOST_KEY_HASH=$(sha256sum "$HOST_KEY" | awk '{print $1}')
135+
fi
136+
137+
# ==========================================================================
138+
# Check 1: SEV-SNP Hardware Attestation
139+
# ==========================================================================
140+
echo "[1/$TOTAL_CHECKS] SEV-SNP Hardware"
141+
142+
if [ -z "$ATTESTATION_CLI" ]; then
143+
echo " Status: SKIP (attestation-cli not found)"
144+
echo " Hint: install attestation-cli or set ATTESTATION_CLI_PATH"
145+
FAIL_COUNT=$((FAIL_COUNT + 1))
146+
echo ""
147+
else
148+
# Request a fresh SNP attestation report with the host key hash as report_data
149+
SNP_REPORT_DATA=""
150+
if [ -n "$CURRENT_HOST_KEY_HASH" ]; then
151+
SNP_REPORT_DATA=$(printf '%-128s' "$CURRENT_HOST_KEY_HASH" | tr ' ' '0')
152+
fi
153+
154+
SNP_TMPFILE=$(mktemp /tmp/snp_evidence_XXXXXX.json)
155+
SNP_OK=false
156+
157+
if [ -n "$SNP_REPORT_DATA" ]; then
158+
SNP_ATTEST_OUT=$($ATTESTATION_CLI attest \
159+
--platform az-snp \
160+
--report-data-hex "$SNP_REPORT_DATA" \
161+
-o "$SNP_TMPFILE" 2>&1) || true
162+
else
163+
SNP_ATTEST_OUT=$($ATTESTATION_CLI attest \
164+
--platform az-snp \
165+
-o "$SNP_TMPFILE" 2>&1) || true
115166
fi
167+
168+
if [ -f "$SNP_TMPFILE" ] && [ -s "$SNP_TMPFILE" ] && jq -e . "$SNP_TMPFILE" &>/dev/null; then
169+
# Verify the SNP report via attestation-cli (validates VCEK cert chain)
170+
SNP_VERIFY_RESULT=$($ATTESTATION_CLI verify -e "$SNP_TMPFILE" 2>/dev/null) || true
171+
172+
if [ -n "$SNP_VERIFY_RESULT" ] && echo "$SNP_VERIFY_RESULT" | jq -e . &>/dev/null; then
173+
SNP_SIG_VALID=$(echo "$SNP_VERIFY_RESULT" | jq -r '.signature_valid // false')
174+
SNP_PLATFORM=$(echo "$SNP_VERIFY_RESULT" | jq -r '.platform // "unknown"')
175+
176+
echo " Platform: $SNP_PLATFORM"
177+
178+
# Extract VCEK certificate subject if present
179+
VCEK_SUBJECT=$(echo "$SNP_VERIFY_RESULT" | jq -r '.vcek_subject // empty' 2>/dev/null)
180+
if [ -n "$VCEK_SUBJECT" ]; then
181+
echo " VCEK: $VCEK_SUBJECT"
182+
fi
183+
184+
# Extract SNP report fields from the evidence or verify result
185+
GUEST_SVN=$(jq -r '.snp_report.guest_svn // .guest_svn // empty' "$SNP_TMPFILE" 2>/dev/null)
186+
SNP_POLICY=$(jq -r '.snp_report.policy // .policy // empty' "$SNP_TMPFILE" 2>/dev/null)
187+
MEASUREMENT=$(jq -r '.snp_report.measurement // .measurement // empty' "$SNP_TMPFILE" 2>/dev/null)
188+
HOST_DATA=$(jq -r '.snp_report.host_data // .host_data // empty' "$SNP_TMPFILE" 2>/dev/null)
189+
REPORT_DATA_FIELD=$(jq -r '.snp_report.report_data // .report_data // empty' "$SNP_TMPFILE" 2>/dev/null)
190+
191+
[ -n "$GUEST_SVN" ] && echo " Guest SVN: $GUEST_SVN"
192+
[ -n "$SNP_POLICY" ] && echo " Policy: $SNP_POLICY"
193+
if [ -n "$MEASUREMENT" ]; then
194+
echo " Measurement: ${MEASUREMENT:0:32}..."
195+
fi
196+
if [ -n "$HOST_DATA" ] && [ "$HOST_DATA" != "0000000000000000000000000000000000000000000000000000000000000000" ]; then
197+
echo " Host Data: ${HOST_DATA:0:32}..."
198+
fi
199+
if [ -n "$REPORT_DATA_FIELD" ]; then
200+
echo " Report Data: ${REPORT_DATA_FIELD:0:32}... (SSH key hash)"
201+
fi
202+
203+
echo " VCEK Chain: $([ "$SNP_SIG_VALID" = "true" ] && echo 'VALID (AMD root CA -> VCEK -> SNP report)' || echo 'INVALID')"
204+
205+
if [ "$SNP_SIG_VALID" = "true" ]; then
206+
echo " Status: PASS"
207+
PASS_COUNT=$((PASS_COUNT + 1))
208+
SNP_OK=true
209+
# Save SNP evidence for reference
210+
cp "$SNP_TMPFILE" "$SNP_EVIDENCE_FILE" 2>/dev/null || true
211+
else
212+
echo " Status: FAIL (VCEK signature verification failed)"
213+
FAIL_COUNT=$((FAIL_COUNT + 1))
214+
fi
215+
else
216+
echo " Attestation: report obtained but verification failed"
217+
echo " Status: FAIL"
218+
FAIL_COUNT=$((FAIL_COUNT + 1))
219+
fi
220+
else
221+
echo " Error: could not obtain SNP report from hardware"
222+
if [ -n "$SNP_ATTEST_OUT" ]; then
223+
echo " Detail: $(echo "$SNP_ATTEST_OUT" | head -1)"
224+
fi
225+
echo " Status: FAIL"
226+
FAIL_COUNT=$((FAIL_COUNT + 1))
227+
fi
228+
rm -f "$SNP_TMPFILE"
229+
echo ""
116230
fi
117231

232+
# ==========================================================================
233+
# Check 2: TPM Attestation (HCL Report)
234+
# ==========================================================================
235+
echo "[2/$TOTAL_CHECKS] TPM Attestation"
236+
118237
if [ ! -f "$EVIDENCE_FILE" ]; then
119-
echo " Status: FAIL (could not generate attestation evidence)"
238+
echo " Status: FAIL (no attestation evidence found)"
120239
FAIL_COUNT=$((FAIL_COUNT + 1))
121240
echo ""
122241
else
123-
# Check what kind of evidence we have
124242
METHOD=$(jq -r '.method // "attestation-cli"' "$EVIDENCE_FILE" 2>/dev/null)
125243

126244
if [ "$METHOD" = "tpm2_nvread" ]; then
127-
# Fallback evidence: verify host key hash binding
128-
STORED_HASH=$(jq -r '.host_key_hash' "$EVIDENCE_FILE" 2>/dev/null)
129-
CURRENT_HASH=$(sha256sum "$HOST_KEY" 2>/dev/null | awk '{print $1}')
130245
HCL_PRESENT=$(jq -r '.hcl_report_hex // empty' "$EVIDENCE_FILE" 2>/dev/null)
131-
132-
echo " Platform: Azure SNP (via vTPM)"
133-
echo " Evidence: HCL report from TPM NV index"
134-
246+
echo " Method: tpm2_nvread (TPM NV index $HCL_REPORT_NV)"
135247
if [ -n "$HCL_PRESENT" ] && [ ${#HCL_PRESENT} -gt 100 ]; then
136248
echo " HCL Report: present (${#HCL_PRESENT} hex chars)"
137-
else
138-
echo " HCL Report: MISSING"
139-
fi
140-
141-
if [ "$STORED_HASH" = "$CURRENT_HASH" ]; then
142-
echo " Host Key: bound (hash matches boot-time record)"
143249
echo " Status: PASS"
144250
PASS_COUNT=$((PASS_COUNT + 1))
145251
else
146-
echo " Host Key: FAIL (hash mismatch — key may have changed since boot)"
252+
echo " HCL Report: MISSING or empty"
147253
echo " Status: FAIL"
148254
FAIL_COUNT=$((FAIL_COUNT + 1))
149255
fi
150-
echo ""
151-
152-
elif command -v attestation-cli &>/dev/null; then
153-
# attestation-cli evidence: full cryptographic verification
154-
CURRENT_HASH=$(sha256sum "$HOST_KEY" 2>/dev/null | awk '{print $1}')
155-
EXPECTED_HEX=$(printf '%-128s' "$CURRENT_HASH" | tr ' ' '0')
156-
157-
RESULT=$(attestation-cli verify \
158-
-e "$EVIDENCE_FILE" \
159-
--expected-report-data "$EXPECTED_HEX" 2>/dev/null) || true
160-
161-
if [ -z "$RESULT" ] || ! echo "$RESULT" | jq -e . &>/dev/null; then
162-
echo " Evidence: found"
163-
echo " Verification: attestation-cli verify failed"
164-
echo " Status: FAIL"
165-
FAIL_COUNT=$((FAIL_COUNT + 1))
166-
echo ""
167-
else
168-
SIG_VALID=$(echo "$RESULT" | jq -r '.signature_valid // false')
169-
RD_MATCH=$(echo "$RESULT" | jq -r '.report_data_match // false')
170-
PLATFORM=$(echo "$RESULT" | jq -r '.platform // "unknown"')
171-
172-
echo " Platform: $PLATFORM"
173-
echo " Signature: $([ "$SIG_VALID" = "true" ] && echo 'VALID (AMD cert chain verified)' || echo 'INVALID')"
174-
echo " Host Key: $([ "$RD_MATCH" = "true" ] && echo 'bound to TEE' || echo 'NOT bound')"
175-
176-
if [ "$SIG_VALID" = "true" ] && [ "$RD_MATCH" = "true" ]; then
177-
echo " Status: PASS"
178-
PASS_COUNT=$((PASS_COUNT + 1))
256+
else
257+
# Evidence from attestation-cli — verify it
258+
if [ -n "$ATTESTATION_CLI" ]; then
259+
TPM_VERIFY_RESULT=$($ATTESTATION_CLI verify -e "$EVIDENCE_FILE" 2>/dev/null) || true
260+
if [ -n "$TPM_VERIFY_RESULT" ] && echo "$TPM_VERIFY_RESULT" | jq -e . &>/dev/null; then
261+
TPM_SIG_VALID=$(echo "$TPM_VERIFY_RESULT" | jq -r '.signature_valid // false')
262+
TPM_PLATFORM=$(echo "$TPM_VERIFY_RESULT" | jq -r '.platform // "unknown"')
263+
echo " Method: attestation-cli"
264+
echo " Platform: $TPM_PLATFORM"
265+
echo " Signature: $([ "$TPM_SIG_VALID" = "true" ] && echo 'VALID' || echo 'INVALID')"
266+
if [ "$TPM_SIG_VALID" = "true" ]; then
267+
echo " Status: PASS"
268+
PASS_COUNT=$((PASS_COUNT + 1))
269+
else
270+
echo " Status: FAIL"
271+
FAIL_COUNT=$((FAIL_COUNT + 1))
272+
fi
179273
else
274+
echo " Method: attestation-cli"
275+
echo " Verification: failed"
180276
echo " Status: FAIL"
181277
FAIL_COUNT=$((FAIL_COUNT + 1))
182278
fi
183-
echo ""
279+
else
280+
echo " Method: attestation-cli (evidence present, no verifier)"
281+
echo " Status: FAIL (attestation-cli not found to verify)"
282+
FAIL_COUNT=$((FAIL_COUNT + 1))
184283
fi
284+
fi
285+
echo ""
286+
fi
287+
288+
# ==========================================================================
289+
# Check 3: Host Key Binding
290+
# ==========================================================================
291+
echo "[3/$TOTAL_CHECKS] Host Key Binding"
292+
293+
if [ -z "$CURRENT_HOST_KEY_HASH" ]; then
294+
echo " Status: FAIL (no SSH host key at $HOST_KEY)"
295+
FAIL_COUNT=$((FAIL_COUNT + 1))
296+
elif [ ! -f "$EVIDENCE_FILE" ]; then
297+
echo " Status: FAIL (no evidence file to compare)"
298+
FAIL_COUNT=$((FAIL_COUNT + 1))
299+
else
300+
STORED_HASH=$(jq -r '.host_key_hash // empty' "$EVIDENCE_FILE" 2>/dev/null)
301+
if [ -z "$STORED_HASH" ]; then
302+
# Try reading from the hash file
303+
STORED_HASH=$(cat "$HOST_KEY_HASH_FILE" 2>/dev/null | tr -d '[:space:]')
304+
fi
305+
306+
echo " Current hash: ${CURRENT_HOST_KEY_HASH:0:16}..."
307+
echo " Boot hash: ${STORED_HASH:0:16}..."
308+
309+
if [ "$STORED_HASH" = "$CURRENT_HOST_KEY_HASH" ]; then
310+
echo " Match: YES (key unchanged since boot attestation)"
311+
echo " Status: PASS"
312+
PASS_COUNT=$((PASS_COUNT + 1))
185313
else
186-
echo " Evidence: found but no verifier available"
314+
echo " Match: NO (key changed since boot — attestation is stale)"
187315
echo " Status: FAIL"
188316
FAIL_COUNT=$((FAIL_COUNT + 1))
189-
echo ""
190317
fi
191318
fi
319+
echo ""
192320

193-
# -- Check 2: Inference Provider --
194-
echo "[2/3] Inference Provider"
321+
# ==========================================================================
322+
# Check 4: Inference Provider
323+
# ==========================================================================
324+
echo "[4/$TOTAL_CHECKS] Inference Provider"
195325
OC_CONFIG="$ADMIN_HOME/.openclaw/openclaw.json"
196326
if [ ! -f "$OC_CONFIG" ]; then
197327
echo " Config: not found at $OC_CONFIG"
@@ -222,13 +352,13 @@ cmd_verify() {
222352
elif [ -n "$ATTESTATION" ]; then
223353
echo " Provider: ${INF_PROVIDER:-lunal}"
224354

225-
# Decode attestation: base64 gunzip JSON evidence
355+
# Decode attestation: base64 -> gunzip -> JSON evidence
226356
INF_ATTEST_OK=false
227357
INF_EVIDENCE_FILE=$(mktemp /tmp/inference_attestation_XXXXXX.json)
228358
if echo "$ATTESTATION" | base64 -d 2>/dev/null | gunzip > "$INF_EVIDENCE_FILE" 2>/dev/null; then
229359
# Verify with attestation-cli if available
230-
if command -v attestation-cli &>/dev/null; then
231-
INF_VERIFY_RESULT=$(attestation-cli verify -e "$INF_EVIDENCE_FILE" 2>/dev/null) || true
360+
if [ -n "$ATTESTATION_CLI" ]; then
361+
INF_VERIFY_RESULT=$($ATTESTATION_CLI verify -e "$INF_EVIDENCE_FILE" 2>/dev/null) || true
232362
if [ -n "$INF_VERIFY_RESULT" ] && echo "$INF_VERIFY_RESULT" | jq -e . &>/dev/null; then
233363
INF_SIG_VALID=$(echo "$INF_VERIFY_RESULT" | jq -r '.signature_valid // false')
234364
INF_PLATFORM=$(echo "$INF_VERIFY_RESULT" | jq -r '.platform // "unknown"')
@@ -275,8 +405,10 @@ cmd_verify() {
275405
echo ""
276406
fi
277407

278-
# -- Check 3: External Access Lockout --
279-
echo "[3/3] External Access Lockout"
408+
# ==========================================================================
409+
# Check 5: External Access Lockout
410+
# ==========================================================================
411+
echo "[5/$TOTAL_CHECKS] External Access Lockout"
280412
KEY_COUNT=$(grep -c '^ssh-' "$ADMIN_HOME/.ssh/authorized_keys" 2>/dev/null || echo 0)
281413
KEY_COUNT="${KEY_COUNT:-0}"
282414
echo " SSH keys: $KEY_COUNT authorized"
@@ -296,9 +428,9 @@ cmd_verify() {
296428
# -- Summary --
297429
echo "---"
298430
if [ "$FAIL_COUNT" -eq 0 ]; then
299-
echo "All checks passed ($PASS_COUNT/3)."
431+
echo "All checks passed ($PASS_COUNT/$TOTAL_CHECKS)."
300432
else
301-
echo "$PASS_COUNT passed, $FAIL_COUNT failed."
433+
echo "$PASS_COUNT passed, $FAIL_COUNT failed or warned."
302434
fi
303435
echo ""
304436
}
@@ -476,7 +608,7 @@ case "${1:-}" in
476608
echo "Usage: privateclaw <command>"
477609
echo ""
478610
echo "Commands:"
479-
echo " verify Verify TEE attestation, inference provider, and access lockout"
611+
echo " verify Verify SEV-SNP hardware, TPM, host key, inference, and access lockout"
480612
echo " attest Generate attestation evidence (run at boot)"
481613
echo " assign Apply user configuration from IMDS (run by systemd)"
482614
;;

0 commit comments

Comments
 (0)