cli/command/
policy.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    cli::SubCommand,
7    command::CommandError,
8    device::with_device,
9    job::Job,
10    pcr::{pcr_composite_digest, pcr_get_bank_list, pcr_read},
11    policy::{
12        execute_policy, parse, visit_pcr_expressions_mut, Expression, PolicyError,
13        SoftwarePolicySession, TpmPolicySession,
14    },
15    vtpm::VtpmSession,
16};
17use clap::Args;
18use std::collections::HashSet;
19use strum::{Display, EnumString};
20use tpm2_protocol::{
21    data::{TpmAlgId, TpmRh, TpmSe},
22    TpmHandle,
23};
24
25/// The execution mode for a policy command.
26#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, EnumString)]
27#[strum(serialize_all = "kebab-case")]
28pub enum PolicyMode {
29    #[default]
30    Resolve,
31    Software,
32    Tpm,
33    Session,
34}
35
36/// Builds an authorization policy.
37#[derive(Args, Debug, Default)]
38#[command()]
39pub struct Policy {
40    /// Execution mode: 'resolve' (default), 'software', 'tpm', or 'session'.
41    #[arg(short = 'm', long = "mode", default_value_t = PolicyMode::default(), value_parser = clap::value_parser!(PolicyMode))]
42    pub mode: PolicyMode,
43
44    /// Policy expression
45    pub expression: String,
46}
47
48/// Populates the AST with PCR digests by reading current values from the TPM.
49fn resolve_pcr_digests(
50    device: &mut crate::device::Device,
51    ast: &mut Expression,
52    session_hash_alg: TpmAlgId,
53) -> Result<(), CommandError> {
54    let mut required_selections = HashSet::new();
55    visit_pcr_expressions_mut(ast, &mut |expr| -> Result<(), PolicyError> {
56        if let Expression::Pcr {
57            selections,
58            digest: None,
59            ..
60        } = expr
61        {
62            for s in selections {
63                required_selections.insert(s.clone());
64            }
65        }
66        Ok(())
67    })?;
68
69    if !required_selections.is_empty() {
70        let banks = pcr_get_bank_list(device)?;
71        let selections: Vec<_> = required_selections.into_iter().collect();
72        let tpml_selection = crate::pcr::pcr_selection_vec_to_tpml(&selections, &banks)?;
73        let (pcr_values, _) = pcr_read(device, &tpml_selection)?;
74
75        let mut populator = |expr: &mut Expression| -> Result<(), PolicyError> {
76            if let Expression::Pcr {
77                selections, digest, ..
78            } = expr
79            {
80                if digest.is_none() {
81                    let pcr_subset: Vec<crate::pcr::Pcr> = pcr_values
82                        .iter()
83                        .filter(|pcr| {
84                            selections
85                                .iter()
86                                .any(|sel| sel.alg == pcr.bank && sel.indices.contains(&pcr.index))
87                        })
88                        .cloned()
89                        .collect();
90
91                    let composite_digest = pcr_composite_digest(&pcr_subset, session_hash_alg)?;
92                    *digest = Some(hex::encode(composite_digest));
93                }
94            }
95            Ok(())
96        };
97        visit_pcr_expressions_mut(ast, &mut populator)?;
98    }
99    Ok(())
100}
101
102impl SubCommand for Policy {
103    fn run(&self, job: &mut Job) -> Result<(), CommandError> {
104        with_device(job.device.clone(), |device| {
105            let mut ast = parse(&self.expression)?;
106            match ast {
107                Expression::Auth(_) | Expression::Handle(_) => {
108                    return Err(CommandError::InvalidInput(
109                        "not a valid policy expression".to_string(),
110                    ));
111                }
112                _ => {}
113            }
114            let session_hash_alg = TpmAlgId::Sha256;
115
116            resolve_pcr_digests(device, &mut ast, session_hash_alg)?;
117
118            match self.mode {
119                PolicyMode::Resolve => {
120                    writeln!(job.writer, "{ast}")?;
121                }
122                PolicyMode::Software => {
123                    let mut session = SoftwarePolicySession::new(session_hash_alg, device)?;
124                    let final_digest = execute_policy(&ast, &mut session)?;
125                    writeln!(job.writer, "{}", hex::encode(&*final_digest))?;
126                }
127                PolicyMode::Tpm => {
128                    let session_handle =
129                        start_trial_session(device, TpmSe::Trial, session_hash_alg)?;
130                    let final_digest = {
131                        let mut session =
132                            TpmPolicySession::new(device, session_handle, session_hash_alg);
133                        execute_policy(&ast, &mut session)?
134                    };
135                    let flush_result = device.flush_context(session_handle);
136                    flush_result?;
137                    writeln!(job.writer, "{}", hex::encode(&*final_digest))?;
138                }
139                PolicyMode::Session => {
140                    let (resp, nonce_caller) = device.start_session(
141                        TpmSe::Policy,
142                        session_hash_alg,
143                        (TpmRh::Null as u32).into(),
144                    )?;
145                    let mut tpm_policy_session =
146                        TpmPolicySession::new(device, resp.session_handle, session_hash_alg);
147                    match execute_policy(&ast, &mut tpm_policy_session) {
148                        Ok(_) => {
149                            let mut session =
150                                VtpmSession::new(session_hash_alg, nonce_caller, &resp, &[])?;
151                            session.context = device.save_context(resp.session_handle)?;
152                            let vhandle = job.cache.add_session(session);
153                            job.cache.save()?;
154                            writeln!(job.writer, "vtpm:{vhandle:08x}")?;
155                        }
156                        Err(e) => {
157                            let _ = device.flush_context(resp.session_handle);
158                            return Err(e.into());
159                        }
160                    }
161                }
162            }
163            Ok(())
164        })
165    }
166}
167
168/// Starts a trial session.
169///
170/// # Errors
171///
172/// Returns `PolicyError` on failure.
173pub fn start_trial_session(
174    device: &mut crate::device::Device,
175    session_type: tpm2_protocol::data::TpmSe,
176    hash_alg: TpmAlgId,
177) -> Result<TpmHandle, PolicyError> {
178    let (resp, _) = device.start_session(session_type, hash_alg, (TpmRh::Null as u32).into())?;
179    Ok(resp.session_handle)
180}