Skip to content

Index

SEDI Identity Credential (sedi-id)

Purpose

sedi-id is the root credential of Utah's State-Endorsed Digital Identity (SEDI, Utah Code Title 63A, Chapter 20, enacted by SB 275, effective 2026-05-06). It is what the State — through the Department of Government Operations (DGO) — issues to a resident after identity proofing: a cryptographically verifiable assertion of the resident's core civil identity, bound to an identifier the holder created and controls.

SEDI's founding premise inverts the usual state-ID relationship: identity is "innate to the individual's existence and independent of the state… fundamental and inalienable" (§63A-20-101(1)). The state endorses an identity; it does not create one. So sedi-id's issuer field names the State, but the subject is bound by the holder's own personal digital identifier (a KERI AID) — withdraw the endorsement and the identity persists, because the holder, not the state, holds the key.

This credential is not a driver's license and not a general attribute bag. The statute lets the department verify and endorse exactly four attributes — name, birth date, image, and Utah residence address (§63A-20-301(2)(f)) — and forbids collecting anything unnecessary (§302(7)). sedi-id carries those four and nothing more; everything richer (age thresholds, eligibility, presentations, guardianship) is a separate credential that chains to this one. See the family.

Why an attribute (a) section — not an aggregate

sedi-id carries its attributes in a partially-disclosable attribute (a) section, where each attribute is its own nested, individually-blinded block (own SAID d and blinding nonce u). A holder discloses any subset by revealing those blocks and leaving the rest as bare block SAIDs; withheld values never cross the wire.

An earlier draft used the aggregate (A) section (mDL-style). It was reversed on first-principles grounds (see PR WebOfTrust/keripy#1505 and this.i @sdav5t): the only thing an aggregate buys over an attribute section is that its top-level blocks carry no meaningful labels, so block position can be arranged to not leak information. That helps only when a field's presence/identity/position is itself sensitive, or the field set is variable. The core identity credential is a fixed set of heterogeneous, meaningfully- labeled fields — nothing benefits from hiding labels — and it is the most-used credential, so the aggregate's real cost bites: you can no longer path to a field by label, so graduated-disclosure negotiation (apply/offer path lists) would need a label→index reverse-lookup, or you put the index in the path and defeat the very unlinkability the array was for. Labels are the right handle here; the aggregate is the right choice for the age credential instead.

The attributes

The a section holds the issuee (a.i = the holder's AID) plus one nested block per attribute:

Block Field(s) Statutory basis
Given-name nameGiven name — §301(2)(f)
Family-name nameFamily name — §301(2)(f)
Date-of-birth dob (date) birth date — §301(2)(f)
Image image = {digest, format, dim, captured} image — §301(2)(f)
Face-template faceTemplate = {digest, protocol} optional, consented dedup (not endorsed identity)
Residence-street residenceStreet (whole line) Utah residence address — §301(2)(f)
Residence-city residenceCity "
Residence-state residenceState "
Residence-postal residencePostal "
Residence-county residenceCounty (optional) "

The holder AID lives at a.i, the natural issuee location for an attributive ACDC — this is what a sedi-id → X chain checks (an I2I edge holds only when the chaining credential's issuer equals this credential's a.i). Note that attribute-form partial disclosure reveals a.i and the a-section metadata on any disclosure; for SEDI that is fine, because the holder binding is normally wanted.

Name and residence are decomposed — deliberately

Both name and the residence address are split into separate nested blocks, not carried as one lump or a single object — and decomposed is the sole production shape. Because a block discloses atomically, a single residence block would force over-disclosure on every verifier who only needs jurisdiction, making the statute's minimization duty (§501, §701) un-satisfiable at the schema level. "Prove Utah resident" (voter eligibility, in-state tuition, tax/jurisdiction, alcohol) is the most common residence predicate, and withholding the street line protects exactly the Title-20A withheld/at-risk-voter classes (DV victims, LEOs, protected persons). Not decomposing would also make SEDI a privacy regression from the ISO 18013-5 mDL it replaces.

Grain: keep the street line whole (residenceStreet) rather than parse it (PO boxes, rural routes, non-US formats are brittle; the useful predicates live in the clean jurisdiction fields); split only city/state/postal/county; go no finer. Naming is semantic interop, not field-name interop: the labels use Utah's statutory term "residence" (residenceCity, …), not ISO mDL's "resident" — align the component semantics with 18013-5 so a bridge crosswalk is mechanical, but keep the statute's vocabulary. Labels are flat and dot-free on purpose (a dotted key like residence.street signals a nesting the model doesn't have and is a cross-tooling footgun in jq/JSONPath/JS/MongoDB).

Image: committed by digest, bytes holder-carried

The image block carries a content digest plus minimal metadata (format, dim, captured); the actual portrait bytes are holder-carried and attached to a presentation under chain-link confidentiality, verified against the digest. A download locator was rejected (it breaks mandatory offline presentation §301(1)(d), is phone-home by another name §301(3)/§701, and creates an availability chokepoint), and inlining raw bytes was rejected as bloat. There is one canonical portrait (ISO/IEC 19794-5-grade) — no resolution ladder. The optional faceTemplate block is a different artifact: a cancelable/biohashed (ISO/IEC 24745-style) template for consented 1:N dedup, carrying a named biometricProtocol and disclosed only under a correlation-consent rule.

Selective (partial) disclosure — "prove Utah residence without the street"

To prove residence at jurisdiction grain, the holder reveals the residence-city/state/postal blocks and leaves the street, name, DOB, image, etc. as bare SAIDs. Illustratively, the disclosed a section looks like:

"a": {
  "d": "…", "u": "…", "i": "<Alice's AID>",
  "nameGiven":       "<block SAID>",           // withheld
  "nameFamily":      "<block SAID>",            // withheld
  "dob":             "<block SAID>",            // withheld
  "image":           "<block SAID>",            // withheld
  "residenceStreet": "<block SAID>",            // withheld — the street never appears
  "residenceCity":   { "d": "…", "u": "…", "residenceCity": "Salt Lake City" },
  "residenceState":  { "d": "…", "u": "…", "residenceState": "UT" },
  "residencePostal": { "d": "…", "u": "…", "residencePostal": "84101" },
  "residenceCounty": "<block SAID>"             // withheld
}

The verifier hashes each disclosed block, substitutes the withheld SAIDs, and recomputes a.d to confirm the disclosure is authentic. (This is shown as prose rather than a SAID-checked gallery file because the repo's pinned keri 1.2.13 oracle does not compact nested blocks — see the SAID note.)

Schema

See sedi-id.schema.json. The a section is oneOf(section SAID, section detail); the detail requires the endorsed attributes, each a nested block that is itself oneOf(block SAID, block detail) — so any subset can be disclosed as detail while the rest stay bare SAIDs.

Worked example

example.json is the fully-disclosed issued credential (all attributes present) — a private variant (top-level u), a version-stamped v1 ACDC issued by the State to the holder Alice. The invalid/ corpus rejects: a top-level extra property, a missing/a-section-less credential, a missing issuee, a missing required attribute, a block with an extra property, a block missing its blinding nonce, a non-date dob, an image missing its digest, and a non-string residenceState.

A note on SAIDs

Because sedi-id is a plain attributive v1 ACDC, this repo's keri 1.2.13 oracle version-stamps and SAIDs it fully — unlike the aggregate sedi-age, it carries a real version string and a canonical d. One residual: keri 1.2.13 does not compact nested partial-disclosure blocks, so a partial-disclosure form's a.d/d differ from the full form's (a v2 stack computes the disclosure- invariant compact SAID). That is why the residence disclosure above is illustrative prose, and the machine-validated selective-disclosure gallery lives on sedi-age, where the AGID is stable across disclosures. See this.i @sdav5t / @sd2qfw.

The SEDI family

sedi-id is the root; the other members chain to it: sedi-age (the aggregate boolean age-threshold vector), sedi-present-age-portrait (the first holder-issued presentation pattern), and — on the roadmap — sedi-guardian (digital-guardian delegation) and sedi-bridge (foreign-credential reissuer). See this.i @sd4rkp.

Governance

SEDI's duty-of-loyalty and minimization obligations (§701, §702, Parts 4–6) are carried as the family-wide SEDI governance framework in rules.json (this root credential's folder is its home), referenced by SAID from every SEDI credential's r. Issuing or accepting a SEDI credential is binding acceptance of it.

Provenance and further reading

Design decisions live in this.i (@sd4rkp and children; the attribute-vs-aggregate reversal is @sdav5t). The reusable principle is distilled in docs/choosing-attribute-vs-aggregate.md. The full research synthesis is in the bakobo/sedi repo (sedi/artifacts/sedi-schema-synthesis.md); the executable reference for the disclosure + chain-link-confidentiality mechanics is keripy's tests/acdc/test_clc_disclosure.py.