lit_utilities_wasm/
sev_snp.rs

1use std::collections::BTreeMap;
2
3use js_sys::Uint8Array;
4use sev::certs::snp::Certificate;
5use sev::firmware::host::TcbVersion;
6use sha2::{Digest, Sha512};
7use wasm_bindgen::prelude::*;
8
9use sev::certs::snp::{builtin::milan, ca, Chain, Verifiable};
10use sev::firmware::guest::AttestationReport;
11
12use crate::abi::{from_js, JsResult};
13
14/// Gets the vcek url for the given attestation report.  You can fetch this certificate yourself, and pass it in to verify_attestation_report
15#[wasm_bindgen(js_name = "sevSnpGetVcekUrl")]
16pub fn sev_snp_get_vcek_url(attestation_report: &[u8]) -> JsResult<String> {
17    let attestation_report = parse_attestation_report(attestation_report)?;
18    let url = get_vcek_url(attestation_report);
19    Ok(url)
20}
21
22fn get_vcek_url(attestation_report: AttestationReport) -> String {
23    const KDS_CERT_SITE: &str = "https://kdsintf.amd.com";
24    #[allow(dead_code)]
25    const KDS_DEV_CERT_SITE: &str = "https://kdsintfdev.amd.com";
26
27    #[allow(dead_code)]
28    const KDS_CEK: &str = "/cek/id/";
29
30    const KDS_VCEK: &str = "/vcek/v1/"; // KDS_VCEK/{product_name}/{hwid}?{tcb parameter list}
31    #[allow(dead_code)]
32    const KDS_VCEK_CERT_CHAIN: &str = "cert_chain"; // KDS_VCEK/{product_name}/cert_chain
33    #[allow(dead_code)]
34    const KDS_VCEK_CRL: &str = "crl"; // KDS_VCEK/{product_name}/crl"
35
36    const PRODUCT_NAME: &str = "Milan";
37
38    let AttestationReport {
39        chip_id,
40        reported_tcb:
41            TcbVersion {
42                bootloader,
43                tee,
44                snp,
45                microcode,
46                ..
47            },
48        ..
49    } = attestation_report;
50
51    format!(
52        "{}{}{}/{}?blSPL={bootloader:0>2}&teeSPL={tee:0>2}&snpSPL={snp:0>2}&ucodeSPL={microcode:0>2}",
53        KDS_CERT_SITE, KDS_VCEK, PRODUCT_NAME, hex::encode(chip_id)
54    )
55}
56
57#[wasm_bindgen]
58extern "C" {
59    #[wasm_bindgen(typescript_type = "Record<string, Uint8Array>")]
60    pub type AttestationData;
61}
62
63#[wasm_bindgen(js_name = "sevSnpVerify")]
64pub fn sev_snp_verify(
65    attestation_report: &[u8],
66    attestation_data: AttestationData,
67    signatures: Vec<Uint8Array>,
68    challenge: &[u8],
69    vcek_certificate: &[u8],
70) -> JsResult<()> {
71    let attestation_report = parse_attestation_report(attestation_report)?;
72    let attestation_data = from_js(attestation_data)?;
73    let signatures = signatures
74        .into_iter()
75        .map(from_js::<Vec<u8>>)
76        .collect::<JsResult<Vec<_>>>()?;
77    let vcek_certificate = parse_certificate(vcek_certificate)?;
78
79    verify_certificate(vcek_certificate, attestation_report)?;
80    verify_challenge(challenge, attestation_data, signatures, attestation_report)?;
81
82    Ok(())
83}
84
85fn parse_attestation_report(attestation_report: &[u8]) -> JsResult<AttestationReport> {
86    let report = unsafe { std::ptr::read(attestation_report.as_ptr() as *const _) };
87    // TODO: run some validation here?
88    Ok(report)
89}
90
91fn parse_certificate(vcek_certificate: &[u8]) -> JsResult<Certificate> {
92    Certificate::from_der(vcek_certificate).map_err(|e| JsError::new(e.to_string().as_str()))
93}
94
95fn verify_certificate(vcek: Certificate, report: AttestationReport) -> JsResult<()> {
96    let ark = milan::ark().unwrap();
97    let ask = milan::ask().unwrap();
98
99    let ca = ca::Chain { ark, ask };
100
101    let chain = Chain { ca, vcek };
102
103    (&chain, &report)
104        .verify()
105        .map_err(|e| JsError::new(e.to_string().as_str()))
106}
107
108fn verify_challenge(
109    challenge: &[u8],
110    data: BTreeMap<String, Vec<u8>>,
111    signatures: Vec<Vec<u8>>,
112    attestation_report: AttestationReport,
113) -> JsResult<()> {
114    let expected_report_data = get_expected_report_data(data, signatures, challenge);
115
116    if attestation_report.report_data != expected_report_data {
117        return Err(
118            JsError::new(
119                "Report data does not match.  This generally indicates that the data, challenge/nonce, or signatures are bad."
120            )
121        );
122    }
123    Ok(())
124}
125
126fn get_expected_report_data(
127    data: BTreeMap<String, Vec<u8>>,
128    signatures: Vec<Vec<u8>>,
129    challenge: &[u8],
130) -> [u8; 64] {
131    let mut hasher = Sha512::new();
132
133    hasher.update("noonce");
134    hasher.update(challenge);
135
136    hasher.update("data");
137    for (key, value) in data {
138        hasher.update(key);
139        hasher.update(value);
140    }
141
142    // FIXME: can we really have `signatures.len() == 0`?
143    if signatures.is_empty() {
144        hasher.update("signatures");
145
146        // FIXME: why is the slice needed?
147        for s in &signatures[..signatures.len() - 1] {
148            hasher.update(s);
149        }
150    }
151
152    let result = hasher.finalize();
153    let mut array = [0u8; 64];
154    array.copy_from_slice(&result[..]);
155    array
156}