§7.4 registers wai-mark:tent64 by its definition and digest and keeps wai-mark:conformance as the scheme with no algorithm. §7.5 is the scheme: frames and luma, the grid and the tent pattern, the stream, the shuffle, the signs and the mask, the shortened BCH code, the informed embedding, the detector in eight orientations, what it guarantees (no other channel, a PSNR floor, a false-positive bound derived and checked), the battery and the table of what it survives, with the claims it makes. §7.6 proves a marking by recomputation and reproduces a detection; §7.7 gives the signpost in the content and in the C2PA statement, and a well-known resolver; §7.8 has a sink that generates mark as part of the reconstruction it is held to. _wai_mark.py is the scheme and the battery in Python from the text; synthesis_verify.py runs §7.6's checks with it; synthesis_mark_verify.py verifies the scheme's corpus, mutates it and weakens itself in turn. Co-Authored-By: Claude Opus 5.5 <[email protected]>
136 lines
6.5 KiB
Rust
136 lines
6.5 KiB
Rust
//! The registered watermark scheme of extensions/synthesis.md §7.5 and its corpus
|
||
//! `live-conformance/synthesis-mark/`: every file regenerates byte for byte from its fixed seeds
|
||
//! (the measurement among them) and no other file is there; every embedding, detection and case
|
||
//! gets what it states; the extension's definition, generator polynomial and shuffle are the
|
||
//! reference's; and §7.5.8 claims only attacks the measurement shows survived, with §7.5.6's
|
||
//! figures the measurement's. Run it in release: the measurement embeds and attacks every
|
||
//! picture of the measurement sets.
|
||
#![cfg(feature = "synthesis_mark")]
|
||
|
||
use serde_json::Value;
|
||
use std::collections::BTreeMap;
|
||
use std::path::PathBuf;
|
||
use wai::synthesis::mark;
|
||
use wai::synthesis_mark::corpus;
|
||
|
||
fn wai_dir() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
||
}
|
||
|
||
fn committed() -> BTreeMap<String, Vec<u8>> {
|
||
let root = wai_dir().join("live-conformance/synthesis-mark");
|
||
let mut out = BTreeMap::new();
|
||
let mut stack = vec![root.clone()];
|
||
while let Some(d) = stack.pop() {
|
||
for e in std::fs::read_dir(&d).expect("the committed corpus").flatten() {
|
||
let p = e.path();
|
||
if p.is_dir() {
|
||
stack.push(p);
|
||
continue;
|
||
}
|
||
let rel = p.strip_prefix(&root).expect("under the corpus").to_string_lossy().replace('\\', "/");
|
||
if rel != "README.md" {
|
||
out.insert(rel, std::fs::read(&p).expect("a file"));
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn extension() -> String {
|
||
std::fs::read_to_string(wai_dir().join("extensions/synthesis.md")).expect("the extension")
|
||
}
|
||
|
||
/// The text of the section that starts at `head`, up to the next heading of its level or above.
|
||
fn section(text: &str, head: &str) -> String {
|
||
let at = text.find(&format!("\n{head}")).unwrap_or_else(|| panic!("a section {head}"));
|
||
let level = head.chars().take_while(|c| *c == '#').count();
|
||
let end = text[at + 1..]
|
||
.match_indices("\n#")
|
||
.find(|(i, _)| text[at + 1 + i + 1..].chars().take_while(|c| *c == '#').count() <= level)
|
||
.map_or(text.len(), |(i, _)| at + 1 + i);
|
||
text[at..end].to_string()
|
||
}
|
||
|
||
fn fold(s: &str) -> String {
|
||
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
||
}
|
||
|
||
#[test]
|
||
fn the_committed_corpus_regenerates_from_its_seeds() {
|
||
let files = committed();
|
||
let generated: BTreeMap<String, Vec<u8>> = corpus::generate().into_iter().collect();
|
||
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
|
||
let differ: Vec<&String> = generated.keys().filter(|k| files.get(*k) != generated.get(*k)).collect();
|
||
assert!(extra.is_empty() && differ.is_empty(), "extra {extra:?}, differ or missing {differ:?}: run `wai_synthesis_mark_vectors gen`");
|
||
}
|
||
|
||
#[test]
|
||
fn every_vector_and_case_gets_what_it_states() {
|
||
let fails = corpus::verify(&committed());
|
||
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
||
}
|
||
|
||
/// §7.4's definition is the reference's, with its digest; §7.5.3's generator polynomial and
|
||
/// the shuffle's first entries are the reference's.
|
||
#[test]
|
||
fn the_extension_pins_the_scheme() {
|
||
let text = extension();
|
||
let s74 = section(&text, "### 7.4 ");
|
||
let block = s74.split("```").nth(1).expect("§7.4's definition");
|
||
let def: String = block.lines().map(str::trim).collect();
|
||
assert_eq!(def, mark::definition_text(), "§7.4's definition");
|
||
let digest: String = mark::digest().iter().map(|b| format!("{b:02x}")).collect();
|
||
assert!(fold(&s74).contains(&format!("its digest is `{digest}`")), "§7.4's digest");
|
||
let s753 = fold(§ion(&text, "#### 7.5.3 "));
|
||
assert!(s753.contains(&format!("in hexadecimal, are `{}`", mark::GENERATOR_HEX)), "§7.5.3's generator");
|
||
let p = &mark::layout().perm;
|
||
assert!(s753.contains(&format!("π begins {}, {}, {}, {},", p[0], p[1], p[2], p[3])), "§7.5.3's shuffle");
|
||
}
|
||
|
||
/// §7.5.8 claims only attacks every set survived with no unmarked reading; §7.5.6's figures are
|
||
/// battery.json's.
|
||
#[test]
|
||
fn the_extension_claims_only_what_the_measurement_shows() {
|
||
let text = extension();
|
||
let battery: Value = serde_json::from_slice(&committed()["battery.json"]).expect("battery.json");
|
||
let s758 = fold(§ion(&text, "#### 7.5.8 "));
|
||
let at = s758.find("For pictures, these attacks, each with the parameter measured: ").expect("§7.5.8's claims");
|
||
let rest = &s758[at..];
|
||
let clips_at = rest.find(" For clips: ").expect("the clips' claims");
|
||
let names = |s: &str| -> Vec<String> { s.split('`').skip(1).step_by(2).map(String::from).collect() };
|
||
let pictures = names(&rest[..clips_at]);
|
||
let clips = names(&rest[clips_at..rest[clips_at..].find(". ").map_or(rest.len(), |e| clips_at + e)]);
|
||
let has = |kind: &str, a: &String| battery["claims"][kind].as_array().expect("claims").iter().any(|x| x == a);
|
||
assert!(!pictures.is_empty() && pictures.iter().all(|a| has("pictures", a)), "§7.5.8 claims {pictures:?}");
|
||
assert!(!clips.is_empty() && clips.iter().all(|a| has("clips", a)), "§7.5.8 claims {clips:?}");
|
||
let w = &battery["words"];
|
||
let s756 = fold(§ion(&text, "#### 7.5.6 "));
|
||
let fp = battery["false_positive_log2"].as_str().expect("a bound").trim_start_matches('-').to_string();
|
||
assert!(s756.contains(&format!("2^−{fp}, about")), "§7.5.6's bound");
|
||
let pairs = w["pairs"].as_u64().expect("pairs");
|
||
let words = format!(
|
||
"have a mean of {} ones with a standard deviation of {}, and the {},{:03} pairs of those words from different pictures differ in a mean of {} bits with a standard deviation of {} and in no fewer than {};",
|
||
w["ones_mean"].as_str().unwrap(),
|
||
w["ones_sd"].as_str().unwrap(),
|
||
pairs / 1000,
|
||
pairs % 1000,
|
||
w["distance_mean"].as_str().unwrap(),
|
||
w["distance_sd"].as_str().unwrap(),
|
||
w["distance_min"]
|
||
);
|
||
assert!(s756.contains(&words), "§7.5.6's figures: {words}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_corpus_holds_only_what_its_vectors_name() {
|
||
let files = committed();
|
||
let doc: Value = serde_json::from_slice(&files["vectors.json"]).expect("vectors.json");
|
||
let text = String::from_utf8_lossy(&files["vectors.json"]).to_string();
|
||
for k in files.keys().filter(|k| k.starts_with("frames/")) {
|
||
let h = &k["frames/".len()..k.len() - ".rgb".len()];
|
||
assert!(text.contains(h), "{k} is named by no vector");
|
||
assert_eq!(&mark::sha256(&files[k]).iter().map(|b| format!("{b:02x}")).collect::<String>(), h, "{k} is not named by its SHA-256");
|
||
}
|
||
assert!(doc["cases"].as_array().is_some_and(|c| !c.is_empty()));
|
||
}
|