bastion agent 0.1.14

This commit is contained in:
2026-09-26 18:31:40 +00:00
commit 3f1d48bf9b
65 changed files with 15548 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
"""Unit tests for the seal stage. Run: python3 -m unittest discover .
Round trips use a real age identity generated with age-keygen when the
binaries are on PATH (they are on the server and in CI); the envelope
tests are skipped where age is absent, the fail-closed tests are not.
"""
import io
import json
import os
import shutil
import subprocess
import tarfile
import tempfile
import unittest
from unittest import mock
from . import seal as sl
from .catalog import catalog
from .extract import extract
from .manifest import validate_document
from .sanitize import sanitize
from .walk import walk
HAVE_AGE = bool(shutil.which("age") and shutil.which("age-keygen"))
def manifest(root, seal_to=(), stages=None):
doc = {
"freeagent": 1, "id": "t", "owner": {"host": "h"},
"scope": {"roots": [root]},
"mask": ["read", "search", "index"],
"stages": stages or ["walk", "extract", "sanitize", "catalog", "seal"], "sweep": "manual",
"sanitize": {"mode": "flag"},
"sink": {"catalog": "host", "vectors": "local", "plaintext": "never", "seal_to": list(seal_to)},
}
return validate_document(doc)
def keygen(dirpath):
ident = os.path.join(dirpath, "key.txt")
out = subprocess.run(["age-keygen", "-o", ident], capture_output=True, text=True, check=True)
pub = [l for l in (out.stderr + out.stdout).splitlines() if "age1" in l][-1].split()[-1]
return ident, pub
class Case(unittest.TestCase):
def setUp(self):
self.root = tempfile.mkdtemp(prefix="fa-seal-")
self.out = tempfile.mkdtemp(prefix="fa-sealout-")
with open(os.path.join(self.root, "note.txt"), "w", encoding="utf-8") as fh:
fh.write("Marisol's email is m@example.com and card 4111 1111 1111 1111.\n")
m = manifest(self.root)
walk(m, self.out)
extract(m, self.out, use_external=False)
sanitize(m, self.out, env={})
catalog(m, self.out, env={})
def tearDown(self):
shutil.rmtree(self.root, ignore_errors=True)
shutil.rmtree(self.out, ignore_errors=True)
class FailClosedTests(Case):
def test_no_owner_recipient(self):
# age is faked present: seal checks for it first, and this test is
# about the recipient refusal, whether or not age is installed
with mock.patch.object(sl, "tool_path", lambda n: "/usr/bin/" + n):
with self.assertRaises(sl.SealError) as cm:
sl.seal(manifest(self.root), self.out, env={})
self.assertIn("FREEAGENT_OWNER_RECIPIENT", str(cm.exception))
self.assertFalse(os.path.exists(os.path.join(self.out, "sealed")))
def test_bad_recipient_strings(self):
env = {"FREEAGENT_OWNER_RECIPIENT": "not-a-key"}
with mock.patch.object(sl, "tool_path", lambda n: "/usr/bin/" + n):
with self.assertRaises(sl.SealError) as cm:
sl.seal(manifest(self.root), self.out, env=env)
self.assertIn("not an age recipient", str(cm.exception))
env = {"FREEAGENT_OWNER_RECIPIENT": "age1" + "q" * 58}
with self.assertRaises(sl.SealError) as cm:
sl.recipients_for(manifest(self.root, seal_to=["partner-key"]), env)
self.assertIn("partner-key", str(cm.exception))
def test_no_age_on_path(self):
env = {"FREEAGENT_OWNER_RECIPIENT": "age1" + "q" * 58}
with mock.patch.object(sl, "tool_path", lambda n: None):
with self.assertRaises(sl.SealError) as cm:
sl.seal(manifest(self.root), self.out, env=env)
self.assertIn("age", str(cm.exception))
def test_seal_not_in_stages(self):
# (a `catalog: host` manifest cannot even omit seal — the schema refuses it)
m = validate_document({
"freeagent": 1, "id": "t", "owner": {"host": "h"}, "scope": {"roots": [self.root]},
"mask": ["read", "index"], "stages": ["walk", "extract", "sanitize", "catalog"],
"sweep": "manual", "sanitize": {"mode": "flag"},
"sink": {"catalog": "local", "vectors": "local", "plaintext": "never"}})
with self.assertRaises(sl.SealError):
sl.seal(m, self.out, env={"FREEAGENT_OWNER_RECIPIENT": "age1" + "q" * 58})
def test_bundle_allowlist(self):
plain, inv = sl.build_bundle(manifest(self.root), self.out, "20260828T000000Z")
with tarfile.open(fileobj=io.BytesIO(plain)) as tar:
names = tar.getnames()
self.assertIn("catalog.jsonl", names)
self.assertIn("catalog.dirs.jsonl", names)
self.assertIn("manifest.json", names)
self.assertIn("seal.manifest.json", names)
self.assertIn("walk.summary.json", names)
self.assertIn("sanitize.summary.json", names)
for forbidden in ("walk.jsonl", "extract.jsonl", "sanitize.jsonl", "vectors.db", "vault.db"):
self.assertFalse(any(n.endswith(forbidden) for n in names), forbidden)
self.assertFalse(any(n.startswith(("extract/", "sanitize/")) for n in names))
# nothing in the bundle is a value
self.assertNotIn(b"m@example.com", plain)
self.assertNotIn(b"4111", plain)
self.assertIn(b"[PII_EMAIL_1]", plain) # sanitized head is there, as designed
self.assertEqual(set(inv["members"]), set(names))
def test_bundle_reproducible(self):
a, _ = sl.build_bundle(manifest(self.root), self.out, "20260828T000000Z")
b, _ = sl.build_bundle(manifest(self.root), self.out, "20260828T000000Z")
self.assertEqual(a, b)
@unittest.skipUnless(HAVE_AGE, "age / age-keygen not on PATH")
class RoundTripTests(Case):
def test_seal_and_unseal(self):
ident, pub = keygen(self.out)
other_ident, other_pub = keygen(tempfile.mkdtemp(prefix="fa-k2-"))
env = {"FREEAGENT_OWNER_RECIPIENT": pub}
s = sl.seal(manifest(self.root, seal_to=[other_pub]), self.out, env=env, now="20260828T000000Z")
sealed = os.path.join(self.out, s["file"])
self.assertTrue(os.path.exists(sealed))
self.assertEqual(s["recipients"], [pub, other_pub])
self.assertGreater(s["bytes_sealed"], 0)
self.assertIsNone(s["signature"])
with open(sealed, "rb") as fh:
blob = fh.read()
self.assertTrue(blob.startswith(b"age-encryption.org/v1"))
self.assertNotIn(b"PII_EMAIL", blob) # sealed: the catalog text is not visible
with open(os.path.join(self.out, "sealed", "latest.json")) as fh:
self.assertEqual(json.load(fh)["file"], s["file"])
# both recipients can open it; a stranger cannot
for who in (ident, other_ident):
dest = tempfile.mkdtemp(prefix="fa-un-")
inv = sl.unseal(sealed, who, dest)
self.assertEqual(inv["manifest_id"], "t")
with open(os.path.join(dest, "catalog.jsonl"), encoding="utf-8") as fh:
self.assertIn("[PII_EMAIL_1]", fh.read())
shutil.rmtree(dest, ignore_errors=True)
stranger, _ = keygen(tempfile.mkdtemp(prefix="fa-k3-"))
with self.assertRaises(sl.SealError):
sl.unseal(sealed, stranger, tempfile.mkdtemp(prefix="fa-un-"))
def test_owner_recipient_deduped_and_signature(self):
ident, pub = keygen(self.out)
key = os.path.join(self.out, "sign.pem")
subprocess.run(["openssl", "ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out", key],
check=True, capture_output=True)
env = {"FREEAGENT_OWNER_RECIPIENT": pub, "SEAL_SIGN_KEY": key}
s = sl.seal(manifest(self.root, seal_to=[pub]), self.out, env=env)
self.assertEqual(s["recipients"], [pub])
sig = os.path.join(self.out, "sealed", s["signature"])
self.assertTrue(os.path.exists(sig))
# the signature verifies against the public half
pubkey = os.path.join(self.out, "sign.pub")
subprocess.run(["openssl", "ec", "-in", key, "-pubout", "-out", pubkey],
check=True, capture_output=True)
v = subprocess.run(["openssl", "dgst", "-sha256", "-verify", pubkey, "-signature", sig,
os.path.join(self.out, s["file"])], capture_output=True, text=True)
self.assertEqual(v.returncode, 0, v.stderr)
def test_tampered_member_refused(self):
ident, pub = keygen(self.out)
env = {"FREEAGENT_OWNER_RECIPIENT": pub}
s = sl.seal(manifest(self.root), self.out, env=env)
# rebuild a bundle with a modified catalog but the old inventory, seal it, and unseal
plain, _ = sl.build_bundle(manifest(self.root), self.out, "x")
buf = io.BytesIO()
with tarfile.open(fileobj=io.BytesIO(plain)) as src, tarfile.open(fileobj=buf, mode="w") as dst:
for m in src.getmembers():
data = src.extractfile(m).read()
if m.name == "catalog.jsonl":
data = data + b"\n{\"forged\": true}\n"
m.size = len(data)
dst.addfile(m, io.BytesIO(data))
bad = os.path.join(self.out, "bad.tar.age")
subprocess.run(["age", "-r", pub, "-o", bad], input=buf.getvalue(), check=True)
with self.assertRaises(sl.SealError) as cm:
sl.unseal(bad, ident, tempfile.mkdtemp(prefix="fa-un-"))
self.assertIn("does not match the inventory", str(cm.exception))
if __name__ == "__main__":
unittest.main()