feat(email-recovery): frontend wizard + EMAIL_RECOVERY flag (beta.id.ai default-on)#3844
Closed
sea-snake wants to merge 21 commits into
Closed
feat(email-recovery): frontend wizard + EMAIL_RECOVERY flag (beta.id.ai default-on)#3844sea-snake wants to merge 21 commits into
sea-snake wants to merge 21 commits into
Conversation
This was referenced May 6, 2026
Closed
Contributor
There was a problem hiding this comment.
Pull request overview
This PR (stacked on prior email-recovery PRs) wires up the frontend email-recovery setup + recovery wizards, adds the EMAIL_RECOVERY feature flag (default-off, auto-enabled on beta.id.ai), and includes supporting interface/backend changes so the UI can render email-recovery state and drive the canister recovery flow.
Changes:
- Add Svelte wizards for setup (bind address + send magic email + polling) and recovery (anonymous flow producing a
SignedDelegation). - Add a DNSSEC proof assembly client (wire-format DoH, parsing, chain walking) for caller-provided DNS proofs, with fallback behavior for unsigned domains.
- Extend interface/backend types to surface email recovery credential state in
IdentityInfoand support the end-to-end email recovery flows.
Reviewed changes
Copilot reviewed 90 out of 93 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
src/frontend/src/lib/state/featureFlags.ts |
Adds EMAIL_RECOVERY feature flag and environment-based override behavior. |
src/frontend/src/routes/(new-styling)/manage/(authenticated)/(access-and-recovery)/recovery/+page.svelte |
Adds/manage-page wiring for the email-recovery card and setup entry point. |
src/frontend/src/routes/(new-styling)/manage/(authenticated)/(access-and-recovery)/recovery/components/ActiveEmailRecovery.svelte |
Renders active/bound email-recovery credential details and actions. |
src/frontend/src/routes/(new-styling)/manage/(authenticated)/(access-and-recovery)/recovery/components/InactiveEmailRecovery.svelte |
Renders inactive state and entry point for setting up email recovery. |
src/frontend/src/routes/(new-styling)/recovery/+page.svelte |
Adds “Recover with email” entry point and session establishment from email-recovery delegation. |
src/frontend/src/lib/components/wizards/setupEmailRecovery/index.ts |
Setup wizard exports and integration surface. |
src/frontend/src/lib/components/wizards/setupEmailRecovery/SetupEmailRecoveryWizard.svelte |
Multi-step setup flow orchestration (prepare/add, polling, countdown UX). |
src/frontend/src/lib/components/wizards/setupEmailRecovery/views/EnterAddress.svelte |
Collects user email address for setup flow. |
src/frontend/src/lib/components/wizards/setupEmailRecovery/views/SendMagicEmail.svelte |
Displays “send email” step with countdown/polling UX. |
src/frontend/src/lib/components/wizards/setupEmailRecovery/views/Done.svelte |
Setup completion view. |
src/frontend/src/lib/components/wizards/setupEmailRecovery/views/FailedView.svelte |
Setup failure view and reason display. |
src/frontend/src/lib/components/wizards/recoverWithEmail/index.ts |
Recovery wizard exports and integration surface. |
src/frontend/src/lib/components/wizards/recoverWithEmail/RecoverWithEmailWizard.svelte |
Multi-step recovery flow orchestration (prepare/status/get delegation). |
src/frontend/src/lib/components/wizards/recoverWithEmail/views/EnterAddressForRecovery.svelte |
Collects user email address for recovery flow. |
src/frontend/src/lib/utils/dnssec/doh.ts |
Wire-format DoH client used for DNSSEC proof assembly. |
src/frontend/src/lib/utils/dnssec/index.ts |
DNS message parsing + proof bundle construction utilities. |
src/frontend/src/lib/utils/dnssec/rrsig.ts |
RRSIG parsing / key-tag computation utilities for DNSSEC assembly. |
src/frontend/tests/e2e-playwright/routes/emailRecovery.spec.ts |
Playwright coverage for feature-flag gating and wizard mounting. |
src/frontend/tests/e2e-playwright/fixtures/emailRecovery.ts |
E2E fixture helpers for email recovery flows. |
src/frontend/tests/e2e-playwright/fixtures/index.ts |
Fixture exports wiring for Playwright tests. |
src/frontend/src/routes/vc-flow/index/+page.svelte |
Adjustments related to routing/feature exposure in the frontend. |
src/frontend/src/lib/utils/iiConnection.test.ts |
Test updates related to connection/util behavior affected by new flows. |
src/internet_identity_interface/src/internet_identity/types/email_recovery.rs |
Interface types for email recovery flows and errors. |
src/internet_identity_interface/src/internet_identity/types/api_v2.rs |
API surface updates to include email recovery in identity info / v2 types. |
src/internet_identity_interface/src/internet_identity/types.rs |
Top-level type wiring changes for new modules/types. |
src/internet_identity_interface/src/internet_identity/types/dnssec.rs |
Interface types for DNSSEC proof bundles and related structures. |
src/internet_identity_interface/src/internet_identity/types/doh.rs |
Interface types/config for DoH fallback configuration. |
src/internet_identity_interface/src/archive/types.rs |
Archive/interface adjustments due to expanded types. |
src/internet_identity/internet_identity.did |
Candid interface updates for the new email recovery and related config/types. |
src/archive/archive.did |
Archive candid updates aligned with interface/type changes. |
src/internet_identity/Cargo.toml |
Backend dependencies/config updated for added verifier modules. |
Cargo.toml |
Workspace-level dependency/config updates. |
Cargo.lock |
Lockfile updates for new/updated dependencies. |
src/internet_identity/src/email_recovery/mod.rs |
Email recovery module wiring. |
src/internet_identity/src/email_recovery/prepare.rs |
Canister-side prepare logic for setup/recovery challenges. |
src/internet_identity/src/email_recovery/pending.rs |
Pending challenge tracking/state for email recovery flows. |
src/internet_identity/src/email_recovery/smtp.rs |
SMTP request handling and binding/recovery completion logic. |
src/internet_identity/src/email_recovery/remove.rs |
Removal/unbind logic for email recovery credentials. |
src/internet_identity/src/email_recovery/rng.rs |
RNG utilities for challenges/tokens. |
src/internet_identity/src/dnssec/mod.rs |
DNSSEC verifier module wiring. |
src/internet_identity/src/dnssec/types.rs |
DNSSEC proof/record types for verification. |
src/internet_identity/src/dnssec/canonical.rs |
Canonical encoding primitives for DNSSEC verification. |
src/internet_identity/src/dnssec/signature.rs |
DNSSEC signature verification primitives. |
src/internet_identity/src/dnssec/verify.rs |
End-to-end DNSSEC chain + leaf verification logic. |
src/internet_identity/src/dnssec/test_vectors.rs |
DNSSEC verification test vectors + harness. |
src/internet_identity/src/dkim/mod.rs |
DKIM verifier module wiring. |
src/internet_identity/src/dkim/types.rs |
DKIM verification result/error types. |
src/internet_identity/src/dkim/parse.rs |
DKIM-Signature parsing. |
src/internet_identity/src/dkim/canonicalize.rs |
DKIM header/body canonicalization. |
src/internet_identity/src/dkim/dns_record.rs |
DKIM TXT record parsing. |
src/internet_identity/src/dkim/signature.rs |
DKIM signature verification. |
src/internet_identity/src/dkim/verify.rs |
DKIM orchestration/verification pipeline. |
src/internet_identity/src/dkim/test_vectors.rs |
DKIM test vector harness. |
src/internet_identity/src/dmarc/mod.rs |
DMARC verifier module wiring. |
src/internet_identity/src/dmarc/types.rs |
DMARC types/outcomes/policy structures. |
src/internet_identity/src/dmarc/from_header.rs |
From-header parsing for DMARC alignment. |
src/internet_identity/src/dmarc/alignment.rs |
DMARC alignment evaluation logic. |
src/internet_identity/src/dmarc/parse.rs |
DMARC TXT parsing. |
src/internet_identity/src/dmarc/verify.rs |
DMARC orchestration (DKIM + From + alignment). |
src/internet_identity/src/dmarc/test_vectors.rs |
DMARC test vector harness. |
src/internet_identity/src/doh/types.rs |
DoH module types for config/errors. |
src/internet_identity/src/storage.rs |
Storage wiring updated for email-recovery credential/index. |
src/internet_identity/src/storage/storable.rs |
Storable types wiring for new persisted data. |
src/internet_identity/src/storage/storable/anchor.rs |
Anchor schema update to include email recovery credential field. |
src/internet_identity/src/storage/storable/email_recovery_credential.rs |
Persistent representation of bound email recovery credential. |
src/internet_identity/src/storage/storable/email_recovery_address_hash.rs |
Address-hash type for reverse index (address → anchor). |
src/internet_identity/src/storage/storable/storable_persistent_state.rs |
Persistent state updates for new configs/data. |
src/internet_identity/src/storage/anchor.rs |
Anchor storage logic updated for email recovery fields. |
src/internet_identity/src/storage/anchor/tests.rs |
Storage tests updated for new anchor/email-recovery behavior. |
src/internet_identity/src/storage/tests.rs |
Storage-level tests updated for new structures/indices. |
src/internet_identity/src/state.rs |
Persistent state wiring updates (dnssec/doh/email recovery config). |
src/internet_identity/tests/integration/main.rs |
Integration test updates covering new canister behavior. |
src/canister_tests/src/api/internet_identity.rs |
Canister-tests API updates for new endpoints/types. |
src/canister_tests/src/api/archive.rs |
Canister-tests API updates for archive/type changes. |
test_vectors/dnssec/iana-root-anchors-2026-05.json |
Root trust anchor test vector(s) for DNSSEC verification. |
test_vectors/dkim/README.md |
DKIM test vector provenance/docs. |
test_vectors/dkim/synth-rsa-test1._domainkey.test.example.com.txt |
DKIM TXT record test vector. |
test_vectors/dkim/synth-rsa-simple-simple.eml |
DKIM signed email fixture (simple/simple). |
test_vectors/dkim/synth-rsa-relaxed-simple.eml |
DKIM signed email fixture (relaxed/simple). |
test_vectors/dkim/synth-rsa-relaxed-relaxed.eml |
DKIM signed email fixture (relaxed/relaxed). |
scripts/capture-dnssec-chain.py |
Script to capture DNSSEC chains for reproducible test vectors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
9e42d96 to
abdcf07
Compare
9 tasks
b8abb80 to
6f437cc
Compare
2 tasks
4a596a2 to
0af14dd
Compare
Brings the recovery half (`prepare_delegation` → `smtp_request` →
`submit_dkim_leaf` (DNSSEC) or finished synchronously (DoH) →
`get_delegation`) onto the new two-phase storage-and-smtp base. The
recovery flow shares all the heavy lifting with the setup flow —
prepare validation, skeleton chain caching, DKIM-signature parsing,
body-hash check, partial-verification stash, leaf admission, DKIM
crypto verify, DMARC alignment — and only diverges at finalization.
Setup and recovery now share:
- One `prepare_common` validation core. Setup parks
`PendingKind::Register{anchor}`; recovery parks
`PendingKind::Recover{session_pk}` after capping `session_pk` to
`MAX_SESSION_KEY_BYTES = 1024`.
- One `smtp_request` dispatcher. Recipient (`register@id.ai` vs
`recover@id.ai`) is cross-checked against the entry's
`PendingKind` so a forged `to:` can't run the wrong flow.
- One DNSSEC partial-verification path: parse → bh= → digest →
cache → flip `NeedDkimLeaf{selector}`. Recovery is no different
from setup at this step.
- One `submit_dkim_leaf` path: leaf admission against the cached
zone DNSKEY, DKIM crypto verify (prehash), DMARC alignment.
What diverges at finalization:
- **Setup**: `bind_credential(anchor, address)` writes the
`EmailRecoveryCredential` to the named anchor.
- **Recovery**: `stamp_recovery_delegation` looks up the anchor
from the verified `From:` via the reverse-address index
(memory ID 24, hashed-key map), derives the seed
`H(salt || "email-recovery" || lowercase(address) || anchor)`,
adds the canister signature for `(session_pk, expiration)`, and
caches a `RecoveryOutcome { user_key, expiration, anchor_number,
seed }` on the pending entry. Polling then surfaces
`RecoveryReady{user_key, expiration, anchor_number}`.
This finalization fork lives in two places:
- `smtp.rs` for the DoH path (verification finishes synchronously
inside `smtp_request`).
- `submit_leaf.rs` for the DNSSEC path (verification finishes
inside `email_recovery_submit_dkim_leaf` after the FE submits
the leaf).
Both call `stamp_recovery_delegation` on the recovery branch and
`bind_credential` on the setup branch.
Other scoped pieces:
- `EmailRecoveryStatus::RecoveryReady` carries `anchor_number` so
the FE seeds its auth store without a separate lookup.
- `PendingChallenge.recovery_outcome: Option<RecoveryOutcome>`
caches the seed + anchor + user_key for `get_delegation`.
- `email_recovery_get_delegation(args)` query mirrors
`openid_get_delegation` in shape — uses the cached seed to
retrieve the canister signature.
- `recovery_seed_for_nonce(nonce)` exposes the cached seed to
`get_delegation` without re-deriving from the anchor.
- Reverse address index: `SHA-256(lowercase(address)) →
AnchorNumber`, memory ID 24, kept in sync with anchor writes.
- `IdentityInfo.email_recovery: Option<EmailRecoveryCredential>`
surfaced on `identity_info` so the manage page renders the
recovery-email card without a second canister call.
- `check_authorization` recognises an additional principal kind:
delegations rooted in `H(salt || "email-recovery" ||
lowercase(address) || anchor)` are accepted as authenticating
the matching anchor. After a recovery completes the FE's
session keypair holds such a delegation; this lets the user
call `identity_info` and the rest of the authenticated surface
immediately, without re-mint.
- Archive operations: payload-free `AddEmailRecovery` /
`RemoveEmailRecovery` variants on `Operation` so audit
consumers can answer "who changed their recovery email when?"
without leaking the address (§8.2).
- Activity-stats counter for email-recovery delegation issuance,
alongside the existing per-issuer OpenID counter.
- `archive.did`, `internet_identity.did` updated; FE bindings
regenerated.
444 unit tests pass (3 new). The integration-test happy-path
fixture still needs a follow-up rewrite to exercise the new
two-phase shape end-to-end (prepare → smtp_request[NeedDkimLeaf]
→ submit_dkim_leaf), but compiles clean against the new types.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings the recovery half (`prepare_delegation` → `smtp_request` →
`submit_dkim_leaf` (DNSSEC) or finished synchronously (DoH) →
`get_delegation`) onto the new two-phase storage-and-smtp base. The
recovery flow shares all the heavy lifting with the setup flow —
prepare validation, skeleton chain caching, DKIM-signature parsing,
body-hash check, partial-verification stash, leaf admission, DKIM
crypto verify, DMARC alignment — and only diverges at finalization.
Setup and recovery now share:
- One `prepare_common` validation core. Setup parks
`PendingKind::Register{anchor}`; recovery parks
`PendingKind::Recover{session_pk}` after capping `session_pk` to
`MAX_SESSION_KEY_BYTES = 1024`.
- One `smtp_request` dispatcher. Recipient (`register@id.ai` vs
`recover@id.ai`) is cross-checked against the entry's
`PendingKind` so a forged `to:` can't run the wrong flow.
- One DNSSEC partial-verification path: parse → bh= → digest →
cache → flip `NeedDkimLeaf{selector}`. Recovery is no different
from setup at this step.
- One `submit_dkim_leaf` path: leaf admission against the cached
zone DNSKEY, DKIM crypto verify (prehash), DMARC alignment.
What diverges at finalization:
- **Setup**: `bind_credential(anchor, address)` writes the
`EmailRecoveryCredential` to the named anchor.
- **Recovery**: `stamp_recovery_delegation` looks up the anchor
from the verified `From:` via the reverse-address index
(memory ID 24, hashed-key map), derives the seed
`H(salt || "email-recovery" || lowercase(address) || anchor)`,
adds the canister signature for `(session_pk, expiration)`, and
caches a `RecoveryOutcome { user_key, expiration, anchor_number,
seed }` on the pending entry. Polling then surfaces
`RecoveryReady{user_key, expiration, anchor_number}`.
This finalization fork lives in two places:
- `smtp.rs` for the DoH path (verification finishes synchronously
inside `smtp_request`).
- `submit_leaf.rs` for the DNSSEC path (verification finishes
inside `email_recovery_submit_dkim_leaf` after the FE submits
the leaf).
Both call `stamp_recovery_delegation` on the recovery branch and
`bind_credential` on the setup branch.
Other scoped pieces:
- `EmailRecoveryStatus::RecoveryReady` carries `anchor_number` so
the FE seeds its auth store without a separate lookup.
- `PendingChallenge.recovery_outcome: Option<RecoveryOutcome>`
caches the seed + anchor + user_key for `get_delegation`.
- `email_recovery_get_delegation(args)` query mirrors
`openid_get_delegation` in shape — uses the cached seed to
retrieve the canister signature.
- `recovery_seed_for_nonce(nonce)` exposes the cached seed to
`get_delegation` without re-deriving from the anchor.
- Reverse address index: `SHA-256(lowercase(address)) →
AnchorNumber`, memory ID 24, kept in sync with anchor writes.
- `IdentityInfo.email_recovery: Option<EmailRecoveryCredential>`
surfaced on `identity_info` so the manage page renders the
recovery-email card without a second canister call.
- `check_authorization` recognises an additional principal kind:
delegations rooted in `H(salt || "email-recovery" ||
lowercase(address) || anchor)` are accepted as authenticating
the matching anchor. After a recovery completes the FE's
session keypair holds such a delegation; this lets the user
call `identity_info` and the rest of the authenticated surface
immediately, without re-mint.
- Archive operations: payload-free `AddEmailRecovery` /
`RemoveEmailRecovery` variants on `Operation` so audit
consumers can answer "who changed their recovery email when?"
without leaking the address (§8.2).
- Activity-stats counter for email-recovery delegation issuance,
alongside the existing per-issuer OpenID counter.
- `archive.did`, `internet_identity.did` updated; FE bindings
regenerated.
444 unit tests pass (3 new). The integration-test happy-path
fixture still needs a follow-up rewrite to exercise the new
two-phase shape end-to-end (prepare → smtp_request[NeedDkimLeaf]
→ submit_dkim_leaf), but compiles clean against the new types.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Brings the recovery half (`prepare_delegation` → `smtp_request` →
`submit_dkim_leaf` (DNSSEC) or finished synchronously (DoH) →
`get_delegation`) onto the new two-phase storage-and-smtp base. The
recovery flow shares all the heavy lifting with the setup flow —
prepare validation, skeleton chain caching, DKIM-signature parsing,
body-hash check, partial-verification stash, leaf admission, DKIM
crypto verify, DMARC alignment — and only diverges at finalization.
Setup and recovery now share:
- One `prepare_common` validation core. Setup parks
`PendingKind::Register{anchor}`; recovery parks
`PendingKind::Recover{session_pk}` after capping `session_pk` to
`MAX_SESSION_KEY_BYTES = 1024`.
- One `smtp_request` dispatcher. Recipient (`register@id.ai` vs
`recover@id.ai`) is cross-checked against the entry's
`PendingKind` so a forged `to:` can't run the wrong flow.
- One DNSSEC partial-verification path: parse → bh= → digest →
cache → flip `NeedDkimLeaf{selector}`. Recovery is no different
from setup at this step.
- One `submit_dkim_leaf` path: leaf admission against the cached
zone DNSKEY, DKIM crypto verify (prehash), DMARC alignment.
What diverges at finalization:
- **Setup**: `bind_credential(anchor, address)` writes the
`EmailRecoveryCredential` to the named anchor.
- **Recovery**: `stamp_recovery_delegation` looks up the anchor
from the verified `From:` via the reverse-address index
(memory ID 24, hashed-key map), derives the seed
`H(salt || "email-recovery" || lowercase(address) || anchor)`,
adds the canister signature for `(session_pk, expiration)`, and
caches a `RecoveryOutcome { user_key, expiration, anchor_number,
seed }` on the pending entry. Polling then surfaces
`RecoveryReady{user_key, expiration, anchor_number}`.
This finalization fork lives in two places:
- `smtp.rs` for the DoH path (verification finishes synchronously
inside `smtp_request`).
- `submit_leaf.rs` for the DNSSEC path (verification finishes
inside `email_recovery_submit_dkim_leaf` after the FE submits
the leaf).
Both call `stamp_recovery_delegation` on the recovery branch and
`bind_credential` on the setup branch.
Other scoped pieces:
- `EmailRecoveryStatus::RecoveryReady` carries `anchor_number` so
the FE seeds its auth store without a separate lookup.
- `PendingChallenge.recovery_outcome: Option<RecoveryOutcome>`
caches the seed + anchor + user_key for `get_delegation`.
- `email_recovery_get_delegation(args)` query mirrors
`openid_get_delegation` in shape — uses the cached seed to
retrieve the canister signature.
- `recovery_seed_for_nonce(nonce)` exposes the cached seed to
`get_delegation` without re-deriving from the anchor.
- Reverse address index: `SHA-256(lowercase(address)) →
AnchorNumber`, memory ID 24, kept in sync with anchor writes.
- `IdentityInfo.email_recovery: Option<EmailRecoveryCredential>`
surfaced on `identity_info` so the manage page renders the
recovery-email card without a second canister call.
- `check_authorization` recognises an additional principal kind:
delegations rooted in `H(salt || "email-recovery" ||
lowercase(address) || anchor)` are accepted as authenticating
the matching anchor. After a recovery completes the FE's
session keypair holds such a delegation; this lets the user
call `identity_info` and the rest of the authenticated surface
immediately, without re-mint.
- Archive operations: payload-free `AddEmailRecovery` /
`RemoveEmailRecovery` variants on `Operation` so audit
consumers can answer "who changed their recovery email when?"
without leaking the address (§8.2).
- Activity-stats counter for email-recovery delegation issuance,
alongside the existing per-issuer OpenID counter.
- `archive.did`, `internet_identity.did` updated; FE bindings
regenerated.
444 unit tests pass (3 new). The integration-test happy-path
fixture still needs a follow-up rewrite to exercise the new
two-phase shape end-to-end (prepare → smtp_request[NeedDkimLeaf]
→ submit_dkim_leaf), but compiles clean against the new types.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`Button.svelte` is gone on `main` (migrated to a `btn` utility class on plain `<button>` elements); this PR's branch hasn't merged that change yet. The new email-recovery wizard files were copy-pasting the `<Button>` import that worked on the branch but not against the PR's merged state, so the docker frontend build (and svelte-check) failed on every CI run after the migration landed. Migrate the 7 new wizard views to the post-migration shape, matching how `InactiveRecoveryPhrase` / `ActiveRecoveryPhrase` look on main. Existing files that still import `<Button>` on this branch are intentionally untouched — main's btn-class versions win on merge.
ESLint flagged six new violations the FE PR's pre-existing checks don't accept: - `prepareEmailDelegation` / `getEmailDelegation` / `prepareAddEmail` / `emailRecoveryStatus` / `statusEmailRecovery` were declared `async` but only forwarded a `.then()` chain — drop the redundant `async`. - `dnssecTestSigner.rootAnchor` was async but doesn't `await`; same fix. - `dkimTestSigner.signEmail` used `||` to default `fromDomain` / `toDomain` — explicit empty-string check instead, which the `strict-boolean-expressions` rule requires.
The interceptor reads rtype from the SignedRRset bundle directly and only constructs RRSIG records itself. ESLint --max-warnings 0 flagged the unused TYPE_TXT / TYPE_DNSKEY / TYPE_DS constants.
CI runs prettier --check; the new FE files weren't all formatted to the project's prettier config. Auto-format them.
The canister keeps a global `address → anchor` reverse index, so two tests reusing the same address against the same canister deployment collide on `bind_credential_to_anchor`'s same-anchor check after the first run. CI hides this because each shard runs against its own canister, but local serial reruns surface the collision. Generate `alice-<rand>@<test_domain>` per fixture instance so the test stays isolated from prior bindings.
The recovery sign-in page renamed the "Get started" button to "Recover with phrase" when the email-recovery sibling was added (see `frontend wizard + EMAIL_RECOVERY flag` commit). The fixture in `tests/e2e-playwright/fixtures/recoveryPage.ts` was missed — every recovery-phrase wizard test that goes through `/recovery` times out waiting for the old label. Match the new label.
Drops `discoverSelector` and `SELECTOR_CANDIDATES` entirely — the
DKIM selector is now read from the email itself by the canister and
returned to the FE via the `NeedDkimLeaf { selector }` mid-flow
status. The FE then walks just that one DNSSEC leaf and submits it
via `email_recovery_submit_dkim_leaf`. No more probing.
What landed:
- **`lib/utils/dnssec/`** split:
- `walkSkeletonChain(domain, wantDmarc)` — chain rooted at IANA
+ optional DMARC leaf at `_dmarc.<domain>`. No DKIM.
- `walkDkimLeaf(domain, selector)` — chain + DKIM TXT leaf for
the post-email selector. Returns the bundle to submit and
the leaf separately.
- Shared `walkUpFromLeafZone` helper — both walks share the
bottom-up RRSIG-walk to root.
- Public re-exports renamed to `assembleSkeleton` /
`assembleDkimLeaf`. `discoverSelector` and the
`SELECTOR_CANDIDATES` table are gone.
- **`SetupEmailRecoveryWizard`** + **`RecoverWithEmailWizard`**:
- Drop `discoverSelector` call site.
- Drop the `selector` field from the `EmailRecoveryDnsInput`
sent to prepare.
- Polling loop watches for `NeedDkimLeaf { selector }`:
when it lands, walk the DKIM leaf (`assembleDkimLeaf`) and
call `submitDkimLeaf({ nonce, dkim_leaf })`. The submission
response carries the post-call status (`RegistrationSucceeded`
for setup, `RecoveryReady{...}` for recovery), so the FE can
finish the wizard one round-trip earlier on the happy path.
- Failed-status copy: drop `SelectorMismatch`; add the new
`DkimLeafMismatch` and `NoDkimLeafExpected` variants.
- **Host pages** (`recovery/+page.svelte` and the manage page's
recovery sub-route): add an anonymous-actor wrapper for
`email_recovery_submit_dkim_leaf` and pass it as the new
`submitDkimLeaf` prop to the corresponding wizard.
- **Test fixtures** (`dnssecTestSigner` + `dohRouteInterceptor` +
`emailRecovery` fixture):
- `buildChain` returns `{ anchor, skeleton, dkimLeaf, dmarcLeaf }`
separately so callers compose single-leaf bundles for whichever
phase they're testing.
- `installDohInterceptor(page, bundle, extraLeaves[])` —
accepts extra leaves on top of the skeleton bundle's chain
so the DoH layer answers DKIM-leaf queries from the
post-email walk too.
- The `EmailRecoveryFixtures` ctx now holds skeleton + dkimLeaf
+ signer; the interceptor publishes both.
FE type-check passes (0 errors). Canister-side unit tests still
pass (444). The FE wizard end-to-end will exercise the full
two-phase path once the e2e DoH interceptor's now-extra-leaf hookup
is run by the playwright suite.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`assembleSkeleton` and `assembleDkimLeaf` are thin wrappers that just return the inner walker's promise — there's nothing to `await`. ESLint's `require-await` rule (enforced as error in CI) flagged both. Drop the redundant `async` keywords; the call-site behaviour is identical (returning a `Promise<…>` either way). Also: trailing rebase fmt drift across `pending.rs` and `api_v2.rs` — `cargo fmt` noop, just whitespace.
CI's prettier --check flagged three files modified by the FE two-phase rewrite. Re-apply prettier formatting; logic unchanged.
Real-world DKIM resolution often crosses zone boundaries via CNAME —
proton.me → proton.ch, tutanota.com → tutanota.de, M365 custom
domains, etc. DNSSEC signatures don't span zones, so each hop in
the chain must be authenticated by its own zone's DNSKEY.
The FE walker now follows CNAMEs at submit time and supplies any
*additional* delegation chains for zones that weren't already
covered by the prepare bundle.
Public surface:
- `assembleSkeleton(domain, wantDmarc)` — same purpose, returns the
new bundle shape (`chains`, `hops`).
- `assembleDkimResolution(domain, selector)` — replaces
`assembleDkimLeaf`. Returns `{ hops, extraChains }` for the new
`email_recovery_submit_dkim_leaf` API.
Internals (chain.ts):
- `walkCnameChain(name)` — fetches each hop one at a time, picking
TXT or CNAME based on what the zone publishes. Loops, oversize
chains, and CNAMEs into unsigned territory (live.com case) all
surface as `undefined`, which the caller turns into a no-bundle
prepare so the canister falls through to DoH.
- `walkChainForZone(zoneBytes)` — walks a delegation chain root
→ … → zone, used to populate `extraChains` for any new signing
zone the resolution crossed into.
Generated bindings regenerated from the updated `.did` via `npm run
generate:types && npm run generate:js`.
The off-chain SMTP gateway calls a query method at RCPT TO time, before it pulls the message body from the sending MTA, to decide whether to accept the connection at all. The PoC defined this as \`smtp_request_validate\` and gated acceptance on the user-part parsing as a numeric anchor number — so on the email-recovery deploy, the gateway rejects \`register@id.ai\` and \`recover@id.ai\` outright, and \`smtp_request\` is never called. Bring the query back, with the email-recovery-specific shape: - \`register@id.ai\` (case-insensitive) → Ok. - \`recover@id.ai\` (case-insensitive) → Ok. Recognised at the validate query even on this PR (where the actual handler lives in the recovery follow-up dfinity#3843), since a "yes accept" here with no handler is harmless and avoids a deploy-step ordering constraint between this PR and the recovery one. - Everything else → 550 (mailbox unavailable). Numeric postbox- style addresses are no longer handled; the gateway should bounce them. The query is open (anyone can call it) but has no side effects and leaks nothing beyond the two recipient labels themselves, both already documented in the design doc. Six new unit tests cover register / recover / case-insensitivity, unknown user, known user with wrong domain, missing envelope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The off-chain SMTP gateway routes mail at a domain that varies per deploy: id.ai on prod, beta.id.ai on beta. The canister was hardcoding `id.ai` everywhere — recipient dispatch, the validate query, and the user-facing label returned from prepare — so on the beta canister mail to `register@beta.id.ai` reached the canister but failed the recipient match and was silently dropped. Drop the hardcoded constant. Derive the accepted mailbox domains from `related_origins`, which is already a per-deploy arg the deploy scripts wire through (and the same one used for security headers + the FE's `getPrimaryOrigin`). All entries are treated as equal aliases — recipient dispatch and the `smtp_request_validate` query accept `register@<host>` / `recover@<host>` for any host listed in `related_origins`. So a prod deploy with `id.ai` + the `*.icp0.io` aliases accepts mail at all of them; a beta deploy with `beta.id.ai` accepts that one. Drop the `mailbox` field from `EmailRecoveryChallenge` too. The FE already knows which origin the user is on (`window.location.hostname`), so it pairs that with `register` / `recover` to render the label — each tab automatically shows the alias matching the origin the user is on, and the canister never has to single one out as canonical. Empty / unset `related_origins` → no domains accepted; the canister drops every inbound recipient. Real deploys always configure this. Tests: extended `email_recovery::smtp::tests` with `set_related_origins` helper + 15 cases (single host, multi-alias prod, beta-only, unknown user, wrong domain, no-origins-configured); all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The canister no longer returns a \`mailbox\` field on the prepare response — all \`related_origins\` aliases are accepted equally on the SMTP side, and the FE picks the user-facing label by pairing the recipient user-part (\`register\` / \`recover\`) with \`window.location.hostname\`. Each tab automatically shows the alias matching the origin the user is on, and the canister never has to single one out as canonical. Generated TS bindings regenerated from the updated \`.did\` (the \`EmailRecoveryChallenge\` type loses its \`mailbox\` field). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ubmitDkimLeaf shape
0af14dd to
cfdaa12
Compare
Contributor
Author
|
Replaced with a new PR from an upstream branch (enables direct collaboration). Same content, new PR number. |
This was referenced May 12, 2026
Merged
sea-snake
added a commit
that referenced
this pull request
May 12, 2026
…l args Email recovery (PRs #3838-#3844) needs two new BE init args set on the canister: `dnssec_config.root_anchors` (the IANA root KSKs the DNSSEC verifier trusts) and `doh_config.allowed_domains` (the mailbox-provider domains that can use the DoH-fallback path). This PR plumbs both through the three deploy scripts. What landed: - **`scripts/fetch-iana-root-anchors.bash`** — new module, sourced by the other deploy scripts. `fetch_and_review_iana_root_anchors`: - `curl`s `data.iana.org/root-anchors/root-anchors.xml` (retry + bounded timeout). - Parses every `<KeyDigest>` block with Node, filtering to those whose `validFrom`/`validUntil` window contains "now" — drops retired keys (Kjqmt7v expired 2019) and keeps the active KSKs (Klajeyz key_tag=20326, Kmyv6jo key_tag=38696 today). - Prints a one-line human summary per entry to stderr (id + key_tag + algo + digest_type + digest preview + validity window). - Prompts for confirmation on /dev/tty (gracefully falls back to "accept default" when there's no controlling terminal, so CI / sandboxes don't deadlock). - Writes Candid `vec record { key_tag = ...; algorithm = ...; digest_type = ...; digest = blob "..."; }` to stdout, ready to drop into `dnssec_config.root_anchors`. - **`scripts/deploy-common.bash`** (consumed by `deploy-pr-to-beta` and `deploy-local-to-beta`): - New `DEFAULT_DOH_ALLOWED_DOMAINS` list — gmail, googlemail, outlook, hotmail, live, icloud, me, mac, yahoo, protonmail, proton.me, pm.me. Adding a domain is operator config, not code; override per-deploy with `--doh-domains <a,b,c,...>`. Empty list = DNSSEC-only, no DoH fallback. - New `--skip-email-recovery-init` flag — when set, leave both fields as `opt null` (preserve previous on-chain values). Default is to fetch fresh anchors + use the curated DoH allowlist, since the staging canisters are still in the "needs first-run init" stage. - `build_be_install_arg` now emits `dnssec_config = ...` and `doh_config = ...` alongside the existing trio (BE id, BE_URL, FE_URL). - **`scripts/make-upgrade-proposal`** (production): didc-assist's interactive flow doesn't know how to type a 32-byte SHA-256 digest as Candid blob, so we *post-process* the args.txt it produces: - After didc-assist writes the file (or the trivial `(null)` when the user accepts the no-update default), call `patch_email_recovery_init_fields`. - The patcher uses Node to find/replace the `dnssec_config` and `doh_config` fields in the Candid arg text — or insert them inside the outermost `record { ... }` if didc-assist didn't emit them — with values from the same IANA fetch + DoH allowlist. The user can opt out with `II_SKIP_EMAIL_RECOVERY_INIT=1` for upgrades where the on-chain values are already correct, and override the DoH allowlist via `II_DOH_DOMAINS=...`. - Both staging and production paths share the patcher; encoded Candid round-trips through `didc encode` cleanly in all three edge cases tested locally: existing fields = null, fields absent, args.txt = `(null)`. The reason we don't bundle the IANA anchors into the wasm is that they roll over every ~7 years; the next rollover becomes a one-line change in the next upgrade arg instead of a code change + recompile.
sea-snake
added a commit
that referenced
this pull request
May 12, 2026
…l args Email recovery (PRs #3838-#3844) needs two new BE init args set on the canister: `dnssec_config.root_anchors` (the IANA root KSKs the DNSSEC verifier trusts) and `doh_config.allowed_domains` (the mailbox-provider domains that can use the DoH-fallback path). This PR plumbs both through the three deploy scripts. What landed: - **`scripts/fetch-iana-root-anchors.bash`** — new module, sourced by the other deploy scripts. `fetch_and_review_iana_root_anchors`: - `curl`s `data.iana.org/root-anchors/root-anchors.xml` (retry + bounded timeout). - Parses every `<KeyDigest>` block with Node, filtering to those whose `validFrom`/`validUntil` window contains "now" — drops retired keys (Kjqmt7v expired 2019) and keeps the active KSKs (Klajeyz key_tag=20326, Kmyv6jo key_tag=38696 today). - Prints a one-line human summary per entry to stderr (id + key_tag + algo + digest_type + digest preview + validity window). - Prompts for confirmation on /dev/tty (gracefully falls back to "accept default" when there's no controlling terminal, so CI / sandboxes don't deadlock). - Writes Candid `vec record { key_tag = ...; algorithm = ...; digest_type = ...; digest = blob "..."; }` to stdout, ready to drop into `dnssec_config.root_anchors`. - **`scripts/deploy-common.bash`** (consumed by `deploy-pr-to-beta` and `deploy-local-to-beta`): - New `DEFAULT_DOH_ALLOWED_DOMAINS` list — gmail, googlemail, outlook, hotmail, live, icloud, me, mac, yahoo, protonmail, proton.me, pm.me. Adding a domain is operator config, not code; override per-deploy with `--doh-domains <a,b,c,...>`. Empty list = DNSSEC-only, no DoH fallback. - New `--skip-email-recovery-init` flag — when set, leave both fields as `opt null` (preserve previous on-chain values). Default is to fetch fresh anchors + use the curated DoH allowlist, since the staging canisters are still in the "needs first-run init" stage. - `build_be_install_arg` now emits `dnssec_config = ...` and `doh_config = ...` alongside the existing trio (BE id, BE_URL, FE_URL). - **`scripts/make-upgrade-proposal`** (production): didc-assist's interactive flow doesn't know how to type a 32-byte SHA-256 digest as Candid blob, so we *post-process* the args.txt it produces: - After didc-assist writes the file (or the trivial `(null)` when the user accepts the no-update default), call `patch_email_recovery_init_fields`. - The patcher uses Node to find/replace the `dnssec_config` and `doh_config` fields in the Candid arg text — or insert them inside the outermost `record { ... }` if didc-assist didn't emit them — with values from the same IANA fetch + DoH allowlist. The user can opt out with `II_SKIP_EMAIL_RECOVERY_INIT=1` for upgrades where the on-chain values are already correct, and override the DoH allowlist via `II_DOH_DOMAINS=...`. - Both staging and production paths share the patcher; encoded Candid round-trips through `didc encode` cleanly in all three edge cases tested locally: existing fields = null, fields absent, args.txt = `(null)`. The reason we don't bundle the IANA anchors into the wasm is that they roll over every ~7 years; the next rollover becomes a one-line change in the next upgrade arg instead of a code change + recompile.
sea-snake
added a commit
that referenced
this pull request
May 12, 2026
…l args Email recovery (PRs #3838-#3844) needs two new BE init args set on the canister: `dnssec_config.root_anchors` (the IANA root KSKs the DNSSEC verifier trusts) and `doh_config.allowed_domains` (the mailbox-provider domains that can use the DoH-fallback path). This PR plumbs both through the three deploy scripts. What landed: - **`scripts/fetch-iana-root-anchors.bash`** — new module, sourced by the other deploy scripts. `fetch_and_review_iana_root_anchors`: - `curl`s `data.iana.org/root-anchors/root-anchors.xml` (retry + bounded timeout). - Parses every `<KeyDigest>` block with Node, filtering to those whose `validFrom`/`validUntil` window contains "now" — drops retired keys (Kjqmt7v expired 2019) and keeps the active KSKs (Klajeyz key_tag=20326, Kmyv6jo key_tag=38696 today). - Prints a one-line human summary per entry to stderr (id + key_tag + algo + digest_type + digest preview + validity window). - Prompts for confirmation on /dev/tty (gracefully falls back to "accept default" when there's no controlling terminal, so CI / sandboxes don't deadlock). - Writes Candid `vec record { key_tag = ...; algorithm = ...; digest_type = ...; digest = blob "..."; }` to stdout, ready to drop into `dnssec_config.root_anchors`. - **`scripts/deploy-common.bash`** (consumed by `deploy-pr-to-beta` and `deploy-local-to-beta`): - New `DEFAULT_DOH_ALLOWED_DOMAINS` list — gmail, googlemail, outlook, hotmail, live, icloud, me, mac, yahoo, protonmail, proton.me, pm.me. Adding a domain is operator config, not code; override per-deploy with `--doh-domains <a,b,c,...>`. Empty list = DNSSEC-only, no DoH fallback. - New `--skip-email-recovery-init` flag — when set, leave both fields as `opt null` (preserve previous on-chain values). Default is to fetch fresh anchors + use the curated DoH allowlist, since the staging canisters are still in the "needs first-run init" stage. - `build_be_install_arg` now emits `dnssec_config = ...` and `doh_config = ...` alongside the existing trio (BE id, BE_URL, FE_URL). - **`scripts/make-upgrade-proposal`** (production): didc-assist's interactive flow doesn't know how to type a 32-byte SHA-256 digest as Candid blob, so we *post-process* the args.txt it produces: - After didc-assist writes the file (or the trivial `(null)` when the user accepts the no-update default), call `patch_email_recovery_init_fields`. - The patcher uses Node to find/replace the `dnssec_config` and `doh_config` fields in the Candid arg text — or insert them inside the outermost `record { ... }` if didc-assist didn't emit them — with values from the same IANA fetch + DoH allowlist. The user can opt out with `II_SKIP_EMAIL_RECOVERY_INIT=1` for upgrades where the on-chain values are already correct, and override the DoH allowlist via `II_DOH_DOMAINS=...`. - Both staging and production paths share the patcher; encoded Candid round-trips through `didc encode` cleanly in all three edge cases tested locally: existing fields = null, fields absent, args.txt = `(null)`. The reason we don't bundle the IANA anchors into the wasm is that they roll over every ~7 years; the next rollover becomes a one-line change in the next upgrade arg instead of a code change + recompile.
pull Bot
pushed a commit
to mikeyhodl/internet-identity
that referenced
this pull request
May 12, 2026
…iring (dfinity#3838) ## Summary First PR in the email-recovery stack (`docs/ongoing/email-recovery.md` §10 Phase 0). Lands a working RFC-4035-compliant DNSSEC verifier for caller-supplied DNS proof bundles, plus the trust-anchor wiring that drives it. PR 2 (DKIM verifier) and PRs 4–9 (storage + recovery methods) build on this. ## What's in this PR ### Verifier core - New `dnssec/` module under `src/internet_identity/src/`: - `types.rs` — `DnsProofBundle`, `SignedRRset`, `DelegationLink`, `Rrsig`, `DnsName`, `DnssecError`, `VerifiedRecord`. - `canonical.rs` — owner-name canonicalization, RR canonical form, RRSIG signed-data construction (RFC 4034 §3.1.8.1, §6.2, §6.3), DS digest input. - `signature.rs` — algorithm dispatch + DS digest matching (SHA-256). - `verify.rs` — four-step algorithm (root anchor match → chain walk → leaf RRSIG → freshness). - Algorithm coverage (RFC 8624 MUST set): - **alg 8** — RSA-SHA256 (RFC 5702): root, com., most legacy zones. - **alg 13** — ECDSA-P256-SHA256 (RFC 6605): most TLDs, Cloudflare, modern zones. - **alg 15** — Ed25519 (RFC 8080): rare in production but mandatory. - Anything else returns `UnsupportedAlgorithm`. ### Wiring - New `DnssecConfig` and `DnssecRootAnchor` types in `internet_identity_interface`, exposed at the top of `InternetIdentityInit` as `dnssec_config: opt opt DnssecConfig` (set/clear semantics matching `analytics_config` and `dummy_auth`). - Trust-anchor list plumbed through `init`/`post_upgrade` into `PersistentState.dnssec_config` (and `StorablePersistentState` for cross-upgrade persistence). - `internet_identity.did` updated. ### Tests 13 unit tests in `dnssec/` covering: - Real cloudflare.com chain verifies end-to-end (exercises alg 8 at root and alg 13 at com → cloudflare.com → leaf). - Empty trust-anchor list rejected with `NoTrustAnchors`. - Wrong trust anchor rejected with `RootAnchorMismatch`. - Flipped byte in root DNSKEY → `RootAnchorMismatch` or `BadSignature`. - Flipped byte in leaf signature → `BadSignature`. - Stale signature (clock advanced past expiration) → `StaleOrFutureSignature`. - Unsupported algorithm (alg 5 / RSA-SHA1) → `UnsupportedAlgorithm(5)`. - Plus canonical-encoding + RFC 3110 RSA key parsing unit tests. ### Test infrastructure - `test_vectors/dnssec/cloudflare-com-2026-05.json` — real DoH-captured chain (root DNSKEY + 2 delegation links + leaf TXT). - `test_vectors/dnssec/iana-root-anchors-2026-05.json` — IANA root KSK trust anchors (Klajeyz/2017 + Kmyv6jo/2024). - `scripts/capture-dnssec-chain.py` — reproducible capture script using dnspython + DoH wire format. Tests use a frozen now from the capture's metadata so freshness checks stay stable indefinitely. ### New deps - `domain` (NLnet Labs, pure Rust) — referenced in docstrings for canonicalisation primitives; signature verification is hand-rolled on top of RustCrypto. - `p256` — ECDSA P-256 verification. - `ed25519-dalek` — Ed25519 verification. All three build cleanly for wasm32-unknown-unknown. ## What's deferred to later PRs in the stack - Captures for additional providers (proton.me, protonmail.com, tutanota.com — gmail.com / icloud.com / outlook.com / fastmail.com don't sign with DNSSEC; this is acknowledged in design doc §7.6). - Synthetic Ed25519 (alg 15) test vector — most production zones are alg 8 or 13; alg 15 is structurally exercised by the dispatch logic but doesn't have a real captured chain in this PR. ## Test plan - [x] `cargo check -p internet_identity --target wasm32-unknown-unknown` — clean (no warnings). - [x] `cargo test -p internet_identity --bin internet_identity` — 238 tests pass (was 227 pre-PR). - [x] `cargo test -p internet_identity_interface --lib` — 42 tests pass. - [x] `cargo clippy -p internet_identity --bin internet_identity --tests -- -D warnings` — clean. - [x] `cargo fmt --check` — clean (modulo a pre-existing diff in attributes.rs unrelated to this PR). - [x] CI on dfinity#3838 — fully green. ## Design doc https://github.com/sea-snake/internet-identity/blob/design/email-recovery/docs/ongoing/email-recovery.md (PR pending review on dfinity/internet-identity) ## Stack This is PR 1 of a 12-PR series. Subsequent PRs: - **PR 2** — mail-auth-backed DKIM verifier, consuming DnsProofBundle from this PR. - **PR 3** — DMARC alignment. - **PRs 4–9** — storage + Candid + behavior for email recovery. - **PRs 10–12** — frontend (DoH walker, Manage UI, recovery wizard). ## PR Stack | # | PR | Description | Status | |---|---|---|---| | 0 | [dfinity#3836](dfinity#3836) | Design doc | Open | | 1 | [dfinity#3838](dfinity#3838) | DNSSEC verifier scaffold | Open | | 2 | [dfinity#3839](dfinity#3839) | DKIM verifier (RFC 6376) | Open | | 3 | [dfinity#3840](dfinity#3840) | DMARC alignment (RFC 7489) | Open | | 4 | [dfinity#3841](dfinity#3841) | DoH fallback | Open | | 5+6 | [dfinity#3842](dfinity#3842) | Setup flow (storage + smtp_request) | Open | | 7 | [dfinity#3843](dfinity#3843) | Recovery flow (delegation) | Open | | 8 | [dfinity#3844](dfinity#3844) | Frontend + feature flag | Open | | 9 | [dfinity#3855](dfinity#3855) | Deploy/upgrade scripts: dnssec_config + doh_config | Open | | 10 | [dfinity#3857](dfinity#3857) | Email-recovery UX overhaul | Open |
sea-snake
added a commit
that referenced
this pull request
May 13, 2026
…l args Email recovery (PRs #3838-#3844) needs two new BE init args set on the canister: `dnssec_config.root_anchors` (the IANA root KSKs the DNSSEC verifier trusts) and `doh_config.allowed_domains` (the mailbox-provider domains that can use the DoH-fallback path). This PR plumbs both through the three deploy scripts. What landed: - **`scripts/fetch-iana-root-anchors.bash`** — new module, sourced by the other deploy scripts. `fetch_and_review_iana_root_anchors`: - `curl`s `data.iana.org/root-anchors/root-anchors.xml` (retry + bounded timeout). - Parses every `<KeyDigest>` block with Node, filtering to those whose `validFrom`/`validUntil` window contains "now" — drops retired keys (Kjqmt7v expired 2019) and keeps the active KSKs (Klajeyz key_tag=20326, Kmyv6jo key_tag=38696 today). - Prints a one-line human summary per entry to stderr (id + key_tag + algo + digest_type + digest preview + validity window). - Prompts for confirmation on /dev/tty (gracefully falls back to "accept default" when there's no controlling terminal, so CI / sandboxes don't deadlock). - Writes Candid `vec record { key_tag = ...; algorithm = ...; digest_type = ...; digest = blob "..."; }` to stdout, ready to drop into `dnssec_config.root_anchors`. - **`scripts/deploy-common.bash`** (consumed by `deploy-pr-to-beta` and `deploy-local-to-beta`): - New `DEFAULT_DOH_ALLOWED_DOMAINS` list — gmail, googlemail, outlook, hotmail, live, icloud, me, mac, yahoo, protonmail, proton.me, pm.me. Adding a domain is operator config, not code; override per-deploy with `--doh-domains <a,b,c,...>`. Empty list = DNSSEC-only, no DoH fallback. - New `--skip-email-recovery-init` flag — when set, leave both fields as `opt null` (preserve previous on-chain values). Default is to fetch fresh anchors + use the curated DoH allowlist, since the staging canisters are still in the "needs first-run init" stage. - `build_be_install_arg` now emits `dnssec_config = ...` and `doh_config = ...` alongside the existing trio (BE id, BE_URL, FE_URL). - **`scripts/make-upgrade-proposal`** (production): didc-assist's interactive flow doesn't know how to type a 32-byte SHA-256 digest as Candid blob, so we *post-process* the args.txt it produces: - After didc-assist writes the file (or the trivial `(null)` when the user accepts the no-update default), call `patch_email_recovery_init_fields`. - The patcher uses Node to find/replace the `dnssec_config` and `doh_config` fields in the Candid arg text — or insert them inside the outermost `record { ... }` if didc-assist didn't emit them — with values from the same IANA fetch + DoH allowlist. The user can opt out with `II_SKIP_EMAIL_RECOVERY_INIT=1` for upgrades where the on-chain values are already correct, and override the DoH allowlist via `II_DOH_DOMAINS=...`. - Both staging and production paths share the patcher; encoded Candid round-trips through `didc encode` cleanly in all three edge cases tested locally: existing fields = null, fields absent, args.txt = `(null)`. The reason we don't bundle the IANA anchors into the wasm is that they roll over every ~7 years; the next rollover becomes a one-line change in the next upgrade arg instead of a code change + recompile.
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.
Summary
EMAIL_RECOVERYfeature flag: default off; auto-enabled onbeta.id.aivia the same temporary-override patternGUIDED_UPGRADEalready uses.What lands here
lib/components/wizards/setupEmailRecovery/— enter address → send magic email (with countdown) → done. Pollsemail_recovery_statuswith 1→5s exponential backoff.lib/components/wizards/recoverWithEmail/— anonymous, ends with aSignedDelegation. Generates an ECDSA session keypair locally, drivesprepare_delegation→status→get_delegation, hands the result to the host page.lib/utils/dnssec/— wire-format DoH client (RFC 8484), DNS message parser, RRSIG parser, key-tag computation (RFC 4034 App. B), and a chain walker that bottom-up assembles aDnsProofBundle. Uses RRSIGsigner_nameto drive zone discovery so arbitrary depths work. Returnsundefinedfor unsigned domains → canister falls through to its DoH path.IdentityInfogains anemail_recoveryfield so the manage-page card renders correct active/inactive state on first load without a separate query.Tests
tests/e2e-playwright/routes/emailRecovery.spec.ts— feature-flag gating + wizard mount on both manage and recovery pages.npm run checkclean.PR Stack
Notes for reviewers
smtp_requestwith a DKIM signature requires standing up a gateway-side test rig that's out of scope here. The canister-side PocketIC integration tests cover that path. The new spec validates the FE wiring (flag gating, dialog mount).Accept: application/dns-message) because the JSON variant returns RDATA in presentation form which can't be re-encoded back to the canonical bytes RRSIG verification needs.hooks.client.tstime, so beta.id.ai users get the wizard surface without any manual configuration.🤖 Generated with Claude Code
E2E tests
The wizard surface (
emailRecovery.spec.ts) now covers two layers:page.route(), drives the wizard to the magic-email step, and submits the canister-issued nonce email throughsmtp_request. Both setup and recovery legs run in the same test. CI computes the trust anchor on the fly viarenderDnssecTestAnchor.mjs.Changes during review
FE review fixes in
98a9ad80: useRecoveryReady.anchor_number, addemailRecoveryauthMethod variant, drop dynamic<Trans>, replaceAbortSignal.timeoutwithAbortController, type wrappers against generated Candid types +throwCanisterError.Test fixups:
64151289btn migration,c7a8c03aeslint,fcb1a3e7unused constants,006f2f99prettier,b6c28bc1recoveryPage fixture button label,09864b1aunique From address per fixture.