clawft_kernel/licence/
request.rs1use std::collections::HashMap;
14
15use ed25519_dalek::SigningKey;
16
17use crate::workload_pkg::codec::hex_decode_exact;
18
19pub use weft_licence_wire::request::{
20 CLOCK_FLOOR_SECS, REQUEST_DOMAIN, REQUEST_WINDOW_MS, signing_string, valid_nonce,
21};
22
23pub const MAX_REQUEST_BODY: usize = 16 * 1024;
25const MAX_NONCES: usize = 4096;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RequestAuth {
31 pub node: String,
33 pub timestamp_ms: u64,
35 pub nonce: String,
37 pub signature: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct LicenceRequest {
44 pub method: String,
46 pub path: String,
48 pub auth: Option<RequestAuth>,
50 pub body: Vec<u8>,
52}
53
54#[allow(clippy::too_many_arguments)]
56pub fn sign_request(
57 key: &SigningKey,
58 node: &str,
59 audience: &str,
60 method: &str,
61 path: &str,
62 body: Vec<u8>,
63 ts_ms: u64,
64 nonce: &str,
65) -> LicenceRequest {
66 let sig = weft_licence_wire::request::sign(key, node, audience, method, path, &body, ts_ms, nonce);
67 LicenceRequest {
68 method: method.to_owned(),
69 path: path.to_owned(),
70 auth: Some(RequestAuth {
71 node: node.to_owned(),
72 timestamp_ms: ts_ms,
73 nonce: nonce.to_owned(),
74 signature: sig,
75 }),
76 body,
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
82pub enum RequestRefused {
83 #[error("request is not signed")]
85 Unsigned,
86 #[error("request body too large")]
88 TooLarge,
89 #[error("request names a node that is not the bound steward")]
91 WrongNode,
92 #[error("request timestamp is outside the window")]
94 Stale,
95 #[error("malformed request header")]
97 Malformed,
98 #[error("request signature does not verify")]
100 BadSignature,
101 #[error("request nonce was already used")]
103 Replay,
104}
105
106#[derive(Debug, Default)]
110pub struct ReplayGuard {
111 seen: HashMap<String, u64>,
112}
113
114impl ReplayGuard {
115 pub fn first_use(&mut self, nonce: &str, ts_ms: u64, now_ms: u64) -> bool {
118 self.seen.retain(|_, t| t.saturating_add(REQUEST_WINDOW_MS) >= now_ms);
119 if self.seen.contains_key(nonce) || self.seen.len() >= MAX_NONCES {
120 return false;
121 }
122 self.seen.insert(nonce.to_owned(), ts_ms);
123 true
124 }
125}
126
127pub fn verify_request(
130 req: &LicenceRequest,
131 steward_pubkey: &[u8; 32],
132 steward_node: &str,
133 audience: &str,
134 now_ms: u64,
135 replay: &mut ReplayGuard,
136) -> Result<(), RequestRefused> {
137 let auth = req.auth.as_ref().ok_or(RequestRefused::Unsigned)?;
138 if req.body.len() > MAX_REQUEST_BODY {
139 return Err(RequestRefused::TooLarge);
140 }
141 if !weft_licence_wire::request::valid_ts(auth.timestamp_ms) || !valid_nonce(&auth.nonce) {
142 return Err(RequestRefused::Malformed);
143 }
144 if hex_decode_exact::<64>(&auth.signature).is_none() {
145 return Err(RequestRefused::Malformed);
146 }
147 if auth.node != steward_node {
148 return Err(RequestRefused::WrongNode);
149 }
150 if !weft_licence_wire::request::within_window(auth.timestamp_ms, now_ms, REQUEST_WINDOW_MS) {
151 return Err(RequestRefused::Stale);
152 }
153 if !weft_licence_wire::request::verify_signature(
154 steward_pubkey,
155 &auth.node,
156 audience,
157 &req.method,
158 &req.path,
159 &req.body,
160 auth.timestamp_ms,
161 &auth.nonce,
162 &auth.signature,
163 ) {
164 return Err(RequestRefused::BadSignature);
165 }
166 if !replay.first_use(&auth.nonce, auth.timestamp_ms, now_ms) {
167 return Err(RequestRefused::Replay);
168 }
169 Ok(())
170}