use std::sync::Arc;
use auths_core::signing::{SecureSigner, StorageSigner};
use auths_core::storage::keychain::KeyAlias;
use auths_crypto::CurveType;
use auths_id::keri::types::Prefix;
use auths_id::keri::{Event, Said, Seal};
use auths_keri::KeriPublicKey;
use serde::{Deserialize, Serialize};
use crate::context::AuthsContext;
use crate::domains::org::error::OrgError;
fn curve_tag(curve: CurveType) -> &'static str {
match curve {
CurveType::Ed25519 => "ed25519",
CurveType::P256 => "p256",
}
}
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<String>,
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 sign_offboarding_record(
ctx: &AuthsContext,
org_alias: &KeyAlias,
org_curve: CurveType,
record: OffboardingRecord,
) -> Result<SignedOffboardingRecord, OrgError> {
let canonical = json_canon::to_string(&record)
.map_err(|e| OrgError::Signing(format!("canonicalize offboarding record: {e}")))?;
let signer = StorageSigner::new(Arc::clone(&ctx.key_storage));
let sig = signer
.sign_with_alias(
org_alias,
ctx.passphrase_provider.as_ref(),
canonical.as_bytes(),
)
.map_err(|e| OrgError::Signing(e.to_string()))?;
Ok(SignedOffboardingRecord {
record,
org_curve: curve_tag(org_curve).to_string(),
signature: hex::encode(sig),
})
}
pub fn store_offboarding_record(
ctx: &AuthsContext,
org_prefix: &Prefix,
member_prefix: &Prefix,
signed: &SignedOffboardingRecord,
) -> Result<(), OrgError> {
let bytes = serde_json::to_vec(signed)
.map_err(|e| OrgError::Signing(format!("serialize offboarding record: {e}")))?;
let key = Said::new_unchecked(member_prefix.as_str().to_string());
ctx.registry
.store_credential(org_prefix, &key, &bytes)
.map_err(OrgError::Storage)
}
pub fn load_offboarding_record(
ctx: &AuthsContext,
org_prefix: &Prefix,
member_prefix: &Prefix,
) -> Result<Option<SignedOffboardingRecord>, OrgError> {
let key = Said::new_unchecked(member_prefix.as_str().to_string());
let bytes = ctx
.registry
.load_credential(org_prefix, &key)
.map_err(OrgError::Storage)?;
match bytes {
Some(b) => {
let signed: SignedOffboardingRecord = serde_json::from_slice(&b)
.map_err(|e| OrgError::Signing(format!("deserialize offboarding record: {e}")))?;
Ok(Some(signed))
}
None => Ok(None),
}
}
pub fn verify_offboarding_record(
signed: &SignedOffboardingRecord,
org_public_key: &[u8],
org_curve: CurveType,
org_kel: &[Event],
) -> Result<(), OrgError> {
if curve_from_tag(&signed.org_curve) != org_curve {
return Err(OrgError::Signing(
"offboarding record curve tag does not match the org key curve".to_string(),
));
}
let canonical = json_canon::to_string(&signed.record)
.map_err(|e| OrgError::Signing(format!("canonicalize offboarding record: {e}")))?;
let sig = hex::decode(&signed.signature)
.map_err(|e| OrgError::Signing(format!("decode offboarding signature: {e}")))?;
let key = KeriPublicKey::from_verkey_bytes(org_public_key, org_curve)
.map_err(|e| OrgError::InvalidPublicKey(e.to_string()))?;
key.verify_signature(canonical.as_bytes(), &sig)
.map_err(|e| OrgError::Signing(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(OrgError::Signing(
"offboarding record is not bound to a matching org KEL revocation seal".to_string(),
)),
}
}