gunnar-server 3.0.0

The gunnar git server: SSH and smart-HTTP git transports and the gRPC control plane. The appliance half, extracted from gunnar-cli so the server code lives in its own crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
//! **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}");
    }
}