crates/gunnar-server/src/layout_config.rs
//! **The layout config: gunnar is TOLD where its data lives, and obeys.**
//!
//! # The rule, and why it is the whole design
//!
//! gunnar has **no disk logic**. It does not survey `/sys/block`, it does not
//! read the mount table to work out what `/data` is, and it does not probe to
//! check what it was told. `korp-installer` chooses the layout, partitions it,
//! RAIDs it, lays the directories — and then installs gunnar **with parameters
//! saying where to fetch data**. That is not a new idea: `GUNNAR_DATA_ROOT=/data`
//! is a boot parameter the installer bakes in today, and this file is that idea
//! carried to completion.
//!
//! Inferring is how *"why is this disk unused?"* gets answered wrongly. A
//! current mount table can say what exists; it can never say **why** a disk was
//! rejected, which group won `/data`, or what the installer was aiming at. So
//! the reasoning is written down at install time by the thing that did the
//! reasoning, and read back verbatim.
//!
//! # Where it is, and why there
//!
//! `/boot/korp/layout.conf` — on the ESP, mounted read-only at `/boot`, beside
//! the trust records (`korp_installer::layout::CONFIG_ESP_PATH`). It is not on
//! `/data` for a plain reason the installer states: it is the thing that SAYS
//! where `/data` is, and a config that has to be found by first knowing what it
//! tells you is not a config.
//!
//! # One artifact, two readers
//!
//! The same file is read for **placement** (`crate::slow`) and rendered for the
//! **admin** (`Admin.StorageInfo`'s storage reality). Not two views of the same
//! facts that can drift apart: one parse, one set of key-value pairs, handed to
//! both.
//!
//! # The format
//!
//! `korp_installer::layout::LayoutConfig::body` + `seal` — version FIRST, then
//! `key=value`, one per line, keys unique, values escaped (`\n`, `\\`) so no
//! value can forge a line; lists are indexed keys with a `.count`:
//!
//! ```text
//! korp-layout-config 1
//! install.unix=1789…
//! boot.dev=/dev/vda
//! boot.why=largest-group-remainder
//! data.count=4
//! data.0.dev=/dev/vdb
//! unused.0.why=unequal, not used
//! dir.log.path=/data/log ← the parameters: what the app is TOLD
//! dir.log.device=serialdat
//! dir.log.off_stripe_satisfied=yes
//! auth.domain=korp.layout-config.v1
//! auth.blake3=…
//! auth.signature=
//! ```
//!
//! Unknown KEYS are ignored — that is what lets version 1 gain keys — but an
//! unknown VERSION is refused, because a key whose meaning changed underneath
//! is worse than no config.
//!
//! # `dir.<name>.path` is obeyed, and there is no second placement mechanism
//!
//! The installer lays `crash`, `log`, `gc`, `quarantine` (and the two that are
//! not gunnar's) PHYSICALLY on `/serialdat` and makes `/data/<name>` a
//! **symlink** to each. So `dir.log.path` is `/data/log` and writing there
//! already lands on the other spindle. gunnar writes the path it is given and
//! adds nothing on top: two things deciding the same thing is how they start
//! disagreeing.
//!
//! # Absent is a first-class answer
//!
//! Every appliance installed before this config existed has none, and gunnar
//! must start on it. So the absent case is not an error and not a silence: it
//! is [`Load::Absent`] carrying **the sentence that says why**, which travels
//! all the way to the admin's screen.
//!
//! # Authenticity — and the third state
//!
//! [`verify`] is `korp_installer::layout::verify`, arm for arm:
//!
//! * a first line that is not this format and version, or a body whose blake3
//! does not match what is written beside it ⇒ `Err`. A config that does not
//! hold together is not read for its values, and the caller treats it as
//! **absent rather than trusted** — this file says where the forge's data
//! lives, so forging it redirects the forge's storage.
//! * a signature present AND an enrolled key present ⇒ it must verify, or
//! `Err`. **A failed verify is never downgraded to integrity-only**; that
//! downgrade is how an attacker turns "signed" into "either will do".
//! * `auth.signature=` EMPTY, or no enrolled key ⇒
//! [`Authenticity::IntegrityOnly`].
//!
//! That third state is the one that matters today. **No `KORP_ROOT_SIGNING_KEY`
//! is enrolled on this estate**, so every config built today carries an empty
//! signature and says so in its own bytes (`auth.note`). An empty signature is
//! not a failed signature and not a passed one — it is *nobody signed this*.
//! The installer's console prints `INTEGRITY ONLY`; [`Authenticity::word`]
//! prints the same two words, and nothing anywhere calls it verified.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
/// `korp_installer::layout::CONFIG_ESP_PATH`, as the full path on a running
/// appliance: the ESP is mounted read-only at `/boot`.
pub const DEFAULT_PATH: &str = "/boot/korp/layout.conf";
/// The boot parameter that names the config, when it is not at the default.
/// Rides the same channel `GUNNAR_DATA_ROOT` already rides (Linux turns an
/// unrecognised `key=value` boot parameter into an environment variable for
/// init).
pub const CONFIG_ENV: &str = "GUNNAR_LAYOUT_CONFIG";
/// The enrolled ed25519 root public key, 64 hex characters — the key an
/// `auth.signature` must verify against.
///
/// **Unset on this estate.** With no key enrolled, a signature that is present
/// cannot be checked and the answer is [`Authenticity::IntegrityOnly`], never
/// "verified".
pub const ROOT_KEY_ENV: &str = "GUNNAR_LAYOUT_ROOT_KEY";
/// The first token of the first line.
pub const HEADER: &str = "korp-layout-config";
/// The only version this build reads.
pub const VERSION: u32 = 1;
/// `korp_installer::layout::CONFIG_DOMAIN` — the domain tag the signature
/// covers, so a signature over this config can never be replayed as a
/// signature over anything else the constellation signs.
pub const DOMAIN: &[u8] = b"korp.layout-config.v1\0";
/// Where the authenticity block begins. The body is everything before it, and
/// the body is what the digest and the signature are over.
const AUTH_MARKER: &str = "auth.domain=";
/// What is known about the config's authenticity.
///
/// Three states, and the third is the point: an EMPTY `auth.signature` is not
/// a failed signature and not a passed one. A failure is not in this enum at
/// all — it is an `Err` out of [`verify`], which the caller turns into an
/// absent config.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Authenticity {
/// An ed25519 signature over the domain-separated body digest verified
/// against the enrolled root key.
Verified,
/// The body's blake3 matches what is written beside it, and **nothing else
/// is claimed**. Integrity, not authenticity: anyone who can write the
/// plaintext ESP can rewrite the body and its digest together. This is the
/// state every appliance built today is in.
IntegrityOnly,
}
impl Authenticity {
/// The words the console prints — **the same words the installer's console
/// prints**, so an admin comparing the two is not left wondering whether
/// they mean the same thing.
pub fn word(self) -> &'static str {
match self {
Authenticity::Verified => "verified",
Authenticity::IntegrityOnly => "INTEGRITY ONLY",
}
}
/// Only [`Authenticity::Verified`]. An unsigned config is never presented
/// as verified.
pub fn is_verified(self) -> bool {
matches!(self, Authenticity::Verified)
}
}
/// The parsed config: the version, where it came from, how authentic it is,
/// and every `key=value` in it, verbatim.
#[derive(Clone, Debug)]
pub struct LayoutConfig {
path: PathBuf,
version: u32,
authenticity: Authenticity,
/// The `auth.note` the installer wrote — the sentence saying, in the
/// file's own bytes, what is NOT verified. Empty on a signed config.
note: String,
fields: BTreeMap<String, String>,
}
impl LayoutConfig {
pub fn path(&self) -> &Path {
&self.path
}
pub fn version(&self) -> u32 {
self.version
}
pub fn authenticity(&self) -> Authenticity {
self.authenticity
}
/// The installer's own sentence about what is not verified, or empty.
pub fn note(&self) -> &str {
&self.note
}
/// One value, exactly as written. `None` is "the config does not carry
/// this key" and is never a zero, an empty string or a guess.
pub fn get(&self, key: &str) -> Option<&str> {
self.fields
.get(key)
.map(String::as_str)
.filter(|v| !v.is_empty())
}
/// One value as a number, or `None` — including for a value that does not
/// parse, because a number that could not be read must not be reported as
/// one.
pub fn u64(&self, key: &str) -> Option<u64> {
self.get(key).and_then(|v| v.parse().ok())
}
/// A `yes`/`no` value. Anything else, or absent, is `None`.
pub fn yes_no(&self, key: &str) -> Option<bool> {
match self.get(key)? {
"yes" => Some(true),
"no" => Some(false),
_ => None,
}
}
/// `<prefix>.count`, or 0.
pub fn count(&self, prefix: &str) -> usize {
self.u64(&format!("{prefix}.count")).unwrap_or(0) as usize
}
/// Every field, in key order — what the admin view renders.
pub fn fields(&self) -> impl Iterator<Item = (&str, &str)> {
self.fields.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
/// **`dir.<name>.path` — the parameter gunnar obeys.**
pub fn dir_path(&self, name: &str) -> Option<PathBuf> {
self.get(&format!("dir.{name}.path")).map(PathBuf::from)
}
/// `dir.<name>.off_stripe_required`.
pub fn dir_requires_off_stripe(&self, name: &str) -> Option<bool> {
self.yes_no(&format!("dir.{name}.off_stripe_required"))
}
/// `dir.<name>.off_stripe_satisfied` — **the installer's verdict**, not
/// gunnar's re-derivation of it.
pub fn dir_off_stripe_satisfied(&self, name: &str) -> Option<bool> {
self.yes_no(&format!("dir.{name}.off_stripe_satisfied"))
}
/// Parse a SEALED config's bytes. `Err` is the sentence that says why not.
///
/// Parsing does not verify; [`load`] verifies first and parses only what
/// held together.
pub fn parse(path: PathBuf, text: &str, authenticity: Authenticity) -> Result<Self, String> {
let mut lines = text.lines();
let header = lines.next().unwrap_or_default().trim();
let mut tokens = header.split_whitespace();
if tokens.next() != Some(HEADER) {
return Err(format!(
"{} does not begin with `{HEADER}`; it is not a layout config",
path.display()
));
}
let version: u32 = tokens
.next()
.and_then(|v| v.parse().ok())
.ok_or_else(|| format!("{} carries no format version", path.display()))?;
if version != VERSION {
return Err(format!(
"{} is format version {version}; this build reads version {VERSION} only",
path.display()
));
}
let mut fields = BTreeMap::new();
for line in lines {
let line = line.trim_end_matches('\r');
if line.trim().is_empty() || line.starts_with('#') {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
fields.insert(key.to_owned(), unescape(value));
}
let note = fields.get("auth.note").cloned().unwrap_or_default();
Ok(LayoutConfig {
path,
version,
authenticity,
note,
fields,
})
}
}
/// The writer escapes `\n` and `\\`; unescape in one pass so a trailing
/// backslash cannot swallow the next character.
fn unescape(value: &str) -> String {
let mut out = String::with_capacity(value.len());
let mut chars = value.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('\\') => out.push('\\'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
/// The answer to "is there a layout config?", and never anything in between.
#[derive(Clone, Debug)]
pub enum Load {
Found(Box<LayoutConfig>),
/// **The sentence that says why there is none.** It reaches the admin's
/// screen unchanged: "the config is missing" is an answer, and inferring
/// the installer's decisions from the current mount table is not.
Absent(String),
}
impl Load {
pub fn config(&self) -> Option<&LayoutConfig> {
match self {
Load::Found(c) => Some(c),
Load::Absent(_) => None,
}
}
pub fn absent_reason(&self) -> Option<&str> {
match self {
Load::Absent(why) => Some(why),
Load::Found(_) => None,
}
}
}
/// Where the config lives: [`CONFIG_ENV`], else [`DEFAULT_PATH`]. **One place,
/// not a search** — a config found by looking around is a config an attacker
/// can put somewhere.
pub fn config_path() -> PathBuf {
std::env::var_os(CONFIG_ENV)
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(DEFAULT_PATH))
}
/// Read, verify, and parse the layout config.
///
/// Never an error and never a panic: an old appliance has none and must still
/// start.
pub fn load() -> Load {
load_from(&config_path(), enrolled_root_key().as_ref())
}
/// [`load`] with the path and the enrolled key named — the seam the tests
/// drive, so none of them touches a process-global.
pub fn load_from(path: &Path, key: Option<&[u8; 32]>) -> Load {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Load::Absent(format!(
"no layout config at {}. An appliance installed before the config existed has \
none, and nothing here infers the installer's decisions from the current \
machine",
path.display()
))
}
Err(err) => return Load::Absent(format!("{} could not be read: {err}", path.display())),
};
// **Verify BEFORE reading a single value.** A config that does not hold
// together is treated as absent rather than trusted: it says where the
// forge's data lives.
let authenticity = match verify(&text, key) {
Ok(a) => a,
Err(why) => {
return Load::Absent(format!(
"{} is present but does not verify ({why}); it is treated as absent rather \
than trusted",
path.display()
))
}
};
match LayoutConfig::parse(path.to_path_buf(), &text, authenticity) {
Ok(config) => Load::Found(Box::new(config)),
Err(why) => Load::Absent(why),
}
}
/// [`ROOT_KEY_ENV`] as 32 raw bytes, or `None`.
///
/// A malformed value is `None` and not a refusal: the honest consequence is
/// that a present signature cannot be checked, which is
/// [`Authenticity::IntegrityOnly`] — never "verified".
pub fn enrolled_root_key() -> Option<[u8; 32]> {
let spec = std::env::var(ROOT_KEY_ENV).ok()?;
let spec = spec.trim();
if spec.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(spec.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
}
/// **Verify a sealed config** — `korp_installer::layout::verify`, arm for arm.
///
/// See this module's header for the three outcomes and why the middle one may
/// never be reached by downgrading the last.
pub fn verify(sealed: &str, pubkey: Option<&[u8; 32]>) -> Result<Authenticity, String> {
let header = sealed.lines().next().unwrap_or_default();
let want = format!("{HEADER} {VERSION}");
if header != want {
return Err(format!(
"first line is {header:?}, not {want:?} — refusing to read a shape this version \
does not know"
));
}
let (body, auth) = sealed
.split_once(AUTH_MARKER)
.ok_or_else(|| "no auth block — it was never sealed".to_string())?;
let field =
|k: &str| -> Option<&str> { auth.lines().find_map(|l| l.strip_prefix(&format!("{k}="))) };
let want_hash = field("auth.blake3").ok_or_else(|| "no auth.blake3".to_string())?;
let got = blake3::hash(body.as_bytes()).to_hex().to_string();
if got != want_hash {
return Err(format!(
"body blake3 is {got}, the file claims {want_hash} — it has been altered since it \
was written"
));
}
let sig_hex = field("auth.signature").unwrap_or_default();
// **The third state.** Nobody signed it: not a failure, not a pass.
if sig_hex.is_empty() {
return Ok(Authenticity::IntegrityOnly);
}
// A signature nobody can check is not evidence of anything either. It is
// still integrity-only, and it is never called verified.
let Some(key) = pubkey else {
return Ok(Authenticity::IntegrityOnly);
};
let sig_bytes: [u8; 64] = decode_hex(sig_hex)
.and_then(|v| v.try_into().ok())
.ok_or_else(|| "auth.signature is not 64 hex bytes".to_string())?;
let vk = ed25519_dalek::VerifyingKey::from_bytes(key)
.map_err(|err| format!("the enrolled pubkey is malformed: {err}"))?;
// The same refusal the installer makes: a small-order key verifies a
// forged signature over ANY config, so it is refused rather than trusted.
if vk.is_weak() {
return Err(
"the enrolled pubkey is a small-order point — such a key verifies a forged \
signature over ANY config, so it is refused rather than trusted"
.to_string(),
);
}
use ed25519_dalek::Verifier as _;
vk.verify(
&signing_message(body),
&ed25519_dalek::Signature::from_bytes(&sig_bytes),
)
.map_err(|_| {
"the signature does not verify against the enrolled root key — the config is NOT the \
one the installer signed"
.to_string()
})?;
Ok(Authenticity::Verified)
}
/// The bytes an ed25519 signature covers: the domain tag, then the body's
/// blake3. Fixed width, one interpretation, and the tag is what stops a
/// signature over this being replayed as a signature over something else.
pub fn signing_message(body: &str) -> Vec<u8> {
let mut m = Vec::with_capacity(DOMAIN.len() + 32);
m.extend_from_slice(DOMAIN);
m.extend_from_slice(blake3::hash(body.as_bytes()).as_bytes());
m
}
fn decode_hex(s: &str) -> Option<Vec<u8>> {
if s.len() % 2 != 0 {
return None;
}
(0..s.len() / 2)
.map(|i| u8::from_str_radix(s.get(i * 2..i * 2 + 2)?, 16).ok())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
const BODY: &str = concat!(
"korp-layout-config 1\n",
"install.unix=1789000000\n",
"install.product=gunnar\n",
"install.mode=fresh\n",
"boot.dev=/dev/vda\n",
"boot.why=largest-group-remainder\n",
"boot.p3_dev=/dev/vda3\n",
"data.mountpoint=/data\n",
"data.shape=raid0\n",
"data.striped=yes\n",
"data.count=4\n",
"data.0.dev=/dev/vdb\n",
"data.3.dev=/dev/vde\n",
"serialdat.mountpoint=/serialdat\n",
"serialdat.class=p3\n",
"serialdat.dev=/dev/vda3\n",
"serialdat.off_stripe=yes\n",
"unused.count=1\n",
"unused.0.name=vdf\n",
"unused.0.why=unequal, not used\n",
"dir.users_dir=repos\n",
"dir.count=8\n",
"dir.repos.path=/data/repos\n",
"dir.repos.device=data\n",
"dir.repos.off_stripe_required=no\n",
"dir.repos.off_stripe_satisfied=yes\n",
"dir.meta.path=/serialdat/meta\n",
"dir.meta.device=serialdat\n",
"dir.meta.off_stripe_required=yes\n",
"dir.meta.off_stripe_satisfied=yes\n",
"dir.log.path=/data/log\n",
"dir.log.device=serialdat\n",
"dir.log.off_stripe_required=yes\n",
"dir.log.off_stripe_satisfied=yes\n",
"threshold.rootfs_measured_bytes=12163481\n",
);
/// Seal exactly as `korp_installer::layout::seal` does with no key: an
/// empty signature and the note that says so.
fn seal_unsigned(body: &str) -> String {
format!(
"{body}auth.domain=korp.layout-config.v1\nauth.blake3={}\nauth.signature=\n\
auth.pubkey=\nauth.note=NOT SIGNED: no ed25519 root key was enrolled at build \
time (KORP_ROOT_SIGNING_KEY unset)\n",
blake3::hash(body.as_bytes()).to_hex()
)
}
fn seal_signed(body: &str, seed: &[u8; 32]) -> (String, [u8; 32]) {
use ed25519_dalek::Signer;
let sk = ed25519_dalek::SigningKey::from_bytes(seed);
let sig = sk.sign(&signing_message(body));
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
(
format!(
"{body}auth.domain=korp.layout-config.v1\nauth.blake3={}\nauth.signature={}\n\
auth.pubkey={}\nauth.note=\n",
blake3::hash(body.as_bytes()).to_hex(),
hex(&sig.to_bytes()),
hex(sk.verifying_key().as_bytes())
),
sk.verifying_key().to_bytes(),
)
}
// ── the format ─────────────────────────────────────────────────────────
#[test]
fn the_config_parses_into_its_keys() {
let c = LayoutConfig::parse(
PathBuf::from(DEFAULT_PATH),
&seal_unsigned(BODY),
Authenticity::IntegrityOnly,
)
.expect("the sample parses");
assert_eq!(c.version(), 1);
assert_eq!(c.get("boot.why"), Some("largest-group-remainder"));
assert_eq!(c.count("data"), 4);
assert_eq!(c.get("data.3.dev"), Some("/dev/vde"));
assert_eq!(c.get("unused.0.why"), Some("unequal, not used"));
assert_eq!(c.u64("threshold.rootfs_measured_bytes"), Some(12_163_481));
}
/// The parameters gunnar obeys, read as parameters.
#[test]
fn the_dir_parameters_are_what_the_app_is_told() {
let c = LayoutConfig::parse(
PathBuf::from(DEFAULT_PATH),
&seal_unsigned(BODY),
Authenticity::IntegrityOnly,
)
.expect("parses");
// The consumer's path stays under /data — it is a SYMLINK onto the
// other spindle, which is exactly why nothing in gunnar has to build a
// second placement mechanism.
assert_eq!(c.dir_path("log"), Some(PathBuf::from("/data/log")));
// The metadata mirror is a second copy and has no /data twin.
assert_eq!(c.dir_path("meta"), Some(PathBuf::from("/serialdat/meta")));
assert_eq!(c.dir_path("repos"), Some(PathBuf::from("/data/repos")));
assert_eq!(c.dir_requires_off_stripe("log"), Some(true));
assert_eq!(c.dir_requires_off_stripe("repos"), Some(false));
assert_eq!(c.dir_off_stripe_satisfied("log"), Some(true));
assert_eq!(c.dir_path("nothing-like-this"), None);
}
/// **Never report a number you did not read.**
#[test]
fn an_absent_key_is_none_and_never_a_zero() {
let c = LayoutConfig::parse(
PathBuf::from("/x"),
&seal_unsigned(BODY),
Authenticity::IntegrityOnly,
)
.expect("parses");
assert_eq!(c.get("data.bytes"), None);
assert_eq!(c.u64("data.bytes"), None);
assert_eq!(
c.u64("boot.why"),
None,
"a value that is not a number is not one"
);
assert_eq!(c.yes_no("boot.why"), None);
assert_eq!(c.count("nothing"), 0);
}
/// Unknown KEYS are kept; an unknown VERSION is refused.
#[test]
fn version_1_may_gain_keys_but_never_change_version_silently() {
let grown = format!("{BODY}future.key=from a later installer\n");
let c = LayoutConfig::parse(
PathBuf::from("/x"),
&seal_unsigned(&grown),
Authenticity::IntegrityOnly,
)
.expect("parses");
assert_eq!(c.get("future.key"), Some("from a later installer"));
let err = LayoutConfig::parse(
PathBuf::from("/x"),
"korp-layout-config 2\nboot.dev=/dev/vda\n",
Authenticity::IntegrityOnly,
)
.expect_err("version 2 must be refused");
assert!(err.contains("version 2"), "{err}");
}
#[test]
fn escaped_values_come_back_whole() {
assert_eq!(
unescape("two\\nlines and a \\\\ backslash"),
"two\nlines and a \\ backslash"
);
}
// ── authenticity: three states, and a failure is not one of them ───────
/// ★ **Today's reality on every appliance.** `KORP_ROOT_SIGNING_KEY` is not
/// enrolled, so the signature is empty — *nobody signed this* — and the
/// words are the installer's own: `INTEGRITY ONLY`.
#[test]
fn an_empty_signature_is_integrity_only_and_never_verified() {
let a = verify(&seal_unsigned(BODY), None).expect("an unsigned config still holds");
assert_eq!(a, Authenticity::IntegrityOnly);
assert!(!a.is_verified());
assert_eq!(a.word(), "INTEGRITY ONLY");
}
/// And it stays integrity-only even where a key IS enrolled: an empty
/// signature is the absence of a claim, not a broken one.
#[test]
fn an_empty_signature_with_a_key_enrolled_is_still_integrity_only() {
let (_, key) = seal_signed(BODY, &[7u8; 32]);
assert_eq!(
verify(&seal_unsigned(BODY), Some(&key)),
Ok(Authenticity::IntegrityOnly)
);
}
#[test]
fn a_real_signature_against_the_enrolled_key_verifies() {
let (sealed, key) = seal_signed(BODY, &[9u8; 32]);
let a = verify(&sealed, Some(&key)).expect("it must verify");
assert_eq!(a, Authenticity::Verified);
assert_eq!(a.word(), "verified");
}
/// ★ **A failed verify is NEVER downgraded to integrity-only.** That
/// downgrade is how an attacker turns "signed" into "either will do".
#[test]
fn a_signature_that_does_not_verify_is_an_error_not_a_downgrade() {
let (sealed, _) = seal_signed(BODY, &[9u8; 32]);
let (_, other_key) = seal_signed(BODY, &[11u8; 32]);
let err = verify(&sealed, Some(&other_key)).expect_err("it must not pass");
assert!(err.contains("does not verify"), "{err}");
}
/// A body edited after sealing does not hold together, and is not read for
/// its values at all.
#[test]
fn an_altered_body_is_refused_by_name() {
let sealed = seal_unsigned(BODY).replace("/dev/vdb", "/dev/sdz");
let err = verify(&sealed, None).expect_err("the digest must catch it");
assert!(err.contains("altered since it was written"), "{err}");
}
#[test]
fn something_that_was_never_sealed_is_refused() {
let err = verify(BODY, None).expect_err("no auth block");
assert!(err.contains("never sealed"), "{err}");
}
// ── loading ────────────────────────────────────────────────────────────
#[test]
fn an_absent_config_names_where_it_looked_and_is_not_an_error() {
let dir = tempfile::tempdir().expect("a temp dir");
let load = load_from(&dir.path().join("layout.conf"), None);
let why = load.absent_reason().expect("absent");
assert!(why.contains("no layout config at"), "{why}");
assert!(why.contains("layout.conf"), "{why}");
assert!(load.config().is_none());
}
#[test]
fn a_sealed_config_on_disk_loads_as_integrity_only() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("layout.conf");
std::fs::write(&path, seal_unsigned(BODY)).expect("write");
let load = load_from(&path, None);
let c = load.config().expect("found");
assert_eq!(c.authenticity(), Authenticity::IntegrityOnly);
assert!(!c.authenticity().is_verified());
assert!(c.note().contains("NOT SIGNED"), "{}", c.note());
assert_eq!(c.dir_path("meta"), Some(PathBuf::from("/serialdat/meta")));
}
/// **Tampered is treated as ABSENT, not as trusted** — and the sentence
/// says which, because "there is no config" and "the config on this box
/// has been altered" send an admin to two different places.
#[test]
fn a_tampered_config_is_absent_rather_than_trusted() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("layout.conf");
std::fs::write(&path, seal_unsigned(BODY).replace("/dev/vdb", "/dev/sdz")).expect("write");
let load = load_from(&path, None);
assert!(
load.config().is_none(),
"not one value may be read out of it"
);
let why = load.absent_reason().expect("absent");
assert!(why.contains("does not verify"), "{why}");
assert!(why.contains("altered"), "{why}");
assert!(why.contains("rather than trusted"), "{why}");
}
}