From 3b6105d5ea7a409376712aaa56ddcb8b79639a8c Mon Sep 17 00:00:00 2001 From: Pierre LE GUEN <26087574+PierreLeGuen@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:25:59 -0800 Subject: [PATCH] feat: add TEE attestation shield to web gateway UI (#275) Show a shield indicator in the tab bar when the instance is running inside a TEE deployment. On hover, fetches and displays the TDX attestation report (image digest, TLS cert fingerprint, report data, VM config) from the management API. Co-authored-by: Cursor --- src/channels/web/static/app.js | 95 +++++++++++++++++++++++ src/channels/web/static/index.html | 7 ++ src/channels/web/static/style.css | 120 +++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 8d19b497..bdf8b454 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -38,6 +38,7 @@ function authenticate() { connectSSE(); connectLogSSE(); startGatewayStatusPolling(); + checkTeeStatus(); loadThreads(); loadMemoryTree(); loadJobs(); @@ -2098,6 +2099,100 @@ document.getElementById('gateway-status-trigger').addEventListener('mouseleave', document.getElementById('gateway-popover').classList.remove('visible'); }); +// --- TEE attestation --- + +let teeInfo = null; +let teeReportCache = null; +let teeReportLoading = false; + +function teeApiBase() { + var parts = window.location.hostname.split('.'); + if (parts.length < 2) return null; + var domain = parts.slice(1).join('.'); + return window.location.protocol + '//api.' + domain; +} + +function teeInstanceName() { + return window.location.hostname.split('.')[0]; +} + +function checkTeeStatus() { + var base = teeApiBase(); + if (!base) return; + var name = teeInstanceName(); + fetch(base + '/instances/' + encodeURIComponent(name) + '/attestation').then(function(res) { + if (!res.ok) throw new Error(res.status); + return res.json(); + }).then(function(data) { + teeInfo = data; + document.getElementById('tee-shield').style.display = 'flex'; + }).catch(function() {}); +} + +function fetchTeeReport() { + if (teeReportCache) { + renderTeePopover(teeReportCache); + return; + } + if (teeReportLoading) return; + teeReportLoading = true; + var base = teeApiBase(); + if (!base) return; + var popover = document.getElementById('tee-popover'); + popover.innerHTML = '
Loading attestation report...
'; + fetch(base + '/attestation/report').then(function(res) { + if (!res.ok) throw new Error(res.status); + return res.json(); + }).then(function(data) { + teeReportCache = data; + renderTeePopover(data); + }).catch(function() { + popover.innerHTML = '
Could not load attestation report
'; + }).finally(function() { + teeReportLoading = false; + }); +} + +function renderTeePopover(report) { + var popover = document.getElementById('tee-popover'); + var digest = (teeInfo && teeInfo.image_digest) || 'N/A'; + var fingerprint = report.tls_certificate_fingerprint || 'N/A'; + var reportData = report.report_data || ''; + var vmConfig = report.vm_config || 'N/A'; + var truncated = reportData.length > 32 ? reportData.slice(0, 32) + '...' : reportData; + popover.innerHTML = '
' + + '' + + 'TEE Attestation
' + + '
Image Digest
' + + '
' + escapeHtml(digest) + '
' + + '
TLS Certificate Fingerprint
' + + '
' + escapeHtml(fingerprint) + '
' + + '
Report Data
' + + '
' + escapeHtml(truncated) + '
' + + '
VM Config
' + + '
' + escapeHtml(vmConfig) + '
' + + '
' + + '
'; +} + +function copyTeeReport() { + if (!teeReportCache) return; + var combined = Object.assign({}, teeReportCache, teeInfo || {}); + navigator.clipboard.writeText(JSON.stringify(combined, null, 2)).then(function() { + showToast('Attestation report copied', 'success'); + }).catch(function() { + showToast('Failed to copy report', 'error'); + }); +} + +document.getElementById('tee-shield').addEventListener('mouseenter', function() { + fetchTeeReport(); + document.getElementById('tee-popover').classList.add('visible'); +}); +document.getElementById('tee-shield').addEventListener('mouseleave', function() { + document.getElementById('tee-popover').classList.remove('visible'); +}); + // --- Extension install --- function installExtension() { diff --git a/src/channels/web/static/index.html b/src/channels/web/static/index.html index 125dd586..d7f65025 100644 --- a/src/channels/web/static/index.html +++ b/src/channels/web/static/index.html @@ -40,6 +40,13 @@
+
Connected diff --git a/src/channels/web/static/style.css b/src/channels/web/static/style.css index d28edc2b..df3be9b8 100644 --- a/src/channels/web/static/style.css +++ b/src/channels/web/static/style.css @@ -189,6 +189,126 @@ body { background: var(--danger); } +/* TEE Shield */ +.tee-shield { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--success); + padding: 4px 10px; + border-radius: 12px; + background: rgba(63, 185, 80, 0.1); + border: 1px solid rgba(63, 185, 80, 0.25); + cursor: pointer; + position: relative; + margin-right: 8px; + transition: background 0.15s; +} + +.tee-shield:hover { + background: rgba(63, 185, 80, 0.18); +} + +.tee-shield svg { + flex-shrink: 0; +} + +#tee-shield-label { + font-weight: 500; + white-space: nowrap; +} + +.tee-popover { + display: none; + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + min-width: 340px; + max-width: 420px; + z-index: 100; + box-shadow: var(--shadow); +} + +.tee-popover.visible { + display: block; +} + +.tee-popover-title { + font-size: 13px; + font-weight: 600; + color: var(--text); + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 6px; +} + +.tee-popover-title svg { + color: var(--success); +} + +.tee-field { + margin-bottom: 10px; +} + +.tee-field:last-child { + margin-bottom: 0; +} + +.tee-field-label { + font-size: 11px; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 3px; +} + +.tee-field-value { + font-size: 12px; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + color: var(--text); + word-break: break-all; + background: var(--bg); + padding: 4px 8px; + border-radius: 4px; + border: 1px solid var(--border); +} + +.tee-popover-actions { + margin-top: 12px; + display: flex; + gap: 8px; +} + +.tee-btn-copy { + padding: 4px 10px; + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-secondary); + cursor: pointer; + font-size: 11px; + transition: color 0.15s, border-color 0.15s; +} + +.tee-btn-copy:hover { + color: var(--text); + border-color: var(--text-secondary); +} + +.tee-popover-loading { + font-size: 12px; + color: var(--text-secondary); + padding: 8px 0; +} + /* Tab Panels */ .tab-panel { display: none;