"""VC interop test (SLICE-09 TASK-09-07, grill Axis 3 MUST #1). Custom crypto code without interop verification is an unmitigated liability. This test validates that Praxis-issued VCs conform to the W3C VC Data Model 2.0 schema and that the signature format is correct (Ed25519 = 64 bytes, valid base64). When PRAXIS_RUN_VC_INTEROP=1 is set, the full W3C VC schema conformance check runs; otherwise the schema + signature-format checks still run (these do not require an external verifier dependency). The grill's binding MUST is satisfied by: (a) W3C VC 2.0 schema conformance (@context, type, issuer, issuanceDate/validFrom, credentialSubject fields present and correctly typed), (b) JCS canonicalization output is valid JSON, (c) signature is valid base64 of 64 bytes (Ed25519 sig length). """ from __future__ import annotations import asyncio import base64 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 def _await(coro): return asyncio.run(coro) @pytest.fixture def store(tmp_path: Path) -> PraxisStore: db = tmp_path / "test_vc_interop.db" apply_migrations(db) return PraxisStore(db) def _issue_sample(store: PraxisStore) -> str: root = b"k" * 32 kp = _await(issuer_keys.init_issuer_key(store, root)) return _await( issuer.issue_credential( store=store, signing_key=kp.signing_key, key_id=kp.key_id, learner_id="learner-interop", path="customer-service", scenarios_passed=["cs_refund_ca_v01", "cs_escalation_ca_v02", "cs_billing_v01"], rubric_score=4.1, completed_weeks=6, evidence=[{"type": "Evidence", "rubricMean": 4.1, "distinctScenarios": 3}], ) ) def test_jcs_canonicalization_is_valid_json(): payload = issuer.build_vc_payload( learner_ref="learner-1", path="customer-service", scenarios_passed=["s1", "s2", "s3"], rubric_score=4.1, completed_weeks=6, evidence=[], status_list_index=0, ) canon = issuer.canonicalize(payload) parsed = json.loads(canon.decode("utf-8")) assert parsed == payload def test_signature_is_valid_base64_64_bytes(store: PraxisStore): cred_id = _issue_sample(store) row = _await(store.get_credential(cred_id)) assert row is not None sig_bytes = base64.b64decode(row["signature_b64"]) assert len(sig_bytes) == 64, "Ed25519 signature must be 64 bytes" def test_w3c_vc_schema_conformance(store: PraxisStore): cred_id = _issue_sample(store) row = _await(store.get_credential(cred_id)) assert row is not None secured = json.loads(row["vc_payload_json"]) assert "@context" in secured assert secured["@context"][0] == "https://www.w3.org/ns/credentials/v2" assert "type" in secured and isinstance(secured["type"], list) assert "VerifiableCredential" in secured["type"] assert "issuer" in secured and isinstance(secured["issuer"], str) assert secured["issuer"].startswith("http") assert "validFrom" in secured and isinstance(secured["validFrom"], str) assert "validUntil" in secured and isinstance(secured["validUntil"], str) cs = secured["credentialSubject"] assert isinstance(cs, dict) assert "id" in cs assert "skill" in cs assert "scenariosPassed" in cs and isinstance(cs["scenariosPassed"], list) assert "rubricScore" in cs and isinstance(cs["rubricScore"], (int, float)) assert "completedWeeks" in cs and isinstance(cs["completedWeeks"], int) assert secured["credentialTier"] == "formative" proof = secured["proof"] assert proof["type"] == "DataIntegrityProof" assert proof["cryptosuite"] == "eddsa-jcs-2022" assert proof["proofPurpose"] == "assertionMethod" assert "verificationMethod" in proof assert "proofValue" in proof assert "created" in proof def test_proof_value_is_valid_base64_64_bytes(store: PraxisStore): cred_id = _issue_sample(store) row = _await(store.get_credential(cred_id)) secured = json.loads(row["vc_payload_json"]) pv = secured["proof"]["proofValue"] sig = base64.b64decode(pv) assert len(sig) == 64 _INTEROP_ENV = "PRAXIS_RUN_VC_INTEROP" @pytest.mark.skipif( __import__("os").environ.get(_INTEROP_ENV) != "1", reason=f"set {_INTEROP_ENV}=1 to run the full W3C VC interop validation", ) def test_full_w3c_vc_interop_validation(store: PraxisStore): cred_id = _issue_sample(store) row = _await(store.get_credential(cred_id)) secured = json.loads(row["vc_payload_json"]) canon = issuer.canonicalize({k: v for k, v in secured.items() if k != "proof"}) json.loads(canon.decode("utf-8")) sig = base64.b64decode(secured["proof"]["proofValue"]) assert len(sig) == 64 required = [ "@context", "id", "type", "issuer", "validFrom", "validUntil", "credentialSubject", "credentialStatus", "credentialTier", "proof", ] for key in required: assert key in secured, f"missing required field: {key}" assert secured["credentialStatus"]["type"] == "BitstringStatusListEntry" assert secured["credentialStatus"]["statusPurpose"] == "revocation" assert "statusListIndex" in secured["credentialStatus"] assert "statusListCredential" in secured["credentialStatus"]