diff --git a/crates/quicproquo-kt/src/error.rs b/crates/quicproquo-kt/src/error.rs index cdaa1e8..2b716c9 100644 --- a/crates/quicproquo-kt/src/error.rs +++ b/crates/quicproquo-kt/src/error.rs @@ -10,4 +10,7 @@ pub enum KtError { #[error("serialisation error: {0}")] Serialisation(String), + + #[error("identity key is already revoked")] + AlreadyRevoked, } diff --git a/crates/quicproquo-kt/src/lib.rs b/crates/quicproquo-kt/src/lib.rs index 245278e..e1e6f92 100644 --- a/crates/quicproquo-kt/src/lib.rs +++ b/crates/quicproquo-kt/src/lib.rs @@ -25,10 +25,12 @@ use sha2::{Digest, Sha256}; mod error; mod proof; +pub mod revocation; mod tree; pub use error::KtError; pub use proof::{verify_inclusion, InclusionProof}; +pub use revocation::{RevocationEntry, RevocationLog, RevocationReason}; pub use tree::MerkleLog; /// Domain-separation prefix for leaf nodes (RFC 6962 §2.1). diff --git a/crates/quicproquo-kt/src/revocation.rs b/crates/quicproquo-kt/src/revocation.rs new file mode 100644 index 0000000..3205c62 --- /dev/null +++ b/crates/quicproquo-kt/src/revocation.rs @@ -0,0 +1,278 @@ +//! Key revocation tracking for the Key Transparency log. +//! +//! Revocation entries are appended to the same Merkle log as regular key +//! bindings, using a distinct leaf hash prefix to differentiate them. A +//! separate in-memory index tracks which identity keys have been revoked, +//! enabling O(1) revocation checks. +//! +//! ## Revocation leaf hash +//! +//! ```text +//! SHA-256(0x02 || SHA-256(identity_key || 0x00 || reason_bytes)) +//! ``` +//! +//! The 0x02 prefix domain-separates revocation leaves from binding leaves (0x00) +//! and internal nodes (0x01). + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{KtError, MerkleLog}; + +/// Domain-separation prefix for revocation leaves. +const REVOCATION_PREFIX: u8 = 0x02; + +/// Reason for key revocation. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum RevocationReason { + /// Key material was compromised. + Compromised, + /// Key was superseded by a new key. + Superseded, + /// User-initiated revocation (e.g. account deletion). + UserRevoked, +} + +impl RevocationReason { + fn as_bytes(&self) -> &[u8] { + match self { + RevocationReason::Compromised => b"compromised", + RevocationReason::Superseded => b"superseded", + RevocationReason::UserRevoked => b"user_revoked", + } + } + + /// Parse from a string tag. + pub fn from_tag(tag: &str) -> Option { + match tag { + "compromised" => Some(RevocationReason::Compromised), + "superseded" => Some(RevocationReason::Superseded), + "user_revoked" => Some(RevocationReason::UserRevoked), + _ => None, + } + } + + /// Return the string tag for serialization. + pub fn as_tag(&self) -> &str { + match self { + RevocationReason::Compromised => "compromised", + RevocationReason::Superseded => "superseded", + RevocationReason::UserRevoked => "user_revoked", + } + } +} + +/// A record of a key revocation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RevocationEntry { + /// The 32-byte identity key that was revoked. + pub identity_key: Vec, + /// Reason for revocation. + pub reason: RevocationReason, + /// Timestamp (ms since UNIX epoch) when the revocation was recorded. + pub timestamp_ms: u64, + /// Index of the revocation leaf in the Merkle log. + pub leaf_index: u64, +} + +/// Tracks revoked identity keys alongside the Merkle log. +/// +/// Revocation entries are appended to the Merkle log (with a distinct prefix) +/// and indexed in-memory by identity key for O(1) lookup. +#[derive(Default, Serialize, Deserialize, Clone)] +pub struct RevocationLog { + /// Revocation entries in append order. + entries: Vec, + /// Index from identity_key bytes to entry index for O(1) lookup. + #[serde(skip)] + index: HashMap, usize>, +} + +impl RevocationLog { + /// Create an empty revocation log. + pub fn new() -> Self { + Self::default() + } + + /// Rebuild the in-memory index from the entries list. + /// + /// Must be called after deserialization. + pub fn rebuild_index(&mut self) { + self.index.clear(); + for (i, entry) in self.entries.iter().enumerate() { + self.index.insert(entry.identity_key.clone(), i); + } + } + + /// Record a key revocation, appending a revocation leaf to the Merkle log. + /// + /// Returns the leaf index in the Merkle log, or an error if the key is + /// already revoked. + pub fn revoke( + &mut self, + kt_log: &mut MerkleLog, + identity_key: &[u8], + reason: RevocationReason, + timestamp_ms: u64, + ) -> Result { + if self.index.contains_key(identity_key) { + return Err(KtError::AlreadyRevoked); + } + + // Compute revocation leaf hash and append to the Merkle log. + let leaf = revocation_leaf_hash(identity_key, &reason); + let leaf_index = kt_log.append_raw(leaf); + + let entry = RevocationEntry { + identity_key: identity_key.to_vec(), + reason, + timestamp_ms, + leaf_index, + }; + + let entry_idx = self.entries.len(); + self.entries.push(entry); + self.index.insert(identity_key.to_vec(), entry_idx); + + Ok(leaf_index) + } + + /// Check if an identity key has been revoked. + pub fn is_revoked(&self, identity_key: &[u8]) -> bool { + self.index.contains_key(identity_key) + } + + /// Get the revocation entry for an identity key, if revoked. + pub fn get(&self, identity_key: &[u8]) -> Option<&RevocationEntry> { + self.index + .get(identity_key) + .map(|&idx| &self.entries[idx]) + } + + /// Return all revocation entries in append order. + pub fn entries(&self) -> &[RevocationEntry] { + &self.entries + } + + /// Number of revoked keys. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Return `true` if no keys have been revoked. + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Serialise the revocation log to bytes (bincode). + pub fn to_bytes(&self) -> Result, KtError> { + bincode::serialize(self).map_err(|e| KtError::Serialisation(e.to_string())) + } + + /// Deserialise from bytes and rebuild the in-memory index. + pub fn from_bytes(bytes: &[u8]) -> Result { + let mut log: Self = + bincode::deserialize(bytes).map_err(|e| KtError::Serialisation(e.to_string()))?; + log.rebuild_index(); + Ok(log) + } +} + +/// Compute the leaf hash for a revocation entry. +/// +/// `SHA-256(0x02 || SHA-256(identity_key || 0x00 || reason_bytes))` +pub fn revocation_leaf_hash(identity_key: &[u8], reason: &RevocationReason) -> [u8; 32] { + let mut inner = Sha256::new(); + inner.update(identity_key); + inner.update([0x00]); + inner.update(reason.as_bytes()); + let inner_digest: [u8; 32] = inner.finalize().into(); + + let mut outer = Sha256::new(); + outer.update([REVOCATION_PREFIX]); + outer.update(inner_digest); + outer.finalize().into() +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + #[test] + fn revoke_and_check() { + let mut kt = MerkleLog::new(); + let mut revlog = RevocationLog::new(); + + // Append a normal binding first. + kt.append("alice", &[1u8; 32]); + + // Revoke alice's key. + let leaf_idx = revlog + .revoke(&mut kt, &[1u8; 32], RevocationReason::Compromised, 1000) + .unwrap(); + assert_eq!(leaf_idx, 1); // second leaf in the log + + assert!(revlog.is_revoked(&[1u8; 32])); + assert!(!revlog.is_revoked(&[2u8; 32])); + + let entry = revlog.get(&[1u8; 32]).unwrap(); + assert_eq!(entry.reason, RevocationReason::Compromised); + assert_eq!(entry.timestamp_ms, 1000); + } + + #[test] + fn double_revoke_fails() { + let mut kt = MerkleLog::new(); + let mut revlog = RevocationLog::new(); + + revlog + .revoke(&mut kt, &[1u8; 32], RevocationReason::Compromised, 1000) + .unwrap(); + let result = revlog.revoke(&mut kt, &[1u8; 32], RevocationReason::Superseded, 2000); + assert!(matches!(result, Err(KtError::AlreadyRevoked))); + } + + #[test] + fn revocation_leaf_is_distinct_from_binding_leaf() { + let binding_hash = crate::leaf_hash("alice", &[1u8; 32]); + let revocation_hash = + revocation_leaf_hash(&[1u8; 32], &RevocationReason::Compromised); + assert_ne!(binding_hash, revocation_hash); + } + + #[test] + fn serialization_roundtrip() { + let mut kt = MerkleLog::new(); + let mut revlog = RevocationLog::new(); + + revlog + .revoke(&mut kt, &[1u8; 32], RevocationReason::Compromised, 1000) + .unwrap(); + revlog + .revoke(&mut kt, &[2u8; 32], RevocationReason::Superseded, 2000) + .unwrap(); + + let bytes = revlog.to_bytes().unwrap(); + let restored = RevocationLog::from_bytes(&bytes).unwrap(); + + assert_eq!(restored.len(), 2); + assert!(restored.is_revoked(&[1u8; 32])); + assert!(restored.is_revoked(&[2u8; 32])); + } + + #[test] + fn reason_tag_roundtrip() { + for reason in &[ + RevocationReason::Compromised, + RevocationReason::Superseded, + RevocationReason::UserRevoked, + ] { + let tag = reason.as_tag(); + let parsed = RevocationReason::from_tag(tag).unwrap(); + assert_eq!(*reason, parsed); + } + } +} diff --git a/crates/quicproquo-kt/src/tree.rs b/crates/quicproquo-kt/src/tree.rs index 134b285..2a509be 100644 --- a/crates/quicproquo-kt/src/tree.rs +++ b/crates/quicproquo-kt/src/tree.rs @@ -93,6 +93,33 @@ impl MerkleLog { .map(|i| i as u64) } + /// Append a pre-computed leaf hash directly (used by revocation entries). + /// + /// Returns the leaf index. + pub fn append_raw(&mut self, hash: [u8; 32]) -> u64 { + let idx = self.leaves.len() as u64; + self.leaves.push(hash); + idx + } + + /// Return log entries in the range `[start, end)` as `(index, leaf_hash)` pairs. + /// + /// Used for KT audit — clients download the full log and verify inclusion proofs. + /// Returns an empty vec if `start >= self.len()`. + pub fn audit_log(&self, start: u64, end: u64) -> Vec<(u64, [u8; 32])> { + let n = self.len(); + let start = start.min(n) as usize; + let end = end.min(n) as usize; + if start >= end { + return Vec::new(); + } + self.leaves[start..end] + .iter() + .enumerate() + .map(|(i, &h)| ((start + i) as u64, h)) + .collect() + } + /// Serialise the log to bytes (bincode). pub fn to_bytes(&self) -> Result, KtError> { bincode::serialize(self) diff --git a/crates/quicproquo-proto/src/lib.rs b/crates/quicproquo-proto/src/lib.rs index 54f3fd3..39c178e 100644 --- a/crates/quicproquo-proto/src/lib.rs +++ b/crates/quicproquo-proto/src/lib.rs @@ -107,6 +107,11 @@ pub mod method_ids { pub const RESOLVE_USER: u16 = 500; pub const RESOLVE_IDENTITY: u16 = 501; + // Key Transparency (510-520) + pub const REVOKE_KEY: u16 = 510; + pub const CHECK_REVOCATION: u16 = 511; + pub const AUDIT_KEY_TRANSPARENCY: u16 = 520; + // Blob (600-601) pub const UPLOAD_BLOB: u16 = 600; pub const DOWNLOAD_BLOB: u16 = 601; diff --git a/crates/quicproquo-server/src/domain/types.rs b/crates/quicproquo-server/src/domain/types.rs index c315377..3451d01 100644 --- a/crates/quicproquo-server/src/domain/types.rs +++ b/crates/quicproquo-server/src/domain/types.rs @@ -208,6 +208,44 @@ pub struct FetchHybridKeysResp { pub keys: Vec>, } +// ── Key Transparency / Revocation ──────────────────────────────────── + +pub struct RevokeKeyReq { + pub identity_key: Vec, + pub reason: String, +} + +pub struct RevokeKeyResp { + pub success: bool, + pub leaf_index: u64, +} + +pub struct CheckRevocationReq { + pub identity_key: Vec, +} + +pub struct CheckRevocationResp { + pub revoked: bool, + pub reason: String, + pub timestamp_ms: u64, +} + +pub struct AuditKeyTransparencyReq { + pub start: u64, + pub end: u64, +} + +pub struct AuditLogEntry { + pub index: u64, + pub leaf_hash: Vec, +} + +pub struct AuditKeyTransparencyResp { + pub entries: Vec, + pub tree_size: u64, + pub root: Vec, +} + // ── Channel ────────────────────────────────────────────────────────────────── pub struct CreateChannelReq { diff --git a/crates/quicproquo-server/src/domain/users.rs b/crates/quicproquo-server/src/domain/users.rs index 67a9e07..65f14de 100644 --- a/crates/quicproquo-server/src/domain/users.rs +++ b/crates/quicproquo-server/src/domain/users.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; -use quicproquo_kt::MerkleLog; +use quicproquo_kt::{MerkleLog, RevocationLog, RevocationReason}; use crate::storage::Store; @@ -12,6 +12,7 @@ use super::types::*; pub struct UserService { pub store: Arc, pub kt_log: Arc>, + pub revocation_log: Arc>, } impl UserService { @@ -60,4 +61,86 @@ impl UserService { Ok(ResolveIdentityResp { username }) } + + /// Revoke an identity key in the Key Transparency log. + pub fn revoke_key(&self, req: RevokeKeyReq) -> Result { + if req.identity_key.len() != 32 { + return Err(DomainError::InvalidIdentityKey(req.identity_key.len())); + } + + let reason = RevocationReason::from_tag(&req.reason) + .ok_or_else(|| DomainError::BadParams(format!("invalid revocation reason: {}", req.reason)))?; + + let timestamp_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + let mut kt = self.kt_log.lock().map_err(|e| DomainError::Io(e.to_string()))?; + let mut revlog = self.revocation_log.lock().map_err(|e| DomainError::Io(e.to_string()))?; + + let leaf_index = revlog + .revoke(&mut kt, &req.identity_key, reason, timestamp_ms) + .map_err(|e| DomainError::BadParams(e.to_string()))?; + + // Persist updated logs. + if let Ok(bytes) = kt.to_bytes() { + let _ = self.store.save_kt_log(bytes); + } + if let Ok(bytes) = revlog.to_bytes() { + let _ = self.store.save_revocation_log(bytes); + } + + Ok(RevokeKeyResp { + success: true, + leaf_index, + }) + } + + /// Check if an identity key has been revoked. + pub fn check_revocation(&self, req: CheckRevocationReq) -> Result { + let revlog = self.revocation_log.lock().map_err(|e| DomainError::Io(e.to_string()))?; + + if let Some(entry) = revlog.get(&req.identity_key) { + Ok(CheckRevocationResp { + revoked: true, + reason: entry.reason.as_tag().to_string(), + timestamp_ms: entry.timestamp_ms, + }) + } else { + Ok(CheckRevocationResp { + revoked: false, + reason: String::new(), + timestamp_ms: 0, + }) + } + } + + /// Return a range of KT log entries for client-side audit. + pub fn audit_key_transparency( + &self, + req: AuditKeyTransparencyReq, + ) -> Result { + let kt = self.kt_log.lock().map_err(|e| DomainError::Io(e.to_string()))?; + + let end = if req.end == 0 { kt.len() } else { req.end }; + let log_entries = kt.audit_log(req.start, end); + + let entries: Vec = log_entries + .into_iter() + .map(|(index, hash)| AuditLogEntry { + index, + leaf_hash: hash.to_vec(), + }) + .collect(); + + let tree_size = kt.len(); + let root = kt.root().map(|r| r.to_vec()).unwrap_or_default(); + + Ok(AuditKeyTransparencyResp { + entries, + tree_size, + root, + }) + } } diff --git a/crates/quicproquo-server/src/storage.rs b/crates/quicproquo-server/src/storage.rs index 1b5b071..f3512ff 100644 --- a/crates/quicproquo-server/src/storage.rs +++ b/crates/quicproquo-server/src/storage.rs @@ -103,6 +103,12 @@ pub trait Store: Send + Sync { /// Load the persisted KT Merkle log, if any. fn load_kt_log(&self) -> Result>, StorageError>; + /// Persist the Key Transparency revocation log (bincode-serialised). + fn save_revocation_log(&self, bytes: Vec) -> Result<(), StorageError>; + + /// Load the persisted revocation log, if any. + fn load_revocation_log(&self) -> Result>, StorageError>; + /// Store an OPAQUE user record (serialized `ServerRegistration`). fn store_user_record(&self, username: &str, record: Vec) -> Result<(), StorageError>; @@ -397,6 +403,7 @@ pub struct FileBackedStore { setup_path: PathBuf, signing_key_path: PathBuf, kt_log_path: PathBuf, + revocation_log_path: PathBuf, users_path: PathBuf, identity_keys_path: PathBuf, channels_path: PathBuf, @@ -452,6 +459,7 @@ impl FileBackedStore { let setup_path = dir.join("server_setup.bin"); let signing_key_path = dir.join("server_signing_key.bin"); let kt_log_path = dir.join("kt_log.bin"); + let revocation_log_path = dir.join("revocation_log.bin"); let users_path = dir.join("users.bin"); let identity_keys_path = dir.join("identity_keys.bin"); let channels_path = dir.join("channels.bin"); @@ -470,6 +478,7 @@ impl FileBackedStore { setup_path, signing_key_path, kt_log_path, + revocation_log_path, users_path, identity_keys_path, channels_path, @@ -815,6 +824,26 @@ impl Store for FileBackedStore { Ok(Some(bytes)) } + fn save_revocation_log(&self, bytes: Vec) -> Result<(), StorageError> { + if let Some(parent) = self.revocation_log_path.parent() { + fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?; + } + fs::write(&self.revocation_log_path, &bytes) + .map_err(|e| StorageError::Io(e.to_string())) + } + + fn load_revocation_log(&self) -> Result>, StorageError> { + if !self.revocation_log_path.exists() { + return Ok(None); + } + let bytes = + fs::read(&self.revocation_log_path).map_err(|e| StorageError::Io(e.to_string()))?; + if bytes.is_empty() { + return Ok(None); + } + Ok(Some(bytes)) + } + fn store_user_record(&self, username: &str, record: Vec) -> Result<(), StorageError> { let mut map = lock(&self.users)?; match map.entry(username.to_string()) { diff --git a/crates/quicproquo-server/src/v2_handlers/mod.rs b/crates/quicproquo-server/src/v2_handlers/mod.rs index 9e1f27a..d1e41a0 100644 --- a/crates/quicproquo-server/src/v2_handlers/mod.rs +++ b/crates/quicproquo-server/src/v2_handlers/mod.rs @@ -44,6 +44,7 @@ pub struct ServerState { pub hooks: Arc, pub signing_key: Arc, pub kt_log: Arc>, + pub revocation_log: Arc>, pub data_dir: PathBuf, pub redact_logs: bool, /// Structured audit logger for security-relevant events. @@ -281,6 +282,23 @@ pub fn build_registry(default_rpc_timeout: std::time::Duration) -> MethodRegistr user::handle_resolve_identity, ); + // Key Transparency (510-520) + reg.register( + method_ids::REVOKE_KEY, + "RevokeKey", + user::handle_revoke_key, + ); + reg.register( + method_ids::CHECK_REVOCATION, + "CheckRevocation", + user::handle_check_revocation, + ); + reg.register( + method_ids::AUDIT_KEY_TRANSPARENCY, + "AuditKeyTransparency", + user::handle_audit_key_transparency, + ); + // Blob (600-601) — longer timeout for file transfers. reg.register_with_timeout( method_ids::UPLOAD_BLOB, diff --git a/crates/quicproquo-server/src/v2_handlers/user.rs b/crates/quicproquo-server/src/v2_handlers/user.rs index 1551232..7d13382 100644 --- a/crates/quicproquo-server/src/v2_handlers/user.rs +++ b/crates/quicproquo-server/src/v2_handlers/user.rs @@ -1,4 +1,5 @@ -//! User resolution handlers — username <-> identity key lookups. +//! User resolution handlers — username <-> identity key lookups, +//! key revocation, and KT audit. use std::sync::Arc; @@ -7,11 +8,21 @@ use prost::Message; use quicproquo_proto::qpq::v1; use quicproquo_rpc::method::{HandlerResult, RequestContext}; -use crate::domain::types::{ResolveIdentityReq, ResolveUserReq}; +use crate::domain::types::{ + AuditKeyTransparencyReq, CheckRevocationReq, ResolveIdentityReq, ResolveUserReq, RevokeKeyReq, +}; use crate::domain::users::UserService; use super::{domain_err, require_auth, ServerState}; +fn user_svc(state: &Arc) -> UserService { + UserService { + store: Arc::clone(&state.store), + kt_log: Arc::clone(&state.kt_log), + revocation_log: Arc::clone(&state.revocation_log), + } +} + pub async fn handle_resolve_user(state: Arc, ctx: RequestContext) -> HandlerResult { let _identity_key = match require_auth(&state, &ctx) { Ok(ik) => ik, @@ -28,10 +39,7 @@ pub async fn handle_resolve_user(state: Arc, ctx: RequestContext) - } }; - let svc = UserService { - store: Arc::clone(&state.store), - kt_log: Arc::clone(&state.kt_log), - }; + let svc = user_svc(&state); let domain_req = ResolveUserReq { username: req.username, @@ -68,10 +76,7 @@ pub async fn handle_resolve_identity( } }; - let svc = UserService { - store: Arc::clone(&state.store), - kt_log: Arc::clone(&state.kt_log), - }; + let svc = user_svc(&state); let domain_req = ResolveIdentityReq { identity_key: req.identity_key, @@ -87,3 +92,122 @@ pub async fn handle_resolve_identity( Err(e) => domain_err(e), } } + +pub async fn handle_revoke_key(state: Arc, ctx: RequestContext) -> HandlerResult { + let _identity_key = match require_auth(&state, &ctx) { + Ok(ik) => ik, + Err(e) => return e, + }; + + let req = match v1::RevokeKeyRequest::decode(ctx.payload) { + Ok(r) => r, + Err(e) => { + return HandlerResult::err( + quicproquo_rpc::error::RpcStatus::BadRequest, + &format!("decode: {e}"), + ) + } + }; + + let svc = user_svc(&state); + + let domain_req = RevokeKeyReq { + identity_key: req.identity_key, + reason: req.reason, + }; + + match svc.revoke_key(domain_req) { + Ok(resp) => { + let proto = v1::RevokeKeyResponse { + success: resp.success, + leaf_index: resp.leaf_index, + }; + HandlerResult::ok(Bytes::from(proto.encode_to_vec())) + } + Err(e) => domain_err(e), + } +} + +pub async fn handle_check_revocation( + state: Arc, + ctx: RequestContext, +) -> HandlerResult { + let _identity_key = match require_auth(&state, &ctx) { + Ok(ik) => ik, + Err(e) => return e, + }; + + let req = match v1::CheckRevocationRequest::decode(ctx.payload) { + Ok(r) => r, + Err(e) => { + return HandlerResult::err( + quicproquo_rpc::error::RpcStatus::BadRequest, + &format!("decode: {e}"), + ) + } + }; + + let svc = user_svc(&state); + + let domain_req = CheckRevocationReq { + identity_key: req.identity_key, + }; + + match svc.check_revocation(domain_req) { + Ok(resp) => { + let proto = v1::CheckRevocationResponse { + revoked: resp.revoked, + reason: resp.reason, + timestamp_ms: resp.timestamp_ms, + }; + HandlerResult::ok(Bytes::from(proto.encode_to_vec())) + } + Err(e) => domain_err(e), + } +} + +pub async fn handle_audit_key_transparency( + state: Arc, + ctx: RequestContext, +) -> HandlerResult { + let _identity_key = match require_auth(&state, &ctx) { + Ok(ik) => ik, + Err(e) => return e, + }; + + let req = match v1::AuditKeyTransparencyRequest::decode(ctx.payload) { + Ok(r) => r, + Err(e) => { + return HandlerResult::err( + quicproquo_rpc::error::RpcStatus::BadRequest, + &format!("decode: {e}"), + ) + } + }; + + let svc = user_svc(&state); + + let domain_req = AuditKeyTransparencyReq { + start: req.start, + end: req.end, + }; + + match svc.audit_key_transparency(domain_req) { + Ok(resp) => { + let proto = v1::AuditKeyTransparencyResponse { + entries: resp + .entries + .into_iter() + .map(|e| v1::LogEntry { + index: e.index, + leaf_hash: e.leaf_hash, + }) + .collect(), + tree_size: resp.tree_size, + root: resp.root, + }; + HandlerResult::ok(Bytes::from(proto.encode_to_vec())) + } + Err(e) => domain_err(e), + } +} diff --git a/proto/qpq/v1/keys.proto b/proto/qpq/v1/keys.proto index 7515552..1526564 100644 --- a/proto/qpq/v1/keys.proto +++ b/proto/qpq/v1/keys.proto @@ -43,3 +43,42 @@ message FetchHybridKeysRequest { message FetchHybridKeysResponse { repeated bytes keys = 1; } + +// Key revocation (method ID 510). +message RevokeKeyRequest { + bytes identity_key = 1; + string reason = 2; // "compromised", "superseded", "user_revoked" +} + +message RevokeKeyResponse { + bool success = 1; + uint64 leaf_index = 2; // Index of revocation entry in the KT Merkle log +} + +// Check revocation status (method ID 511). +message CheckRevocationRequest { + bytes identity_key = 1; +} + +message CheckRevocationResponse { + bool revoked = 1; + string reason = 2; + uint64 timestamp_ms = 3; +} + +// KT audit log retrieval (method ID 520). +message AuditKeyTransparencyRequest { + uint64 start = 1; + uint64 end = 2; // 0 = up to current size +} + +message AuditKeyTransparencyResponse { + repeated LogEntry entries = 1; + uint64 tree_size = 2; + bytes root = 3; +} + +message LogEntry { + uint64 index = 1; + bytes leaf_hash = 2; +}