use std::ops::ControlFlow;
use std::sync::Arc;
use auths_core::crypto::said::{compute_next_commitment, verify_commitment};
use auths_core::crypto::signer::{decrypt_keypair, encrypt_keypair};
use auths_core::signing::PassphraseProvider;
use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
use auths_crypto::{CurveType, Pkcs8Der};
use ring::signature::KeyPair;
use crate::error::InitError;
use crate::identity::helpers::load_keypair_from_der_or_seed;
use crate::keri::inception::{generate_keypair_for_init, sign_with_pkcs8_for_init};
use crate::keri::{
CesrKey, Event, KeriSequence, Prefix, Said, Seal, Threshold, VersionString, finalize_dip_event,
finalize_drt_event, serialize_for_signing,
};
use crate::storage::registry::RegistryBackend;
use auths_keri::{
AgentScope, DipEvent, DipEventInit, DrtEvent, DrtEventInit, IndexedSignature, IxnEvent,
KeriPublicKey, SourceSeal, decode_agent_scope, encode_agent_scope, finalize_ixn_event,
serialize_attachment, serialize_source_seal_couples,
};
pub struct DelegatedDevice {
pub device_did: IdentityDID,
pub device_prefix: Prefix,
pub device_alias: KeyAlias,
}
#[allow(clippy::too_many_arguments)]
pub fn incept_delegated_device(
backend: Arc<dyn RegistryBackend + Send + Sync>,
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
device_alias: &KeyAlias,
device_curve: CurveType,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<DelegatedDevice, InitError> {
let bundle = build_device_dip(root_prefix, device_curve)?;
let device_did = bundle.device_did.clone();
let device_prefix = bundle.device_prefix.clone();
anchor_received_dip(
backend.as_ref(),
root_prefix,
root_alias,
root_curve,
&bundle.dip,
&bundle.attachment,
passphrase_provider,
keychain,
)?;
let pass = passphrase_provider.get_passphrase(&format!(
"Create passphrase for device key '{}':",
device_alias
))?;
let enc_cur = encrypt_keypair(bundle.current_pkcs8.as_ref(), &pass)?;
keychain.store_key(device_alias, &device_did, KeyRole::Primary, &enc_cur)?;
let next_alias = KeyAlias::new_unchecked(format!("{}--next-0", device_alias));
let enc_next = encrypt_keypair(bundle.next_pkcs8.as_ref(), &pass)?;
keychain.store_key(&next_alias, &device_did, KeyRole::NextRotation, &enc_next)?;
Ok(DelegatedDevice {
device_did,
device_prefix,
device_alias: device_alias.clone(),
})
}
pub struct DeviceDipBundle {
pub dip: DipEvent,
pub attachment: Vec<u8>,
pub device_prefix: Prefix,
pub device_did: IdentityDID,
pub current_pkcs8: Pkcs8Der,
pub next_pkcs8: Pkcs8Der,
pub device_curve: CurveType,
}
pub fn build_device_dip(
root_prefix: &Prefix,
device_curve: CurveType,
) -> Result<DeviceDipBundle, InitError> {
let device_cur =
generate_keypair_for_init(device_curve).map_err(|e| InitError::Crypto(e.to_string()))?;
let device_next =
generate_keypair_for_init(device_curve).map_err(|e| InitError::Crypto(e.to_string()))?;
let device_next_commitment = compute_next_commitment(&device_next.verkey());
let dip = finalize_dip_event(DipEvent::new(DipEventInit {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(device_cur.cesr_encoded.clone())],
nt: Threshold::Simple(1),
n: vec![device_next_commitment],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
di: root_prefix.clone(),
}))
.map_err(|e| InitError::Keri(e.to_string()))?;
let device_prefix = dip.i.clone();
let dip_canonical = serialize_for_signing(&Event::Dip(dip.clone()))
.map_err(|e| InitError::Keri(e.to_string()))?;
let dip_sig = sign_with_pkcs8_for_init(device_curve, &device_cur.pkcs8, &dip_canonical)
.map_err(|e| InitError::Crypto(e.to_string()))?;
let attachment = serialize_attachment(&[IndexedSignature {
index: 0,
prior_index: None,
sig: dip_sig,
}])
.map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
#[allow(clippy::disallowed_methods)]
let device_did = IdentityDID::new_unchecked(format!("did:keri:{}", device_prefix));
Ok(DeviceDipBundle {
dip,
attachment,
device_prefix,
device_did,
current_pkcs8: device_cur.pkcs8,
next_pkcs8: device_next.pkcs8,
device_curve,
})
}
#[allow(clippy::too_many_arguments)]
pub fn anchor_received_dip(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
dip: &DipEvent,
dip_attachment: &[u8],
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(IdentityDID, IxnEvent), InitError> {
let device_prefix = dip.i.clone();
let dip_said = dip.d.clone();
let anchor_ixn = author_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
vec![Seal::KeyEvent {
i: device_prefix.clone(),
s: KeriSequence::new(0),
d: dip_said,
}],
passphrase_provider,
keychain,
)?;
let source_seal = SourceSeal {
s: anchor_ixn.s,
d: anchor_ixn.d.clone(),
};
let mut anchored_dip = dip.clone();
anchored_dip.source_seal = Some(source_seal.clone());
let mut attachment = dip_attachment.to_vec();
attachment.extend_from_slice(
&serialize_source_seal_couples(&[source_seal])
.map_err(|e| InitError::Keri(format!("source seal serialization: {e}")))?,
);
backend
.append_signed_event(&device_prefix, &Event::Dip(anchored_dip), &attachment)
.map_err(|e| InitError::Registry(e.to_string()))?;
#[allow(clippy::disallowed_methods)]
let device_did = IdentityDID::new_unchecked(format!("did:keri:{}", device_prefix));
Ok((device_did, anchor_ixn))
}
#[allow(clippy::too_many_arguments)]
pub fn stage_root_anchor_ixn(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
anchors: Vec<Seal>,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
batch: &mut crate::storage::registry::backend::AtomicWriteBatch,
) -> Result<IxnEvent, InitError> {
let root_state = backend
.get_key_state(root_prefix)
.map_err(|e| InitError::Registry(e.to_string()))?;
if !root_state.can_emit_ixn() {
return Err(InitError::InvalidData(
"root identity cannot anchor (interaction events forbidden)".to_string(),
));
}
let ixn = finalize_ixn_event(IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: root_prefix.clone(),
s: KeriSequence::new(root_state.sequence + 1),
p: root_state.last_event_said.clone(),
a: anchors,
})
.map_err(|e| InitError::Keri(e.to_string()))?;
let canonical = serialize_for_signing(&Event::Ixn(ixn.clone()))
.map_err(|e| InitError::Keri(e.to_string()))?;
let (_did, _role, encrypted) = keychain.load_key(root_alias)?;
let pass = passphrase_provider
.get_passphrase(&format!("Enter passphrase for root key '{}':", root_alias))?;
let pkcs8 = Pkcs8Der::new(decrypt_keypair(&encrypted, &pass)?.to_vec());
let sig = sign_with_pkcs8_for_init(root_curve, &pkcs8, &canonical)
.map_err(|e| InitError::Crypto(e.to_string()))?;
let attachment = serialize_attachment(&[IndexedSignature {
index: 0,
prior_index: None,
sig,
}])
.map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
batch.stage_event(root_prefix.clone(), Event::Ixn(ixn.clone()), attachment);
Ok(ixn)
}
pub fn author_root_anchor_ixn(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
anchors: Vec<Seal>,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<IxnEvent, InitError> {
let mut batch = crate::storage::registry::backend::AtomicWriteBatch::new();
let ixn = stage_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
anchors,
passphrase_provider,
keychain,
&mut batch,
)?;
backend
.commit_batch(&batch)
.map_err(|e| InitError::Registry(e.to_string()))?;
Ok(ixn)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DelegatedRole {
Device,
Agent,
}
pub struct DelegatedDeviceInfo {
pub device_prefix: Prefix,
pub revoked: bool,
pub role: DelegatedRole,
}
fn agent_role_marker(agent_prefix: &Prefix) -> String {
format!("agent:{}", agent_prefix.as_str())
}
#[allow(clippy::too_many_arguments)]
pub fn mark_delegated_agent(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
agent_prefix: &Prefix,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(), InitError> {
author_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
vec![Seal::Digest {
d: Said::new_unchecked(agent_role_marker(agent_prefix)),
}],
passphrase_provider,
keychain,
)
.map(|_| ())
}
#[allow(clippy::too_many_arguments)]
pub fn mark_agent_scope(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
agent_prefix: &Prefix,
scope: &AgentScope,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(), InitError> {
author_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
vec![Seal::Digest {
d: Said::new_unchecked(encode_agent_scope(agent_prefix.as_str(), scope)),
}],
passphrase_provider,
keychain,
)
.map(|_| ())
}
pub fn read_agent_scope(events: &[Event], agent_prefix: &Prefix) -> Option<AgentScope> {
let mut found: Option<AgentScope> = None;
for event in events {
for seal in event.anchors() {
if let Seal::Digest { d } = seal
&& let Some((prefix, scope)) = decode_agent_scope(d.as_str())
&& prefix == agent_prefix.as_str()
{
found = Some(scope);
}
}
}
found
}
const ORG_POLICY_MARKER: &str = "policy:";
#[allow(clippy::too_many_arguments)]
pub fn mark_org_policy(
backend: &(dyn RegistryBackend + Send + Sync),
org_prefix: &Prefix,
org_alias: &KeyAlias,
org_curve: CurveType,
source_hash_hex: &str,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(), InitError> {
author_root_anchor_ixn(
backend,
org_prefix,
org_alias,
org_curve,
vec![Seal::Digest {
d: Said::new_unchecked(format!("{ORG_POLICY_MARKER}{source_hash_hex}")),
}],
passphrase_provider,
keychain,
)
.map(|_| ())
}
pub fn read_org_policy_hash(events: &[Event]) -> Option<String> {
let mut found: Option<String> = None;
for event in events {
for seal in event.anchors() {
if let Seal::Digest { d } = seal
&& let Some(hash) = d.as_str().strip_prefix(ORG_POLICY_MARKER)
{
found = Some(hash.to_string());
}
}
}
found
}
pub fn list_delegated_devices(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
) -> Result<Vec<DelegatedDeviceInfo>, InitError> {
let mut delegated: Vec<String> = Vec::new();
let mut revoked: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut agents: std::collections::HashSet<String> = std::collections::HashSet::new();
backend
.visit_events(root_prefix, 0, &mut |event| {
for seal in event.anchors() {
match seal {
Seal::KeyEvent { i, .. } => {
let p = i.as_str().to_string();
if !delegated.contains(&p) {
delegated.push(p);
}
}
Seal::Digest { d } => match d.as_str().strip_prefix("agent:") {
Some(prefix) => {
agents.insert(prefix.to_string());
}
None => {
revoked.insert(d.as_str().to_string());
}
},
_ => {}
}
}
ControlFlow::Continue(())
})
.map_err(|e| InitError::Registry(e.to_string()))?;
Ok(delegated
.into_iter()
.map(|p| DelegatedDeviceInfo {
revoked: revoked.contains(&p),
role: if agents.contains(&p) {
DelegatedRole::Agent
} else {
DelegatedRole::Device
},
device_prefix: Prefix::new_unchecked(p),
})
.collect())
}
fn delegation_status(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
device_prefix: &Prefix,
) -> Result<(bool, bool), InitError> {
let mut delegated = false;
let mut revoked = false;
backend
.visit_events(root_prefix, 0, &mut |event| {
for seal in event.anchors() {
match seal {
Seal::KeyEvent { i, .. } if i.as_str() == device_prefix.as_str() => {
delegated = true;
}
Seal::Digest { d } if d.as_str() == device_prefix.as_str() => {
revoked = true;
}
_ => {}
}
}
ControlFlow::Continue(())
})
.map_err(|e| InitError::Registry(e.to_string()))?;
Ok((delegated, revoked))
}
#[allow(clippy::too_many_arguments)]
pub fn revoke_delegated_device(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
device_prefix: &Prefix,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(), InitError> {
if device_prefix.as_str() == root_prefix.as_str() {
return Err(InitError::InvalidData(
"cannot revoke the root identity's own controller".to_string(),
));
}
let (delegated, revoked) = delegation_status(backend, root_prefix, device_prefix)?;
if !delegated {
return Err(InitError::InvalidData(format!(
"device {device_prefix} is not a delegated controller of the identity"
)));
}
if revoked {
return Ok(());
}
let revocation = Seal::Digest {
d: Said::new_unchecked(device_prefix.as_str().to_string()),
};
author_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
vec![revocation],
passphrase_provider,
keychain,
)
.map(|_| ())
}
#[allow(clippy::too_many_arguments)]
pub fn revoke_delegated_devices_batch(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
device_prefixes: &[Prefix],
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(Vec<Prefix>, Option<IxnEvent>), InitError> {
let mut seals = Vec::new();
let mut newly_revoked = Vec::new();
for device_prefix in device_prefixes {
if device_prefix.as_str() == root_prefix.as_str() {
return Err(InitError::InvalidData(
"cannot revoke the root identity's own controller".to_string(),
));
}
let (delegated, revoked) = delegation_status(backend, root_prefix, device_prefix)?;
if !delegated {
return Err(InitError::InvalidData(format!(
"device {device_prefix} is not a delegated controller of the identity"
)));
}
if revoked {
continue; }
seals.push(Seal::Digest {
d: Said::new_unchecked(device_prefix.as_str().to_string()),
});
newly_revoked.push(device_prefix.clone());
}
if seals.is_empty() {
return Ok((newly_revoked, None));
}
let ixn = author_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
seals,
passphrase_provider,
keychain,
)?;
Ok((newly_revoked, Some(ixn)))
}
fn reveal_pre_committed_next_key(
keychain: &(dyn KeyStorage + Send + Sync),
passphrase_provider: &dyn PassphraseProvider,
device_alias: &KeyAlias,
device_state: &auths_keri::KeyState,
) -> Result<(KeyAlias, Pkcs8Der, KeriPublicKey), InitError> {
let next_alias = KeyAlias::new_unchecked(format!(
"{}--next-{}",
device_alias, device_state.last_establishment_sequence
));
let (_did, _role, encrypted) = keychain.load_key(&next_alias)?;
let pass = passphrase_provider.get_passphrase(&format!(
"Enter passphrase for device next key '{}':",
next_alias
))?;
let revealed_pkcs8 = Pkcs8Der::new(decrypt_keypair(&encrypted, &pass)?.to_vec());
let revealed_keypair = load_keypair_from_der_or_seed(revealed_pkcs8.as_ref())?;
#[allow(clippy::expect_used)] let revealed_verkey = KeriPublicKey::ed25519(revealed_keypair.public_key().as_ref())
.expect("ring Ed25519 public key is 32 bytes");
if device_state.next_commitment.is_empty()
|| !verify_commitment(&revealed_verkey, &device_state.next_commitment[0])
{
return Err(InitError::InvalidData(
"device next key does not match its prior commitment".to_string(),
));
}
Ok((next_alias, revealed_pkcs8, revealed_verkey))
}
#[allow(clippy::too_many_arguments)]
pub fn rotate_delegated_device(
backend: &(dyn RegistryBackend + Send + Sync),
root_prefix: &Prefix,
root_alias: &KeyAlias,
root_curve: CurveType,
device_prefix: &Prefix,
device_alias: &KeyAlias,
device_curve: CurveType,
passphrase_provider: &dyn PassphraseProvider,
keychain: &(dyn KeyStorage + Send + Sync),
) -> Result<(), InitError> {
let device_state = backend
.get_key_state(device_prefix)
.map_err(|e| InitError::Registry(e.to_string()))?;
let (next_alias, revealed_pkcs8, revealed_verkey) =
reveal_pre_committed_next_key(keychain, passphrase_provider, device_alias, &device_state)?;
let fresh_next =
generate_keypair_for_init(device_curve).map_err(|e| InitError::Crypto(e.to_string()))?;
let new_next_commitment = compute_next_commitment(&fresh_next.verkey());
let new_sequence = device_state.sequence + 1;
let revealed_cesr = revealed_verkey
.to_qb64()
.map_err(|e| InitError::InvalidData(e.to_string()))?;
let drt = finalize_drt_event(DrtEvent::new(DrtEventInit {
v: VersionString::placeholder(),
d: Said::default(),
i: device_prefix.clone(),
s: KeriSequence::new(new_sequence),
p: device_state.last_event_said.clone(),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(revealed_cesr)],
nt: Threshold::Simple(1),
n: vec![new_next_commitment],
bt: Threshold::Simple(0),
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
di: root_prefix.clone(),
}))
.map_err(|e| InitError::Keri(e.to_string()))?;
let drt_said = drt.d.clone();
let canonical = serialize_for_signing(&Event::Drt(drt.clone()))
.map_err(|e| InitError::Keri(e.to_string()))?;
let sig = sign_with_pkcs8_for_init(device_curve, &revealed_pkcs8, &canonical)
.map_err(|e| InitError::Crypto(e.to_string()))?;
let mut attachment = serialize_attachment(&[IndexedSignature {
index: 0,
prior_index: None,
sig,
}])
.map_err(|e| InitError::Keri(format!("attachment serialization: {e}")))?;
let anchor_ixn = author_root_anchor_ixn(
backend,
root_prefix,
root_alias,
root_curve,
vec![Seal::KeyEvent {
i: device_prefix.clone(),
s: KeriSequence::new(new_sequence),
d: drt_said,
}],
passphrase_provider,
keychain,
)?;
let source_seal = SourceSeal {
s: anchor_ixn.s,
d: anchor_ixn.d.clone(),
};
let mut anchored_drt = drt;
anchored_drt.source_seal = Some(source_seal.clone());
attachment.extend_from_slice(
&serialize_source_seal_couples(&[source_seal])
.map_err(|e| InitError::Keri(format!("source seal serialization: {e}")))?,
);
backend
.append_signed_event(device_prefix, &Event::Drt(anchored_drt), &attachment)
.map_err(|e| InitError::Registry(e.to_string()))?;
#[allow(clippy::disallowed_methods)]
let device_did = IdentityDID::new_unchecked(format!("did:keri:{}", device_prefix));
let store_pass = passphrase_provider.get_passphrase(&format!(
"Create passphrase for rotated device key '{}':",
device_alias
))?;
let enc_cur = encrypt_keypair(revealed_pkcs8.as_ref(), &store_pass)?;
keychain.store_key(device_alias, &device_did, KeyRole::Primary, &enc_cur)?;
let new_next_alias =
KeyAlias::new_unchecked(format!("{}--next-{}", device_alias, new_sequence));
let enc_next = encrypt_keypair(fresh_next.pkcs8.as_ref(), &store_pass)?;
keychain.store_key(
&new_next_alias,
&device_did,
KeyRole::NextRotation,
&enc_next,
)?;
let _ = keychain.delete_key(&next_alias);
Ok(())
}