Files
VerseVDI-Protocol/tools/native_conformance.rs
T
sechmachine afcd5d99db
Verify Protocol / module (push) Successful in 1m13s
Verify Protocol / verify (push) Successful in 35s
feat(protocol): define native session credentials
2026-08-11 18:38:19 +07:00

347 lines
14 KiB
Rust

use std::fs;
use std::path::PathBuf;
fn values(input: &str) -> std::collections::BTreeMap<String, String> {
input
.split(';')
.filter_map(|item| item.split_once('='))
.map(|(key, value)| (key.to_owned(), value.to_owned()))
.collect()
}
fn evaluate(version: &str, kind: &str, input: &str) -> &'static str {
let values = values(input);
match kind {
"version" if matches!(input, "2" | "1" | "0") => "valid",
"version" => "invalid:unsupported_version",
"page" => match values.get("limit").and_then(|value| value.parse::<i64>().ok()) {
Some(limit) if (1..=100).contains(&limit) => "valid",
_ => "invalid:invalid_limit",
},
"manifest" if ["provider_url", "vm_address", "password", "private_key"]
.iter()
.any(|key| values.contains_key(*key)) => "invalid:forbidden_field",
"manifest"
if values.get("version").map(String::as_str) == Some("1")
&& values.contains_key("gateway_id")
&& values.contains_key("public_identity")
&& values.get("grant").map_or(false, |value| value.len() >= 43)
&& values.get("purpose").map(String::as_str) == Some("launch") => "valid",
"manifest" => "invalid:invalid_manifest",
"clipboard" if values.get("encoding").map(String::as_str) == Some("utf-8")
&& !values.contains_key("file") => "valid",
"clipboard" => "invalid:unsupported_clipboard",
"session_request" if version != "2" => "invalid:unsupported_version",
"session_request" if values.contains_key("policy_snapshot") => "invalid:forbidden_field",
"session_request" => match SessionRequest::new(
values.get("client_device_id").cloned().unwrap_or_default(),
values.get("device_key_id").cloned().unwrap_or_default(),
values.get("pool_id").cloned().unwrap_or_default(),
values.get("idempotency_key").cloned().unwrap_or_default(),
None,
) {
Ok(_) => "valid",
Err(_) => "invalid:required",
},
"browser_authenticated_session" if values.contains_key("client_device_id") || values.contains_key("device_key_id") => "invalid:forbidden_field",
"browser_authenticated_session" => match BrowserAuthenticatedSession::new(
values.get("username").cloned().unwrap_or_default(),
values.get("provider").cloned().unwrap_or_default(),
vec![values.get("roles").cloned().unwrap_or_default()],
values.get("role").cloned().unwrap_or_default(),
) {
Ok(_) => "valid",
Err(_) => "invalid:invalid_session",
},
"native_authenticated_session" if !values.contains_key("client_device_id") || !values.contains_key("device_key_id") => "invalid:required",
"native_authenticated_session" => {
let identity = match NativeSessionIdentity::new(values["client_device_id"].clone(), values["device_key_id"].clone()) {
Ok(identity) => identity,
Err(_) => return "invalid:required",
};
match NativeAuthenticatedSession::new(
values.get("username").cloned().unwrap_or_default(),
values.get("provider").cloned().unwrap_or_default(),
vec![values.get("roles").cloned().unwrap_or_default()],
values.get("role").cloned().unwrap_or_default(),
identity,
) {
Ok(_) => "valid",
Err(_) => "invalid:invalid_session",
}
}
"native_tunnel_credential" => match NativeTunnelCredential::new(
values.get("client_device_id").cloned().unwrap_or_default(),
values.get("device_key_id").cloned().unwrap_or_default(),
values.get("certificate_chain_pem").cloned().unwrap_or_default(),
values.get("trust_bundle_pem").cloned().unwrap_or_default(),
values.get("expires_at").cloned().unwrap_or_default(),
) {
Ok(_) => "valid",
Err(_) => "invalid:invalid_credential",
},
"event" if values.get("version").map(String::as_str) != Some("1") => {
"invalid:unsupported_version"
}
"event" if values.get("after").and_then(|value| value.parse::<i64>().ok()).is_some()
&& values.get("earliest").and_then(|value| value.parse::<i64>().ok()).is_some()
&& values["after"].parse::<i64>().unwrap() > 0
&& values["earliest"].parse::<i64>().unwrap() > 0
&& values["after"].parse::<i64>().unwrap() < values["earliest"].parse::<i64>().unwrap() - 1 => {
"invalid:gap"
}
"event" if values.get("payload_bytes").and_then(|value| value.parse::<usize>().ok()).map_or(true, |size| size > 16384) => {
"invalid:payload_limit"
}
"event" if values.get("sequence").and_then(|value| value.parse::<i64>().ok()).map_or(true, |sequence| sequence < 1)
|| !values.contains_key("correlation_id") => "invalid:required",
"event" => "valid",
"tunnel" if matches!(values.get("offered").map(String::as_str), Some("2") | Some("1") | Some("0"))
&& matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("control.v2") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => "valid",
"tunnel" if !matches!(values.get("feature").map(String::as_str), Some("control.v1") | Some("control.v2") | Some("display.request.v1") | Some("input.absolute.v1") | Some("input.scroll.v1")) => {
"invalid:unsupported_feature"
}
"tunnel" => "invalid:unsupported_version",
"datagram" => classify_datagram(values.get("hex").map(String::as_str).unwrap_or_default()),
"gateway_input" => classify_gateway_input(values.get("hex").map(String::as_str).unwrap_or_default()),
"gateway_feedback" => classify_gateway_feedback(values.get("hex").map(String::as_str).unwrap_or_default()),
"gateway_clipboard" if values.contains_key("file") => "invalid:forbidden",
"gateway_clipboard" => match (
values.get("direction"),
values.get("text"),
values.get("encoding"),
values.get("loop_token"),
) {
(Some(direction), Some(text), Some(encoding), Some(token))
if GatewayClipboardText::new(
direction.clone(), text.clone(), encoding.clone(), token.clone(),
).is_ok() => "valid",
_ => "invalid:clipboard",
},
"gateway_clipboard_audit" if values.contains_key("text") => "invalid:forbidden",
"gateway_clipboard_audit"
if matches!(values.get("direction").map(String::as_str), Some("client_to_provider") | Some("provider_to_client"))
&& matches!(values.get("outcome").map(String::as_str), Some("forwarded") | Some("suppressed") | Some("rejected"))
&& matches!(values.get("reason").map(String::as_str), Some("forwarded") | Some("loop") | Some("policy") | Some("rate") | Some("provider") | Some("malformed"))
&& values.get("text_bytes").and_then(|value| value.parse::<usize>().ok()).map_or(false, |size| size <= 65536) => "valid",
"gateway_clipboard_audit" => "invalid:clipboard_audit",
_ => "invalid:unknown_kind",
}
}
fn classify_gateway_input(encoded: &str) -> &'static str {
let raw = match decode_hex(encoded) {
Some(raw) => raw,
None => return "invalid:hex",
};
if raw.len() < 6 {
return "invalid:truncated";
}
if raw[0..4] != *b"VGI1" {
return "invalid:magic";
}
let kind = raw[4];
let body = &raw[6..];
if body.len() != raw[5] as usize {
return "invalid:length";
}
match kind {
1 if body.len() == 4 && body[0] <= 1 && (body[2] != 0 || body[3] != 0) => "valid",
1 => "invalid:field",
2 if body.len() != 3 => "invalid:length",
2 if body[0] > 1 || !(1..=5).contains(&body[1]) => "invalid:field",
2 if body[2] != 0 => "invalid:reserved",
2 => "valid",
3 if body.len() == 4 => "valid",
3 => "invalid:length",
4 if (1..=4).contains(&body.len()) && std::str::from_utf8(body).ok().map_or(false, |value| value.chars().count() == 1) => "valid",
4 => "invalid:utf8",
5 if body.len() != 17 => "invalid:length",
5 if body[0] > 15 => "invalid:field",
5 if body[1] == 0 && body[2] == 0 && body[3..].iter().any(|value| *value != 0) => "invalid:field",
5 => "valid",
6 if body.len() != 8 => "invalid:length",
6 => {
let x = u16::from_be_bytes([body[0], body[1]]);
let y = u16::from_be_bytes([body[2], body[3]]);
let width = u16::from_be_bytes([body[4], body[5]]);
let height = u16::from_be_bytes([body[6], body[7]]);
if width != 0 && height != 0 && x < width && y < height { "valid" } else { "invalid:field" }
}
7 if body.len() == 4 => "valid",
7 => "invalid:length",
_ => "invalid:kind",
}
}
fn classify_gateway_feedback(encoded: &str) -> &'static str {
let raw = match decode_hex(encoded) {
Some(raw) => raw,
None => return "invalid:hex",
};
if raw.len() < 8 {
return "invalid:truncated";
}
if raw[0..4] != *b"VGF1" {
return "invalid:magic";
}
let direction = raw[4];
let kind = raw[5];
let body = &raw[8..];
if body.len() != ((raw[6] as usize) << 8 | raw[7] as usize) {
return "invalid:length";
}
if direction > 1 {
return "invalid:direction";
}
if direction == 0 {
if (0x10..=0x12).contains(&kind) {
return "invalid:direction";
}
return match kind {
1 if body.is_empty() => "valid",
1 => "invalid:length",
2 if valid_fec_status(body) => "valid",
2 => "invalid:field",
3 if body.is_empty() => "valid",
3 => "invalid:length",
_ => "invalid:type",
};
}
if kind == 1 || kind == 2 || kind == 3 {
return "invalid:direction";
}
match kind {
0x10 if body.len() == 4 => "valid",
0x10 => "invalid:length",
0x11 if body.len() != 5 => "invalid:length",
0x11 if body[0] <= 15 => "valid",
0x11 => "invalid:field",
0x12 if body.len() != 1 => "invalid:length",
0x12 if body[0] <= 1 => "valid",
0x12 => "invalid:field",
_ => "invalid:type",
}
}
fn valid_fec_status(body: &[u8]) -> bool {
body.len() == 21
&& ((body[10] as u16) << 8 | body[11] as u16) > 0
&& ((body[14] as u16) << 8 | body[15] as u16) <= ((body[10] as u16) << 8 | body[11] as u16)
&& ((body[16] as u16) << 8 | body[17] as u16) <= ((body[12] as u16) << 8 | body[13] as u16)
&& body[18] <= 100
&& body[20] > 0
&& body[19] < body[20]
}
fn decode_hex(input: &str) -> Option<Vec<u8>> {
if input.len() % 2 != 0 {
return None;
}
(0..input.len())
.step_by(2)
.map(|index| u8::from_str_radix(&input[index..index + 2], 16).ok())
.collect()
}
fn classify_datagram(encoded: &str) -> &'static str {
let raw = match decode_hex(encoded) {
Some(raw) => raw,
None => return "invalid:hex",
};
if raw.len() < 3 {
return "invalid:truncated";
}
if raw[0..2] != *b"VD" {
return "invalid:magic";
}
if raw[2] != 1 && raw[2] != 2 {
return "invalid:unsupported_version";
}
let header_bytes = if raw[2] == 1 { 21 } else { 23 };
if raw.len() < header_bytes {
return "invalid:truncated";
}
let limit = match (raw[2], raw[3]) {
(1, 1) => 1024,
(1, 2) => 2048,
(1, 3) => 65515,
(1, 10 | 11 | 12) => 1179,
(2, 10 | 11) => 1177,
_ => return "invalid:unknown_channel",
};
if raw[4] != 0 {
return "invalid:flags";
}
let (fragment_index, fragment_count, payload_offset) = if raw[2] == 1 {
(raw[17] as usize, raw[18] as usize, 19)
} else {
let count = ((raw[19] as usize) << 8) | raw[20] as usize;
if count > 891 {
return "invalid:fragment_limit";
}
((((raw[17] as usize) << 8) | raw[18] as usize), count, 21)
};
if fragment_count == 0 || fragment_index >= fragment_count {
return "invalid:fragment";
}
let payload_length = ((raw[payload_offset] as usize) << 8) | raw[payload_offset + 1] as usize;
if payload_length > limit {
return "invalid:payload_limit";
}
if raw.len() != header_bytes + payload_length {
return "invalid:length_mismatch";
}
if raw[2] == 1 && raw.len() > 65536 || raw[2] == 2 && raw.len() > 1200 {
return "invalid:frame_limit";
}
"valid"
}
fn normalized_digest(results: &[String]) -> String {
let mut value: u64 = 14695981039346656037;
for result in results {
for byte in format!("{result}\n").bytes() {
value ^= u64::from(byte);
value = value.wrapping_mul(1099511628211);
}
}
format!("{value:016x}")
}
fn fixture_hash() -> String {
let text = fs::read_to_string("fixtures/manifest.json").expect("fixture manifest");
text.split("\"corpus_sha256\": \"")
.nth(1)
.and_then(|value| value.split('"').next())
.expect("fixture hash")
.to_owned()
}
fn main() {
let mut paths: Vec<PathBuf> = fs::read_dir("fixtures/conformance")
.expect("fixture corpus")
.map(|entry| entry.expect("fixture entry").path())
.filter(|path| path.extension().and_then(|value| value.to_str()) == Some("tsv"))
.collect();
paths.sort();
let mut results = Vec::new();
for path in paths {
let text = fs::read_to_string(path).expect("fixture file");
let mut lines = text.lines();
assert_eq!(lines.next(), Some("id\tversion\tkind\tinput\texpected"));
for line in lines {
let fields: Vec<&str> = line.split('\t').collect();
assert_eq!(fields.len(), 5);
let actual = evaluate(fields[1], fields[2], fields[3]);
assert_eq!(actual, fields[4], "{}", fields[0]);
results.push(format!("{}\t{}", fields[0], actual));
}
}
println!(
"Rust conformance passed normalized={} fixtures={}",
normalized_digest(&results),
fixture_hash()
);
}