Align signature verification with Trusted Server v1.1#80
Align signature verification with Trusted Server v1.1#80ChristianPavilonis wants to merge 12 commits intomainfrom
Conversation
aram356
left a comment
There was a problem hiding this comment.
PR Review
The v1.1 signing protocol and SigningPayload canonicalization are solid structural improvements. The fixed-pricing simplification makes sense for a mock bidder. A few things need attention before merge — mostly around the trust model for signed fields and consistency across docs.
Summary
| Emoji | Count | Meaning |
|---|---|---|
| 🔧 | 4 | Needs change |
| ♻️ | 1 | Refactoring suggestion |
| 🌱 | 1 | Future consideration |
| 📌 | 1 | Out-of-scope follow-up |
| 💯 | 1 | Positive |
Non-inline notes
⛏ test_price_from_ext_and_iframe_bid_param (auction.rs:474) — the test name still implies we're verifying that ext.bid drives the price. It now tests the opposite. Rename to something like test_ext_bid_override_is_ignored.
⛏ log::info!("URI: {}", uri) at verification.rs:73 looks like a debug leftover from development. The line above already logs at debug level. Demote or remove.
📌 The request example in openrtb-auction.md (lines 52–55) still includes "ext": {"mocktioneer": {"bid": 2.5}} in the impression — contradicts the new Pricing section that says bid overrides are ignored. Remove it from the example to avoid confusion.
… with fixed pricing - Cross-check ext.trusted_server host/scheme against server-observed values - Enforce timestamp freshness window (±5 min) to prevent replay attacks - Add field-order warning comment on SigningPayload struct - Fix ts unit mismatch: align tests with docs (milliseconds) - Add positive-path Ed25519 round-trip test with deterministic keypair - Remove debug log leftover in fetch_jwks - Simplify APS size selection to area-based ranking (replaces CPM) - Remove CPM from /sizes endpoint response - Rename test to test_ext_bid_override_is_ignored - Docs sweep: remove bid override references across all doc pages
Review feedback addressed in 3bbbbc7🔧 Trust model for signed fields (verification.rs)
🌱 Field order comment on
|
Align remaining docs with fixed /bin/zsh.01 bidding and update Playwright /_/sizes assertions to match the cpm-free response, so review cleanups and CI checks reflect the current API behavior.
aram356
left a comment
There was a problem hiding this comment.
Review: Round 2
The follow-up commits addressed the first review well — host/scheme cross-checks, timestamp freshness, v1.1 canonical payload, E2E roundtrip test, docs sweep, and APS size selection are all in. Good work.
Remaining issues below, grouped by severity.
What's done well
- Check ordering — cheap validations (field extraction, host/scheme cross-check, timestamp freshness) all run before the expensive JWKS fetch + crypto. Good.
- Roundtrip crypto test —
verify_ed25519_roundtrip_with_known_keypairproves the full sign→verify pipeline and catches tampered payloads. Most valuable test in the file. - Creative sig param whitelist — sanitizes
sigquery param against known-safe values instead of trusting arbitrary input. Clean pattern. - Docs consistency — all references to dynamic pricing, price override, and
bidparam removed across 7 doc files. Thorough.
aram356
left a comment
There was a problem hiding this comment.
Review: Round 3
Prior rounds addressed host/scheme normalization, dead code removal (get_cpm, DEFAULT_CPM, MAX_AREA_BONUS), unused JS variable, and fragile test assertions. The branch is CI-clean (fmt, clippy, tests, WASM targets all pass).
One blocking item remains from round 2 that was partially addressed — the SIZE_MAP values were documented as legacy but not actually zeroed out.
CI Status
- fmt: PASS
- clippy: PASS
- tests: PASS (all workspace targets)
cargo check --features "fastly cloudflare": PASScargo check --target wasm32-unknown-unknown(Cloudflare): PASS
… with fixed pricing - Cross-check ext.trusted_server host/scheme against server-observed values - Enforce timestamp freshness window (±5 min) to prevent replay attacks - Add field-order warning comment on SigningPayload struct - Fix ts unit mismatch: align tests with docs (milliseconds) - Add positive-path Ed25519 round-trip test with deterministic keypair - Remove debug log leftover in fetch_jwks - Simplify APS size selection to area-based ranking (replaces CPM) - Remove CPM from /sizes endpoint response - Rename test to test_ext_bid_override_is_ignored - Docs sweep: remove bid override references across all doc pages
Align remaining docs with fixed /bin/zsh.01 bidding and update Playwright /_/sizes assertions to match the cpm-free response, so review cleanups and CI checks reflect the current API behavior.
- Normalize X-Forwarded-Proto: split on comma, trim, lowercase, fall back to request URI scheme before defaulting to https - Add canonicalize_host() to strip default ports (:443/:80) and lowercase both sides of host/scheme cross-checks in verification - Log deprecation warning when imp.ext.mocktioneer.bid is present but ignored - Remove dead pricing code: get_cpm(), DEFAULT_CPM, MAX_AREA_BONUS - Update CLAUDE.md key constants to reflect FIXED_BID_CPM - Remove unused JS variable 'b' in creative template - Strengthen timestamp test assertions with matches! on error variant
…calize_host tests
…ot bidder host The host cross-check was comparing ext.trusted_server.request_host (the publisher's domain) against the bidder's own ForwardedHost, which will never match in header bidding. Changed to compare against site.domain from the OpenRTB request instead. Also removed the redundant scheme cross-check since request_scheme is already verified cryptographically as part of the signed payload.
Remove the phf dependency entirely — SIZE_MAP values were zeroed out and only used for key membership checks. Replace with a sorted const array of (i64, i64) tuples. O(n) scan over 13 entries is trivially fast and eliminates the build-time codegen dependency.
Update all references from $0.01 to $0.20 across docs, CLAUDE.md, and inline code comments to match the actual constant value.
00c23d8 to
4766574
Compare
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Well-structured PR that reworks signature verification for Trusted Server v1.1 and simplifies pricing to a fixed $0.20 CPM. The verification logic is sound with proper timestamp freshness, host cross-validation against site.domain, and comprehensive Ed25519 roundtrip tests. One blocking issue: incorrect base64-encoded prices in APS docs.
🔧 Needs Change
- Incorrect base64-encoded prices in APS docs:
MC4wMQ==decodes to"0.01", not the actual fixed bid price.encode_aps_price(0.20)producesMC4ysince Rust formats 0.20 as"0.2". The decode example (MC4yMA==) is also wrong. See inline comments ondocs/api/aps-bid.md.
⛏ Nitpicks
- Stale "highest CPM" comment (
routes.rs:905): Says "should select 970x250 with highest CPM" but size selection is now by largest area. Suggest:// should select 970x250 with largest area from [728x90, 970x250] - Stale "highest CPM" comment (
tests/aps_endpoints.rs:197): Says "Should bid on the highest CPM size (970x250 = $4.20 > 728x90 = $3.00)" — references old pricing. Suggest:// Should bid on the largest area size (970x250 = 242500 > 728x90 = 65520) - Stale CLAUDE.md reference (
CLAUDE.md:154): See inline comment.
♻️ Refactoring
- Dead
NoJwksDomainvariant (verification.rs:67): Defined inVerificationErrorbut never constructed anywhere in the codebase. Consider removing since the verification module is being reworked, or#[allow(dead_code)]if reserved for future use.
🤔 Thoughts
SystemTime::now()WASM compatibility (verification.rs:268): New in this PR. Compiles for WASM targets today, but behavior onwasm32-unknown-unknownis platform-dependent. Consistent with existingInstant::now()usage so not a regression — noting for awareness.
🌱 Seeds
- Replay protection: The 5-minute freshness window prevents large-scale replay but permits replay within that window. Fine for a mock bidder; a nonce/seen-ID cache could layer on if production trust enforcement is ever needed.
👍 Praise
- Canonical payload design: Struct field order as signing contract + pinned JSON test is the right pattern for security-critical serialization
- Timestamp freshness:
abs_diff-based check correctly handles both stale and future timestamps - Host cross-check fix: Comparing
request_hostagainstsite.domaininstead of bidder ForwardedHost is the correct header-bidding trust model - Ed25519 roundtrip test: Full sign-verify cycle with tampered payload case provides strong confidence
- phf removal: Const array simplification eliminates build-time codegen with negligible runtime cost
CI Status
- fmt: PASS
- clippy: PASS
- tests: PASS (100 tests)
prk-Jr
left a comment
There was a problem hiding this comment.
PR Review
Summary
The v1.1 signing and fixed-pricing changes look solid overall, and the required Rust CI gates are green. I found one blocking docs mismatch plus two non-blocking follow-ups worth addressing before merge.
Findings
🔧 Needs Change
- Trusted Server example uses mismatched hosts: The request example shows
site.domain = "example.com"butext.trusted_server.request_host = "publisher.example". The implementation now rejects that combination becauserequest_hostmust matchsite.domainafter canonicalization.
🤔 Thoughts
- Floor-testing docs imply Mocktioneer enforces floors: The integration docs read as if Mocktioneer itself rejects bids below
imp.bidfloor, but the auction path always emitsFIXED_BID_CPMand never evaluatesbidfloor. Clarify that the host integration or mediation path is responsible for floor enforcement.
🌱 Seeds
- Add a full positive-path verification test: The new tests cover malformed inputs and raw Ed25519 verification, but not a full
verify_request_id_signature()success path with JWKS lookup plus a returnedkid.
CI Status
- fmt: PASS
- clippy: PASS
- tests: PASS
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Upgrades signature verification to Trusted Server v1.1 (canonical payload, host cross-check, timestamp freshness) and simplifies pricing to fixed $0.20 CPM. Well-structured with thorough tests (100/100 pass). Two stale CLAUDE.md references need fixing; a few questions and observations inline.
Findings
🔧 Needs Change
- Stale
phfreference in CLAUDE.md: Dependencies Philosophy (line 305) still listsphf (static maps)but the dep was removed in this PR. Remove it. - Stale auction module description in CLAUDE.md: Module table (line 144) says
auction.rs — Size pricing, CPM calculation, standard sizesbutget_cpm()/DEFAULT_CPM/MAX_AREA_BONUSwere all removed. Update to e.g.auction.rs — Fixed-price bidding, standard size validation. - Stale price in
encode_aps_pricedoc comment (auction.rs:186): Still showsecho "Mi41MA==" | base64 -d → 2.50referencing the old $2.50 CPM. Update toecho "MC4y" | base64 -d → 0.2.
❓ Questions
- Redundant
domain/site_domainparams: Both always receivereq.site.domain— is there a planned scenario where they differ? (see inline onroutes.rs) SystemTime::now()on WASM: Newcurrent_time_ms()may not work onwasm32-unknown-unknownwithout a polyfill — has this been validated? (see inline onverification.rs)
🤔 Thoughts
- Non-determinism exception: Timestamp freshness check is justified for replay prevention, but contradicts the "same input → same output" rule in CLAUDE.md. Worth a documented exception. (see inline on
verification.rs) - Serde field-order contract:
SigningPayloadcorrectness depends on declaration-order serialization — the pinning test is great mitigation. (see inline onverification.rs)
⛏ Nitpicks
canonicalize_hostport stripping: Only handles:443/:80— fine for prod, just noting. (see inline onverification.rs)
CI Status
- fmt: PASS
- clippy: PASS
- tests: PASS (100/100)
| &req.id, | ||
| req.ext.as_ref(), | ||
| domain, | ||
| domain, |
There was a problem hiding this comment.
❓ Redundant parameter: Both domain (JWKS fetch) and the new site_domain (host cross-check) receive the same req.site.domain value. Is there a planned scenario where they'd differ (e.g. JWKS hosted on a separate infra domain)? If not, collapsing to a single parameter would reduce surface area for confusion.
| } | ||
|
|
||
| /// Strip default ports (:443 for https, :80 for http) and lowercase the host | ||
| /// so that `example.com:443` matches `example.com` from the signer. |
There was a problem hiding this comment.
❓ WASM compatibility: SystemTime::now() may not work on wasm32-unknown-unknown (Cloudflare Workers) without a polyfill. Instant::now() was already used for JWKS caching (pre-existing), but SystemTime is a new addition. Has this been validated on the Cloudflare target?
| .or_else(|| h.strip_suffix(":80")) | ||
| .unwrap_or(h) | ||
| .to_lowercase() | ||
| } |
There was a problem hiding this comment.
🤔 Non-determinism exception: The freshness check is time-dependent — same request verifies now but fails 6 minutes later. Justified for replay prevention, but CLAUDE.md says "no time-dependent logic" and "same input always produces same output." Consider adding a note near the determinism rule so future contributors don't try to "fix" this.
|
|
||
| // IMPORTANT: Field order defines the canonical signing payload. | ||
| // `serde_json::to_string` serializes struct fields in declaration order. | ||
| // Reordering fields will silently break signature verification. |
There was a problem hiding this comment.
🤔 Serde field-order contract: Canonical payload correctness depends on serde_json::to_string serializing fields in declaration order. The warning comment and build_signing_payload_uses_v11_shape test are great mitigation — just flagging for awareness that adding a field to this struct requires updating the test and coordinating with upstream signers.
|
|
||
| fn current_time_ms() -> Result<u64, VerificationError> { | ||
| std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) |
There was a problem hiding this comment.
⛏ Only strips :443 and :80 — non-standard HTTPS ports like :8443 won't be canonicalized. Fine for production publishers (standard ports), just noting for completeness.
Summary
ext.trusted_serverfields (version,kid,request_host,request_scheme,ts) to prevent partial or ambiguous verification inputs.$0.01) for generated OpenRTB and APS responses, and remove request-level bid override behavior from generated ad markup.Changes
crates/mocktioneer-core/src/verification.rsversion,kid,host,scheme,id,ts), required v1.1 ext fields, enforced signing version1.1, and added payload/version tests.crates/mocktioneer-core/src/render.rscrates/mocktioneer-core/static/templates/creative.html.hbsverified/failed/not_present).crates/mocktioneer-core/src/auction.rs$0.01), removed request-supplied bid overrides from response generation, and updated related tests.docs/api/openrtb-auction.mdCloses
Closes #79
Closes #81
Test plan
cargo test --workspace --all-targetscargo clippy --workspace --all-targets --all-features -- -D warningscargo check --workspace --all-targets --features "fastly cloudflare"cargo run -p mocktioneer-adapter-axumcd tests/playwright && npm test)cargo fmt --all -- --check;cargo test -p mocktioneer-coreChecklist
mocktioneer-core, not in adapter cratesroutes.rsandedgezero.tomlrender.rs, not inlined in handlers#[cfg(test)]or intests/)