1use crate::objgv::*;
6use anyhow::{Context, Result};
7use camino::Utf8PathBuf;
8use fn_error_context::context;
9use gio::glib;
10use gio::prelude::*;
11use glib::Variant;
12use gvariant::aligned_bytes::TryAsAligned;
13use gvariant::{Marker, Structure, gv};
14use ostree::gio;
15use rustix::fd::BorrowedFd;
16use std::collections::{BTreeMap, HashMap};
17use std::ffi::CString;
18use std::fs::File;
19use std::io::Seek;
20use std::os::unix::io::AsRawFd;
21use std::process::{Command, Stdio};
22
23const IMA_XATTR: &str = "security.ima";
25
26#[derive(Debug, Clone)]
28pub struct ImaOpts {
29 pub algorithm: String,
31
32 pub key: Utf8PathBuf,
34
35 pub overwrite: bool,
37}
38
39fn xattrs_to_map(v: &glib::Variant) -> BTreeMap<Vec<u8>, Vec<u8>> {
41 let v = v.data_as_bytes();
42 let v = v.try_as_aligned().unwrap();
43 let v = gv!("a(ayay)").cast(v);
44 let mut map: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
45 for e in v.iter() {
46 let (k, v) = e.to_tuple();
47 map.insert(k.into(), v.into());
48 }
49 map
50}
51
52pub(crate) fn new_variant_a_ayay<'a, T: 'a + AsRef<[u8]>>(
54 items: impl IntoIterator<Item = (T, T)>,
55) -> glib::Variant {
56 let children = items.into_iter().map(|(a, b)| {
57 let a = a.as_ref();
58 let b = b.as_ref();
59 Variant::tuple_from_iter([a.to_variant(), b.to_variant()])
60 });
61 Variant::array_from_iter::<(&[u8], &[u8])>(children)
62}
63
64struct CommitRewriter<'a> {
65 repo: &'a ostree::Repo,
66 ima: &'a ImaOpts,
67 tempdir: tempfile::TempDir,
68 rewritten_files: HashMap<String, String>,
70}
71
72#[allow(unsafe_code)]
73#[context("Gathering xattr {}", k)]
74fn steal_xattr(f: &File, k: &str) -> Result<Vec<u8>> {
75 let k = &CString::new(k)?;
76 unsafe {
77 let k = k.as_ptr() as *const _;
78 let r = libc::fgetxattr(f.as_raw_fd(), k, std::ptr::null_mut(), 0);
79 if r < 0 {
80 return Err(std::io::Error::last_os_error().into());
81 }
82 let sz: usize = r.try_into()?;
83 let mut buf = vec![0u8; sz];
84 let r = libc::fgetxattr(f.as_raw_fd(), k, buf.as_mut_ptr() as *mut _, sz);
85 if r < 0 {
86 return Err(std::io::Error::last_os_error().into());
87 }
88 let r = libc::fremovexattr(f.as_raw_fd(), k);
89 if r < 0 {
90 return Err(std::io::Error::last_os_error().into());
91 }
92 Ok(buf)
93 }
94}
95
96impl<'a> CommitRewriter<'a> {
97 fn new(repo: &'a ostree::Repo, ima: &'a ImaOpts) -> Result<Self> {
98 Ok(Self {
99 repo,
100 ima,
101 tempdir: tempfile::tempdir_in(format!("/proc/self/fd/{}/tmp", repo.dfd()))?,
102 rewritten_files: Default::default(),
103 })
104 }
105
106 #[allow(unsafe_code)]
112 #[context("IMA signing object")]
113 fn ima_sign(&self, instream: &gio::InputStream) -> Result<HashMap<Vec<u8>, Vec<u8>>> {
114 let mut tempf = tempfile::NamedTempFile::new_in(self.tempdir.path())?;
115 if let Ok(instream) = instream.clone().downcast::<gio::UnixInputStream>() {
117 use cap_std_ext::cap_std::io_lifetimes::AsFilelike;
118 let instream_fd = unsafe { BorrowedFd::borrow_raw(instream.as_raw_fd()) };
120 let instream_fd = instream_fd.as_filelike_view::<std::fs::File>();
121 std::io::copy(&mut (&*instream_fd), tempf.as_file_mut())?;
122 } else {
123 let mut instream = instream.clone().into_read();
126 let _n = std::io::copy(&mut instream, tempf.as_file_mut())?;
127 }
128 tempf.seek(std::io::SeekFrom::Start(0))?;
129
130 let mut proc = Command::new("evmctl");
131 proc.current_dir(self.tempdir.path())
132 .stdout(Stdio::null())
133 .stderr(Stdio::piped())
134 .args(["ima_sign", "--xattr-user", "--key", self.ima.key.as_str()])
135 .args(["--hashalgo", self.ima.algorithm.as_str()])
136 .arg(tempf.path().file_name().unwrap());
137 let status = proc.output().context("Spawning evmctl")?;
138 if !status.status.success() {
139 return Err(anyhow::anyhow!(
140 "evmctl failed: {:?}\n{}",
141 status.status,
142 String::from_utf8_lossy(&status.stderr),
143 ));
144 }
145 let mut r = HashMap::new();
146 let user_k = IMA_XATTR.replace("security.", "user.");
147 let v = steal_xattr(tempf.as_file(), user_k.as_str())?;
148 r.insert(Vec::from(IMA_XATTR.as_bytes()), v);
149 Ok(r)
150 }
151
152 #[context("Content object {}", checksum)]
153 fn map_file(&mut self, checksum: &str) -> Result<Option<String>> {
154 let cancellable = gio::Cancellable::NONE;
155 let (instream, meta, xattrs) = self.repo.load_file(checksum, cancellable)?;
156 let instream = if let Some(i) = instream {
157 i
158 } else {
159 return Ok(None);
160 };
161 let mut xattrs = xattrs_to_map(&xattrs);
162 let existing_sig = xattrs.remove(IMA_XATTR.as_bytes());
163 if existing_sig.is_some() && !self.ima.overwrite {
164 return Ok(None);
165 }
166
167 let xattrs = {
169 let signed = self.ima_sign(&instream)?;
170 xattrs.extend(signed);
171 new_variant_a_ayay(&xattrs)
172 };
173 let (instream, _, _) = self.repo.load_file(checksum, cancellable)?;
175 let instream = instream.unwrap();
176 let (ostream, size) =
177 ostree::raw_file_to_content_stream(&instream, &meta, Some(&xattrs), cancellable)?;
178 let new_checksum = self
179 .repo
180 .write_content(None, &ostream, size, cancellable)?
181 .to_hex();
182
183 Ok(Some(new_checksum))
184 }
185
186 fn map_dirtree(&mut self, checksum: &str) -> Result<String> {
188 let src = &self
189 .repo
190 .load_variant(ostree::ObjectType::DirTree, checksum)?;
191 let src = src.data_as_bytes();
192 let src = src.try_as_aligned()?;
193 let src = gv_dirtree!().cast(src);
194 let (files, dirs) = src.to_tuple();
195
196 let mut hexbuf = [0u8; 64];
198
199 let mut new_files = Vec::new();
200 for file in files {
201 let (name, csum) = file.to_tuple();
202 let name = name.to_str();
203 hex::encode_to_slice(csum, &mut hexbuf)?;
204 let checksum = std::str::from_utf8(&hexbuf)?;
205 if let Some(mapped) = self.rewritten_files.get(checksum) {
206 new_files.push((name, hex::decode(mapped)?));
207 } else if let Some(mapped) = self.map_file(checksum)? {
208 let mapped_bytes = hex::decode(&mapped)?;
209 self.rewritten_files.insert(checksum.into(), mapped);
210 new_files.push((name, mapped_bytes));
211 } else {
212 new_files.push((name, Vec::from(csum)));
213 }
214 }
215
216 let mut new_dirs = Vec::new();
217 for item in dirs {
218 let (name, contents_csum, meta_csum_bytes) = item.to_tuple();
219 let name = name.to_str();
220 hex::encode_to_slice(contents_csum, &mut hexbuf)?;
221 let contents_csum = std::str::from_utf8(&hexbuf)?;
222 let mapped = self.map_dirtree(contents_csum)?;
223 let mapped = hex::decode(mapped)?;
224 new_dirs.push((name, mapped, meta_csum_bytes));
225 }
226
227 let new_dirtree = (new_files, new_dirs).to_variant();
228
229 let mapped = self
230 .repo
231 .write_metadata(
232 ostree::ObjectType::DirTree,
233 None,
234 &new_dirtree,
235 gio::Cancellable::NONE,
236 )?
237 .to_hex();
238
239 Ok(mapped)
240 }
241
242 #[context("Mapping {}", rev)]
244 fn map_commit(&mut self, rev: &str) -> Result<String> {
245 let checksum = self.repo.require_rev(rev)?;
246 let cancellable = gio::Cancellable::NONE;
247 let (commit_v, _) = self.repo.load_commit(&checksum)?;
248 let commit_v = &commit_v;
249
250 let commit_bytes = commit_v.data_as_bytes();
251 let commit_bytes = commit_bytes.try_as_aligned()?;
252 let commit = gv_commit!().cast(commit_bytes);
253 let commit = commit.to_tuple();
254 let contents = &hex::encode(commit.6);
255
256 let new_dt = self.map_dirtree(contents)?;
257
258 let n_parts = 8;
259 let mut parts = Vec::with_capacity(n_parts);
260 for i in 0..n_parts {
261 parts.push(commit_v.child_value(i));
262 }
263 let new_dt = hex::decode(new_dt)?;
264 parts[6] = new_dt.to_variant();
265 let new_commit = Variant::tuple_from_iter(&parts);
266
267 let new_commit_checksum = self
268 .repo
269 .write_metadata(ostree::ObjectType::Commit, None, &new_commit, cancellable)?
270 .to_hex();
271
272 Ok(new_commit_checksum)
273 }
274}
275
276pub fn ima_sign(repo: &ostree::Repo, ostree_ref: &str, opts: &ImaOpts) -> Result<String> {
284 let writer = &mut CommitRewriter::new(repo, opts)?;
285 writer.map_commit(ostree_ref)
286}