1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! M29-mirror — id_token `cat` profile-routing assertion (Phase 10.10).
//!
//! Symmetric to access-token's M29 (`engine::check_claims::run` line 137-140
//! — `cat == "access"` else `TokenTypeMismatch`). Closes the asymmetry where
//! `id_token::verify` had no value-side profile gate and relied on M72's
//! BASE_CLAIMS-omission of `cat` to implicitly forbid it.
//!
//! Phase 10.10 lifts that omission (id_token wire now carries `cat="id"` so
//! self-issued tokens round-trip cleanly via M72) and replaces the implicit
//! "forbid by allowlist absence" with an explicit value gate here.
//!
//! ── Why a separate engine submodule ─────────────────────────────────────
//!
//! Module-per-M-row is the established id_token-engine pattern (`check_nonce`,
//! `check_at_hash`, `check_c_hash`, `check_azp`, `check_auth_time`,
//! `check_acr`, `check_id_token_pii`). Inlining a 4-line value gate inside
//! `id_token::verify::verify` would diverge from that pattern; the audit
//! cost (one less greppable file when reviewing M-row coverage) outweighs
//! the LOC savings.
//!
//! The access-token side leaves M29 inline in `check_claims.rs` because that
//! module already validates the registered-claim cluster (M17-M30 + M32) and
//! M29 sits naturally with its iss/sub/aud/exp/iat siblings. id_token has no
//! analogous registered-claim aggregator yet (Phase 10.5-10.7 will add OIDC
//! exp/iat/iss/aud validation), so the M29-mirror lands as its own module.
//!
//! ── Order in `id_token::verify` ─────────────────────────────────────────
//!
//! Runs immediately AFTER `parse_payload_json` and BEFORE any typed gate
//! (M66 nonce, M67 at_hash, …). Rationale: this is a profile-routing check
//! ("is this even an id_token?"), so a wrong `cat` should short-circuit the
//! id_token-specific pipeline before it spends cycles on nonce/binding
//! comparisons. The position parallels access-token's M29 firing inside
//! `check_claims` BEFORE the domain-layer M45 catch-net.
//!
//! ── Why pinned to the literal `"id"` ────────────────────────────────────
//!
//! Symmetric to access-token's `cat == "access"` literal. PAS issues
//! exactly two token categories today (`access`, `id`); a future third
//! category (e.g. refresh) lands as a new variant + verifier, not as a
//! configurable axis on this check. No-agility mirrors the engine's
//! algorithm pinning (`Algorithm::EdDSA` only) and PASETO v4.public
//! lineage: zero negotiation surface for attackers.
use crateAuthError;
/// Refuse any id_token whose `cat` payload claim is not exactly `"id"`.
///
/// Absent `cat` collapses to the empty string, surfacing as
/// `CatMismatch("")` — distinct audit signal from `CatMismatch("access")`
/// (substitution attempt) or `CatMismatch("<other>")` (bespoke forgery /
/// non-ppoppo issuer drift). Treating absence as error rather than admit
/// matches access-token M29 (`unwrap_or("")` then strict equality).
pub