open-standards/wai/wai-rs/tests/signer_ids_are_labels.rs
Transaction Science 72e3300752 wai: key discovery, signing authority and module revocation
jwp-receipts §7: a did:web signer_id names a web origin, which publishes
a key log at /.well-known/wai-keys: revisions signed by a root key that
pre-commits to its successor, listing the origin's keys, the authorities
each holds, and whether each is active, retired or revoked. A verifier
that has anchored the origin, pinned or fetched, confirms an accepted
receipt, claim or module grant against the log offline, and reports the
outcome beside the item's own verdict, never in it. Hardware-class
energy, lawful-basis and pre-emption authorities are confirmed only
under a pinned anchor. Revocation refuses every item under a revoked key.

§7.9 authorises decoder and reconstruct modules, and pinned parameter
sets, by SHA-256 digest through signed module grants from the sink's
module origins, revokes them by digest in the key log, and bounds how
long a revoked module keeps running by grant_max_s + status_max_age_s,
under ceilings of 86400 s and 3600 s. Every failure, offline included,
leaves the module not held and dispatch falls back.

wai-rs gains signer_id (ungated) and key_log (feature key_discovery),
the wai_signer_vectors and wai-keys binaries, and the signer-conformance
corpus with an independent Python verifier. The reference meters and
the world demo sign under labels. A section-reference guard checks that
every section SPEC.md or an extension cites in another of them exists.

This commit is the stream as merged onto link 14. The commits after it
reconcile it with the parameter-set pin and the energy reports.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 07:12:20 -04:00

160 lines
6.8 KiB
Rust

//! No signer id written in the WAI tree names a real web origin (jwp-receipts §7.1, §7.8).
//!
//! An origin-bound `signer_id` — `did:web:<host>[%3A<port>]#<key-id>` — says that the
//! origin's key log lists the signer's key. Every key the tree signs with comes from a
//! seed published in it, and a published key attests nothing, so no id the tree writes may
//! name an origin a verifier could anchor, nor a domain name at all, even in a spelling
//! that is a label today. This test reads every tracked `.rs`, `.json` and `.js` file
//! under `wai/` and checks each `did:web:` and `did:key:` literal: a `did:web` literal
//! whose host is a domain name, with or without a fragment, must be under a reserved
//! example name (`.example`, `.test`, `.invalid`, RFC 2606), which no one can publish a
//! key log for; an origin-bound one must be too; anything else is a label or an Ed25519
//! `did:key`, which names one key and no origin. `signer-conformance/`, whose vectors are
//! about such ids, is skipped.
//!
//! The files are listed with `git ls-files`, so an untracked scratch file does not fail
//! it. Where git cannot list them — not installed, or refusing a checkout another user
//! owns — the test walks the directory instead, leaving out build output and dependencies.
use std::path::{Path, PathBuf};
use std::process::Command;
use wai::signer_id::{classify, SignerIdForm};
const RESERVED: &[&str] = &[".example", ".test", ".invalid"];
const EXTENSIONS: &[&str] = &["rs", "json", "js"];
fn tracked(wai: &Path) -> Vec<PathBuf> {
let listed = Command::new("git")
.arg("-C")
.arg(wai)
.args(["ls-files", "-z", "--", "*.rs", "*.json", "*.js"])
.output()
.ok()
.filter(|o| o.status.success() && !o.stdout.is_empty());
match listed {
Some(out) => out
.stdout
.split(|b| *b == 0)
.filter(|p| !p.is_empty())
.map(|p| wai.join(String::from_utf8_lossy(p).as_ref()))
.collect(),
None => {
eprintln!("git cannot list {}; walking the directory instead", wai.display());
let mut out = Vec::new();
walk(wai, &mut out);
out
}
}
}
/// Every file with one of [`EXTENSIONS`] under `dir`, leaving out hidden directories,
/// build output and dependencies.
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else { return };
for e in entries.flatten() {
let p = e.path();
let name = e.file_name().to_string_lossy().into_owned();
if p.is_dir() {
if !name.starts_with('.') && !matches!(name.as_str(), "target" | "node_modules" | "pkg" | "dist") {
walk(&p, out);
}
} else if p.extension().and_then(|x| x.to_str()).is_some_and(|x| EXTENSIONS.contains(&x)) {
out.push(p);
}
}
}
/// The host a `did:web:` literal names, where it is a domain name — dot-separated labels
/// of letters, digits and `-`, the last not all digits — or `None`.
fn domain_of(lit: &str) -> Option<String> {
let rest = lit.strip_prefix("did:web:")?;
let host = rest.split(['#', '%', ':', '/']).next().unwrap_or("").to_ascii_lowercase();
let labels: Vec<&str> = host.split('.').collect();
let domain = labels.len() >= 2
&& labels.iter().all(|l| !l.is_empty() && l.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-'))
&& !labels.last().is_some_and(|l| l.bytes().all(|b| b.is_ascii_digit()));
domain.then_some(host)
}
fn reserved(host: &str) -> bool {
RESERVED.iter().any(|t| host.ends_with(t))
}
/// Every `did:web:…` and `did:key:…` run of identifier characters in `text`.
fn literals(text: &str) -> Vec<String> {
let ok = |c: char| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | ':' | '%' | '#');
let mut out = Vec::new();
for start in ["did:web:", "did:key:"] {
let mut from = 0;
while let Some(i) = text[from..].find(start) {
let at = from + i;
let end = text[at..].find(|c: char| !ok(c)).map_or(text.len(), |e| at + e);
out.push(text[at..end].trim_end_matches(['.', ':']).to_owned());
from = end.max(at + start.len());
}
}
out
}
#[test]
fn every_signer_id_in_the_tree_is_a_label() {
let wai = Path::new(env!("CARGO_MANIFEST_DIR")).parent().expect("wai-rs sits in wai/").to_path_buf();
let files = tracked(&wai);
assert!(files.len() > 50, "too few files listed under {}: {}", wai.display(), files.len());
let mut bad = Vec::new();
let mut seen = 0usize;
for f in files {
let rel = f.strip_prefix(&wai).unwrap_or(&f).to_string_lossy().replace('\\', "/");
if rel.starts_with("signer-conformance/") {
continue;
}
let Ok(text) = std::fs::read_to_string(&f) else { continue };
for lit in literals(&text) {
seen += 1;
if let Some(host) = domain_of(&lit)
&& !reserved(&host)
{
bad.push(format!("{rel}: {lit} names the domain {host}"));
continue;
}
if let SignerIdForm::OriginBound { origin, .. } = classify(&lit) {
let host = origin.trim_start_matches("https://").split(':').next().unwrap_or("");
if !reserved(host) {
bad.push(format!("{rel}: {lit} names the origin {origin}"));
}
}
}
}
assert!(seen > 0, "no did: literal found: the scan read nothing");
assert!(bad.is_empty(), "signer ids naming a domain outside reserved names:\n{}", bad.join("\n"));
}
/// A `did:web` literal naming a domain is caught with or without a fragment; one under a
/// reserved name, or with a host that is not a domain name, is not. The caught cases are
/// spelled in two pieces, so this file does not hold what the scan above refuses, and
/// their top-level label is not a delegated one.
#[test]
fn a_domain_is_caught_whatever_the_spelling() {
let w = |rest: &str| format!("{}{rest}", concat!("did:", "web:"));
for (lit, caught) in [
(w("wai.signer.zz"), true),
(w("Wai.Signer.zz#k"), true),
(w("wai.signer.zz%3A8443#k"), true),
(w("wai.signer.zz:users:a"), true),
(w("publisher.example"), false),
(w("publisher.example%3A8443#k"), false),
(w("wai#prov"), false),
(w("meter"), false),
(w("127.0.0.1#d1"), false),
] {
assert_eq!(domain_of(&lit).is_some_and(|h| !reserved(&h)), caught, "{lit}");
}
}
#[test]
fn the_scan_finds_what_it_must() {
let found = literals(r#"x "did:web:wai.example#m", `did:key:wai-${k}` did:web:a.example%3A8443#k1. did:key:z6Mk"#);
assert_eq!(found, vec!["did:web:wai.example#m", "did:web:a.example%3A8443#k1", "did:key:wai-", "did:key:z6Mk"]);
assert!(matches!(classify(&found[0]), SignerIdForm::OriginBound { .. }));
}