identity-model.md
1 # Identity Model 2 3 KERI-inspired identity management: inception events, the Key Event Log, DID derivation, and key rotation. 4 5 ## Core Concepts 6 7 Auths uses three KERI (Key Event Receipt Infrastructure) concepts adapted for Git-native storage: 8 9 1. **Self-certifying identifiers** -- the identity DID is derived from the inception event's content hash, making it cryptographically bound to its creation parameters. 10 11 2. **Pre-rotation** -- each event commits to the *next* rotation key before it is needed. A compromised current key cannot forge a rotation because the next key was committed in a previous event. 12 13 3. **Key Event Log (KEL)** -- an append-only, hash-chained sequence of events that records every key lifecycle operation. Replaying the KEL produces the current key state. 14 15 ## Identity vs. Device 16 17 !!! info "The one thing to remember" 18 **Your identity is not your key.** Keys live on devices and can be rotated. Your identity (`did:keri:E...`) is permanent and survives key changes. 19 20 ```mermaid 21 flowchart TD 22 A["<strong>IDENTITY</strong><br/>did:keri:E...<br/><small>Permanent, derived from KEL</small>"] 23 B["<strong>DEVICE A</strong><br/>did:key:z6Mk...<br/><small>laptop</small>"] 24 C["<strong>DEVICE B</strong><br/>did:key:z6Mk...<br/><small>phone</small>"] 25 26 A -->|"signed attestation"| B 27 A -->|"signed attestation"| C 28 ``` 29 30 - **Identity** (`did:keri:E...`): Your stable cryptographic identifier. Derived from the inception event's SAID. Survives key rotation. 31 - **Device** (`did:key:z6Mk...`): A per-machine Ed25519 keypair. Devices are instruments that act on behalf of an identity, authorized via signed attestations. 32 33 ## DID Derivation 34 35 ### did:keri (identity) 36 37 The `did:keri` identifier is derived from the inception event: 38 39 1. Build the inception event JSON with `d` and `i` fields set to empty defaults 40 2. Compute the Blake3 hash of that JSON 41 3. Encode as Base64url (no padding) with an `E` prefix (KERI derivation code for Blake3-256) 42 4. Set both `d` (SAID) and `i` (prefix) to this value -- for inception, they are identical 43 44 ``` 45 Inception JSON (d="", i="") --> Blake3 --> Base64url --> "E" + encoded 46 | 47 v 48 did:keri:EXq5Yqa... 49 ``` 50 51 The SAID is 44 characters: `E` prefix + 43 characters of Base64url-encoded Blake3 hash. 52 53 From source (`auths-core/src/crypto/said.rs`): 54 55 ```rust 56 pub fn compute_said(event_json: &[u8]) -> Said { 57 let hash = blake3::hash(event_json); 58 let encoded = URL_SAFE_NO_PAD.encode(hash.as_bytes()); 59 Said::new_unchecked(format!("E{}", encoded)) 60 } 61 ``` 62 63 ### did:key (device) 64 65 Device identifiers use the `did:key` method, which encodes the public key directly in the DID string: 66 67 1. Prepend the Ed25519 multicodec prefix `[0xED, 0x01]` to the 32-byte public key 68 2. Encode as Base58btc 69 3. Prepend `did:key:z` 70 71 ``` 72 32-byte Ed25519 pubkey --> [0xED, 0x01] ++ pubkey --> Base58btc --> "did:key:z" + encoded 73 ``` 74 75 From source (`auths-crypto/src/did_key.rs`): 76 77 ```rust 78 pub fn ed25519_pubkey_to_did_key(public_key: &[u8; 32]) -> String { 79 let mut prefixed = vec![0xED, 0x01]; 80 prefixed.extend_from_slice(public_key); 81 let encoded = bs58::encode(prefixed).into_string(); 82 format!("did:key:z{encoded}") 83 } 84 ``` 85 86 Decoding reverses this process: strip `did:key:z`, Base58-decode, validate the `[0xED, 0x01]` multicodec prefix, extract the 32-byte key. 87 88 ## KERI Event Types 89 90 The KEL contains three event types, discriminated by a `t` field: 91 92 ### Inception Event (`icp`) 93 94 Creates a new identity. The inception event establishes the identifier prefix and commits to the first rotation key. 95 96 | Field | Type | Description | 97 |-------|------|-------------| 98 | `v` | string | Version: `"KERI10JSON"` | 99 | `t` | string | Type: `"icp"` | 100 | `d` | string | SAID (Blake3 hash of event with `d`, `i`, `x` cleared) | 101 | `i` | string | Identifier prefix (same as `d` for inception) | 102 | `s` | string | Sequence number: `"0"` | 103 | `kt` | string | Key threshold: `"1"` for single-sig | 104 | `k` | string[] | Current public key(s), KERI CESR encoded (`D` + Base64url) | 105 | `nt` | string | Next key threshold: `"1"` | 106 | `n` | string[] | Next key commitment(s) (Blake3 hash of next public key) | 107 | `bt` | string | Witness threshold: `"0"` when no witnesses | 108 | `b` | string[] | Witness list (empty when no witnesses) | 109 | `a` | Seal[] | Anchored seals (optional) | 110 | `x` | string | Ed25519 signature over canonical event (Base64url) | 111 112 ### Rotation Event (`rot`) 113 114 Rotates to a pre-committed key. The new key must match the previous event's next-key commitment. 115 116 | Field | Type | Description | 117 |-------|------|-------------| 118 | `v` | string | Version: `"KERI10JSON"` | 119 | `t` | string | Type: `"rot"` | 120 | `d` | string | SAID of this event | 121 | `i` | string | Identifier prefix | 122 | `s` | string | Sequence number (increments with each event) | 123 | `p` | string | Previous event SAID (creates the hash chain) | 124 | `kt` | string | Key threshold | 125 | `k` | string[] | New current key(s) | 126 | `nt` | string | Next key threshold | 127 | `n` | string[] | New next key commitment(s) | 128 | `bt` | string | Witness threshold | 129 | `b` | string[] | Witness list | 130 | `a` | Seal[] | Anchored seals (optional) | 131 | `x` | string | Signature by the **new** key (the key that satisfied the commitment) | 132 133 Setting `nt` to `"0"` and `n` to `[]` abandons the identity -- no further rotations are possible. 134 135 ### Interaction Event (`ixn`) 136 137 Anchors data in the KEL without changing keys. Used to link attestations, delegations, or other artifacts to the identity's event history. 138 139 | Field | Type | Description | 140 |-------|------|-------------| 141 | `v` | string | Version: `"KERI10JSON"` | 142 | `t` | string | Type: `"ixn"` | 143 | `d` | string | SAID of this event | 144 | `i` | string | Identifier prefix | 145 | `s` | string | Sequence number | 146 | `p` | string | Previous event SAID | 147 | `a` | Seal[] | Anchored seals (the primary purpose of IXN events) | 148 | `x` | string | Signature by the current key | 149 150 ## Key Event Log (KEL) 151 152 The KEL is an append-only, hash-chained sequence of events stored as Git commits: 153 154 ``` 155 icp (s=0) --> rot (s=1) --> ixn (s=2) --> rot (s=3) --> ... 156 d=E_abc d=E_def d=E_ghi d=E_jkl 157 p=E_abc p=E_def p=E_ghi 158 ``` 159 160 ### Chain Integrity 161 162 Each event references the previous event's SAID via the `p` field, forming a verifiable hash chain. Breaking any link invalidates all subsequent events. 163 164 ### SAID Computation 165 166 The SAID is computed by: 167 168 1. Clearing the `d`, `x`, and (for inception) `i` fields 169 2. Serializing to JSON 170 3. Computing Blake3-256 hash 171 4. Encoding as `E` + Base64url (no padding) 172 173 This same canonical form (with `d`, `i`, `x` cleared) is used for signature computation, avoiding circular dependencies between SAID and signature. 174 175 ### Pre-Rotation 176 177 The `n` field in each event contains a **commitment** to the next rotation key: 178 179 ``` 180 commitment = "E" + Base64url(Blake3(next_public_key_bytes)) 181 ``` 182 183 When a rotation event appears, the verifier: 184 185 1. Extracts the new key from `k[0]` 186 2. Computes `Blake3(new_key_bytes)` 187 3. Compares with the previous event's `n[0]` 188 4. Only accepts the rotation if they match 189 190 This prevents a compromised current key from forging a rotation -- the attacker would need to know the pre-committed next key. 191 192 ## Key State 193 194 Replaying the KEL produces a `KeyState` struct: 195 196 ```rust 197 pub struct KeyState { 198 pub prefix: Prefix, // KERI identifier (used in did:keri:<prefix>) 199 pub current_keys: Vec<String>, // Current signing key(s), CESR encoded 200 pub next_commitment: Vec<String>, // Next key commitment(s) for pre-rotation 201 pub sequence: u64, // Current sequence number 202 pub last_event_said: Said, // SAID of the last processed event 203 pub is_abandoned: bool, // Empty next commitment = abandoned 204 } 205 ``` 206 207 Key state is derived, not stored. It is computed by replaying events from inception. The `validate_kel` function is a pure function with no I/O: 208 209 ```rust 210 pub fn validate_kel(events: &[Event]) -> Result<KeyState, ValidationError> 211 ``` 212 213 For performance, a three-tier caching strategy avoids full replay on every access: 214 215 1. **Cache hit**: Cached state matches current tip -- return immediately (O(1)) 216 2. **Incremental**: Cache is behind by k events -- validate only new events (O(k)) 217 3. **Full replay**: Cache missing or invalid -- replay entire KEL (O(n)) 218 219 ## Seals 220 221 Seals anchor external data in KERI events. They contain a digest of the anchored artifact and a type indicator: 222 223 ```json 224 { "d": "EAttestDigest...", "type": "device-attestation" } 225 ``` 226 227 Seal types include: 228 229 | Type | Purpose | 230 |------|---------| 231 | `device-attestation` | Links a device attestation to the KEL | 232 | `revocation` | Records a revocation in the KEL | 233 | `delegation` | Records a capability delegation | 234 235 Seals appear in the `a` field of any event type, binding the external artifact's integrity to the identity's event history. 236 237 ## KERI Key Encoding 238 239 Public keys in KERI events use CESR (Composable Event Streaming Representation) encoding: 240 241 - **Prefix**: `D` -- derivation code for Ed25519 242 - **Payload**: Base64url (no padding) encoded 32-byte public key 243 244 ``` 245 "D" + Base64url(ed25519_public_key_bytes) 246 ``` 247 248 Parsing a KERI-encoded key (`auths-crypto/src/keri.rs`): 249 250 1. Validate the `D` prefix 251 2. Base64url-decode the remaining characters 252 3. Validate the result is exactly 32 bytes