feat(rp-registration): parse and verify EUDI WRPRC (ETSI TS 119 475)#133
Merged
Conversation
The other half of #67's WRPAC: where the access certificate authenticates *who is asking*, the registration certificate answers "were they registered to ask for this?" — it carries the relying party's entitlements and the credentials/attributes it may request. New module `openvc.rp_registration`, same trusted/untrusted split as `openvc.rp_cert`. Both profiled forms are read: the JWT (rc-wrp+jwt) over the JOSE lane and the CWT (rc-wrp+cwt) over the dependency-free CBOR/COSE codec, sharing the algorithm allow-list applied before any crypto and the x5c primitives now exported from `openvc.x5c`. Verification alone only proves a registrar signed something, so two fail-closed cross-checks carry the authorization decision: the WRPRC's `sub` is bound to the WRPAC's entity identifier (GEN-5.1.1-04 — otherwise an attacker pairs their own valid WRPAC with someone else's valid WRPRC), and a DCQL request must stay inside the registered format/meta/claim-path scope. Only a verify subset of the JAdES baseline B-B profile is implemented: the signed-header profile plus chain validation, no signature-policy processing, timestamps or augmentation. Three properties of the profile that are easy to get wrong are handled explicitly: one WRPRC carries exactly one intended use (clause 5.2.4 flattens TS5's nested intendedUse[]), `exp` is optional so the 12-month ceiling binds only when present, and the CWT form has no claim-key mapping in the spec — its claims map is read accepting both RFC 8392 integer keys and text keys, and that lane stays provisional until a real artifact exists to pin. Two published spec defects are absorbed rather than inherited (sname/name, intermediary.sub/act.sub); the German BMI rc-rp+jwt profile is refused by name rather than half-parsed under the wrong claim semantics. No official signed vectors exist, so the suite pins the informative Annex C payload verbatim and otherwise builds both forms over the library's own machinery — 89 tests, negative paths first. Also fixes documentation drift: the WRPAC was attributed to TS 119 475 (it is TS 119 411-8) and described as carrying registered entitlements, which is the WRPRC's job. Closes #89
A `meta` value is attacker-influenced JSON and may hold unhashable members (an object, an array). Subset-testing it with `set(want) <= set(have)` raised a bare TypeError straight past the OpenvcError family. Found by adversarial review; equality containment has no such edge.
The one that mattered: the claim-path reader took `claim` before `claims` on
both sides. That is correct for a registration (the spec's spelling), but on a
*request* it let a relying party hide a narrow decoy in `claim` and the real,
broader ask in `claims` — openvc authorized the decoy while the wallet, which
follows DCQL, answers `claims`. Unknown query members are ignored downstream,
so the escalating query stayed valid end to end. The request side now takes the
union of both spellings; the registration side keeps precedence, since only
there does a second spelling risk widening a grant.
Also closed:
- A bare OverflowError escaped the OpenvcError family on a bignum iat/exp/nbf:
math.isfinite casts to float, and json.loads yields such ints from the wire.
- An empty registered {"path": []} was a blanket grant over the whole
credential — an empty tuple is a prefix of every path.
- A non-object `meta` was coerced to {}, turning a malformed constraint into no
constraint and widening the entry to every credential of that format. Absent
and malformed are now distinct: {} matches an unconstrained request, None
matches nothing. (Not hypothetical — clause B.2.9 types `meta` as a string
while clause 5.2.4 and Annex C use an object.)
- `meta` matching conflated True with 1, unlike the path matcher.
- The entitlement floor accepted any non-empty string while
ENTITLEMENT_URI_PREFIX sat exported-but-unused; it now checks the clause-A.2
namespace GEN-5.2.4-03 requires, with require_entitlement=False to opt out.
Each has a regression test named for the attack. The binding check, the
algorithm allow-list, the chain handling and the CWT parser were probed and
held.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(rp-registration): parse and verify EUDI WRPRC (ETSI TS 119 475)
The other half of #67's WRPAC: where the access certificate authenticates
who is asking, the registration certificate answers "were they registered
to ask for this?" — it carries the relying party's entitlements and the
credentials/attributes it may request.
New module
openvc.rp_registration, same trusted/untrusted split asopenvc.rp_cert. Both profiled forms are read: the JWT (rc-wrp+jwt) over theJOSE lane and the CWT (rc-wrp+cwt) over the dependency-free CBOR/COSE codec,
sharing the algorithm allow-list applied before any crypto and the x5c
primitives now exported from
openvc.x5c.Verification alone only proves a registrar signed something, so two
fail-closed cross-checks carry the authorization decision: the WRPRC's
subis bound to the WRPAC's entity identifier (GEN-5.1.1-04 — otherwise an
attacker pairs their own valid WRPAC with someone else's valid WRPRC), and a
DCQL request must stay inside the registered format/meta/claim-path scope.
Only a verify subset of the JAdES baseline B-B profile is implemented: the
signed-header profile plus chain validation, no signature-policy processing,
timestamps or augmentation.
Three properties of the profile that are easy to get wrong are handled
explicitly: one WRPRC carries exactly one intended use (clause 5.2.4 flattens
TS5's nested intendedUse[]),
expis optional so the 12-month ceiling bindsonly when present, and the CWT form has no claim-key mapping in the spec —
its claims map is read accepting both RFC 8392 integer keys and text keys,
and that lane stays provisional until a real artifact exists to pin. Two
published spec defects are absorbed rather than inherited (sname/name,
intermediary.sub/act.sub); the German BMI rc-rp+jwt profile is refused by
name rather than half-parsed under the wrong claim semantics.
No official signed vectors exist, so the suite pins the informative Annex C
payload verbatim and otherwise builds both forms over the library's own
machinery — 89 tests, negative paths first.
Also fixes documentation drift: the WRPAC was attributed to TS 119 475 (it is
TS 119 411-8) and described as carrying registered entitlements, which is the
WRPRC's job.
Closes #89