feat(dmarc): RFC 7489 alignment + combined DKIM+DMARC verifier (PR 3 of email-recovery stack)#3878
Conversation
23985ad to
bcf8233
Compare
15811ad to
9208444
Compare
|
Grok's review PR Review: #3878 – Combined DKIM + DMARC email verification for recovery flow High-level verdict Major Security AssessmentNo critical or high-severity gaps found.
The only minor design decisions (no SPF, stricter-than-RFC parser) are explicitly called out in the code/comments and are acceptable for this use-case. BlockersNone. Easy-to-change improvements (do these before merge – all <30 min)
Strategic recommendations (nice-to-have, low effort)
Positive notes
Recommendation: Approve with the two easy parser/docs changes above. This PR is a clear net security win for Internet Identity’s recovery path with zero downside. Great work. |
|
Took a pass at Grok's three "easy-to-change improvements" — opened #3886 (draft, targets (Tried to push directly to Summary:
The four strategic recommendations (observability, frontend error surface, SPF wiring, README) are explicitly "nice-to-have" / "future" and belong in later PRs of the stack. Tests: 49 dmarc tests pass (+1). Clippy clean. wasm32 clean. Generated by Claude Code |
Addresses the two actionable "easy-to-change improvements" from Grok's review on #3878 (#3878 (comment)). Targets `feat/dmarc-alignment` so it can land as a follow-up commit on that PR. (Tried to push directly to `feat/dmarc-alignment` per the maintainer's note from #3877 / #3885, but my account got a 403 against that branch — falling back to a stacked draft PR.) ## What changed 1. **`dmarc/mod.rs` — consolidated `# Security model` docblock.** Calls out the three deliberate deviations from "stock" DMARC, each with a one-paragraph rationale: - **No Public Suffix List** — label-anchored suffix check prevents `evilexample.com`-style spoofs at the cost of closing multi-domain orgs (`gmail.com` ↔ `googlemail.com`). Safe direction for a recovery surface. Design doc §6.4. - **No SPF** — recovery proves mailbox control, not path-of-delivery. DKIM gives the cryptographic binding; SPF would need a source IP the gateway payload doesn't carry. Design doc §6.5. - **Fail-closed everywhere** — every malformed/unknown step collapses to `Unverified`. No quarantine, no downgrade. 2. **`dmarc/test_vectors.rs` — `verifies_end_to_end_when_dmarc_record_has_unknown_tags`.** Runs the full `verify_email` against a DMARC record carrying `rua=`, `ruf=`, `fo=`, and a synthetic `vendorext=` tag. Pins the ignore-unknown-tags behaviour through the public entry point, not just the parser unit (which already has a `unknown_tags_are_ignored` test in `dmarc/parse.rs`). ## Not actioned - **Grok item #1 ("parser must ignore unknown tags").** Already implemented and unit-tested before this PR. `dmarc/parse.rs` is lookup-by-name, so unknown tags pass through silently. The PR description even calls this out: "ignores unknown / reporting tags". The new end-to-end test is the explicit confirmation Grok asked for under item #3. The four strategic recommendations (observability metrics, frontend error surface, SPF wiring, README) are explicitly "nice-to-have" / "future" and belong in later PRs of the stack. ## Tests - `cargo test -p internet_identity --bin internet_identity dmarc::` — 49 pass (was 48, +1 e2e unknown-tag test). - `cargo clippy -p internet_identity --bin internet_identity --tests -- -D warnings` — clean. - `cargo check -p internet_identity --target wasm32-unknown-unknown` — clean. - `cargo fmt --check` on the two touched files — clean (other pre-existing fmt drifts in `dnssec/`, `openid/`, `tests/integration/`, `types/attributes.rs` are unrelated and predate this branch). --- _Generated by [Claude Code](https://claude.ai/code/session_01NZmvbHgzN5NQc7Hqx9ZzpP)_ Co-authored-by: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a DMARC alignment layer (RFC 7489 subset) on top of the existing DKIM verifier, introducing a new top-level dmarc::verify_email entry point that produces a combined DKIM+DMARC EmailVerificationStatus for the email-recovery verification pipeline.
Changes:
- Introduces a new
dmarc/module: DMARC TXT parsing, From-header single-mailbox parsing, alignment logic, and verification orchestration (+ unit/e2e tests). - Refactors the DKIM verifier API to be DKIM-only (
DkimVerifyResult) and re-exports it asverify_dkimfor use by the DMARC layer. - Adds synthetic DKIM test-vector fixtures (
.eml+ TXT record) and wires SMTP gateway Candid types into the interface crate.
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test_vectors/dkim/synth-rsa-test1._domainkey.test.example.com.txt | DKIM public-key TXT fixture for synthetic vectors. |
| test_vectors/dkim/synth-rsa-simple-simple.eml | Synthetic DKIM-signed email fixture (simple/simple). |
| test_vectors/dkim/synth-rsa-relaxed-simple.eml | Synthetic DKIM-signed email fixture (relaxed/simple). |
| test_vectors/dkim/synth-rsa-relaxed-relaxed.eml | Synthetic DKIM-signed email fixture (relaxed/relaxed). |
| test_vectors/dkim/README.md | Documentation for DKIM test-vector fixtures and provenance. |
| src/internet_identity/src/main.rs | Registers new dkim and dmarc modules in the canister binary. |
| src/internet_identity/src/dmarc/verify.rs | New top-level combined DKIM+DMARC verification orchestration. |
| src/internet_identity/src/dmarc/types.rs | DMARC types + combined EmailVerificationStatus for callers/UI. |
| src/internet_identity/src/dmarc/test_vectors.rs | End-to-end tests for combined verification using DKIM fixtures. |
| src/internet_identity/src/dmarc/parse.rs | DMARC TXT record parser (tag/value list). |
| src/internet_identity/src/dmarc/mod.rs | DMARC module wiring, exports, and security-model notes. |
| src/internet_identity/src/dmarc/from_header.rs | From-header single-mailbox parser for DMARC alignment. |
| src/internet_identity/src/dmarc/alignment.rs | Strict/relaxed (PSL-free) domain alignment logic + tests. |
| src/internet_identity/src/dkim/verify.rs | DKIM verification orchestration (multi-signature loop, enforcement). |
| src/internet_identity/src/dkim/types.rs | DKIM-only result type rename and new failure reasons for DMARC layer. |
| src/internet_identity/src/dkim/test_vectors.rs | DKIM end-to-end tests consuming .eml/TXT fixtures. |
| src/internet_identity/src/dkim/signature.rs | RSA/Ed25519 DKIM signature verification + body hash helper. |
| src/internet_identity/src/dkim/parse.rs | DKIM-Signature header parser (tag list, base64 handling). |
| src/internet_identity/src/dkim/mod.rs | DKIM module wiring and verify_dkim re-export. |
| src/internet_identity/src/dkim/dns_record.rs | DKIM DNS TXT record parser (p=, k=, t= flags). |
| src/internet_identity/src/dkim/canonicalize.rs | Relaxed canonicalization implementations for DKIM. |
| src/internet_identity_interface/src/internet_identity/types/smtp.rs | SMTP gateway Candid protocol types + validation helpers. |
| src/internet_identity_interface/src/internet_identity/types.rs | Exposes/re-exports SMTP types from the interface crate. |
| Cargo.lock | Dependency lockfile updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ck) (dfinity#3877) ## Summary PR 2 of the email-recovery stack (`docs/ongoing/email-recovery.md` §10 Phase 0). Stacks on top of PR 3838 (DNSSEC verifier). Lands a hand-rolled RFC 6376 DKIM verifier that consumes a parsed `SmtpRequest` plus an already-trusted DKIM TXT record and returns a per-step `EmailVerificationStatus`. **Note:** This PR targets `main` but includes PR 3838's commits (DNSSEC verifier) as its base. Review the DKIM-specific changes by looking at commits after `9bbd8717` (the last PR 3838 commit). Once PR 3838 merges, this PR's diff will shrink to just the DKIM additions. ## Why hand-rolled The design originally specified `mail-auth` (Stalwart's well-tested DKIM library), but mail-auth pulls a non-optional `hickory-resolver` dep that fails to compile for `wasm32-unknown-unknown` (transitive: tokio + mio). Forking + patching mail-auth would be possible but creates perpetual rebase burden. We hand-roll instead — "the right way, no shortcuts" was the explicit guidance. ## What's in this PR ### `src/internet_identity_interface/src/internet_identity/types/smtp.rs` Brings forward the SMTP gateway protocol types from PoC PR 3760: `SmtpRequest`/`SmtpResponse`/`SmtpHeader`/`SmtpMessage`/`SmtpAddress`/`SmtpEnvelope`, the size bounds, and the input-bound validation (`format_address` lowercases both halves; `truncate_at_char_boundary` clamps to the previous UTF-8 boundary so a multi-byte subject can't trap the canister). Drops postbox-specific bits (PostboxEmail, ValidatedSmtpRequest, anchor-number parser). ### `src/internet_identity/src/dkim/` - **`types.rs`** — Algorithm (RsaSha256, Ed25519Sha256), HeaderCanon/BodyCanon (Relaxed, Simple), DkimCheck/DkimCheckName/DkimCheckStatus per-step diagnostics, EmailVerificationStatus / VerificationFailReason result shape. - **`parse.rs`** (RFC 6376 §3.5) — DKIM-Signature header tag-list parser. Splits structurally on `;` first then on the *first* `=` per element, so a literal `b=` substring inside another tag's base64 doesn't get misread as a new tag start (the bug class the PoC PR review specifically flagged). Folded whitespace inside base64 values is stripped before decoding. Tag names case-insensitive; duplicates rejected. - **`canonicalize.rs`** (§3.4.2 / §3.4.4) — relaxed header canon (lowercase name, unfold continuations, collapse WSP+ to single SP, strip trailing WSP, strip WSP around colon) and relaxed body canon (per-line WSP cleanup, drop trailing empty lines, ensure non-empty output ends in exactly one CRLF). - **`dns_record.rs`** (§3.6.2) — DKIM TXT record parser. Tag names case-insensitive (`P=` vs `p=` was a PoC bug), whitespace inside `p=` tolerated (multi-chunk DNS TXT records), `t=y`/`t=s` flags honoured, unknown tags ignored. - **`signature.rs`** — RSA-SHA256 (RFC 5702 / RFC 8301) and Ed25519-SHA256 (RFC 8463) signature verification on top of `rsa`+`sha2`+`ed25519-dalek` from PR 1's deps. Enforces 1024-bit RSA minimum per design §5.6. Ed25519 path wraps in SHA-256 per RFC 8463. Plus `body_hash_sha256` with optional `l=` truncation per §3.4.5. - **`verify.rs`** — orchestration. Multi-signature loop per §5.5 (accept on first pass), tag enforcement per design §5.4 (c=relaxed/* only, x= expiration, i= alignment with d=, k= match, t=y testing-mode), bottom-up header selection per §5.4 when h= lists a name multiple times, b=value blanking that's structural-position-aware so it doesn't mis-target an internal substring. - **`test_vectors.rs`** — `#[cfg(test)]` .eml loader + 8 end-to-end tests against committed fixtures. ### `test_vectors/dkim/` - 3 synthetic .eml files generated offline with dkimpy + a 2048-bit RSA key (`relaxed/relaxed`, `relaxed/simple`, `simple/simple`). - The matching DKIM TXT record (public key only). - README documenting provenance — the throwaway private key is **not** committed. ## Test plan - [x] `cargo check -p internet_identity --target wasm32-unknown-unknown` — clean. - [x] `cargo test -p internet_identity --bin internet_identity dkim` — 75 tests pass (parse 14, canonicalize 18, dns_record 16, signature 7, verify 12, end-to-end 8). - [x] `cargo test -p internet_identity --bin internet_identity` — 313 tests pass total (was 238 before this PR; +75 DKIM, plus a few in smtp types). - [x] `cargo test -p internet_identity_interface --lib` — 52 tests pass (was 42; +10 SMTP type tests). - [x] `cargo clippy -p internet_identity --bin internet_identity --tests -- -D warnings` — clean. - [x] `cargo fmt --check` — clean (modulo pre-existing diffs unrelated to this PR). ## Stack This is PR 2 of a 12-PR series. Includes PR 3838's commits as its base; once PR 3838 merges, the diff shrinks to just the DKIM additions. Subsequent PRs: - **PR 3** — DMARC alignment. - **PR 4** — DoH outcall fallback for unsigned domains (Gmail / Outlook / iCloud — see the design doc §7.6 and the team Slack writeup). - **PRs 5–9** — storage + Candid + behavior for email recovery. - **PRs 10–12** — frontend. ## PR Stack | # | PR | Description | Status | |---|---|---|---| | 0 | [dfinity#3836](dfinity#3836) | Design doc | Open | | 1 | [dfinity#3838](dfinity#3838) | DNSSEC verifier scaffold | Open | | 2 | [dfinity#3877](dfinity#3877) | DKIM verifier (RFC 6376) | Open | | 3 | [dfinity#3878](dfinity#3878) | DMARC alignment (RFC 7489) | Open | | 4 | [dfinity#3879](dfinity#3879) | DoH fallback | Open | | 5+6 | [dfinity#3880](dfinity#3880) | Setup flow (storage + smtp_request) | Open | | 7 | [dfinity#3881](dfinity#3881) | Recovery flow (delegation) | Open | | 8 | [dfinity#3882](dfinity#3882) | Frontend + feature flag | Open | | 9 | [dfinity#3883](dfinity#3883) | Deploy/upgrade scripts: dnssec_config + doh_config | Open | | 10 | [dfinity#3884](dfinity#3884) | Email-recovery UX overhaul | Open | --------- Co-authored-by: Arshavir Ter-Gabrielyan <arshavir.ter.gabrielyan@dfinity.org> Co-authored-by: Claude <noreply@anthropic.com>
PR 3 of the email-recovery stack (docs/ongoing/email-recovery.md §6).
Stacks on top of PR 2 (DKIM verifier) and reshapes the verifier API:
dkim::verify_dkim now returns a DKIM-only DkimVerifyResult, and the new
dmarc::verify_email is the public top-level entry point that produces
the combined EmailVerificationStatus.
New module src/internet_identity/src/dmarc/:
- types.rs: DmarcOutcome (Aligned / Misaligned / NoRecord / Malformed),
DmarcPolicy, AlignmentMode, DmarcRecord, plus the combined
EmailVerificationStatus that carries both the DKIM diagnostic and
the DMARC outcome on success.
- parse.rs: DMARC TXT record parser per RFC 7489 §6.3. Enforces
v=DMARC1 must be first, p= must be one of {none, quarantine, reject},
pct= 0..=100, rejects duplicate tags, ignores unknown tags. 12 unit
tests.
- from_header.rs: RFC 5322 single-mailbox From: parser. Accepts
bare addr-spec, name-addr, and quoted-display-name forms; rejects
zero/multiple From: headers, address-lists, group syntax. Tolerates
comma/colon inside quoted display names. 16 unit tests.
- alignment.rs: strict (exact match) + relaxed (exact match OR
label-aligned subdomain in either direction). Stricter than RFC-
compliant relaxed alignment because we deliberately don't consult
the PSL — see design doc §6.4. The dot anchor on the subdomain check
prevents 'evilexample.com' from aliasing 'example.com'. 8 unit tests.
- verify.rs: orchestration. DKIM first; on failure, surface the DKIM
reason verbatim. On DKIM pass, parse From and check DMARC alignment.
Accepted iff Aligned, OR NoRecord with dkim_domain == from_domain.
8 unit tests.
dkim/types.rs:
- Renamed EmailVerificationStatus -> DkimVerifyResult (DKIM-only).
The combined verdict moved to dmarc::EmailVerificationStatus so it
can carry the DmarcOutcome.
- Added MalformedFromHeader, DmarcMalformed, DmarcMisaligned to
VerificationFailReason.
44 DMARC tests pass (12 parse + 16 from_header + 8 alignment + 8 verify),
on top of the existing 78 DKIM tests. Wasm32 build clean.
5 end-to-end tests in dmarc/test_vectors.rs reusing the synthetic .eml fixtures from PR 2 (alice@test.example.com signed with d=test.example.com, exact match → trivially aligned regardless of mode). Covers: - no DMARC record + dkim==from → Verified(NoRecord) - aligned DMARC strict → Verified(Aligned, Strict) - aligned DMARC relaxed (default) → Verified(Aligned, Relaxed) - malformed DMARC TXT → Unverified(DmarcMalformed) - From: with address-list → Unverified 49 dmarc tests total (44 unit + 5 e2e). 365 total in the II suite. The parse_eml helper is duplicated from dkim/test_vectors.rs because the original is #[cfg(test)] and not pub-visible across modules; the duplication is contained to test code.
Three fixes originally landed bundled with DoH review feedback (commit 5d5f686 on PR #3879). They belong here in PR 3's DMARC module since they're parser / spec-compliance fixes: - parse.rs: enforce p= immediately after v=DMARC1 per RFC 7489 §6.3. Real-world records all do this; OpenDMARC and other reference parsers reject the alternative. - from_header.rs: honour backslash escapes inside quoted-string display names so `"Alice \"Ops, Inc\"" <a@e.com>` parses correctly. - verify.rs: removed empty placeholder test that asserted nothing; the e2e path is exercised by test_vectors::*.
Addresses the two actionable "easy-to-change improvements" from Grok's review on #3878 (#3878 (comment)). Targets `feat/dmarc-alignment` so it can land as a follow-up commit on that PR. (Tried to push directly to `feat/dmarc-alignment` per the maintainer's note from #3877 / #3885, but my account got a 403 against that branch — falling back to a stacked draft PR.) ## What changed 1. **`dmarc/mod.rs` — consolidated `# Security model` docblock.** Calls out the three deliberate deviations from "stock" DMARC, each with a one-paragraph rationale: - **No Public Suffix List** — label-anchored suffix check prevents `evilexample.com`-style spoofs at the cost of closing multi-domain orgs (`gmail.com` ↔ `googlemail.com`). Safe direction for a recovery surface. Design doc §6.4. - **No SPF** — recovery proves mailbox control, not path-of-delivery. DKIM gives the cryptographic binding; SPF would need a source IP the gateway payload doesn't carry. Design doc §6.5. - **Fail-closed everywhere** — every malformed/unknown step collapses to `Unverified`. No quarantine, no downgrade. 2. **`dmarc/test_vectors.rs` — `verifies_end_to_end_when_dmarc_record_has_unknown_tags`.** Runs the full `verify_email` against a DMARC record carrying `rua=`, `ruf=`, `fo=`, and a synthetic `vendorext=` tag. Pins the ignore-unknown-tags behaviour through the public entry point, not just the parser unit (which already has a `unknown_tags_are_ignored` test in `dmarc/parse.rs`). ## Not actioned - **Grok item #1 ("parser must ignore unknown tags").** Already implemented and unit-tested before this PR. `dmarc/parse.rs` is lookup-by-name, so unknown tags pass through silently. The PR description even calls this out: "ignores unknown / reporting tags". The new end-to-end test is the explicit confirmation Grok asked for under item #3. The four strategic recommendations (observability metrics, frontend error surface, SPF wiring, README) are explicitly "nice-to-have" / "future" and belong in later PRs of the stack. ## Tests - `cargo test -p internet_identity --bin internet_identity dmarc::` — 49 pass (was 48, +1 e2e unknown-tag test). - `cargo clippy -p internet_identity --bin internet_identity --tests -- -D warnings` — clean. - `cargo check -p internet_identity --target wasm32-unknown-unknown` — clean. - `cargo fmt --check` on the two touched files — clean (other pre-existing fmt drifts in `dnssec/`, `openid/`, `tests/integration/`, `types/attributes.rs` are unrelated and predate this branch). --- _Generated by [Claude Code](https://claude.ai/code/session_01NZmvbHgzN5NQc7Hqx9ZzpP)_ Co-authored-by: Claude <noreply@anthropic.com>
Addresses the five inline Copilot review comments across #3878 and its rebased twin #3887. dmarc/from_header.rs - Reject internal whitespace in the bare addr-spec form (no angle brackets). Previously `"Alice alice@example.com"` and `"alice @example.com"` slipped through with domain=example.com because only the post-`@` domain part was whitespace-checked. - Reject `>` without a matching `<`, multiple `>`, and content after the closing `>`. Previously `alice@example.com>` was parsed as a bare addr-spec with the `>` ending up in the extracted domain. - 5 new regression tests covering the above. dmarc/verify.rs - The `email.message == None` branch after a successful DKIM is dead in practice (verify_dkim returns Unverified(NoSignature) otherwise), but we keep the defensive branch rather than .expect()/unreachable!() — canister code on the IC must never trap, an invariant violation should surface as a structured fail-closed verdict instead. Updated the inline comment and switched the placeholder reason from the misleading MalformedFromHeader to NoSignature. dkim/verify.rs - On a successful signature, return only that signature's per-step checks in DkimVerifyResult::Verified.checks, matching the doc ("checks for the winning signature"). Previously the verifier concatenated every failed attempt's checks onto the winner's. - Update the module-level doc snippet to reference `verify_dkim` (the re-exported public name) instead of `verify`.
271edb2 to
7d157bd
Compare
|
Thanks for the thorough Grok review @aterga — the three actionable items were all addressed:
These landed via #3886 (merged into `feat/dmarc-alignment`). The branch has also been rebased onto current `main` (PR #3877 merged), and Copilot's five inline comments from #3878 and #3887 are addressed in 7d157bd. Strategic recommendations (observability, frontend error surface, SPF wiring, README) are tracked for later PRs in the stack. |
Summary
PR 3 of the email-recovery stack (
docs/ongoing/email-recovery.md§6). Stacks on top of #3877 (DKIM verifier). Lands a hand-rolled DMARC alignment check and reshapes the verifier API:dkim::verify_dkimbecomes a DKIM-only primitive, and the newdmarc::verify_emailis the public top-level entry point that produces the combinedEmailVerificationStatus.Note: This PR targets
mainbut includes PRs 1+2's commits as its base. Review the DMARC-specific changes by looking at commits on top ofec371aae3(PR 2's tip). Once PRs 1+2 merge, this PR's diff shrinks to just the DMARC additions.What's in this PR
src/internet_identity/src/dmarc/types.rs—DmarcOutcome(Aligned / Misaligned / NoRecord / Malformed),DmarcPolicy(None / Quarantine / Reject),AlignmentMode(Strict / Relaxed),DmarcRecord, plus the combinedEmailVerificationStatusthat carries both DKIM diagnostics and the DMARC outcome on success.parse.rs(RFC 7489 §6.3) — DMARC TXT record parser. Enforcesv=DMARC1must be first,p=must be one of {none, quarantine, reject},pct=0..=100, rejects duplicate tags, ignores unknown / reporting tags. 12 unit tests.from_header.rs(RFC 5322 / RFC 7489 §3.1.1) — single-mailbox From-header parser. Accepts bare addr-spec, name-addr, and quoted-display-name forms; rejects zero/multiple From: headers, address-lists, group syntax. Tolerates comma/colon inside quoted display names. 16 unit tests.alignment.rs— strict (exact match) + relaxed (exact match OR label-aligned subdomain in either direction). Stricter than RFC-compliant relaxed alignment because we deliberately don't consult the PSL — design doc §6.4 documents the trust + asymmetric-failure-mode reasoning. The dot anchor on the subdomain check preventsevilexample.comfrom aliasingexample.com. 8 unit tests.verify.rs— orchestration. DKIM first; on failure, surface the DKIM reason verbatim. On DKIM pass, parse From and check DMARC alignment. Accepted iff Aligned, OR NoRecord withdkim_domain == from_domain. 8 unit tests.test_vectors.rs— 5 end-to-end tests reusing PR 2's synthetic .eml fixtures.src/internet_identity/src/dkim/types.rs(rename + new variants)EmailVerificationStatus→DkimVerifyResult(DKIM-only). The combined verdict moved todmarc::EmailVerificationStatusso it can carry theDmarcOutcome.MalformedFromHeader(String),DmarcMalformed(String),DmarcMisalignedtoVerificationFailReason.src/internet_identity/src/dkim/mod.rsverifyasverify_dkimso downstream callers (the dmarc layer) don't have to deal with both adkim::verifyanddmarc::verifyin scope at the same time.Test plan
cargo check -p internet_identity --target wasm32-unknown-unknown— clean.cargo test -p internet_identity --bin internet_identity dmarc— 49 tests pass (12 parse + 16 from_header + 8 alignment + 8 verify + 5 e2e).cargo test -p internet_identity --bin internet_identity— 365 tests pass total (was 313 with PR 2; +49 dmarc + 3 small reshape adjustments).cargo clippy -p internet_identity --bin internet_identity --tests -- -D warnings— clean.cargo fmt --check— clean (modulo pre-existing unrelated diffs).PR Stack