4d39596a7d
Phase 1 complete. Mastery scoring + competency rubrics + VC issuer shipped. 9 slices, 5 waves, 238 tests passing, 13/13 REQ-IDs covered. 4/4 grill MUST conditions satisfied. VERIFY: APPROVE_WITH_NOTES. ---ci--- project: praxis phase: 1 milestone: v0.3 status: complete requirements: covered: [REQ-MAST-01, REQ-MAST-02, REQ-MAST-03, REQ-SCEN-02, REQ-SCEN-03, REQ-SCEN-04, REQ-PATH-02, REQ-NFR-MAST-01, REQ-NFR-MAST-02, REQ-NFR-VC-01, REQ-NFR-VC-02, REQ-NFR-IRT-01] partial: [] ---/ci---
121 lines
4.2 KiB
Python
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 |