Keep and read a European Digital Credential (#670) #1182

Merged
tiagoagueda merged 3 commits from 670-digital-credentials into main 2026-10-31 22:24:20 +00:00
Owner

Postulo now keeps and reads a European Digital Credential (ELM v3 JSON-LD), in two commits.

Stage 1: keep a credential as the file it is

  • documents/credentials.py recognises a credential by its content: a JAdES envelope ({"payload", "signatures"}) or a bare VC whose type holds VerifiableCredential. Caps: 5 MiB and a depth of 40, and @context is never followed.
  • json and jsonld uploads are accepted only when they hold a credential, and only as a certificate, a diploma or a diploma supplement.
  • An ELM v2 XML credential is refused with a sentence that sends the person back to the Europass wallet.
  • The file is stored byte for byte, with a grey "Seal not checked by Postulo" label, never a tick. The API's DocumentOut gains seal.
  • Migration documents/0032: the extension list changed.
  • Fixtures: the four EUPL-1.2 samples from european-commission-empl/European-Learning-Model, byte-identical, with their provenance in tests/data/edc/README.md.

Stage 2: take what it states

  • plugins/europass/credential.py reads:
    • the title, preferring a LearningAchievement claim;
    • who awarded it, from awardingBody.legalName and never the sealing issuer;
    • when: awardingDate, else validFrom, else issued;
    • the EQF level, from eqfLevel.id.
  • The edit page shows these facts on a "What this credential says" card.
  • The summary is read from the file each time and not stored: no new column, no archive format change, no identifier kept.

Out of scope, per the issue: checking the seal (a separate plugin later), and creating an education entry from a credential.

Tests: tests/test_credentials.py (41 tests), tests/e2e/test_credentials.py, and the documents, API, plugin-surface, import-layer, migration, translation, template-lint and stylesheet tests: 8,313 passed in the agent's run.

Closes #670

🤖 Generated with Claude Code

Postulo now keeps and reads a European Digital Credential (ELM v3 JSON-LD), in two commits. **Stage 1: keep a credential as the file it is** - `documents/credentials.py` recognises a credential by its content: a JAdES envelope (`{"payload", "signatures"}`) or a bare VC whose `type` holds `VerifiableCredential`. Caps: 5 MiB and a depth of 40, and `@context` is never followed. - `json` and `jsonld` uploads are accepted only when they hold a credential, and only as a certificate, a diploma or a diploma supplement. - An ELM v2 XML credential is refused with a sentence that sends the person back to the Europass wallet. - The file is stored byte for byte, with a grey "Seal not checked by Postulo" label, never a tick. The API's `DocumentOut` gains `seal`. - Migration `documents/0032`: the extension list changed. - Fixtures: the four EUPL-1.2 samples from european-commission-empl/European-Learning-Model, byte-identical, with their provenance in `tests/data/edc/README.md`. **Stage 2: take what it states** - `plugins/europass/credential.py` reads: - the title, preferring a `LearningAchievement` claim; - who awarded it, from `awardingBody.legalName` and never the sealing issuer; - when: `awardingDate`, else `validFrom`, else `issued`; - the EQF level, from `eqfLevel.id`. - The edit page shows these facts on a "What this credential says" card. - The summary is read from the file each time and **not stored**: no new column, no archive format change, no identifier kept. Out of scope, per the issue: checking the seal (a separate plugin later), and creating an education entry from a credential. **Tests:** `tests/test_credentials.py` (41 tests), `tests/e2e/test_credentials.py`, and the documents, API, plugin-surface, import-layer, migration, translation, template-lint and stylesheet tests: 8,313 passed in the agent's run. Closes #670 🤖 Generated with [Claude Code](https://claude.com/claude-code)
An uploaded JSON or JSON-LD file is accepted when it holds a verifiable
credential, sealed (a JAdES envelope in the JWS JSON general serialisation,
the credential as an unencoded payload) or bare, and only as a certificate,
a diploma or a diploma supplement. It is stored byte for byte, since a seal
covers exact bytes, checksummed and delivered as an attachment like any file.

What a credential is is decided by content, in a new foundation module,
documents.credentials, which integrity.problem now asks for the two
extensions: a size cap before parsing (5 MiB; samples run to 1.3 MB of
inline base64), a nesting cap after, no @context followed. Any other JSON
is refused. The old ELM v2 XML credential is recognised by its start only
to be refused with a sentence saying to download it again from the
Europass wallet, before and instead of the extension's refusal.

Postulo does not check the seal and says so: a grey "Seal not checked by
Postulo" on the Files list, and `seal: "unchecked"` (null for other files)
in the API. The extension list on the model changes, hence migration 0032.

The four fixtures are the European Learning Model's own samples, EUPL-1.2,
provenance in tests/data/edc/README.md; they are marked -text and left out
of the pre-commit hooks so not a byte of them changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Show what a European Digital Credential states, beside the file (#670, stage 2)
Some checks failed
CI / Unit tests and coverage (Python 3.14) (pull_request) Failing after 15m26s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m11s
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
ad8922980f
A reader in the core's own europass plugin, plugins/europass/credential.py,
reads an ELM v3 credential for four facts: the title (a LearningAchievement
claim first, the display title only as a fallback), who awarded it
(awardedBy.awardingBody[*].legalName, never the sealing issuer), when
(awardingDate, else validFrom, else issued) and the EQF level, taken only
from an eqfLevel address the credential states, 1 to 8. Language maps give
the reader's language, else English, else the first; compaction's lists of
one are read as lists; claims and bodies are capped; no @context is
followed; no subject field (names, identifiers, birth date) is read.

It is a document's reader, not an import: the file's edit page shows
"What this credential says" beside the grey "Seal not checked by Postulo",
read from the file each time the page is drawn, so nothing is stored, no
column, migration or archive format change. With the plugin off for the
person, the file is kept and labelled and nothing is read. The reader
reaches documents.credentials for the one parse of a credential file,
recorded in REACHING_PAST.

Not done here, by the scope of this work: the "Add an education entry from
this" button, credits and validity dates, and the link to the EDC Viewer.
Checking the seal is a separate plugin.

Closes #670

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tell the credential reader its language rather than have it ask Django (#670)
Some checks failed
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
6c82f1755c
CI's guards found two things the reader's commits broke: it asked
`get_language()` itself, where `postulo.core.languages` is the one place a
language is asked (#337), so the page now passes `languages.current()`
and the plugin no longer imports Django's translation state; and a
docstring used "is taken as it is", the phrasing tests/test_telephone_typing.py
keeps out of the source since three places said it of telephone numbers.

Refs #670

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed 670-digital-credentials from 6c82f1755c
Some checks failed
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
to 7cb50991e0
All checks were successful
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m48s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 2m10s
CI / Database tests on PostgreSQL (pull_request) Successful in 3m35s
CI / Security audit (pull_request) Successful in 42s
CI / Browser tests (Chromium) (pull_request) Successful in 34m32s
2026-10-31 19:32:26 +00:00
Compare
tiagoagueda deleted branch 670-digital-credentials 2026-10-31 22:24:25 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!1182
No description provided.