Loading vLEI.wiki
Fetching knowledge base...
Fetching knowledge base...
This comprehensive explanation has been generated from 71 GitHub source documents. All source documents are searchable here.
Last updated: October 7, 2025
This content is meant to be consumed by AI agents via MCP. Click here to get the MCP configuration.
Note: In rare cases it may contain LLM hallucinations.
For authoritative documentation, please consult the official GLEIF vLEI trainings and the ToIP Glossary.
A [non-transferable](/concept/non-transferable "A property indicating that control authority over a digital identifier or asset ...") identifier is a KERI autonomic identifier (AID) whose controlling keys cannot be rotated, making control authority permanently fixed to the initial key pair established at inception. This immutability prevents transfer of control to other entities, making these identifiers inherently ephemeral and suitable for short-lived, peer-to-peer, or one-time-use scenarios.
A non-transferable identifier is a specialized type of autonomic identifier (AID) in the KERI protocol where the controlling cryptographic keys are permanently bound to the identifier at inception and cannot be rotated or transferred. This fundamental characteristic distinguishes non-transferable identifiers from their transferable counterparts and defines their entire lifecycle and security model.
As defined in the KERI specification, a non-transferable identifier is created through an inception event that establishes the initial key state, but unlike transferable identifiers, this key state is immutable. The identifier's prefix is cryptographically derived from the public key(s) using a derivation code that explicitly signals the non-transferable nature of the identifier.
The structure of a non-transferable identifier follows KERI's self-certifying identifier architecture:
Prefix Composition: The identifier prefix consists of:
B for Ed25519 non-transferable prefix)nxt field in the inception event is empty or has threshold 0)When implementing non-transferable identifier creation, ensure the derivation code in the prefix explicitly signals non-transferability:
B (not E which is for transferable self-addressing)1AAA (not 1AAB which can be transferable)1AAC (not 1AAD which can be transferable)The derivation code is the first character(s) of the prefix and must be validated by verifiers.
Implementations MUST enforce these constraints for non-transferable inception events:
nt) must be "0" - This is the primary signal of non-transferabilityn) must be empty - No pre-rotated key commitmentss) must be "0" - First event in KELt) must be "icp" - Inception eventFailure to enforce these constraints will allow creation of identifiers that appear non-transferable but could potentially be rotated.
When processing a KEL for a non-transferable identifier:
For non-transferable identifiers:
Key Event Log (KEL) Structure: A non-transferable identifier's KEL has a minimal structure:
The inception event for a non-transferable identifier includes:
{
"v": "KERI10JSON00011c_",
"t": "icp",
"d": "EXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148",
"i": "BXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148",
"s": "0",
"kt": "1",
"k": ["DXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148"],
"nt": "0",
"n": [],
"bt": "0",
"b": [],
"c": [],
"a": []
}
Key structural elements:
nt: "0" indicates zero next key threshold (no rotation capability)n: Empty array (no pre-rotated key commitments)kt: Current key threshold (typically "1" for single-sig)k: Array of current public keysDerivation Code: The derivation code in the prefix explicitly signals non-transferability. Common codes include:
B: Ed25519 non-transferable prefix1AAA: ECDSA secp256k1 non-transferable prefix1AAC: Ed448 non-transferable prefixThese codes are distinct from transferable identifier codes (like E for self-addressing transferable identifiers), allowing verifiers to immediately recognize the identifier's transferability status from the prefix alone.
Inception Configuration: The inception event's configuration fields establish the non-transferable nature:
nt): Set to "0" or omittedn): Empty arrayControl Authority: Control authority is permanently vested in the private key(s) corresponding to the public key(s) in the inception event. Loss or compromise of these keys results in permanent loss of control, as no recovery mechanism exists.
The inception event for a non-transferable identifier contains the following critical fields:
Version String (v): Specifies the KERI protocol version and serialization format (e.g., KERI10JSON00011c_)
Event Type (t): Always icp (inception) for the first and only establishment event
Event Digest (d): SAID of the inception event itself, providing content-addressability
Identifier Prefix (i): The non-transferable AID prefix, derived from the public key(s)
Sequence Number (s): Always "0" for inception
Current Key Threshold (kt): Number or fractional weight of signatures required from current keys
Current Keys (k): Array of qualified Base64-encoded public keys
Next Key Threshold (nt): Must be "0" for non-transferable identifiers
Next Keys (n): Must be empty array for non-transferable identifiers
Witness Threshold (bt): Number of witness receipts required (often "0" for non-transferable)
Witnesses (b): Array of witness AIDs (often empty for non-transferable)
Configuration Traits (c): Array of configuration strings (e.g., ["EO"] for establishment-only)
Anchors (a): Array of seals for anchoring external data
Non-transferable identifiers use CESR (Composable Event Streaming Representation) encoding for all cryptographic primitives:
Text Domain: The identifier prefix in text domain uses Base64 URL-safe encoding:
BXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148
B is the derivation codeBinary Domain: The same identifier in binary domain:
0x053e6b96a6ab8be3c65fd1fbbb2148b4d0945a536fd1c453f400b8e7c189f5f3
0x05 encodes the derivation codeSerialization: The inception event can be serialized in multiple formats:
All serializations maintain field ordering for deterministic SAID computation.
Prefix Length: Varies by cryptographic algorithm:
B): 44 characters (text) / 33 bytes (binary)1AAA): 48 characters (text) / 35 bytes (binary)1AAC): 80 characters (text) / 59 bytes (binary)KEL Size: Minimal compared to transferable identifiers:
Key Material:
Constraints:
nt must be "0", n must be emptyKey Generation: The first step in creating a non-transferable identifier is generating the cryptographic key pair:
import keri.core.coring as coring
import keri.core.eventing as eventing
# Generate a random salt for deterministic key derivation
salt = coring.Salter().qb64
# Create a key manager with the salt
mgr = keeping.Manager(ks=keystore, salt=salt)
# Generate key pair (icount=1 for single key, ncount=0 for no rotation keys)
verfers, digers, cst, nst = mgr.incept(icount=1, ncount=0)
The ncount=0 parameter explicitly signals that no pre-rotated keys should be generated, making this a non-transferable identifier.
Inception Event Creation: With the key pair generated, create the inception event:
# Create inception event with non-transferable configuration
srdr = eventing.incept(
keys=[verfers[0].qb64], # Current public key
code=coring.MtrDex.Ed25519_Seed, # Derivation code for Ed25519
nxt="", # Empty next key digest (non-transferable)
toad=0, # No witnesses required
wits=[] # No witness list
)
# The resulting prefix is non-transferable
aid_prefix = srdr.ked['i'] # e.g., "BXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148"
Keystore Storage: The private key must be securely stored:
# Store in encrypted keystore
keystore.put(keys=[(aid_prefix, verfers[0].qb64, signers[0].qb64)])
KEL Initialization: The inception event becomes the first and only establishment event in the KEL:
# Add inception event to KEL database
kever = eventing.Kever(serder=srdr, sigers=[siger], db=database)
Interaction Events: While non-transferable identifiers cannot rotate keys, they can create interaction events to anchor external data:
# Create interaction event
ixn_srdr = eventing.interact(
pre=aid_prefix, # Identifier prefix
dig=last_event_digest, # Digest of previous event
sn=sequence_number, # Incremented sequence number
data=[seal] # Seal anchoring external data
)
# Sign the interaction event
siger = signers[0].sign(ser=ixn_srdr.raw, index=0)
# Append to KEL
kever.update(serder=ixn_srdr, sigers=[siger])
Interaction events maintain the KEL's append-only property and enable non-transferable identifiers to participate in KERI's data anchoring mechanisms without changing key state.
No Rotation Operations: Attempting to create a rotation event for a non-transferable identifier will fail validation:
# This will raise an error
try:
rot_srdr = eventing.rotate(
pre=aid_prefix,
keys=[new_verfer.qb64],
dig=last_event_digest,
sn=sequence_number,
nxt=next_key_digest
)
except ValueError as e:
print(f"Rotation not allowed: {e}")
# Error: Non-transferable identifier cannot rotate keys
Abandonment: A non-transferable identifier can be explicitly abandoned by creating a final interaction event with specific configuration, though this is rarely necessary since the identifier is already non-transferable:
# Create abandonment interaction event
abandon_srdr = eventing.interact(
pre=aid_prefix,
dig=last_event_digest,
sn=sequence_number,
data=[],
config=["EO"] # Establishment-only configuration
)
Prefix Validation: Verifiers must check that the prefix correctly derives from the public key:
def verify_non_transferable_prefix(prefix: str, public_key: str) -> bool:
"""
Verify that a non-transferable prefix correctly derives from its public key.
"""
# Extract derivation code
code = prefix[0]
# Verify it's a non-transferable code
if code not in ['B', '1AAA', '1AAC']: # Non-transferable codes
return False
# Recompute prefix from public key
verfer = coring.Verfer(qb64=public_key)
computed_prefix = verfer.qb64
# Compare with provided prefix
return prefix == computed_prefix
Inception Event Validation: Verify the inception event structure:
def validate_non_transferable_inception(event: dict) -> bool:
"""
Validate that an inception event creates a non-transferable identifier.
"""
# Check event type
if event['t'] != 'icp':
return False
# Check sequence number
if event['s'] != '0':
return False
# Check next key threshold is zero
if event['nt'] != '0':
return False
# Check next keys array is empty
if len(event['n']) != 0:
return False
# Verify prefix derives from keys
prefix = event['i']
keys = event['k']
# For single-sig, prefix should match first key
if len(keys) == 1:
return verify_non_transferable_prefix(prefix, keys[0])
return True
Signature Verification: Verify signatures on events:
def verify_event_signature(event_serder, siger, verfer):
"""
Verify a signature on a non-transferable identifier event.
"""
# Verify signature using public key
return verfer.verify(sig=siger.raw, ser=event_serder.raw)
KEL Validation: Validate the entire KEL for a non-transferable identifier:
def validate_non_transferable_kel(kel: list) -> bool:
"""
Validate a complete KEL for a non-transferable identifier.
"""
if len(kel) == 0:
return False
# First event must be inception
if not validate_non_transferable_inception(kel[0]):
return False
# Extract prefix and keys from inception
prefix = kel[0]['i']
keys = kel[0]['k']
# Verify no rotation events exist
for event in kel[1:]:
if event['t'] == 'rot':
return False # Rotation not allowed
# Verify event prefix matches
if event['i'] != prefix:
return False
# Verify sequence numbers increment
# Verify digest chaining
# Verify signatures
return True
KERI Core Protocol: Non-transferable identifiers are fundamental primitives in KERI, used for:
# Create witness with non-transferable identifier
witness_verfers, _, _, _ = mgr.incept(icount=1, ncount=0)
witness_srdr = eventing.incept(
keys=[witness_verfers[0].qb64],
code=coring.MtrDex.Ed25519,
nxt="",
toad=0,
wits=[]
)
witness_aid = witness_srdr.ked['i']
Ephemeral Session Identifiers: For temporary peer-to-peer connections:
Device-Specific Identifiers: For IoT devices or hardware tokens:
ACDC (Authentic Chained Data Container): Non-transferable identifiers can serve as:
{
"v": "ACDC10JSON00011c_",
"d": "EXq5YqaL6L48pf0fu7IUhL0JRaU2_RxFP0AL43wYn148",
"i": "BIssuerAID...",
"s": "ESchemaAID...",
"a": {
"d": "EAttributeBlockSAID...",
"i": "BNonTransferableSubjectAID...",
"dt": "2024-01-15T12:00:00.000000+00:00",
"LEI": "254900OPPU84GM83MG36"
}
}
OOBI (Out-Of-Band Introduction): Non-transferable identifiers in OOBI:
# OOBI for non-transferable witness
oobi = {
"eid": "BNonTransferableWitnessAID...",
"scheme": "http",
"url": "http://witness.example.com:5642/"
}
DID:WEBS Method: Non-transferable identifiers can be used in did:webs for:
Creation Phase:
nt=0 and empty n arrayActive Phase:
Termination Phase:
Key Compromise Handling:
Unlike transferable identifiers, non-transferable identifiers have no recovery mechanism from key compromise:
def handle_non_transferable_compromise(aid_prefix: str):
"""
Handle compromise of non-transferable identifier keys.
"""
# 1. Immediately stop using the identifier
revoke_identifier_usage(aid_prefix)
# 2. Notify relying parties (out-of-band)
notify_relying_parties(aid_prefix, "COMPROMISED")
# 3. Create new non-transferable identifier
new_aid = create_new_non_transferable_identifier()
# 4. Migrate to new identifier
migrate_to_new_identifier(old_aid=aid_prefix, new_aid=new_aid)
# 5. Securely delete compromised keys
secure_delete_keys(aid_prefix)
return new_aid
This lack of recovery is a fundamental trade-off: non-transferable identifiers are simpler and more efficient, but require replacement rather than rotation when compromised.
Transferable Identifiers: The complementary identifier type that supports key rotation:
Delegated Identifiers: Non-transferable identifiers can be delegation targets, but cannot themselves delegate:
# A transferable identifier can delegate to a non-transferable identifier
delegation_seal = {
"i": "BNonTransferableDelegate...",
"s": "0",
"d": "EDelegateInceptionDigest..."
}
# But the non-transferable identifier cannot further delegate
Multi-Sig Identifiers: Non-transferable identifiers can use multi-sig:
# Create multi-sig non-transferable identifier
verfers, _, _, _ = mgr.incept(icount=3, ncount=0) # 3 keys, no rotation
srdr = eventing.incept(
keys=[v.qb64 for v in verfers],
code=coring.MtrDex.Ed25519,
nxt="",
toad=0,
wits=[],
isith="2" # 2-of-3 threshold
)
Witness Pools: Non-transferable identifiers typically don't use witnesses, but can:
# Non-transferable identifier with witnesses (rare)
srdr = eventing.incept(
keys=[verfers[0].qb64],
code=coring.MtrDex.Ed25519,
nxt="",
toad=2, # Require 2 witness receipts
wits=["BWitness1...", "BWitness2...", "BWitness3..."]
)
However, witnesses provide no additional security for non-transferable identifiers since there's no key rotation to witness.
Key Compromise: The primary security risk for non-transferable identifiers:
Impersonation Attacks: Attackers cannot create valid events without the private key:
Replay Attacks: Old events could be replayed:
Use Case Selection: Only use non-transferable identifiers when:
Key Management:
Operational Security:
Architecture Decisions:
Creation Speed: Non-transferable identifiers are faster to create:
Verification Speed: Verification is simpler:
Storage Efficiency: Minimal storage requirements:
Network Efficiency: Reduced bandwidth:
| Aspect | Non-Transferable | Transferable |
|---|---|---|
| Key Rotation | Not supported | Supported via pre-rotation |
| Recovery | No recovery mechanism | Can recover from key compromise |
| Lifecycle | Ephemeral (hours-days) | Persistent (years-decades) |
| Complexity | Simple | Complex |
| Storage | Minimal | Larger (rotation history) |
| Performance | Faster | Slower (rotation overhead) |
| Use Cases | Sessions, devices, infrastructure | Organizations, long-lived identities |
| Security Model | Replace on compromise | Rotate on compromise |
| Witness Requirement | Optional (rarely used) | Recommended for security |
| Delegation | Can be delegate, cannot delegate | Can delegate and be delegated |
Non-transferable identifiers represent a fundamental trade-off in KERI's design: simplicity and efficiency in exchange for persistence and recoverability. They are essential for scenarios where identifiers are truly ephemeral, where replacement is preferable to rotation, or where the operational complexity of key rotation is unjustified by the security requirements.
The key insight is that not all identifiers need to be persistent. By providing both transferable and non-transferable identifier types as composable primitives, KERI enables architects to choose the appropriate identifier type for each use case, optimizing the balance between security, complexity, and operational efficiency.
For production systems, the general guidance is:
Understanding non-transferable identifiers is essential for KERI developers, as they form one of the three core identifier primitives (along with transferable and delegated identifiers) that compose KERI's flexible, scalable key management infrastructure.
Non-transferable identifiers enable several optimizations:
Key Compromise: The primary risk is that compromised keys cannot be rotated:
Use Case Selection: Only use non-transferable identifiers when:
While non-transferable identifiers CAN use witnesses, this is rarely beneficial:
toad=0, wits=[] (no witnesses)Non-transferable identifiers have limited delegation capabilities:
Implementations should include tests for:
E instead of B for Ed25519When migrating from a non-transferable to a transferable identifier:
When implementing non-transferable identifiers for interoperability: