cli/
pcr.rs

1// SPDX-License-Identifier: GPL-3-0-or-later
2// Copyright (c) 2025 Opinsys Oy
3// Copyright (c) 2024-2025 Jarkko Sakkinen
4
5//! Abstractions and logic for handling Platform Configuration Registers (PCRs).
6
7use crate::{
8    crypto::{crypto_digest, CryptoError},
9    device::{Device, DeviceError},
10    key::Tpm2shAlgId,
11};
12use std::{convert::TryFrom, fmt};
13use thiserror::Error;
14use tpm2_protocol::{
15    constant::TPM_PCR_SELECT_MAX,
16    data::{
17        TpmAlgId, TpmCap, TpmCc, TpmlPcrSelection, TpmsPcrSelect, TpmsPcrSelection,
18        TpmuCapabilities,
19    },
20    message::TpmPcrReadCommand,
21    TpmError,
22};
23
24#[derive(Debug, Error)]
25pub enum PcrError {
26    #[error("device: {0}")]
27    Device(#[from] DeviceError),
28    #[error("invalid algorithm: {0:?}")]
29    InvalidAlgorithm(TpmAlgId),
30    #[error("invalid PCR selection: {0}")]
31    InvalidPcrSelection(String),
32    #[error("TPM: {0}")]
33    Tpm(TpmError),
34    #[error("crypto: {0}")]
35    Crypto(#[from] CryptoError),
36}
37
38impl From<TpmError> for PcrError {
39    fn from(err: TpmError) -> Self {
40        Self::Tpm(err)
41    }
42}
43
44/// Represents the state of a single PCR register.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Pcr {
47    pub bank: TpmAlgId,
48    pub index: u32,
49    pub value: Vec<u8>,
50}
51
52/// Represents the properties of a single PCR bank.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct PcrBank {
55    pub alg: TpmAlgId,
56    pub count: usize,
57}
58
59/// Represents a user's selection of PCR indices for a specific bank.
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct PcrSelection {
62    pub alg: TpmAlgId,
63    pub indices: Vec<u32>,
64}
65
66impl fmt::Display for PcrSelection {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        let indices_str = self
69            .indices
70            .iter()
71            .map(ToString::to_string)
72            .collect::<Vec<_>>()
73            .join(",");
74        write!(f, "{}:{}", crate::key::Tpm2shAlgId(self.alg), indices_str)
75    }
76}
77
78/// Discovers the list of available PCR banks and their sizes from the TPM.
79///
80/// # Errors
81///
82/// Returns a `PcrError` if the TPM capability query fails or if the TPM reports
83/// no active PCR banks.
84pub fn pcr_get_bank_list(device: &mut Device) -> Result<Vec<PcrBank>, PcrError> {
85    let (_, cap_data) = device.get_capability_page(TpmCap::Pcrs, 0, 1)?;
86    let mut banks = Vec::new();
87    if let TpmuCapabilities::Pcrs(pcrs) = cap_data.data {
88        for bank in pcrs.iter() {
89            banks.push(PcrBank {
90                alg: bank.hash,
91                count: bank.pcr_select.len() * 8,
92            });
93        }
94    }
95    if banks.is_empty() {
96        return Err(PcrError::InvalidPcrSelection(
97            "TPM reported no active PCR banks.".to_string(),
98        ));
99    }
100    banks.sort_by_key(|b| b.alg);
101    Ok(banks)
102}
103
104/// Parses a PCR selection string (e.g., "sha256:0,7+sha1:1") into a vector of
105/// `PcrSelection`.
106///
107/// # Errors
108///
109/// Returns a `PcrError` if the selection string is malformed, contains an
110/// invalid algorithm name, or has non-numeric PCR indices.
111pub fn pcr_selection_vec_from_str(selection_str: &str) -> Result<Vec<PcrSelection>, PcrError> {
112    selection_str
113        .split('+')
114        .map(|part| {
115            let (alg_str, indices_str) = part
116                .split_once(':')
117                .ok_or_else(|| PcrError::InvalidPcrSelection(part.to_string()))?;
118
119            let alg = Tpm2shAlgId::try_from(alg_str)
120                .map_err(|e| PcrError::InvalidPcrSelection(e.to_string()))?
121                .0;
122
123            let indices: Vec<u32> = indices_str
124                .split(',')
125                .map(|s| {
126                    s.parse::<u32>()
127                        .map_err(|_| PcrError::InvalidPcrSelection(indices_str.to_string()))
128                })
129                .collect::<Result<_, _>>()?;
130
131            Ok(PcrSelection { alg, indices })
132        })
133        .collect()
134}
135
136/// Converts a vector of `PcrSelection` into the low-level `TpmlPcrSelection`
137/// format.
138///
139/// # Errors
140///
141/// Returns a `PcrError` if a selected algorithm is not present in the provided
142/// list of banks, or if a selected PCR index is out of bounds for its bank.
143pub fn pcr_selection_vec_to_tpml(
144    selections: &[PcrSelection],
145    banks: &[PcrBank],
146) -> Result<TpmlPcrSelection, PcrError> {
147    let mut list = TpmlPcrSelection::new();
148    for selection in selections {
149        let bank = banks
150            .iter()
151            .find(|b| b.alg == selection.alg)
152            .ok_or_else(|| {
153                PcrError::InvalidPcrSelection(format!(
154                    "PCR bank for algorithm {:?} not found or supported by TPM",
155                    selection.alg
156                ))
157            })?;
158        let pcr_select_size = bank.count.div_ceil(8);
159        if pcr_select_size > TPM_PCR_SELECT_MAX {
160            return Err(PcrError::InvalidPcrSelection(format!(
161                "invalid select size {pcr_select_size} (> {TPM_PCR_SELECT_MAX})"
162            )));
163        }
164        let mut pcr_select_bytes = vec![0u8; pcr_select_size];
165        for &pcr_index in &selection.indices {
166            let pcr_index = pcr_index as usize;
167            if pcr_index >= bank.count {
168                return Err(PcrError::InvalidPcrSelection(format!(
169                    "invalid index {pcr_index} for {:?} bank (max is {})",
170                    bank.alg,
171                    bank.count - 1
172                )));
173            }
174            pcr_select_bytes[pcr_index / 8] |= 1 << (pcr_index % 8);
175        }
176        list.try_push(TpmsPcrSelection {
177            hash: selection.alg,
178            pcr_select: TpmsPcrSelect::try_from(pcr_select_bytes.as_slice())?,
179        })?;
180    }
181    Ok(list)
182}
183
184/// Reads the selected PCRs and returns them in a structured format.
185///
186/// # Errors
187///
188/// Returns a `PcrError` if the `TPM2_PcrRead` command fails or if the TPM's
189/// response does not contain the expected number of digests for the selection.
190pub fn pcr_read(
191    device: &mut Device,
192    pcr_selection_in: &TpmlPcrSelection,
193) -> Result<(Vec<Pcr>, u32), PcrError> {
194    let cmd = TpmPcrReadCommand {
195        pcr_selection_in: *pcr_selection_in,
196    };
197    let (resp, _) = device.execute(&cmd, &[])?;
198    let pcr_read_resp = resp
199        .PcrRead()
200        .map_err(|_| DeviceError::ResponseMismatch(TpmCc::PcrRead))?;
201    let mut pcrs = Vec::new();
202    let mut digest_iter = pcr_read_resp.pcr_values.iter();
203    for selection in pcr_read_resp.pcr_selection_out.iter() {
204        for (byte_idx, &byte) in selection.pcr_select.iter().enumerate() {
205            if byte == 0 {
206                continue;
207            }
208            for bit_idx in 0..8 {
209                if (byte >> bit_idx) & 1 == 1 {
210                    let pcr_index = u32::try_from(byte_idx * 8 + bit_idx)
211                        .map_err(|_| PcrError::InvalidPcrSelection("PCR index overflow".into()))?;
212                    let value = digest_iter.next().ok_or_else(|| {
213                        PcrError::InvalidPcrSelection("PCR selection mismatch".to_string())
214                    })?;
215                    pcrs.push(Pcr {
216                        bank: selection.hash,
217                        index: pcr_index,
218                        value: value.to_vec(),
219                    });
220                }
221            }
222        }
223    }
224    Ok((pcrs, pcr_read_resp.pcr_update_counter))
225}
226
227/// Computes a composite digest from a set of PCRs using a specified algorithm.
228///
229/// # Errors
230///
231/// Returns a `PcrError` if the provided hash algorithm is not supported for
232/// creating a composite digest.
233pub fn pcr_composite_digest(pcrs: &[Pcr], alg: TpmAlgId) -> Result<Vec<u8>, PcrError> {
234    let digests: Vec<&[u8]> = pcrs.iter().map(|p| p.value.as_slice()).collect();
235    Ok(crypto_digest(alg, &digests)?)
236}