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
214 lines
5.7 KiB
Rust
214 lines
5.7 KiB
Rust
//! User resolution handlers — username <-> identity key lookups,
|
|
//! key revocation, and KT audit.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use bytes::Bytes;
|
|
use prost::Message;
|
|
use quicproquo_proto::qpq::v1;
|
|
use quicproquo_rpc::method::{HandlerResult, RequestContext};
|
|
|
|
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,
|
|
Err(e) => return e,
|
|
};
|
|
|
|
let req = match v1::ResolveUserRequest::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 = ResolveUserReq {
|
|
username: req.username,
|
|
};
|
|
|
|
match svc.resolve_user(domain_req) {
|
|
Ok(resp) => {
|
|
let proto = v1::ResolveUserResponse {
|
|
identity_key: resp.identity_key,
|
|
inclusion_proof: resp.inclusion_proof,
|
|
};
|
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
|
}
|
|
Err(e) => domain_err(e),
|
|
}
|
|
}
|
|
|
|
pub async fn handle_resolve_identity(
|
|
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::ResolveIdentityRequest::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 = ResolveIdentityReq {
|
|
identity_key: req.identity_key,
|
|
};
|
|
|
|
match svc.resolve_identity(domain_req) {
|
|
Ok(resp) => {
|
|
let proto = v1::ResolveIdentityResponse {
|
|
username: resp.username,
|
|
};
|
|
HandlerResult::ok(Bytes::from(proto.encode_to_vec()))
|
|
}
|
|
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),
|
|
}
|
|
}
|