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:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user