73 lines
2.9 KiB
Rust
73 lines
2.9 KiB
Rust
//! Sign agent → server HTTP requests with the device's existing Ed25519
|
|
//! keypair (the same one rendezvous uses for `RegisterPk`). Producing the
|
|
//! header pair below for any signed call:
|
|
//!
|
|
//! X-RD-Device-Id: <id>
|
|
//! X-RD-Signature: v1.<unix_ts>.<base64(ed25519_sig)>
|
|
//!
|
|
//! Server verifier: `/Users/sn0/Desktop/rustdesk-server/src/api/device_auth.rs`.
|
|
//!
|
|
//! Signed message format (must match the server byte-for-byte):
|
|
//! "rd-api-v1\n" || METHOD || "\n" || PATH || "\n" || TS || "\n" || sha256(body)
|
|
|
|
use hbb_common::config::Config;
|
|
use hbb_common::sodiumoxide::crypto::{hash::sha256, sign};
|
|
|
|
/// Returns the two HTTP header lines joined by `\n`, ready to hand to
|
|
/// `post_request`'s extended `header` parser. Returns `None` if the local
|
|
/// keypair hasn't been generated yet (very early boot, before rendezvous) —
|
|
/// the caller should fall back to an unsigned request in that case; the
|
|
/// server's TOFU promote will still flip `managed=1` on the next signed
|
|
/// request and any unsigned attempts after that flip will be rejected.
|
|
pub fn build_signed_headers(method: &str, path: &str, body: &[u8]) -> Option<String> {
|
|
let (sk_bytes, _pk_bytes) = Config::get_key_pair();
|
|
if sk_bytes.is_empty() {
|
|
return None;
|
|
}
|
|
let sk = sign::SecretKey::from_slice(&sk_bytes)?;
|
|
let id = Config::get_id();
|
|
if id.is_empty() {
|
|
return None;
|
|
}
|
|
let ts = chrono::Utc::now().timestamp();
|
|
|
|
let body_sha = sha256::hash(body);
|
|
let ts_s = ts.to_string();
|
|
let mut msg = Vec::with_capacity(64 + method.len() + path.len());
|
|
msg.extend_from_slice(b"rd-api-v1\n");
|
|
msg.extend_from_slice(method.as_bytes());
|
|
msg.push(b'\n');
|
|
msg.extend_from_slice(path.as_bytes());
|
|
msg.push(b'\n');
|
|
msg.extend_from_slice(ts_s.as_bytes());
|
|
msg.push(b'\n');
|
|
msg.extend_from_slice(body_sha.as_ref());
|
|
|
|
let sig = sign::sign_detached(&msg, &sk);
|
|
let sig_b64 = crate::encode64(sig.as_ref());
|
|
|
|
Some(format!(
|
|
"X-RD-Device-Id: {}\nX-RD-Signature: v1.{}.{}",
|
|
id, ts, sig_b64
|
|
))
|
|
}
|
|
|
|
/// Extract the `/path` portion of a full URL. Used to derive the signed
|
|
/// path from sync.rs's `url` variable, which is always something like
|
|
/// `https://server.example.com:21114/api/heartbeat`. Falls back to "/" if
|
|
/// the URL doesn't parse — server-side verification will then fail, which
|
|
/// is the right outcome (a malformed agent URL is a misconfiguration the
|
|
/// operator should see).
|
|
pub fn path_from_url(url: &str) -> String {
|
|
// Manual parse to avoid pulling in the `url` crate for one call. The
|
|
// structure is always scheme://host[:port]/path[?query]. Strip scheme,
|
|
// then take from the first '/' onward, then drop any '?query'.
|
|
let no_scheme = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
|
|
let path_and_q = no_scheme.find('/').map(|i| &no_scheme[i..]).unwrap_or("/");
|
|
let path = path_and_q
|
|
.split_once('?')
|
|
.map(|(p, _)| p)
|
|
.unwrap_or(path_and_q);
|
|
path.to_string()
|
|
}
|