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    context::ContextCache,
9    device::{self, Auth, Device},
10    pcr::{
11        pcr_composite_digest, pcr_get_bank_list, pcr_read, pcr_selection_vec_from_str,
12        pcr_selection_vec_to_tpml, Pcr,
13    },
14    policy::{
15        execute_policy, parse, Expression, PolicyError, SoftwarePolicySession, TpmPolicySession,
16    },
17    uri::Uri,
18};
19use argh::FromArgs;
20use std::{cell::RefCell, collections::HashSet, rc::Rc, str::FromStr};
21use strum::{Display, EnumString};
22use tpm2_protocol::{data::TpmAlgId, TpmHandle};
23
24/// The execution mode for a policy command.
25#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, EnumString)]
26#[strum(serialize_all = "kebab-case")]
27pub enum PolicyMode {
28    #[default]
29    Resolve,
30    Software,
31    Tpm,
32}
33
34/// Builds an authorization policy.
35#[derive(FromArgs, Debug, Default)]
36#[argh(
37    subcommand,
38    name = "policy",
39    note = "A policy expression for the digest is defined with an expression language
40e.g, 'sha256:0,...' or 'secret(\"tpm://...\")'."
41)]
42pub struct Policy {
43    /// execution mode: 'resolve' (default), 'software', or 'tpm'.
44    #[argh(option, long = "mode", default = "Default::default()")]
45    pub mode: PolicyMode,
46
47    /// session to be updated with policy commands
48    #[argh(option)]
49    pub auth: Option<String>,
50
51    /// policy expression
52    #[argh(positional)]
53    pub expression: String,
54}
55
56/// Traverses the AST, applying a fallible visitor closure to each `Pcr` expression.
57fn try_visit_pcr_expressions_mut<F>(
58    ast: &mut Expression,
59    visitor: &mut F,
60) -> Result<(), CommandError>
61where
62    F: FnMut(&mut Expression) -> Result<(), CommandError>,
63{
64    match ast {
65        Expression::Pcr {
66            selection: _,
67            digest: _,
68            count: _,
69        } => visitor(ast)?,
70        Expression::Or(branches) => {
71            for branch in branches.iter_mut() {
72                try_visit_pcr_expressions_mut(branch, visitor)?;
73            }
74        }
75        Expression::Secret {
76            auth_handle_uri, ..
77        } => {
78            try_visit_pcr_expressions_mut(auth_handle_uri, visitor)?;
79        }
80        Expression::Uri(_) => {}
81    }
82    Ok(())
83}
84
85impl SubCommand for Policy {
86    fn run(
87        &self,
88        device: Option<Rc<RefCell<Device>>>,
89        context: &mut ContextCache,
90        _plain: bool,
91    ) -> Result<(), CommandError> {
92        device::with_device(device, |device| {
93            let mut ast = parse(&self.expression)?;
94            let session_hash_alg = TpmAlgId::Sha256;
95
96            let mut required_selections = HashSet::new();
97            try_visit_pcr_expressions_mut(&mut ast, &mut |expr| {
98                if let Expression::Pcr {
99                    selection,
100                    digest: None,
101                    ..
102                } = expr
103                {
104                    required_selections.insert(selection.clone());
105                }
106                Ok(())
107            })?;
108
109            if !required_selections.is_empty() {
110                let banks = pcr_get_bank_list(device)?;
111                let selections_str = required_selections
112                    .into_iter()
113                    .collect::<Vec<_>>()
114                    .join("+");
115                let selections = pcr_selection_vec_from_str(&selections_str)?;
116                let tpml_selection = pcr_selection_vec_to_tpml(&selections, &banks)?;
117                let (pcr_values, _) = pcr_read(device, &tpml_selection)?;
118
119                let mut populator = |expr: &mut Expression| -> Result<(), CommandError> {
120                    if let Expression::Pcr {
121                        selection, digest, ..
122                    } = expr
123                    {
124                        if digest.is_none() {
125                            let selections_for_node = pcr_selection_vec_from_str(selection)?;
126                            let pcr_subset: Vec<Pcr> = pcr_values
127                                .iter()
128                                .filter(|pcr| {
129                                    selections_for_node.iter().any(|sel| {
130                                        sel.alg == pcr.bank && sel.indices.contains(&pcr.index)
131                                    })
132                                })
133                                .cloned()
134                                .collect();
135
136                            let composite_digest =
137                                pcr_composite_digest(&pcr_subset, session_hash_alg)?;
138                            *digest = Some(hex::encode(composite_digest));
139                        }
140                    }
141                    Ok(())
142                };
143                try_visit_pcr_expressions_mut(&mut ast, &mut populator)?;
144            }
145
146            if let Some(session_uri_str) = &self.auth {
147                let session_uri = Uri::from_str(session_uri_str)?;
148
149                let Uri::Session(session_handle) = session_uri else {
150                    return Err(CommandError::InvalidInput(
151                        "Session must be a session:// URI".to_string(),
152                    ));
153                };
154
155                context
156                    .session_map
157                    .prepare_sessions(device, &[Auth::Tracked(session_handle)])?;
158
159                let live_handle = context
160                    .session_map
161                    .get(&Uri::Session(session_handle).to_string())?
162                    .handle;
163
164                let mut session = TpmPolicySession::new(device, live_handle, session_hash_alg);
165                execute_policy(&ast, &mut session)?;
166
167                let new_context = device.save_context(live_handle.0)?;
168                let session_to_update = context.session_map.get_mut(session_uri_str)?;
169                session_to_update.context = new_context;
170                session_to_update.handle = tpm2_protocol::TpmHandle(0);
171            } else {
172                match self.mode {
173                    PolicyMode::Resolve => {
174                        writeln!(context.writer, "{ast}")?;
175                    }
176                    PolicyMode::Software => {
177                        let mut session = SoftwarePolicySession::new(session_hash_alg, device)?;
178                        let final_digest = execute_policy(&ast, &mut session)?;
179                        writeln!(context.writer, "{}", hex::encode(&*final_digest))?;
180                    }
181                    PolicyMode::Tpm => {
182                        let session_handle = start_trial_session(
183                            device,
184                            tpm2_protocol::data::TpmSe::Trial,
185                            session_hash_alg,
186                        )?;
187                        let final_digest = {
188                            let mut session =
189                                TpmPolicySession::new(device, session_handle, session_hash_alg);
190                            execute_policy(&ast, &mut session)?
191                        };
192                        device.flush_context(session_handle.0)?;
193                        writeln!(context.writer, "{}", hex::encode(&*final_digest))?;
194                    }
195                }
196            }
197            Ok(())
198        })
199    }
200}
201
202/// Starts a trial session.
203///
204/// # Errors
205///
206/// Returns `PolicyError` on failure.
207pub fn start_trial_session(
208    device: &mut Device,
209    session_type: tpm2_protocol::data::TpmSe,
210    hash_alg: TpmAlgId,
211) -> Result<TpmHandle, PolicyError> {
212    let (resp, _) = device.start_session(session_type, hash_alg)?;
213    Ok(resp.session_handle)
214}