generate aliases for opaque identifiers
Supersedes COIA 1.x. See CHANGES for what moved and why.
Opaque identifiers — DIDs, KERI AIDs, SSH and PGP keys, payment addresses, UUIDs, passkeys, account numbers — are hard for people to remember, speak, compare and search. Management tools paper over this with a free-text label field and offer no guidance about what to put in it. Users invent ad hoc conventions or none at all.
COIA is a convention for that label. It is not an identifier, not a namespace, and not a protocol.
COIA aliases look like this:
cecilia-second-violin-vienna-symphonymoi-directeur-général-l-oréal — reflexive: me, as CEO at L’Oréalhans-müller-buchhalter — no scope, because none is interesting hereトヨタ-購買者-サプライチェーン,0 — Toyota, purchaser, supply chain, flagged unverifiedعلي-شريك-تجاري,1 — Ali, business partner, flagged as intended for one relationshipbob-payee-bitcoin,9 — flagged compromised, so it is shown but not usedEvery one of those is real output from the reference implementation, not an illustration. You can generate your own in the browser, or run any of the six ports — the oracle in python, plus javascript, go, java, rust and swift. All six are held byte-for-byte to the same vectors; see IMPLEMENTATIONS.
The three fields are always the same three, in the same order, in every language: who, role, scope (§4.1). Anything after a comma is a flag (§6) — a safety-relevant warning about the identifier, not part of the label.
Flag placement and the template both changed in 2.0:
0-cecilia-as-ceo-at-acme <- COIA 1.x
cecilia-ceo-acme,0 <- COIA 2.0
An alias improves UX for the person who creates it. It is not a commitment to meaning for anyone else. An alias may change at its creator’s whim, is unresolvable outside the creator’s context, and parsing someone else’s alias for strong meaning is a dangerous antipattern.
This specification defines:
It does not define: how aliases are stored, whether generation inputs are retained, how collisions between identical aliases are resolved, or how an application ranks results beyond the default order in §7.
G1 — intuitable. Given a handful of examples and no explicit instruction, a user develops accurate intuition about how the conventions work.
G2 — memorable. Encountering an alias they created, a user immediately remembers who is identified and in what context.
G3 — predictable. Given a context and an actor, a user correctly predicts what a relevant alias might look like, and can therefore search with confidence.
G4 — resident. An alias is usable as-is in the places aliases live: the label or comment field of a wallet, password manager, config file, or certificate wizard; and the search box that finds it again.
G5 — durable in transit. If an alias is copied elsewhere — an email, a chat message, a word processor, a log file, a shell, a spreadsheet, a Markdown document — it survives as a single token, and it can be read aloud with high confidence.
G5 is a robustness claim, not an endorsement. §1 says aliases are not made to be reshared. G5 says that when one is copied anyway, it should not be mangled.
Known limitations of G5. U+002D HYPHEN-MINUS has line-break class HY (UAX #14), so a renderer may break an alias at any hyphen, and double-click word selection does not select the whole token. Aliases MUST NOT be used as DNS labels when flagged (§6.6), nor as unescaped CSS or XML identifiers. In scriptio-continua languages (Chinese, Japanese, Thai) an alias is a single unsegmented token and the read-aloud clause of G5 does not apply.
An implementation MAY conform as any subset of:
Every MUST in this specification binds one of these three classes. A requirement that constrains a human’s judgment, a user interface, or third-party software is written as guidance, not as a normative keyword.
The golden vectors in Appendix C are normative. Where prose and vectors disagree, that is a defect in this specification; report it.
Software often knows all three when it helps a user create or accept an identifier.
For a reflexive alias — one whose subject is the creator — the caller supplies a
distinguished sentinel rather than a name, and the generator substitutes the pronoun for the
chosen language (Appendix A.2). A generator MUST NOT use the empty string as
this sentinel: a caller that omits who by accident would otherwise mint a reflexive alias for
a third party’s identifier.
A generator MUST reject a language it does not support rather than falling back to another. Templates and their per-language rules are in Appendix A, which is normative for the Generator class.
Substitution MUST be a single simultaneous pass. Substituted values MUST NOT be rescanned,
and MUST NOT be interpreted as replacement patterns. A who of {role} produces the
literal text; a who containing $& produces those characters.
A generator MUST evaluate preconditions on normalized values, not raw input:
role MUST be non-empty after normalization. (A raw role of . normalizes to the
empty string and MUST be rejected, even though the raw value is non-empty.)who MUST be non-empty after normalization, unless the alias is reflexive.A generator’s output MUST satisfy all of:
normalize(alias) == alias — normalization is idempotent on a generated alias.Normalization maps any string to canonical form. Step order is normative.
strings.ToLower
applies simple mapping (İ → i rather than i + U+0307) and omits the
Final_Sigma rule; Java’s toLowerCase() is locale-sensitive and yields ıstanbul
under tr_TR; and no lowercase operation expands ß to ss.Cf, emit it only if §5.1 licenses it, else nothing;Me, emit nothing;Mn or Mc, emit it only if the previous emitted
character was a letter, number or mark, else nothing;L* or N*, emit it, applying the folds in
Appendix B.4;The character policy is therefore an allowlist: letters, numbers and orthographic marks
are kept,
a listed set becomes separators, and everything else is discarded. This is deliberate.
Naming Unicode properties directly is not portable — the Rust regex crate rejects
\p{Cs}, and neither Go nor Java can express \p{Dash} — and a property-based
definition makes the alias a function of each runtime’s Unicode version.
Modifier letters (Lm) are kept. NFKC already folds the phonetic and superscript
modifiers; what survives is orthography — the katakana prolonged sound mark, kana and
ideographic iteration marks, the okina, and tone and length marks in Miao, Lisu, Ol Chiki,
N’Ko and others.
Combining marks are kept, except enclosing marks (Me). The thirteen Me characters are
enclosing circles, keycaps and Cyrillic liturgical number signs — decorative or symbolic,
not part of any orthography, and the keycap is an emoji component that would otherwise
survive where every other emoji is discarded. Deleting the other marks destroys Yoruba and Igbo
tone marking and every Indic and Southeast Asian script, while protecting nothing: NFKC
recomposes European
diacritics into single codepoints before this step runs, so José and João are
unaffected either way.
Compatibility decompositions are accepted as-is. NFKC turns 94 letters into base plus mark. All 94 are Unicode composition exclusions, so no recomposition pass restores them, and this specification does not attempt one. Both spellings converge, so a user typing either finds the alias.
U+200D ZERO WIDTH JOINER and U+200C ZERO WIDTH NON-JOINER are Cf, and are discarded
except where orthography requires them. Modelled on IDNA2008 CONTEXTJ (RFC 5892 A.1, A.2):
This preserves Sinhala ශ්රී, Devanagari conjuncts, Malayalam chillus, and Persian and
Urdu word-internal breaks, while discarding a bare joiner between Latin letters, which
would otherwise produce two aliases that render identically.
The Arabic clause is an approximation. RFC 5892 tests Joining_Type, which is unreachable from Go and JavaScript regular expressions and would add several hundred table entries. It over-permits slightly, within Arabic-script text only.
A flag qualifies the alias assertion: that the identifier is controlled by the party
the creator calls who; that this party acts in the capacity role; and that it does so
within the context scope. A flag qualifies all three components, not only the subject.
alias := body [ "," group1 [ "," group2 ] ]
group1 holds registry digits (§6.3). group2 is private use. Each group is a run of
ASCII decimal digits. A generator MUST emit at most two groups.
Both groups MUST have duplicates collapsed and MUST be sorted in descending numeric order, so that the most serious flag appears first. (COIA 1.x sorted ascending. See CHANGES.)
An empty group1 with a populated group2 is written body,,digits. Canonical form
omits trailing empty groups: body,, is not canonical and normalizes to body.
Because normalization (§5) discards all punctuation, a comma in a generated alias is always a delimiter and never content.
A matcher or reader MUST split flag groups off before normalizing the body. The reverse order destroys the delimiter.
On input, a reader MUST accept U+3001, U+060C and U+FF0C as group delimiters in addition to U+002C, so a user retyping an alias on a CJK or Arabic keyboard is understood. (U+FF0C folds to U+002C under NFKC in any case.) A generator MUST emit U+002C.
Similarly, a generator MUST emit ASCII digits, folding any Unicode decimal digit to its ASCII equivalent; a reader MUST accept any Unicode decimal digit.
0 unverified doubt about the alias assertion is unresolved
1 pairwise for one relationship only; do not share
2 — reserved
3 — reserved
4 unfit technical posture weak for high-stakes use, and not yet accepted
5 second-hand imported, restored, synced, or accepted from another party
6 test throwaway, test, demo; no real-world consequence
7 do-not-use the creator has decided not to transact
8 retired no longer in service; historical references still resolve
9 compromised positive evidence that the wrong party controls it
Full definitions, including what sets and clears each flag and what an application must decide for itself, are in Appendix D.
Digits are ordered by seriousness, ascending. This ordering carries meaning: a reader encountering an unrecognized digit MAY use its position as a severity hint.
Absence is never a guarantee. For every flag, absence means only that the flag was not set; it never asserts the negation. An application MUST NOT render an absent flag as a positive assurance.
A generator MUST NOT emit a reserved digit. A reader encountering an unrecognized digit in
group1 MUST surface it rather than ignore it — it is a warning from a later version of
this specification. A reader MUST NOT interpret group2.
Reflexive aliases MUST NOT carry 0.
Setting and clearing are decisions, not observations. Clearing 0 does not assert that
evidence exists; it records that a person, or software acting under that person’s policy,
judged the remaining risk not worth tracking.
4 is set by software from facts it can derive, and cleared by a deliberate acceptance of
the identified weakness. Because those facts remain true after acceptance, software
re-checking them will observe them again. An implementation MUST have a policy governing
when 4 is re-set. This specification does not define one; see Appendix D.4
for a non-normative example.
4 does not travel between applications: it is the only flag whose presence depends on
local policy rather than on facts about the world.
Facts an application can derive from an identifier’s own record — witness count, signature threshold, delegation, key type, whether control is multi-signature — are not flags. Software should read them fresh rather than cache them in a string that can go stale. What the registry records is the acceptance state attached to such a fact, which is a decision, and decisions are never derivable.
An unflagged alias is a valid IDNA label in every language this specification supports, subject to length. A flagged alias is not: the comma is not permitted in a label, and a flag digit at the start of a right-to-left label violates the Bidi Rule (RFC 5893). Since the delimiter appears only on flagged aliases, this is self-enforcing.
A query is normalized (§5) and its flag groups discarded before comparison.
Predicate. A matcher MUST report a match if and only if every term of the normalized query is a substring of the normalized, flag-stripped alias, where a term is a hyphen-delimited part of the normalized query. Term order does not matter.
Substring rather than segment comparison, because in scriptio-continua languages the whole alias is a single segment and segment comparison could not find it by any word inside it.
Order. A matcher MUST return matches ordered by, in priority order:
Key 4 leaves no ties, so the order is total and testable. This order is a default that is correct with no information beyond the strings. An application holding its own signals — recency, frequency, favourites — MAY re-rank.
Key 3 divides by length in characters, which favours short aliases. CJK aliases are shorter in characters than Latin aliases carrying the same content, so a mixed-language corpus will float them upward. This is a known bias, accepted in preference to normalizing by segment count, which is biased the other way.
There is no minimum query length. A single-character query matching most of a corpus is correct behaviour for incremental search, and CJK names are routinely one character.
Uniqueness is out of scope. Two identical aliases for different identifiers are not a lookup failure: the user searches, receives both, and chooses. COIA 1.x described an optional numeric suffix; it was never implemented and is removed. See CHANGES.
alias := body [ "," group [ "," group ] ]
body := segment ( "-" segment )*
segment := ( L | N | Mn | Mc )+
group := DIGIT*
where L, N, M are Unicode General_Category letter, number and mark, plus the
joiners licensed by §5.1.
As a regular expression, for the body:
^[\p{L}\p{N}\p{M}]+(-[\p{L}\p{N}\p{M}]+)*$
A permissive variant for user input, tolerating hyphen alternatives a keyboard may produce:
^[\p{L}\p{N}\p{M}]+([-‐‑‒–−][\p{L}\p{N}\p{M}]+)*$
An alias is plaintext, and its structure is regular. who, role and scope always
appear in the same positions, so a backup, a sync service, a crash report, a screenshot or
a subpoena yields structured relationship data rather than idiosyncratic notes.
This is worse than an arbitrary label, not neutral, and the trade is deliberate: the
structure is what makes an alias memorable and searchable. An alias such as
alice-smith-hiv-patient-mercy-clinic,1 is a plausible one to create and a
damaging one to leak.
Applications should consider: whether alias text is included in crash reports and telemetry; whether it appears in logs that leave the device; whether it is visible on a lock screen; and whether sensitive aliases warrant a separate store.
This specification does not require or forbid retaining generation inputs. Storage is outside the conformance classes.
This specification uses semantic versioning, with these meanings.
MAJOR — any change to normalization, to the grammar, or to the meaning of an existing flag digit. A stored alias may stop matching a freshly normalized query.
MINOR — adding a language, assigning a reserved flag digit, or clarifying prose. A MINOR change MUST NOT change the alias produced for any previously valid input.
Implementations MUST record the COIA version and the Unicode version used to produce an alias. A matcher MUST compare against the stored alias as it stands, and MUST NOT assume it was produced by the current version.
Normalization is deliberately defined by explicit tables rather than by Unicode properties, so a Unicode upgrade does not silently change an alias: new characters are preserved by default because they are letters, and the SPLIT and VIRAMA tables change only when this specification changes.
And, non-normative:
v1/ — COIA 1.x, preserved.