Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ 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 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 with a 256-byte pre-normalization input ceiling, 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
11 changes: 11 additions & 0 deletions crates/originweave-fingerprint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod stealth;
mod ua_hints;

pub use stealth::{
CanvasNoise, StealthError, StealthSurface, WebAudioRate, WebGlRendererToken, WebRtcInterface,
require_stealth_surfaces,
};
pub use ua_hints::{
ClientHintsError, HintsArchitecture, HintsBitness, HintsPlatform, UaBrand, UaClientHints,
};
Comment on lines +27 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 New surfaces lack evidence binding

UaClientHints and stealth values remain outside PresentationProfile and its digest. Confirm adapters cannot claim replayable identity evidence without binding these fields.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +24 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Pull-request description omits feature scope

The description calls this only stack realignment despite adding public fingerprint contracts and ADRs. It omits required effects, verification evidence, and residual risk.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


use sha2::{Digest, Sha256};
use std::error::Error;
use std::fmt;
Expand Down
209 changes: 209 additions & 0 deletions crates/originweave-fingerprint/src/stealth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//! 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;

/// Maximum renderer spelling length normalized before token classification.
const MAX_WEBGL_RENDERER_SPELLING_BYTES: usize = 256;

/// 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> {
if spelling.len() > MAX_WEBGL_RENDERER_SPELLING_BYTES {
return None;
}
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)
Comment on lines +152 to +153

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Malformed renderer names bypass rejection

WebGlRendererToken::canonical accepts any spelling beginning with ANGLE, including ANGLEevil. Unknown renderers therefore enter the closed presentation set.

Prompt for agents
Tighten WebGlRendererToken::canonical in crates/originweave-fingerprint/src/stealth.rs so it recognizes only the documented ANGLE renderer grammar, rather than every string with an ANGLE prefix. Preserve software and SwiftShader precedence. Add realistic boundary tests for the accepted ANGLE form and malformed prefixes such as ANGLEevil, ANGLE suffixes without the required delimiter, and case variants, while retaining complete branch coverage.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

} 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