77
88ATTEST_DIR=" /etc/privateclaw"
99EVIDENCE_FILE=" $ATTEST_DIR /evidence.json"
10+ SNP_EVIDENCE_FILE=" $ATTEST_DIR /snp_evidence.json"
1011HOST_KEY=" /etc/ssh/ssh_host_ed25519_key.pub"
1112HOST_KEY_HASH_FILE=" $ATTEST_DIR /host_key_hash.txt"
1213INFERENCE_CONF=" $ATTEST_DIR /inference.conf"
1314ASSIGN_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
1627HCL_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# ---------------------------------------------------------------------------
90108cmd_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