Skip to content

Add SEDI guardianship represented-presentation worked example - #1530

Draft
dhh1128 wants to merge 6 commits into
WebOfTrust:mainfrom
dhh1128:feat-sedi-guardianship-example
Draft

Add SEDI guardianship represented-presentation worked example#1530
dhh1128 wants to merge 6 commits into
WebOfTrust:mainfrom
dhh1128:feat-sedi-guardianship-example

Conversation

@dhh1128

@dhh1128 dhh1128 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

This PR adds a worked example of a a digital guardian presenting a fact about a ward (a person who cannot fully consent) to a verifier, under SEDI). It is part of a set of changes, and is an example whose holder is not the subject, a sibling to test_cp_disclosure.py and test_examples.py.

Scenario. Bob, a custodial parent, proves to an age-restricted online service that his 14-year-old ward Cara is over 13 — without disclosing her birthdate, which other age thresholds exist, or any ward attribute beyond the predicate — while the service can prove afterward that a guardian (not the child) acted.

Three edge operators are interesting in this scenario:

  • I2I (authority): presentation → Bob's guardian credential — proves the presenter holds the authority.
  • NI2I (ward data): presentation → Cara's identity/age credentials — Bob is not the subject; an I2I ward edge would be impersonation.
  • E1E (identity): Cara's age → her identity credential — same subject, issuer != issuee — consuming the operator added in Add E1E identity edge operator to credential chain verification #1527 over its aggregate far-node path (.iseaidA[1].i), end to end.

The code demonstrates: guardian-mediated aggregate predicate disclosure (unlinkable over-13, birthdate withheld), holder ≠ subject with a gated IPEX exchange, blindable-registry dynamic revocation, and a verifiable accountability chain back to an accountable human — things a document-centric mDL/EU-ARF flow cannot express.

The credential shapes are minimal ports of the draft sedi-guardian family in bakobo/schema. One divergence worth a decision: the draft uses ri for the registry field, while keri v2 (acdcmap/acdcagg) emits rd — this example uses rd (ground truth). Candidate revision to the bakobo first-cut schema.

Rebased onto main after #1527 merged; the diff is now the worked example alone.

@dhh1128
dhh1128 force-pushed the feat-sedi-guardianship-example branch from ce21e46 to b998147 Compare July 30, 2026 00:10
@dhh1128 dhh1128 changed the title Add SEDI guardianship represented-presentation worked example (stacked on #1527) Add SEDI guardianship represented-presentation worked example Jul 30, 2026
@dhh1128
dhh1128 force-pushed the feat-sedi-guardianship-example branch from b998147 to e2e98ad Compare July 30, 2026 00:12
A worked, working ACDC v2 example (tests/acdc/test_guardianship_presentation.py)
of a *represented presentation* -- a digital guardian presenting a fact about a
ward under Utah's State-Endorsed Digital Identity law. It is the first example in
the set whose HOLDER IS NOT THE SUBJECT, and it stacks on the E1E identity operator
(PR WebOfTrust#1523), consuming its aggregate-far-node path end to end.

Scenario: Bob, a custodial parent, proves to an age-restricted online service that
his 14-year-old ward Cara is over 13 -- without disclosing her birthdate, which
other age thresholds exist, or any ward attribute beyond the predicate, and while
the service can prove afterward that a guardian (not the child) acted.

Five phases, strict-TDD, each ACDC validated against a purpose-authored JSON Schema
(Draft 2020-12) from its first commit:

  * Phase 1 -- the ward's attributive sedi-id and aggregative sedi-age, chained by
    an E1E identity edge (same subject, issuer != issuee); selective disclosure of
    the over-13 predicate with the birthdate and other thresholds withheld.
  * Phase 2 -- Bob's disclosed-whole sedi-guardian authority credential, holder !=
    subject (issuee = Bob; the ward named only by a NI2I subject edge), grounded in
    a birth-certificate authorization edge.
  * Phase 3 -- the represented presentation with three pinned edge operators (I2I
    authority, NI2I ward-data, E1E identity) and a five-point verifier binding. The
    schema const-pins make the impersonation SHAPE unrepresentable (a mislabeled I2I
    ward edge is rejected); the holder != subject GUARANTEE is enforced by the
    verifier binding logic plus the issuee field and signature, not by the operator
    alone. A gated IPEX exchange discloses the over-13 flag only in the grant.
  * Phase 4 -- dynamic revocation via a blindable registry (state word blinded;
    issued honored, revoked refused) and provable accountability.
  * Phase 5 -- the invariants across JSON/CESR/CBOR/MessagePack.

Uses the disclosure section each credential fits, per the resolved aggregate-vs-
attribute criterion: aggregate for the homogeneous age-threshold vector, attribute
for the fixed labeled identity and authority fields.

Docstrings reviewed by the keri-review-panel and revised to scope the claims
precisely: the operators mark intent/shape while the app-layer binding enforces the
invariant; E1E is a keripy addition not yet in the ACDC spec operator set (rides
WebOfTrust#1523); attribute minimization is achieved but identifier-level cross-verifier
unlinkability needs deployment measures (per-facet AIDs / bulk issuance); the
blindable registry blinds the state word, not the revocation timing; and the
pre-agreement offer withholds issuer-committed source SAIDs.

Signed-off-by: Daniel Hardman <daniel.hardman@gmail.com>
@dhh1128
dhh1128 force-pushed the feat-sedi-guardianship-example branch from e2e98ad to 56849f2 Compare July 30, 2026 03:47
@dhh1128

dhh1128 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Updated: rebased onto current main (now including #1529) and converted the IPEX request to the dp construct settled in discussion #1549 — matching #1561, which does the same for the two already-merged examples.

  • disclose (attribute section, dict keyed by schema SAID) → dp in the query section (exchange(modifiers=...)), holding an ordered list of (schemaSAID, [paths]) pairs.
  • Paths are ACDC-relative, so no leading / — under Revised Disclosure Paths `dp` field value syntax #1549 a leading / roots the path at the DAG origin, which would make /a/i name the origin's issuee rather than the credential the tuple names.
  • The solicited offer now carries dp: [], meaning "the same paths the apply asked for".

The represented-presentation case is the one where the joining paths carry real weight: the service asks for the issuee from both the guardian credential (a/i, who is acting) and the ward's age credential (A/i, whose fact is asserted), and those two AIDs are deliberately different. It is the guardianship edge, not issuee equality, that licenses Bob to speak for Cara — so the request has to name both and the verifier has to check the edge rather than compare the two issuees.

Same open question as #1561, flagged in a comment at the call site: #1549 says the zeroth tuple MUST be the DAG origin, but a first-contact apply cannot name the bespoke presentation's schema SAID (#1512). Origin tuple omitted for now; @SmithSamuelM, this is the question still open on #1542.

Still a single commit adding a single file. tests/acdc/ is green (54 passed). The apply/offer SAIDs moved, and the agree/grant/admit SAIDs that chain off the offer via p moved with them.

dhh1128 added 2 commits August 4, 2026 16:11
Picks up the KWA NameError fix (WebOfTrust#1562) that was breaking tests/vdr/test_verifying.py
on this branch's stale base.

Signed-off-by: Daniel Hardman <daniel.hardman@gmail.com>
Tracks the sibling change on the CLC and bulk-issuance examples, so this
example lands already carrying the settled construct rather than needing a
follow-up.

The `_` component (WebOfTrust#1549, 2026-08-02): a DAG-absolute path reaching a
non-origin ACDC must cross the edge that links it, written as the virtual
component `_`. No path data changes -- every path here is ACDC-relative and
none traverses an edge -- but the comment now records the absolute rendering
of this DAG, whose three edges make it the clearest of the three examples to
read: /e/authority/_/a/i, /e/wardAge/_/A/over13.

The zeroth origin tuple: WebOfTrust#1549 says the zeroth `dp` entry MUST be the DAG
origin. This example previously omitted it on the grounds that a first-contact
applicant cannot name a presenter-issued origin's schema; @SmithSamuelM
answered that on WebOfTrust#1542 -- the origin is presenter-issued but its shape is
governance, so the applicant does know it. The service now asks the origin for
issuer and issuee, which in a represented presentation is the first place the
holder != subject split is visible: the origin's issuer is Bob, and the ward
does not appear in it at all.

The five exn SAIDs move as a result; the apply -> offer -> agree -> grant ->
admit cascade is the `p` binding working, and each was re-derived by running
the example.

The field-label conformance guard that comes with the same discussion (no `-`
in a label, no label that is exactly `_`) lands on WebOfTrust#1561 rather than here, to
keep it in one place.

Test-only; no product code changes.

Signed-off-by: Daniel Hardman <daniel.hardman@gmail.com>
@dhh1128

dhh1128 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto the current branch head and updated to match #1561, which took two changes @SmithSamuelM settled after both PRs were opened.

The apply's dp list now carries the zeroth origin tuple. #1549 says the zeroth element MUST represent the DAG origin; I had omitted it here and on #1561 on the grounds that a first-contact applicant cannot name a presenter-issued origin's schema. #1542 answers that — the origin is presenter-issued but its shape is governance, so the applicant does know it. In a represented presentation the origin entry earns its place twice over: asking it for issuer and issuee is the first point at which the holder != subject split is visible, since the origin's issuer is Bob and the ward does not appear in it at all.

The _ DAG-hop component from #1549 changes no path data, since every path here is ACDC-relative and none crosses an edge. The comment now records the absolute rendering, which with three edges is the clearest of the three examples to read: /e/authority/_/a/i, /e/wardAge/_/A/over13.

The label-conformance guard that comes with the same discussion lands on #1561 rather than here, to keep it in one place. Both restrictions are proposed for the spec in trustoverip/kswg-acdc-specification#202, and dp itself in trustoverip/kswg-acdc-specification#203.

Five exn SAIDs moved through the p chain and were re-derived by running the example. All 8 tests pass on the rebased branch.

dhh1128 added 2 commits August 4, 2026 23:22
Utah has decided not to depend on a separate GCD (Generalized Cooperative
Delegation) credential for guardianship, so the optional 'scope' edge that
reached one is removed and the edge section is closed at subject +
authorization. A new negative asserts that any other edge is now rejected,
which makes the removal a property the suite holds rather than an absence.

Removing a property from the schema map moves the schema's own SAID, which is
embedded in the credential's 's' section, so the guardian credential SAID moves
and the cascade reaches the presentation, the blinded registry state, and the
apply/offer/agree/grant/admit chain linked by 'p'. Ten asserted digests were
re-derived by running the example.

The module docstring records why GCD is absent, since a reader coming from the
bakobo/schema family will otherwise wonder where the two-layer factoring went,
and notes what follows: 'powers' is now the whole scope vocabulary, which is
coarse, and a guardianship needing more carries it inline (discussion WebOfTrust#1550).

The identifier-unlinkability residual now cites the two bulk-issuance worked
examples rather than promising them, names the independent-registry variant as
the one Utah intends, and adds that the guardian's own AID needs partitioning
under bulk issuance or two verifiers can join on it to re-link the ward.

Signed-off-by: Daniel Hardman <daniel.hardman@gmail.com>
The example claims the service learns an over-13 predicate and not Cara's
birthdate, and then shipped her birthdate anyway. The guardian credential is
disclosed WHOLE, and its effectiveDate was the date the parental right arose,
which for a custodial parent is exactly the ward's date of birth -- 2012-04-10
in both places. effectiveDate is now the date the State recorded the
relationship, which is what a verifier needs (when this authority became
checkable) and is not derived from the ward.

The remaining leak is structural and is now asserted rather than hidden. A
minor guardianship expires at majority, so expiryDate is the ward's 18th
birthday and a verifier can subtract 18 to recover her birth month and day.
Nothing in the represented-presentation shape fixes that; mitigations are
deployment-level, either coarsening the date or carrying validity solely in the
registry. The test asserts the residual is present so it cannot regress into
being forgotten, and WARD_DOB is named once so the check is real rather than a
comparison of two literals that could drift apart.

Found while building the ward-presents sibling example, which had the same bug
for the same reason.

Signed-off-by: Daniel Hardman <daniel.hardman@gmail.com>
The disclosure-paths `dp` field of an IPEX apply's query section is an
ordered list of tuples. Each tuple gains a middle element, so it goes from
(schemaSAID, [paths]) to (schemaSAID, prefix, [paths]).

The new middle element is a path prefix: the DAG-absolute route to the ACDC
that the entry's schema SAID names. It is either the empty string or a route
that both begins and ends with '/', and the effective path is prefix + entry
concatenated with no delimiter inserted, so an entry in the path list never
begins with '/'.

Every prefix is empty in this example. That leaves the entry paths
ACDC-relative, exactly as they were before this change, so an entry's ACDC is
identified by the entry's position in the list -- breadth-first from the
origin node, with the origin at index zero. That ordering is required even
where a non-empty prefix would make it technically unnecessary. The
disclosure semantics are unchanged: same schema SAIDs, same order, same paths.
The solicited offer still carries an empty `dp` list and takes no prefix.

The apply's serialization changes, so its pinned SAID changes, and with it the
SAIDs of the offer, agree, grant and admit that chain onto it.

Proposed by @SmithSamuelM in
WebOfTrust#1549 (comment)
and confirmed, including the retained ordering requirement, in
WebOfTrust#1549 (reply in thread)

Signed-off-by: Daniel Hardman <daniel.hardman@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant