1use 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#[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
66pub struct InceptionResult {
68 pub prefix: Prefix,
70
71 pub current_keypair_pkcs8: Pkcs8Der,
73
74 pub next_keypair_pkcs8: Pkcs8Der,
76
77 pub current_public_key: Vec<u8>,
79
80 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 pub fn did(&self) -> String {
99 format!("did:keri:{}", self.prefix.as_str())
100 }
101}
102
103pub 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 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 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 let current_pub_encoded = format!(
141 "D{}",
142 URL_SAFE_NO_PAD.encode(current_keypair.public_key().as_ref())
143 );
144
145 let next_commitment = compute_next_commitment(next_keypair.public_key().as_ref());
147
148 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 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 let mut finalized = finalize_icp_event(icp)?;
175 let prefix = finalized.i.clone();
176
177 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 let kel = GitKel::new(repo, prefix.as_str());
184 kel.create(&finalized, now)?;
185
186 #[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
212pub 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
280pub 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 let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8_bytes)
301 .map_err(|e| InceptionError::KeyGeneration(format!("invalid PKCS8 key: {e}")))?;
302
303 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
357pub fn prefix_to_did(prefix: &str) -> String {
359 format!("did:keri:{}", prefix)
360}
361
362pub 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 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 assert!(result.prefix.as_str().starts_with('E'));
396
397 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 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 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 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 assert_eq!(icp.v, KERI_VERSION);
448
449 assert_eq!(icp.d.as_str(), icp.i.as_str());
451 assert_eq!(icp.d.as_str(), result.prefix.as_str());
452
453 assert_eq!(icp.s, KeriSequence::new(0));
455
456 assert_eq!(icp.k.len(), 1);
458 assert!(icp.k[0].starts_with('D')); assert_eq!(icp.n.len(), 1);
462 assert!(icp.n[0].starts_with('E')); 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 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 let (_dir2, repo2) = setup_repo();
508 let result2 = create_keri_identity(&repo2, None, chrono::Utc::now()).unwrap();
509
510 assert_ne!(result1.prefix, result2.prefix);
512 }
513}