1use std::collections::BTreeSet;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use hmac::{Hmac, KeyInit, Mac};
17use serde::{Deserialize, Deserializer, Serialize};
18use sha2::Sha256;
19use thiserror::Error;
20
21use crate::frame::{self, PLUGIN_MAX_FRAME};
22
23pub const PROTOCOL_VERSION: i64 = 1;
25
26pub const TOKEN_TTL_SECONDS: i64 = 600;
28
29pub const TELEMETRY_STATE_TOPIC: &str = "telemetry.state";
33
34pub const TELEMETRY_STATE_MAX_HZ: u32 = 10;
38
39pub const TOKEN_REFRESH_METHOD: &str = "token.refresh";
47
48pub const ENV_PLUGIN_HTTP_SOCKET: &str = "ADOS_PLUGIN_HTTP_SOCKET";
51
52type HmacSha256 = Hmac<Sha256>;
53
54#[derive(Debug, Error)]
55pub enum PluginError {
56 #[error("framing error: {0}")]
57 Frame(#[from] frame::FrameError),
58 #[error("msgpack encode error: {0}")]
59 Encode(#[from] rmp_serde::encode::Error),
60 #[error("msgpack decode error: {0}")]
61 Decode(#[from] rmp_serde::decode::Error),
62}
63
64fn empty_args() -> rmpv::Value {
65 rmpv::Value::Map(Vec::new())
66}
67
68fn default_version() -> i64 {
69 PROTOCOL_VERSION
70}
71
72fn de_args<'de, D>(deserializer: D) -> Result<rmpv::Value, D::Error>
75where
76 D: Deserializer<'de>,
77{
78 let value = rmpv::Value::deserialize(deserializer)?;
79 Ok(match value {
80 rmpv::Value::Nil => empty_args(),
81 other => other,
82 })
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct Envelope {
89 #[serde(rename = "v", default = "default_version")]
90 pub version: i64,
91 #[serde(rename = "t")]
93 pub kind: String,
94 #[serde(rename = "m")]
95 pub method: String,
96 #[serde(rename = "c", default)]
97 pub capability: String,
98 #[serde(rename = "a", default = "empty_args", deserialize_with = "de_args")]
99 pub args: rmpv::Value,
100 #[serde(rename = "id")]
101 pub request_id: String,
102 #[serde(rename = "tok", default)]
103 pub token: String,
104 #[serde(rename = "err", default)]
105 pub error: Option<String>,
106}
107
108impl Envelope {
109 pub fn to_msgpack(&self) -> Result<Vec<u8>, PluginError> {
111 Ok(rmp_serde::to_vec_named(self)?)
114 }
115
116 pub fn from_msgpack(body: &[u8]) -> Result<Self, PluginError> {
118 Ok(rmp_serde::from_slice(body)?)
119 }
120
121 pub fn encode_frame(&self) -> Result<Vec<u8>, PluginError> {
124 let body = self.to_msgpack()?;
125 Ok(frame::encode_frame(&body, PLUGIN_MAX_FRAME)?)
126 }
127}
128
129#[derive(Debug, Error, PartialEq, Eq)]
130pub enum TokenError {
131 #[error("malformed capability token")]
132 Malformed,
133 #[error("capability blob is not valid hex")]
134 BadCapsBlob,
135 #[error("token timestamp is not an integer")]
136 BadTimestamp,
137 #[error("capability token HMAC mismatch")]
138 HmacMismatch,
139 #[error("capability token expired")]
140 Expired,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct CapabilityToken {
147 pub plugin_id: String,
148 pub session_id: String,
149 pub granted_caps: BTreeSet<String>,
152 pub issued_at: i64,
153 pub expires_at: i64,
154 pub signature: String,
156}
157
158impl CapabilityToken {
159 fn caps_csv(&self) -> String {
160 self.granted_caps
162 .iter()
163 .cloned()
164 .collect::<Vec<_>>()
165 .join(",")
166 }
167
168 pub fn to_token_string(&self) -> String {
170 let caps_hex = hex::encode(self.caps_csv().as_bytes());
171 [
172 "v1",
173 &self.plugin_id,
174 &self.session_id,
175 &self.issued_at.to_string(),
176 &self.expires_at.to_string(),
177 &caps_hex,
178 &self.signature,
179 ]
180 .join("|")
181 }
182
183 pub fn from_token_string(encoded: &str) -> Result<Self, TokenError> {
185 let parts: Vec<&str> = encoded.split('|').collect();
186 if parts.len() != 7 || parts[0] != "v1" {
187 return Err(TokenError::Malformed);
188 }
189 let caps_bytes = hex::decode(parts[5]).map_err(|_| TokenError::BadCapsBlob)?;
190 let caps_csv = String::from_utf8(caps_bytes).map_err(|_| TokenError::BadCapsBlob)?;
191 let granted_caps: BTreeSet<String> = if caps_csv.is_empty() {
192 BTreeSet::new()
193 } else {
194 caps_csv
195 .split(',')
196 .filter(|c| !c.is_empty())
197 .map(str::to_owned)
198 .collect()
199 };
200 let issued_at: i64 = parts[3].parse().map_err(|_| TokenError::BadTimestamp)?;
201 let expires_at: i64 = parts[4].parse().map_err(|_| TokenError::BadTimestamp)?;
202 Ok(Self {
203 plugin_id: parts[1].to_owned(),
204 session_id: parts[2].to_owned(),
205 granted_caps,
206 issued_at,
207 expires_at,
208 signature: parts[6].to_owned(),
209 })
210 }
211
212 pub fn is_expired(&self, now: i64) -> bool {
213 now >= self.expires_at
214 }
215}
216
217pub struct TokenIssuer {
220 secret: Vec<u8>,
221}
222
223impl TokenIssuer {
224 pub fn new(secret: Vec<u8>) -> Self {
225 Self { secret }
226 }
227
228 pub fn new_random() -> Self {
230 let mut secret = vec![0u8; 32];
231 getrandom::fill(&mut secret).expect("OS RNG unavailable");
232 Self { secret }
233 }
234
235 pub fn mint(
237 &self,
238 plugin_id: &str,
239 granted_caps: &BTreeSet<String>,
240 ttl_seconds: i64,
241 ) -> CapabilityToken {
242 let now = SystemTime::now()
245 .duration_since(UNIX_EPOCH)
246 .map(|d| d.as_secs() as i64)
247 .unwrap_or(0);
248 let mut session_bytes = [0u8; 8];
249 getrandom::fill(&mut session_bytes).expect("OS RNG unavailable");
250 let session_id = hex::encode(session_bytes);
251 self.mint_at(plugin_id, granted_caps, ttl_seconds, now, &session_id)
252 }
253
254 pub fn mint_at(
256 &self,
257 plugin_id: &str,
258 granted_caps: &BTreeSet<String>,
259 ttl_seconds: i64,
260 now: i64,
261 session_id: &str,
262 ) -> CapabilityToken {
263 let expires_at = now.saturating_add(ttl_seconds);
267 let signature = self.sign(plugin_id, session_id, now, expires_at, granted_caps);
268 CapabilityToken {
269 plugin_id: plugin_id.to_owned(),
270 session_id: session_id.to_owned(),
271 granted_caps: granted_caps.clone(),
272 issued_at: now,
273 expires_at,
274 signature,
275 }
276 }
277
278 pub fn verify(&self, token: &CapabilityToken, now: i64) -> Result<(), TokenError> {
280 let actual = hex::decode(&token.signature).map_err(|_| TokenError::HmacMismatch)?;
281 let payload = self.sign_payload(
282 &token.plugin_id,
283 &token.session_id,
284 token.issued_at,
285 token.expires_at,
286 &token.granted_caps,
287 );
288 let mut mac =
290 HmacSha256::new_from_slice(&self.secret).expect("HMAC accepts any key length");
291 mac.update(payload.as_bytes());
292 mac.verify_slice(&actual)
293 .map_err(|_| TokenError::HmacMismatch)?;
294 if token.is_expired(now) {
295 return Err(TokenError::Expired);
296 }
297 Ok(())
298 }
299
300 fn sign_payload(
301 &self,
302 plugin_id: &str,
303 session_id: &str,
304 issued_at: i64,
305 expires_at: i64,
306 caps: &BTreeSet<String>,
307 ) -> String {
308 let caps_csv = caps.iter().cloned().collect::<Vec<_>>().join(",");
309 [
310 plugin_id,
311 session_id,
312 &issued_at.to_string(),
313 &expires_at.to_string(),
314 &caps_csv,
315 ]
316 .join("|")
317 }
318
319 fn sign(
320 &self,
321 plugin_id: &str,
322 session_id: &str,
323 issued_at: i64,
324 expires_at: i64,
325 caps: &BTreeSet<String>,
326 ) -> String {
327 let payload = self.sign_payload(plugin_id, session_id, issued_at, expires_at, caps);
328 let mut mac =
329 HmacSha256::new_from_slice(&self.secret).expect("HMAC accepts any key length");
330 mac.update(payload.as_bytes());
331 hex::encode(mac.finalize().into_bytes())
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 fn caps(items: &[&str]) -> BTreeSet<String> {
340 items.iter().map(|s| s.to_string()).collect()
341 }
342
343 #[test]
346 fn version_matches_registry() {
347 assert_eq!(
348 PROTOCOL_VERSION as u16,
349 crate::contracts::contract_version("plugin.rpc").unwrap()
350 );
351 }
352
353 #[test]
354 fn envelope_round_trips_through_msgpack() {
355 let env = Envelope {
356 version: PROTOCOL_VERSION,
357 kind: "request".into(),
358 method: "event.publish".into(),
359 capability: "event.publish".into(),
360 args: rmpv::Value::Map(vec![(
361 rmpv::Value::String("topic".into()),
362 rmpv::Value::String("demo".into()),
363 )]),
364 request_id: "abc123".into(),
365 token: "v1|p|s|0|600||sig".into(),
366 error: None,
367 };
368 let body = env.to_msgpack().unwrap();
369 let back = Envelope::from_msgpack(&body).unwrap();
370 assert_eq!(env, back);
371 }
372
373 #[test]
374 fn nil_args_decode_to_empty_map() {
375 let env = Envelope {
378 version: 1,
379 kind: "event".into(),
380 method: "m".into(),
381 capability: String::new(),
382 args: rmpv::Value::Nil,
383 request_id: "id".into(),
384 token: String::new(),
385 error: None,
386 };
387 let body = env.to_msgpack().unwrap();
388 let back = Envelope::from_msgpack(&body).unwrap();
389 assert_eq!(back.args, rmpv::Value::Map(Vec::new()));
390 }
391
392 #[test]
393 fn envelope_uses_short_keys_on_the_wire() {
394 let env = Envelope {
395 version: 1,
396 kind: "event".into(),
397 method: "m".into(),
398 capability: "c".into(),
399 args: empty_args(),
400 request_id: "id".into(),
401 token: "tok".into(),
402 error: Some("boom".into()),
403 };
404 let body = env.to_msgpack().unwrap();
405 let value: rmpv::Value = rmp_serde::from_slice(&body).unwrap();
408 let map = match value {
409 rmpv::Value::Map(m) => m,
410 other => panic!("expected map, got {other:?}"),
411 };
412 let keys: BTreeSet<String> = map
413 .iter()
414 .filter_map(|(k, _)| k.as_str().map(str::to_owned))
415 .collect();
416 assert_eq!(keys, caps(&["v", "t", "m", "c", "a", "id", "tok", "err"]));
417 }
418
419 #[test]
420 fn token_string_round_trips() {
421 let issuer = TokenIssuer::new(b"test-secret-key".to_vec());
422 let token = issuer.mint_at(
423 "com.example.plugin",
424 &caps(&["mavlink.read", "event.publish"]),
425 600,
426 1000,
427 "deadbeef",
428 );
429 let s = token.to_token_string();
430 assert!(s.starts_with("v1|com.example.plugin|deadbeef|1000|1600|"));
432 let parsed = CapabilityToken::from_token_string(&s).unwrap();
433 assert_eq!(parsed, token);
434 }
435
436 #[test]
437 fn caps_are_canonically_sorted_in_the_signed_payload() {
438 let issuer = TokenIssuer::new(b"k".to_vec());
439 let a = issuer.mint_at("p", &caps(&["b", "a", "c"]), 600, 0, "s");
441 let b = issuer.mint_at("p", &caps(&["c", "b", "a"]), 600, 0, "s");
442 assert_eq!(a.signature, b.signature);
443 }
444
445 #[test]
446 fn verify_accepts_a_good_token_and_rejects_tampering() {
447 let issuer = TokenIssuer::new(b"secret".to_vec());
448 let token = issuer.mint_at("p", &caps(&["mavlink.read"]), 600, 1000, "sess");
449 assert!(issuer.verify(&token, 1500).is_ok());
450
451 let mut tampered = token.clone();
453 tampered.granted_caps.insert("mavlink.write".into());
454 assert_eq!(
455 issuer.verify(&tampered, 1500),
456 Err(TokenError::HmacMismatch)
457 );
458
459 let other = TokenIssuer::new(b"different".to_vec());
461 assert_eq!(other.verify(&token, 1500), Err(TokenError::HmacMismatch));
462 }
463
464 #[test]
465 fn verify_rejects_expired_token() {
466 let issuer = TokenIssuer::new(b"secret".to_vec());
467 let token = issuer.mint_at("p", &caps(&["x"]), 600, 1000, "sess");
468 assert_eq!(issuer.verify(&token, 1600), Err(TokenError::Expired));
470 assert!(issuer.verify(&token, 1599).is_ok());
471 }
472
473 #[test]
474 fn empty_caps_token_round_trips() {
475 let issuer = TokenIssuer::new(b"k".to_vec());
476 let token = issuer.mint_at("p", &BTreeSet::new(), 600, 0, "s");
477 let parsed = CapabilityToken::from_token_string(&token.to_token_string()).unwrap();
478 assert!(parsed.granted_caps.is_empty());
479 assert_eq!(parsed, token);
480 }
481}