A host states a sealing key (an X25519 public key) as its screen statement's last field. A controller seals every key, text and clipboard action to it: a sealed action (type 8) keeps in the clear the type it seals, its dt_us, a clipboard text's length and a salted commitment binding its fields, its order and its author; the statement ends with one HPKE seal (RFC 9180 base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM) of the actions' openings. Pointer, wheel and touch actions stay in the clear. §5.3 gains rule 8 (input_unsealed) and rule 9 (input_bad_seal), §12 a fourth table (seal_no_key, seal_not_opened, seal_unreadable, seal_commitment), §11.4 the two verification levels. wai-rs: Input::seal, Input::open, commitment, decide_opened, verify_opened; HPKE moves to a crate module behind its own feature (hpke), which telemetry_aggregate and remote_desktop both use. The corpus keeps its 42 vectors byte for byte and appends ten sealed ones, each stating the opened level; remote_desktop_verify.py checks both levels. Co-Authored-By: Claude Opus 5.5 <[email protected]>
85 lines
4.3 KiB
Rust
85 lines
4.3 KiB
Rust
//! `wai.live.remote` (extensions/remote-desktop.md): the committed corpus `live-conformance/remote/`
|
|
//! regenerates byte for byte from its fixed seeds, holds no other file, every case in it gets the
|
|
//! outcome it states at the commitment level and at the opened level, and the extension's §12
|
|
//! lists exactly the reasons the reference reports: the verification codes in its first table,
|
|
//! the host's refusals, with their numbers, in its second, the reasons a floor statement is
|
|
//! refused in its third, and the reasons a seal is not opened in its fourth. The generator itself
|
|
//! asserts that the corpus gives every one of them.
|
|
#![cfg(feature = "remote_desktop")]
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
use wai::remote_desktop::{corpus, FloorRefusal, Reason, Refusal, SealFault};
|
|
|
|
fn wai_dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
}
|
|
|
|
fn committed() -> BTreeMap<String, String> {
|
|
let dir = wai_dir().join("live-conformance/remote");
|
|
std::fs::read_dir(&dir)
|
|
.expect("the committed corpus")
|
|
.flatten()
|
|
.map(|e| (e.file_name().to_string_lossy().into_owned(), e.path()))
|
|
.filter(|(n, _)| n != "README.md")
|
|
.map(|(n, p)| (n, std::fs::read_to_string(p).expect("a text file")))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn the_committed_corpus_regenerates_from_its_seeds() {
|
|
let files = committed();
|
|
let generated: BTreeMap<String, String> = corpus::generate().into_iter().collect();
|
|
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
|
|
let missing: Vec<&String> = generated.keys().filter(|k| !files.contains_key(*k)).collect();
|
|
let differ: Vec<&String> = generated.iter().filter(|(k, v)| files.get(*k).is_some_and(|f| f != *v)).map(|(k, _)| k).collect();
|
|
assert!(extra.is_empty() && missing.is_empty() && differ.is_empty(), "extra {extra:?}, missing {missing:?}, differ {differ:?}: run `wai_remote_desktop_vectors gen`");
|
|
}
|
|
|
|
#[test]
|
|
fn every_case_gets_its_stated_outcome() {
|
|
let fails = corpus::verify(&committed());
|
|
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
|
}
|
|
|
|
/// §12 of the extension: its first table lists exactly the codes `Reason::ALL` has, each once, in
|
|
/// order; its second, exactly `Refusal::ALL`'s, each with its number; its third, exactly
|
|
/// `FloorRefusal::ALL`'s; its fourth, exactly `SealFault::ALL`'s.
|
|
#[test]
|
|
fn the_extension_lists_the_reasons() {
|
|
let text = std::fs::read_to_string(wai_dir().join("extensions/remote-desktop.md")).expect("the extension");
|
|
let at = text.find("\n## 12. Reasons").expect("a §12");
|
|
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
|
|
let section = &text[at..end];
|
|
// The tables, in order: each a run of lines that begin with `|`.
|
|
let mut tables: Vec<Vec<&str>> = Vec::new();
|
|
let mut cur: Vec<&str> = Vec::new();
|
|
for l in section.lines() {
|
|
if l.starts_with('|') {
|
|
cur.push(l);
|
|
} else if !cur.is_empty() {
|
|
tables.push(std::mem::take(&mut cur));
|
|
}
|
|
}
|
|
if !cur.is_empty() {
|
|
tables.push(cur);
|
|
}
|
|
assert_eq!(tables.len(), 4, "§12 has four tables");
|
|
let codes = |t: &[&str]| t.iter().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect::<Vec<_>>();
|
|
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
|
|
assert_eq!(codes(&tables[0]), want, "§12's first table and Reason::ALL differ");
|
|
let want: Vec<String> = FloorRefusal::ALL.iter().map(|r| r.code().to_string()).collect();
|
|
assert_eq!(codes(&tables[2]), want, "§12's third table and FloorRefusal::ALL differ");
|
|
let want: Vec<String> = SealFault::ALL.iter().map(|r| r.code().to_string()).collect();
|
|
assert_eq!(codes(&tables[3]), want, "§12's fourth table and SealFault::ALL differ");
|
|
let numbered: Vec<(u8, String)> = tables[1]
|
|
.iter()
|
|
.filter_map(|l| {
|
|
let cells: Vec<&str> = l.split('|').map(str::trim).collect();
|
|
let n: u8 = cells.get(1)?.parse().ok()?;
|
|
Some((n, cells.get(2)?.trim_matches('`').to_string()))
|
|
})
|
|
.collect();
|
|
let want: Vec<(u8, String)> = Refusal::ALL.iter().map(|r| (r.number(), r.code().to_string())).collect();
|
|
assert_eq!(numbered, want, "§12's second table and Refusal::ALL differ");
|
|
}
|