From 440528daad344300ca5b22ca4b5a77c9fa9ca9b1 Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:19:23 +0700 Subject: [PATCH] fix(kernel): require MFA step-up for inbound address rotation kernel.inbound.rotate becomes R3. Plan section 4.7 defines R3 as financial, legal or irreversible, and rotation is irreversible: the old address is tombstoned for good and the suppliers holding it are cut off. As R3 the runtime requires aal2 with a second factor in the last 5 minutes (P11) and the audit row records the assurance used, so a hijacked aal1 owner session can no longer cut off an org's inbound mail. For agents R3 means review, which R2 already required. The invariant suite gains a step-up test (aal1, stale and missing MFA timestamps refused with STEP_UP_REQUIRED and audited, fresh MFA rotates); the settings page tells the owner that MFA is needed. Part of #113. Co-Authored-By: Claude Opus 5.5 --- apps/app/src/routes/settings.tsx | 6 ++- apps/kernel/test/inbound-rotation.db.test.ts | 28 +++++++++--- .../invariant/inbound-rotation.db.test.ts | 44 +++++++++++++++++-- docs/plan/crove-os-kernel.md | 2 +- packages/kernel/src/commands.ts | 9 ++-- 5 files changed, 74 insertions(+), 15 deletions(-) diff --git a/apps/app/src/routes/settings.tsx b/apps/app/src/routes/settings.tsx index 8bd3caa4..9cf24769 100644 --- a/apps/app/src/routes/settings.tsx +++ b/apps/app/src/routes/settings.tsx @@ -86,7 +86,10 @@ function MemberControls({ member }: { member: MemberRow }) { ); } -// Owner only. One idempotency key per confirmation, so a retried request never rotates twice. +// Owner only. One idempotency key per confirmation, so a retried request never rotates twice. R3 (#113): without a +// second factor in the last 5 minutes the server answers STEP_UP_REQUIRED, shown below the buttons like any error; +// the key is kept until a rotation succeeds, so a retry after a refusal can still rotate at most once. The fresh +// MFA challenge screen (StepUpPrompt) lands with the SPA MFA flows (#32). function RotateInboundAddress() { const [confirming, setConfirming] = useState(false); const [idempotencyKey, setIdempotencyKey] = useState(() => crypto.randomUUID()); @@ -130,6 +133,7 @@ function RotateInboundAddress() { Địa chỉ hiện tại sẽ ngừng nhận thư ngay lập tức và không bao giờ được cấp lại. Thư gửi tới địa chỉ cũ sẽ bị bỏ qua mà không báo lỗi cho người gửi, vì vậy hãy gửi địa chỉ mới cho các nhà cung cấp.

+

Việc này cần xác thực hai lớp (MFA) trong 5 phút gần nhất. Mỗi tổ chức chỉ đổi được vài lần mỗi ngày.