Repository navigation
fix: graceful shutdown, resumable key backfill, constant-time OTP compare, escaped email templates - #375
Merged
Emmyt24 merged 2 commits intoSep 28, 2026
Conversation
…pare, escaped emails - feat(server): drain in-flight HTTP requests and the current ingest tick on SIGTERM/SIGINT, bounded by SHUTDOWN_DRAIN_TIMEOUT_SECS (default 25) before forcing exit. Previously the process exited immediately, which could drop requests or cut an ingest page mid-way. - fix(migrate-keys): persist the after_id cursor to a checkpoint file (bound to a fingerprint of the key pair) after each completed batch, resume from it on restart, log per-batch progress, and remove it on clean completion. Also reject --batch-size <= 0, which made the tool report "complete" without migrating anything. - fix(store): compare OTP code hashes with subtle::ConstantTimeEq instead of a short-circuiting string inequality. - fix(email): HTML-escape every externally sourced value (email address, asset code, destination, tx hash, failure reason) before interpolating it into templates. Closes Octo-Protocol-org#271 Closes Octo-Protocol-org#272 Closes Octo-Protocol-org#273 Closes Octo-Protocol-org#274
|
@graceuvala-collab 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! 🚀 |
…checkpoint-otp-email # Conflicts: # crates/store/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
Not tested. This PR is an implementation only. No tests were written or run (
cargo test,cargo check,cargo fmtandclippywere not run; no Rust toolchain was available). The test cases listed in #271–#274 still need to be added. Reviewers should build and run CI before merging.Closes #271
Closes #272
Closes #273
Closes #274
#271: Graceful shutdown on SIGTERM (
bin/server,crates/ingest)Audit: the process did not handle SIGTERM.
axum::serve(...).awaithad no shutdown hook, and the ingest supervisor was a detachedtokio::spawnrunningloop { tick; sleep }. So a SIGTERM killed it immediately, mid-request or mid-page.shutdown_signal()listens forSIGTERM(tokio::signal::unix) andSIGINT, and falls back to Ctrl-C only on non-unix.tokio_util::sync::CancellationTokendrives bothaxum::serve(..).with_graceful_shutdown(..)and a newSupervisor::run_until_cancelled. The supervisor only checks the token between ticks, so the current tick always finishes its page. Cancellation also cuts the inter-tick sleep short.Supervisor::runkeeps its old signature.SHUTDOWN_DRAIN_TIMEOUT_SECS(default 25, below k8s' 30 s grace period). After that the tasks are aborted and the process exits. A page cut short this way is safe because the cursor advances per record and deposit inserts are deduplicated on(tx_hash, op_index).shutdown signal received→draining …→ingest supervisor stopped …→drained(ordrain timeout elapsed; forcing exit) →exiting.README.mdand in.env.example.#272: Resumable key-rotation backfill (
bin/migrate-keys)Audit:
after_idlived only in memory, so an interrupted run started over from the beginning.migrate-keys.checkpoint(override withMIGRATE_KEYS_CHECKPOINT). It is written as a temp file and renamed, so a crash never leaves a half-written checkpoint. On startup the tool resumes from it, and it is removed on clean completion.(MASTER_KEY, MASTER_KEY_NEXT)pair (domain-separated; no key material). A checkpoint left over from a different rotation is refused. Without this, a stale cursor would silently skip unmigrated rows while the tool still reported "0 remaining". Malformed checkpoints are also refused, with a clear "delete it to restart" message.batches_completed,total_migrated,total_skipped,after_id) and a final summary.--batch-size 0(or a negative value) producedLIMIT 0, an empty first page, and a false "migration complete". It is now rejected.#274: Constant-time OTP hash compare (
crates/store)otp.code_hash != code_hashis replaced byotp_hash_matches, which usessubtle::ConstantTimeEq. OTP semantics (attempts, expiry, consumption, tx binding) are unchanged.subtlerather than a hand-rolled loop: it is already inCargo.lock(transitively viahmac, the same primitive behind theverify_slicechecks for JWT and webhook signatures), so it adds no new code to the supply chain. It is also well reviewed and guards against the compiler optimizing it into an early-exit comparison, which a manual loop can't guarantee.verify_slice.tx_hash_boundis a public tx hash, not a secret.#273: HTML-escape template values (
crates/email)Added
pub fn html_escape(escapes& < > " '; no new dependency) and a module-level escaping convention.otp_emailcodeotp_emailpurposewelcome_emailemail<"'&)withdrawal_success_email/withdrawal_failed_emailamount.only)assetdestinationwithdrawal_success_emailtx_hashwithdrawal_failed_emailreasondetailshell/socialsNo template currently interpolates a wallet label or username; the convention covers future additions.
Out-of-scope findings (reported, not fixed here)
list_wallets_needing_resealfilters onsealed_scheme <> SCHEME_V1, andSCHEME_V1is the only scheme. So aMASTER_KEY → MASTER_KEY_NEXTrotation under the same scheme would select zero rows and report success, and the server's scheme-based choice between old and new key can't tell migrated rows from unmigrated ones. Needs a key-version marker (for example asealed_key_idcolumn) rather than relying on the scheme tag. Worth a dedicated issue.email_otpscan invert every live code with a 10⁶-entry table. Suggest HMAC-SHA256 with a server secret inocto_email::hash_otp. This matters more than the timing fix in Use a constant-time comparison for OTP code-hash verification #274.