use crate::crypto::{PublicKey, Signature, SigningKey};
use crate::domain::{REVOCATION_REQUEST_CONTEXT, SRL_CONTEXT};
use crate::error::{Error, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
pub const MAX_REVOCATION_REQUEST_AGE_SECS: i64 = 300;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevocationRequest {
pub warrant_id: String,
pub reason: String,
pub requestor: PublicKey,
pub requested_at: DateTime<Utc>,
signature: Signature,
}
impl RevocationRequest {
pub fn new(
warrant_id: impl Into<String>,
reason: impl Into<String>,
requestor_keypair: &SigningKey,
) -> Result<Self> {
let warrant_id = warrant_id.into();
let reason = reason.into();
let requested_at = Utc::now();
let requestor = requestor_keypair.public_key();
let payload = (&warrant_id, &reason, &requestor, requested_at.timestamp());
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let preimage = Self::build_preimage(&payload_bytes);
let signature = requestor_keypair.sign(&preimage);
Ok(Self {
warrant_id,
reason,
requestor,
requested_at,
signature,
})
}
pub fn verify_signature(&self) -> Result<()> {
let payload = (
&self.warrant_id,
&self.reason,
&self.requestor,
self.requested_at.timestamp(),
);
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let preimage = Self::build_preimage(&payload_bytes);
self.requestor
.verify(&preimage, &self.signature)
.map_err(|_| Error::SignatureInvalid("Revocation request signature invalid".into()))
}
pub fn validate(
&self,
warrant_id: &str,
warrant_issuer: &PublicKey,
warrant_holder: Option<&PublicKey>,
warrant_expires_at: DateTime<Utc>,
control_plane_key: &PublicKey,
) -> Result<()> {
self.verify_signature()?;
if self.warrant_id != warrant_id {
return Err(Error::Unauthorized(format!(
"Request warrant_id '{}' does not match provided warrant '{}'",
self.warrant_id, warrant_id
)));
}
if !self.is_authorized(warrant_issuer, warrant_holder, control_plane_key) {
return Err(Error::Unauthorized(format!(
"Requestor {} is not authorized to revoke this warrant",
hex::encode(self.requestor.to_bytes())
)));
}
let age = Utc::now().signed_duration_since(self.requested_at);
if age.num_seconds() > MAX_REVOCATION_REQUEST_AGE_SECS {
return Err(Error::Unauthorized(format!(
"Revocation request is too old ({} seconds, max {})",
age.num_seconds(),
MAX_REVOCATION_REQUEST_AGE_SECS
)));
}
if age.num_seconds() < -60 {
return Err(Error::Unauthorized(
"Revocation request timestamp is in the future".into(),
));
}
if warrant_expires_at < Utc::now() {
return Err(Error::Unauthorized(
"Warrant has already expired; revocation unnecessary".into(),
));
}
Ok(())
}
pub fn is_authorized(
&self,
warrant_issuer: &PublicKey,
warrant_holder: Option<&PublicKey>,
control_plane_key: &PublicKey,
) -> bool {
if &self.requestor == control_plane_key {
return true;
}
if &self.requestor == warrant_issuer {
return true;
}
if let Some(holder) = warrant_holder {
if &self.requestor == holder {
return true;
}
}
false
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
ciborium::ser::into_writer(self, &mut bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
Ok(bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
ciborium::de::from_reader(bytes).map_err(|e| Error::DeserializationError(e.to_string()))
}
fn build_preimage(payload_bytes: &[u8]) -> Vec<u8> {
let mut preimage =
Vec::with_capacity(REVOCATION_REQUEST_CONTEXT.len() + payload_bytes.len());
preimage.extend_from_slice(REVOCATION_REQUEST_CONTEXT);
preimage.extend_from_slice(payload_bytes);
preimage
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SrlPayload {
revoked_ids: Vec<String>,
version: u64,
issued_at: DateTime<Utc>,
issuer: PublicKey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedRevocationList {
payload: SrlPayload,
signature: Signature,
#[serde(skip)]
lookup_cache: Option<HashSet<String>>,
}
impl SignedRevocationList {
pub fn builder() -> SrlBuilder {
SrlBuilder::new()
}
pub fn empty(keypair: &SigningKey) -> Result<Self> {
Self::builder().version(0).build(keypair)
}
pub fn verify(&self, expected_issuer: &PublicKey) -> Result<()> {
if &self.payload.issuer != expected_issuer {
return Err(Error::SignatureInvalid(
"SRL issuer does not match expected key".into(),
));
}
let payload_bytes = self.payload_bytes()?;
let preimage = Self::build_preimage(&payload_bytes);
expected_issuer
.verify(&preimage, &self.signature)
.map_err(|_| Error::SignatureInvalid("SRL signature verification failed".into()))
}
pub fn is_revoked(&self, warrant_id: &str) -> bool {
if let Some(cache) = &self.lookup_cache {
cache.contains(warrant_id)
} else {
self.payload.revoked_ids.iter().any(|id| id == warrant_id)
}
}
pub fn build_cache(&mut self) {
self.lookup_cache = Some(self.payload.revoked_ids.iter().cloned().collect());
}
pub fn version(&self) -> u64 {
self.payload.version
}
pub fn issued_at(&self) -> DateTime<Utc> {
self.payload.issued_at
}
pub fn issuer(&self) -> &PublicKey {
&self.payload.issuer
}
pub fn revoked_ids(&self) -> &[String] {
&self.payload.revoked_ids
}
pub fn len(&self) -> usize {
self.payload.revoked_ids.len()
}
pub fn is_empty(&self) -> bool {
self.payload.revoked_ids.is_empty()
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
ciborium::ser::into_writer(self, &mut bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
Ok(bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let mut srl: Self = ciborium::de::from_reader(bytes)
.map_err(|e| Error::DeserializationError(e.to_string()))?;
srl.build_cache();
Ok(srl)
}
fn payload_bytes(&self) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
ciborium::ser::into_writer(&self.payload, &mut bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
Ok(bytes)
}
fn build_preimage(payload_bytes: &[u8]) -> Vec<u8> {
let mut preimage = Vec::with_capacity(SRL_CONTEXT.len() + payload_bytes.len());
preimage.extend_from_slice(SRL_CONTEXT);
preimage.extend_from_slice(payload_bytes);
preimage
}
}
pub struct SrlBuilder {
revoked_ids: Vec<String>,
version: u64,
}
impl SrlBuilder {
pub fn new() -> Self {
Self {
revoked_ids: Vec::new(),
version: 1,
}
}
pub fn revoke(mut self, warrant_id: impl Into<String>) -> Self {
self.revoked_ids.push(warrant_id.into());
self
}
pub fn revoke_all(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
for id in ids {
self.revoked_ids.push(id.into());
}
self
}
pub fn from_existing_pruned<F>(mut self, existing: &SignedRevocationList, is_expired: F) -> Self
where
F: Fn(&str) -> bool,
{
for id in existing.revoked_ids() {
if !is_expired(id) {
self.revoked_ids.push(id.clone());
}
}
self
}
pub fn from_existing(self, existing: &SignedRevocationList) -> Self {
self.from_existing_pruned(existing, |_| false)
}
pub fn version(mut self, version: u64) -> Self {
self.version = version;
self
}
pub fn build(self, keypair: &SigningKey) -> Result<SignedRevocationList> {
let payload = SrlPayload {
revoked_ids: self.revoked_ids.clone(),
version: self.version,
issued_at: Utc::now(),
issuer: keypair.public_key(),
};
let mut payload_bytes = Vec::new();
ciborium::ser::into_writer(&payload, &mut payload_bytes)
.map_err(|e| Error::SerializationError(e.to_string()))?;
let preimage = SignedRevocationList::build_preimage(&payload_bytes);
let signature = keypair.sign(&preimage);
let lookup_cache = Some(self.revoked_ids.into_iter().collect());
Ok(SignedRevocationList {
payload,
signature,
lookup_cache,
})
}
}
impl Default for SrlBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signed_revocation_list_basic() {
let keypair = SigningKey::generate();
let srl = SignedRevocationList::builder()
.revoke("tnu_wrt_compromised_1")
.revoke("tnu_wrt_compromised_2")
.version(1)
.build(&keypair)
.unwrap();
assert!(srl.verify(&keypair.public_key()).is_ok());
assert!(srl.is_revoked("tnu_wrt_compromised_1"));
assert!(srl.is_revoked("tnu_wrt_compromised_2"));
assert!(!srl.is_revoked("tnu_wrt_valid"));
assert_eq!(srl.version(), 1);
assert_eq!(srl.len(), 2);
}
#[test]
fn test_signed_revocation_list_wrong_key() {
let keypair = SigningKey::generate();
let other_keypair = SigningKey::generate();
let srl = SignedRevocationList::builder()
.revoke("tnu_wrt_test")
.build(&keypair)
.unwrap();
let result = srl.verify(&other_keypair.public_key());
assert!(result.is_err());
}
#[test]
fn test_signed_revocation_list_serialization() {
let keypair = SigningKey::generate();
let srl = SignedRevocationList::builder()
.revoke("tnu_wrt_test1")
.revoke("tnu_wrt_test2")
.version(42)
.build(&keypair)
.unwrap();
let bytes = srl.to_bytes().unwrap();
let loaded = SignedRevocationList::from_bytes(&bytes).unwrap();
assert!(loaded.verify(&keypair.public_key()).is_ok());
assert!(loaded.is_revoked("tnu_wrt_test1"));
assert!(loaded.is_revoked("tnu_wrt_test2"));
assert_eq!(loaded.version(), 42);
}
#[test]
fn test_anti_rollback_version() {
let keypair = SigningKey::generate();
let v1 = SignedRevocationList::builder()
.revoke("tnu_wrt_old")
.version(1)
.build(&keypair)
.unwrap();
let v2 = SignedRevocationList::builder()
.revoke("tnu_wrt_old")
.revoke("tnu_wrt_new")
.version(2)
.build(&keypair)
.unwrap();
assert!(v2.version() > v1.version());
assert!(!v1.is_revoked("tnu_wrt_new"));
assert!(v2.is_revoked("tnu_wrt_new"));
}
#[test]
fn test_empty_srl() {
let keypair = SigningKey::generate();
let srl = SignedRevocationList::empty(&keypair).unwrap();
assert!(srl.verify(&keypair.public_key()).is_ok());
assert!(srl.is_empty());
assert_eq!(srl.version(), 0);
}
#[test]
fn test_pruning_expired_warrants() {
let keypair = SigningKey::generate();
let v1 = SignedRevocationList::builder()
.revoke("tnu_wrt_expired_1")
.revoke("tnu_wrt_still_valid")
.revoke("tnu_wrt_expired_2")
.version(1)
.build(&keypair)
.unwrap();
assert_eq!(v1.len(), 3);
let expired_ids = ["tnu_wrt_expired_1", "tnu_wrt_expired_2"];
let v2 = SignedRevocationList::builder()
.from_existing_pruned(&v1, |id| expired_ids.contains(&id))
.version(2)
.build(&keypair)
.unwrap();
assert_eq!(v2.len(), 1);
assert!(v2.is_revoked("tnu_wrt_still_valid"));
assert!(!v2.is_revoked("tnu_wrt_expired_1"));
assert!(!v2.is_revoked("tnu_wrt_expired_2"));
}
#[test]
fn test_from_existing_adds_new_revocations() {
let keypair = SigningKey::generate();
let v1 = SignedRevocationList::builder()
.revoke("tnu_wrt_old")
.version(1)
.build(&keypair)
.unwrap();
let v2 = SignedRevocationList::builder()
.from_existing(&v1)
.revoke("tnu_wrt_new")
.version(2)
.build(&keypair)
.unwrap();
assert_eq!(v2.len(), 2);
assert!(v2.is_revoked("tnu_wrt_old"));
assert!(v2.is_revoked("tnu_wrt_new"));
}
#[test]
fn test_revocation_request_creation_and_verification() {
let requestor = SigningKey::generate();
let request =
RevocationRequest::new("tnu_wrt_compromised", "Key compromise detected", &requestor)
.unwrap();
assert_eq!(request.warrant_id, "tnu_wrt_compromised");
assert_eq!(request.reason, "Key compromise detected");
assert_eq!(request.requestor, requestor.public_key());
assert!(request.verify_signature().is_ok());
}
#[test]
fn test_revocation_request_serialization() {
let requestor = SigningKey::generate();
let request =
RevocationRequest::new("tnu_wrt_test", "Test revocation", &requestor).unwrap();
let bytes = request.to_bytes().unwrap();
let loaded = RevocationRequest::from_bytes(&bytes).unwrap();
assert_eq!(loaded.warrant_id, request.warrant_id);
assert_eq!(loaded.reason, request.reason);
assert!(loaded.verify_signature().is_ok());
}
#[test]
fn test_revocation_request_authorization_control_plane() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let request =
RevocationRequest::new("tnu_wrt_any", "Admin revocation", &control_plane).unwrap();
assert!(request.is_authorized(
&issuer.public_key(),
Some(&holder.public_key()),
&control_plane.public_key(),
));
}
#[test]
fn test_revocation_request_authorization_issuer() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let request = RevocationRequest::new(
"tnu_wrt_issued_by_me",
"Revoking delegated warrant",
&issuer,
)
.unwrap();
assert!(request.is_authorized(
&issuer.public_key(),
Some(&holder.public_key()),
&control_plane.public_key(),
));
}
#[test]
fn test_revocation_request_authorization_holder_surrender() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let request =
RevocationRequest::new("tnu_wrt_my_warrant", "Voluntary surrender", &holder).unwrap();
assert!(request.is_authorized(
&issuer.public_key(),
Some(&holder.public_key()),
&control_plane.public_key(),
));
}
#[test]
fn test_revocation_request_unauthorized() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let random_attacker = SigningKey::generate();
let request = RevocationRequest::new(
"tnu_wrt_not_mine",
"Trying to revoke someone else's warrant",
&random_attacker,
)
.unwrap();
assert!(!request.is_authorized(
&issuer.public_key(),
Some(&holder.public_key()),
&control_plane.public_key(),
));
}
#[test]
fn test_revocation_request_full_validation() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let warrant_id = "tnu_wrt_valid_123";
let expires_at = Utc::now() + chrono::Duration::hours(1);
let request = RevocationRequest::new(warrant_id, "Voluntary surrender", &holder).unwrap();
assert!(request
.validate(
warrant_id,
&issuer.public_key(),
Some(&holder.public_key()),
expires_at,
&control_plane.public_key(),
)
.is_ok());
}
#[test]
fn test_revocation_request_wrong_warrant_id() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let expires_at = Utc::now() + chrono::Duration::hours(1);
let request =
RevocationRequest::new("tnu_wrt_requested", "DoS attempt", &control_plane).unwrap();
let result = request.validate(
"tnu_wrt_actual", &issuer.public_key(),
None,
expires_at,
&control_plane.public_key(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("does not match"));
}
#[test]
fn test_revocation_request_unauthorized_requestor() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let attacker = SigningKey::generate();
let warrant_id = "tnu_wrt_target";
let expires_at = Utc::now() + chrono::Duration::hours(1);
let request =
RevocationRequest::new(warrant_id, "Malicious revocation", &attacker).unwrap();
let result = request.validate(
warrant_id,
&issuer.public_key(),
Some(&holder.public_key()),
expires_at,
&control_plane.public_key(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not authorized"));
}
#[test]
fn test_revocation_request_expired_warrant() {
let control_plane = SigningKey::generate();
let issuer = SigningKey::generate();
let warrant_id = "tnu_wrt_already_expired";
let expires_at = Utc::now() - chrono::Duration::hours(1);
let request = RevocationRequest::new(
warrant_id,
"Trying to revoke expired warrant",
&control_plane,
)
.unwrap();
let result = request.validate(
warrant_id,
&issuer.public_key(),
None,
expires_at,
&control_plane.public_key(),
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already expired"));
}
}