feat(warden): WARDEN-WP-0003 — test coverage, permissions, status --state-dir

- File permissions: os.chmod(cert, 0o600) after every sign in LocalCA and
  VaultCA; chmod(privkey, 0o600) and chmod(pubkey, 0o644) after generate_keypair
- Scorecard: add check_file_permissions() that flags world/group-readable
  cert and key files; run_scorecard now returns 6 checks
- warden status --state-dir: bypasses config loading entirely for operators
  who have a cert but no warden.yaml installed
- tests/test_vault.py: 11 VaultCA unit tests covering success, HTTP 403,
  RequestError, missing token, missing role, missing pubkey, TTL enforcement,
  eviction, signatures log, and cert mode 600
- tests/test_ca.py: generate_keypair tests (paths, args, overwrite, error,
  permissions) and cert mode 600 assertion after sign
- tests/test_scorecard.py: file_permissions check tests (pass, fail cert,
  fail keys dir); scorecard count updated to 6
- tests/test_cli.py: covers sign, issue, status, scorecard, inventory, log,
  cleanup commands using CliRunner and tmp config/inventory files
- tests/test_integration.py: @pytest.mark.integration tests against real
  ssh-keygen; excluded from default suite via pyproject addopts
- pyproject.toml: addopts = "-m 'not integration'", integration marker declared

All 100 unit tests pass; 3 integration tests pass; ruff clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 17:05:38 +02:00
parent cd1e385bc1
commit f3547acd0b
11 changed files with 974 additions and 55 deletions

View File

@@ -335,6 +335,28 @@ def test_local_ca_sign_evicts_existing_cert(tmp_path):
assert len(list(state.glob("agt-state-hub-bridge-cert.pub"))) == 1
def test_local_ca_sign_cert_mode_600(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
pubkey = tmp_path / "key.pub"
pubkey.write_text("ssh-ed25519 AAAA actor-key")
state = tmp_path / "state"
spec = CertSpec(
actor_name="agt-state-hub-bridge",
actor_type=ActorType.AGT,
pubkey_path=pubkey,
ttl_hours=24,
principals=["agt-task-bridge"],
identity="agt-state-hub-bridge",
)
with patch("warden.ca.subprocess.run", side_effect=_mock_run_factory(CERT_CONTENT)):
ca = LocalCA(ca_key, state)
record = ca.sign(spec)
assert oct(record.cert_path.stat().st_mode & 0o777) == oct(0o600)
def test_local_ca_sign_writes_signature_log(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
@@ -360,3 +382,102 @@ def test_local_ca_sign_writes_signature_log(tmp_path):
assert entry["actor"] == "agt-state-hub-bridge"
assert entry["backend"] == "local"
assert entry["ttl_hours"] == 24
# ---------------------------------------------------------------------------
# LocalCA.generate_keypair
# ---------------------------------------------------------------------------
def _mock_keygen_gen(cmd, **kwargs):
"""Mock for generate_keypair: writes privkey and pubkey based on -f arg."""
result = MagicMock()
result.returncode = 0
result.stdout = ""
result.stderr = ""
if "-f" in cmd:
idx = cmd.index("-f")
privkey = Path(cmd[idx + 1])
privkey.parent.mkdir(parents=True, exist_ok=True)
privkey.write_text("fake private key")
(privkey.parent / (privkey.name + ".pub")).write_text("ssh-ed25519 AAAA pubkey")
return result
def test_generate_keypair_returns_paths(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
ca = LocalCA(ca_key, tmp_path / "state")
with patch("warden.ca.subprocess.run", side_effect=_mock_keygen_gen):
privkey, pubkey = ca.generate_keypair("agt-test")
assert privkey.name == "agt-test_ed25519"
assert pubkey.name == "agt-test_ed25519.pub"
assert str(privkey).endswith("state/keys/agt-test_ed25519")
def test_generate_keypair_ed25519_no_passphrase(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
ca = LocalCA(ca_key, tmp_path / "state")
calls = []
def capturing_mock(cmd, **kwargs):
calls.append(cmd)
return _mock_keygen_gen(cmd, **kwargs)
with patch("warden.ca.subprocess.run", side_effect=capturing_mock):
ca.generate_keypair("agt-test")
assert len(calls) == 1
cmd = calls[0]
assert "-t" in cmd and cmd[cmd.index("-t") + 1] == "ed25519"
assert "-N" in cmd and cmd[cmd.index("-N") + 1] == ""
assert "-C" in cmd and cmd[cmd.index("-C") + 1] == "agt-test"
def test_generate_keypair_overwrites_existing(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
state = tmp_path / "state"
keys_dir = state / "keys"
keys_dir.mkdir(parents=True)
old_priv = keys_dir / "agt-test_ed25519"
old_pub = keys_dir / "agt-test_ed25519.pub"
old_priv.write_text("old key")
old_pub.write_text("old pubkey")
ca = LocalCA(ca_key, state)
with patch("warden.ca.subprocess.run", side_effect=_mock_keygen_gen):
privkey, pubkey = ca.generate_keypair("agt-test")
assert privkey.read_text() == "fake private key"
def test_generate_keypair_ca_error_on_failure(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
ca = LocalCA(ca_key, tmp_path / "state")
def fail_run(cmd, **kwargs):
result = MagicMock()
result.returncode = 1
result.stderr = "failed to generate key"
return result
with patch("warden.ca.subprocess.run", side_effect=fail_run):
with pytest.raises(CAError, match="Key generation failed"):
ca.generate_keypair("agt-test")
def test_generate_keypair_sets_permissions(tmp_path):
ca_key = tmp_path / "ca_key"
ca_key.write_text("fake-ca")
ca = LocalCA(ca_key, tmp_path / "state")
with patch("warden.ca.subprocess.run", side_effect=_mock_keygen_gen):
privkey, pubkey = ca.generate_keypair("agt-test")
assert oct(privkey.stat().st_mode & 0o777) == oct(0o600)
assert oct(pubkey.stat().st_mode & 0o777) == oct(0o644)