use crate::{signing, Did, Error, Result, RootKey};
use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Verifier};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DelegationType {
Session,
Recovery,
Service,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
Sign,
Handshake,
Delegate,
RotateRoot,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delegation {
#[serde(rename = "type")]
pub type_: String,
pub version: String,
pub root_did: String,
pub delegate_pubkey: String,
pub delegate_type: DelegationType,
pub issued_at: i64,
pub expires_at: i64,
pub capabilities: Vec<Capability>,
pub revocation_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
impl Delegation {
pub fn new(
root_did: Did,
delegate_pubkey: String,
delegate_type: DelegationType,
capabilities: Vec<Capability>,
expires_at: DateTime<Utc>,
) -> Self {
Self {
type_: "KeyDelegation".to_string(),
version: "1.0".to_string(),
root_did: root_did.to_string(),
delegate_pubkey,
delegate_type,
issued_at: Utc::now().timestamp_millis(),
expires_at: expires_at.timestamp_millis(),
capabilities,
revocation_id: Uuid::now_v7().to_string(),
signature: None,
}
}
pub fn sign(mut self, root_key: &RootKey) -> Result<Self> {
self.signature = None;
let canonical = signing::canonicalize(&self)?;
let sig = root_key.sign(&canonical);
self.signature = Some(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
sig.to_bytes(),
));
Ok(self)
}
pub fn verify(&self) -> Result<()> {
let sig_b64 = self.signature.as_ref().ok_or(Error::InvalidSignature)?;
let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, sig_b64)
.map_err(|_| Error::InvalidSignature)?;
let signature =
Signature::from_bytes(&sig_bytes.try_into().map_err(|_| Error::InvalidSignature)?);
let root_did: Did = self.root_did.parse()?;
let public_key = root_did.public_key()?;
let mut unsigned = self.clone();
unsigned.signature = None;
let canonical = signing::canonicalize(&unsigned)?;
public_key
.verify(&canonical, &signature)
.map_err(|_| Error::InvalidSignature)
}
pub fn is_valid_at(&self, now: DateTime<Utc>) -> Result<()> {
let now_ms = now.timestamp_millis();
if now_ms < self.issued_at {
return Err(Error::DelegationNotYetValid);
}
if now_ms > self.expires_at {
return Err(Error::DelegationExpired);
}
Ok(())
}
pub fn has_capability(&self, cap: &Capability) -> bool {
self.capabilities.contains(cap)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_delegation_roundtrip() {
let root = RootKey::generate();
let session_pubkey = "test_session_key_base58".to_string();
let delegation = Delegation::new(
root.did(),
session_pubkey,
DelegationType::Session,
vec![Capability::Sign, Capability::Handshake],
Utc::now() + Duration::hours(24),
);
let signed = delegation.sign(&root).unwrap();
assert!(signed.signature.is_some());
signed.verify().unwrap();
}
#[test]
fn test_delegation_expiry() {
let root = RootKey::generate();
let delegation = Delegation::new(
root.did(),
"test".to_string(),
DelegationType::Session,
vec![Capability::Sign],
Utc::now() - Duration::hours(1), );
let result = delegation.is_valid_at(Utc::now());
assert!(matches!(result, Err(Error::DelegationExpired)));
}
}