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
//! [`redact_passport`] — the audience-filtered view of a whole passport.
//!
//! # Why this is not a method on `Passport`
//!
//! It was one, and that is how the leak happened. `Passport` sits below this
//! module on the tier ladder, so an inherent `redact` could not reach
//! [`filter_by_audience`] without pointing an import down the ladder. It
//! therefore grew its own redaction — a second implementation of a
//! compliance-critical rule, with nothing proving the two agreed. They did not:
//! the copy served `seal`, `publicJwsSignature` and `disclosureSignatures` to
//! every audience, because those had no disclosure class and absent defaulted to
//! public.
//!
//! Redaction is the access layer's job. Putting it here lets it use the one
//! filter, and the ladder now enforces that rather than merely suggesting it.
use crateAudience;
use crate;
use ;
/// Return an audience-filtered JSON view of `passport`.
///
/// Three rules, in order:
///
/// 1. **Proofs never travel in a view.** Every key in
/// [`PASSPORT_PROOF_FIELDS`] is removed for every audience, without
/// exception. A view is a payload; whoever serves it attaches the one
/// proof that covers exactly the bytes being sent. See that constant for
/// why this is not expressible as a disclosure class.
/// 2. **Envelope fields follow [`crate::disclosure::PASSPORT_FIELD_DISCLOSURE`]**, applied
/// through the shared scope-aware filter so a product group's schema can
/// never reclassify an envelope field by declaring a property of the same
/// name.
/// 3. **Product-group data follows the policy for *this passport's* schema
/// version**, not the catalog's current one.
///
/// # Why there is no `catalog` parameter
///
/// There used to be, and it resolved the policy through the catalog's single
/// unversioned disclosure map. A passport's signatures are frozen over the
/// redaction that produced them, so filtering by whatever that map says
/// *today* applies rules that may postdate the signature: the served body and
/// its proof then disagree for reasons no reader can distinguish from
/// tampering, and one reclassification breaks verification for every
/// already-published passport at once.
///
/// The passport carries its own [`schema_version`](Passport::schema_version), so
/// the correct version is never the caller's to supply — and with no
/// parameter there is no way to supply the wrong one.
///
/// # Failing closed
///
/// An unknown product group, or a schema version this build does not carry,
/// resolves to no policy. Product-group data is then reduced to its
/// `productGroup` tag for **every** audience rather than served unfiltered:
/// without per-field classes this crate cannot tell which fields are safe to
/// expose, so it exposes none. Envelope fields still apply, because they are
/// version-independent.
/// Drop any `productGroupData` key the passport's **declared schema version**
/// does not declare.
///
/// # The hazard this closes
///
/// Sourcing classes from the version a passport was validated against is what
/// keeps a published passport filtered by the rules that produced its
/// signatures. The corollary is the danger: a key that version does not declare
/// is classified by nobody, and an unclassified key falls to the policy default
/// — `Public`. The version-pinned policy is safer than the catalog's single map
/// in every other respect and *less* safe in exactly this one, because it
/// under-applies where the map over-applied. **Under-applying disclosure is a
/// leak.**
///
/// Raising `default_disclosure` does not work: this policy covers the whole
/// document, and the envelope's public fields — `productName`, `status` — are
/// declared in no product group schema, so they would all vanish. The guard has
/// to be structural and scoped to `productGroupData`: where nobody has
/// classified the content, keep only what is accounted for.
///
/// Reaching it needs an **invalid** passport, since every product group schema
/// sets `additionalProperties: false` and validation rejects undeclared keys.
/// This is defence in depth. It runs for **every** audience, because an
/// unclassified field is not more disclosable to a credentialed reader than to
/// an anonymous one.