Files
praxis/tests/test_vc_key_rotation_drill.py
T
Praxis CI 813bd586d6 docs(milestone): merge v0.3-mastery-scoring → main
v0.3 milestone merged to main. Mastery scoring + competency rubrics +
verifiable credentials (formative-tier) shipped. 13/13 REQ-IDs covered.
Next milestone: v0.4 (operator tier — cohort dashboard + auth + Postgres).

---ci---
project: praxis
phase: 2
milestone: v0.3
status: complete
milestone_complete: true
milestone_merged_to_main: true
---/ci---
2026-08-04 00:14:59 +00:00

121 lines
4.2 KiB
Python

"""Key-rotation operational drill (SLICE-09 TASK-09-08, grill Axis 3 MUST #2).
End-to-end operational drill:
1. issue 3 VCs with key A
2. rotate to key B (archive A as superseded)
3. issue 2 VCs with key B
4. verify all 5 VCs (3 from A verify against archived A public key,
2 from B verify against active B)
5. revoke one from each key
6. verify revoked ones fail
This is the one crypto procedure that, if broken, silently invalidates
every credential ever issued.
"""
from __future__ import annotations
import asyncio
import json
from pathlib import Path
import pytest
from db.migrate import apply_migrations
from db.store import PraxisStore
from server.vc import issuer, issuer_keys
from server.vc.verification import verify_credential, revoke_credential
def _await(coro):
return asyncio.run(coro)
@pytest.fixture
def store(tmp_path: Path) -> PraxisStore:
db = tmp_path / "test_vc_rotation.db"
apply_migrations(db)
return PraxisStore(db)
def _issue(store: PraxisStore, signing_key, key_id: str, learner: str) -> str:
return _await(
issuer.issue_credential(
store=store,
signing_key=signing_key,
key_id=key_id,
learner_id=learner,
path="customer-service",
scenarios_passed=["s1", "s2", "s3"],
rubric_score=4.0 + (0.1 if learner.endswith("a") else 0.2),
completed_weeks=6,
evidence=[{"type": "Evidence"}],
)
)
def test_key_rotation_operational_drill(store: PraxisStore):
root = b"k" * 32
kp_a = _await(issuer_keys.init_issuer_key(store, root))
creds_a = [
_issue(store, kp_a.signing_key, kp_a.key_id, f"learner-{i}a")
for i in range(3)
]
assert len(creds_a) == 3
kp_b = _await(issuer_keys.rotate_key(store, root))
creds_b = [
_issue(store, kp_b.signing_key, kp_b.key_id, f"learner-{i}b")
for i in range(2)
]
assert len(creds_b) == 2
old_row = _await(store.get_public_key_row(kp_a.key_id))
assert old_row["status"] == "superseded"
active_row = _await(store.get_active_signing_key_row())
assert active_row["id"] == kp_b.key_id
all_creds = creds_a + creds_b
for cid in all_creds:
res = _await(verify_credential(store, cid))
assert res is not None, f"credential {cid} not found"
assert res["valid"] is True, f"credential {cid} failed verification"
assert res["credentialTier"] == "formative"
for cid in creds_a:
row = _await(store.get_credential(cid))
secured = json.loads(row["vc_payload_json"])
vm = secured["proof"]["verificationMethod"]
assert kp_a.key_id in vm
for cid in creds_b:
row = _await(store.get_credential(cid))
secured = json.loads(row["vc_payload_json"])
vm = secured["proof"]["verificationMethod"]
assert kp_b.key_id in vm
revoked_a = creds_a[0]
revoked_b = creds_b[0]
assert _await(revoke_credential(store, revoked_a)) is True
assert _await(revoke_credential(store, revoked_b)) is True
res_ra = _await(verify_credential(store, revoked_a))
assert res_ra["valid"] is False
assert res_ra["status"] == "revoked"
res_rb = _await(verify_credential(store, revoked_b))
assert res_rb["valid"] is False
assert res_rb["status"] == "revoked"
for cid in [creds_a[1], creds_a[2], creds_b[1]]:
res = _await(verify_credential(store, cid))
assert res["valid"] is True, f"non-revoked credential {cid} should still verify"
assert res["status"] == "active"
def test_rotated_key_public_key_still_served(store: PraxisStore):
root = b"k" * 32
kp_a = _await(issuer_keys.init_issuer_key(store, root))
_await(issuer_keys.rotate_key(store, root))
vk = _await(issuer_keys.get_public_key_for_verification(store, kp_a.key_id))
assert bytes(vk) == bytes(kp_a.verify_key)
def test_active_key_after_rotation_is_new(store: PraxisStore):
root = b"k" * 32
kp_a = _await(issuer_keys.init_issuer_key(store, root))
kp_b = _await(issuer_keys.rotate_key(store, root))
assert kp_a.key_id != kp_b.key_id
active = _await(issuer_keys.get_active_signing_key(store, root))
assert active[0].key_id == kp_b.key_id