cli/
job.rs

1// SPDX-License-Identifier: GPL-3-0-or-later
2// Copyright (c) 2025 Opinsys Oy
3// Copyright (c) 2024-2025 Jarkko Sakkinen
4
5use crate::{
6    auth::{Auth, AuthClass, AuthError},
7    crypto::{crypto_hash_size, crypto_make_name, CryptoError},
8    device::{with_device, Device, DeviceError, TpmCommandObject},
9    handle::{Handle, HandleClass},
10    key::{AnyKey, KeyError, TpmKey},
11    vtpm::{build_password_session, create_auth, VtpmCache, VtpmContext, VtpmError},
12    write_object,
13};
14use rand::{thread_rng, RngCore};
15use std::{cell::RefCell, collections::HashSet, io, io::Write, num::TryFromIntError, rc::Rc};
16use thiserror::Error;
17use tpm2_protocol::{
18    data::{Tpm2bNonce, TpmAlgId, TpmCc, TpmRcBase, TpmRh, TpmaNv, TpmaSession, TpmsAuthCommand},
19    message::{
20        TpmAuthResponses, TpmEvictControlCommand, TpmNvReadCommand, TpmNvReadPublicCommand,
21        TpmResponseBody,
22    },
23    TpmError, TpmHandle,
24};
25
26#[derive(Debug, Error)]
27pub enum JobError {
28    #[error("handle not found: {0}{1:08x}")]
29    HandleNotFound(&'static str, u32),
30    #[error("invalid auth")]
31    InvalidAuth,
32    #[error("invalid key format")]
33    InvalidFormat,
34    #[error("invalid parent: {0}{1:08x}")]
35    InvalidParent(&'static str, u32),
36    #[error("malformed data")]
37    MalformedData,
38    #[error("parent not found")]
39    ParentNotFound,
40    #[error("response mismatch: {0}")]
41    ResponseMismatch(TpmCc),
42    #[error("trailing authorizations")]
43    TrailingAuthorizations,
44    #[error("I/O: {0}")]
45    Io(#[from] io::Error),
46    #[error("key error: {0}")]
47    Key(#[from] KeyError),
48    #[error("cache: {0}")]
49    Vtpm(#[from] VtpmError),
50    #[error("device: {0}")]
51    Device(#[from] DeviceError),
52    #[error("auth error: {0}")]
53    Auth(#[from] AuthError),
54    #[error("crypto: {0}")]
55    Crypto(#[from] CryptoError),
56    #[error("int decode: {0}")]
57    IntDecode(#[from] TryFromIntError),
58}
59
60impl From<TpmError> for JobError {
61    fn from(err: TpmError) -> Self {
62        Self::Device(DeviceError::from(err))
63    }
64}
65
66pub struct Job<'a> {
67    pub device: Option<Rc<RefCell<Device>>>,
68    pub cache: &'a mut VtpmCache<'a>,
69    pub auth_list: &'a [Auth],
70    pub writer: &'a mut dyn Write,
71}
72
73impl<'a> Job<'a> {
74    /// Creates a new `Job`.
75    #[must_use]
76    pub fn new(
77        device: Option<Rc<RefCell<Device>>>,
78        cache: &'a mut VtpmCache<'a>,
79        auth_list: &'a [Auth],
80        writer: &'a mut dyn Write,
81    ) -> Self {
82        Self {
83            device,
84            cache,
85            auth_list,
86            writer,
87        }
88    }
89
90    /// Finds the ancestor chain for a given VTPM handle.
91    ///
92    /// Traverses up the parent hierarchy from the target `vhandle`, checking
93    /// both the cache and persistent TPM handles, until it finds the root. The
94    /// root can be a persistent physical handle or a non-persistent primary key
95    /// stored in the VTPM cache.
96    ///
97    /// Returns a list of `(Handle, Auth)` pairs representing the path from the
98    /// root *down* to the target, ready for loading. The first handle in the
99    /// vector indicates the root type (`HandleClass::Tpm` or `HandleClass::Vtpm`).
100    ///
101    /// # Errors
102    ///
103    /// Returns [`HandleNotFound`](crate::job::JobError::HandleNotFound) when the
104    /// `target_vhandle` doesn't exist in the cache.
105    /// Returns [`ParentNotFound`](crate::job::JobError::ParentNotFound) when an
106    /// intermediate parent cannot be found in the cache or as a persistent
107    /// handle.
108    /// Returns [`Device`](crate::job::JobError::Device) when reading persistent
109    /// handles fails.
110    fn fetch_ancestor_chain(
111        &self,
112        target_vhandle: u32,
113        device: &mut Device,
114    ) -> Result<Vec<Handle>, JobError> {
115        let mut current_vhandle = target_vhandle;
116        let mut vtp_chain: Vec<Handle> = Vec::new();
117        let mut physical_primary: Option<Handle> = None;
118
119        loop {
120            let key = self.cache.find_by_vhandle(current_vhandle)?;
121
122            if key.parent.inner.object_type == TpmAlgId::Null {
123                break;
124            }
125
126            if let Some(parent_key) = self.cache.find_by_public(&key.parent.inner) {
127                let parent_vhandle = parent_key.handle();
128                vtp_chain.push(Handle((HandleClass::Vtpm, current_vhandle)));
129                current_vhandle = parent_vhandle;
130            } else {
131                match device.find_persistent(&key.parent.inner)? {
132                    Some((phandle, _)) => {
133                        physical_primary = Some(Handle((HandleClass::Tpm, phandle.0)));
134                        break;
135                    }
136                    None => {
137                        return Err(JobError::ParentNotFound);
138                    }
139                }
140            }
141        }
142
143        vtp_chain.push(Handle((HandleClass::Vtpm, current_vhandle)));
144        vtp_chain.reverse();
145
146        if let Some(root_handle) = physical_primary {
147            let mut final_chain = vec![root_handle];
148            final_chain.extend(vtp_chain);
149            Ok(final_chain)
150        } else {
151            Ok(vtp_chain)
152        }
153    }
154
155    /// Loads a TPM context from a handle, recursively loading its ancestors
156    /// first.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`HandleNotFound`](crate::job::JobError::HandleNotFound) when the
161    /// target handle or any parent handle cannot be found, or if the chain is empty.
162    /// Returns [`ParentNotFound`](crate::job::JobError::ParentNotFound) when a
163    /// necessary parent handle isn't found in cache or persistent storage.
164    /// Returns [`Device`](crate::job::JobError::Device) or
165    /// [`TpmProtocol`](crate::job::JobError::Device) when TPM commands fail.
166    /// Returns [`Vtpm`](crate::job::JobError::Vtpm) when tracking the loaded
167    /// handle fails.
168    /// Returns [`InvalidParent`](crate::job::JobError::InvalidParent) if a
169    /// loaded key's parent does not match the expected parent in the chain.
170    pub fn load_context(
171        &mut self,
172        device: &mut Device,
173        target: &Handle,
174    ) -> Result<TpmHandle, JobError> {
175        if target.class() == HandleClass::Tpm {
176            return Ok(TpmHandle(target.value()));
177        }
178
179        let target_vhandle = target.value();
180        let chain = self.fetch_ancestor_chain(target_vhandle, device)?;
181
182        if chain.is_empty() {
183            return Err(JobError::HandleNotFound("vtpm:", target_vhandle));
184        }
185
186        let mut phandle: Option<TpmHandle> = None;
187        let mut chain_iter = chain.into_iter();
188
189        if let Some(first_handle) = chain_iter.next() {
190            match first_handle.class() {
191                HandleClass::Tpm => {
192                    phandle = Some(TpmHandle(first_handle.value()));
193                }
194                HandleClass::Vtpm => {
195                    let key = self.cache.find_by_vhandle(first_handle.value())?;
196                    let loaded_phandle = device.load_context(key.context.clone())?;
197                    self.cache.track(loaded_phandle)?;
198                    phandle = Some(loaded_phandle);
199                }
200            }
201        }
202
203        for handle in chain_iter {
204            let vhandle = handle.value();
205            let key = self.cache.find_by_vhandle(vhandle)?;
206
207            let parent_phandle = phandle.ok_or(JobError::ParentNotFound)?;
208            let loaded_phandle = device.load_context(key.context.clone())?;
209
210            if device.read_public(parent_phandle)?.1 != crypto_make_name(&key.parent.inner)? {
211                self.cache.untrack(loaded_phandle.0);
212                device.flush_context(loaded_phandle)?;
213                return Err(JobError::InvalidParent("vtpm:", vhandle));
214            }
215
216            self.cache.track(loaded_phandle)?;
217            phandle = Some(loaded_phandle);
218        }
219
220        phandle.ok_or(JobError::HandleNotFound("vtpm:", target_vhandle))
221    }
222
223    /// Builds the authorization area for a command.
224    ///
225    /// # Errors
226    ///
227    /// Returns [`TrailingAuthorizations`](crate::job::JobError::TrailingAuthorizations)
228    /// when more auth values are provided than handles requiring authorization.
229    /// Returns [`InvalidAuth`](crate::job::JobError::InvalidAuth) when a `Policy`
230    /// auth class is encountered.
231    /// Returns [`HandleNotFound`](crate::job::JobError::HandleNotFound) when a
232    /// session handle in `auth_list` is not found.
233    /// Returns [`MalformedData`](crate::job::JobError::MalformedData) when the
234    /// session's hash algorithm is unsupported.
235    /// Returns [`Device`](crate::job::JobError::Device) when building TPM data
236    /// structures fails or crypto operations fail.
237    /// Returns [`Auth`](crate::job::JobError::Auth) when extracting a session
238    /// handle fails.
239    /// Returns [`Vtpm`](crate::job::JobError::Vtpm) when building a password
240    /// session fails.
241    fn build_auth_area<C: TpmCommandObject>(
242        &self,
243        device: &mut Device,
244        command: &C,
245        handles: &[u32],
246        auth_list: &[Auth],
247    ) -> Result<Vec<TpmsAuthCommand>, JobError> {
248        let mut built_auths = Vec::new();
249        let params = write_object(command).map_err(DeviceError::TpmProtocol)?;
250
251        let mut nonce_decrypt: Option<Tpm2bNonce> = None;
252        let mut nonce_encrypt: Option<Tpm2bNonce> = None;
253
254        for auth in auth_list {
255            if auth.class() == AuthClass::Session {
256                let vhandle = auth.session()?;
257                if let Some(session) = self.cache.get_session(vhandle) {
258                    if session.attributes.contains(TpmaSession::DECRYPT) {
259                        nonce_decrypt = Some(session.nonce_tpm);
260                    }
261                    if session.attributes.contains(TpmaSession::ENCRYPT) {
262                        nonce_encrypt = Some(session.nonce_tpm);
263                    }
264                }
265                if nonce_decrypt.is_some() && nonce_encrypt.is_some() {
266                    break;
267                }
268            }
269        }
270
271        for (i, auth) in auth_list.iter().enumerate() {
272            let handle_param = handles.get(i).ok_or(JobError::TrailingAuthorizations)?;
273
274            match auth.class() {
275                AuthClass::Password => {
276                    built_auths.push(build_password_session(auth.value())?);
277                }
278                AuthClass::Session => {
279                    let vhandle = auth.session()?;
280                    let session = self
281                        .cache
282                        .get_session(vhandle)
283                        .ok_or(JobError::HandleNotFound("vtpm:", vhandle))?;
284                    let nonce_size =
285                        crypto_hash_size(session.auth_hash).ok_or(JobError::MalformedData)?;
286                    let mut nonce_bytes = vec![0; nonce_size];
287                    thread_rng().fill_bytes(&mut nonce_bytes);
288                    let nonce_caller = Tpm2bNonce::try_from(nonce_bytes.as_slice())
289                        .map_err(DeviceError::TpmProtocol)?;
290                    let (current_nonce_decrypt, current_nonce_encrypt) = if i == 0 {
291                        (nonce_decrypt.as_ref(), nonce_encrypt.as_ref())
292                    } else {
293                        (None, None)
294                    };
295
296                    let result = create_auth(
297                        device,
298                        session,
299                        &nonce_caller,
300                        &[],
301                        C::CC,
302                        &[*handle_param],
303                        &params,
304                        current_nonce_decrypt,
305                        current_nonce_encrypt,
306                    )?;
307                    built_auths.push(result);
308                }
309                AuthClass::Policy => return Err(JobError::InvalidAuth),
310            }
311        }
312        Ok(built_auths)
313    }
314
315    /// Executes a TPM command with full authorization session handling.
316    ///
317    /// This function encapsulates the prepare, build, execute, and teardown
318    /// sequence for authorized commands.
319    ///
320    /// # Errors
321    ///
322    /// Returns [`JobError`] if any stage of the session management or
323    /// command execution fails. This includes errors from `prepare_sessions`,
324    /// `build_auth_area`, `device.execute`, or `teardown_sessions`.
325    pub fn execute<C: TpmCommandObject>(
326        &mut self,
327        device: &mut Device,
328        command: &C,
329        handles: &[u32],
330        auth_list: &[Auth],
331    ) -> Result<(TpmResponseBody, TpmAuthResponses), JobError> {
332        let auth_handles = self.cache.prepare_sessions(device, auth_list)?;
333        for &handle in &auth_handles {
334            self.cache.track(handle)?;
335        }
336
337        let sessions = self.build_auth_area(device, command, handles, auth_list)?;
338        let (resp, auth_responses) = match device.execute(command, &sessions) {
339            Ok((resp, auth_responses)) => (resp, auth_responses),
340            Err(DeviceError::TpmRc(rc)) => {
341                if rc.base() == TpmRcBase::PolicyFail {
342                    for auth in auth_list {
343                        if auth.class() == AuthClass::Session {
344                            let vhandle = auth.session()?;
345                            log::debug!("vtpm:{vhandle} is stale");
346                            self.cache.remove(device, vhandle)?;
347                        }
348                    }
349                }
350                return Err(JobError::Device(DeviceError::TpmRc(rc)));
351            }
352            Err(err) => return Err(JobError::Device(err)),
353        };
354
355        let mut used_auth_list = HashSet::new();
356        for auth in auth_list {
357            if auth.class() == AuthClass::Session {
358                let handle = auth.session()?;
359                used_auth_list.insert(handle);
360            }
361        }
362
363        self.cache
364            .teardown_sessions(device, &used_auth_list, &auth_responses)?;
365
366        for handle in auth_handles {
367            self.cache.untrack(handle.0);
368        }
369
370        Ok((resp, auth_responses))
371    }
372
373    /// Evicts a persistent object or makes a transient object persistent using `Job::execute`.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`Device`](crate::job::JobError::Device) when the underlying
378    /// `with_device` fails.
379    /// Returns [`ResponseMismatch`](crate::job::JobError::ResponseMismatch) when
380    /// the TPM command returns an unexpected response type.
381    /// Returns [`JobError`] from the underlying `execute` call on failure.
382    pub fn evict_control(
383        &mut self,
384        auth_handle: TpmHandle,
385        object_to_evict: TpmHandle,
386        persistent_handle: TpmHandle,
387        auths: &[Auth],
388    ) -> Result<(), JobError> {
389        with_device(self.device.clone(), |device| {
390            let cmd = TpmEvictControlCommand {
391                auth: auth_handle,
392                object_handle: object_to_evict.0.into(),
393                persistent_handle,
394            };
395            let handles_for_session = [auth_handle.0];
396
397            let (resp, _) = self.execute(device, &cmd, &handles_for_session, auths)?;
398
399            resp.EvictControl()
400                .map_err(|_| JobError::ResponseMismatch(TpmCc::EvictControl))?;
401            Ok(())
402        })
403    }
404
405    /// Imports an external key under a TPM parent, creating a new `TpmKey`.
406    ///
407    /// # Errors
408    ///
409    /// Returns [`InvalidFormat`](crate::job::JobError::InvalidFormat) when the
410    /// input bytes represent a TPM key, not an external key.
411    /// Returns [`Key`](crate::job::JobError::Key) when converting the external
412    /// key or building the TPM key fails.
413    /// Returns [`JobError`] from the underlying `TpmKey::from_external_key` call
414    /// on failure.
415    pub fn import_key(
416        &mut self,
417        device: &mut Device,
418        parent_handle: TpmHandle,
419        input_bytes: &[u8],
420        auths: &[Auth],
421    ) -> Result<TpmKey, JobError> {
422        let external_key = match AnyKey::try_from(input_bytes)? {
423            AnyKey::Tpm(_) => {
424                return Err(JobError::InvalidFormat);
425            }
426            AnyKey::External(key) => key,
427        };
428        let mut rng = rand::thread_rng();
429        Ok(TpmKey::from_external_key(
430            device,
431            self,
432            parent_handle,
433            &external_key,
434            &mut rng,
435            &[parent_handle.0],
436            auths,
437        )?)
438    }
439
440    /// Reads a certificate from a given NV index.
441    ///
442    /// # Errors
443    ///
444    /// Returns [`ResponseMismatch`](crate::job::JobError::ResponseMismatch) when
445    /// TPM commands return unexpected response types.
446    /// Returns [`IntDecode`](crate::job::JobError::IntDecode) when converting
447    /// chunk size or offset fails.
448    /// Returns [`JobError`] from the underlying `execute` calls on failure.
449    pub fn read_certificate(
450        &mut self,
451        device: &mut Device,
452        auths: &[Auth],
453        handle: u32,
454        max_read_size: usize,
455    ) -> Result<Option<Vec<u8>>, JobError> {
456        let nv_read_public_cmd = TpmNvReadPublicCommand {
457            nv_index: handle.into(),
458        };
459        let (resp, _) = self.execute(device, &nv_read_public_cmd, &[], &[])?;
460        let read_public_resp = resp
461            .NvReadPublic()
462            .map_err(|_| JobError::ResponseMismatch(TpmCc::NvReadPublic))?;
463        let nv_public = read_public_resp.nv_public;
464        let data_size = nv_public.data_size as usize;
465
466        if data_size == 0 {
467            return Ok(None);
468        }
469
470        let auth_handle_val = if nv_public.attributes.contains(TpmaNv::AUTHREAD) {
471            handle
472        } else if nv_public.attributes.contains(TpmaNv::PPREAD) {
473            TpmRh::Platform as u32
474        } else if nv_public.attributes.contains(TpmaNv::OWNERREAD) {
475            TpmRh::Owner as u32
476        } else {
477            handle
478        };
479
480        let mut cert_bytes = Vec::with_capacity(data_size);
481        let mut offset = 0;
482        while offset < data_size {
483            let chunk_size = std::cmp::min(max_read_size, data_size - offset);
484
485            let nv_read_cmd = TpmNvReadCommand {
486                auth_handle: auth_handle_val.into(),
487                nv_index: handle.into(),
488                size: u16::try_from(chunk_size)?,
489                offset: u16::try_from(offset)?,
490            };
491
492            let flags_to_check = TpmaNv::AUTHREAD | TpmaNv::OWNERREAD | TpmaNv::PPREAD;
493            let needs_auth = (nv_public.attributes.bits() & flags_to_check.bits()) != 0;
494
495            let effective_auths: &[Auth] = if needs_auth { auths } else { &[] };
496
497            let (resp, _) =
498                self.execute(device, &nv_read_cmd, &[auth_handle_val], effective_auths)?;
499
500            let read_resp = resp
501                .NvRead()
502                .map_err(|_| JobError::ResponseMismatch(TpmCc::NvRead))?;
503            cert_bytes.extend_from_slice(read_resp.data.as_ref());
504            offset += chunk_size;
505        }
506
507        Ok(Some(cert_bytes))
508    }
509}
510
511impl Drop for Job<'_> {
512    fn drop(&mut self) {
513        self.cache.teardown(self.device.clone());
514    }
515}