use std::collections::HashMap;
use ed25519_dalek::SigningKey;
use crate::workload_pkg::codec::hex_decode_exact;
pub use weft_licence_wire::request::{
CLOCK_FLOOR_SECS, REQUEST_DOMAIN, REQUEST_WINDOW_MS, signing_string, valid_nonce,
};
pub const MAX_REQUEST_BODY: usize = 16 * 1024;
const MAX_NONCES: usize = 4096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestAuth {
pub node: String,
pub timestamp_ms: u64,
pub nonce: String,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LicenceRequest {
pub method: String,
pub path: String,
pub auth: Option<RequestAuth>,
pub body: Vec<u8>,
}
#[allow(clippy::too_many_arguments)]
pub fn sign_request(
key: &SigningKey,
node: &str,
audience: &str,
method: &str,
path: &str,
body: Vec<u8>,
ts_ms: u64,
nonce: &str,
) -> LicenceRequest {
let sig = weft_licence_wire::request::sign(key, node, audience, method, path, &body, ts_ms, nonce);
LicenceRequest {
method: method.to_owned(),
path: path.to_owned(),
auth: Some(RequestAuth {
node: node.to_owned(),
timestamp_ms: ts_ms,
nonce: nonce.to_owned(),
signature: sig,
}),
body,
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RequestRefused {
#[error("request is not signed")]
Unsigned,
#[error("request body too large")]
TooLarge,
#[error("request names a node that is not the bound steward")]
WrongNode,
#[error("request timestamp is outside the window")]
Stale,
#[error("malformed request header")]
Malformed,
#[error("request signature does not verify")]
BadSignature,
#[error("request nonce was already used")]
Replay,
}
#[derive(Debug, Default)]
pub struct ReplayGuard {
seen: HashMap<String, u64>,
}
impl ReplayGuard {
pub fn first_use(&mut self, nonce: &str, ts_ms: u64, now_ms: u64) -> bool {
self.seen.retain(|_, t| t.saturating_add(REQUEST_WINDOW_MS) >= now_ms);
if self.seen.contains_key(nonce) || self.seen.len() >= MAX_NONCES {
return false;
}
self.seen.insert(nonce.to_owned(), ts_ms);
true
}
}
pub fn verify_request(
req: &LicenceRequest,
steward_pubkey: &[u8; 32],
steward_node: &str,
audience: &str,
now_ms: u64,
replay: &mut ReplayGuard,
) -> Result<(), RequestRefused> {
let auth = req.auth.as_ref().ok_or(RequestRefused::Unsigned)?;
if req.body.len() > MAX_REQUEST_BODY {
return Err(RequestRefused::TooLarge);
}
if !weft_licence_wire::request::valid_ts(auth.timestamp_ms) || !valid_nonce(&auth.nonce) {
return Err(RequestRefused::Malformed);
}
if hex_decode_exact::<64>(&auth.signature).is_none() {
return Err(RequestRefused::Malformed);
}
if auth.node != steward_node {
return Err(RequestRefused::WrongNode);
}
if !weft_licence_wire::request::within_window(auth.timestamp_ms, now_ms, REQUEST_WINDOW_MS) {
return Err(RequestRefused::Stale);
}
if !weft_licence_wire::request::verify_signature(
steward_pubkey,
&auth.node,
audience,
&req.method,
&req.path,
&req.body,
auth.timestamp_ms,
&auth.nonce,
&auth.signature,
) {
return Err(RequestRefused::BadSignature);
}
if !replay.first_use(&auth.nonce, auth.timestamp_ms, now_ms) {
return Err(RequestRefused::Replay);
}
Ok(())
}