Skip to content

Use constant-time comparison for token and credential verification#506

Open
prk-Jr wants to merge 9 commits intomainfrom
hardening/constant-time-comparisons
Open

Use constant-time comparison for token and credential verification#506
prk-Jr wants to merge 9 commits intomainfrom
hardening/constant-time-comparisons

Conversation

@prk-Jr
Copy link
Collaborator

@prk-Jr prk-Jr commented Mar 16, 2026

Summary

  • Replace == comparisons with subtle::ConstantTimeEq in all three secret-verification sites (tstoken, clear-URL signature, Basic Auth), eliminating timing side-channel attacks
  • Fix a secondary short-circuit oracle in Basic Auth: && allowed an attacker to distinguish wrong-username from wrong-password via timing — replaced with bitwise & on subtle::Choice so both fields are always evaluated
  • Add log::warn! on auth failure (path only, no credentials) and five new targeted tests covering each fix

Changes

File Change
Cargo.toml Add subtle = "2" to workspace dependencies
crates/common/Cargo.toml Add subtle as direct dependency
crates/common/src/auth.rs CT comparison with & instead of &&; log::warn! on failure; 2 new tests
crates/common/src/http_util.rs CT comparison in verify_clear_url_signature; 2 new tests
crates/common/src/proxy.rs CT comparison for tstoken in reconstruct_and_validate_signed_target; 1 new test

Closes

Closes #410

Test plan

  • cargo test --workspace
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --bin trusted-server-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve

JS tests are failing on main due to a pre-existing ESM/CJS incompatibility in html-encoding-sniffer node_modules — unrelated to this PR.

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses tracing macros (not println!)
  • New code has tests
  • No secrets or credentials committed

Replace standard == comparisons with subtle::ConstantTimeEq in the three
places that verify secrets: tstoken signature in proxy.rs, clear-URL
signature in http_util.rs, and Basic Auth credentials in auth.rs.

The auth fix also removes the && short-circuit that created a username-
existence oracle — both username and password are now always evaluated
using bitwise & on subtle::Choice values.

Adds log::warn on auth failure (path only, no credentials) and five
targeted tests covering tampered tokens, wrong-username-right-password,
and empty tokens.

Closes #410
@prk-Jr prk-Jr self-assigned this Mar 16, 2026
Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

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

See below

Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

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

Verdict: Clean, well-focused security hardening PR. Ready to merge with minor improvements.

Excellent anti-oracle design in auth.rs — bitwise & on subtle::Choice instead of && prevents username-existence oracle. Minimal, focused changes — only touches the 3 comparison sites + targeted tests. Good test coverage (5 new tests). subtle v2 is the right dependency choice.

Copy link
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

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

Approved — ready to merge

This PR correctly hardens the three token/credential comparison sites with constant-time operations from the subtle crate. The changes are minimal, focused, and well-documented.

Highlights

  • Excellent anti-oracle design in auth.rs: using bitwise & on subtle::Choice instead of && prevents username-existence oracles. The inline comment explaining why is exactly what future maintainers need.
  • Well-chosen tests covering tampered tokens (same length, wrong bytes), wrong-username/right-password, correct-username/wrong-password, and empty tokens.
  • Good doc comments explaining security invariants on enforce_basic_auth, verify_clear_url_signature, and inline in proxy.rs.
  • log::warn! on auth failure logs path only (no credentials) — correct observability without leaking secrets.
  • Minimal, focused changes — only the 3 comparison sites plus targeted tests. No unnecessary refactoring.
  • All CI checks pass (fmt, test, clippy, vitest, CodeQL).

Minor note (not actionable for this PR)

  • extract_credentials in auth.rs (lines 49-73) has pre-existing early returns that leak timing about credential format (missing header, wrong scheme, malformed base64). This is not introduced by this PR and is fine — the CT comparison correctly protects credential values, not header format parsing.

Copy link
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

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

Summary

This PR replaces == comparisons with subtle::ConstantTimeEq in all three secret-verification sites (tstoken, clear-URL signature, Basic Auth), fixing timing side-channel vulnerabilities. The bitwise & in auth to eliminate the username-existence oracle is textbook. One structural issue to address before merge.

Blocking

🔧 wrench

  • Redacted::PartialEq re-introduces the timing oracle this PR fixes: PartialEq for Redacted<T> (in redacted.rs:69-72, not in this PR's diff but directly relevant) delegates to T::PartialEq, which for String is a short-circuiting byte comparison. This is the type wrapping proxy_secret, username, and password. While the call sites in this PR correctly use expose() + ct_eq(), the PartialEq impl creates an attractive nuisance — a future developer comparing Redacted values directly would unknowingly bypass constant-time. Consider removing the PartialEq impls or implementing via ct_eq in a follow-up.

❓ question

  • reconstruct_and_validate_signed_target returns 502 for invalid tstoken (proxy.rs:1084): A tampered or forged tstoken is a client error, not a gateway error. Should this be 400 Bad Request or 403 Forbidden instead of 502 Bad Gateway?

Non-blocking

♻️ refactor

  • Duplicated constant-time comparison pattern: The len == len && bool::from(ct_eq) pattern appears in auth.rs:39, http_util.rs:331, and proxy.rs:1082. Extract a ct_str_eq(a, b) -> bool helper to prevent future copies from forgetting the length check or bool::from conversion. (auth.rs:39)

🌱 seedling

  • sign_clear_url is SHA-256(prefix || secret || message), not HMAC (http_util.rs:320): The doc comment acknowledges this. The construction is safe here because URLs are validated against the signature (not extended), but SHA-256(prefix || secret || msg) is vulnerable to length-extension if reused elsewhere. Consider migrating to hmac::Hmac<Sha256> in a future PR — sha2 is already a dependency.

⛏ nitpick

  • PR body checklist says "Uses tracing macros (not println!)" but the codebase uses log, not tracing — minor template inconsistency.

CI Status

  • cargo fmt: PASS
  • cargo clippy: PASS
  • cargo test: PASS
  • vitest: PASS
  • integration tests: PASS
  • browser integration tests: PASS
  • CodeQL: PASS

@prk-Jr prk-Jr requested a review from aram356 March 21, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-constant-time token/password comparison enables timing attacks

3 participants