From 69ae5a8a9b65461583b055fdeb0cfca5551ff10b Mon Sep 17 00:00:00 2001 From: Alex Newman Date: Fri, 29 May 2026 18:34:44 +0000 Subject: [PATCH] Serve noise.ita_token so clients verify attestation without minting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent already mints an Intel Trust Authority token for CP registration (over a freshness nonce). Clients connecting directly over Noise had to mint their own appraisal of the agent's separate Noise quote, which required every client to hold an ITA API key (an Intel account) — minting needs a key, but verifying a token only needs Intel's public JWKS. Mint a second token over the Noise quote (the one binding the Noise pubkey into report_data) and serve it on /health as noise.ita_token, refreshed alongside the registration token. Clients now verify this token against the public JWKS — no account — and check the report_data binding, instead of minting. Additive: the existing quote_b64/pubkey_hex fields stay for older/insecure-skip clients. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/agent.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/agent.rs b/src/agent.rs index b49ff90..c6bcb0a 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -64,8 +64,14 @@ struct St { /// CP keys off this to look up the tunnel_id. agent_id: String, started: Instant, - /// Current Intel-signed JWT. Refreshed by a background task. + /// Current Intel-signed JWT minted over a freshness nonce. Used by the CP's + /// scrape-and-verify loop. Refreshed by a background task. ita_token: Arc>, + /// Intel-signed JWT minted over the *Noise* quote (binds the Noise pubkey in + /// `report_data`). Served as `noise.ita_token` so clients can verify the + /// agent's attestation against Intel's public JWKS without an ITA account of + /// their own — they verify, they don't mint. Refreshed alongside `ita_token`. + noise_ita_token: Arc>, /// Live set of per-workload ingress rules this agent has asked /// the CP to publish. Seeded from boot `cfg.extra_ingress`; /// appended each time a POSTed workload declares `expose`. The @@ -174,6 +180,34 @@ pub async fn run() -> Result<()> { .map_err(|e| Error::Internal(format!("noise keypair: {e}")))?, ); eprintln!("agent: noise_pubkey={}", hex::encode(attestor.public_key())); + + // Mint an ITA token over the Noise quote so clients can verify the agent's + // attestation against Intel's public JWKS without minting (and thus without + // an Intel account). Stable per boot; refreshed before expiry like the + // registration token. + let noise_ita_token = Arc::new(RwLock::new( + mint_noise_ita(&cfg, &attestor).await.unwrap_or_else(|e| { + eprintln!("agent: initial noise ITA mint failed ({e}); serving empty until refresh"); + String::new() + }), + )); + { + let cfg = cfg.clone(); + let attestor = attestor.clone(); + let token = noise_ita_token.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(ITA_REFRESH).await; + match mint_noise_ita(&cfg, &attestor).await { + Ok(t) => *token.write().await = t, + Err(e) => { + eprintln!("agent: noise ITA refresh failed (keeping stale token): {e}") + } + } + } + }); + } + let ee_token = std::env::var("EE_TOKEN").ok(); let upstream = Arc::new(noise_gateway::upstream::EeAgent::new( std::path::PathBuf::from(noise_gateway::upstream::DEFAULT_EE_AGENT_SOCK), @@ -212,6 +246,7 @@ pub async fn run() -> Result<()> { agent_id: b.agent_id, started: Instant::now(), ita_token, + noise_ita_token, extras: Arc::new(RwLock::new(cfg.extra_ingress.clone())), gh, attest: attestor, @@ -487,6 +522,18 @@ async fn mint_ita(cfg: &Cfg, ee: &Ee) -> Result { ita::mint(&cfg.ita.base_url, &cfg.ita.api_key, "e_b64).await } +/// Mint an Intel-signed appraisal of the *Noise* quote (the one binding the +/// Noise pubkey into `report_data`). Served on `/health` as `noise.ita_token` +/// so clients can verify it against Intel's public JWKS without an account. +async fn mint_noise_ita(cfg: &Cfg, attest: &noise_gateway::attest::Attestor) -> Result { + if cfg.ita.mode == ItaMode::Local { + return ita::mint_local(&cfg.ita.issuer, &cfg.ita.api_key, &cfg.common.vm_name); + } + use base64::Engine; + let quote_b64 = base64::engine::general_purpose::STANDARD.encode(attest.quote()); + ita::mint(&cfg.ita.base_url, &cfg.ita.api_key, "e_b64).await +} + fn spawn_cloudflared(token: String) { tokio::spawn(async move { eprintln!("agent: spawning cloudflared"); @@ -546,6 +593,7 @@ async fn health(State(s): State) -> Json { .unwrap_or_default(); let m = metrics::collect().await; let ita_token = s.ita_token.read().await.clone(); + let noise_ita_token = s.noise_ita_token.read().await.clone(); let agent_owner = s.agent_owner.read().await.clone(); let oracles = s.oracles.read().await.clone(); let taint_reasons = s.taint.snapshot().await; @@ -630,6 +678,10 @@ async fn health(State(s): State) -> Json { "noise": { "quote_b64": base64::engine::general_purpose::STANDARD.encode(s.attest.quote()), "pubkey_hex": hex::encode(s.attest.public_key()), + // Intel-signed appraisal of the Noise quote. Clients verify this + // against Intel's public JWKS (no account) instead of minting their + // own. Empty if minting failed at boot / in local ITA mode. + "ita_token": noise_ita_token, }, })) }