Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
69215b1
feat(fingerprint): add bounded stealth-normalization surfaces
seonghobae Aug 27, 2026
9ca8e7f
docs(adr): record bounded stealth-normalization surfaces
seonghobae Aug 27, 2026
7f0368b
docs(doctoring): cite render/media surface entropy for stealth bounds
seonghobae Aug 27, 2026
2814e28
docs(changelog): record bounded stealth-normalization surfaces
seonghobae Aug 27, 2026
f4560c9
test(fingerprint): require explicit WebRTC disclosure naming
seonghobae Aug 27, 2026
e1577e5
fix(fingerprint): make WebRTC disclosure policy explicit
seonghobae Aug 27, 2026
0202e13
docs(adr): align WebRTC policy naming
seonghobae Aug 27, 2026
9b1c44e
test(fingerprint): classify ANGLE software renderers
seonghobae Aug 27, 2026
3855784
fix(fingerprint): prioritize software renderer markers
seonghobae Aug 27, 2026
a0c7924
docs(adr): include ADR 0111 in branch provenance
seonghobae Aug 27, 2026
dc1c209
feat(fingerprint): add bounded UA Client Hints surfaces
seonghobae Aug 27, 2026
f0ba4ed
docs(adr): record bounded User-Agent Client Hints surfaces
seonghobae Aug 27, 2026
6c175b2
docs(doctoring): cite UA Client Hints draft for bounded UA-CH surfaces
seonghobae Aug 27, 2026
16b7b38
feat(fingerprint): bind presentation and UA-CH platform coherence
seonghobae Aug 27, 2026
8a8c673
docs(adr): record cross-surface platform coherence
seonghobae Aug 27, 2026
bcda885
docs(changelog): record cross-surface platform coherence
seonghobae Aug 27, 2026
8920420
test(fingerprint): define Web Audio blocking contract
seonghobae Aug 27, 2026
4eff822
build(privacy): stage reviewed Web Audio materializer
seonghobae Aug 27, 2026
b2aa631
fix(privacy): index ADR 0114 in both canonical indexes
seonghobae Aug 27, 2026
287b337
ci(privacy): retry materialization after ADR index repair
seonghobae Aug 27, 2026
d3f3300
fix(privacy): derive the default Web Audio policy
seonghobae Aug 27, 2026
2ddffd8
ci(privacy): retry after clippy derivation repair
seonghobae Aug 27, 2026
40ebde2
ci(privacy): publish verified non-workflow changes only
seonghobae Aug 27, 2026
d758b83
feat(privacy): enforce default-deny Web Audio policy
github-actions[bot] Aug 27, 2026
5234852
ci(privacy): execute pinned Web Audio blocking evidence
seonghobae Aug 27, 2026
aef6deb
ci(privacy): remove completed one-shot materializer
seonghobae Aug 27, 2026
aa1fb0c
build(privacy): stage exact browser-probe repair
seonghobae Aug 27, 2026
06be499
ci(privacy): verify Chrome alias-aware probe repair
seonghobae Aug 27, 2026
36332fc
ci(privacy): allow pinned Rust toolchain distribution
seonghobae Aug 27, 2026
257f291
ci(privacy): allow locked Cargo dependency endpoints
seonghobae Aug 27, 2026
0e635d5
ci(privacy): exclude browser downloads from repair commit
seonghobae Aug 27, 2026
1326d8a
fix(privacy): accept absent legacy Web Audio aliases
github-actions[bot] Aug 27, 2026
7d82ac3
ci(privacy): remove completed probe repair workflow
seonghobae Aug 27, 2026
5abd124
ci(privacy): use Node 24 artifact uploader
seonghobae Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/mv3-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ on:
paths:
- ".github/workflows/mv3-compatibility.yml"
- "crates/originweave-core/**"
- "crates/originweave-fingerprint/**"
- "extensions/originweave-privacy-guard/**"
- "scripts/ci/run_mv3_compatibility.py"
- "scripts/ci/run_web_audio_privacy.py"
- "tests/fixtures/mv3_basic/**"
- "tests/fixtures/web_audio_privacy/**"
- "tests/test_mv3_compatibility_contract.py"
- "tests/test_web_audio_privacy_contract.py"
- "docs/adr/0114-default-deny-web-audio-fingerprinting.md"
- "docs/doctoring/mv3-compatibility.md"
- "docs/doctoring/web-audio-privacy.md"

permissions:
contents: read
Expand Down Expand Up @@ -74,14 +81,16 @@ jobs:
"$CHROME_BIN" --version
"$CHROMEDRIVER_BIN" --version
python3 scripts/ci/run_mv3_compatibility.py | tee mv3-compatibility.json
python3 scripts/ci/run_web_audio_privacy.py | tee web-audio-privacy.json

- name: Upload bounded compatibility evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mv3-compatibility-${{ github.event.pull_request.head.sha || github.sha }}
path: |
mv3-download-sha256.txt
mv3-compatibility.json
web-audio-privacy.json
if-no-files-found: warn
retention-days: 7
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Refreshed the product-gap queue to 126 open pull requests (54 ready, 72 draft) after #190, #188, #185, #192, #182, #184, #115, #181, #116, #117, #118, #183, #114, #127, #112, #109, #186, #110, #108, #111, #174, and #113 were merged into their immediate stacked prerequisites. PRs #147, #146, #145, #144, #143, #142, #141, #139, #136, #132, #129, and #128 moved to ready after exact-head checks and thread review; these are queue-consolidation results, not protected-main shipment.

### Added
- Added a default-deny Web Audio fingerprinting boundary for isolated Agent and Crawler profiles: exact-origin grants capped at 128 unique canonical origins, a deterministic Rust-rendered MAIN-world `document_start` guard, and a pinned-Chromium top-document/child-frame proof that blocks online, offline, prefixed, and AudioWorklet construction entry points (see ADR 0114).
- Added cross-surface platform coherence to the fingerprint kernel: `PresentationPlatform::hints_platform` is the single source of truth mapping each presentation platform to its canonical UA Client Hints platform, and `require_hints_coherence` fails closed on any contradiction so the presentation-platform, UA-token, and UA-CH-platform triad cannot leak a mismatched identity (see ADR 0113).
- Added bounded User-Agent Client Hints surfaces to the fingerprint kernel: ASCII brand/version validation with a 32-character name bound, enumerated architecture/bitness/platform tokens, a non-empty brand-list requirement, and the spec rule that a non-mobile user agent reports an empty model. Control-plane contract only, grounded in the User-Agent Client Hints draft (WICG, 2026); see ADR 0112.
- Added bounded stealth-normalization surfaces to the fingerprint kernel: enumerated canvas-noise classes, canonicalized WebGL renderer tokens, standard-rate Web Audio normalization, bounded WebRTC interface policy, and a fail-closed Canvas/WebGL/WebAudio/WebRtc surface-admission contract. This is a privacy-preserving control-plane contract with no real-browser or anti-evasion claim (see ADR 0111).
- Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate.
- Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge.

Expand Down Expand Up @@ -99,6 +103,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Corrected the baseline evidence collector to flatten every paginated input, apply current reviewer and last-push approval semantics, and discard verdicts when either the PR head or base moves.

### Security
- Web Audio constructors now fail with a fixed `NotAllowedError` in the managed default profile unless a trusted policy grants the exact canonical origin; the guard has no storage, network, messaging, model, or secret authority and does not affect ordinary media-element playback.

- Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority.
- Raw page content cannot become a trusted instruction.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/originweave-fingerprint/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ homepage.workspace = true
publish = false

[dependencies]
originweave-core = { path = "../originweave-core" }
sha2 = "=0.10.9"

[lints]
Expand Down
47 changes: 47 additions & 0 deletions crates/originweave-fingerprint/src/coherence.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Cross-surface platform-coherence contracts for stealth presentations.
//!
//! The presentation platform, the JavaScript UA token, and the UA Client
//! Hints platform are three surfaces a page can reconcile into one identity.
//! If an adapter presents one platform in the static profile and a different
//! one in `navigator.userAgentData`, the contradiction is itself a
//! reidentification signal. This module binds the hints platform to the
//! presentation platform with a deterministic, fail-closed check. It
//! performs no evasion and never reads the host.

use crate::{PresentationPlatform, UaClientHints};
use std::error::Error;
use std::fmt;

/// A cross-surface coherence failure.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoherenceError {
/// The UA Client Hints platform contradicted the presentation platform.
HintsPlatformMismatch,
}

impl fmt::Display for CoherenceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::HintsPlatformMismatch => formatter
.write_str("UA Client Hints platform contradicts the presentation platform"),
}
}
}

impl Error for CoherenceError {}

/// Require the presented UA Client Hints to agree with the presentation
/// platform.
///
/// The canonical hints token for `presentation` comes from
/// [`PresentationPlatform::hints_platform`]; any other hints platform fails
/// closed so an adapter cannot surface a contradicting identity.
pub fn require_hints_coherence(
hints: &UaClientHints,
presentation: PresentationPlatform,
) -> Result<(), CoherenceError> {
if hints.platform() != presentation.hints_platform() {
return Err(CoherenceError::HintsPlatformMismatch);
}
Ok(())
}
29 changes: 29 additions & 0 deletions crates/originweave-fingerprint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod coherence;
mod stealth;
mod ua_hints;
mod web_audio_guard;

pub use coherence::{CoherenceError, require_hints_coherence};
pub use stealth::{
CanvasNoise, StealthError, StealthSurface, WebAudioRate, WebGlRendererToken, WebRtcInterface,
require_stealth_surfaces,
};
pub use ua_hints::{
ClientHintsError, HintsArchitecture, HintsBitness, HintsPlatform, UaBrand, UaClientHints,
};
pub use web_audio_guard::{WebAudioDecision, WebAudioFingerprintPolicy, WebAudioPolicyError};

use sha2::{Digest, Sha256};
use std::error::Error;
use std::fmt;
Expand Down Expand Up @@ -282,6 +297,20 @@ impl PresentationPlatform {
Self::Linux => "Linux x86_64",
}
}

/// Return the canonical UA Client Hints platform token for this family.
///
/// A page reconciles the presentation platform with
/// `navigator.userAgentData.platform`, so the two must agree; the mapping
/// is the single source of truth an adapter consumes (see ADR 0112).
#[must_use]
pub const fn hints_platform(self) -> HintsPlatform {
match self {
Self::Windows => HintsPlatform::Windows,
Self::MacOS => HintsPlatform::MacOs,
Self::Linux => HintsPlatform::Linux,
}
}
}

/// A lowercase SHA-256 digest identifier bound to one canonical profile.
Expand Down
203 changes: 203 additions & 0 deletions crates/originweave-fingerprint/src/stealth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! Bounded stealth-normalization surfaces for browser presentation.
//!
//! A page can observe rendered and media surfaces that carry more entropy
//! than static profile fields: canvas readback noise, WebGL renderer tokens,
//! Web Audio sample-rate reporting, and WebRTC interface exposure. The W3C
//! Fingerprinting Guidance prefers standardized, bounded values over
//! independent per-session randomization, and longitudinal fingerprint
//! research shows that renderer and audio surfaces are strong
//! re-identification vectors (Laperdrix, Bielova, Baudry, & Avoine, 2020).
//! This module exposes the deterministic, evidence-bound contract those
//! surfaces must satisfy before an adapter may claim a complete stealth
//! presentation. It deliberately performs no evasion: it never defeats an
//! access-control, CAPTCHA, or bot-management gate, and never reads the host.

use std::error::Error;
use std::fmt;

/// A page-observable render or media surface that a stealth adapter must
/// prove before admission.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StealthSurface {
/// Canvas pixel and text rendering observations.
Canvas,
/// WebGL vendor, renderer, and UNMASKED extension observations.
WebGL,
/// WebAudio sample-rate and analyser observations.
WebAudio,
/// WebRTC interface candidate observations.
WebRtc,
}

const REQUIRED_STEALTH_SURFACES: [StealthSurface; 4] = [
StealthSurface::Canvas,
StealthSurface::WebGL,
StealthSurface::WebAudio,
StealthSurface::WebRtc,
];

/// Validate that an adapter overrides every required stealth surface.
///
/// The first missing surface is reported in stable contract order. Extra,
/// duplicate, or reordered supported entries do not change admission, so
/// feature negotiation stays order independent.
pub fn require_stealth_surfaces(supported: &[StealthSurface]) -> Result<(), StealthError> {
for required in REQUIRED_STEALTH_SURFACES {
if !supported.contains(&required) {
return Err(StealthError::MissingSurface(required));
}
}
Ok(())
}

/// A validation failure when assembling a stealth presentation surface set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StealthError {
/// A canvas noise class was outside the enumerated supported set.
InvalidCanvasNoise,
/// A WebAudio sample rate was not a supported standard rate.
InvalidSampleRate,
/// An adapter claims a stealth surface it cannot override.
MissingSurface(StealthSurface),
}

impl fmt::Display for StealthError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCanvasNoise => formatter
.write_str("canvas noise class must be one of the enumerated supported values"),
Self::InvalidSampleRate => {
formatter.write_str("web audio sample rate must be a supported standard rate")
}
Self::MissingSurface(surface) => {
write!(
formatter,
"adapter cannot override required {surface:?} stealth surface"
)
}
}
}
}

impl Error for StealthError {}

/// A bounded, deterministic canvas pixel-noise class.
///
/// Classes map to small closed ranges of least-significant pixel bits so an
/// adapter can widen or narrow noise without presenting a freshly randomized
/// per-session value, which W3C guidance warns can create new distinguishers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CanvasNoise {
/// No injected pixel noise; the smallest observed-distortion class.
Crisp,
/// A single least-significant-bit noise class.
Smooth,
/// A two-bit noise class.
Diffuse,
}

impl CanvasNoise {
/// Map an enumerated class index onto a noise class, rejecting others.
pub const fn quantize(class: u8) -> Result<Self, StealthError> {
match class {
0 => Ok(Self::Crisp),
1 => Ok(Self::Smooth),
2 => Ok(Self::Diffuse),
_ => Err(StealthError::InvalidCanvasNoise),
}
}

/// Return the bounded least-significant bit shift for this class.
#[must_use]
pub const fn bit_shift(self) -> u8 {
match self {
Self::Crisp => 0,
Self::Smooth => 1,
Self::Diffuse => 2,
}
}
}

/// A standardized WebGL renderer token that does not name the host GPU.
///
/// Adapters expose one of these tokens instead of surfacing vendor-specific
/// GPU model strings, which fingerprinting research identifies as a strong
/// re-identification signal (Laperdrix et al., 2020).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebGlRendererToken {
/// ANGLE over a hardware driver family.
Angle,
/// Software rendering with no identifying driver string.
Standard,
}

impl WebGlRendererToken {
/// Canonicalize a known renderer spelling onto a bounded token.
///
/// Known software-renderer markers take precedence over an `ANGLE`
/// prefix because Chromium's SwiftShader renderer is itself ANGLE-backed.
/// Unrecognized spellings fail closed to `None` rather than being echoed
/// to a new class, so an adapter cannot widen the token set by fiat.
#[must_use]
pub fn canonical(spelling: &str) -> Option<Self> {
let upper = spelling.to_ascii_uppercase();
if upper.contains("SOFTWARE") || upper.contains("SWIFTSHADER") {
Some(Self::Standard)
} else if upper.starts_with("ANGLE") {
Some(Self::Angle)
} else {
None
}
}
}

/// A supported WebAudio sample rate in hertz.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebAudioRate {
/// The standard 44.1 kHz rate.
Rate44100,
/// The standard 48 kHz rate.
Rate48000,
}

impl WebAudioRate {
/// Normalize an observed sample rate onto a supported standard rate.
pub fn normalize(rate_hz: u32) -> Result<Self, StealthError> {
match rate_hz {
44_100 => Ok(Self::Rate44100),
48_000 => Ok(Self::Rate48000),
_ => Err(StealthError::InvalidSampleRate),
}
}

/// Return the exact hertz value for this rate.
#[must_use]
pub const fn rate_hz(self) -> u32 {
match self {
Self::Rate44100 => 44_100,
Self::Rate48000 => 48_000,
}
}
}

/// A bounded WebRTC interface-candidate policy.
///
/// This is policy only; the kernel never creates a peer connection or exposes
/// an address. Variant names describe the page-visible candidate behavior
/// directly so adapter code cannot mistake candidate disclosure for a safe
/// privacy mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebRtcInterface {
/// The adapter deliberately exposes direct interface candidates.
DirectCandidates,
/// The adapter publishes only mDNS-candidate interfaces.
MDnsOnly,
}

impl WebRtcInterface {
/// Whether this policy exposes local interface candidates directly.
#[must_use]
pub fn exposes_candidates(self) -> bool {
matches!(self, Self::DirectCandidates)
}
}
Loading
Loading