From 88fa9935993cb50e1dd9865e383b331f7dd8495d Mon Sep 17 00:00:00 2001 From: "pr-automation-bot-public[bot]" Date: Tue, 26 May 2026 10:28:21 +0000 Subject: [PATCH] chore: sync II spec to dfinity/internet-identity release-2026-05-26 --- .sources/VERSIONS | 2 +- .sources/internetidentity | 2 +- public/references/internet-identity.did | 271 ++++++++++++++++++++++++ 3 files changed, 273 insertions(+), 2 deletions(-) diff --git a/.sources/VERSIONS b/.sources/VERSIONS index 32e6a9c..2ff1a7c 100644 --- a/.sources/VERSIONS +++ b/.sources/VERSIONS @@ -62,4 +62,4 @@ motoko-core v2.4.0 cdk-rs ic-cdk v0.20.1 / ic-cdk-timers v1.0.0 / ic-cdk-executor v2.0.0 317f55c candid 2025-12-18 # candid v0.10.20, didc v0.5.4 2e4a2cf response-verification v3.1.0 18c5a37 -internetidentity release-2026-05-08 f6cf858 +internetidentity release-2026-05-26 c47531f1 diff --git a/.sources/internetidentity b/.sources/internetidentity index f6cf858..c47531f 160000 --- a/.sources/internetidentity +++ b/.sources/internetidentity @@ -1 +1 @@ -Subproject commit f6cf85819113361acc3bb74880ab2f03c89a8658 +Subproject commit c47531f11e9b0d923f8e587c6399e40cc52b9d60 diff --git a/public/references/internet-identity.did b/public/references/internet-identity.did index 3d3ab6a..d5b3107 100644 --- a/public/references/internet-identity.did +++ b/public/references/internet-identity.did @@ -299,6 +299,51 @@ type InternetIdentityInit = record { backend_canister_id : opt principal; // Backend origin, needed to sync configuration with frontend. backend_origin : opt text; + // DNSSEC verification configuration. Trust anchors used by any feature + // that verifies DNS records against the IANA-rooted DNSSEC chain + // (currently the email-recovery DKIM/DMARC flow). See + // `docs/ongoing/email-recovery.md` §7.5. + // + // Wrapped in `opt opt` to match the same set/clear pattern as + // `analytics_config` / `dummy_auth`: outer null keeps the previously + // stored value across an upgrade, `opt null` clears it, `opt opt c` + // sets it to `c`. + dnssec_config : opt opt DnssecConfig; + // DoH (DNS-over-HTTPS) fallback configuration. Allowlists the + // domains for which the canister may fetch DKIM/DMARC TXT records + // via HTTP outcalls when no DNSSEC chain is available — see + // `docs/ongoing/email-recovery.md` §7.6. Same set/clear pattern. + doh_config : opt opt DohConfig; +}; + +// DNSSEC trust-anchor list. Any feature that needs DNSSEC-verified DNS +// records consumes the same anchors; not specific to email recovery. +type DnssecConfig = record { + // IANA root KSK trust anchors. Multiple are accepted simultaneously so + // KSK rollover is a single config change in the next upgrade arg. + root_anchors : vec DnssecRootAnchor; +}; + +// One IANA root KSK trust anchor, in the same shape IANA publishes at +// `data.iana.org/root-anchors/root-anchors.xml`. Only `digest_type = 2` +// (SHA-256) is accepted; the legacy SHA-1 form is rejected at the +// verifier boundary. +type DnssecRootAnchor = record { + key_tag : nat16; + algorithm : nat8; + digest_type : nat8; + digest : blob; +}; + +// DoH (DNS-over-HTTPS) fallback configuration. +// +// The canister will only fetch DKIM/DMARC TXT records via HTTP outcalls +// for an FQDN whose registered domain is in `allowed_domains`. Cache +// entries are populated on demand and re-used until `max_cache_age_secs` +// elapses (default 3600s when null, capped at 24h). +type DohConfig = record { + allowed_domains : vec text; + max_cache_age_secs : opt nat64; }; type ChallengeKey = text; @@ -462,6 +507,191 @@ type OpenIdPrepareDelegationResponse = record { anchor_number : UserNumber; }; +// Email-recovery types +// ==================== +// See `docs/ongoing/email-recovery.md` for the full design. Covers +// both halves of the flow: setup (binding a recovery email to an +// anchor) and recovery (proving control of a previously-bound +// address to obtain a signed delegation). + +type EmailRecoveryCredential = record { + address : text; + created_at : Timestamp; + last_used : opt Timestamp; +}; + +type EmailRecoveryChallenge = record { + nonce : text; + expires_at : Timestamp; +}; + +type EmailRecoveryDnsInput = record { + address : text; + dns_proof : opt DnsProofBundle; +}; + +type EmailRecoverySubmitDkimLeafArg = record { + nonce : text; + // The DKIM resolution chain in CNAME order, ending in a TXT. At + // least one hop required; bounded by `MAX_CNAME_HOPS = 4` at the + // canister side. For the Gmail-style direct-TXT case this is a + // single-element vec. + hops : vec SignedRRset; + // Delegation chains for signed zones touched by `hops` that + // weren't already covered by the skeleton chain anchored at + // prepare time. Empty for same-zone resolution. + extra_chains : vec DelegationChain; +}; + +// DNSSEC proof bundle and supporting types — see +// `internet_identity_interface::types::dnssec`. +type Rrsig = record { + type_covered : nat16; + algorithm : nat8; + labels : nat8; + original_ttl : nat32; + expiration : nat32; + inception : nat32; + key_tag : nat16; + signer_name : blob; + signature : blob; +}; + +type SignedRRset = record { + name : blob; + rtype : nat16; + rdata : vec blob; + ttl : nat32; + rrsig : Rrsig; +}; + +type DelegationLink = record { + child_ds : SignedRRset; + child_dnskey : SignedRRset; +}; + +type DelegationChain = record { + links : vec DelegationLink; +}; + +type DnsProofBundle = record { + root_dnskey : SignedRRset; + // One delegation chain per signing zone the bundle touches. + // Single-zone direct case (Gmail, iCloud, …): one chain. + // Cross-zone CNAME case (Proton, Tutanota, M365 custom domains): + // one chain per signing zone touched. + chains : vec DelegationChain; + // The RRsets being authenticated, in CNAME-resolution order. + // Single-leaf case: one hop. CNAME case: intermediate CNAMEs, + // then the final TXT. + hops : vec SignedRRset; +}; + +type EmailRecoveryError = variant { + Unauthorized : principal; + NonceUnknown; + NonceExpired; + DomainNotAllowlisted : text; + DohFetchFailed : text; + DomainNotSupported : text; + EmailVerificationFailed : text; + DkimLeafMismatch; + NoDkimLeafExpected; + AddressMismatch; + SubjectNotSigned; + AddressAlreadyRegistered; + AddressNotRegistered; + InternalCanisterError : text; +}; + +type EmailRecoveryStatus = variant { + Pending; + NeedDkimLeaf : record { selector : text }; + RegistrationSucceeded; + RecoveryReady : record { + user_key : UserKey; + expiration : Timestamp; + anchor_number : IdentityNumber; + }; + Failed : EmailRecoveryError; + Expired; +}; + +type EmailRecoveryGetDelegationArgs = record { + nonce : text; + session_key : SessionKey; + expiration : Timestamp; +}; + +// SMTP gateway types — see `internet_identity_interface::smtp`. Carried +// forward from PoC #3760 surface (the existing gateway can target this +// canister without changes). +type SmtpHeader = record { + name : text; + value : text; +}; + +type SmtpMessage = record { + headers : vec SmtpHeader; + body : blob; +}; + +type SmtpAddress = record { + user : text; + domain : text; +}; + +type SmtpEnvelope = record { + from : SmtpAddress; + // SMTP allows multiple `RCPT TO` recipients per envelope, so this + // is a vec at the wire level. For the recovery flows this canister + // serves, however, we require *exactly one* recipient and it must + // be `register@` or `recover@` — a legitimate + // recovery email never targets a CC/BCC alongside us, so any + // additional recipient can only come from a phishy forwarder + // trying to exfiltrate the user's canister-signed challenge. + // Multi-recipient envelopes (and empty ones) are rejected with + // code 551 ("User not local"); single-recipient envelopes whose + // recipient isn't one of our reserved mailboxes get 550 ("No + // such user here"). The vec is also capped at 100 entries (RFC + // 5321 §4.5.3.1.10); envelopes exceeding the cap are rejected + // with code 555. + to : vec SmtpAddress; +}; + +type SmtpRequest = record { + message : opt SmtpMessage; + envelope : opt SmtpEnvelope; + gateway_flags : opt vec text; +}; + +// Error returned by `smtp_request` / `smtp_request_validate`. +// +// `code` mirrors the SMTP reply codes the off-chain gateway should +// emit upstream: +// - `550` (mailbox unavailable) — "No such user here". Returned when +// the envelope carries exactly one recipient but it isn't a mailbox +// this canister handles (i.e. neither `register@` nor +// `recover@` for any `` in `related_origins`). +// - `551` (user not local) — envelope-shape rejection. Returned for +// empty `to` and for multi-recipient envelopes, even when one of +// the recipients is ours. Distinct from 550 so the gateway can tell +// "this envelope shape isn't accepted" from "we don't know this +// user". Recovery emails never legitimately address a CC/BCC +// alongside `register@…` / `recover@…`. +// - `555` (syntax error) — the request shape itself is malformed +// (e.g. missing envelope, oversize address/header/body, recipient +// list exceeds the 100-entry cap). +type SmtpRequestError = record { + code : nat64; + message : text; +}; + +type SmtpResponse = variant { + Ok : record {}; + Err : SmtpRequestError; +}; + // API V2 specific types // WARNING: These type are experimental and may change in the future. type IdentityNumber = nat64; @@ -621,6 +851,12 @@ type IdentityInfo = record { name : opt text; // The timestamp at which the anchor was created created_at : opt Timestamp; + // Email-recovery credentials bound to this anchor (absent when + // none is configured). The canister API currently caps the list + // at one entry — the FE renders the recovery-email card from + // the first one — but exposing it as a `vec` lets future + // multi-credential support land without a candid schema bump. + email_recovery : opt vec EmailRecoveryCredential; }; type IdentityInfoError = variant { @@ -1230,6 +1466,41 @@ service : (opt InternetIdentityInit) -> { openid_prepare_delegation : (JWT, Salt, SessionKey) -> (variant { Ok : OpenIdPrepareDelegationResponse; Err : OpenIdDelegationError }); openid_get_delegation : (JWT, Salt, SessionKey, Timestamp) -> (variant { Ok : SignedDelegation; Err : OpenIdDelegationError }) query; + // Email-recovery protocol + // ======================= + // See `docs/ongoing/email-recovery.md`. Covers both flows: + // - Setup: prepare_add (authenticated) → smtp_request for + // register@id.ai → credential bound to the anchor. Removed + // later via credential_remove. + // - Recovery: prepare_delegation (anonymous, bound to a + // session_key) → smtp_request for recover@id.ai → canister + // stamps a signed delegation seed. The FE then calls + // email_recovery_get_delegation to retrieve the + // SignedDelegation. + // Both flows share the polling status query. + email_recovery_credential_prepare_add : (IdentityNumber, EmailRecoveryDnsInput) -> (variant { Ok : EmailRecoveryChallenge; Err : EmailRecoveryError }); + email_recovery_prepare_delegation : (EmailRecoveryDnsInput, SessionKey) -> (variant { Ok : EmailRecoveryChallenge; Err : EmailRecoveryError }); + email_recovery_status : (text) -> (EmailRecoveryStatus) query; + email_recovery_submit_dkim_leaf : (EmailRecoverySubmitDkimLeafArg) -> (variant { Ok : EmailRecoveryStatus; Err : EmailRecoveryError }); + email_recovery_get_delegation : (EmailRecoveryGetDelegationArgs) -> (variant { Ok : SignedDelegation; Err : EmailRecoveryError }) query; + email_recovery_credential_remove : (IdentityNumber, text) -> (variant { Ok; Err : EmailRecoveryError }); + + // SMTP gateway protocol + // ===================== + // The off-chain SMTP gateway forwards every inbound message via + // smtp_request. The canister verifies the email cryptographically + // and dispatches by recipient: register@id.ai → setup completion, + // recover@id.ai → recovery delegation stamping. Always returns Ok + // — the gateway shouldn't get a per-message verification signal + // back. The FE sees outcomes via the polling status query. + smtp_request : (SmtpRequest) -> (SmtpResponse); + + // Called by the gateway at RCPT TO time to decide whether to + // accept the connection before pulling the message body. Returns + // Ok for register@id.ai / recover@id.ai (case-insensitive), and + // 550 (mailbox unavailable) for everything else. + smtp_request_validate : (SmtpRequest) -> (SmtpResponse) query; + // HTTP Gateway protocol // ===================== http_request : (request : HttpRequest) -> (HttpResponse) query;