Skip to content

Security: smprather/engineering-loadout

Security

docs/SECURITY.md

Security & Malware Assurance

This project deploys into chip-design environments where TSMC infosec mandates no project backups -- ransomware or a wiper inside a bundled tool would be unrecoverable. Assurance is therefore treated as a first-class deliverable, with a per-package framework combining supply-chain provenance (pin + hash + scan everything that ships) and behavioral analysis (canary/strace detonation in a network-isolated container). The source of truth is the machine-checkable ledger under assurance/.

Every claim below is reproducible by running the named command.

1. Content integrity: .content-manifest

.content-manifest records the SHA-256 of every shipped payload file, as stored on disk (per .part-NNN chunk). It is generated by build/gen-content-manifest and regenerated automatically by strip-all-elf-binaries after any payload change.

  • loadout install verifies each payload archive against the manifest before extracting it (default-on; --no-verify escapes). A byte mismatch, or a payload file not listed in the manifest, aborts the install.
  • loadout doctor --verify re-hashes every manifest entry.
  • tests/content-verify and the content-manifest in sync check (build/gen-content-manifest --check) run in tests/run-all Tier 1.

Trust chain: the manifest lives in git; the release tag is SSH-signed (see §6), so tampering with a shipped payload requires the signing key, not just a file drop.

2. Malware scan: scan-for-malware

Two engines; the scan fails if either detects:

  • YARA-Forge full ruleset (vendored yara + payload/yara/), one --scan-list invocation over all files.
  • ClamAV (clamscan) when present; degrades to YARA-only with a warning if absent. Refresh signatures with freshclam on the build box.

./build/release runs it as a mandatory Step 0 in parallel with the other pre-release gates. The final tag/release step waits and aborts on detection.

Coverage is the whole shipped tree -- ~69,000 files: bin/lib64, runtime archives, wheels, typelibs, treesitter parsers, fonts, the tldr cache, the Windows payload, the crate store, portable-python, and the Neovim + tmux plugin bundles. (Historically only bin/lib64/runtime were effectively scanned; the crate store scanned a dead path and portable-python was skipped entirely -- both fixed.)

./build/scan-for-malware              # full scan (YARA + ClamAV)
./build/scan-for-malware --fast       # bin/lib64 only (dev tier)
./build/scan-for-malware --no-clamav  # YARA only
./build/scan-for-malware --no-cache   # force a fresh scan and do not write cache
./build/scan-for-malware --clear-cache # delete cached clean scan results first
./build/scan-for-malware --path DIR   # scan an arbitrary staging tree (pre-pack)

Clean-result cache. Clean scan results are cached under ${XDG_CACHE_HOME:-~/.cache}/engineering-loadout/malware-scan-v1/. Only clean verdicts are cached; detections are never cached. The cache key hashes the source scan corpus (compressed payloads plus vendored plugin trees, or the explicit --path tree), scan-for-malware itself, vendored YARA binary/rules, the YARA release tag, scan mode flags, and the ClamAV engine/signature fingerprint from clamscan --version. Any payload byte change, rule update, allowlist/script edit, or ClamAV signature update misses the cache and performs a full scan. --verbose bypasses the cache so raw YARA output is available.

False-positive allowlist. _YARA_ALLOWLIST in scan-for-malware excuses a short, audited set of (rule, path) pairs, each with a written justification; any other rule on the same file, or that rule on any other file, still fails. Currently one entry: Firefox's omni.ja (its own resource zip trips a structural APPX/MSIX heuristic). The offensive-security tldr pages (mimikatz/certutil/msfvenom) that used to trip keyword rules are not allowlisted -- ./build/update tldr-data prunes them from the bundled cache entirely (_TLDR_OFFENSIVE_GLOBS).

3. Plugin pinning (Neovim)

The Neovim plugin bundle is the largest third-party attack surface. It is now commit-pinned in envs/nvim/lazy-lock.json (24 plugins), and env-nvim ships that lockfile so :Lazy restore reproduces the exact vetted commits.

  • Install-time online plugin sync is default-off. --allow-online-plugin-sync opts in and runs Lazy! restore against the committed lockfile (pinned commits), never a bare sync to upstream HEAD.
  • build/build-nvim-plugins rebuilds the bundle from the lockfile in a clean staging directory (never the maintainer's live ~/.local/share/nvim/lazy), scans it with scan-for-malware --path, and packs deterministically.

The tmux plugin bundle is likewise commit-pinned, via envs/tmux/vendor/plugins.lock (4 plugins). ./build/update tmux-plugins reads that lock, clones each plugin and checks out the pinned commit, then rewrites the file with the commits it actually landed on. The plugin list is scraped from the set -g @plugin lines in envs/tmux/tmux.conf; commented-out entries are skipped, so a disabled plugin is neither cloned nor pinned.

4. Behavioral analysis: tests/dynamic-analysis

A tool is "detonated" in a mock chip-design HOME (canary .v/.lib/.sdc files + docs + a binary blob), wrapped in strace, inside the clean AlmaLinux 8.10 container under docker run --network none. The tool FAILS if it behaves like ransomware/a wiper: any canary modified, an entropy spike (encryption), a write/unlink/rename to project data outside the profile's allowlist, a real AF_INET/AF_INET6 network attempt (local AF_UNIX IPC is ignored), or a mass-unlink. Profiles live in assurance/profiles/<pkg>.toml.

tests/prebuilt-binaries-almalinux8 --dynamic   # container, network isolated
tests/run-all --container                       # includes --full and --dynamic

nvim (the pilot) passes with zero out-of-allowlist mutations and no network.

5. Upstream hash verification: build/verify-binaries

For tools with an official upstream binary release, downloads it, reproduces the bundling transform (strip -> patchelf RPATH), and SHA-256 compares against the bundled binary.

build/verify-binaries            # all
build/verify-binaries rg uv      # spot-check

Outcomes: PASS (byte match), PASS (patchelf layout delta) (identical NEEDED libs + near-identical size), SKIP (EL8 source build / no upstream binary), FAIL (differing deps or size -- investigate). Most tools (bash, rg, bat, jq, eza, fd, tmux, vim, gnuplot, nvim, etc.) are intentionally source-built on EL8, so they SKIP here and are covered by the scan + content manifest + per-package assurance record instead.

6. Supply-chain provenance

  • ./build/update hashes every download (_download_file) and appends date url sha256 to assurance/downloads.log (TOFU provenance); expected_sha256 can pin a download and abort on mismatch.
  • The nodejs bundle is fetched via nvm, which verifies against nodejs.org SHASUMS256.txt.

Release signing (active)

./build/release runs the malware scan, binary smoke, version table, and checksum generation as independent parallel gates, then signs the tag with git tag -s and attaches sha256sums.txt plus the .content-manifest asset. The trust chain (manifest in git -> signed tag) is tamper-evident. Verify with git tag -v <tag>; GitHub shows the tag as Verified. The notes carry an on-page SHA-256 table of the integrity-critical files (§1) for at-a-glance checking without a download.

EL8 signer requirement. git SSH signing shells out to ssh-keygen -Y sign, added in OpenSSH 8.2. Stock EL8 ships 8.0p1, whose ssh-keygen has no -Y subcommand -- git tag -s there dies with unknown option -- Y. The bundled openssh package (OpenSSH 10.4p1, ./loadout install openssh) provides a signer that works; it is also how the maintainer's own build box signs. It intentionally does not install bare ssh, scp, or sftp, because mainline OpenSSH cannot parse some Red Hat crypto-policy directives that stock RHEL OpenSSH accepts. Normal ssh stays host-integrated; use ssh10 only when an explicit 10.x client is needed. Configure signing once:

git config gpg.format ssh
git config user.signingkey ~/.ssh/<key>.pub
git config gpg.ssh.program ~/.local/bin/ssh-keygen   # the bundled 10.4p1, not stock 8.0p1

For one-off GitHub transport with the bundled client, use GIT_SSH_COMMAND=ssh10 git fetch or git config core.sshCommand ssh10.

A passphrase-protected key must be unlocked in an ssh-agent first (ssh-add ~/.ssh/<key>) -- otherwise ssh-keygen -Y sign falls back to a GUI askpass and fails on a headless box. ./build/release inherits SSH_AUTH_SOCK from its environment.

Alternative signer sources (documented for reproducibility): build from the official openssh/openssh-portable tag with build/build-openssh.sh (provenance preferred), or a third-party EL8 OpenSSH RPM (github.com/Nugent1a/OpenSSH-rpms/releases -- unvetted; using an unvetted RPM to build the signer itself is a provenance hole, so the source build is primary).

7. Assurance ledger

assurance/records/<pkg>.toml records, per package: source/build provenance, artifact SHA-256s, scan result, dynamic-analysis result, and package-specific pins:

  • nvim -- per-plugin commit pins (envs/nvim/lazy-lock.json).
  • rust-crate-store -- the full name version cksum closure in assurance/crate-store.lock. build/verify-crate-store re-hashes every one of the 2272 .crate files against the crates.io SHA-256 embedded in the store's own registry index (run in Tier 2 as --check-lock), so a tampered crate fails against both the store index and the signed-tag'd lock.
  • treesitter-parsers -- per-grammar url + revision + shipped-.so sha256 for 318 grammars in assurance/treesitter-parser-locks.json (build/gen-parser-locks, checked in Tier 1). Grammars are already commit-pinned by the vendored parsers.lua; this records the pin -> shipped bytes mapping.

tests/assurance-check re-hashes each record's artifacts against disk and validates these pins, so a record cannot silently drift from reality. nvim, rust, rust-crate-store, and treesitter-parsers are status = verified; coverage rolls out package-by-package.

There aren't any published security advisories