"""Public VC verification (SLICE-09 TASK-09-04, D-043, REQ-NFR-VC-02). `GET /vc/verify/` — public, unauthenticated. Fetches the credential from SQLite, fetches the issuer public key, validates the Ed25519 signature against the JCS-canonicalized payload, checks the Bitstring Status List (no cache — fetched on every verify call, REQ-NFR-VC-02). Returns JSON {valid, status, issuer, credential, mastery, credentialTier, verifiedAt}. No PII beyond what the credential asserts. """ from __future__ import annotations import datetime as _dt import json from typing import Any from db.store import PraxisStore from server.vc.issuer import verify_proof, extract_key_id, CREDENTIAL_TIER from server.vc.issuer_keys import get_public_key_for_verification from server.vc.status_list import BitstringStatusList def _now_iso() -> str: return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") async def verify_credential( store: PraxisStore, credential_id: str ) -> dict[str, Any] | None: row = await store.get_credential(credential_id) if row is None: return None secured_doc = json.loads(row["vc_payload_json"]) key_id = extract_key_id(secured_doc) if key_id is None: return _invalid(row, secured_doc) try: verify_key = await get_public_key_for_verification(store, key_id) except KeyError: return _invalid(row, secured_doc) sig_valid = verify_proof(secured_doc, verify_key) revoked = False cs = secured_doc.get("credentialStatus") or {} idx_str = cs.get("statusListIndex") if idx_str is not None: sl = BitstringStatusList(store, "default") revoked = await sl.get_status(int(idx_str)) status = "revoked" if revoked else "active" valid = bool(sig_valid and not revoked) subject = secured_doc.get("credentialSubject") or {} issuer = secured_doc.get("issuer") return { "valid": valid, "status": status, "issuer": issuer, "credential": { "id": secured_doc.get("id"), "type": secured_doc.get("type"), "validFrom": secured_doc.get("validFrom"), "validUntil": secured_doc.get("validUntil"), }, "mastery": { "skill": subject.get("skill"), "level": subject.get("level"), "path": subject.get("path"), "rubricScore": subject.get("rubricScore"), "scenariosPassed": subject.get("scenariosPassed", []), "completedWeeks": subject.get("completedWeeks"), }, "credentialTier": subject.get("credentialTier", CREDENTIAL_TIER), "verifiedAt": _now_iso(), } def _invalid(row: dict, secured_doc: dict) -> dict[str, Any]: subject = secured_doc.get("credentialSubject") or {} return { "valid": False, "status": row.get("status", "active"), "issuer": secured_doc.get("issuer"), "credential": { "id": secured_doc.get("id"), "type": secured_doc.get("type"), "validFrom": secured_doc.get("validFrom"), "validUntil": secured_doc.get("validUntil"), }, "mastery": { "skill": subject.get("skill"), "level": subject.get("level"), "path": subject.get("path"), "rubricScore": subject.get("rubricScore"), "scenariosPassed": subject.get("scenariosPassed", []), "completedWeeks": subject.get("completedWeeks"), }, "credentialTier": subject.get("credentialTier", CREDENTIAL_TIER), "verifiedAt": _now_iso(), } async def revoke_credential(store: PraxisStore, credential_id: str) -> bool: row = await store.get_credential(credential_id) if row is None: return False secured_doc = json.loads(row["vc_payload_json"]) cs = secured_doc.get("credentialStatus") or {} idx_str = cs.get("statusListIndex") if idx_str is None: await store.set_credential_status(credential_id, "revoked") return True sl = BitstringStatusList(store, "default") await sl.set_status(int(idx_str), True) await store.set_credential_status(credential_id, "revoked") return True __all__ = ["verify_credential", "revoke_credential"]