1use std::os::fd::AsFd;
4use std::os::unix::ffi::OsStrExt;
5use std::path::Path;
6use std::str::FromStr;
7
8use anyhow::{Context, Result};
9use cap_std::fs::Dir;
10use cap_std_ext::cap_std;
11use composefs::fsverity as composefs_fsverity;
12use composefs_fsverity::Sha256HashValue;
13use ostree::gio;
14
15use crate::keyfileext::KeyFileExt;
16use crate::ostree_prepareroot::Tristate;
17
18const CONFIG_PATH: &str = "config";
20
21pub const INTEGRITY_SECTION: &str = "ex-integrity";
23pub const INTEGRITY_FSVERITY: &str = "fsverity";
25
26#[derive(Debug, Clone)]
28pub struct RepoVerityState {
29 pub desired: Tristate,
31 pub enabled: bool,
33}
34
35pub fn is_verity_enabled(repo: &ostree::Repo) -> Result<RepoVerityState> {
37 let desired = repo
38 .config()
39 .optional_string(INTEGRITY_SECTION, INTEGRITY_FSVERITY)?
40 .map(|s| Tristate::from_str(s.as_str()))
41 .transpose()?
42 .unwrap_or_default();
43 let repo_dir = &Dir::reopen_dir(&repo.dfd_borrow())?;
44 let config = repo_dir
45 .open(CONFIG_PATH)
46 .with_context(|| format!("Opening repository {CONFIG_PATH}"))?;
47 let enabled = composefs_fsverity::measure_verity::<Sha256HashValue>(config.as_fd()).is_ok();
50 Ok(RepoVerityState { desired, enabled })
51}
52
53fn enable_fsverity_in_objdir(d: &Dir) -> anyhow::Result<()> {
55 for ent in d.entries()? {
56 let ent = ent?;
57 if !ent.file_type()?.is_file() {
58 continue;
59 }
60 let name = ent.file_name();
61 let Some(b"file") = Path::new(&name).extension().map(|e| e.as_bytes()) else {
62 continue;
63 };
64 let f = d.open(&name)?;
65 let enabled =
66 composefs::fsverity::measure_verity_opt::<Sha256HashValue>(f.as_fd())?.is_some();
67 if !enabled {
68 composefs_fsverity::enable_verity_with_retry::<Sha256HashValue>(f.as_fd())?;
73 }
74 }
75 Ok(())
76}
77
78pub async fn ensure_verity(repo: &ostree::Repo) -> Result<()> {
84 let state = is_verity_enabled(repo)?;
85 if state.enabled {
87 return Ok(());
88 }
89
90 const MAX_CONCURRENT: usize = 3;
92
93 let repodir = Dir::reopen_dir(&repo.dfd_borrow())?;
94
95 let mut joinset = tokio::task::JoinSet::new();
97
98 for ent in repodir.read_dir("objects")? {
100 while joinset.len() >= MAX_CONCURRENT {
102 let _: () = joinset.join_next().await.unwrap()??;
104 }
105 let ent = ent?;
106 if !ent.file_type()?.is_dir() {
107 continue;
108 }
109 let objdir = ent.open_dir()?;
110 joinset.spawn_blocking(move || enable_fsverity_in_objdir(&objdir));
113 }
114
115 while let Some(output) = joinset.join_next().await {
117 let _: () = output??;
118 }
119
120 if state.desired != Tristate::Enabled {
122 let config = repo.copy_config();
123 config.set_boolean(INTEGRITY_SECTION, INTEGRITY_FSVERITY, true);
124 repo.write_config(&config)?;
125 repo.reload_config(gio::Cancellable::NONE)?;
126 }
127 let f = repodir.open(CONFIG_PATH)?;
130 match composefs_fsverity::enable_verity_raw::<Sha256HashValue>(f.as_fd()) {
131 Ok(()) => Ok(()),
132 Err(composefs_fsverity::EnableVerityError::AlreadyEnabled) => Ok(()),
133 Err(e) => Err(e.into()),
134 }
135}