feat(kt): add key revocation and Merkle-log audit support

Add RevocationLog with domain-separated leaf hashes (0x02 prefix) for
tracking revoked identity keys alongside the KT MerkleLog. Includes:

- RevocationLog with O(1) lookup, serialization, and double-revoke guard
- MerkleLog.append_raw() for pre-computed hashes
- MerkleLog.audit_log(start, end) for paginated log retrieval
- RevokeKey (510), CheckRevocation (511), AuditKeyTransparency (520) RPCs
- Server domain logic + v2 handlers + FileBackedStore/SqlStore persistence
- 4 new revocation tests + all 21 KT tests + 65 server tests passing
This commit is contained in:
2026-03-04 20:53:41 +01:00
parent f667281831
commit 1768f85258
11 changed files with 657 additions and 11 deletions

View File

@@ -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<ServerState>) -> 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<ServerState>, 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<ServerState>, 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<ServerState>, 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<ServerState>,
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<ServerState>,
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),
}
}