1use a3s_box_core::error::{BoxError, Result};
8use std::path::PathBuf;
9
10use super::attestation::{CertificateChain, TcbVersion};
11
12const AMD_KDS_BASE_URL: &str = "https://kds.amd.com";
14
15const AMD_KDS_VCEK_PATH: &str = "vcek/v1";
17
18const PRODUCT_MILAN: &str = "Milan";
20
21const PRODUCT_GENOA: &str = "Genoa";
23
24pub struct AmdKdsClient {
26 http: reqwest::Client,
28 cache_dir: Option<PathBuf>,
30}
31
32impl AmdKdsClient {
33 pub fn new(cache_dir: Option<PathBuf>) -> Self {
39 Self {
40 http: reqwest::Client::builder()
41 .no_proxy()
42 .build()
43 .expect("failed to build AMD KDS HTTP client"),
44 cache_dir,
45 }
46 }
47
48 pub async fn fetch_cert_chain(
57 &self,
58 chip_id: &str,
59 tcb: &TcbVersion,
60 product: &str,
61 ) -> Result<CertificateChain> {
62 if let Some(cached) = self.load_from_cache(chip_id, tcb).await {
64 tracing::debug!(chip_id = &chip_id[..16], "Using cached certificate chain");
65 return Ok(cached);
66 }
67
68 let vcek = self.fetch_vcek(chip_id, tcb, product).await?;
70
71 let (ask, ark) = self.fetch_ask_ark(product).await?;
73
74 let chain = CertificateChain { vcek, ask, ark };
75
76 self.save_to_cache(chip_id, tcb, &chain).await;
78
79 Ok(chain)
80 }
81
82 async fn fetch_vcek(&self, chip_id: &str, tcb: &TcbVersion, product: &str) -> Result<Vec<u8>> {
86 let url = format!(
87 "{}/{}/{}/{}?blSPL={}&teeSPL={}&snpSPL={}&ucodeSPL={}",
88 AMD_KDS_BASE_URL,
89 AMD_KDS_VCEK_PATH,
90 product,
91 chip_id,
92 tcb.boot_loader,
93 tcb.tee,
94 tcb.snp,
95 tcb.microcode,
96 );
97
98 tracing::debug!(url = %url, "Fetching VCEK from AMD KDS");
99
100 let response = self
101 .http
102 .get(&url)
103 .header("Accept", "application/x-pem-file")
104 .send()
105 .await
106 .map_err(|e| {
107 BoxError::AttestationError(format!("Failed to fetch VCEK from AMD KDS: {}", e))
108 })?;
109
110 if !response.status().is_success() {
111 return Err(BoxError::AttestationError(format!(
112 "AMD KDS returned {} for VCEK request",
113 response.status()
114 )));
115 }
116
117 response
118 .bytes()
119 .await
120 .map(|b| b.to_vec())
121 .map_err(|e| BoxError::AttestationError(format!("Failed to read VCEK response: {}", e)))
122 }
123
124 async fn fetch_ask_ark(&self, product: &str) -> Result<(Vec<u8>, Vec<u8>)> {
129 let url = format!(
130 "{}/{}/{}/{}",
131 AMD_KDS_BASE_URL, AMD_KDS_VCEK_PATH, product, "cert_chain",
132 );
133
134 tracing::debug!(url = %url, "Fetching ASK+ARK from AMD KDS");
135
136 let response = self
137 .http
138 .get(&url)
139 .header("Accept", "application/x-pem-file")
140 .send()
141 .await
142 .map_err(|e| {
143 BoxError::AttestationError(format!(
144 "Failed to fetch cert chain from AMD KDS: {}",
145 e
146 ))
147 })?;
148
149 if !response.status().is_success() {
150 return Err(BoxError::AttestationError(format!(
151 "AMD KDS returned {} for cert chain request",
152 response.status()
153 )));
154 }
155
156 let pem_bundle = response.bytes().await.map_err(|e| {
157 BoxError::AttestationError(format!("Failed to read cert chain response: {}", e))
158 })?;
159
160 Self::split_pem_bundle(&pem_bundle)
163 }
164
165 fn split_pem_bundle(bundle: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
167 let pem_str = String::from_utf8_lossy(bundle);
168 let certs: Vec<&str> = pem_str
169 .split("-----END CERTIFICATE-----")
170 .filter(|s| s.contains("-----BEGIN CERTIFICATE-----"))
171 .collect();
172
173 if certs.len() < 2 {
174 return Err(BoxError::AttestationError(format!(
175 "Expected 2 certificates in PEM bundle, found {}",
176 certs.len()
177 )));
178 }
179
180 let ask = Self::pem_to_der(certs[0])?;
181 let ark = Self::pem_to_der(certs[1])?;
182
183 Ok((ask, ark))
184 }
185
186 fn pem_to_der(pem: &str) -> Result<Vec<u8>> {
188 let b64: String = pem
189 .lines()
190 .filter(|line| !line.starts_with("-----") && !line.is_empty())
191 .collect();
192
193 base64_decode(&b64).map_err(|e| {
194 BoxError::AttestationError(format!("Failed to decode PEM certificate: {}", e))
195 })
196 }
197
198 async fn load_from_cache(&self, chip_id: &str, tcb: &TcbVersion) -> Option<CertificateChain> {
200 let cache_dir = self.cache_dir.as_ref()?;
201 let cache_key = Self::cache_key(chip_id, tcb);
202 let cache_path = cache_dir.join(&cache_key);
203
204 let data = tokio::fs::read(&cache_path).await.ok()?;
205 serde_json::from_slice(&data).ok()
206 }
207
208 async fn save_to_cache(&self, chip_id: &str, tcb: &TcbVersion, chain: &CertificateChain) {
210 let Some(cache_dir) = &self.cache_dir else {
211 return;
212 };
213
214 if let Err(e) = tokio::fs::create_dir_all(cache_dir).await {
215 tracing::warn!("Failed to create cert cache dir: {}", e);
216 return;
217 }
218
219 let cache_key = Self::cache_key(chip_id, tcb);
220 let cache_path = cache_dir.join(&cache_key);
221
222 match serde_json::to_vec(chain) {
223 Ok(data) => {
224 if let Err(e) = tokio::fs::write(&cache_path, &data).await {
225 tracing::warn!("Failed to cache certificate chain: {}", e);
226 }
227 }
228 Err(e) => {
229 tracing::warn!("Failed to serialize certificate chain for cache: {}", e);
230 }
231 }
232 }
233
234 fn cache_key(chip_id: &str, tcb: &TcbVersion) -> String {
236 let short_id = &chip_id[..chip_id.len().min(16)];
238 format!(
239 "snp_certs_{}_bl{}_tee{}_snp{}_uc{}.json",
240 short_id, tcb.boot_loader, tcb.tee, tcb.snp, tcb.microcode,
241 )
242 }
243
244 pub fn product_name(generation: &str) -> &'static str {
246 match generation.to_lowercase().as_str() {
247 "milan" => PRODUCT_MILAN,
248 "genoa" => PRODUCT_GENOA,
249 _ => PRODUCT_MILAN, }
251 }
252}
253
254fn base64_decode(input: &str) -> std::result::Result<Vec<u8>, String> {
256 use base64::{engine::general_purpose, Engine};
257 let cleaned: String = input.chars().filter(|c| !c.is_whitespace()).collect();
259 general_purpose::STANDARD_NO_PAD
261 .decode(&cleaned)
262 .or_else(|_| general_purpose::STANDARD.decode(&cleaned))
263 .map_err(|e| format!("base64 decode error: {}", e))
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_product_name() {
272 assert_eq!(AmdKdsClient::product_name("milan"), "Milan");
273 assert_eq!(AmdKdsClient::product_name("Milan"), "Milan");
274 assert_eq!(AmdKdsClient::product_name("genoa"), "Genoa");
275 assert_eq!(AmdKdsClient::product_name("Genoa"), "Genoa");
276 assert_eq!(AmdKdsClient::product_name("unknown"), "Milan");
277 }
278
279 #[test]
280 fn test_cache_key() {
281 let tcb = TcbVersion {
282 boot_loader: 3,
283 tee: 0,
284 snp: 8,
285 microcode: 115,
286 };
287 let key = AmdKdsClient::cache_key("abcdef1234567890aabbccdd", &tcb);
288 assert_eq!(key, "snp_certs_abcdef1234567890_bl3_tee0_snp8_uc115.json");
289 }
290
291 #[test]
292 fn test_base64_decode() {
293 let encoded = "SGVsbG8gV29ybGQ=";
294 let decoded = base64_decode(encoded).unwrap();
295 assert_eq!(decoded, b"Hello World");
296 }
297
298 #[test]
299 fn test_base64_decode_no_padding() {
300 let encoded = "SGVsbG8";
301 let decoded = base64_decode(encoded).unwrap();
302 assert_eq!(decoded, b"Hello");
303 }
304
305 #[test]
306 fn test_base64_decode_with_newlines() {
307 let encoded = "SGVs\nbG8g\nV29ybGQ=";
308 let decoded = base64_decode(encoded).unwrap();
309 assert_eq!(decoded, b"Hello World");
310 }
311
312 #[test]
313 fn test_split_pem_bundle() {
314 let bundle = b"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nBAUG\n-----END CERTIFICATE-----\n";
315 let (ask, ark) = AmdKdsClient::split_pem_bundle(bundle).unwrap();
316 assert_eq!(ask, vec![1, 2, 3]);
317 assert_eq!(ark, vec![4, 5, 6]);
318 }
319
320 #[test]
321 fn test_split_pem_bundle_too_few() {
322 let bundle = b"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n";
323 let result = AmdKdsClient::split_pem_bundle(bundle);
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_pem_to_der() {
329 let pem = "-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----";
330 let der = AmdKdsClient::pem_to_der(pem).unwrap();
331 assert_eq!(der, vec![1, 2, 3]);
332 }
333
334 #[test]
335 fn test_kds_client_creation() {
336 let client = AmdKdsClient::new(None);
337 assert!(client.cache_dir.is_none());
338
339 let client = AmdKdsClient::new(Some(PathBuf::from("/tmp/test-certs")));
340 assert_eq!(client.cache_dir, Some(PathBuf::from("/tmp/test-certs")));
341 }
342}