| name | trust-graph |
| description | Pod-side certification-edge trust-graph admission with the opt-in cert-graph (sparq-trust) + trust-graph (sparq-solid) cargo features, both default-OFF: anchor trust in a certifier (a framework operator / Trusted-List authority) via a Control-gated trust:TrustRule, then let the depth-bounded, attenuation-only, fail-closed closure (derive_effective_rules) turn that certifier's signed trustx:Certification edges into derived rules the UNCHANGED admission gate consumes — so a pod admits facts from issuers the certifier vouches for, never wider than the certifier's own authority. Use when access should follow certified-issuer attestations rather than a controller-enumerated agent list; covers the anchor-rule TTL shape, the certification-bundle shape, the six EdgeRejection fail-closed reject reasons, and the PodStore wiring. RESEARCH prototype — no privacy/unlinkability claim, ZK estate externally unaudited (sq-qhy4). |
| license | MIT |
| metadata | {"version":"0.1.0","homepage":"https://github.com/jeswr/sparq"} |
sparq trust-graph — certification-edge admission (pod-side closure)
The certification-edge layer answers: "my pod anchors trust in a certifier (a government
framework operator, a Trusted-List registrar) — may it therefore admit facts from an issuer
that certifier has certified, without me enumerating that issuer myself?"
sparq_trust::graph::derive_effective_rules (opt-in cert-graph feature on
sparq-trust, default-OFF) runs a depth-bounded (v1: depth-1), attenuation-only,
fail-closed closure over signed trustx:Certification edges. It is pure
pre-processing: it produces the Vec<TrustRule> that the UNCHANGED admission gate
(sparq_trust::admit) then consumes exactly as it consumes a hand-authored policy.
direct_rules (Control-gated trust:TrustRule anchors) signed trustx:Certification edges
│ │
▼ ▼
┌────────────────────────────────────────────────────────────────────┐
│ derive_effective_rules (opt-in `cert-graph`, default-OFF) │
│ depth-1 · attenuation-only · fail-closed │
│ every derived rule ⊆ (anchor ∩ cert scope ∩ validity window) │
└────────────────────────────────────────────────────────────────────┘
│ effective_rules = direct_rules ++ derived
▼
ADMISSION gate (sparq_trust::admit, UNCHANGED) ─→ auth grants in <urn:sparq:auth>
(pod wiring: sparq-solid `trust-graph`, default-OFF)
What this is — and is NOT (honest scope, read first)
- RESEARCH prototype, default-OFF. Every flag involved is opt-in and default-OFF
(
cert-graph on sparq-trust, trust-graph on sparq-solid, solid-authz-trust on
sparq-server). With the features off, the crates build byte-identically to plain
WAC/ACP today — strict additivity is a design property (G6,
research/solid-trust-graph-authz-design.md §2.2).
- NO privacy / unlinkability / anonymity claim. Admission is the clear path: the
credential is admitted in the clear and the holder binding authenticates a clear WebID.
The ZK estate this layer shares a signature primitive with has no external
accredited-cryptographer sign-off (open audit gate sq-qhy4), and
sparq-mpc is
honest-majority semi-honest only. Nothing in this skill is a production security
guarantee.
- Framework trust is ANCHORED, not proven. A
trustx:Certification bottoms out in a
trust anchor you chose, not cryptography (design §7.2). The certifier's own key is
still operator-/DID-asserted — the live upstream forgery vector (D′, sq-pfae.3). The
closure defeats broadening and forged-edge escalation given honest anchor keys;
it does not close the key-trust gap above it.
- Attenuation-only is the HARD invariant. A derived rule that grants an issuer
authority its certifier does not hold is a privilege escalation, not a bug to paper
over. Where narrowing cannot be proven (undecidable SHACL-shape containment), the edge
contributes nothing.
When to reach for trust-graph vs plain WAC/ACP
- Plain WAC/ACP (
skills/access-control/SKILL.md) — the controller can enumerate the
agents/clients/issuers directly (acl:agent, ACP matchers). No sparq-trust needed.
- Trust-graph admission, direct anchors only — access should follow an
externally-attested fact ("age over 18, said by an issuer I trust for
age
statements"), and you can enumerate each trusted issuer yourself as a
trust:TrustRule. That path is documented in skills/access-control/SKILL.md
(the trust-graph feature's admission stratum) — this skill does not repeat it.
- The certification-edge closure (this skill) — you anchor a certifier (framework
operator / Trusted-List) rather than each issuer, and want issuers it certifies to be
admitted transitively (depth-1), automatically narrowed to what the certifier itself may
confer. The
trustx:trustsFramework mode of the trust-requirements vocabulary
("admit issuers certified under this framework, subject to scope conformance") is what
this closure operationalises pod-side.
Feature flags (all default-OFF, opt-in — the core stays lean)
sparq-trust = { path = "crates/sparq-trust", features = ["cert-graph"] }
sparq-solid = { path = "crates/sparq-solid", features = ["trust-graph"] }
sparq-solid = { path = "crates/sparq-solid", features = ["trust-graph-did"] }
The HTTP surface (sparq-server) exposes the same closure behind its own opt-in
solid-authz-trust feature: the request's JSON trust block may carry a
"certifications" array (wire schema in crates/sparq-server/src/solid_authz.rs), and
the handler runs derive_effective_rules(..., depth_bound = 1) ahead of admission,
reporting certGraphDerived in the decision JSON.
The anchor rule — trust:TrustRule is the ceiling
Anchors are the trust ROOT: only a certifier that already appears in the Control-gated
policy (as a rule trust:source with the matching key) can confer anything. The policy
graph is parsed by sparq_trust::parse_policy (possession of a ControlGate is the
authoring channel — same discipline as .acl/.acr). Reified authoring form
(namespace trust: = https://sparq.dev/ns/trust#):
@prefix trust: <https://sparq.dev/ns/trust#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
[] a trust:TrustRule ;
trust:source <https://gov.example/framework> ; # the certifier's identity IRI
trust:issuerKey "a1b2…" ; # its verification key, hex (or trust:issuerDid)
trust:forPredicate <https://schema.org/age> ; # statement-type CEILING (desugars to
# a single-predicate trust:forShape)
trust:scope <https://pod.example/medical/> ; # resource/container ceiling
trust:freshWithin "P30D"^^xsd:duration . # staleness ceiling
Field-by-field, each is a ceiling the closure can only narrow, never widen:
trust:source + trust:issuerKey — a certification is anchored only if its certifier
IRI and key both match a rule here (trust:issuerDid works too, behind the did /
trust-graph-did features).
trust:forShape / trust:forPredicate — the statement-type ceiling: a SHACL node-shape
(a forPredicate is desugared into a single-predicate shape). The derived rule's shape
is anchor.shape ∩ cert.scope.
trust:scope — the resource ceiling. A certification narrows WHO and WHAT-TYPE, never
widens WHERE: the derived rule inherits the anchor's resource scope unchanged.
trust:freshWithin (xsd:duration) — the staleness ceiling: the derived rule's
freshness is min(anchor.fresh_within, certification window remaining).
The equivalent claim-level relational form (trust:trustsSourceFor) desugars to the same
Vec<TrustRule> — see skills/access-control/SKILL.md for both authoring forms.
The certification bundle — trustx:Certification
A certification is a signed edge: an authority attests that an issuer is certified —
under a framework, over a scope, within a validity window. A Trusted-List entry or a
DIATF-register entry IS one such edge. The vocabulary (namespace trustx: — the same
https://sparq.dev/ns/trust# namespace, machine-readable form in
crates/sparq-trust/ontologies/trust/trust-framework.ttl):
@prefix trustx: <https://sparq.dev/ns/trust#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<urn:example:cert-1> a trustx:Certification ;
trustx:certifies <https://issuer.example/dvs> ; # the vouched-for issuer
trustx:underFramework <https://gov.example/framework> ; # the trustx:Framework
trustx:scope trustx:AnyServiceScope ; # or a SHACL shape / rulebook IRI
trustx:validFrom "2026-01-01T00:00:00Z"^^xsd:dateTime ;
trustx:validUntil "2027-01-01T00:00:00Z"^^xsd:dateTime .
What the closure actually consumes is the sparq_trust::graph::Certification struct —
the edge plus the two keys and the certifier's signature:
certifier / certifier_key — must match an anchor rule's source + issuer_key.
certified_issuer / certified_key — the derived rule's issuer identity + key. The
key is bound into the signed message, so a substituted key breaks the signature.
scope — CertScope::AnyService (the coarsest, service-level scope — the honest DIATF
granularity; imposes no statement-type narrowing of its own, the anchor shape stays the
ceiling) or CertScope::Shape(…) (the issuer is certified only for statements matching
that SHACL node-shape).
valid_from_unix_secs / valid_until_unix_secs — the inclusive window.
signature_hex — the certifier's Schnorr signature over the domain-separated
certification_message (domain tag "TRUSTCRT", distinct from delegation-hop /
commitment / holder-PoP tags). A graph merely claiming trustx:certifies proves
nothing: the signature is CHECKED, never taken on assertion, and it covers the scope +
window, so a narrow certification cannot be re-presented broadened.
The closure — derive_effective_rules → the unchanged admit
use sparq_trust::graph::{derive_effective_rules, Certification};
use sparq_trust::policy::TrustRule;
let effective: Vec<TrustRule> =
derive_effective_rules(&direct_rules, &certifications, now_unix_secs, 1);
Load-bearing properties (each adversarially tested in
crates/sparq-trust/tests/certification_graph_e2e.rs):
- Strict additivity — zero surviving certifications ⇒ the output is
direct_rules
byte-identical. The closure can only append.
- Depth-bounded — v1 is depth-1: derived rules are never re-used as anchors for a
further round;
depth_bound = 0 short-circuits to direct_rules verbatim.
- Attenuation-only — every derived rule satisfies
derived ⊆ (anchor ∩ cert scope ∩ validity window). Shape containment is
target-set-contravariant, conformance-covariant: a cert shape narrows the anchor
only when its SHACL selection/target predicates are a subset of the anchor's
(targets may only shrink — an extra sh:targetSubjectsOf WIDENS) and its conformance
constraints are a superset (constraints may only grow). Anything unprovable fails
closed.
Fail-closed reject reasons — EdgeRejection
derive_effective_rules silently drops a rejected edge; explain_edge runs the same
gates in the same order and returns why an edge contributed nothing (the adversarial
matrix asserts against these). The variants, in gate order:
| Variant | The edge is dropped because … |
|---|
Cyclic | certifier == certified_issuer (self-certification), or the certified issuer already anchors the certifier — a cycle could launder a broadening, and adds no authority. Checked FIRST, before the signature. |
NoAnchor | no direct_rules anchor has this certifier's source and matching key — the pod does not anchor this certifier, so it can confer nothing. |
SignatureInvalid | the signature is absent, unparseable, or does not verify under the certifier's key over the domain-separated certification_message (a self-asserted / forged edge). |
OutOfWindow | the window is ill-formed (valid_until < valid_from) or does not cover now — expired / not-yet-valid; fail-closed on time. |
Broadening | the certification scope is NOT provably contained in the certifier's anchor shape — a broadening attempt or an undecidable containment (undecidable ⇒ not contained ⇒ contributes nothing). |
OverDepth | depth_bound was 0, or the edge would only be reachable beyond the bound (v1 depth-1: derived rules are never anchors). |
Pod-side wiring — sparq-solid (trust-graph feature)
With trust-graph ON, PodStore gains the admission-install methods that put derived
grants into <urn:sparq:auth> on top of the unchanged WAC/ACP view (grants only union
allows; a trust-graph grant can never drop a WAC/ACP grant). Two install paths — choosing
the wrong one is a soundness bug (the sq-xc4y static/dynamic split):
PodStore::admit_trust_credential_static — the materialise-time path: runs only the
session-independent class (admit_static) and installs auth:ConditionalGrants whose
holder + freshness are re-checked per request. Use this for a long-lived
materialise-once view.
PodStore::admit_trust_credential_and_materialize (and _with_rule) — the
single-request snapshot path: runs the combined gate against one live Session and
installs an unconditional grant valid for that request only. Never use it to populate
a long-lived view.
Feed effective_rules from the closure to either path as the rules argument; admission
itself never errors — a credential that fails any gate simply admits nothing
(fail-closed, default-deny).
Cross-references
skills/access-control/SKILL.md — the WAC/ACP layer, the admission stratum, both
anchor-authoring forms, the TrustStore narrowing model (this skill deliberately does
not duplicate it).
skills/verifiable-credentials/SKILL.md — the standards-interop signed-graph
complement; skills/zk-query-proofs/SKILL.md — the (externally unaudited, sq-qhy4)
ZK estate.
crates/sparq-trust/README.md + the sparq_trust::graph module docs — the full
invariant discussion; crates/sparq-trust/tests/certification_graph_e2e.rs — the
adversarial edge-forgery matrix.
research/trust-expression-spec.md §3.4 and
research/solid-trust-graph-authz-design.md — the design records (epic sq-pfae,
issue #940).