Draft 0.5: identity/registry spec; Phase A signed MA registry, mailbox challenge auth, key rotation, skew rejection
This commit is contained in:
+73
-2
@@ -13,17 +13,25 @@ use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::message::{Envelope, TYPE_AGGREGATE, TYPE_QUERY, TYPE_RESPONSE};
|
||||
use crate::crypto::{now_ts, poll_signing_bytes, random_nonce, verify_signature};
|
||||
use crate::message::{Envelope, TYPE_AGGREGATE, TYPE_QUERY, TYPE_RESPONSE, timestamp_is_fresh};
|
||||
|
||||
pub const DEFAULT_CAPACITY: usize = 256;
|
||||
const CHALLENGE_TTL: Duration = Duration::from_secs(120);
|
||||
|
||||
#[derive(Default)]
|
||||
struct MemberQueue {
|
||||
items: VecDeque<(u64, Envelope)>,
|
||||
}
|
||||
|
||||
struct Challenge {
|
||||
member: String,
|
||||
created: Instant,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
members: HashMap<String, MemberQueue>,
|
||||
challenges: HashMap<String, Challenge>,
|
||||
seq: u64,
|
||||
}
|
||||
|
||||
@@ -37,6 +45,7 @@ impl Relay {
|
||||
Arc::new(Self {
|
||||
inner: Mutex::new(Inner {
|
||||
members: HashMap::new(),
|
||||
challenges: HashMap::new(),
|
||||
seq: 0,
|
||||
}),
|
||||
capacity,
|
||||
@@ -79,6 +88,7 @@ impl Relay {
|
||||
pub fn router(relay: Arc<Relay>) -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/v1/challenge", get(challenge))
|
||||
.route("/v1/publish", post(publish))
|
||||
.route("/v1/unicast", post(unicast))
|
||||
.route("/v1/poll", get(poll))
|
||||
@@ -97,14 +107,48 @@ pub async fn bind(addr: &str) -> Result<(TcpListener, SocketAddr)> {
|
||||
Ok((listener, local))
|
||||
}
|
||||
|
||||
fn valid_key(member: &str) -> bool {
|
||||
matches!(hex::decode(member), Ok(bytes) if bytes.len() == 32)
|
||||
}
|
||||
|
||||
async fn health() -> &'static str {
|
||||
"ok"
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChallengeParams {
|
||||
member: String,
|
||||
}
|
||||
|
||||
async fn challenge(
|
||||
State(relay): State<Arc<Relay>>,
|
||||
Query(params): Query<ChallengeParams>,
|
||||
) -> Response {
|
||||
if !valid_key(¶ms.member) {
|
||||
return bad_request("valid member key required");
|
||||
}
|
||||
let nonce = random_nonce();
|
||||
let mut inner = relay.inner.lock().expect("relay lock");
|
||||
inner
|
||||
.challenges
|
||||
.retain(|_, challenge| challenge.created.elapsed() < CHALLENGE_TTL);
|
||||
inner.challenges.insert(
|
||||
nonce.clone(),
|
||||
Challenge {
|
||||
member: params.member,
|
||||
created: Instant::now(),
|
||||
},
|
||||
);
|
||||
(StatusCode::OK, Json(json!({ "nonce": nonce }))).into_response()
|
||||
}
|
||||
|
||||
async fn publish(State(relay): State<Arc<Relay>>, Json(envelope): Json<Envelope>) -> Response {
|
||||
if envelope.verify().is_err() {
|
||||
return bad_request("invalid signature");
|
||||
}
|
||||
if !timestamp_is_fresh(envelope.ts, now_ts()) {
|
||||
return bad_request("stale timestamp");
|
||||
}
|
||||
if envelope.msg_type != TYPE_QUERY {
|
||||
return bad_request("relay carries broadcast queries only");
|
||||
}
|
||||
@@ -131,6 +175,9 @@ async fn unicast(
|
||||
if envelope.verify().is_err() {
|
||||
return bad_request("invalid signature");
|
||||
}
|
||||
if !timestamp_is_fresh(envelope.ts, now_ts()) {
|
||||
return bad_request("stale timestamp");
|
||||
}
|
||||
if envelope.msg_type != TYPE_RESPONSE && envelope.msg_type != TYPE_AGGREGATE {
|
||||
return bad_request("unicast carries responses and aggregates only");
|
||||
}
|
||||
@@ -148,13 +195,33 @@ async fn unicast(
|
||||
#[derive(Deserialize)]
|
||||
struct PollParams {
|
||||
member: String,
|
||||
nonce: String,
|
||||
sig: String,
|
||||
timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
async fn poll(State(relay): State<Arc<Relay>>, Query(params): Query<PollParams>) -> Response {
|
||||
if params.member.is_empty() || hex::decode(¶ms.member).is_err() {
|
||||
if !valid_key(¶ms.member) {
|
||||
return bad_request("valid member key required");
|
||||
}
|
||||
if verify_signature(
|
||||
¶ms.member,
|
||||
&poll_signing_bytes(¶ms.member, ¶ms.nonce),
|
||||
¶ms.sig,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return unauthorized("invalid poll signature");
|
||||
}
|
||||
{
|
||||
let mut inner = relay.inner.lock().expect("relay lock");
|
||||
match inner.challenges.remove(¶ms.nonce) {
|
||||
Some(challenge)
|
||||
if challenge.member == params.member
|
||||
&& challenge.created.elapsed() < CHALLENGE_TTL => {}
|
||||
_ => return unauthorized("unknown, expired, or reused challenge"),
|
||||
}
|
||||
}
|
||||
let timeout = Duration::from_millis(params.timeout_ms.unwrap_or(25_000).min(60_000));
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
@@ -181,6 +248,10 @@ fn bad_request(message: &str) -> Response {
|
||||
(StatusCode::BAD_REQUEST, Json(json!({ "error": message }))).into_response()
|
||||
}
|
||||
|
||||
fn unauthorized(message: &str) -> Response {
|
||||
(StatusCode::UNAUTHORIZED, Json(json!({ "error": message }))).into_response()
|
||||
}
|
||||
|
||||
fn backpressure(status: StatusCode) -> Response {
|
||||
(
|
||||
status,
|
||||
|
||||
Reference in New Issue
Block a user