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.
1. Scope
Section titled “1. Scope”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:false→payloadis the literal credential JSON), x5c chain, RFC-3161adoTsttimestamp. Not Data Integrity, not emergent’s VC-JOSE. - conform-ed ships (dev-channel):
@conform-ed/contracts/elm/v3_3;@conform-ed/credential-verification→verifyEdc(one-call orchestrator: seal + SHACL + validity window + status →EdcVerdict), plus the primitivesvalidateAgainstProfile/verifyJadesSeal/extractSealedCredential/isSealedEdc(verify-only — no seal production);@conform-ed/elm-render→renderEdc→{html, viewModel}. The EDC SHACL shapes ship FROM@conform-ed/credential-verificationitself —edcGenericFullShapes()(theowl:importsclosure forverifyEdc) +elmPlainProfileShapes()(LOQ/ AMS/PID) — so emergent never vendors shapes (added upstream inaef0952; D10). EdcVerdictshape:{ verdict: "verified"|"invalid"|"unverifiable", seal, sealIntact, trustAnchored, structure (SHACL), validityWindow, revocation, issuer, credential?, reasons[] }.trustAnchoredis a separate honest axis that never downgradesverdict.- emergent rail to reuse: wallet import (
POST /workflow/wallet/credentials, file/URL, verify-on-ingest),held_credential(sync;credential_typediscriminator; server-onlyrawPayloadverificationMeta; synceddisplay_meta;subject_id/subject_match; erasurecascade), credential-verification service + resolvers, adminCredentialVerifierPanel, learner-web wallet.
- Signing today = Ed25519 VC-JOSE (ADR-0016); no X.509/eIDAS/JAdES/RFC-3161 in emergent. Trust
anchors in
verifyEdc/verifyJadesSealare host-injected — emergent injects empty in v1 (D3).
3. Upstream (conform-ed) — DONE
Section titled “3. Upstream (conform-ed) — DONE”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 callsverifyEdc(parsed, { shapes: <vendored edc-generic-full>, trustAnchors: [] })and maps theEdcVerdictinto the service response. Trust anchors empty (D3) →trustAnchored:falsereported honestly. - Wallet import (
apps/api/src/features/credential-wallet/import-service.ts): detect EDC (isSealedEdcon the parsed JSON-LD); gate =sealIntact; on pass, persist toheld_credential(credential_type="EuropeanDigitalCredential",rawPayload=sealed JSON,verificationMeta=EdcVerdict,display_meta=fromrenderEdc().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 rendersrenderEdc().viewModelvia React/Mantine, learner locale → language selection; list badge from synceddisplay_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 (fullEdcVerdict) + “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 → isSealedEdc → verifyEdc (empty trust) → gate: sealIntact
→ renderEdc().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-edc4bcbef; ELM SHACL shapes shipped from@conform-ed/credential-verificationinaef0952). - P1 — Deps & wiring: ✅ DONE —
@conform-ed/elm-renderadded; the three@conform-ed/*pins bumped toaef0952. Imports + typecheck green. - P2 — EDC verification service: ✅ DONE —
verifyEdcDocument(apps/api/…/credential-verification) callsverifyEdcwithedcGenericFullShapes()+ 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 —
importHeldCredentialbranches onisSealedEdc, gates on the seal, extracts EDC metadata, and persists toheld_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 —
/exportserves the original sealed EDC verbatim; detail modal “Download original”. - P7 — Conformance overlay: ✅ DONE —
docs/conformance/overlays/elm-edc-v3.3.yamlover the pinned EDC map; consumed model elementsdone(evidence-proven), issuance/VP/domain-import explicitlydeferred;conformance:checkgreen,[covers:]evidence proves under--require-proof. - P8 — Tests, docs, status: ✅ DONE —
tests/conformance/edc-credential.conformance.test.ts(registered inprove.ts), this status, the rendered conformance docs.validategreen.
7. Testing strategy
Section titled “7. Testing strategy”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.
8. New dependencies
Section titled “8. New dependencies”- emergent:
@conform-ed/elm-render(new); bumped@conform-ed/contracts,@conform-ed/credential-verification. - conform-ed: none required (the lane is built and published).
9. Risks & open items
Section titled “9. Risks & open items”- 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.
10. Definition of done (v1)
Section titled “10. Definition of done (v1)”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.