use ed25519_dalek::SigningKey;
use serde::{Deserialize, Serialize};
use crate::crypto::signing;
use crate::identity::IdentityId;
use crate::receipt::WitnessSignature;
use super::grant::TrustId;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevocationConfig {
pub revocation_key_id: String,
pub revocation_channel: RevocationChannel,
pub required_witnesses: Vec<IdentityId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RevocationChannel {
Local,
Http { url: String },
Ledger { ledger_id: String },
Multi(Vec<RevocationChannel>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Revocation {
pub trust_id: TrustId,
pub revoker: IdentityId,
pub revoker_key: String,
pub revoked_at: u64,
pub reason: RevocationReason,
pub signature: String,
pub witnesses: Vec<WitnessSignature>,
}
impl Revocation {
pub fn create(
trust_id: TrustId,
revoker: IdentityId,
reason: RevocationReason,
signing_key: &SigningKey,
) -> Self {
let now = crate::time::now_micros();
let revoker_key = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
signing_key.verifying_key().to_bytes(),
);
let to_sign = format!(
"revoke:{}:{}:{}:{}",
trust_id.0,
revoker.0,
now,
reason.as_str(),
);
let signature = signing::sign_to_base64(signing_key, to_sign.as_bytes());
Self {
trust_id,
revoker,
revoker_key,
revoked_at: now,
reason,
signature,
witnesses: Vec::new(),
}
}
pub fn add_witness(&mut self, witness: WitnessSignature) {
self.witnesses.push(witness);
}
pub fn verify_signature(&self) -> crate::error::Result<()> {
let pub_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
&self.revoker_key,
)
.map_err(|e| {
crate::error::IdentityError::InvalidKey(format!("invalid base64 revoker key: {e}"))
})?;
let key_bytes: [u8; 32] = pub_bytes.try_into().map_err(|_| {
crate::error::IdentityError::InvalidKey("revoker key must be 32 bytes".into())
})?;
let verifying_key =
crate::crypto::keys::Ed25519KeyPair::verifying_key_from_bytes(&key_bytes)?;
let to_verify = format!(
"revoke:{}:{}:{}:{}",
self.trust_id.0,
self.revoker.0,
self.revoked_at,
self.reason.as_str(),
);
signing::verify_from_base64(&verifying_key, to_verify.as_bytes(), &self.signature)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RevocationReason {
Expired,
Compromised,
PolicyViolation,
ManualRevocation,
GranteeRequest,
Custom(String),
}
impl RevocationReason {
pub fn as_str(&self) -> &str {
match self {
Self::Expired => "expired",
Self::Compromised => "compromised",
Self::PolicyViolation => "policy_violation",
Self::ManualRevocation => "manual_revocation",
Self::GranteeRequest => "grantee_request",
Self::Custom(s) => s.as_str(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::IdentityAnchor;
#[test]
fn test_revocation_create_and_verify() {
let grantor = IdentityAnchor::new(Some("grantor".into()));
let trust_id = TrustId("atrust_test123".to_string());
let revocation = Revocation::create(
trust_id.clone(),
grantor.id(),
RevocationReason::ManualRevocation,
grantor.signing_key(),
);
assert_eq!(revocation.trust_id, trust_id);
assert_eq!(revocation.revoker, grantor.id());
assert_eq!(revocation.reason, RevocationReason::ManualRevocation);
assert!(revocation.verify_signature().is_ok());
}
#[test]
fn test_revocation_reason_strings() {
assert_eq!(RevocationReason::Expired.as_str(), "expired");
assert_eq!(RevocationReason::Compromised.as_str(), "compromised");
assert_eq!(
RevocationReason::PolicyViolation.as_str(),
"policy_violation"
);
assert_eq!(
RevocationReason::ManualRevocation.as_str(),
"manual_revocation"
);
assert_eq!(RevocationReason::GranteeRequest.as_str(), "grantee_request");
assert_eq!(RevocationReason::Custom("breach".into()).as_str(), "breach");
}
}