use auths_core::storage::keychain::IdentityDID;
use auths_crypto::CurveType;
use auths_keri::KeriPublicKey;
use super::{Prefix, Said};
#[derive(Debug, Clone)]
pub struct ControllerDescriptor {
pub identity_did: IdentityDID,
pub current_verkey: KeriPublicKey,
}
#[derive(Debug, Clone)]
pub struct SharedKelArtifacts {
pub prefix: Prefix,
pub inception_event_json: String,
pub controllers: Vec<ControllerDescriptor>,
pub kt: u32,
}
#[derive(Debug, Clone)]
pub struct SharedKelRotArtifacts {
pub prefix: Prefix,
pub event_said: Said,
pub event_json: String,
pub controllers: Vec<ControllerDescriptor>,
}
pub fn incept_shared_kel_prepared(
controllers: &[ControllerDescriptor],
) -> Result<Vec<ControllerDescriptor>, SharedKelError> {
if controllers.is_empty() {
return Err(SharedKelError::WouldOrphanIdentity);
}
Ok(controllers.to_vec())
}
pub fn rot_add_controller(
current: &[ControllerDescriptor],
new: &ControllerDescriptor,
) -> Result<Vec<ControllerDescriptor>, SharedKelError> {
apply_shared_kel_change(current, &SharedKelChange::AddController { new })
}
pub fn rot_remove_controller(
current: &[ControllerDescriptor],
target: &IdentityDID,
) -> Result<Vec<ControllerDescriptor>, SharedKelError> {
apply_shared_kel_change(current, &SharedKelChange::RemoveController { target })
}
#[derive(Debug, Clone)]
pub enum SharedKelChange<'a> {
AddController { new: &'a ControllerDescriptor },
RemoveController { target: &'a IdentityDID },
SwapController {
old: &'a IdentityDID,
new: &'a ControllerDescriptor,
},
}
#[derive(Debug, thiserror::Error)]
pub enum SharedKelError {
#[error("controller {0} is not in the current shared-KEL controller set")]
ControllerNotFound(String),
#[error("rotation would orphan the identity (no remaining controllers)")]
WouldOrphanIdentity,
#[error("rotation event construction failed: {0}")]
EventConstruction(String),
}
pub fn resolve_controller_index(
controllers: &[ControllerDescriptor],
target: &IdentityDID,
) -> Result<usize, SharedKelError> {
controllers
.iter()
.position(|c| c.identity_did.as_str() == target.as_str())
.ok_or_else(|| SharedKelError::ControllerNotFound(target.as_str().to_string()))
}
pub fn apply_shared_kel_change(
current: &[ControllerDescriptor],
change: &SharedKelChange<'_>,
) -> Result<Vec<ControllerDescriptor>, SharedKelError> {
match change {
SharedKelChange::AddController { new } => {
let mut next: Vec<ControllerDescriptor> = current.to_vec();
next.push((*new).clone());
Ok(next)
}
SharedKelChange::RemoveController { target } => {
let idx = resolve_controller_index(current, target)?;
if current.len() <= 1 {
return Err(SharedKelError::WouldOrphanIdentity);
}
let mut next: Vec<ControllerDescriptor> = current.to_vec();
next.remove(idx);
Ok(next)
}
SharedKelChange::SwapController { old, new } => {
let idx = resolve_controller_index(current, old)?;
let mut next: Vec<ControllerDescriptor> = current.to_vec();
next[idx] = (*new).clone();
Ok(next)
}
}
}
pub fn rot_swap_controller(
current: &[ControllerDescriptor],
old_did: &IdentityDID,
new: &ControllerDescriptor,
) -> Result<Vec<ControllerDescriptor>, SharedKelError> {
apply_shared_kel_change(
current,
&SharedKelChange::SwapController { old: old_did, new },
)
}
pub fn controller_from_parts(
did: IdentityDID,
verkey_bytes: Vec<u8>,
curve: CurveType,
) -> Result<ControllerDescriptor, SharedKelError> {
let current_verkey = match curve {
CurveType::Ed25519 => {
let arr: [u8; 32] = verkey_bytes.as_slice().try_into().map_err(|_| {
SharedKelError::EventConstruction("Ed25519 verkey must be 32 bytes".into())
})?;
KeriPublicKey::Ed25519(arr)
}
CurveType::P256 => {
let arr: [u8; 33] = verkey_bytes.as_slice().try_into().map_err(|_| {
SharedKelError::EventConstruction(
"P-256 verkey must be 33-byte compressed SEC1".into(),
)
})?;
KeriPublicKey::P256 {
key: arr,
transferable: true,
}
}
};
Ok(ControllerDescriptor {
identity_did: did,
current_verkey,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn did(s: &str) -> IdentityDID {
#[allow(clippy::disallowed_methods)]
IdentityDID::new_unchecked(s.to_string())
}
fn controller(did_str: &str) -> ControllerDescriptor {
ControllerDescriptor {
identity_did: did(did_str),
current_verkey: KeriPublicKey::Ed25519([0u8; 32]),
}
}
#[test]
fn add_appends_controller() {
let current = vec![controller("did:keri:EAAAMac")];
let new = controller("did:keri:EBBBPhone");
let next = apply_shared_kel_change(¤t, &SharedKelChange::AddController { new: &new })
.expect("add");
assert_eq!(next.len(), 2);
assert_eq!(next[1].identity_did.as_str(), "did:keri:EBBBPhone");
}
#[test]
fn remove_drops_named_controller() {
let current = vec![
controller("did:keri:EAAAMac"),
controller("did:keri:EBBBPhone"),
];
let target = did("did:keri:EAAAMac");
let next = apply_shared_kel_change(
¤t,
&SharedKelChange::RemoveController { target: &target },
)
.expect("remove");
assert_eq!(next.len(), 1);
assert_eq!(next[0].identity_did.as_str(), "did:keri:EBBBPhone");
}
#[test]
fn remove_of_missing_controller_errors() {
let current = vec![controller("did:keri:EAAAMac")];
let target = did("did:keri:EZZZMissing");
let err = apply_shared_kel_change(
¤t,
&SharedKelChange::RemoveController { target: &target },
)
.unwrap_err();
assert!(matches!(err, SharedKelError::ControllerNotFound(_)));
}
#[test]
fn remove_of_last_controller_would_orphan_errors() {
let current = vec![controller("did:keri:EAAAMac")];
let target = did("did:keri:EAAAMac");
let err = apply_shared_kel_change(
¤t,
&SharedKelChange::RemoveController { target: &target },
)
.unwrap_err();
assert!(matches!(err, SharedKelError::WouldOrphanIdentity));
}
#[test]
fn swap_is_atomic_controller_count_invariant() {
let current = vec![
controller("did:keri:EAAAMacOld"),
controller("did:keri:EBBBPhone"),
];
let old = did("did:keri:EAAAMacOld");
let new = controller("did:keri:ECCCMacNew");
let next = rot_swap_controller(¤t, &old, &new).expect("swap");
assert_eq!(
next.len(),
current.len(),
"controller count must be invariant"
);
assert!(
next.iter()
.any(|c| c.identity_did.as_str() == "did:keri:ECCCMacNew")
);
assert!(
!next
.iter()
.any(|c| c.identity_did.as_str() == "did:keri:EAAAMacOld")
);
}
#[test]
fn swap_replaces_in_place() {
let current = vec![
controller("did:keri:EAAAMacOld"),
controller("did:keri:EBBBPhone"),
];
let old = did("did:keri:EAAAMacOld");
let new = controller("did:keri:ECCCMacNew");
let next = apply_shared_kel_change(
¤t,
&SharedKelChange::SwapController {
old: &old,
new: &new,
},
)
.expect("swap");
assert_eq!(next.len(), 2);
assert_eq!(next[0].identity_did.as_str(), "did:keri:ECCCMacNew");
assert_eq!(next[1].identity_did.as_str(), "did:keri:EBBBPhone");
}
#[test]
fn add_then_remove_by_did_regardless_of_index() {
let mut current = vec![controller("did:keri:EAAAMac")];
let phone_a = controller("did:keri:EAAAPhone");
let phone_b = controller("did:keri:EBBBPhone");
current =
apply_shared_kel_change(¤t, &SharedKelChange::AddController { new: &phone_a })
.unwrap();
current =
apply_shared_kel_change(¤t, &SharedKelChange::AddController { new: &phone_b })
.unwrap();
let target = did("did:keri:EAAAPhone");
let next = apply_shared_kel_change(
¤t,
&SharedKelChange::RemoveController { target: &target },
)
.unwrap();
assert!(
next.iter()
.any(|c| c.identity_did.as_str() == "did:keri:EBBBPhone"),
"B must survive removal of A"
);
assert!(
!next
.iter()
.any(|c| c.identity_did.as_str() == "did:keri:EAAAPhone"),
"A must be gone"
);
}
}