use auths_crypto::CurveType;
use auths_keri::{Event, KeriPublicKey, Prefix, Seal};
use serde::{Deserialize, Serialize};
use super::error::OrgBundleError;
fn curve_from_tag(tag: &str) -> CurveType {
match tag {
"ed25519" => CurveType::Ed25519,
_ => CurveType::P256,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OffboardingRecord {
pub org_did: String,
pub member_did: String,
pub revoked_at_seq: u128,
pub revocation_seal_said: String,
pub reason: Option<String>,
pub operator_did: String,
pub prior_role: Option<String>,
pub prior_caps: Vec<auths_keri::Capability>,
pub recorded_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedOffboardingRecord {
pub record: OffboardingRecord,
pub org_curve: String,
pub signature: String,
}
pub fn find_revocation_event(org_kel: &[Event], member_prefix: &Prefix) -> Option<(String, u128)> {
let mut found = None;
for event in org_kel {
for seal in event.anchors() {
if let Seal::Digest { d } = seal
&& d.as_str() == member_prefix.as_str()
{
found = Some((event.said().as_str().to_string(), event.sequence().value()));
}
}
}
found
}
pub fn verify_offboarding_record(
signed: &SignedOffboardingRecord,
org_public_key: &[u8],
org_curve: CurveType,
org_kel: &[Event],
) -> Result<(), OrgBundleError> {
if curve_from_tag(&signed.org_curve) != org_curve {
return Err(OrgBundleError::RecordInvalid(
"offboarding record curve tag does not match the org key curve".to_string(),
));
}
let canonical = json_canon::to_string(&signed.record)
.map_err(|e| OrgBundleError::Canonicalize(format!("offboarding record: {e}")))?;
let sig = hex::decode(&signed.signature)
.map_err(|e| OrgBundleError::RecordInvalid(format!("decode offboarding signature: {e}")))?;
let key = KeriPublicKey::from_verkey_bytes(org_public_key, org_curve)
.map_err(|e| OrgBundleError::RecordInvalid(format!("invalid org public key: {e}")))?;
key.verify_signature(canonical.as_bytes(), &sig)
.map_err(|e| {
OrgBundleError::RecordInvalid(format!("offboarding signature invalid: {e}"))
})?;
let member_prefix = Prefix::new_unchecked(
signed
.record
.member_did
.strip_prefix("did:keri:")
.unwrap_or(&signed.record.member_did)
.to_string(),
);
match find_revocation_event(org_kel, &member_prefix) {
Some((said, seq))
if said == signed.record.revocation_seal_said
&& seq == signed.record.revoked_at_seq =>
{
Ok(())
}
_ => Err(OrgBundleError::RecordInvalid(
"offboarding record is not bound to a matching org KEL revocation seal".to_string(),
)),
}
}