Skip to main content

az_cvm_vtpm/vtpm/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use core::time::Duration;
5use serde::{Deserialize, Serialize};
6use std::thread;
7use thiserror::Error;
8use tss_esapi::abstraction::{nv, pcr, public::DecodedKey};
9use tss_esapi::attributes::NvIndexAttributesBuilder;
10use tss_esapi::handles::{NvIndexHandle, NvIndexTpmHandle, PcrHandle, TpmHandle};
11use tss_esapi::interface_types::algorithm::HashingAlgorithm;
12use tss_esapi::interface_types::resource_handles::{NvAuth, Provision};
13use tss_esapi::interface_types::session_handles::AuthSession;
14use tss_esapi::structures::pcr_selection_list::PcrSelectionListBuilder;
15use tss_esapi::structures::pcr_slot::PcrSlot;
16use tss_esapi::structures::{
17    Attest, AttestInfo, Data, DigestValues, MaxNvBuffer, NvPublicBuilder, Signature,
18    SignatureScheme,
19};
20use tss_esapi::tcti_ldr::{DeviceConfig, TctiNameConf};
21use tss_esapi::traits::{Marshall, UnMarshall};
22use tss_esapi::Context;
23
24#[cfg(feature = "verifier")]
25mod verify;
26
27#[cfg(feature = "verifier")]
28pub use verify::VerifyError;
29
30const VTPM_HCL_REPORT_NV_INDEX: u32 = 0x01400001;
31const INDEX_REPORT_DATA: u32 = 0x01400002;
32const VTPM_AK_HANDLE: u32 = 0x81000003;
33const VTPM_QUOTE_PCR_SLOTS: [PcrSlot; 24] = [
34    PcrSlot::Slot0,
35    PcrSlot::Slot1,
36    PcrSlot::Slot2,
37    PcrSlot::Slot3,
38    PcrSlot::Slot4,
39    PcrSlot::Slot5,
40    PcrSlot::Slot6,
41    PcrSlot::Slot7,
42    PcrSlot::Slot8,
43    PcrSlot::Slot9,
44    PcrSlot::Slot10,
45    PcrSlot::Slot11,
46    PcrSlot::Slot12,
47    PcrSlot::Slot13,
48    PcrSlot::Slot14,
49    PcrSlot::Slot15,
50    PcrSlot::Slot16,
51    PcrSlot::Slot17,
52    PcrSlot::Slot18,
53    PcrSlot::Slot19,
54    PcrSlot::Slot20,
55    PcrSlot::Slot21,
56    PcrSlot::Slot22,
57    PcrSlot::Slot23,
58];
59
60fn to_pcr_handle(pcr: u8) -> Result<PcrHandle, ExtendError> {
61    match pcr {
62        0 => Ok(PcrHandle::Pcr0),
63        1 => Ok(PcrHandle::Pcr1),
64        2 => Ok(PcrHandle::Pcr2),
65        3 => Ok(PcrHandle::Pcr3),
66        4 => Ok(PcrHandle::Pcr4),
67        5 => Ok(PcrHandle::Pcr5),
68        6 => Ok(PcrHandle::Pcr6),
69        7 => Ok(PcrHandle::Pcr7),
70        8 => Ok(PcrHandle::Pcr8),
71        9 => Ok(PcrHandle::Pcr9),
72        10 => Ok(PcrHandle::Pcr10),
73        11 => Ok(PcrHandle::Pcr11),
74        12 => Ok(PcrHandle::Pcr12),
75        13 => Ok(PcrHandle::Pcr13),
76        14 => Ok(PcrHandle::Pcr14),
77        15 => Ok(PcrHandle::Pcr15),
78        16 => Ok(PcrHandle::Pcr16),
79        17 => Ok(PcrHandle::Pcr17),
80        18 => Ok(PcrHandle::Pcr18),
81        19 => Ok(PcrHandle::Pcr19),
82        20 => Ok(PcrHandle::Pcr20),
83        21 => Ok(PcrHandle::Pcr21),
84        22 => Ok(PcrHandle::Pcr22),
85        23 => Ok(PcrHandle::Pcr23),
86        _ => Err(ExtendError::InvalidPcr),
87    }
88}
89
90#[derive(Error, Debug)]
91pub enum ReportError {
92    #[error("tpm error")]
93    Tpm(#[from] tss_esapi::Error),
94    #[error("Failed to write value to nvindex")]
95    NvWriteFailed,
96}
97
98/// Get a HCL report from an nvindex
99pub fn get_report() -> Result<Vec<u8>, ReportError> {
100    let nv_index = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX)?;
101    let mut context = get_session_context()?;
102
103    let report = nv::read_full(&mut context, NvAuth::Owner, nv_index)?;
104    Ok(report)
105}
106
107/// Retrieve a fresh HCL report from a nvindex. The specified report_data will be reflected
108/// in the HCL report in its user_data field and mixed into a hash in the TEE report's report_data.
109/// The Function contains a 3 seconds delay to avoid retrieving a stale report.
110pub fn get_report_with_report_data(report_data: &[u8]) -> Result<Vec<u8>, ReportError> {
111    let mut context = get_session_context()?;
112
113    let nv_index_report_data = NvIndexTpmHandle::new(INDEX_REPORT_DATA)?;
114    write_nv_index(&mut context, nv_index_report_data, report_data)?;
115
116    thread::sleep(Duration::new(3, 0));
117
118    let nv_index = NvIndexTpmHandle::new(VTPM_HCL_REPORT_NV_INDEX)?;
119    let report = nv::read_full(&mut context, NvAuth::Owner, nv_index)?;
120    Ok(report)
121}
122
123fn get_session_context() -> Result<Context, ReportError> {
124    let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default());
125    let mut context = Context::new(conf)?;
126    let auth_session = AuthSession::Password;
127    context.set_sessions((Some(auth_session), None, None));
128    Ok(context)
129}
130
131enum NvSearchResult {
132    Found,
133    NotFound,
134    SizeMismatch,
135}
136
137fn find_index(
138    context: &mut Context,
139    nv_index: NvIndexTpmHandle,
140    len: usize,
141) -> Result<NvSearchResult, ReportError> {
142    let list = nv::list(context)?;
143    let result = list
144        .iter()
145        .find(|(public, _)| public.nv_index() == nv_index);
146    let Some((public, _)) = result else {
147        return Ok(NvSearchResult::NotFound);
148    };
149    if public.data_size() != len {
150        return Ok(NvSearchResult::SizeMismatch);
151    }
152
153    Ok(NvSearchResult::Found)
154}
155
156fn create_index(
157    context: &mut Context,
158    handle: NvIndexTpmHandle,
159    len: usize,
160) -> Result<NvIndexHandle, ReportError> {
161    let attributes = NvIndexAttributesBuilder::new()
162        .with_owner_write(true)
163        .with_owner_read(true)
164        .build()?;
165
166    let owner = NvPublicBuilder::new()
167        .with_nv_index(handle)
168        .with_index_name_algorithm(HashingAlgorithm::Sha256)
169        .with_index_attributes(attributes)
170        .with_data_area_size(len)
171        .build()?;
172
173    let index = context.nv_define_space(Provision::Owner, None, owner)?;
174    Ok(index)
175}
176
177fn resolve_handle(
178    context: &mut Context,
179    handle: NvIndexTpmHandle,
180) -> Result<NvIndexHandle, ReportError> {
181    let key_handle = context.execute_without_session(|c| c.tr_from_tpm_public(handle.into()))?;
182    Ok(key_handle.into())
183}
184
185fn delete_index(context: &mut Context, handle: NvIndexTpmHandle) -> Result<(), ReportError> {
186    let index = resolve_handle(context, handle)?;
187    context.nv_undefine_space(Provision::Owner, index)?;
188    Ok(())
189}
190
191fn write_nv_index(
192    context: &mut Context,
193    handle: NvIndexTpmHandle,
194    data: &[u8],
195) -> Result<(), ReportError> {
196    let buffer = MaxNvBuffer::try_from(data)?;
197    let result = find_index(context, handle, data.len())?;
198    let index = match result {
199        NvSearchResult::NotFound => create_index(context, handle, data.len())?,
200        NvSearchResult::SizeMismatch => {
201            delete_index(context, handle)?;
202            create_index(context, handle, data.len())?
203        }
204        NvSearchResult::Found => resolve_handle(context, handle)?,
205    };
206    context.nv_write(NvAuth::Owner, index, buffer, 0)?;
207    Ok(())
208}
209
210#[derive(Error, Debug)]
211pub enum ExtendError {
212    #[error("tpm error")]
213    Tpm(#[from] tss_esapi::Error),
214    #[error("invalid pcr number (expected 0-23)")]
215    InvalidPcr,
216}
217
218/// Extend a PCR register with a sha256 digest
219pub fn extend_pcr(pcr: u8, digest: &[u8; 32]) -> Result<(), ExtendError> {
220    let pcr_handle = to_pcr_handle(pcr)?;
221
222    let mut vals = DigestValues::new();
223    let sha256_digest = digest.to_vec().try_into()?;
224    vals.set(HashingAlgorithm::Sha256, sha256_digest);
225
226    let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default());
227    let mut context = Context::new(conf)?;
228
229    let auth_session = AuthSession::Password;
230    context.set_sessions((Some(auth_session), None, None));
231    context.pcr_extend(pcr_handle, vals)?;
232
233    Ok(())
234}
235
236#[derive(Error, Debug)]
237pub enum AKPubError {
238    #[error("tpm error")]
239    Tpm(#[from] tss_esapi::Error),
240    #[error("asn1 der error")]
241    WrongKeyType,
242}
243
244#[derive(Serialize, Deserialize, Debug)]
245pub struct PublicKey {
246    n: Vec<u8>,
247    e: Vec<u8>,
248}
249
250impl PublicKey {
251    /// Get the modulus of the public key as big-endian unsigned bytes
252    pub fn modulus(&self) -> &[u8] {
253        &self.n
254    }
255
256    /// Get the public exponent of the public key as big-endian unsigned bytes
257    pub fn exponent(&self) -> &[u8] {
258        &self.e
259    }
260}
261
262/// Get the AK pub of the vTPM
263pub fn get_ak_pub() -> Result<PublicKey, AKPubError> {
264    let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default());
265    let mut context = Context::new(conf)?;
266    let tpm_handle: TpmHandle = VTPM_AK_HANDLE.try_into()?;
267    let key_handle = context.tr_from_tpm_public(tpm_handle)?;
268    let (pk, _, _) = context.read_public(key_handle.into())?;
269
270    let decoded_key: DecodedKey = pk.try_into()?;
271    let DecodedKey::RsaPublicKey(rsa_pk) = decoded_key else {
272        return Err(AKPubError::WrongKeyType);
273    };
274
275    let bytes_n = rsa_pk.modulus.as_unsigned_bytes_be();
276    let bytes_e = rsa_pk.public_exponent.as_unsigned_bytes_be();
277    let pkey = PublicKey {
278        n: bytes_n.into(),
279        e: bytes_e.into(),
280    };
281    Ok(pkey)
282}
283
284#[non_exhaustive]
285#[derive(Error, Debug)]
286pub enum QuoteError {
287    #[error("tpm error")]
288    Tpm(#[from] tss_esapi::Error),
289    #[error("data too large")]
290    DataTooLarge,
291    #[error("Not a quote, that should not occur")]
292    NotAQuote,
293    #[error("Wrong signature, that should not occur")]
294    WrongSignature,
295    #[error("PCR bank not found")]
296    PcrBankNotFound,
297    #[error("PCR reading error")]
298    PcrRead,
299}
300
301#[derive(Serialize, Deserialize, Debug, Clone)]
302pub struct Quote {
303    signature: Vec<u8>,
304    message: Vec<u8>,
305    pcrs: Vec<[u8; 32]>,
306}
307
308impl Quote {
309    /// Retrieve sha256 PCR values from a Quote
310    pub fn pcrs_sha256(&self) -> impl Iterator<Item = &[u8; 32]> {
311        self.pcrs.iter()
312    }
313
314    /// Extract signature from a Quote
315    pub fn signature(&self) -> Vec<u8> {
316        self.signature.clone()
317    }
318
319    /// Extract nonce from a Quote
320    pub fn nonce(&self) -> Result<Vec<u8>, QuoteError> {
321        let attest = Attest::unmarshall(&self.message)?;
322        let nonce = attest.extra_data().to_vec();
323        Ok(nonce)
324    }
325
326    /// Extract message from a Quote
327    pub fn message(&self) -> Vec<u8> {
328        self.message.clone()
329    }
330}
331
332/// Get a signed vTPM Quote
333///
334/// # Arguments
335///
336/// * `data` - A byte slice to use as nonce
337pub fn get_quote(data: &[u8]) -> Result<Quote, QuoteError> {
338    if data.len() > Data::MAX_SIZE {
339        return Err(QuoteError::DataTooLarge);
340    }
341    let conf: TctiNameConf = TctiNameConf::Device(DeviceConfig::default());
342    let mut context = Context::new(conf)?;
343    let tpm_handle: TpmHandle = VTPM_AK_HANDLE.try_into()?;
344    let key_handle = context.tr_from_tpm_public(tpm_handle)?;
345
346    let quote_data: Data = data.try_into()?;
347    let scheme = SignatureScheme::Null;
348    let hash_algo = HashingAlgorithm::Sha256;
349    let selection_list = PcrSelectionListBuilder::new()
350        .with_selection(hash_algo, &VTPM_QUOTE_PCR_SLOTS)
351        .build()?;
352
353    let auth_session = AuthSession::Password;
354    context.set_sessions((Some(auth_session), None, None));
355
356    let (attest, signature) = context.quote(
357        key_handle.into(),
358        quote_data,
359        scheme,
360        selection_list.clone(),
361    )?;
362
363    let AttestInfo::Quote { .. } = attest.attested() else {
364        return Err(QuoteError::NotAQuote);
365    };
366    let Signature::RsaSsa(rsa_sig) = signature else {
367        return Err(QuoteError::WrongSignature);
368    };
369
370    let signature = rsa_sig.signature().to_vec();
371    let message = attest.marshall()?;
372
373    context.clear_sessions();
374    let pcr_data = pcr::read_all(&mut context, selection_list)?;
375
376    let pcr_bank = pcr_data
377        .pcr_bank(hash_algo)
378        .ok_or(QuoteError::PcrBankNotFound)?;
379
380    let pcrs: Result<Vec<[u8; 32]>, _> = pcr_bank
381        .into_iter()
382        .map(|(_, digest)| digest.clone().try_into().map_err(|_| QuoteError::PcrRead))
383        .collect();
384    let pcrs = pcrs?;
385
386    Ok(Quote {
387        signature,
388        message,
389        pcrs,
390    })
391}