Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

xtask/src/key_census.rs

//! `cargo xtask key-census`: which tracked files hold a private key.
//!
//! # The finding, and what this is and is not
//!
//! `assets/test/vetra-test-pki/` holds three throwaway private keys, committed
//! in the clear and documented at length as such — *"a test fixture nobody has
//! to fetch from a secret store, and a key nobody can mistake for a real one"*.
//! The argument is sound and the keys really are worthless: nothing in this
//! repository references them (`git grep` over `crates/`, `xtask/`,
//! `installtest/` finds the directory named only by its own generator), and the
//! generator beside them reproduces the whole chain with one command.
//!
//! The repository went public on 2026-09-08, and a published private key trips
//! every scanner ever pointed at a git remote whether or not it is worthless.
//! So this does not delete them — that is a call for whoever knows which
//! sibling repositories consume the fixture, and the review's own note says to
//! regenerate rather than rewrite history. What it does is make the exception
//! **exactly three files wide**: a fourth private key anywhere in the tracked
//! tree fails the build, and it fails with the sentence explaining why the
//! three are allowed and it is not.
//!
//! # Base64 is checked too, and that is the load-bearing part
//!
//! Every artefact in that directory ships twice — PEM and `.pem.b64` — and the
//! `.b64` copies carry the same private keys past any scan that greps for
//! `BEGIN PRIVATE KEY`. A census that only read the plain form would report
//! three keys where the tree holds six, and would have said "clean" about a
//! directory whose `.b64` half nobody had looked at. So the scan decodes the
//! prefix of every candidate too.

use std::path::{Path, PathBuf};
use std::process::Command;

/// The PEM headers that open a private key, in any of the shapes that reach a
/// repository. Matched as a prefix so `BEGIN RSA PRIVATE KEY`,
/// `BEGIN OPENSSH PRIVATE KEY` and the bare form are all one rule.
const PRIVATE_KEY_MARK: &str = "PRIVATE KEY-----";

/// **The known, argued exceptions**, by exact path.
///
/// By path and not by directory: "anything under `assets/test/`" would let a
/// fourth key join the three by being dropped in beside them, which is the one
/// thing this census exists to notice.
pub const ALLOWED: &[&str] = &[
    "assets/test/vetra-test-pki/root-ca-key.pem",
    "assets/test/vetra-test-pki/root-ca-key.pem.b64",
    "assets/test/vetra-test-pki/issuing-ca-key.pem",
    "assets/test/vetra-test-pki/issuing-ca-key.pem.b64",
    "assets/test/vetra-test-pki/signer-key.pem",
    "assets/test/vetra-test-pki/signer-key.pem.b64",
    // The encrypted-repository known-answer vectors (round 2, gunnar-crypt):
    // `keys.age` beside it is encrypted TO this throwaway key, so the KAT must
    // carry it fixed — a key minted per test could not decrypt the committed
    // vector. Referenced only by crates/gunnar-crypt/tests/vectors.rs.
    "crates/gunnar-crypt/tests/vectors/test_ed25519",
];

/// The sentence a new private key is refused with.
pub const WHY: &str = "\
A private key is tracked in a repository that is published. The three in \
`assets/test/vetra-test-pki/` are a documented exception: throwaway keys, \
referenced by no code here, reproducible with `cd assets/test/vetra-test-pki/\
generator && cargo run --release -- ../`. Nothing else may join them. If this \
key is a fixture, mint it in the test that needs it; if it is real, it does not \
belong in a git history at all — rotate it, because a git history is not a \
thing you can take something back out of.";

/// Every file git tracks, as repository-relative paths.
fn tracked(root: &Path) -> Result<Vec<String>, String> {
    let out = Command::new("git")
        .args(["ls-files", "-z"])
        .current_dir(root)
        .output()
        .map_err(|err| format!("running `git ls-files`: {err}"))?;
    if !out.status.success() {
        return Err(format!(
            "`git ls-files` failed: {}",
            String::from_utf8_lossy(&out.stderr)
        ));
    }
    Ok(String::from_utf8_lossy(&out.stdout)
        .split('\0')
        .filter(|path| !path.is_empty())
        .map(str::to_owned)
        .collect())
}

/// Does `bytes` open a PEM private key, plainly or base64-wrapped?
///
/// Only the head of the file is read on either side: a private key is what a
/// file BEGINS with, and scanning whole blobs would make this a content
/// scanner rather than a census.
pub fn holds_a_private_key(bytes: &[u8]) -> bool {
    let head = &bytes[..bytes.len().min(512)];
    if String::from_utf8_lossy(head).contains(PRIVATE_KEY_MARK) {
        return true;
    }
    // The `.b64` half. Decoded with the repository's own alphabet arithmetic
    // rather than a crate, because `xtask` has no dependencies and this needs
    // to read four characters at a time, not to be a base64 library.
    let decoded = decode_base64_prefix(head);
    String::from_utf8_lossy(&decoded).contains(PRIVATE_KEY_MARK)
}

/// Decode as much of `bytes` as is whole base64, stopping at the first
/// character that is not. Enough to see a PEM header through the wrapper.
fn decode_base64_prefix(bytes: &[u8]) -> Vec<u8> {
    let value = |b: u8| -> Option<u32> {
        match b {
            b'A'..=b'Z' => Some((b - b'A') as u32),
            b'a'..=b'z' => Some((b - b'a') as u32 + 26),
            b'0'..=b'9' => Some((b - b'0') as u32 + 52),
            b'+' => Some(62),
            b'/' => Some(63),
            _ => None,
        }
    };
    let mut out = Vec::new();
    let mut acc = 0u32;
    let mut bits = 0u32;
    for &b in bytes {
        if b.is_ascii_whitespace() {
            continue;
        }
        let Some(v) = value(b) else { break };
        acc = (acc << 6) | v;
        bits += 6;
        if bits >= 8 {
            bits -= 8;
            out.push((acc >> bits) as u8);
        }
    }
    out
}

/// Every tracked file that holds a private key, in `git ls-files` order.
pub fn census(root: &Path) -> Result<Vec<String>, String> {
    let mut out = Vec::new();
    for path in tracked(root)? {
        let Ok(bytes) = std::fs::read(root.join(&path)) else {
            // A tracked path that is not readable as a file — a submodule
            // gitlink, a symlink into nowhere — holds no key here.
            continue;
        };
        if holds_a_private_key(&bytes) {
            out.push(path);
        }
    }
    Ok(out)
}

/// The workspace root, from this crate's location.
pub fn workspace_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask has a parent")
        .to_path_buf()
}

/// Run the census and print the result. `true` on a pass.
pub fn run(root: &Path) -> bool {
    let found = match census(root) {
        Ok(found) => found,
        Err(why) => {
            eprintln!("xtask key-census: {why}");
            return false;
        }
    };
    let unexpected: Vec<&String> = found
        .iter()
        .filter(|path| !ALLOWED.contains(&path.as_str()))
        .collect();
    if unexpected.is_empty() {
        eprintln!(
            "key-census: {} tracked private key(s), all of them the documented \
             test-PKI exception",
            found.len()
        );
        return true;
    }
    for path in unexpected {
        eprintln!("key-census: {path}");
    }
    eprintln!("\n{WHY}");
    false
}