# Content-addressed names: RFC 6920 ni: URIs over canonical CBOR, with worked hashes

## Content-addressed names: RFC 6920 ni: URIs over canonical CBOR, with worked hashes

A content address should name a value, not one spelling of it. The 561 Group's draft specification of the *semiotic content address* (SCA) names a piece of data by the RFC 6920 `ni:///sha-256;…` URI of the SHA-256 digest of its **deterministic CBOR** encoding, after Unicode NFC normalization of every string and key. Two JSON objects that differ only in key order, or two strings that differ only in composed versus decomposed accents, then have the same address; hashing the raw JSON text, the common shortcut, gives them different ones. This article works the examples with real hashes you can reproduce in a terminal.

### What is an RFC 6920 ni: URI?

RFC 6920 defines "Naming Things with Hashes". Its `ni:` URI scheme names content by a digest. The Group uses the SHA-256 form only, with an empty authority:

```
ni:///sha-256;f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk
```

After `sha-256;` comes the 43-character unpadded base64url encoding of the 32-byte digest. The specification adds one strict rule: the last character must be one of the sixteen canonical final sextets, so four spellings cannot name one digest. A validator that accepts non-canonical padding bits allows aliasing, which defeats the point of a name. You can produce the name above yourself:

```bash
ni() { openssl dgst -sha256 -binary | basenc --base64url | tr -d '=' | sed 's|^|ni:///sha-256;|'; }
printf 'Hello World!' | ni
# ni:///sha-256;f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk
```

That name commits to exact bytes. It is a hash of a file. For structured data it is not yet enough.

### Why isn't hashing the JSON text enough?

Take the same two-field object written two ways:

```bash
printf '{"name":"x402","version":2}' | ni
# ni:///sha-256;W6OZAGyazZCr7el9hfVf59AR6VwuIIE49ET6d2xPcGk
printf '{"version":2,"name":"x402"}' | ni
# ni:///sha-256;-kcErArIKfOq9rm9ns6QzSPYFnI-eoFAr2qVqiDWnzg
```

They are the same value and two different names. Whitespace, key order and escape choices each mint a new hash. The Group's draft calls out the existing instances in its own tree as defects: `ni:` names minted over `JSON.stringify` output are "valid RFC 6920 names of those bytes, but key-order dependent, so not names of the value". The remedy is to hash a canonical form, in two steps.

### How is a value canonicalized?

**Step 1: Unicode NFC.** Every string and every map key is normalized to NFC. A key collision introduced by normalization is rejected rather than merged. The composed `é` (U+00E9) and `e` followed by a combining acute (U+0301) become one string.

**Step 2: deterministic CBOR.** The value is encoded with RFC 8949 core deterministic encoding: shortest-form heads, definite lengths, map keys sorted by their encoded bytes, shortest exact floats, no tags. A decoder refuses non-canonical bytes. Our object becomes 20 bytes:

```
a2 64 6e616d65 64 78343032 67 76657273696f6e 02
```

That is a 2-entry map (`a2`), the 4-character text `name` (`64 6e616d65`) with value the 4-character text `x402` (`64 78343032`), and the 7-character text `version` (`67 76657273696f6e`) with value the integer 2 (`02`). Hash it and name it:

```
ni:///sha-256;S14vjN44zabSMQb8L_7tqK6kAI2gtzu-I_4lb58v930
```

Written in either key order, the value encodes to those same 20 bytes, and so has that one address. You can check it:

```bash
printf '\xa2\x64name\x64x402\x67version\x02' | ni
# ni:///sha-256;S14vjN44zabSMQb8L_7tqK6kAI2gtzu-I_4lb58v930
```

The same holds for Unicode. The value `{"café": 1}` written with a precomposed `é` and with `e` plus U+0301 has one address under this scheme, `ni:///sha-256;1Nl25GfeDsGRq0U2MSY4k-GJX0eSCrT69KwzucuPHZQ`, whereas hashing the JSON text of the two spellings gives two unrelated names.

We confirmed our encoder against the reference implementation's published test vectors: the canonical encodings of `null`, `true` and `0` hash to `ni:///sha-256;sLKYi2u-ckus2l6eUkc23gvH2uQcRrQhPFDh011OXxM`, `…J6ve3f6FA0lq3rYjRmyqR9pfY6vSvG-hn2z8tz7P7XA` and `…bjQLnP-zepicpUTmu3gKLHiQHT-zNzh2hRGjBhevoB0` respectively, and the reference's own tests assert that the NFC and NFD spellings of `café` have equal digests. The two-field example above was computed with a short independent encoder, not the reference code, so treat the example addresses as illustrations that you can verify rather than as vectors from the reference suite.

### What does the specification add on top?

The draft (version 0.1.0, dated 2026-09-23, status "realized in part") describes three layers.

1. **Carrier commitment (data).** A *semiotic carrier* is a JSON-shaped value: null, booleans, integers, big integers, NFC strings, byte strings, arrays and plain objects with string keys. `semanticId(value)` is the `ni:` name of its canonical CBOR. This layer is implemented and has test vectors.
2. **Semantic identity (code).** The identity of a *typed claim* is the commitment of a normalized envelope with fields `kind`, `version`, `objectKind`, `semanticType`, `normalizationProfile`, `settlementProfile`, `settledBody` and `witnessRoot` (the `ni:` name of the evidence bundle that the settlement consumed). Two envelopes are the same content exactly when their bodies reduce to the same normal form under the same type, kind, profiles and witness root. This layer needs the Group's expression notation and is partly implemented in its runtime.
3. **Proof-path coordinate (code).** A readable resource name whose segments are generating, witnessing or obstructing steps (`A.B`, `A:B`, `A-B`), with a triple of surface, token and claim that must agree. The draft says plainly that "every protocol surface (HTTP route, MCP resource URI, ActivityPub IRI, package export key, filesystem path) is a reading of an address term", and that the requirement that all readings agree "is a formal obligation, not yet a theorem". We include it for completeness; it is research, not a shipped feature.

One rule runs through all three: a token is opaque. No consumer decodes an address to learn what it names; it reads the claim or carrier the address commits to and verifies the commitment.

### What does not count as an address?

The draft lists its own non-conformances, which is the most credible part of it:

- `ni:` names over `JSON.stringify` output (key-order dependent), found in resource reservations and package audits.
- `ni:///sha-256;<hex>` strings, which are *not* RFC 6920 (hexadecimal instead of base64url) and fail the validator; they appear in a journal of semantic records and tool evidence, and converting them "is a migration of those journals, not a renaming".
- Bare `sha256:<hex>` strings and `urn:` identifiers, which belong to the protocols that define them (OCI digests, EVM events) and are never house content names.

We publish this because a name that cannot be checked is not evidence. If a record says it is addressed, a reader should be able to recompute.

### Where do these addresses appear in public?

- **Every offer in the market catalog** carries a `sku`, a `cell` and a `law`, each an `ni:///sha-256;…` name (for example, the JSON Pointer offer's SKU is `ni:///sha-256;fAnZehn7tM6Qdq9XoP2A8cv7zISE0iPitrf0iq9yn5o` in the [live catalog](https://market.561.group/v1/catalog)).
- **The ActivityPub outbox** of that market publishes each offer as a document of type `application/sen+cbor` named by its address ([One catalog, five machine faces](https://engineering.561.group/x402-one-catalog-machine-interfaces/)).
- **This site.** Each article's JSON-LD includes an `identifier` that is the `ni:` name of its cell ([how the site is built](https://engineering.561.group/how-this-site-is-built-tufte/)).
- **Public challenge observations.** The x402 boundary reference returns an `observationDigest` as an `ni:` name over the normalized challenge it saw ([payee verification](https://engineering.561.group/verify-x402-payee-buyer-side/)).
- **Six paid content-address offers** create, inspect and verify SHA-2 multihashes and CIDs (base conversion, raw-block CIDs), at $0.0012 per call. They state their limits: "There is no upload, pinning, retrieval or authorship claim", and raw verification refuses DAG and UnixFS.

A public consumer of the carrier-address function is the MIT-licensed [cell-identity-verification-service](https://github.com/red-cup-engineering/cell-identity-verification-service), which addresses capability-cell manifests and refuses undefined members, fractional numbers, malformed Unicode, `__proto__` keys, NFC key collisions, cycles and hidden properties.

### What are the caveats?

The specification is a draft with a declared stability of "draft". Number handling is its sharpest edge: the written text admits integers, while the current reference implementation also admits finite floats and encodes them as shortest exact floats, so check the version when you depend on a float. A content address proves that two carriers are the same value; it does not prove who made them or that they are true. Semantic identity (layer 2) is meaningful only relative to a normalization profile and a settlement profile, and two envelopes with different profiles do not become equivalent by sharing digest syntax. Finally, several of the repositories involved are not public; we describe behavior from source and tests we ran, and the examples in this article are reproducible without any access.

Continue to [evidence in the publishing pipeline](https://engineering.561.group/how-this-site-is-built-tufte/) for where the address meets web publication.
