Repository navigation
security(ebsi): TIR roles are an exact allow-list; Revoked was trusted - #371
Merged
Merged
Conversation
Both registry APIs render the on-chain issuer type as a name and both spell the revoked one `Revoked` (legacy TIR v5 OpenAPI 5.0.0: "RootTAO, TAO, TI, Revoked or Undefined"; Core API v1 OpenAPI 1.0.0, served by every node at /v1/openapi.json: ["undefined", "RootTAO", "TAO", "TI", "Revoked"]). `Accreditation.is_revoked` compared with the lowercase `revoked`, which neither emits, and nothing checked that the role is a live one, so on v4, v5 and Core v1 a revoked accreditation, an unset one (`undefined`, absent, empty) and any other string were eligible for single-level and recursive trust, and a TI could stand above the leaf. An accreditation is now eligible only if its issuerType is exactly RootTAO, TAO or TI, and above the leaf only RootTAO or TAO may accredit; the role rule runs before the type rule; REVOKED_ROLE spells Revoked and is_revoked also reads the old lowercase; the "no valid accreditation" messages name the roles set aside, bounded. The adapters are unchanged. Closes #358
The adversarial review of PR #371 (no wrong-accept found) left two LOW items: the refusal message above the leaf quoted `sorted(needed)` raw (with a wildcard leaf accreditation that is the credential's whole type list; 1 002 163 characters measured on main with 500 types), and the new note de-duplicated roles with a list membership test (v4 inline attributes are uncapped). Both bounded now, with regression tests; the three stale test artefacts it named are refreshed (the wire spelling `Revoked` in the two older tests, the v4 shape docstring). Refs #358
Owner
Author
Adversarial review (before merge)Reviewer: a subagent on the session model, instructed to break the change with runnable repros (scripts kept locally under the session's scratchpad, What was tried and failed closed
Fixed after the review (second commit)
Policy residual, documented: the anchor's own record is never read; with ROOT pinned and ROOT's record saying |
This was referenced Oct 30, 2026
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.
Closes #358. Milestone: Conformance & production readiness. Security fix; the CHANGELOG heading is now
[1.38.0] — unreleased(one release for the trust-policy wave, maintainer's decision of 2026-10-30).Step 0 — the vocabulary, from the two primary sources (read 2026-10-30)
https://api-pilot.ebsi.eu/trusted-issuers-registry/v5/openapi.json(info.version5.0.0),components.schemas.attribute.properties.issuerType.description: "RootTAO, TAO, TI, Revoked or Undefined". Its JSON-RPC example writes the type as an integer (3); the read API renders a name.GET /v1/openapi.jsonon a node withAccept: application/openapi+json(measured onapi.ebsi.iti.gr;info.version1.0.0,openapi3.2.0):issuerTypeis{"type": "string", "enum": ["undefined", "RootTAO", "TAO", "TI", "Revoked"], "description": "Issuer type.undefined(lowercase) is returned for type 0."}.tir_v5_revisions.json; Core v1 preprod GRNET / INCM and prod ITI): every attribute wrapper carriesRootTAO, nothing else. Pinned as a control.ti/tao/roottao/revoked(lowercase) and the trust-model pages writeRootTAO; the wire vocabulary is the OpenAPI one above. The352-walk-shape-binding/review/notes folder cited by the issue is not on this machine; the repro was rebuilt from the issue text.Measurement on
mainat b0feaeb (CPython 3.14.10)Real P-256 signatures through
evaluate_issuer_trust(single) andverify_trust_chain(chain), LEAF accredited by ROOT, the anchor; the leaf attribute'sissuerTypevaried (script: the harness oftests/test_ebsi_issuer_type_roles.py):So the only refused spelling was the one no registry sends. A revoked accreditation was a wrong-accept on real registry vocabulary, every lane, both trust modes, and a TI (or a revoked / unset accreditor) could accredit. The issue's severity note of 2026-10-21 was right on both counts.
What changed
models.py:TRUST_ROLES = {RootTAO, TAO, TI}(unchanged), newDELEGATING_ROLES = {RootTAO, TAO},REVOKED_ROLE = "Revoked"(the wire spelling) andREVOKED_ROLES = {Revoked, revoked};Accreditation.has_trust_role,Accreditation.can_accredit;is_revokedreads both spellings. The comment cites both OpenAPI documents.trust._pick_accreditation: an accreditation is eligible only withhas_trust_role, and above the leaf only withcan_accredit; the role rule runs before the wildcard / type rule, so an unrestricted accreditation with a bad role authorises nothing. Newtrust.ineligible_roles_note(record, leaf=): a bounded clause (safe_repr, role cut at 48 chars, at most three distinct) appended to theNoTrustedAnchormessage, whose leading words are unchanged.verify.evaluate_issuer_trust: the same eligibility rule; theIssuerNotTrustedmessage gains the same clause.Tests
tests/test_ebsi_issuer_type_roles.py(127 cases, every lane v4 / v5 / Core v1, both policies): the headlineRevoked(throughverify_ebsi_badgein both trust modes andrequire_trust=False); 17 spellings outside the vocabulary (absent, empty,undefined,Undefined,revoked,REVOKED, trailing / leading space, lowercase, zero-width space,TAO,TI,3,Banana); the role rule before the type rule (wildcard + bad role); the two-hop delegation rule (TI,Revoked,undefined, empty, absent accreditor refused;TAO/RootTAOaccepted); a record mixing a revoked and a live attribute picks the live one; bounded messages (a 70 000-character role, both entry points) that name the role and the rule; the model predicates; the recorded registries carryRootTAOonly. Every negative is TRUSTED onmain(table above) and refused here: removing either new check fails named tests (mutation check).Two existing tests pinned the old behaviour and were adjusted, keeping their intent:
test_ebsi_core_v1.py::test_an_attribute_object_with_no_usable_body_is_still_a_wildcard_by_design(two shapes had noissuerType; they now carryTI, since the body is the point there) andtest_trust_chain.py::test_pick_accreditation_accepts_wildcard_consistent_with_single_level(calls_pick_accreditationabove the leaf; the fixture role isTAO). The two tests that speltrevokedlowercase (test_trust_chain.py,test_verify_glue.py) still refuse, now for the role rule.Docs
CHANGELOG
### Securityunder[1.38.0] — unreleased(with a migration note);wiki/Trust.md(the sentence where roles are first explained, plus a bullet);docs/audit/threat-model.mdrow I24;docs/audit/assurance.md§4 bullet#358; module docstrings.Review
Trust evaluation: an adversarial review runs before merge (posted below). Merge waits for the maintainer's OK, as the issue says.