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 keypairs (current + next) for the chosen curve
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 git2::Repository;
10use ring::rand::SystemRandom;
11use ring::signature::{Ed25519KeyPair, KeyPair};
12
13use auths_crypto::CurveType;
14
15use crate::storage::registry::backend::{RegistryBackend, RegistryError};
16use auths_crypto::Pkcs8Der;
17
18/// Sign a message using a PKCS8-encoded key, dispatching on curve.
19/// Public alias for use by `identity/initialize.rs`.
20pub fn sign_with_pkcs8_for_init(
21    curve: CurveType,
22    pkcs8: &Pkcs8Der,
23    message: &[u8],
24) -> Result<Vec<u8>, InceptionError> {
25    sign_with_pkcs8(curve, pkcs8, message)
26}
27
28fn sign_with_pkcs8(
29    curve: CurveType,
30    pkcs8: &Pkcs8Der,
31    message: &[u8],
32) -> Result<Vec<u8>, InceptionError> {
33    // Parse PKCS8 → curve-tagged seed, then dispatch through the curve-agnostic
34    // signer. No `match curve { ... }` at this domain layer — fn-121 ethos.
35    let parsed = auths_crypto::parse_key_material(pkcs8.as_ref())
36        .map_err(|e| InceptionError::KeyGeneration(format!("pkcs8 parse: {e}")))?;
37    if parsed.seed.curve() != curve {
38        return Err(InceptionError::KeyGeneration(format!(
39            "pkcs8 curve mismatch: expected {curve}, got {}",
40            parsed.seed.curve()
41        )));
42    }
43    auths_crypto::typed_sign(&parsed.seed, message)
44        .map_err(|e| InceptionError::KeyGeneration(format!("sign: {e}")))
45}
46
47/// Output of curve-agnostic key generation.
48pub struct GeneratedKeypair {
49    /// PKCS8 DER encoded keypair (zeroed on drop).
50    pub pkcs8: Pkcs8Der,
51    /// Raw public key bytes (32 for Ed25519, 33 for P-256 compressed).
52    pub public_key: Vec<u8>,
53    /// CESR-encoded public key string (e.g., "D..." or "1AAJ...").
54    pub cesr_encoded: String,
55}
56
57impl GeneratedKeypair {
58    /// The public key as a typed [`auths_keri::KeriPublicKey`], curve carried in
59    /// the type. Bridges the raw bytes + CESR tag into the typed key that KERI
60    /// functions (e.g. `compute_next_commitment`) consume, so the curve is never
61    /// re-guessed from byte length.
62    pub fn verkey(&self) -> auths_keri::KeriPublicKey {
63        #[allow(clippy::expect_used)]
64        // INVARIANT: cesr_encoded comes from our own keygen and always parses
65        auths_keri::KeriPublicKey::parse(&self.cesr_encoded)
66            .expect("self-generated keypair has a valid CESR-encoded public key")
67    }
68}
69
70/// Public alias for single-curve keypair generation.
71///
72/// Thin wrapper over [`generate_keypairs_for_init`]; preserved so existing
73/// single-key callers (CLI, SDK workflows, tests) don't have to change when
74/// multi-key inception lands.
75pub fn generate_keypair_for_init(curve: CurveType) -> Result<GeneratedKeypair, InceptionError> {
76    let mut out = generate_keypairs_for_init(&[curve])?;
77    // INVARIANT: generate_keypairs_for_init rejects empty slices, so a slice
78    // of length 1 yields a Vec of length 1.
79    Ok(out.remove(0))
80}
81
82/// Generate N keypairs, one per entry in `curves`.
83///
84/// Accepts mixed curve lists (e.g. `[P256, P256, Ed25519]`). Returns the
85/// keypairs in the same order as the input slice.
86///
87/// Args:
88/// * `curves` — non-empty slice of curve choices, one per device slot.
89///
90/// Returns `InceptionError::KeyGeneration` if `curves` is empty.
91///
92/// Usage:
93/// ```ignore
94/// use auths_crypto::CurveType;
95/// use auths_id::keri::inception::generate_keypairs_for_init;
96/// let kps = generate_keypairs_for_init(&[CurveType::P256, CurveType::P256])?;
97/// assert_eq!(kps.len(), 2);
98/// ```
99pub fn generate_keypairs_for_init(
100    curves: &[CurveType],
101) -> Result<Vec<GeneratedKeypair>, InceptionError> {
102    if curves.is_empty() {
103        return Err(InceptionError::KeyGeneration(
104            "generate_keypairs_for_init requires at least one curve".to_string(),
105        ));
106    }
107    curves.iter().map(|c| generate_keypair(*c)).collect()
108}
109
110/// Generate a keypair for the specified curve.
111fn generate_keypair(curve: CurveType) -> Result<GeneratedKeypair, InceptionError> {
112    match curve {
113        CurveType::Ed25519 => {
114            let rng = SystemRandom::new();
115            let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng)
116                .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
117            let keypair = Ed25519KeyPair::from_pkcs8(pkcs8_doc.as_ref())
118                .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
119            let public_key = keypair.public_key().as_ref().to_vec();
120            let cesr_encoded =
121                auths_keri::KeriPublicKey::from_verkey_bytes(&public_key, CurveType::Ed25519)
122                    .and_then(|k| k.to_qb64())
123                    .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
124            Ok(GeneratedKeypair {
125                pkcs8: Pkcs8Der::new(pkcs8_doc.as_ref().to_vec()),
126                public_key,
127                cesr_encoded,
128            })
129        }
130        CurveType::P256 => {
131            use p256::ecdsa::SigningKey;
132            use p256::elliptic_curve::rand_core::OsRng;
133            use p256::pkcs8::EncodePrivateKey;
134
135            let signing_key = SigningKey::random(&mut OsRng);
136            let verifying_key = p256::ecdsa::VerifyingKey::from(&signing_key);
137
138            // Compressed SEC1 public key (33 bytes)
139            let compressed = verifying_key.to_encoded_point(true);
140            let public_key = compressed.as_bytes().to_vec();
141
142            // CESR encode with 1AAJ prefix (P-256 transferable verkey)
143            let cesr_encoded =
144                auths_keri::KeriPublicKey::from_verkey_bytes(&public_key, CurveType::P256)
145                    .and_then(|k| k.to_qb64())
146                    .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
147
148            // PKCS8 DER encoding
149            let pkcs8_doc = signing_key
150                .to_pkcs8_der()
151                .map_err(|e| InceptionError::KeyGeneration(format!("P-256 PKCS8: {e}")))?;
152            let pkcs8 = Pkcs8Der::new(pkcs8_doc.as_bytes().to_vec());
153
154            Ok(GeneratedKeypair {
155                pkcs8,
156                public_key,
157                cesr_encoded,
158            })
159        }
160    }
161}
162
163use auths_core::crypto::said::compute_next_commitment;
164
165use super::event::{CesrKey, KeriSequence, Threshold, VersionString};
166use super::types::{Prefix, Said};
167use super::{Event, GitKel, IcpEvent, KelError, ValidationError, finalize_icp_event};
168use crate::witness_config::WitnessConfig;
169
170/// Error type for inception operations.
171#[derive(Debug, thiserror::Error)]
172#[non_exhaustive]
173pub enum InceptionError {
174    #[error("Key generation failed: {0}")]
175    KeyGeneration(String),
176
177    #[error("KEL error: {0}")]
178    Kel(#[from] KelError),
179
180    #[error("Storage error: {0}")]
181    Storage(RegistryError),
182
183    #[error("Validation error: {0}")]
184    Validation(#[from] ValidationError),
185
186    #[error("Serialization error: {0}")]
187    Serialization(String),
188
189    #[error("Invalid threshold {threshold} for key_count={key_count}: {reason}")]
190    InvalidThreshold {
191        threshold: String,
192        key_count: usize,
193        reason: String,
194    },
195}
196
197/// Validate that a threshold makes sense for a given key count.
198///
199/// - `Simple(n)` requires `n <= key_count` (can't need more sigs than keys).
200/// - `Weighted(clauses)` requires every clause's length equal to `key_count`
201///   (one weight per key) and the clause's max sum to be >= 1 (otherwise the
202///   threshold is unsatisfiable).
203///
204/// Mirrors keripy's footgun check — reject at inception/rotation time, not at
205/// signature-verification time.
206pub(crate) fn validate_threshold_for_key_count(
207    threshold: &Threshold,
208    key_count: usize,
209) -> Result<(), InceptionError> {
210    let label = || match threshold {
211        Threshold::Simple(n) => format!("Simple({n})"),
212        Threshold::Weighted(clauses) => format!("Weighted({clauses:?})"),
213    };
214    match threshold {
215        Threshold::Simple(n) => {
216            if (*n as usize) > key_count {
217                return Err(InceptionError::InvalidThreshold {
218                    threshold: label(),
219                    key_count,
220                    reason: format!("threshold {n} exceeds key count {key_count}"),
221                });
222            }
223            if *n == 0 && key_count > 0 {
224                return Err(InceptionError::InvalidThreshold {
225                    threshold: label(),
226                    key_count,
227                    reason: "threshold 0 with non-empty key list is unsatisfiable".to_string(),
228                });
229            }
230        }
231        Threshold::Weighted(clauses) => {
232            if clauses.is_empty() {
233                return Err(InceptionError::InvalidThreshold {
234                    threshold: label(),
235                    key_count,
236                    reason: "weighted threshold with no clauses is unsatisfiable".to_string(),
237                });
238            }
239            for (i, clause) in clauses.iter().enumerate() {
240                if clause.len() != key_count {
241                    return Err(InceptionError::InvalidThreshold {
242                        threshold: label(),
243                        key_count,
244                        reason: format!(
245                            "clause {i} has {} weights for {key_count} keys",
246                            clause.len()
247                        ),
248                    });
249                }
250                // "Meetable" check: summing every weight must cross >= 1.
251                let refs: Vec<&auths_keri::Fraction> = clause.iter().collect();
252                if !auths_keri::Fraction::sum_meets_one(&refs) {
253                    return Err(InceptionError::InvalidThreshold {
254                        threshold: label(),
255                        key_count,
256                        reason: format!(
257                            "clause {i} sum < 1 even with all keys signing (unsatisfiable)"
258                        ),
259                    });
260                }
261            }
262        }
263    }
264    Ok(())
265}
266
267impl auths_core::error::AuthsErrorInfo for InceptionError {
268    fn error_code(&self) -> &'static str {
269        match self {
270            Self::KeyGeneration(_) => "AUTHS-E4901",
271            Self::Kel(_) => "AUTHS-E4902",
272            Self::Storage(_) => "AUTHS-E4903",
273            Self::Validation(_) => "AUTHS-E4904",
274            Self::Serialization(_) => "AUTHS-E4905",
275            Self::InvalidThreshold { .. } => "AUTHS-E4906",
276        }
277    }
278
279    fn suggestion(&self) -> Option<&'static str> {
280        match self {
281            Self::KeyGeneration(_) => None,
282            Self::Kel(_) => Some("Check the KEL state; a KEL may already exist for this prefix"),
283            Self::Storage(_) => Some("Check storage backend connectivity"),
284            Self::Validation(_) => None,
285            Self::Serialization(_) => None,
286            Self::InvalidThreshold { .. } => Some(
287                "Ensure the threshold count does not exceed the number of keys, and that weighted clauses have one weight per key summing to at least 1",
288            ),
289        }
290    }
291}
292
293/// Result of a KERI identity inception.
294pub struct InceptionResult {
295    /// The KERI prefix (use with `did:keri:<prefix>`)
296    pub prefix: Prefix,
297
298    /// The current signing keypair (PKCS8 DER encoded, zeroed on drop)
299    pub current_keypair_pkcs8: Pkcs8Der,
300
301    /// The next rotation keypair (PKCS8 DER encoded, zeroed on drop)
302    pub next_keypair_pkcs8: Pkcs8Der,
303
304    /// The current public key (raw 32 bytes)
305    pub current_public_key: Vec<u8>,
306
307    /// The next public key (raw 32 bytes)
308    pub next_public_key: Vec<u8>,
309}
310
311impl std::fmt::Debug for InceptionResult {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        f.debug_struct("InceptionResult")
314            .field("prefix", &self.prefix)
315            .field("current_keypair_pkcs8", &self.current_keypair_pkcs8)
316            .field("next_keypair_pkcs8", &self.next_keypair_pkcs8)
317            .field("current_public_key", &self.current_public_key)
318            .field("next_public_key", &self.next_public_key)
319            .finish()
320    }
321}
322
323impl InceptionResult {
324    /// Get the full DID for this identity.
325    pub fn did(&self) -> String {
326        format!("did:keri:{}", self.prefix.as_str())
327    }
328}
329
330/// Result of a multi-key KERI identity inception.
331///
332/// Each position in the `current_*` / `next_*` vectors corresponds to the
333/// same device slot index: `current_keypairs_pkcs8[i]` signs at `k[i]`,
334/// and its next-rotation counterpart is at `next_keypairs_pkcs8[i]`
335/// committed at `n[i]`.
336pub struct MultiKeyInceptionResult {
337    pub prefix: Prefix,
338    pub current_keypairs_pkcs8: Vec<Pkcs8Der>,
339    pub next_keypairs_pkcs8: Vec<Pkcs8Der>,
340    pub current_public_keys: Vec<Vec<u8>>,
341    pub next_public_keys: Vec<Vec<u8>>,
342}
343
344impl std::fmt::Debug for MultiKeyInceptionResult {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        f.debug_struct("MultiKeyInceptionResult")
347            .field("prefix", &self.prefix)
348            .field("device_count", &self.current_public_keys.len())
349            .finish()
350    }
351}
352
353impl MultiKeyInceptionResult {
354    pub fn did(&self) -> String {
355        format!("did:keri:{}", self.prefix.as_str())
356    }
357}
358
359/// Create a multi-key KERI identity.
360///
361/// Generates `curves.len()` current keypairs and `curves.len()` next-rotation
362/// keypairs. The inception event carries all current pubkeys in `k` and
363/// commitment hashes of all next keypairs in `n`, with the supplied
364/// `kt`/`nt` thresholds.
365///
366/// Signs with index 0's current key only — satisfying any `Simple(1)` or
367/// single-slot threshold. Multi-device signature aggregation is wired
368/// through the signing-workflow module, which adds additional
369/// `IndexedSignature`s when `kt` is multi-slot.
370///
371/// Args:
372/// * `repo` — Git repository for KEL storage.
373/// * `witness_config` — Optional witness configuration.
374/// * `now` — Current time (injected).
375/// * `curves` — Non-empty slice of curve choices, one per device slot.
376/// * `kt` — Signing threshold. Validated against `curves.len()`.
377/// * `nt` — Rotation threshold. Validated against `curves.len()`.
378pub fn create_keri_identity_multi(
379    repo: &Repository,
380    witness_config: Option<&WitnessConfig>,
381    now: chrono::DateTime<chrono::Utc>,
382    curves: &[CurveType],
383    kt: Threshold,
384    nt: Threshold,
385) -> Result<MultiKeyInceptionResult, InceptionError> {
386    if curves.is_empty() {
387        return Err(InceptionError::KeyGeneration(
388            "create_keri_identity_multi requires at least one curve".to_string(),
389        ));
390    }
391    validate_threshold_for_key_count(&kt, curves.len())?;
392    validate_threshold_for_key_count(&nt, curves.len())?;
393
394    let current_kps = generate_keypairs_for_init(curves)?;
395    let next_kps = generate_keypairs_for_init(curves)?;
396
397    let k: Vec<CesrKey> = current_kps
398        .iter()
399        .map(|kp| CesrKey::new_unchecked(kp.cesr_encoded.clone()))
400        .collect();
401    let n: Vec<Said> = next_kps
402        .iter()
403        .map(|kp| compute_next_commitment(&kp.verkey()))
404        .collect();
405
406    let (bt, b) = match witness_config {
407        Some(cfg) if cfg.is_enabled() => (
408            Threshold::Simple(cfg.threshold as u64),
409            cfg.aids().cloned().collect(),
410        ),
411        _ => (Threshold::Simple(0), vec![]),
412    };
413
414    let icp = IcpEvent {
415        v: VersionString::placeholder(),
416        d: Said::default(),
417        i: Prefix::default(),
418        s: KeriSequence::new(0),
419        kt,
420        k,
421        nt,
422        n,
423        bt,
424        b,
425        c: vec![],
426        a: vec![],
427    };
428
429    let finalized = finalize_icp_event(icp)?;
430    let prefix = finalized.i.clone();
431
432    // Sign with index 0's key — produces a valid single-slot signature. The
433    // multi-sig aggregation module can add additional sigs after the fact
434    // if `kt` requires a quorum.
435    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
436    let _sig_bytes = sign_with_pkcs8(curves[0], &current_kps[0].pkcs8, &canonical)?;
437
438    let kel = GitKel::new(repo, prefix.as_str());
439    kel.create(&finalized, now)?;
440
441    Ok(MultiKeyInceptionResult {
442        prefix,
443        current_keypairs_pkcs8: current_kps.iter().map(|kp| kp.pkcs8.clone()).collect(),
444        next_keypairs_pkcs8: next_kps.iter().map(|kp| kp.pkcs8.clone()).collect(),
445        current_public_keys: current_kps.into_iter().map(|kp| kp.public_key).collect(),
446        next_public_keys: next_kps.into_iter().map(|kp| kp.public_key).collect(),
447    })
448}
449
450/// Create a new KERI identity with proper pre-rotation.
451///
452/// This generates two Ed25519 keypairs:
453/// - Current key: used for immediate signing
454/// - Next key: committed to in the inception event for future rotation
455///
456/// The inception event is stored in the Git repository at:
457/// `refs/did/keri/<prefix>/kel`
458///
459/// # Arguments
460/// * `repo` - Git repository for KEL storage
461/// * `witness_config` - Optional witness configuration. When provided and
462///   enabled, the inception event's `bt`/`b` fields are set accordingly.
463///
464/// # Returns
465/// * `InceptionResult` containing the prefix and both keypairs
466pub fn create_keri_identity(
467    repo: &Repository,
468    witness_config: Option<&WitnessConfig>,
469    now: chrono::DateTime<chrono::Utc>,
470) -> Result<InceptionResult, InceptionError> {
471    create_keri_identity_with_curve(repo, witness_config, now, CurveType::P256)
472}
473
474/// Create a KERI identity with a specific curve type.
475///
476/// Args:
477/// * `repo` — Git repository for KEL storage.
478/// * `witness_config` — Optional witness configuration.
479/// * `now` — Current time (injected, never use `Utc::now()` directly).
480/// * `curve` — The elliptic curve to use (`P256` default, `Ed25519` available).
481pub fn create_keri_identity_with_curve(
482    repo: &Repository,
483    witness_config: Option<&WitnessConfig>,
484    now: chrono::DateTime<chrono::Utc>,
485    curve: CurveType,
486) -> Result<InceptionResult, InceptionError> {
487    // Generate current and next keypairs for the chosen curve
488    let current = generate_keypair(curve)?;
489    let next = generate_keypair(curve)?;
490
491    let current_pub_encoded = current.cesr_encoded.clone();
492
493    // Compute next-key commitment (Blake3 hash of the CESR-qualified next public key bytes)
494    // The commitment is curve-agnostic: Blake3(raw_public_key_bytes)
495    let next_commitment = compute_next_commitment(&next.verkey());
496
497    // Determine witness fields from config
498    let (bt, b) = match witness_config {
499        Some(cfg) if cfg.is_enabled() => (
500            Threshold::Simple(cfg.threshold as u64),
501            cfg.aids().cloned().collect(),
502        ),
503        _ => (Threshold::Simple(0), vec![]),
504    };
505
506    // Build inception event (without SAID)
507    let icp = IcpEvent {
508        v: VersionString::placeholder(),
509        d: Said::default(),
510        i: Prefix::default(),
511        s: KeriSequence::new(0),
512        kt: Threshold::Simple(1),
513        k: vec![CesrKey::new_unchecked(current_pub_encoded)],
514        nt: Threshold::Simple(1),
515        n: vec![next_commitment],
516        bt,
517        b,
518        c: vec![],
519        a: vec![],
520    };
521
522    // Finalize event (computes and sets SAID)
523    let finalized = finalize_icp_event(icp)?;
524    let prefix = finalized.i.clone();
525
526    // Sign the event with the current key
527    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
528    let _sig_bytes = sign_with_pkcs8(curve, &current.pkcs8, &canonical)?;
529
530    // Store in Git KEL
531    let kel = GitKel::new(repo, prefix.as_str());
532    kel.create(&finalized, now)?;
533
534    // Collect witness receipts if configured
535    #[cfg(feature = "witness-client")]
536    if let Some(config) = witness_config
537        && config.is_enabled()
538    {
539        let canonical_for_witness = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
540        super::witness_integration::collect_and_store_receipts(
541            repo.path().parent().unwrap_or(repo.path()),
542            &prefix,
543            &finalized.d,
544            &canonical_for_witness,
545            config,
546            now,
547        )
548        .map_err(|e| InceptionError::Serialization(e.to_string()))?;
549    }
550
551    Ok(InceptionResult {
552        prefix,
553        current_keypair_pkcs8: current.pkcs8,
554        next_keypair_pkcs8: next.pkcs8,
555        current_public_key: current.public_key,
556        next_public_key: next.public_key,
557    })
558}
559
560/// Create a new KERI identity using any [`RegistryBackend`].
561///
562/// Identical logic to [`create_keri_identity`] but stores the inception event
563/// via the provided backend instead of a git repository.
564///
565/// # Arguments
566/// * `backend` - The registry backend to store the inception event
567/// * `witness_config` - Unused in the backend path; kept for API symmetry
568///
569/// # Returns
570/// * `InceptionResult` containing the prefix and both keypairs
571pub fn create_keri_identity_with_backend(
572    backend: &impl RegistryBackend,
573    _witness_config: Option<&WitnessConfig>,
574) -> Result<InceptionResult, InceptionError> {
575    let rng = SystemRandom::new();
576
577    let current_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
578        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
579    let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8.as_ref())
580        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
581
582    let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
583        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
584    let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref())
585        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
586
587    let current_pub_encoded =
588        auths_keri::KeriPublicKey::ed25519(current_keypair.public_key().as_ref())
589            .and_then(|k| k.to_qb64())
590            .map_err(|e| InceptionError::KeyGeneration(format!("ed25519 verkey: {e}")))?;
591    let next_commitment = compute_next_commitment(
592        &auths_keri::KeriPublicKey::ed25519(next_keypair.public_key().as_ref())
593            .map_err(|e| InceptionError::KeyGeneration(format!("ed25519 verkey: {e}")))?,
594    );
595
596    let icp = IcpEvent {
597        v: VersionString::placeholder(),
598        d: Said::default(),
599        i: Prefix::default(),
600        s: KeriSequence::new(0),
601        kt: Threshold::Simple(1),
602        k: vec![CesrKey::new_unchecked(current_pub_encoded)],
603        nt: Threshold::Simple(1),
604        n: vec![next_commitment],
605        bt: Threshold::Simple(0),
606        b: vec![],
607        c: vec![],
608        a: vec![],
609    };
610
611    let finalized = finalize_icp_event(icp)?;
612    let prefix = finalized.i.clone();
613
614    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
615    let sig = current_keypair.sign(&canonical);
616    let attachment = auths_keri::serialize_attachment(&[auths_keri::IndexedSignature {
617        index: 0,
618        prior_index: None,
619        sig: sig.as_ref().to_vec(),
620    }])
621    .map_err(|e| InceptionError::Serialization(e.to_string()))?;
622
623    backend
624        .append_signed_event(&prefix, &Event::Icp(finalized), &attachment)
625        .map_err(InceptionError::Storage)?;
626
627    Ok(InceptionResult {
628        prefix,
629        current_keypair_pkcs8: Pkcs8Der::new(current_pkcs8.as_ref()),
630        next_keypair_pkcs8: Pkcs8Der::new(next_pkcs8.as_ref()),
631        current_public_key: current_keypair.public_key().as_ref().to_vec(),
632        next_public_key: next_keypair.public_key().as_ref().to_vec(),
633    })
634}
635
636/// Create a KERI identity from an existing Ed25519 key (PKCS8 DER).
637///
638/// Used for migrating existing `did:key` identities to `did:keri`.
639/// The provided key becomes the current signing key; a new next key
640/// is generated for pre-rotation.
641///
642/// # Arguments
643/// * `repo` - Git repository for KEL storage
644/// * `current_pkcs8_bytes` - Existing Ed25519 key in PKCS8 v2 DER format
645/// * `witness_config` - Optional witness configuration
646/// * `now` - Timestamp for the inception event
647pub fn create_keri_identity_from_key(
648    repo: &Repository,
649    current_pkcs8_bytes: &[u8],
650    witness_config: Option<&WitnessConfig>,
651    now: chrono::DateTime<chrono::Utc>,
652) -> Result<InceptionResult, InceptionError> {
653    let rng = SystemRandom::new();
654
655    // Use the provided key as the current keypair
656    let current_keypair = Ed25519KeyPair::from_pkcs8(current_pkcs8_bytes)
657        .map_err(|e| InceptionError::KeyGeneration(format!("invalid PKCS8 key: {e}")))?;
658
659    // Generate next keypair (for pre-rotation)
660    let next_pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
661        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
662    let next_keypair = Ed25519KeyPair::from_pkcs8(next_pkcs8.as_ref())
663        .map_err(|e| InceptionError::KeyGeneration(e.to_string()))?;
664
665    let current_pub_encoded =
666        auths_keri::KeriPublicKey::ed25519(current_keypair.public_key().as_ref())
667            .and_then(|k| k.to_qb64())
668            .map_err(|e| InceptionError::KeyGeneration(format!("ed25519 verkey: {e}")))?;
669    let next_commitment = compute_next_commitment(
670        &auths_keri::KeriPublicKey::ed25519(next_keypair.public_key().as_ref())
671            .map_err(|e| InceptionError::KeyGeneration(format!("ed25519 verkey: {e}")))?,
672    );
673
674    let (bt, b) = match witness_config {
675        Some(cfg) if cfg.is_enabled() => (
676            Threshold::Simple(cfg.threshold as u64),
677            cfg.aids().cloned().collect(),
678        ),
679        _ => (Threshold::Simple(0), vec![]),
680    };
681
682    let icp = IcpEvent {
683        v: VersionString::placeholder(),
684        d: Said::default(),
685        i: Prefix::default(),
686        s: KeriSequence::new(0),
687        kt: Threshold::Simple(1),
688        k: vec![CesrKey::new_unchecked(current_pub_encoded)],
689        nt: Threshold::Simple(1),
690        n: vec![next_commitment],
691        bt,
692        b,
693        c: vec![],
694        a: vec![],
695    };
696
697    let finalized = finalize_icp_event(icp)?;
698    let prefix = finalized.i.clone();
699
700    let canonical = super::serialize_for_signing(&Event::Icp(finalized.clone()))?;
701    let _sig = current_keypair.sign(&canonical);
702
703    let kel = GitKel::new(repo, prefix.as_str());
704    kel.create(&finalized, now)?;
705
706    Ok(InceptionResult {
707        prefix,
708        current_keypair_pkcs8: Pkcs8Der::new(current_pkcs8_bytes),
709        next_keypair_pkcs8: Pkcs8Der::new(next_pkcs8.as_ref()),
710        current_public_key: current_keypair.public_key().as_ref().to_vec(),
711        next_public_key: next_keypair.public_key().as_ref().to_vec(),
712    })
713}
714
715/// Format a KERI prefix as a full DID.
716pub fn prefix_to_did(prefix: &str) -> String {
717    format!("did:keri:{}", prefix)
718}
719
720/// Extract the prefix from a did:keri DID.
721///
722/// Prefer [`auths_verifier::IdentityDID`] at API boundaries for type safety.
723pub fn did_to_prefix(did: &str) -> Option<&str> {
724    did.strip_prefix("did:keri:")
725}
726
727#[cfg(test)]
728#[allow(clippy::disallowed_methods)]
729mod tests {
730    use super::*;
731    use crate::keri::{Event, validate_kel};
732    use tempfile::TempDir;
733
734    fn setup_repo() -> (TempDir, Repository) {
735        let dir = TempDir::new().unwrap();
736        let repo = Repository::init(dir.path()).unwrap();
737
738        // Set git config for CI environments
739        let mut config = repo.config().unwrap();
740        config.set_str("user.name", "Test User").unwrap();
741        config.set_str("user.email", "test@example.com").unwrap();
742
743        (dir, repo)
744    }
745
746    #[test]
747    fn create_identity_returns_valid_result() {
748        let (_dir, repo) = setup_repo();
749
750        let result = create_keri_identity_with_curve(
751            &repo,
752            None,
753            chrono::Utc::now(),
754            auths_crypto::CurveType::Ed25519,
755        )
756        .unwrap();
757
758        // Prefix should start with 'E' (Blake3 SAID prefix)
759        assert!(result.prefix.as_str().starts_with('E'));
760
761        // Keys should be present
762        assert!(!result.current_keypair_pkcs8.is_empty());
763        assert!(!result.next_keypair_pkcs8.is_empty());
764        assert_eq!(result.current_public_key.len(), 32);
765        assert_eq!(result.next_public_key.len(), 32);
766
767        // DID should be formatted correctly
768        assert!(result.did().starts_with("did:keri:E"));
769    }
770
771    #[test]
772    fn create_identity_stores_kel() {
773        let (_dir, repo) = setup_repo();
774
775        let result = create_keri_identity_with_curve(
776            &repo,
777            None,
778            chrono::Utc::now(),
779            auths_crypto::CurveType::Ed25519,
780        )
781        .unwrap();
782
783        // Verify KEL exists and has one event
784        let kel = GitKel::new(&repo, result.prefix.as_str());
785        assert!(kel.exists());
786
787        let events = kel.get_events().unwrap();
788        assert_eq!(events.len(), 1);
789        assert!(events[0].is_inception());
790    }
791
792    #[test]
793    fn inception_event_is_valid() {
794        let (_dir, repo) = setup_repo();
795
796        let result = create_keri_identity_with_curve(
797            &repo,
798            None,
799            chrono::Utc::now(),
800            auths_crypto::CurveType::Ed25519,
801        )
802        .unwrap();
803        let kel = GitKel::new(&repo, result.prefix.as_str());
804        let events = kel.get_events().unwrap();
805
806        // Validate the KEL
807        let state = validate_kel(&events).unwrap();
808        assert_eq!(state.prefix, result.prefix);
809        assert_eq!(state.sequence, 0);
810        assert!(!state.is_abandoned);
811    }
812
813    fn witness_cfg(threshold: usize, aids: &[&str]) -> WitnessConfig {
814        use crate::witness_config::{WitnessPolicy, WitnessRef};
815        WitnessConfig {
816            witnesses: aids
817                .iter()
818                .enumerate()
819                .map(|(i, aid)| WitnessRef {
820                    url: format!("http://w{i}:3333").parse().unwrap(),
821                    aid: Prefix::new_unchecked((*aid).to_string()),
822                    operator_info: None,
823                })
824                .collect(),
825            threshold,
826            timeout_ms: 5000,
827            // Warn (not Enforce): inception designates b[] regardless, but the
828            // test witnesses are unreachable — Enforce would (correctly) fail
829            // receipt collection. We assert designation, not live collection.
830            policy: WitnessPolicy::Warn,
831            ..Default::default()
832        }
833    }
834
835    #[test]
836    fn icp_designates_configured_witnesses_and_replays_backers() {
837        let (_dir, repo) = setup_repo();
838        let aids = [
839            "BWitnessOne00000000000000000000000000000000",
840            "BWitnessTwo00000000000000000000000000000000",
841        ];
842        let cfg = witness_cfg(2, &aids);
843        let result = create_keri_identity_with_curve(
844            &repo,
845            Some(&cfg),
846            chrono::Utc::now(),
847            auths_crypto::CurveType::Ed25519,
848        )
849        .unwrap();
850        let kel = GitKel::new(&repo, result.prefix.as_str());
851        // The designated witnesses survive replay into the key-state.
852        let state = validate_kel(&kel.get_events().unwrap()).unwrap();
853        let designated: Vec<&str> = state.backers.iter().map(|p| p.as_str()).collect();
854        assert_eq!(designated, aids);
855        assert_eq!(state.backer_threshold, Threshold::Simple(2));
856    }
857
858    #[test]
859    fn icp_zero_witness_path_is_valid() {
860        let (_dir, repo) = setup_repo();
861        let result = create_keri_identity_with_curve(
862            &repo,
863            None,
864            chrono::Utc::now(),
865            auths_crypto::CurveType::Ed25519,
866        )
867        .unwrap();
868        let kel = GitKel::new(&repo, result.prefix.as_str());
869        let state = validate_kel(&kel.get_events().unwrap()).unwrap();
870        assert!(state.backers.is_empty());
871        assert_eq!(state.backer_threshold, Threshold::Simple(0));
872    }
873
874    #[test]
875    fn inception_event_has_correct_structure() {
876        let (_dir, repo) = setup_repo();
877
878        let result = create_keri_identity_with_curve(
879            &repo,
880            None,
881            chrono::Utc::now(),
882            auths_crypto::CurveType::Ed25519,
883        )
884        .unwrap();
885        let kel = GitKel::new(&repo, result.prefix.as_str());
886        let events = kel.get_events().unwrap();
887
888        if let Event::Icp(icp) = &events[0] {
889            // Version
890            assert_eq!(icp.v.kind, "JSON");
891
892            // SAID equals prefix
893            assert_eq!(icp.d.as_str(), icp.i.as_str());
894            assert_eq!(icp.d.as_str(), result.prefix.as_str());
895
896            // Sequence is 0
897            assert_eq!(icp.s, KeriSequence::new(0));
898
899            // Single key
900            assert_eq!(icp.k.len(), 1);
901            assert!(icp.k[0].as_str().starts_with('D')); // Ed25519 prefix
902
903            // Single next commitment
904            assert_eq!(icp.n.len(), 1);
905            assert!(icp.n[0].as_str().starts_with('E')); // Blake3 hash prefix
906
907            // No witnesses
908            assert_eq!(icp.bt, Threshold::Simple(0));
909            assert!(icp.b.is_empty());
910        } else {
911            panic!("Expected inception event");
912        }
913    }
914
915    #[test]
916    fn next_key_commitment_is_correct() {
917        let (_dir, repo) = setup_repo();
918
919        let result = create_keri_identity_with_curve(
920            &repo,
921            None,
922            chrono::Utc::now(),
923            auths_crypto::CurveType::Ed25519,
924        )
925        .unwrap();
926        let kel = GitKel::new(&repo, result.prefix.as_str());
927        let events = kel.get_events().unwrap();
928
929        if let Event::Icp(icp) = &events[0] {
930            // Verify the next commitment matches the next public key
931            let expected_commitment = compute_next_commitment(
932                &auths_keri::KeriPublicKey::ed25519(&result.next_public_key)
933                    .expect("ed25519 verkey is 32 bytes"),
934            );
935            assert_eq!(icp.n[0], expected_commitment);
936        } else {
937            panic!("Expected inception event");
938        }
939    }
940
941    #[test]
942    fn prefix_to_did_works() {
943        assert_eq!(prefix_to_did("ETest123"), "did:keri:ETest123");
944    }
945
946    #[test]
947    fn did_to_prefix_works() {
948        assert_eq!(did_to_prefix("did:keri:ETest123"), Some("ETest123"));
949        assert_eq!(did_to_prefix("did:key:z6Mk..."), None);
950    }
951
952    #[test]
953    fn multiple_identities_have_different_prefixes() {
954        let (_dir, repo) = setup_repo();
955
956        let result1 = create_keri_identity_with_curve(
957            &repo,
958            None,
959            chrono::Utc::now(),
960            auths_crypto::CurveType::Ed25519,
961        )
962        .unwrap();
963
964        // Create second repo for second identity
965        let (_dir2, repo2) = setup_repo();
966        let result2 = create_keri_identity_with_curve(
967            &repo2,
968            None,
969            chrono::Utc::now(),
970            auths_crypto::CurveType::Ed25519,
971        )
972        .unwrap();
973
974        // Prefixes should be different
975        assert_ne!(result1.prefix, result2.prefix);
976    }
977
978    #[test]
979    fn generate_keypairs_for_init_rejects_empty_slice() {
980        let res = generate_keypairs_for_init(&[]);
981        match res {
982            Ok(_) => panic!("expected error on empty slice"),
983            Err(InceptionError::KeyGeneration(msg)) => assert!(msg.contains("at least one")),
984            Err(other) => panic!("expected KeyGeneration error, got {other:?}"),
985        }
986    }
987
988    #[test]
989    fn generate_keypairs_for_init_single_curve_matches_legacy() {
990        // A one-element slice must produce exactly one keypair with the same
991        // shape as the legacy single-curve entry point.
992        let kps = generate_keypairs_for_init(&[auths_crypto::CurveType::P256]).unwrap();
993        assert_eq!(kps.len(), 1);
994        assert!(kps[0].cesr_encoded.starts_with("1AAJ"));
995        assert_eq!(kps[0].public_key.len(), 33);
996
997        let legacy = generate_keypair_for_init(auths_crypto::CurveType::P256).unwrap();
998        assert_eq!(legacy.public_key.len(), 33);
999        assert!(legacy.cesr_encoded.starts_with("1AAJ"));
1000    }
1001
1002    #[test]
1003    fn generate_keypairs_for_init_mixed_curves_distinct_keys() {
1004        use auths_crypto::CurveType::{Ed25519, P256};
1005        let kps = generate_keypairs_for_init(&[P256, P256, Ed25519]).unwrap();
1006        assert_eq!(kps.len(), 3);
1007
1008        // Per-entry curve dispatch: P-256 entries are 33 bytes with "1AAJ"
1009        // CESR prefix; Ed25519 is 32 bytes with "D".
1010        assert_eq!(kps[0].public_key.len(), 33);
1011        assert!(kps[0].cesr_encoded.starts_with("1AAJ"));
1012        assert_eq!(kps[1].public_key.len(), 33);
1013        assert!(kps[1].cesr_encoded.starts_with("1AAJ"));
1014        assert_eq!(kps[2].public_key.len(), 32);
1015        assert!(kps[2].cesr_encoded.starts_with('D'));
1016
1017        // Distinct keypairs (randomness check — pubkeys must differ).
1018        assert_ne!(kps[0].public_key, kps[1].public_key);
1019        assert_ne!(kps[0].public_key, kps[2].public_key);
1020        assert_ne!(kps[1].public_key, kps[2].public_key);
1021    }
1022}