Skip to content

Europass / ELM EDC consumption — design & implementation plan

Companion to ADR-0047 (decisions D1–D14). emergent consumes EU-issued European Digital Credentials (verify → hold → display → re-export); issuance, EU trust anchoring, ELM domain import, and hosting/VP presentation are deferred. The conform-ed side is ready on the dev-channel — emergent’s verification is a single verifyEdc call.

v1 makes emergent a consumer of EU-issued EDCs, delivered as sealed JSON-LD: verify the eIDAS JAdES seal (cryptographic + structural, no EU trust anchoring), hold the EDC in the learner wallet, display it (locale-aware), re-export the original sealed file, and offer validate-only tooling for the plain ELM profiles (LOQ/PID/AMS). Out of scope (deferred lanes): EDC issuance (QTSP-gated), EU trust anchoring (LOTL/TSL), ELM domain import (crosswalk), hosting / VP presentation (EBSI/EU-wallet), and legacy ELM-v2/XML + PDF-embedded ingest (D14 — current v3 delivery is JSON-LD, not PDF).

2. Pinned facts (so a builder need not re-derive)

Section titled “2. Pinned facts (so a builder need not re-derive)”
  • An EDC is a W3C VC (VC-DM 1.1, dual type) sealed with JAdES (JWS-JSON, b64:falsepayload is the literal credential JSON), x5c chain, RFC-3161 adoTst timestamp. Not Data Integrity, not emergent’s VC-JOSE.
  • conform-ed ships (dev-channel): @conform-ed/contracts/elm/v3_3; @conform-ed/credential-verificationverifyEdc (one-call orchestrator: seal + SHACL + validity window + status → EdcVerdict), plus the primitives validateAgainstProfile / verifyJadesSeal / extractSealedCredential / isSealedEdc (verify-only — no seal production); @conform-ed/elm-renderrenderEdc{html, viewModel}. The EDC SHACL shapes ship FROM @conform-ed/credential-verification itself — edcGenericFullShapes() (the owl:imports closure for verifyEdc) + elmPlainProfileShapes() (LOQ/ AMS/PID) — so emergent never vendors shapes (added upstream in aef0952; D10).
  • EdcVerdict shape: { verdict: "verified"|"invalid"|"unverifiable", seal, sealIntact, trustAnchored, structure (SHACL), validityWindow, revocation, issuer, credential?, reasons[] }. trustAnchored is a separate honest axis that never downgrades verdict.
  • emergent rail to reuse: wallet import (POST /workflow/wallet/credentials, file/URL, verify-on-ingest), held_credential (sync; credential_type discriminator; server-only rawPayload
    • verificationMeta; synced display_meta; subject_id/subject_match; erasure cascade), credential-verification service + resolvers, admin CredentialVerifierPanel, learner-web wallet.
  • Signing today = Ed25519 VC-JOSE (ADR-0016); no X.509/eIDAS/JAdES/RFC-3161 in emergent. Trust anchors in verifyEdc/verifyJadesSeal are host-injected — emergent injects empty in v1 (D3).

verifyEdc is built and published (conform-ed c4bcbef) — seal + SHACL + window + status in one call, trustAnchored as a separate axis, corpus-tested against the 5 real signed EU EDCs. The build then surfaced one needed upstream change (fix-upstream-first, never shimmed in emergent): the EDC SHACL shapes lived only in the private @conform-ed/coverage package, so aef0952 ships+exports them from @conform-ed/credential-verification (edcGenericFullShapes() / elmPlainProfileShapes()) — emergent pins aef0952 and feeds those shapes to verifyEdc. No further conform-ed change is needed.

4. Component design (emergent surfaces touched)

Section titled “4. Component design (emergent surfaces touched)”
  • Verification service (apps/api/src/features/credential-verification/): add an EDC path that calls verifyEdc(parsed, { shapes: <vendored edc-generic-full>, trustAnchors: [] }) and maps the EdcVerdict into the service response. Trust anchors empty (D3) → trustAnchored:false reported honestly.
  • Wallet import (apps/api/src/features/credential-wallet/import-service.ts): detect EDC (isSealedEdc on the parsed JSON-LD); gate = sealIntact; on pass, persist to held_credential (credential_type="EuropeanDigitalCredential", rawPayload=sealed JSON, verificationMeta=EdcVerdict, display_meta=from renderEdc().viewModel + a small {profile,sealIntact,trustAnchored} summary, subject_* from the embedded ELM Person).
  • Re-export: endpoint serving the held EDC’s original rawPayload.
  • Display (apps/learner-web/src/features/wallet/): wallet detail renders renderEdc().viewModel via React/Mantine, learner locale → language selection; list badge from synced display_meta (honest “seal intact · EU-trust not verified”). verification_status = lifecycle only (no enum change).
  • Admin verifier (apps/admin-console/src/features/credential-verification/): EDC mode (full EdcVerdict) + “ELM document” validate-only mode for LOQ/PID/AMS (validateAgainstProfile → conforms/violations).
  • Conformance overlay: EDC coverage overlay + drift gate (mirror OB/CLR overlays); explicit satisfied / N/A / deferred rows.

5. Data flow (import → hold → display)

Section titled “5. Data flow (import → hold → display)”

upload (.json/.jsonld) → parse → isSealedEdcverifyEdc (empty trust) → gate: sealIntactrenderEdc().viewModel → persist held_credential (sealed rawPayload, EdcVerdict in verificationMeta, non-PII display_meta incl. seal summary) → wallet list (synced) + detail (server-backed full verdict) → re-export serves rawPayload.

6. Implementation plan (ordered, with exit gates)

Section titled “6. Implementation plan (ordered, with exit gates)”

Status: BUILT (2026-06-27). All phases landed on develop across three commits — 872fbef (P2/P3, server verify + import/hold), 3050259 (P4/P5/P6, display + validate-only + re-export), and the conformance commit (P7/P8). The dev-channel pin is @conform-ed/* 0.1.3-dev.1782514652.aef0952.

  • P0 — Upstream verifyEdc: ✅ DONE (conform-ed c4bcbef; ELM SHACL shapes shipped from @conform-ed/credential-verification in aef0952).
  • P1 — Deps & wiring: ✅ DONE — @conform-ed/elm-render added; the three @conform-ed/* pins bumped to aef0952. Imports + typecheck green.
  • P2 — EDC verification service: ✅ DONE — verifyEdcDocument (apps/api/…/credential-verification) calls verifyEdc with edcGenericFullShapes() + empty trust; the verify route auto-detects a sealed EDC (isSealedEdc) and returns a {kind:"edc"|"vc"} verdict. Conformance lane verifies 3 real EU EDCs; tampered → invalid; trust axis “not verified”.
  • P3 — Import & hold: ✅ DONE — importHeldCredential branches on isSealedEdc, gates on the seal, extracts EDC metadata, and persists to held_credential (no migration). Idempotent; broken seal rejected; PGlite-tested.
  • P4 — Display: ✅ DONE — honest learner list badge (seal intact · EU-trust not verified), a server-backed locale-aware detail modal (/rendered), and the admin verifier’s EDC verdict.
  • P5 — Validate-only (LOQ/PID/AMS): ✅ DONE — validateElmDocument + /validate-elm; admin “ELM document” mode (profile select → conforms/violations).
  • P6 — Re-export: ✅ DONE — /export serves the original sealed EDC verbatim; detail modal “Download original”.
  • P7 — Conformance overlay: ✅ DONE — docs/conformance/overlays/elm-edc-v3.3.yaml over the pinned EDC map; consumed model elements done (evidence-proven), issuance/VP/domain-import explicitly deferred; conformance:check green, [covers:] evidence proves under --require-proof.
  • P8 — Tests, docs, status: ✅ DONE — tests/conformance/edc-credential.conformance.test.ts (registered in prove.ts), this status, the rendered conformance docs. validate green.

Unit: verdict mapping, subject extraction. Conformance lane (mirror the existing credential-verifier conformance test): every real EU EDC sample verifies (seal intact, SHACL conforms, trust “not verified”); a tampered EDC and a malformed ELM doc are rejected/flagged. Integration: import → held_credential → list/detail → re-export round-trip.

  • emergent: @conform-ed/elm-render (new); bumped @conform-ed/contracts, @conform-ed/credential-verification.
  • conform-ed: none required (the lane is built and published).
  • Honest-trust UX — must not read as “broken”; the badge wording is load-bearing (D6/D7).
  • PDF / legacy-v2 ingest — out of scope (D14); revisit only if real v3 PDF-wrapped EDCs surface or pre-2023 Europass-credential ingest becomes a goal.
  • Real EU EDC fixtures in emergent — RESOLVED: emergent re-vendors a representative subset (3 of the 5 signed EU EDCs) under tests/conformance/fixtures/edc/ with provenance; the two ~0.8–1.3 MB samples stay corpus, proven upstream in conform-ed (the full-5 lane). emergent proves its own wiring against the subset.

A learner imports a real EU EDC (.json/.jsonld) → it’s verified (seal intact + SHACL, trust honestly “not verified”) → held in the wallet → rendered in their locale → re-exportable; admin can validate LOQ/PID/AMS documents; an honest EDC conformance overlay is drift-gated; CONTEXT.md + ADR-0047 landed; validate:full + the EDC conformance lane green. Issuance, EU trust anchoring, ELM domain import, and hosting/VP presentation remain explicitly deferred.