@kerits/core
Guides

SAIDs

Compute, embed, and verify Self-Addressing Identifiers using keripy-compliant derivation surfaces.

A SAID (Self-Addressing Identifier) is a cryptographic digest embedded in the very data structure it identifies. The circular binding is achieved by replacing the target field with a placeholder, hashing the serialized result, and storing that hash back into the field. Any reader can verify a SAID by performing the same substitution and recomputing the digest — no external registry or certificate authority is required.

In KERI, SAIDs appear as the d field of every KEL event. For inception events (icp, dip) the i field (the AID itself) is also set to the inception SAID, making the identifier self-certifying from the moment it is created.

Derivation Surfaces

A DerivationSurface declares how an artifact family (KEL event, TEL event, ACDC credential) projects itself onto a SAID preimage. It specifies:

  • Which fields participate in the SAID derivation
  • The insertion order those fields are serialized in (matching the keripy reference implementation)
  • Whether the artifact has a version string and which protocol it belongs to (KERI or ACDC)
surface-example.ts
import type { DerivationSurface } from '@kerits/core';

// A surface for KERI inception events — fields in keripy canonical order
const KEL_ICP_SURFACE: DerivationSurface = {
  saidField: 'd',
  derivedFieldsInOrder: ['v', 't', 'd', 'i', 's', 'kt', 'k', 'nt', 'n', 'bt', 'b', 'c', 'a'],
  hasVersionString: true,
  versionStringField: 'v',
  protocol: 'KERI',
};

Kerits ships pre-built surfaces for all standard artifact families:

SurfaceArtifact
KEL_ICP_SURFACEInception event
KEL_ROT_SURFACERotation event
KEL_IXN_SURFACEInteraction event
KEL_DIP_SURFACEDelegated inception
KEL_DRT_SURFACEDelegated rotation
TEL_VCP_SURFACERegistry inception
TEL_VCP_WITH_NONCE_SURFACERegistry inception with nonce
TEL_VRT_SURFACERegistry rotation
TEL_ISS_SURFACECredential issuance
TEL_REV_SURFACECredential revocation
TEL_BIS_SURFACEBacker credential issuance
TEL_BRV_SURFACEBacker credential revocation
ACDC_SCHEMA_SURFACEACDC schema envelope
ACDC_CREDENTIAL_SURFACEACDC credential (minimal)

For ACDC credentials with optional fields, use buildACDCCredentialSurface(artifact) to construct a surface from the fields actually present.

Compute a SAID

deriveSaid takes an artifact and its surface, computes the SAID using insertion-order serialization and BLAKE3-256, and returns the sealed artifact with the SAID written back:

derive-said.ts
import { deriveSaid, KEL_IXN_SURFACE } from '@kerits/core';

const artifact = {
  v: 'KERI10JSON000000_',
  t: 'ixn',
  d: '',           // placeholder — deriveSaid replaces this
  i: 'EDP1vHcw_wc4M0MPoW1gVXEl3XEY2eJMBqhBMBCAFXMq',
  s: '2',
  p: 'EpriorSaid123456789012345678901234567890123',
  a: [],
};

const { sealed, said } = deriveSaid(artifact, KEL_IXN_SURFACE);
// said: 'E...' — 44-char CESR Blake3-256 digest
// sealed.d === said
// sealed.v — version string with correct byte-length declaration

For versioned surfaces, deriveSaid also computes the version string (e.g. KERI10JSON00009b_) with the correct byte-length declaration. The version string and SAID use the same insertion-order serializer to guarantee alignment.

Verify a SAID

recomputeSaid checks whether a sealed artifact's declared SAID matches a fresh recomputation:

recompute-said.ts
import { recomputeSaid, KEL_IXN_SURFACE } from '@kerits/core';

const result = recomputeSaid(sealedEvent, KEL_IXN_SURFACE);
// result.matches:    true if declared SAID matches recomputed
// result.declared:   the SAID value found on the artifact
// result.recomputed: the freshly computed SAID

if (!result.matches) {
  console.error('SAID verification failed');
}

recomputeSaid returns matches: false rather than throwing for invalid input, making it safe for validation pipelines.

Serialize for Signing

serializeForSigning projects a sealed artifact onto its surface field order and serializes it. The result is the canonical byte sequence that should be signed:

serialize-for-signing.ts
import { serializeForSigning, KEL_ICP_SURFACE } from '@kerits/core';

const { raw, text } = serializeForSigning(sealedEvent, KEL_ICP_SURFACE);
// raw:  Uint8Array — bytes to sign
// text: string — the JSON representation

This produces the same field order as the SAID preimage, but with the actual SAID filled in instead of the placeholder.

SAIDs in KEL Events

Inception events are special: both d (the event SAID) and i (the AID) must be set to the same value because the identifier is the inception event's digest.

icp-said.ts
import { KELEvents, generateKeyPair, encodeKey, nextKeyDigestQb64FromPublicKeyQb64 } from '@kerits/core';

const current = generateKeyPair();
const next = generateKeyPair();

const currentQb64 = encodeKey(current.publicKey).qb64;
const nextQb64 = encodeKey(next.publicKey).qb64;
const nextDigest = nextKeyDigestQb64FromPublicKeyQb64(nextQb64);

const { unsignedEvent } = KELEvents.buildIcp({
  keys: [currentQb64],
  nextKeyDigests: [nextDigest],
});

// computeSaid with isInception=true sets both d and i to the computed SAID
const { event: icpEvent, said } = KELEvents.computeSaid(unsignedEvent, true);
// icpEvent.d === icpEvent.i === said

For rotation and interaction events only d is cleared — i retains the original AID throughout the controller's lifetime.

Pre-rotation Key Commitments

Before publishing an inception or rotation event you must commit to the next set of keys without revealing the private keys. nextKeyDigestQb64FromPublicKeyQb64 computes the BLAKE3-256 digest of a CESR-encoded public key, producing the value that goes into the n[] field.

next-key-digest.ts
import { generateKeyPair, encodeKey, nextKeyDigestQb64FromPublicKeyQb64 } from '@kerits/core';

const nextKeyPair = generateKeyPair();
const nextKeyQb64 = encodeKey(nextKeyPair.publicKey).qb64;

const commitment = nextKeyDigestQb64FromPublicKeyQb64(nextKeyQb64);
// commitment: 'E...' (44-char CESR Blake3-256 digest)

At rotation time, the private key for nextKeyPair is revealed by including nextKeyQb64 in the rotation event's k[]. Validators confirm the pre-rotation commitment holds by hashing the revealed key and matching it against the digest stored in the previous event's n[].

Serialization: Two Paths

Kerits maintains two separate serialization paths for SAID computation:

PathSerializerUse case
KERI derivation (deriveSaid, recomputeSaid)Insertion-order JSONKEL events, TEL events, ACDCs
Generic (Data.saidify)RFC-8785 (lexicographic)Schema SAIDs, messaging digests

The KERI path preserves field insertion order as required by the KERI specification and keripy reference implementation. The generic path uses RFC-8785 canonical JSON for determinism independent of construction order. These paths are tested independently — changes to one must not affect the other.

KERIpy Compliance Testing

SAID computation is deterministic: a correct implementation must produce identical digests for identical inputs regardless of language or runtime. Kerits validates its SAID outputs against KERIpy (the Python reference implementation) using a shared fixture system.

How it works

The compliance pipeline has three stages:

┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────────┐
│ 1. Neutral fixtures │ ──▶ │ 2. KERIpy generates  │ ──▶ │ 3. TypeScript tests     │
│    (shared JSON)    │     │    expected values    │     │    cross-validate       │
└─────────────────────┘     └──────────────────────┘     └─────────────────────────┘

Stage 1: Shared fixtures. Test fixtures live in packages/core/src/said/fixtures/keri-said-fixtures.json. Each fixture declares an artifact and its derivation surface using neutral field names (not Python or TypeScript class names):

keri-said-fixtures.json (excerpt)
{
  "id": "kel-icp",
  "description": "KERI inception event with single signing key",
  "surface": {
    "saidField": "d",
    "derivedFieldsInOrder": ["v", "t", "d", "i", "s", "kt", "k", "nt", "n", "bt", "b", "c", "a"],
    "hasVersionString": true,
    "versionStringField": "v",
    "protocol": "KERI"
  },
  "artifact": {
    "v": "KERI10JSON000000_",
    "t": "icp",
    "d": "",
    "i": "",
    "s": "0",
    "kt": "1",
    "k": ["DHr0-I-mMN7h6cLMOTRJkkfPuMd0vgQPrOk4Y3edaHjr"],
    "nt": "1",
    "n": ["EIFG_uqfr1yN560LoHYHfvPAhxQ5sN6p3any8Ir7kGpq"],
    "bt": "0",
    "b": [],
    "c": [],
    "a": []
  }
}

Stage 2: Python script generates expected values. scripts/generate-keripy-saids.py reads the fixtures, computes SAIDs using KERIpy's Saider.saidify(), and writes the results to keri-said-expected.json:

Generate expected values
# Requires Python 3.12.1+ and KERIpy
pip install keri
python3 scripts/generate-keripy-saids.py

The generated file records the keripy version and the computed SAID + version string for each fixture. It is checked into version control so CI tests validate against keripy without needing Python installed.

Stage 3: TypeScript tests cross-validate. The test suite in keri-said-derivation.test.ts runs two layers of assertions:

  • Structural assertions (always run): CESR format (E prefix, 44 chars), version-string byte alignment, deriveSaid/recomputeSaid round-trip
  • KERIpy cross-validation (when expected file present): exact SAID and version-string equality against keripy-computed values
Cross-validation pattern
const expected = keriPyExpected?.['kel-icp'];
if (expected?.versionString) {
  expect(sealed.v).toBe(expected.versionString);
}

When the expected file is absent, structural tests still run — the suite never fails due to a missing expected file.

Covered artifact families

The fixture set covers KEL events (icp, rot, ixn, dip), TEL events (vcp, iss, rev), and ACDC artifacts (schema, credential). Each fixture tests both deriveSaid and recomputeSaid round-trip.

Adding new fixtures

  1. Add an entry to keri-said-fixtures.json with the artifact and surface definition
  2. Re-run python3 scripts/generate-keripy-saids.py to update expected values
  3. Add a test scenario in keri-said-derivation.test.ts that loads the fixture and asserts against keriPyExpected

Legacy API

The following functions are deprecated. Use deriveSaid and recomputeSaid with a DerivationSurface instead.

  • encodeSAID(obj) — delegates to cesr-ts Saider.saidify, uses RFC-8785 canonicalization (does not match keripy insertion-order serialization)
  • validateSAID(said, obj) — recomputes via cesr-ts and compares
  • saidFromJson(obj) — RFC-8785 canonicalization + Blake3 digest

These remain available for backward compatibility but will be removed in a future release.

On this page