token.rs
1 use pyo3::prelude::*; 2 use serde::{Deserialize, Serialize}; 3 use std::sync::OnceLock; 4 5 use crate::runtime::runtime; 6 7 static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new(); 8 9 fn http_client() -> &'static reqwest::Client { 10 HTTP_CLIENT.get_or_init(|| { 11 reqwest::Client::builder() 12 .timeout(std::time::Duration::from_secs(30)) 13 .build() 14 .expect("failed to create HTTP client") 15 }) 16 } 17 18 #[derive(Serialize)] 19 struct ExchangeRequest { 20 attestation_chain: serde_json::Value, 21 root_public_key: String, 22 #[serde(skip_serializing_if = "Option::is_none")] 23 requested_capabilities: Option<Vec<String>>, 24 } 25 26 #[derive(Deserialize)] 27 struct TokenResponse { 28 access_token: String, 29 } 30 31 /// Exchange an attestation chain for a Bearer token via the OIDC bridge. 32 /// 33 /// Args: 34 /// * `bridge_url`: The OIDC bridge base URL. 35 /// * `chain_json`: JSON string of the attestation chain array. 36 /// * `root_public_key`: Hex-encoded Ed25519 public key. 37 /// * `capabilities`: List of requested capability strings. 38 /// 39 /// Usage: 40 /// ```ignore 41 /// let token = get_token(py, "https://bridge.example.com", "[...]", "abcd...", vec![])?; 42 /// ``` 43 #[pyfunction] 44 pub fn get_token( 45 py: Python<'_>, 46 bridge_url: String, 47 chain_json: String, 48 root_public_key: String, 49 capabilities: Vec<String>, 50 ) -> PyResult<String> { 51 let chain: serde_json::Value = serde_json::from_str(&chain_json).map_err(|e| { 52 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("invalid chain JSON: {e}")) 53 })?; 54 55 let url = format!("{}/token", bridge_url.trim_end_matches('/')); 56 let request_body = ExchangeRequest { 57 attestation_chain: chain, 58 root_public_key, 59 requested_capabilities: if capabilities.is_empty() { 60 None 61 } else { 62 Some(capabilities) 63 }, 64 }; 65 66 py.allow_threads(|| { 67 runtime().block_on(async { 68 let response = http_client() 69 .post(&url) 70 .json(&request_body) 71 .send() 72 .await 73 .map_err(|e| { 74 PyErr::new::<pyo3::exceptions::PyConnectionError, _>(format!( 75 "bridge unreachable: {e}" 76 )) 77 })?; 78 79 if !response.status().is_success() { 80 let status = response.status().as_u16(); 81 let body = response.text().await.unwrap_or_default(); 82 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!( 83 "token exchange failed (HTTP {status}): {body}" 84 ))); 85 } 86 87 let token_resp: TokenResponse = response.json().await.map_err(|e| { 88 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("invalid response: {e}")) 89 })?; 90 91 Ok(token_resp.access_token) 92 }) 93 }) 94 }