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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! Ceremony tasks — the mechanism by which authority is granted, as distinct
//! from the operations that consume it.
//!
//! Three Type URIs carry their **own** authority in the document: a
//! `task-consent/decision` is authorized by the approver's Data-Integrity proof
//! plus membership of the policy-named approver set; a step-up
//! `approve-response` by the approver's proof plus the pending step-up it
//! echoes. In neither case does the submitting peer's standing at this VTA
//! decide anything — the handlers read the proven signer, and
//! [`is_ceremony_task`] is what keeps the PDP from re-gating them (approving a
//! task must not itself require approval).
//!
//! ## Why this module exists
//!
//! The predicate used to be private to `policy_gate`, which meant only *one* of
//! the two gates in front of a handler knew about it. The other — the ACL check
//! every intrinsic-sender transport runs on the authcrypt/TSP sender before
//! dispatch — applied to ceremony tasks like any other, and refused them.
//!
//! That inverts the model. The consent subsystem is explicitly built for an
//! approver that holds no authority to *act*: `task_consent::
//! compute_delegated_contexts` and `policy_gate`'s eligibility count both read
//! "absent from the ACL" as **confers nothing**, never as **cannot speak**.
//! `handle_decision` does not so much as look at its `AuthClaims`. The
//! least-privilege approver the whole `approve_scope` axis exists to serve — a
//! device that can confer a context and act in none — was nonetheless turned
//! away at the door, before any of the code written to accommodate it ran.
//!
//! Worse, it failed *silently*: the transport replies with a `permissionDenied`
//! envelope that an approver wallet has no reason to recognise, so the operator
//! sees an approval that was given, accepted by the human, and then simply
//! never took effect — while the requester re-submits into a pending that will
//! never be granted.
//!
//! So the two gates now share one predicate, and `messaging::auth::
//! auth_for_trust_task_envelope` lets a ceremony task through on a
//! zero-authority claim when — and only when — the ACL turns its sender away.
use crateAuthClaims;
/// Does this Type URI name a ceremony task?
///
/// Ceremony tasks carry their own authority (an approver's proof, a step-up
/// approve-response) and must NOT themselves be gated — else approving a task
/// could itself require consent/step-up, ad infinitum.
pub
/// Could `sender_did` conceivably be an approver for a ceremony of this type?
///
/// A cheap pre-filter, run *before* the carve-out lets an unenrolled sender past
/// the ACL. It answers only "is this sender in the population this ceremony is
/// for", never "is this decision valid" — the handlers still decide that, and
/// nothing here grants anything.
///
/// ## Why it exists
///
/// The carve-out costs the VTA a durable write. A `task-consent/decision` that
/// verifies but cites a digest with no live pending records
/// `consent.decision / denied:no_pending`, and audit retention is time-based,
/// not size-capped — so without a filter a remote party who can reach the
/// mediator could flood the audit log with rows that persist for the retention
/// window. That is not privilege escalation, but it is a new capability, and it
/// is cheap to remove.
///
/// For a consent decision the answer is an in-memory config read: membership of
/// *some* configured approver set. That is the entire population the carve-out
/// serves, so the filter costs nothing in function while shrinking the reachable
/// set from "any DID" to "the DIDs the operator named". The handler still checks
/// the specific set the pending named — this is a floor, not the decision.
///
/// Step-up `approve-response` is unfiltered, deliberately. Its authorized signer
/// is `pending.approver`, recorded when the step-up was minted and **not**
/// required to hold an ACL entry — that is the delegated phone-as-authorizer,
/// and no cheap membership test covers it. It needs no filter either:
/// `handle_approve_response` writes no audit row on `challenge_unknown`,
/// `subject_mismatch` or `approver_unauthorized`, so an unknown sender leaves no
/// durable trace to flood.
pub async
/// Read just the `type` member out of a Trust-Task envelope.
///
/// Deliberately not a full `TrustTask<Value>` parse: this runs *before*
/// authentication, on bytes from a peer we have not yet authorized, and it must
/// answer exactly one question — "is this a ceremony task?" — without taking a
/// position on anything else in the document. A body that doesn't parse, or
/// carries no `type`, yields `None` and is treated as an ordinary task, so a
/// malformed envelope can never talk its way past the ACL.
///
/// The envelope is re-parsed and fully validated by the dispatch spine
/// afterwards; nothing here is trusted beyond routing this one decision.
///
/// Only the intrinsic-sender transports need this — REST authenticates on a JWT
/// the caller had to obtain first, so there is no pre-auth routing decision to
/// make there.
pub
/// Claims for a ceremony task from a sender the ACL does not know.
///
/// Carries the **proven** sender DID — the authcrypt/TSP unpack established it,
/// and the replay-dedup key, the audit trail and `handle_approve_response`'s
/// `issuer == caller` check all read it — over the least-privileged role in the
/// system with no contexts. Per the workspace's act-scope rule, a non-`Admin`
/// role with an empty `allowed_contexts` is authorized *nowhere*, so if a
/// ceremony handler ever does consult its claims it is handed standing to do
/// precisely nothing.
///
/// No session row is created. An unenrolled approver is not a session-holder at
/// this VTA, and minting a session for one would let any DID that can reach the
/// mediator write a row. `session_id` mirrors the DID-keyed convention
/// [`vti_common::auth::session::resolve_did_session`] uses so the value lines up
/// with what an enrolled peer would see, without persisting anything.
pub