use std::sync::Arc;
use crate::blob::{BlobError, BlobStore};
use crate::case::CaseStore;
use crate::core::StoreError;
use crate::export::CASE_PAGE;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct DrillReport {
pub cases: usize,
pub blobs_present: usize,
pub blobs_erased: usize,
pub sealed_open: usize,
pub sealed_erased: usize,
pub findings: Vec<String>,
pub not_checked: Vec<String>,
}
impl DrillReport {
#[must_use]
pub fn is_sound(&self) -> bool {
self.findings.is_empty()
}
}
#[derive(Debug)]
pub struct Stores<'a> {
pub cases: &'a Arc<dyn CaseStore>,
pub blobs: Option<&'a Arc<dyn BlobStore>>,
#[cfg(feature = "keyring")]
pub keys: Option<&'a Arc<dyn crate::keyring::KeyRing>>,
}
pub async fn drill(stores: &Stores<'_>) -> Result<DrillReport, StoreError> {
let mut report = DrillReport {
cases: 0,
blobs_present: 0,
blobs_erased: 0,
sealed_open: 0,
sealed_erased: 0,
findings: Vec::new(),
not_checked: Vec::new(),
};
if stores.blobs.is_none() {
report.not_checked.push(
"blob bytes — no blob store was supplied, so presence and integrity of the \
artifacts each case references were not established"
.to_owned(),
);
}
#[cfg(feature = "keyring")]
if stores.keys.is_none() {
report.not_checked.push(
"sealed-state keys — no key ring was supplied, so whether sealed case state \
still opens was not established"
.to_owned(),
);
}
#[cfg(not(feature = "keyring"))]
report.not_checked.push(
"sealed-state keys — this build carries no `keyring` feature, so whether sealed \
case state still opens was not established"
.to_owned(),
);
let mut after = None;
loop {
let page = stores.cases.cases(after, CASE_PAGE).await?;
let Some(last) = page.last() else { break };
after = Some(last.id);
let full = page.len() >= CASE_PAGE;
for case in page {
report.cases += 1;
if let Some(blobs) = stores.blobs {
check_blobs(&mut report, stores.cases, blobs.as_ref(), case.id).await?;
}
#[cfg(feature = "keyring")]
if let Some(keys) = stores.keys {
check_sealed(&mut report, keys.as_ref(), case.id, &case.state).await;
}
}
if !full {
break;
}
}
#[cfg(feature = "keyring")]
if stores.keys.is_some()
&& report.cases > 0
&& report.sealed_open == 0
&& report.sealed_erased == 0
{
report.not_checked.push(
"sealed-state coverage — a key ring was supplied and no case's state was \
sealed, so this pass proved nothing about sealing: either this plane keeps \
case state plaintext by design, or sealing was never wired to the case \
store. The two cannot be told apart from here, and only the second is a \
misconfiguration worth chasing"
.to_owned(),
);
}
Ok(report)
}
async fn check_blobs(
report: &mut DrillReport,
cases: &Arc<dyn CaseStore>,
blobs: &dyn BlobStore,
case: crate::core::CaseId,
) -> Result<(), StoreError> {
for digest in cases.blobs_of(case).await? {
match blobs.get(digest).await {
Ok(bytes) => {
drop(bytes);
report.blobs_present += 1;
}
Err(BlobError::Expired { .. }) => report.blobs_erased += 1,
Err(BlobError::NotFound(_)) => report.findings.push(format!(
"case {case}, blob {digest}: the bytes are gone with no tombstone — \
unexplained loss, which is a different fact from erasure and cannot be \
settled from the journal, because the journal deliberately never held \
the bytes"
)),
Err(e @ BlobError::Corrupt { .. }) => report.findings.push(format!(
"case {case}, blob {digest}: {e} — content that cannot be trusted is \
worse than content that is missing, because it is used"
)),
Err(BlobError::Backend(e)) => report.not_checked.push(format!(
"case {case}, blob {digest}: the blob store could not be reached ({e}) — \
presence was not established either way"
)),
}
}
Ok(())
}
#[cfg(feature = "keyring")]
async fn check_sealed(
report: &mut DrillReport,
keys: &dyn crate::keyring::KeyRing,
case: crate::core::CaseId,
state: &serde_json::Value,
) {
use crate::keyring::KeyError;
match crate::keyring::probe_sealed_case_state(keys, case, state).await {
None => {}
Some(Ok(())) => report.sealed_open += 1,
Some(Err(KeyError::Destroyed { .. })) => report.sealed_erased += 1,
Some(Err(KeyError::Unavailable(e))) => report.not_checked.push(format!(
"case {case}: the key ring could not be reached ({e}) — whether the sealed \
state opens was not established either way"
)),
Some(Err(e)) => report.findings.push(format!(
"case {case}: sealed state neither opens nor was its key destroyed ({e}) — \
an erasure would have said so, which makes this loss or tampering"
)),
}
}