tpm2_policy_language/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2025 Opinsys Oy
3// Copyright (c) 2024-2025 Jarkko Sakkinen
4
5//! A parser for the TPM 2.0 policy language.
6//!
7//! This crate provides the necessary components to parse a policy language
8//! string into an Abstract Syntax Tree (AST), represented by the
9//! [`Expression`] enum. The main entry point is the [`Expression::new()`]
10//! function.
11//!
12//! It also provides the [`Expression::to_command_list()`] function to convert a
13//! parsed AST into a sequence of serialized TPM command blobs, and the
14//! [`Expression::from_command_list()`] function to perform the reverse
15//! operation.
16
17#![deny(clippy::all)]
18#![deny(clippy::pedantic)]
19
20pub mod error;
21pub mod expression;
22
23pub use error::*;
24pub use expression::*;
25
26use std::{collections::HashMap, fmt, iter::Peekable, slice::Iter};
27
28use tpm2_crypto::TpmHash;
29use tpm2_protocol::{
30    basic::TpmHandle,
31    constant::TPM_PCR_SELECT_MAX,
32    data::{
33        Tpm2bDigest, Tpm2bName, Tpm2bNonce, TpmAlgId, TpmCc, TpmHt, TpmlPcrSelection,
34        TpmsPcrSelect, TpmsPcrSelection,
35    },
36    frame::{TpmPolicyOrCommand, TpmPolicyPcrCommand},
37    TpmMarshal, TpmSized, TpmWriter,
38};
39
40/// Pre-resolved data needed for policy execution.
41///
42/// This structure must be populated by the caller and passed to
43/// [`Expression::to_command_list()`].
44#[derive(Debug, Clone, Default)]
45pub struct TpmPolicyState {
46    names: HashMap<TpmHandle, Tpm2bName>,
47    pcrs: HashMap<TpmAlgId, HashMap<u32, Tpm2bDigest>>,
48    pcr_count: usize,
49}
50
51impl TpmPolicyState {
52    /// Initialize and return a new instace.
53    ///
54    /// # Errors
55    ///
56    /// Returns [`PcrCountMismatch`](crate::TpmPolicyError::PcrCountMismatch) when
57    /// PCR banks don't have exact same amount of PCRs.
58    pub fn new(
59        names: HashMap<TpmHandle, Tpm2bName>,
60        pcrs: HashMap<TpmAlgId, HashMap<u32, Tpm2bDigest>>,
61    ) -> Result<Self, TpmPolicyError> {
62        let mut pcr_count = 0;
63
64        for bank in pcrs.values() {
65            if pcr_count == 0 {
66                pcr_count = bank.len();
67            }
68
69            if pcr_count != bank.len() {
70                return Err(TpmPolicyError::PcrCountMismatch);
71            }
72        }
73
74        Ok(Self {
75            names,
76            pcrs,
77            pcr_count,
78        })
79    }
80
81    #[must_use]
82    pub fn names(&self) -> &HashMap<TpmHandle, Tpm2bName> {
83        &self.names
84    }
85}
86
87/// Parses a PCR selection string (e.g., "sha1:0,1+sha256:7") into a
88/// `TpmlPcrSelection` using context from the `PolicyState`.
89fn parse_tpml_pcr_selection_str(
90    selection_str: &str,
91    context: &TpmPolicyState,
92) -> Result<TpmlPcrSelection, TpmPolicyError> {
93    let mut list = TpmlPcrSelection::new();
94    let pcr_select_size = context.pcr_count.div_ceil(8);
95    if pcr_select_size > TPM_PCR_SELECT_MAX as usize {
96        return Err(TpmPolicyError::PcrSelectionTooLarge);
97    }
98
99    for part in selection_str.split('+') {
100        let (alg_str, indices_str) = part
101            .split_once(':')
102            .ok_or(TpmPolicyError::InvalidPcrSelection)?;
103
104        let alg = alg_str
105            .parse::<TpmHash>()
106            .map_err(|_| TpmPolicyError::InvalidPcrDigestAlgorithm)?;
107
108        if !context.pcrs.contains_key(&alg.into()) {
109            return Err(TpmPolicyError::PcrBankNotAvailable(alg));
110        }
111
112        let indices: Vec<u32> = indices_str
113            .split(',')
114            .map(str::parse)
115            .collect::<Result<_, _>>()
116            .map_err(|_| TpmPolicyError::InvalidPcrSelection)?;
117
118        let mut pcr_select_bytes = vec![0u8; pcr_select_size];
119        for &pcr_index in &indices {
120            let pcr_index = pcr_index as usize;
121            if pcr_index >= context.pcr_count {
122                return Err(TpmPolicyError::PcrIndexTooLarge);
123            }
124            pcr_select_bytes[pcr_index / 8] |= 1 << (pcr_index % 8);
125        }
126
127        list.try_push(TpmsPcrSelection {
128            hash: alg.into(),
129            pcr_select: TpmsPcrSelect::try_from(pcr_select_bytes.as_slice())
130                .map_err(|_| TpmPolicyError::PcrDigestTooLarge)?,
131        })
132        .map_err(|_| TpmPolicyError::PcrSelectionTooLarge)?;
133    }
134    Ok(list)
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138enum Token<'a> {
139    And,
140    Or,
141    LParen,
142    RParen,
143    Comma,
144    Ident(&'a str),
145}
146
147impl fmt::Display for Token<'_> {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            Token::And => write!(f, "'and'"),
151            Token::Or => write!(f, "'or'"),
152            Token::LParen => write!(f, "'('"),
153            Token::RParen => write!(f, "')'"),
154            Token::Comma => write!(f, "','"),
155            Token::Ident(s) => write!(f, "'{s}'"),
156        }
157    }
158}
159
160fn tokenize(input: &str) -> Vec<Token<'_>> {
161    let mut tokens = Vec::new();
162    let mut chars = input.char_indices().peekable();
163
164    while let Some((i, c)) = chars.next() {
165        match c {
166            '(' => tokens.push(Token::LParen),
167            ')' => tokens.push(Token::RParen),
168            ',' => tokens.push(Token::Comma),
169            c if c.is_whitespace() => {}
170            _ => {
171                let start = i;
172                let mut end = i + c.len_utf8();
173
174                while let Some(&(_, next_char)) = chars.peek() {
175                    if next_char.is_whitespace() || "(),".contains(next_char) {
176                        break;
177                    }
178                    let (next_i, _) = chars.next().unwrap();
179                    end = next_i + next_char.len_utf8();
180                }
181
182                let ident_slice = &input[start..end];
183                match ident_slice {
184                    "and" => tokens.push(Token::And),
185                    "or" => tokens.push(Token::Or),
186                    _ => tokens.push(Token::Ident(ident_slice)),
187                }
188            }
189        }
190    }
191    tokens
192}
193
194fn parse_expression<'a>(
195    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
196    context: &TpmPolicyState,
197) -> Result<TpmPolicyExpression, TpmPolicyError> {
198    parse_or(tokens, context)
199}
200
201fn parse_binary_expression<'a, F, G>(
202    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
203    mut operand_parser: F,
204    operator: &Token,
205    mut expression_combiner: G,
206    context: &TpmPolicyState,
207) -> Result<TpmPolicyExpression, TpmPolicyError>
208where
209    F: FnMut(
210        &mut Peekable<Iter<'a, Token<'a>>>,
211        &TpmPolicyState,
212    ) -> Result<TpmPolicyExpression, TpmPolicyError>,
213    G: FnMut(TpmPolicyExpression, TpmPolicyExpression) -> TpmPolicyExpression,
214{
215    let mut node = operand_parser(tokens, context)?;
216    while tokens.peek() == Some(&operator) {
217        tokens.next();
218        let rhs = operand_parser(tokens, context)?;
219        node = expression_combiner(node, rhs);
220    }
221    Ok(node)
222}
223
224fn parse_or<'a>(
225    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
226    context: &TpmPolicyState,
227) -> Result<TpmPolicyExpression, TpmPolicyError> {
228    parse_binary_expression(
229        tokens,
230        parse_and,
231        &Token::Or,
232        |lhs, rhs| match lhs {
233            TpmPolicyExpression::Or(mut terms) => {
234                terms.push(rhs);
235                TpmPolicyExpression::Or(terms)
236            }
237            _ => TpmPolicyExpression::Or(vec![lhs, rhs]),
238        },
239        context,
240    )
241}
242
243fn parse_and<'a>(
244    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
245    context: &TpmPolicyState,
246) -> Result<TpmPolicyExpression, TpmPolicyError> {
247    parse_binary_expression(
248        tokens,
249        parse_primary,
250        &Token::And,
251        |lhs, rhs| match lhs {
252            TpmPolicyExpression::And(mut factors) => {
253                factors.push(rhs);
254                TpmPolicyExpression::And(factors)
255            }
256            _ => TpmPolicyExpression::And(vec![lhs, rhs]),
257        },
258        context,
259    )
260}
261
262fn parse_primary<'a>(
263    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
264    context: &TpmPolicyState,
265) -> Result<TpmPolicyExpression, TpmPolicyError> {
266    let token = tokens.next().ok_or(TpmPolicyError::UnexpectedEnd)?;
267
268    match token {
269        Token::LParen => {
270            let expr = parse_or(tokens, context)?;
271            if tokens.next() != Some(&Token::RParen) {
272                return Err(TpmPolicyError::ParenthesisMismatch);
273            }
274            Ok(expr)
275        }
276        Token::Ident(name) => match *name {
277            "pcr" => Ok(parse_pcr_call(tokens, context)?),
278            "secret" => Ok(parse_secret_call(tokens, context)?),
279            _ => parse_literal(name),
280        },
281        _ => Err(TpmPolicyError::InvalidToken(token.to_string())),
282    }
283}
284
285fn parse_literal(s: &str) -> Result<TpmPolicyExpression, TpmPolicyError> {
286    if s.len() != 8 {
287        return Err(TpmPolicyError::InvalidToken(s.to_string()));
288    }
289
290    let raw =
291        u32::from_str_radix(s, 16).map_err(|_| TpmPolicyError::InvalidToken(s.to_string()))?;
292
293    let ht_byte = (raw >> 24) as u8;
294    TpmHt::try_from(ht_byte).map_err(|_| TpmPolicyError::InvalidHandleType(ht_byte))?;
295
296    Ok(TpmPolicyExpression::Handle(TpmHandle::from(raw)))
297}
298
299fn parse_pcr_call<'a>(
300    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
301    context: &TpmPolicyState,
302) -> Result<TpmPolicyExpression, TpmPolicyError> {
303    match tokens.next() {
304        Some(Token::LParen) => {}
305        Some(actual_token) => {
306            return Err(TpmPolicyError::InvalidToken(actual_token.to_string()));
307        }
308        None => {
309            return Err(TpmPolicyError::UnexpectedEnd);
310        }
311    }
312
313    let mut buf = String::new();
314    loop {
315        match tokens.next() {
316            Some(Token::RParen) => break,
317            Some(Token::Ident(s)) => buf.push_str(s),
318            Some(Token::Comma) => buf.push(','),
319            Some(tok @ (Token::And | Token::Or | Token::LParen)) => {
320                return Err(TpmPolicyError::InvalidToken(tok.to_string()));
321            }
322            None => return Err(TpmPolicyError::UnexpectedEnd),
323        }
324    }
325
326    if let Some((selection_part, digest_part)) = buf.rsplit_once(':') {
327        if let Ok(selections) = parse_tpml_pcr_selection_str(selection_part, context) {
328            if let Ok(digest_bytes) = hex::decode(digest_part) {
329                if let Ok(digest) = Tpm2bDigest::try_from(digest_bytes.as_slice()) {
330                    return Ok(TpmPolicyExpression::Pcr {
331                        selections,
332                        digest: Some(digest),
333                    });
334                }
335            }
336        }
337    }
338
339    let selections = parse_tpml_pcr_selection_str(&buf, context)?;
340    Ok(TpmPolicyExpression::Pcr {
341        selections,
342        digest: None,
343    })
344}
345
346fn parse_secret_call<'a>(
347    tokens: &mut Peekable<Iter<'a, Token<'a>>>,
348    context: &TpmPolicyState,
349) -> Result<TpmPolicyExpression, TpmPolicyError> {
350    match tokens.next() {
351        Some(Token::LParen) => {}
352        Some(actual_token) => {
353            return Err(TpmPolicyError::InvalidToken(actual_token.to_string()));
354        }
355        None => {
356            return Err(TpmPolicyError::UnexpectedEnd);
357        }
358    }
359
360    let auth_handle =
361        parse_or(tokens, context).map_err(|e| TpmPolicyError::InvalidToken(e.to_string()))?;
362
363    let copy_ref = match tokens.peek() {
364        Some(Token::Comma) => {
365            tokens.next();
366
367            let copy_ref_ident = match tokens.next() {
368                Some(Token::Ident(s)) => s,
369                Some(actual_token) => {
370                    return Err(TpmPolicyError::InvalidToken(actual_token.to_string()));
371                }
372                None => {
373                    return Err(TpmPolicyError::UnexpectedEnd);
374                }
375            };
376
377            let (key, value) = copy_ref_ident
378                .split_once(':')
379                .ok_or_else(|| TpmPolicyError::InvalidToken((*copy_ref_ident).to_string()))?;
380
381            if key != "copy_ref" {
382                return Err(TpmPolicyError::InvalidToken((*copy_ref_ident).to_string()));
383            }
384
385            let bytes = hex::decode(value)
386                .map_err(|_| TpmPolicyError::InvalidToken((*copy_ref_ident).to_string()))?;
387
388            if bytes.is_empty() {
389                None
390            } else {
391                let digest = Tpm2bDigest::try_from(bytes.as_slice())
392                    .map_err(|_| TpmPolicyError::InvalidToken((*copy_ref_ident).to_string()))?;
393                Some(digest)
394            }
395        }
396        Some(Token::RParen) => None,
397        Some(actual_token) => {
398            return Err(TpmPolicyError::InvalidToken(actual_token.to_string()));
399        }
400        None => {
401            return Err(TpmPolicyError::UnexpectedEnd);
402        }
403    };
404
405    match tokens.next() {
406        Some(Token::RParen) => {}
407        Some(actual_token) => {
408            return Err(TpmPolicyError::InvalidToken(actual_token.to_string()));
409        }
410        None => {
411            return Err(TpmPolicyError::UnexpectedEnd);
412        }
413    }
414
415    Ok(TpmPolicyExpression::Secret {
416        auth_handle: Box::new(auth_handle),
417        copy_ref,
418    })
419}
420
421/// A session that simulates TPM policy digest calculations in software.
422struct TpmPolicySession {
423    digest: Tpm2bDigest,
424    hash_alg: TpmAlgId,
425    digest_size: usize,
426}
427
428impl TpmPolicySession {
429    /// Creates a new software policy session.
430    fn new(hash_alg: TpmAlgId) -> Result<Self, TpmPolicyError> {
431        let digest_size = TpmHash::from(hash_alg).size();
432        let digest = Tpm2bDigest::try_from(vec![0; digest_size].as_slice())
433            .map_err(TpmPolicyError::Marshal)?;
434        Ok(Self {
435            digest,
436            hash_alg,
437            digest_size,
438        })
439    }
440
441    /// Applies a `TPM2_PolicyPCR` action to the session.
442    fn policy_pcr(&mut self, cmd: &TpmPolicyPcrCommand) -> Result<(), TpmPolicyError> {
443        let mut pcrs_bytes = vec![0u8; TpmlPcrSelection::SIZE];
444        let pcrs_bytes_len = {
445            let mut writer = TpmWriter::new(&mut pcrs_bytes);
446            cmd.pcrs
447                .marshal(&mut writer)
448                .map_err(TpmPolicyError::Marshal)?;
449            writer.len()
450        };
451        pcrs_bytes.truncate(pcrs_bytes_len);
452
453        let cc_bytes = (TpmCc::PolicyPcr as u32).to_be_bytes();
454        let chunks: Vec<&[u8]> = vec![
455            self.digest.as_ref(),
456            &cc_bytes,
457            &pcrs_bytes,
458            cmd.pcr_digest.as_ref(),
459        ];
460
461        let new_digest_bytes = TpmHash::from(self.hash_alg)
462            .digest(&chunks)
463            .map_err(TpmPolicyError::Crypto)?;
464        self.digest =
465            Tpm2bDigest::try_from(new_digest_bytes.as_slice()).map_err(TpmPolicyError::Marshal)?;
466        Ok(())
467    }
468
469    /// Applies a `TPM2_PolicyOR` action to the session.
470    fn policy_or(&mut self, cmd: &TpmPolicyOrCommand) -> Result<(), TpmPolicyError> {
471        let mut digests_as_bytes = Vec::with_capacity(cmd.p_hash_list.len() * self.digest_size);
472        for digest in cmd.p_hash_list.iter() {
473            digests_as_bytes.extend_from_slice(digest.as_ref());
474        }
475
476        let zero_digest = Tpm2bDigest::try_from(vec![0; self.digest_size].as_slice())
477            .map_err(TpmPolicyError::Marshal)?;
478        self.digest = zero_digest;
479
480        let cc_bytes = (TpmCc::PolicyOr as u32).to_be_bytes();
481        let chunks: Vec<&[u8]> = vec![self.digest.as_ref(), &cc_bytes, digests_as_bytes.as_slice()];
482
483        let new_digest_bytes = TpmHash::from(self.hash_alg)
484            .digest(&chunks)
485            .map_err(TpmPolicyError::Crypto)?;
486        self.digest =
487            Tpm2bDigest::try_from(new_digest_bytes.as_slice()).map_err(TpmPolicyError::Marshal)?;
488        Ok(())
489    }
490
491    /// Applies a `TPM2_PolicySecret` action to the session.
492    fn policy_secret(
493        &mut self,
494        auth_handle_name: &Tpm2bName,
495        policy_ref: &Tpm2bNonce,
496    ) -> Result<(), TpmPolicyError> {
497        let cc_bytes = (TpmCc::PolicySecret as u32).to_be_bytes();
498
499        let first_chunks: Vec<&[u8]> =
500            vec![self.digest.as_ref(), &cc_bytes, auth_handle_name.as_ref()];
501
502        let first_digest_bytes = TpmHash::from(self.hash_alg)
503            .digest(&first_chunks)
504            .map_err(TpmPolicyError::Crypto)?;
505        let first_digest = Tpm2bDigest::try_from(first_digest_bytes.as_slice())
506            .map_err(TpmPolicyError::Marshal)?;
507
508        let second_chunks: Vec<&[u8]> = vec![first_digest.as_ref(), policy_ref.as_ref()];
509
510        let new_digest_bytes = TpmHash::from(self.hash_alg)
511            .digest(&second_chunks)
512            .map_err(TpmPolicyError::Crypto)?;
513        self.digest =
514            Tpm2bDigest::try_from(new_digest_bytes.as_slice()).map_err(TpmPolicyError::Marshal)?;
515        Ok(())
516    }
517
518    /// Applies a `TPM2_PolicyRestart` action to the session.
519    fn policy_restart(&mut self) -> Result<(), TpmPolicyError> {
520        self.digest = Tpm2bDigest::try_from(vec![0; self.digest_size].as_slice())
521            .map_err(TpmPolicyError::Marshal)?;
522        Ok(())
523    }
524
525    /// Retrieves the final policy digest from the session.
526    fn get_digest(&self) -> Tpm2bDigest {
527        self.digest
528    }
529}
530
531/// Conditionally wraps a list of expressions in `Expression::And`.
532/// If the list contains exactly one item, it is returned directly.
533fn build_and_branch(mut branch: Vec<TpmPolicyExpression>) -> TpmPolicyExpression {
534    if branch.len() == 1 {
535        match branch.pop() {
536            Some(expr) => expr,
537            None => TpmPolicyExpression::And(Vec::new()),
538        }
539    } else {
540        TpmPolicyExpression::And(branch)
541    }
542}