Skip to content

Index

SEDI Age Attestation (sedi-age)

Purpose

sedi-age is a derived, state-endorsed age attestation in Utah's State-Endorsed Digital Identity family (Utah Code Title 63A, Chapter 20). It satisfies one specific statutory requirement directly: a holder must be able to "prove a minimum age without disclosing age or birth date" (§63A-20-301(1)(e)). Rather than reveal the dob block of the sedi-id root, the holder presents this credential and discloses just the age threshold(s) a verifier needs.

The headline use case is alcohol/tobacco age verification (§63A-20-204(2)(a)): the vendor learns only "21 and over: true" — not the birth date, not the exact age, not the name or address.

Why an aggregate (A) section — a boolean threshold vector

sedi-age carries a selectively disclosable aggregate (A) section: an array of individually- blinded blocks — the issuee, an as-of date, and one boolean flag per age threshold (ageOver13, ageOver16, ageOver18, ageOver21, ageOver55, ageOver65). Element 0 is the AGID, a digest committing to every block's SAID. The holder reveals only the block(s) needed (e.g. ageOver21 for a bar) and leaves the rest as bare, per-block-blinded SAIDs; the verifier recomputes the AGID to confirm authenticity.

This is the right use of an aggregate — and the mirror image of sedi-id, which is attributive (see PR WebOfTrust/keripy#1505 and this.i @sdav5t). The age thresholds are a homogeneous boolean vector: they are really an indexed numeric series, not a set of meaningfully-distinct labeled fields. Treating them as an unordered, blinded set is exactly the ISO mDL age_over_NN model, and it leaves room for the planned upgrade to a sparse Merkle tree with no schema change — "a poor man's sparse Merkle tree." Here the aggregate's blinding of block labels/positions is a genuine (if modest) fit, whereas for the labeled identity fields it would only cost pathing clarity.

One schema serves every threshold; the set here (13, 16, 18, 21, 55, 65) covers the common minimum-age and senior thresholds and is easy to extend. asOf records the point-in-time posture of the attestation (§63A-20-303: verification at a point in time, no continuous monitoring) — the analogue of the mDL rule that age_over_NN is computed at the MSO's validFrom.

Why there is no edge to sedi-id

An I2I edge from sedi-age to sedi-id would hold only if the issuer of sedi-age equalled the issuee of sedi-id — i.e. only if the holder self-issued the age attestation, which would carry no state endorsement. So sedi-age carries no edge to the root. The same-holder binding is established at presentation time by a holder-issued presentation whose I2I edges point at both source credentials (the holder is the issuer there, and the issuee of both) — see sedi-present-age-portrait.

This pre-derived boolean is a genuine zero-knowledge alternative to a predicate proof: it keeps the birth date out of the transaction entirely, with no special cryptography. A ZK-predicate variant is a possible future profile the statute permits.

Schema and examples

See sedi-age.schema.json. A is oneOf(AGID string, uncompacted array); the array is items+anyOf over the block types (issuee, as-of, and one per threshold), each oneOf(block SAID, block detail).

  • example.json — the fully-disclosed issued credential (all six thresholds), issued by the State to the holder Alice as of a fixed date. Alice (born 2000) is over 13/16/18/21 and not over 55/65.
  • examples/prove-over-21.json — the headline selective disclosure: the holder reveals only issuee + asOf + ageOver21, leaving every other threshold (including the false ones) as a bare, blinded SAID. The verifier learns "over 21: true" and nothing about the holder's exact age band. The disclosure verifies against the committed AGID.

The invalid/ corpus rejects: a top-level extra property, a missing A, a mistyped A, a block with an extra property, a block missing its blinding nonce, a non-boolean ageOver21, and a malformed asOf.

The AGID and per-block SAIDs are authentic, computed with the v2 keri Aggor (the same primitive in keripy's tests/acdc/test_clc_disclosure.py). Because it is a v2 aggregate credential, the repo's pinned keri 1.2.13 oracle cannot version-stamp it, so the examples are unversioned and the linter checks top-level SAID consistency and schema validity rather than recomputing the AGID (see this.i @sd2qfw). The attributive sedi-id does not have this limitation.

Governance

As with the rest of the family, the duty-of-loyalty / minimization obligations travel as the shared SEDI governance framework (sedi-id/rules.json), referenced by SAID from r. Issuing or accepting this credential is binding acceptance of that framework.