Skip to content

Fix SAML replay TTL, 2FA backup codes, encryption backfill, and secret rotation grace period - #461

Merged
maugauwi-hash merged 1 commit into
ethos-protocol:mainfrom
Idaonoli:security/saml-2fa-encryption-secret-rotation-fixes
Aug 31, 2026
Merged

Fix SAML replay TTL, 2FA backup codes, encryption backfill, and secret rotation grace period#461
maugauwi-hash merged 1 commit into
ethos-protocol:mainfrom
Idaonoli:security/saml-2fa-encryption-secret-rotation-fixes

Conversation

@Idaonoli

Copy link
Copy Markdown

Summary

Fixes four related security/reliability gaps, one per file:

  • backend/src/saml.rs (Add SAML Assertion Replay Prevention #388): validate_assertion already rejected a
    reused assertion ID, but consumed_assertions kept every ID forever with
    no TTL, growing without bound. It's now a map from assertion ID to the
    instant after which the guard entry may be purged — the assertion's own
    NotOnOrAfter widened by clock_skew_secs — since an assertion can never
    be replayed successfully once it's outside its own validity window
    anyway. Expired entries are purged on each call.
  • backend/src/two_factor.rs (Implement Two-Factor Backup Code Single-Use Enforcement #389): TOTP 2FA had no backup codes at
    all. Adds 10 hashed (SHA-256), single-use backup codes generated on
    enable and returned once in plaintext. Consumption
    (verify_and_consume_backup_code) is guarded by a dedicated mutex so a
    read-check-remove-write sequence can't race — two concurrent verification
    attempts with the same code can't both succeed.
  • backend/src/encryption.rs (Add Field Encryption Key Rotation Backfill Job #390): rotate_field only re-encrypted
    one record at a time, on demand. Adds run_backfill_batch/run_backfill:
    a batched, rate-limited, resumable backfill that walks records still on
    an old key version and re-encrypts them to active_version, persisting a
    cursor between batches so an interruption resumes instead of rescanning.
    Wired into the scheduler to run hourly.
  • backend/src/secret_rotation.rs (Implement Secret Rotation Grace Period Overlap Validation #391): rotation policies had no way
    to express how long a session/token issued with a secret can live, so a
    short grace_period_hours (e.g. jwt_secret's default 1 hour) could
    invalidate a still-valid session mid-use. Adds
    max_token_lifetime_hours to SecretRotationPolicy and
    validate_grace_period_overlap, enforced on policy upsert and at
    default-seeding time; jwt_secret's default grace period is now 192
    hours (comfortably above its ~168h token lifetime) instead of 1 hour.

Also required

backend/src/lib.rs never declared pub mod encryption; or
pub mod saml; — both files were silently excluded from the crate (and
therefore from cargo test) despite containing substantial existing logic
and tests. Registering them was necessary for this PR's own changes (and
tests) to actually compile and run, and surfaces both modules to the build
for the first time.

Test plan

  • cargo check --lib --tests — clean (no errors; pre-existing
    warnings only)
  • cargo test --lib — 413 passed, 0 failed, including new tests for
    SAML replay TTL expiry, backup-code single-use + concurrent-race
    regression, backfill batching/resumability/skip-current, and grace
    period validation (valid/invalid/equal boundary + all seeded
    defaults)
  • Verified via a from-scratch Docker build (no local Rust toolchain
    available in this environment)

Docs updated: docs/saml-sso.md, docs/encrypted-field-storage.md,
docs/secret-rotation-policy.md.

Closes #388
Closes #389
Closes #390
Closes #391

…t rotation grace period

- saml.rs: bound the assertion replay guard to the assertion's own validity
  window (NotOnOrAfter + clock skew) instead of retaining consumed IDs
  forever, so the guard set no longer grows unbounded.
- two_factor.rs: add hashed, single-use backup codes for TOTP, consumed
  atomically under a dedicated lock so concurrent verification attempts
  can't both redeem the same code.
- encryption.rs: add a batched, rate-limited, resumable key-rotation
  backfill job (run_backfill_batch/run_backfill) and wire a periodic run
  into the scheduler.
- secret_rotation.rs: add max_token_lifetime_hours to rotation policies and
  reject any grace_period_hours that doesn't exceed it, so a rotation can't
  invalidate a session/token mid-use.
- lib.rs: register the encryption and saml modules, which were never
  declared and so were silently excluded from the build and test suite.

Closes ethos-protocol#388, closes ethos-protocol#389, closes ethos-protocol#390, closes ethos-protocol#391
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Idaonoli Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@maugauwi-hash
maugauwi-hash merged commit 0cf1ecd into ethos-protocol:main Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants