diff --git a/.agents/skills/redacted-circuit-maintenance/SKILL.md b/.agents/skills/redacted-circuit-maintenance/SKILL.md index 71c8624..4c556d2 100644 --- a/.agents/skills/redacted-circuit-maintenance/SKILL.md +++ b/.agents/skills/redacted-circuit-maintenance/SKILL.md @@ -83,7 +83,7 @@ Each section below is a rule plus the incident that produced it (2026-09-27, PR - Keys come from `src/utils/dkimKeys.ts` (a copy lives in the CLI). It tries Google + Cloudflare DoH first, then every archived key for the selector. `@zk-email/helpers` alone tries one DNS key, and only the first archived key, and only if the DNS lookup fails. That missed 12 of 132 - corpus emails whose keys were rotated under the same selector (mostly Amazon/SES). + corpus emails whose keys were rotated under the same selector. - archive.prove.email allows **10 requests/min per IP**. On a 429 it returns an error object, and the helper's own fallback crashes ("data.find is not a function"). An unpaced corpus DKIM sweep therefore misreports ~30 emails as failures. When sweeping, space archive calls diff --git a/skills/redacted-email-proof/SKILL.md b/skills/redacted-email-proof/SKILL.md index 25ce412..5bc9dc0 100644 --- a/skills/redacted-email-proof/SKILL.md +++ b/skills/redacted-email-proof/SKILL.md @@ -57,7 +57,7 @@ the smallest tier that fits (RSA 1024- or 2048-bit keys): | large | 4,096 B | 48,000 B | ~1 min on 18 cores (longer on fewer), **~7 GB RAM** | | xl | 4,096 B | 100,352 B | ~2–4 min on 18 cores (longer on fewer), **~14 GB RAM** (16 GB+ machine) | -The large-tier figures were measured on a 34 KB PayPal receipt: 58 s, 6.6 GB peak for the native +The large-tier figures were measured on a real 34 KB receipt: 58 s, 6.6 GB peak for the native `bb`. XL figures were measured on 87.6 KB and 94.9 KB receipts (loaded machine; faster idle). Peak memory is the kernel's lifetime physical footprint of the `bb` process (`/usr/bin/time -l`): large 7.4 GB, XL 13.7 GB (18-core M-series Mac, 2026-09-29). diff --git a/skills/redacted-email-proof/scripts/redacted.mjs b/skills/redacted-email-proof/scripts/redacted.mjs index 4481106..f8f3993 100755 --- a/skills/redacted-email-proof/scripts/redacted.mjs +++ b/skills/redacted-email-proof/scripts/redacted.mjs @@ -737,6 +737,34 @@ export function domainsAligned(fromDomain, signingDomain) { return f === d || f.endsWith("." + d) || d.endsWith("." + f); } +// REASON: Google Workspace signs mail for domains without their own DKIM as +// d=..gappssmtp.com, and Microsoft 365 as +// d=.onmicrosoft.com. A Google label equal to the dashed From: domain ties the signature +// to that domain's Workspace (Google signs only for its customers and only From: addresses they +// verified); Microsoft tenant names aren't tied to domains, so those stay unproven. +// NOTE: dots->dashes is ambiguous (bank.co.uk / bank-co.uk -> bank-co-uk) and Workspace's SMTP +// relay can send any From:, so only one-dot From: domains (unambiguous) count as matched. +// Same as src/utils/keyBinding.ts signerProvider (see its NOTE on the dots->dashes collision). +export function signerProvider(fromDomain, signingDomain) { + const d = signingDomain.toLowerCase(); + const g = /^([a-z0-9-]+)\.\d{8}\.gappssmtp\.com$/.exec(d); + if (g) { + const f = fromDomain?.toLowerCase() ?? ""; + const unambiguous = f.split(".").length === 2; + return { kind: "google-workspace", tenant: g[1], matchesFrom: unambiguous && g[1] === f.replace(/\./g, "-") }; + } + const ms = /^([a-z0-9-]+)\.onmicrosoft\.com$/.exec(d); + if (ms) return { kind: "microsoft-365", tenant: ms[1], matchesFrom: false }; + return null; +} + +function alignmentOf(fromDomain, signingDomain) { + const provider = signerProvider(fromDomain, signingDomain); + if (fromDomain && domainsAligned(fromDomain, signingDomain)) return { fromAligned: true, alignedVia: "domain", provider }; + if (provider?.matchesFrom) return { fromAligned: true, alignedVia: "google-workspace", provider }; + return { fromAligned: false, alignedVia: null, provider }; +} + // Decode the masked header/body a proof publishes (0x00 = masked). v2 (prefix 5) commits to the // signed lengths and zeroes everything past them, so this is an exact slice. // Throws on non-canonical inputs (see canonicalPublicInputs). @@ -924,7 +952,7 @@ export async function checkKeyBinding(publicInputs, header, circuit, resolveKeys if (!n) continue; const got = await pubkeyHash(n, keyBits, version); if (got.length === want.length && got.every((v, i) => v === want[i])) { - return { ...out, domain: d, selector: s, matched: out.tried.at(-1), fromAligned: from.domain ? domainsAligned(from.domain, d) : false }; + return { ...out, domain: d, selector: s, matched: out.tried.at(-1), ...alignmentOf(from.domain, d) }; } } } @@ -1173,6 +1201,8 @@ async function cmdVerify(ref, opts) { fromAddress: binding?.from?.address ?? null, fromHidden: binding?.from?.hidden ?? false, fromAlignedWithDkimDomain: binding?.fromAligned ?? null, + fromAlignedVia: binding?.alignedVia ?? null, + signerProvider: binding?.provider ?? null, maskedHeader: outs.headerText, maskedBody: outs.bodyText, }; @@ -1195,6 +1225,12 @@ async function cmdVerify(ref, opts) { : `dkim key: NOT matched to any published key for ${pairs} ` + `(tried: ${result.keysTried.join("; ") || "no key found"}${binding.archiveError ? `; archive: ${binding.archiveError}` : ""}). Treat the sender as unproven.`, ); + if (result.keyMatches && result.fromAlignedVia === "google-workspace") { + console.log( + `from: ${result.fromAddress} — signed by Google Workspace for ${binding.from.domain} (d=${result.dkimDomain}; ` + + `Google signs this way for Workspace domains without their own DKIM key).`, + ); + } if (result.keyMatches && result.fromAlignedWithDkimDomain === false) { // REASON: usually innocent (a mailing service signing with its own domain), so name the // signing domain and the matching key, and say that only the From: line is unproven. @@ -1203,8 +1239,11 @@ async function cmdVerify(ref, opts) { result.fromHidden ? `warning: the ${signer}, but the From: address is hidden, so it isn't proven to be on ${result.dkimDomain}.` : result.fromAddress - ? `warning: From: ${result.fromAddress} is on ${binding.from.domain}, but the ${signer}. This is often a quirk ` + - `of the sender's email service (it signs with its own domain), but the From: address is NOT proven.` + ? `warning: From: ${result.fromAddress} is on ${binding.from.domain}, but the ${signer}. ` + + (result.signerProvider?.kind === "microsoft-365" + ? `${result.dkimDomain} is a Microsoft 365 tenant (Microsoft signs this way when a domain has no DKIM key of its own), ` + + `and which domains a tenant owns isn't public, so the From: address is NOT proven.` + : `This is often a quirk of the sender's email service (it signs with its own domain), but the From: address is NOT proven.`) : `warning: the ${signer}, but the From: address can't be read, so it isn't proven.`, ); } diff --git a/src/utils/keyBinding.ts b/src/utils/keyBinding.ts index c9e923e..73f8483 100644 --- a/src/utils/keyBinding.ts +++ b/src/utils/keyBinding.ts @@ -120,6 +120,46 @@ export function domainsAligned(fromDomain: string, signingDomain: string): boole return f === d || f.endsWith("." + d) || d.endsWith("." + f); } +export interface SignerProvider { + kind: "google-workspace" | "microsoft-365"; + /** the tenant label in d= */ + tenant: string; + /** the tenant provably belongs to the From: domain */ + matchesFrom: boolean; +} + +/** + * Mail platforms that sign customer mail with their own domain when the customer hasn't set up + * DKIM for its domain. + * REASON: plain domain alignment calls these "From: not proven", although for Google Workspace the + * d= names the customer's domain: + * - Google Workspace signs as d=..gappssmtp.com + * (school.example -> school-example.20230601.gappssmtp.com). Google signs that way only for its own + * Workspace customers, so a label equal to the dashed From: domain ties the signature to that + * domain's Workspace. + * NOTE: dots->dashes is not one-to-one: bank.co.uk and bank-co.uk both give bank-co-uk, and + * mail.bank.example / mail-bank.example both give mail-bank-example. Workspace's SMTP relay can be + * set to send any From: address, so the owner of a colliding domain could produce this exact d=. + * The mapping is only unambiguous when the From: domain has exactly one dot (a TLD can't contain + * a plain hyphen), so matchesFrom is limited to those. Longer domains stay unproven with a + * Google-specific explanation. (Same reasoning made zk-email-verify#324 keep gappssmtp opt-in.) + * - Microsoft 365 signs as d=.onmicrosoft.com. The tenant name isn't derived from any + * domain and which domains a tenant owns isn't public, so the From: line stays unproven. The + * banner just explains the quirk. + */ +export function signerProvider(fromDomain: string | null, signingDomain: string): SignerProvider | null { + const d = signingDomain.toLowerCase(); + const g = /^([a-z0-9-]+)\.\d{8}\.gappssmtp\.com$/.exec(d); + if (g) { + const f = fromDomain?.toLowerCase() ?? ""; + const unambiguous = f.split(".").length === 2; + return { kind: "google-workspace", tenant: g[1], matchesFrom: unambiguous && g[1] === f.replace(/\./g, "-") }; + } + const ms = /^([a-z0-9-]+)\.onmicrosoft\.com$/.exec(d); + if (ms) return { kind: "microsoft-365", tenant: ms[1], matchesFrom: false }; + return null; +} + // --------------------------------------------------------------------------------------------- // key binding @@ -194,6 +234,10 @@ export interface KeyBinding { from: ReturnType; /** From: domain aligned with the matched signing domain; null when there is no match */ fromAligned: boolean | null; + /** how fromAligned was established */ + alignedVia?: "domain" | "google-workspace"; + /** the signer is a mail platform's domain (Google Workspace, Microsoft 365) */ + provider?: SignerProvider | null; /** archive lookup failed (e.g. its 10 req/min limit); an unmatched result may be a false negative */ archiveError?: string; } @@ -237,7 +281,7 @@ export async function bindDkimKey( domain, selector, keySource: k.source, - fromAligned: from.domain ? domainsAligned(from.domain, domain) : false, + ...alignmentOf(from.domain, domain), }; } } @@ -246,6 +290,13 @@ export async function bindDkimKey( return result; } +function alignmentOf(fromDomain: string | null, signingDomain: string) { + const provider = signerProvider(fromDomain, signingDomain); + if (fromDomain && domainsAligned(fromDomain, signingDomain)) return { fromAligned: true, alignedVia: "domain" as const, provider }; + if (provider?.matchesFrom) return { fromAligned: true, alignedVia: "google-workspace" as const, provider }; + return { fromAligned: false, provider }; +} + /** Banner for a verified v2 proof, from its DKIM key binding. Used by the verify page; tested in tests/key-binding.test.ts. */ export function keyBindingStatus(b: KeyBinding): { verified: boolean; warning?: boolean; message: string } { if (!b.matched) { @@ -262,6 +313,15 @@ export function keyBindingStatus(b: KeyBinding): { verified: boolean; warning?: } const keyFrom = b.keySource === "archive" ? "the DKIM key archive (no longer in DNS)" : "DNS"; const key = `the DKIM key published at ${b.selector}._domainkey.${b.domain} (${keyFrom})`; + if (b.fromAligned && b.alignedVia === "google-workspace") { + return { + verified: true, + message: + `Proof verified. Signed by Google Workspace for ${b.from.domain}: the proof's key matches ${key}. ` + + `Google signs as ${b.domain} for Workspace domains that haven't set up their own DKIM key, and the ` + + `signature names ${b.from.domain}'s Workspace.`, + }; + } if (b.fromAligned) { return { verified: true, message: `Proof verified. Signed by ${b.domain}: the proof's key matches ${key}, and the From: address is on that domain.` }; } @@ -269,8 +329,17 @@ export function keyBindingStatus(b: KeyBinding): { verified: boolean; warning?: // school/church/HR systems) send on a company's behalf and sign with THEIR domain. Say exactly // which domain signed and which key matched, and that the From: line itself isn't proven. const quirk = - "This is often just how the sender's email service works: services that send mail on someone's behalf " + - "commonly sign with their own domain."; + b.provider?.kind === "microsoft-365" + ? `${b.domain} is a Microsoft 365 tenant: Microsoft signs this way when a domain hasn't set up its own DKIM key. ` + + "Which domains a tenant owns isn't public, so this can't be tied to the From: domain." + : b.provider?.kind === "google-workspace" + ? b.from.domain && b.provider.tenant === b.from.domain.replace(/\./g, "-") + ? `${b.domain} is Google Workspace signing for "${b.provider.tenant}". That name matches ${b.from.domain}, but ` + + `Google writes dots as dashes, so it would also match other domains (e.g. ${b.from.domain.replace(/\.(?=[^.]*\.)/, "-")}); ` + + "the From: domain isn't proven." + : `${b.domain} is Google Workspace signing for the Workspace "${b.provider.tenant}", which doesn't match the From: domain.` + : "This is often just how the sender's email service works: services that send mail on someone's behalf " + + "commonly sign with their own domain."; return { verified: false, warning: true, diff --git a/tests/key-binding.test.ts b/tests/key-binding.test.ts index f0a168e..0d189a2 100644 --- a/tests/key-binding.test.ts +++ b/tests/key-binding.test.ts @@ -275,3 +275,55 @@ test("real proofs: non-canonical inputs, key match, forged key, unaligned From ( await api.destroy(); } }); + +// Google Workspace / Microsoft 365 sign customer mail with their own domain when the customer has +// no DKIM key of its own (see signerProvider). Shape: From school.example, d=school-example.20230601.gappssmtp.com. +test("signerProvider: Google Workspace tenant label = dashed From: domain; Microsoft 365 never ties", () => { + for (const [name, m] of both) { + assert.deepEqual(m.signerProvider("school.example", "school-example.20230601.gappssmtp.com"), + { kind: "google-workspace", tenant: "school-example", matchesFrom: true }, name); + assert.equal(m.signerProvider("bank.example", "school-example.20230601.gappssmtp.com").matchesFrom, false, name); + assert.equal(m.signerProvider(null, "school-example.20230601.gappssmtp.com").matchesFrom, false, name); + assert.equal(m.signerProvider("my-site.example", "my-site-example.20150623.gappssmtp.com").matchesFrom, true, name); + // ambiguous: bank.co.uk and bank-co.uk both map to bank-co-uk, so neither may claim it + assert.equal(m.signerProvider("bank.co.uk", "bank-co-uk.20230601.gappssmtp.com").matchesFrom, false, name); + assert.equal(m.signerProvider("mail.bank.example", "mail-bank-example.20230601.gappssmtp.com").matchesFrom, false, name); + assert.deepEqual(m.signerProvider("school.example", "school.onmicrosoft.com"), + { kind: "microsoft-365", tenant: "school", matchesFrom: false }, name); + assert.equal(m.signerProvider("bank.example", "esp.example"), null, name); + // not Google's shape: no 8-digit date label, or another parent domain + assert.equal(m.signerProvider("school.example", "school-example.gappssmtp.com"), null, name); + assert.equal(m.signerProvider("school.example", "school-example.20230601.gappssmtp.com.evil.example"), null, name); + } +}); + +const workspaceBinding = (fromDomain: string, d: string) => { + const p = site.signerProvider(fromDomain, d); + const aligned = site.domainsAligned(fromDomain, d) || !!p?.matchesFrom; + return { + matched: true, domain: d, selector: "20230601", keySource: "dns:google", + candidates: [{ domain: d, selector: "20230601" }], + from: { address: `principal@${fromDomain}`, domain: fromDomain, hidden: false }, + fromAligned: aligned, alignedVia: aligned ? ("google-workspace" as const) : undefined, provider: p, + }; +}; + +test("verify banner: Google Workspace signature for the From: domain is verified and says so", () => { + const s = site.keyBindingStatus(workspaceBinding("school.example", "school-example.20230601.gappssmtp.com")); + assert.equal(s.verified, true); + assert.match(s.message, /Google Workspace for school\.example/); + const other = site.keyBindingStatus(workspaceBinding("bank.example", "school-example.20230601.gappssmtp.com")); + assert.equal(other.verified, false); + assert.equal(other.warning, true); + assert.match(other.message, /doesn't match the From: domain/); + const ambiguous = site.keyBindingStatus(workspaceBinding("bank.co.uk", "bank-co-uk.20230601.gappssmtp.com")); + assert.equal(ambiguous.verified, false); + assert.match(ambiguous.message, /would also match other domains/); +}); + +test("verify banner: Microsoft 365 tenant stays unproven, with a specific explanation", () => { + const s = site.keyBindingStatus(workspaceBinding("school.example", "school.onmicrosoft.com")); + assert.equal(s.verified, false); + assert.equal(s.warning, true); + assert.match(s.message, /Microsoft 365 tenant/); +});