Skip to main content

auths_id/keri/
inception.rs

1//! KERI identity inception with proper pre-rotation.
2//!
3//! Creates a new KERI identity by:
4//! 1. Generating two Ed25519 keypairs (current + next)
5//! 2. Computing next-key commitment
6//! 3. Building and finalizing inception event with SAID
7//! 4. Storing event in Git-backed KEL
8
9use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
10use git2::Repository;
11use ring::rand::SystemRandom;
12use ring::signature::{Ed25519KeyPair, KeyPair};
13
14use crate::storage::registry::backend::{RegistryBackend, RegistryError};
15use auths_crypto::Pkcs8Der;
16
17use auths_core::crypto::said::compute_next_commitment;
18
19use super::event::KeriSequence;
20use super::types::{Prefix, Said};
21use super::{Event, GitKel, IcpEvent, KERI_VERSION, KelError, ValidationError, finalize_icp_event};
22use crate::witness_config::WitnessConfig;
23
24/// Error type for inception operations.
25#[derive(Debug, thiserror::Error)]
26#[non_exhaustive]
27pub enum InceptionError {
28    #[error("Key generation failed: {0}")]
29    KeyGeneration(String),
30
31    #[error("KEL error: {0}")]
32    Kel(#[from] KelError),
33
34    #[error("Storage error: {0}")]
35    Storage(RegistryError),
36
37    #[error("Validation error: {0}")]
38    Validation(#[from] ValidationError),
39
40    #[error("Serialization error: {0}")]
41    Serialization(String),
42}
43
44impl auths_core::error::AuthsErrorInfo for InceptionError {
45    fn error_code(&self) -> &'static str {
46        match self {
47            Self::KeyGeneration(_) => "AUTHS-E4901",
48            Self::Kel(_) => "AUTHS-E4902",
49            Self::Storage(_) => "AUTHS-E4903",
50            Self::Validation(_) => "AUTHS-E4904",
51            Self::Serialization(_) => "AUTHS-E4905",
52        }
53    }
54
55    fn suggestion(&self) -> Option<&'static str> {
56        match self {
57            Self::KeyGeneration(_) => None,
58            Self::Kel(_) => Some("Check the KEL state; a KEL may already exist for this prefix"),
59            Self::Storage(_) => Some("Check storage backend connectivity"),
60            Self::Validation(_) => None,
61            Self::Serialization(_) => None,
62        }
63    }
64}
65
66/// Result of a KERI identity inception.
67pub struct InceptionResult {
68    /// The KERI prefix (use with `did:keri:<prefix>`)
69    pub prefix: Prefix,
70
71    /// The current signing keypair (PKCS8 DER encoded, zeroed on drop)
72    pub current_keypair_pkcs8: Pkcs8Der,
73
74    /// The next rotation keypair (PKCS8 DER encoded, zeroed on drop)
75    pub next_keypair_pkcs8: Pkcs8Der,
76
77    /// The current public key (raw 32 bytes)
78    pub current_public_key: Vec<u8>,
79
80    /// The next public key (raw 32 bytes)
81    pub next_public_key: Vec<u8>,
82}
83
84impl std::fmt::Debug for InceptionResult {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        f.debug_struct("InceptionResult")
87            .field("prefix", &self.prefix)
88            .field("current_keypair_pkcs8", &self.current_keypair_pkcs8)
89            .field("next_keypair_pkcs8", &self.next_keypair_pkcs8)
90            .field("current_public_key", &self.current_public_key)
91            .field("next_public_key", &self.next_public_key)
92            .finish()
93    }
94}
95
96impl InceptionResult {
97    /// Get the full DID for this identity.
98    pub fn did(&self) -> String {
99        format!("did:keri:{}", self.prefix.as_str())
100    }
101}
102
103/// Create a new KERI identity with proper pre-rotation.
104///
105/// This generates two Ed25519 keypairs:
106/// - Current key: used for immediate signing
107/// - Next key: committed to in the inception event for future rotation
108///
109/// The inception event is stored in the Git repository at:
110/// `refs/did/keri/<prefix>/kel`
111///
112/// # Arguments
113/// * `repo` - Git repository for KEL storage
114/// * `witness_config` - Optional witness configuration. When provided and
115///   enabled, the inception event's `bt`/`b` fields are set accordingly.
116///
117/// # Returns
118/// * `InceptionResult` containing the prefix and both keypairs
119pub fn create_keri_identity(
120    repo: &Repository,
121    witness_config: Option<&WitnessConfig>,
122    now: chrono::DateTime<chrono::Utc>,
123) -> Result<InceptionResult, InceptionError> {
124    let rng = SystemRandom::new();
125
126    // Generate current keypair
127    let current_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
128        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
129    let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8.as_ref())
130        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
131
132    // Generate next keypair (for pre-rotation)
133    let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
134        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
135    let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref())
136        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
137
138    // Encode current public key with derivation code prefix
139    // 'D' prefix indicates Ed25519 in KERI
140    let current_pub_encoded = format!(
141        "D{}",
142        URL_SAFE_NO_PAD.encode(current_keypair.public_key().as_ref())
143    );
144
145    // Compute next-key commitment (Blake3 hash of next public key)
146    let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
147
148    // Determine witness fields from config
149    let (bt, b) = match witness_config {
150        Some(cfg) if cfg.is_enabled() => (
151            cfg.threshold.to_string(),
152            cfg.witness_urls.iter().map(|u| u.to_string()).collect(),
153        ),
154        _ => ("0".to_string(), vec![]),
155    };
156
157    // Build inception event (without SAID)
158    let icp = IcpEvent {
159        v: KERI_VERSION.to_string(),
160        d: Said::default(),
161        i: Prefix::default(),
162        s: KeriSequence::new(0),
163        kt: "1".to_string(),
164        k: vec![current_pub_encoded],
165        nt: "1".to_string(),
166        n: vec![next_commitment],
167        bt,
168        b,
169        a: vec![],
170        x: String::new(),
171    };
172
173    // Finalize event (computes and sets SAID)
174    let mut finalized = finalize_icp_event(icp)?;
175    let prefix = finalized.i.clone();
176
177    // Sign the event with the current key
178    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
179    let sig = current_keypair.sign(&canonical);
180    finalized.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
181
182    // Store in Git KEL
183    let kel = GitKel::new(repo, prefix.as_str());
184    kel.create(&finalized, now)?;
185
186    // Collect witness receipts if configured
187    #[cfg(feature = "witness-client")]
188    if let Some(config) = witness_config
189        && config.is_enabled()
190    {
191        let canonical_for_witness = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
192        super::witness_integration::collect_and_store_receipts(
193            repo.path().parent().unwrap_or(repo.path()),
194            &prefix,
195            &finalized.d,
196            &canonical_for_witness,
197            config,
198            now,
199        )
200        .map_err(|e| InceptionError::Serialization(e.to_string()))?;
201    }
202
203    Ok(InceptionResult {
204        prefix,
205        current_keypair_pkcs8: Pkcs8Der::new(current_pkcs8.as_ref()),
206        next_keypair_pkcs8: Pkcs8Der::new(next_pkcs8.as_ref()),
207        current_public_key: current_keypair.public_key().as_ref().to_vec(),
208        next_public_key: next_keypair.public_key().as_ref().to_vec(),
209    })
210}
211
212/// Create a new KERI identity using any [`RegistryBackend`].
213///
214/// Identical logic to [`create_keri_identity`] but stores the inception event
215/// via the provided backend instead of a git repository.
216///
217/// # Arguments
218/// * `backend` - The registry backend to store the inception event
219/// * `witness_config` - Unused in the backend path; kept for API symmetry
220///
221/// # Returns
222/// * `InceptionResult` containing the prefix and both keypairs
223pub fn create_keri_identity_with_backend(
224    backend: &impl RegistryBackend,
225    _witness_config: Option<&WitnessConfig>,
226) -> Result<InceptionResult, InceptionError> {
227    let rng = SystemRandom::new();
228
229    let current_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
230        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
231    let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8.as_ref())
232        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
233
234    let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
235        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
236    let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref())
237        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
238
239    let current_pub_encoded = format!(
240        "D{}",
241        URL_SAFE_NO_PAD.encode(current_keypair.public_key().as_ref())
242    );
243    let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
244
245    let icp = IcpEvent {
246        v: KERI_VERSION.to_string(),
247        d: Said::default(),
248        i: Prefix::default(),
249        s: KeriSequence::new(0),
250        kt: "1".to_string(),
251        k: vec![current_pub_encoded],
252        nt: "1".to_string(),
253        n: vec![next_commitment],
254        bt: "0".to_string(),
255        b: vec![],
256        a: vec![],
257        x: String::new(),
258    };
259
260    let mut finalized = finalize_icp_event(icp)?;
261    let prefix = finalized.i.clone();
262
263    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
264    let sig = current_keypair.sign(&canonical);
265    finalized.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
266
267    backend
268        .append_event(&prefix, &Event::Icp(finalized))
269        .map_err(InceptionError::Storage)?;
270
271    Ok(InceptionResult {
272        prefix,
273        current_keypair_pkcs8: Pkcs8Der::new(current_pkcs8.as_ref()),
274        next_keypair_pkcs8: Pkcs8Der::new(next_pkcs8.as_ref()),
275        current_public_key: current_keypair.public_key().as_ref().to_vec(),
276        next_public_key: next_keypair.public_key().as_ref().to_vec(),
277    })
278}
279
280/// Create a KERI identity from an existing Ed25519 key (PKCS8 DER).
281///
282/// Used for migrating existing `did:key` identities to `did:keri`.
283/// The provided key becomes the current signing key; a new next key
284/// is generated for pre-rotation.
285///
286/// # Arguments
287/// * `repo` - Git repository for KEL storage
288/// * `current_pkcs8_bytes` - Existing Ed25519 key in PKCS8 v2 DER format
289/// * `witness_config` - Optional witness configuration
290/// * `now` - Timestamp for the inception event
291pub fn create_keri_identity_from_key(
292    repo: &Repository,
293    current_pkcs8_bytes: &[u8],
294    witness_config: Option<&WitnessConfig>,
295    now: chrono::DateTime<chrono::Utc>,
296) -> Result<InceptionResult, InceptionError> {
297    let rng = SystemRandom::new();
298
299    // Use the provided key as the current keypair
300    let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8_bytes)
301        .map_err(|e| InceptionError::KeyGeneration(format!("invalid PKCS8 key: {e}")))?;
302
303    // Generate next keypair (for pre-rotation)
304    let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
305        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
306    let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref())
307        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
308
309    let current_pub_encoded = format!(
310        "D{}",
311        URL_SAFE_NO_PAD.encode(current_keypair.public_key().as_ref())
312    );
313    let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
314
315    let (bt, b) = match witness_config {
316        Some(cfg) if cfg.is_enabled() => (
317            cfg.threshold.to_string(),
318            cfg.witness_urls.iter().map(|u| u.to_string()).collect(),
319        ),
320        _ => ("0".to_string(), vec![]),
321    };
322
323    let icp = IcpEvent {
324        v: KERI_VERSION.to_string(),
325        d: Said::default(),
326        i: Prefix::default(),
327        s: KeriSequence::new(0),
328        kt: "1".to_string(),
329        k: vec![current_pub_encoded],
330        nt: "1".to_string(),
331        n: vec![next_commitment],
332        bt,
333        b,
334        a: vec![],
335        x: String::new(),
336    };
337
338    let mut finalized = finalize_icp_event(icp)?;
339    let prefix = finalized.i.clone();
340
341    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
342    let sig = current_keypair.sign(&canonical);
343    finalized.x = URL_SAFE_NO_PAD.encode(sig.as_ref());
344
345    let kel = GitKel::new(repo, prefix.as_str());
346    kel.create(&finalized, now)?;
347
348    Ok(InceptionResult {
349        prefix,
350        current_keypair_pkcs8: Pkcs8Der::new(current_pkcs8_bytes),
351        next_keypair_pkcs8: Pkcs8Der::new(next_pkcs8.as_ref()),
352        current_public_key: current_keypair.public_key().as_ref().to_vec(),
353        next_public_key: next_keypair.public_key().as_ref().to_vec(),
354    })
355}
356
357/// Format a KERI prefix as a full DID.
358pub fn prefix_to_did(prefix: &str) -> String {
359    format!("did:keri:{}", prefix)
360}
361
362/// Extract the prefix from a did:keri DID.
363///
364/// Prefer [`auths_verifier::IdentityDID`] at API boundaries for type safety.
365pub fn did_to_prefix(did: &str) -> Option<&str> {
366    did.strip_prefix("did:keri:")
367}
368
369#[cfg(test)]
370#[allow(clippy::disallowed_methods)]
371mod tests {
372    use super::*;
373    use crate::keri::{Event, validate_kel};
374    use tempfile::TempDir;
375
376    fn setup_repo() -> (TempDir, Repository) {
377        let dir = TempDir::new().unwrap();
378        let repo = Repository::init(dir.path()).unwrap();
379
380        // Set git config for CI environments
381        let mut config = repo.config().unwrap();
382        config.set_str("user.name", "Test User").unwrap();
383        config.set_str("user.email", "test@example.com").unwrap();
384
385        (dir, repo)
386    }
387
388    #[test]
389    fn create_identity_returns_valid_result() {
390        let (_dir, repo) = setup_repo();
391
392        let result = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
393
394        // Prefix should start with 'E' (Blake3 SAID prefix)
395        assert!(result.prefix.as_str().starts_with('E'));
396
397        // Keys should be present
398        assert!(!result.current_keypair_pkcs8.is_empty());
399        assert!(!result.next_keypair_pkcs8.is_empty());
400        assert_eq!(result.current_public_key.len(), 32);
401        assert_eq!(result.next_public_key.len(), 32);
402
403        // DID should be formatted correctly
404        assert!(result.did().starts_with("did:keri:E"));
405    }
406
407    #[test]
408    fn create_identity_stores_kel() {
409        let (_dir, repo) = setup_repo();
410
411        let result = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
412
413        // Verify KEL exists and has one event
414        let kel = GitKel::new(&repo, result.prefix.as_str());
415        assert!(kel.exists());
416
417        let events = kel.get_events().unwrap();
418        assert_eq!(events.len(), 1);
419        assert!(events[0].is_inception());
420    }
421
422    #[test]
423    fn inception_event_is_valid() {
424        let (_dir, repo) = setup_repo();
425
426        let result = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
427        let kel = GitKel::new(&repo, result.prefix.as_str());
428        let events = kel.get_events().unwrap();
429
430        // Validate the KEL
431        let state = validate_kel(&events).unwrap();
432        assert_eq!(state.prefix, result.prefix);
433        assert_eq!(state.sequence, 0);
434        assert!(!state.is_abandoned);
435    }
436
437    #[test]
438    fn inception_event_has_correct_structure() {
439        let (_dir, repo) = setup_repo();
440
441        let result = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
442        let kel = GitKel::new(&repo, result.prefix.as_str());
443        let events = kel.get_events().unwrap();
444
445        if let Event::Icp(icp) = &events[0] {
446            // Version
447            assert_eq!(icp.v, KERI_VERSION);
448
449            // SAID equals prefix
450            assert_eq!(icp.d.as_str(), icp.i.as_str());
451            assert_eq!(icp.d.as_str(), result.prefix.as_str());
452
453            // Sequence is 0
454            assert_eq!(icp.s, KeriSequence::new(0));
455
456            // Single key
457            assert_eq!(icp.k.len(), 1);
458            assert!(icp.k[0].starts_with('D')); // Ed25519 prefix
459
460            // Single next commitment
461            assert_eq!(icp.n.len(), 1);
462            assert!(icp.n[0].starts_with('E')); // Blake3 hash prefix
463
464            // No witnesses
465            assert_eq!(icp.bt, "0");
466            assert!(icp.b.is_empty());
467        } else {
468            panic!("Expected inception event");
469        }
470    }
471
472    #[test]
473    fn next_key_commitment_is_correct() {
474        let (_dir, repo) = setup_repo();
475
476        let result = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
477        let kel = GitKel::new(&repo, result.prefix.as_str());
478        let events = kel.get_events().unwrap();
479
480        if let Event::Icp(icp) = &events[0] {
481            // Verify the next commitment matches the next public key
482            let expected_commitment = compute_next_commitment(&result.next_public_key);
483            assert_eq!(icp.n[0], expected_commitment);
484        } else {
485            panic!("Expected inception event");
486        }
487    }
488
489    #[test]
490    fn prefix_to_did_works() {
491        assert_eq!(prefix_to_did("ETest123"), "did:keri:ETest123");
492    }
493
494    #[test]
495    fn did_to_prefix_works() {
496        assert_eq!(did_to_prefix("did:keri:ETest123"), Some("ETest123"));
497        assert_eq!(did_to_prefix("did:key:z6Mk..."), None);
498    }
499
500    #[test]
501    fn multiple_identities_have_different_prefixes() {
502        let (_dir, repo) = setup_repo();
503
504        let result1 = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
505
506        // Create second repo for second identity
507        let (_dir2, repo2) = setup_repo();
508        let result2 = create_keri_identity(&repo2, None, chrono::Utc::now()).unwrap();
509
510        // Prefixes should be different
511        assert_ne!(result1.prefix, result2.prefix);
512    }
513}