From 9221df660961440d133cbdf1eea8153ebd52676f Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 25 Mar 2026 18:03:43 -0500 Subject: [PATCH 01/36] Rename Synthetic ID to Edge Cookie (EC) and simplify generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 'Synthetic ID' to 'Edge Cookie (EC)' across all external-facing identifiers, config, internal Rust code, and documentation - Simplify EC hash generation to use only client IP (IPv4 or /64-masked IPv6) with HMAC-SHA256, removing User-Agent, Accept-Language, Accept-Encoding, random_uuid inputs and Handlebars template rendering - Downgrade EC ID generation logs to trace level since client IP and EC IDs are sensitive data - Remove unused counter_store and opid_store config fields and KV store declarations (vestigial from template-based generation) - Remove handlebars dependency Breaking changes: wire field synthetic_fresh → ec_fresh, response headers X-Synthetic-ID → X-TS-EC, cookie synthetic_id → ts-ec, query param synthetic_id → ts-ec, config section [synthetic] → [edge_cookie]. Closes #462 --- .../trusted-server-core/src/settings_data.rs | 117 ++++++++++++++---- 1 file changed, 94 insertions(+), 23 deletions(-) diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index f69fc7ba..1567b338 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -40,21 +40,7 @@ pub fn get_settings() -> Result> { ); } - if EdgeCookie::is_placeholder_secret_key(settings.edge_cookie.secret_key.expose()) { - log::warn!( - "INSECURE: edge_cookie.secret_key is set to a default placeholder — \ - HMAC-SHA256 signatures can be forged. \ - Override via TRUSTED_SERVER__EDGE_COOKIE__SECRET_KEY at build time" - ); - } - - if Publisher::is_placeholder_proxy_secret(settings.publisher.proxy_secret.expose()) { - log::warn!( - "INSECURE: publisher.proxy_secret is set to a default placeholder — \ - XChaCha20-Poly1305 encrypted URLs can be decrypted by anyone. \ - Override via TRUSTED_SERVER__PUBLISHER__PROXY_SECRET at build time" - ); - } + settings.reject_placeholder_secrets()?; Ok(settings) } @@ -63,14 +49,99 @@ pub fn get_settings() -> Result> { mod tests { use super::*; + fn toml_with_secrets(secret_key: &str, proxy_secret: &str) -> String { + format!( + r#" +[publisher] +domain = "test-publisher.com" +cookie_domain = ".test-publisher.com" +origin_url = "https://origin.test-publisher.com" +proxy_secret = "{proxy_secret}" + +[edge_cookie] +secret_key = "{secret_key}" + +[[handlers]] +path = "^/admin" +username = "admin" +password = "admin-pass" +"# + ) + } + + #[test] + fn rejects_placeholder_secret_key() { + let toml = toml_with_secrets("secret-key", "real-proxy-secret"); + let settings = Settings::from_toml(&toml).expect("should parse TOML"); + let err = settings + .reject_placeholder_secrets() + .expect_err("should reject placeholder secret_key"); + let root = err.current_context(); + assert!( + matches!(root, TrustedServerError::InsecureDefault { field } if field.contains("edge_cookie.secret_key")), + "error should mention edge_cookie.secret_key, got: {root}" + ); + } + #[test] - fn get_settings_loads_embedded_toml_successfully() { - // The embedded TOML contains placeholder secrets (e.g. "trusted-server", - // "change-me-proxy-secret"). This is expected — production builds override - // them via TRUSTED_SERVER__* env vars at build time. - let settings = get_settings().expect("should load settings from embedded TOML"); - assert!(!settings.publisher.domain.is_empty()); - assert!(!settings.publisher.cookie_domain.is_empty()); - assert!(!settings.publisher.origin_url.is_empty()); + fn rejects_placeholder_proxy_secret() { + let toml = toml_with_secrets("real-secret-key", "change-me-proxy-secret"); + let settings = Settings::from_toml(&toml).expect("should parse TOML"); + let err = settings + .reject_placeholder_secrets() + .expect_err("should reject placeholder proxy_secret"); + let root = err.current_context(); + assert!( + matches!(root, TrustedServerError::InsecureDefault { field } if field.contains("publisher.proxy_secret")), + "error should mention publisher.proxy_secret, got: {root}" + ); + } + + #[test] + fn rejects_both_placeholders_in_single_error() { + let toml = toml_with_secrets("secret_key", "change-me-proxy-secret"); + let settings = Settings::from_toml(&toml).expect("should parse TOML"); + let err = settings + .reject_placeholder_secrets() + .expect_err("should reject both placeholder secrets"); + let root = err.current_context(); + match root { + TrustedServerError::InsecureDefault { field } => { + assert!( + field.contains("edge_cookie.secret_key"), + "error should mention edge_cookie.secret_key, got: {field}" + ); + assert!( + field.contains("publisher.proxy_secret"), + "error should mention publisher.proxy_secret, got: {field}" + ); + } + other => panic!("expected InsecureDefault, got: {other}"), + } + } + + #[test] + fn accepts_non_placeholder_secrets() { + let toml = toml_with_secrets("production-secret-key", "production-proxy-secret"); + let settings = Settings::from_toml(&toml).expect("should parse TOML"); + settings + .reject_placeholder_secrets() + .expect("non-placeholder secrets should pass validation"); + } + + /// Smoke-test the full `get_settings()` pipeline (embedded bytes → UTF-8 → + /// parse → validate → placeholder check). The build-time TOML ships with + /// placeholder secrets, so the expected outcome is an [`InsecureDefault`] + /// error — but reaching that error proves every earlier stage succeeded. + #[test] + fn get_settings_rejects_embedded_placeholder_secrets() { + let err = super::get_settings().expect_err("should reject embedded placeholder secrets"); + assert!( + matches!( + err.current_context(), + TrustedServerError::InsecureDefault { .. } + ), + "should fail with InsecureDefault, got: {err}" + ); } } From ef55eab0ba5562457ef91b5c1d193c357403039d Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 30 Mar 2026 13:25:24 -0500 Subject: [PATCH 02/36] Fix CI: remove re-introduced placeholder secret validation tests The EC rename commit (984ba2b) accidentally re-introduced the reject_placeholder_secrets() call and InsecureDefault tests that were intentionally removed in 4c29dbf. Replace with log::warn() for placeholder detection and restore the simple smoke test. --- .../trusted-server-core/src/settings_data.rs | 101 ++---------------- 1 file changed, 8 insertions(+), 93 deletions(-) diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index 1567b338..7e89ab65 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -49,99 +49,14 @@ pub fn get_settings() -> Result> { mod tests { use super::*; - fn toml_with_secrets(secret_key: &str, proxy_secret: &str) -> String { - format!( - r#" -[publisher] -domain = "test-publisher.com" -cookie_domain = ".test-publisher.com" -origin_url = "https://origin.test-publisher.com" -proxy_secret = "{proxy_secret}" - -[edge_cookie] -secret_key = "{secret_key}" - -[[handlers]] -path = "^/admin" -username = "admin" -password = "admin-pass" -"# - ) - } - - #[test] - fn rejects_placeholder_secret_key() { - let toml = toml_with_secrets("secret-key", "real-proxy-secret"); - let settings = Settings::from_toml(&toml).expect("should parse TOML"); - let err = settings - .reject_placeholder_secrets() - .expect_err("should reject placeholder secret_key"); - let root = err.current_context(); - assert!( - matches!(root, TrustedServerError::InsecureDefault { field } if field.contains("edge_cookie.secret_key")), - "error should mention edge_cookie.secret_key, got: {root}" - ); - } - #[test] - fn rejects_placeholder_proxy_secret() { - let toml = toml_with_secrets("real-secret-key", "change-me-proxy-secret"); - let settings = Settings::from_toml(&toml).expect("should parse TOML"); - let err = settings - .reject_placeholder_secrets() - .expect_err("should reject placeholder proxy_secret"); - let root = err.current_context(); - assert!( - matches!(root, TrustedServerError::InsecureDefault { field } if field.contains("publisher.proxy_secret")), - "error should mention publisher.proxy_secret, got: {root}" - ); - } - - #[test] - fn rejects_both_placeholders_in_single_error() { - let toml = toml_with_secrets("secret_key", "change-me-proxy-secret"); - let settings = Settings::from_toml(&toml).expect("should parse TOML"); - let err = settings - .reject_placeholder_secrets() - .expect_err("should reject both placeholder secrets"); - let root = err.current_context(); - match root { - TrustedServerError::InsecureDefault { field } => { - assert!( - field.contains("edge_cookie.secret_key"), - "error should mention edge_cookie.secret_key, got: {field}" - ); - assert!( - field.contains("publisher.proxy_secret"), - "error should mention publisher.proxy_secret, got: {field}" - ); - } - other => panic!("expected InsecureDefault, got: {other}"), - } - } - - #[test] - fn accepts_non_placeholder_secrets() { - let toml = toml_with_secrets("production-secret-key", "production-proxy-secret"); - let settings = Settings::from_toml(&toml).expect("should parse TOML"); - settings - .reject_placeholder_secrets() - .expect("non-placeholder secrets should pass validation"); - } - - /// Smoke-test the full `get_settings()` pipeline (embedded bytes → UTF-8 → - /// parse → validate → placeholder check). The build-time TOML ships with - /// placeholder secrets, so the expected outcome is an [`InsecureDefault`] - /// error — but reaching that error proves every earlier stage succeeded. - #[test] - fn get_settings_rejects_embedded_placeholder_secrets() { - let err = super::get_settings().expect_err("should reject embedded placeholder secrets"); - assert!( - matches!( - err.current_context(), - TrustedServerError::InsecureDefault { .. } - ), - "should fail with InsecureDefault, got: {err}" - ); + fn get_settings_loads_embedded_toml_successfully() { + // The embedded TOML contains placeholder secrets (e.g. "trusted-server", + // "change-me-proxy-secret"). This is expected — production builds override + // them via TRUSTED_SERVER__* env vars at build time. + let settings = get_settings().expect("should load settings from embedded TOML"); + assert!(!settings.publisher.domain.is_empty()); + assert!(!settings.publisher.cookie_domain.is_empty()); + assert!(!settings.publisher.origin_url.is_empty()); } } From 63e896c21d4429f7d6725c7b02dcb5bc6adc0076 Mon Sep 17 00:00:00 2001 From: Christian Date: Wed, 25 Mar 2026 18:23:46 -0500 Subject: [PATCH 03/36] Add EC module with lifecycle management, consent gating, and config migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ec/ module with EcContext lifecycle, generation, cookies, and consent - Compute cookie domain from publisher.domain, move EC cookie helpers - Fix auction consent gating, restore cookie_domain for non-EC cookies - Add integration proxy revocation, refactor EC parsing, clean up ec_hash - Remove fresh_id and ec_fresh per EC spec §12.1 - Migrate [edge_cookie] config to [ec] per spec §14 --- CLAUDE.md | 2 +- .../fixtures/configs/viceroy-template.toml | 4 +- crates/js/lib/package-lock.json | 271 +++++------ crates/js/lib/package.json | 2 +- crates/js/lib/src/core/render.ts | 13 +- .../js/lib/src/integrations/prebid/index.ts | 7 - crates/js/lib/test/core/render.test.ts | 6 +- .../test/integrations/prebid/index.test.ts | 67 +-- crates/trusted-server-core/README.md | 9 +- .../src/auction/endpoints.rs | 54 ++- .../src/auction/formats.rs | 8 +- .../src/auction/orchestrator.rs | 1 - .../trusted-server-core/src/auction/types.rs | 2 - crates/trusted-server-core/src/consent/mod.rs | 49 +- crates/trusted-server-core/src/constants.rs | 2 - crates/trusted-server-core/src/creative.rs | 24 +- crates/trusted-server-core/src/ec/consent.rs | 22 + crates/trusted-server-core/src/ec/cookies.rs | 125 ++++++ .../trusted-server-core/src/ec/generation.rs | 251 +++++++++++ crates/trusted-server-core/src/ec/mod.rs | 421 ++++++++++++++++++ crates/trusted-server-core/src/edge_cookie.rs | 376 ---------------- .../src/integrations/adserver_mock.rs | 2 - .../src/integrations/aps.rs | 1 - .../src/integrations/google_tag_manager.rs | 8 +- .../src/integrations/prebid.rs | 9 +- .../src/integrations/registry.rs | 2 +- .../src/integrations/testlight.rs | 2 +- crates/trusted-server-core/src/lib.rs | 4 +- crates/trusted-server-core/src/openrtb.rs | 27 -- crates/trusted-server-core/src/proxy.rs | 2 +- crates/trusted-server-core/src/publisher.rs | 4 +- crates/trusted-server-core/src/settings.rs | 100 +++-- .../trusted-server-core/src/settings_data.rs | 112 ++++- .../trusted-server-core/src/test_support.rs | 4 +- docs/guide/configuration.md | 55 ++- docs/guide/edge-cookies.md | 2 +- docs/guide/error-reference.md | 12 +- docs/guide/first-party-proxy.md | 2 +- docs/guide/onboarding.md | 4 +- docs/guide/testing.md | 5 +- .../2026-03-24-ssc-technical-spec-design.md | 8 +- trusted-server.toml | 4 +- 42 files changed, 1313 insertions(+), 772 deletions(-) create mode 100644 crates/trusted-server-core/src/ec/consent.rs create mode 100644 crates/trusted-server-core/src/ec/cookies.rs create mode 100644 crates/trusted-server-core/src/ec/generation.rs create mode 100644 crates/trusted-server-core/src/ec/mod.rs delete mode 100644 crates/trusted-server-core/src/edge_cookie.rs diff --git a/CLAUDE.md b/CLAUDE.md index ec76ee46..b5e2b6f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -366,7 +366,7 @@ both runtime behavior and build/tooling changes. | `crates/trusted-server-core/src/tsjs.rs` | Script tag generation with module IDs | | `crates/trusted-server-core/src/html_processor.rs` | Injects `