node-keri
v0.2.1
Published
KERI (did:keri) implementation for Node
Downloads
39
Readme
keri
A pure, synchronous, Node.js library for the KERI identity lifecycle. Create, rotate, deactivate, sign with, and verify did:keri identifiers using cryptographically verifiable append-only key event logs (KEL).
Basically, KERI let's you create unique DIDs (decentralized identifiers) which are cryptographically bound to a private key. You can rotate your keys without changing your DID, and other people can verify that the new keys are in-fact owned by the same DID owner as the previous keys. All of this is possible without any centralized service or shared blockchain. Even if your current keys are stolen or their crypto algorithm is cracked, you can rotate your keys to regain control of your identity.
This works by storing the next private key safely offline, or by storing some secret seed offline which is used to deterministically generate all future private keys for the DID
In practice, this means you can have your own persistent "account" in decentralized, peer-to-peer networks. An obvious use-case is in agent-to-agent communication.
This is a deterministic state-machine library, not an authentication framework. It has no dependencies, performs no I/O, and is entirely synchronous. The caller is responsible for secure storage and networking; the library just owns the cryptographic lifecycle. Only Ed25519 keys are supported. Not all KERI features are supported; the supported subset is the KERI Direct JSON Profile v1 — see PROFILE.md for the exact conformance boundary and SECURITY.md for the trust model and security invariants.
Installation
npm install node-keriRequires Node.js v20.13.x or later.
Basic usage
Create a new identifier
import {
createIdentifier,
verifyDid,
generateKeyPair,
publicKeyFromCesr,
} from 'node-keri';
// You can bring your own keys, or generate them like this.
const currentKeyPair = generateKeyPair();
const nextKeyPair = generateKeyPair(); // Pre-committed keys
// Mint a new did:keri identifier.
const result = createIdentifier({
currentPrivateKey: currentKeyPair.privateKey,
nextPublicKey: nextKeyPair.publicKey,
});
console.log(result.did); // => "did:keri:<...>"
// The KEL now contains just one event (the inception event).
const kel = result.event;
// Other people can verify the identifier via the KEL.
const verified = verifyDid({ did: result.did, kel });
if (verified.ok) {
const publicKey = publicKeyFromCesr(verified.state.currentPublicKey);
console.log(publicKey.export({ format: 'jwk' }));
} else {
console.error('Verification failed!');
}Rotate an identifier's keys
import { rotateIdentifier, generateKeyPair } from 'node-keri';
currentKeyPair = nextKeyPair;
nextKeyPair = generateKeyPair(); // New pre-committed keys
const result = rotateIdentifier({
state: verified.state,
newPrivateKey: currentKeyPair.privateKey,
nextPublicKey: nextKeyPair.publicKey,
});
// The KEL now contains another event (the rotation event).
kel += result.event;Sign a message
import { createSignature } from 'node-keri';
const payload = Buffer.from('a message from the DID controller');
const signature = createSignature(currentKeyPair.privateKey, payload);
console.log(signature); // CESR-qualified url-safe ASCII stringVerify a signed message
import { verifySignatureWithDid } from 'node-keri';
const ok = verifySignatureWithDid({
did,
kel,
payload,
signature,
});
if (ok) {
console.log('Verification succeeded!');
} else {
console.log('Verification failed!');
}Key Events and the KEL
KEL stands for "Key Event Log". It is the cryptographically verifiable history of a KERI identifier. Anybody holding a KEL can independently verify it to determine the corresponding identifier's current public key. Each KEL is cryptographically bound to its corresponding identifier, so attempting to verify a KEL against the wrong identifier will always fail.
Events and KELs are exchanged as CESR streams, which are ASCII-safe strings. A KEL is simply a concatenation of each each event it contains, in order.
API reference
The error policy is uniform across the whole library: throwing is reserved for programmer errors — a wrong argument type, a malformed key object, an unsupported KERI feature, etc. Anything that can legitimately arrive from an untrusted source — a tampered KEL, a malformed DID, a non-verifying signature — is reported as a typed { ok: false, error } result or a false return, never thrown. Throwing functions throw a KeriError subclass; see Errors.
Identifier lifecycle
createIdentifier()
function createIdentifier(input: {
currentPrivateKey: PrivateKey;
nextPublicKey: PublicKey;
establishmentOnly?: boolean;
digestCode?: string;
}): {
did: string;
aid: string;
event: string;
state: KeriState;
};Mint a new transferable did:keri identifier. You supply the key material: currentPrivateKey is the current signing key's private half; nextPublicKey is the pre-rotation key's public half. It builds and signs the inception event and returns the DID, the signed event, and the replay-derived KEL state. Keep the current private key to sign with, and securely store the next private key to rotate to later.
The returned event is the signed inception event in its CESR stream ASCII wire form. A KEL is built by concatenating these event strings in order. When an identifier is first created, its KEL is simply equal to this first event.
Options:
establishmentOnly: set totrueto mint an establishment-only identifier, which disables interaction events. It defaults tofalse.digestCodeselects the hash algorithm for the inception SAID, AID, and next-key commitment — it defaults toDIGEST_CODES.SHA2_256(I). See Digest algorithms.
To mint a non-transferable identifier instead — an identifier with no KEL, which can never be rotated — use createNonTransferableIdentifier.
rotateIdentifier()
function rotateIdentifier(input: {
state: KeriState;
newPrivateKey: PrivateKey;
nextPublicKey: PublicKey;
digestCode?: string;
}): {
event: string;
state: KeriState;
};Rotate an identifier's keys. newPrivateKey is the private half of the key being rotated to (it must match with the pre-commited nextPublicKey of the previous event, or rotation will fail). nextPublicKey is the public half of the freshly chosen pre-rotation key for the next rotation; securely store the matching private half to rotate again later. Returns the signed rotation event and the new state.
Options:
digestCode: selects the hash for this event's SAID and new next-key commitment — it defaults toDIGEST_CODES.SHA2_256(I). See Digest algorithms.
interactOnIdentifier()
function interactOnIdentifier(input: {
state: KeriState;
currentPrivateKey: PrivateKey;
data: readonly unknown[];
digestCode?: string;
}): {
event: string;
state: KeriState;
};Anchor arbitrary data to the identifier's KEL without rotating keys. currentPrivateKey must be the currently authoritative signing key. Each data entry must be plain JSON-serializable data. Returns the signed interaction event and the new state.
Options:
digestCode: selects the hash for this event's SAID — it defaults toDIGEST_CODES.SHA2_256(I). See Digest algorithms.
deactivateIdentifier()
function deactivateIdentifier(input: {
state: KeriState;
newPrivateKey: PrivateKey;
digestCode?: string;
}): {
event: string;
state: KeriState;
};Permanently disables an identifier. After deactivation, the identifier cannot have any more events, it cannot be used for signing messages, and its identity is permanently considered invalid/untrustworthy. newPrivateKey must be the private half of the pre-commited nextPublicKey of the previous event. Returns the signed rotation event and the new state.
Options:
digestCode: selects the hash for this event's SAID — it defaults toDIGEST_CODES.SHA2_256("I"). See Digest algorithms.
createNonTransferableIdentifier()
function createNonTransferableIdentifier(input: { publicKey: PublicKey }): {
did: string;
aid: string;
state: KeriState;
};Mint a non-transferable did:keri identifier. It is self-certifying: there is no inception event and no KEL, and it can never rotate. Keep the matching private key to sign with.
Use the empty string '' as the KEL wherever one is expected (e.g., verifyDid({ did, kel: '' }).
Non-transferable identifiers are useful for ephemeral, temporary identifiers.
verifyIdentifier()
function verifyIdentifier(input: {
aid: string;
kel: string;
}): { ok: true; state: KeriState } | { ok: false; error: KeriVerificationError };Verify an identifier and reconstruct its latest authoritative state. The returned state is the only KeriState a caller may treat as verified.
A KEL that ends in a deactivation event (see deactivateIdentifier) verifies normally, and its state has deactivated === true and transferable === false.
verifyDid()
function verifyDid(input: {
did: string;
kel: string;
}): { ok: true; state: KeriState } | { ok: false; error: KeriVerificationError };This is the DID-level counterpart of verifyIdentifier. A DID document can be subsequently created by passing the returned state to createDidDocument().
Signing and verifying signatures
createSignature()
function createSignature(privateKey: PrivateKey, payload: Uint8Array): string;Sign payload with privateKey and return the signature, which is CESR-qualified url-safe ASCII string.
verifySignature()
function verifySignature(input: {
aid: string;
kel: string;
payload: Uint8Array;
signature: string;
}): boolean;Verifies that the provided signature is a valid signature over the provided payload, and that the signature was made by the controller of the given identifier, using the identifier's current authoritative signing key.
Returns false if verification fails (e.g., a non-verifying KEL, a KEL for a different identifier, a KEL for a deactivated identifier, or a malformed or mismatched signature).
verifySignatureWithDid()
function verifySignatureWithDid(input: {
did: string;
kel: string;
payload: Uint8Array;
signature: string;
}): boolean;This is the DID-level counterpart of verifySignature.
DID support functions
createDidDocument()
function createDidDocument(input: {
state: KeriState;
services?: readonly DidService[];
}): DidDocument;Project a verified KeriState into a W3C DID document with a single Ed25519 verification method (JsonWebKey2020 / publicKeyJwk) referenced from both authentication and assertionMethod. You should only pass state returned by verifyIdentifier or verifyDid.
Options:
services: populates the document'sservicefield; bare-fragment ids are expanded against the document's DID.
Key handling
This library expects PublicKey and PrivateKey inputs, which are just Node's KeyObject, branded for Ed25519 and refined to their respective halves:
type PublicKey = KeyObject & { asymmetricKeyType: 'ed25519'; type: 'public' };
type PrivateKey = KeyObject & { asymmetricKeyType: 'ed25519'; type: 'private' };
interface KeyPair {
publicKey: PublicKey;
privateKey: PrivateKey;
}generateKeyPair()
function generateKeyPair(): KeyPair;Generate a fresh Ed25519 keypair from the platform CSPRNG.
asPublicKey() / asPrivateKey()
function asPublicKey(value: unknown): PublicKey;
function asPrivateKey(value: unknown): PrivateKey;Narrow a Node KeyObject to a branded PublicKey or PrivateKey, throwing InvalidArgumentError if it is not an Ed25519 key of the correct half. Use after parsing a stored key:
import { createPrivateKey } from 'node:crypto';
import { asPrivateKey } from 'node-keri';
const privateKey = asPrivateKey(createPrivateKey(pemString)); // PEM/DER/JWK all workpublicKeyToCesr() / publicKeyFromCesr()
function publicKeyToCesr(key: PublicKey): CesrPublicKey;
function publicKeyFromCesr(cesr: CesrPublicKey): PublicKey;Convert between a PublicKey and its CESR-qualified string form. Use publicKeyToCesr to compare a key you hold against a verified KeriState:
console.log(publicKeyToCesr(myKeyPair.publicKey) === state.currentPublicKey);Use publicKeyFromCesr for the inverse — recover a usable PublicKey from state.currentPublicKey.
Digest algorithms
KERI digests are self-describing — every qualified digest carries a CESR derivation code naming its hash algorithm — so a KEL may mix algorithms across events. This library supports any available 256-bit or 512-bit hash.
digestAlgorithms
const digestAlgorithms: Record<string, DigestAlgorithm | undefined>;
interface DigestAlgorithm {
name: string;
hash(input: Uint8Array): Uint8Array;
}The registry of available digest implementations, keyed by CESR derivation code. It is populated at load with every native node:crypto hash the linked OpenSSL provides — SHA2-256 (I), SHA2-512 (0G), SHA3-256 (H), SHA3-512 (0E), BLAKE2s-256 (G), BLAKE2b-512 (0F).
It is monkey-patchable: register an algorithm node-keri does not ship with by assigning an entry. The implementation must return 32 bytes for a one-character code or 64 bytes for a 0-prefixed code.
import { digestAlgorithms } from 'node-keri';
import { blake3 } from 'some-blake3-library';
// Blake3-256 uses CESR code 'E'. Once registered, node-keri can use it.
digestAlgorithms['E'] = {
name: 'Blake3-256',
hash: (input) => blake3(input), // must return 32 bytes
};Choosing an algorithm for generation
createIdentifier(), rotateIdentifier(), interactOnIdentifier(), and deactivateIdentifier() each accept an optional digestCode option. It defaults to SHA-256 (DIGEST_CODES.SHA2_256, 'I'). Attempting to use a code with no registered implementation throws UnsupportedAlgorithmError.
import {
createIdentifier,
rotateIdentifier,
generateKeyPair,
DIGEST_CODES,
} from 'node-keri';
const nextKeyPair = generateKeyPair();
const inception = createIdentifier({
currentPrivateKey: generateKeyPair().privateKey,
nextPublicKey: nextKeyPair.publicKey,
digestCode: DIGEST_CODES.SHA3_256,
});
const rotation = rotateIdentifier({
state: inception.state,
newPrivateKey: nextKeyPair.privateKey,
nextPublicKey: generateKeyPair().publicKey,
digestCode: DIGEST_CODES.SHA2_512, // a KEL may mix digest algorithms
});DIGEST_CODES contains the well-known codes:
SHA2_256SHA2_512SHA3_256SHA3_512BLAKE2B_512BLAKE2S_256BLAKE2B_256BLAKE3_256
Errors
Programmer errors throw a subclass of KeriError:
| Class | Meaning |
| --------------------------- | --------------------------------------------------------------------------------- |
| InvalidArgumentError | An argument does not satisfy the function's contract. |
| UnsupportedAlgorithmError | A requested algorithm or derivation code is not supported. |
| MalformedInputError | Input parsed structurally but failed integrity validation (e.g. a bad CESR code). |
| CanonicalJsonError | A value rejected by the canonical-JSON serialization rules. |
Data-level failures are not thrown. verifyIdentifier and verifyDid return a discriminated { ok } result whose error is a KeriVerificationError:
type KeriVerificationError =
| { code: 'EMPTY_KEL' }
| { code: 'INVALID_DID'; message: string }
| { code: 'UNSUPPORTED_FEATURE'; feature: string }
| { code: 'INVALID_EVENT_TYPE'; eventType: string }
| { code: 'NON_TRANSFERABLE_NOT_EXTENSIBLE'; eventType: string }
| { code: 'DEACTIVATED_NOT_EXTENSIBLE'; eventType: string }
| { code: 'ESTABLISHMENT_ONLY_NO_INTERACTION'; eventType: string }
| { code: 'INVALID_SEQUENCE'; expected: number; actual: number }
| { code: 'INVALID_PREVIOUS_DIGEST' }
| { code: 'INVALID_EVENT_DIGEST' }
| { code: 'INVALID_SIGNATURE' }
| { code: 'INVALID_NEXT_KEY_COMMITMENT' }
| { code: 'INVALID_CESR_CODE'; value: string }
| { code: 'NON_CANONICAL_EVENT' }
| { code: 'MALFORMED_STREAM'; message: string };verifySignature and verifySignatureWithDid collapse every data-level failure to a plain false.
What this library does not do
No storage, no networking, no discovery, no agent runtime, no registries, no filesystem access. It builds and verifies events; transport and persistence are the caller's responsibility.
