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
232
233
234
235
236
237
238
239
240
241
242
243
244
//! [`GrantError`]: why a statement or header was rejected.
/// A rejected grant, epoch or visibility statement, `X-Write-Grant` header,
/// owner signature or verification step.
///
/// There is one variant per SPEC-WRITE-GRANTS §3.5 rule family, plus the §4.2
/// header errors, the §4 owner-signature errors, the §7 verification steps
/// and the verifier configuration errors. [`GrantError::reason`] is stable text for logs and golden
/// fixtures. It is never shown to clients: the server maps every variant to
/// one Connect code (§11), `permission_denied` on writes and `not_found` on
/// private reads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
#[error("{}", self.reason())]
pub enum GrantError {
/// Longer than `MAX_STATEMENT_BYTES`.
StatementTooLong,
/// A carriage return anywhere in the statement.
CarriageReturn,
/// A byte outside `0x21..=0x7E` inside a field (other than CR).
ByteOutOfRange,
/// A line feed as the last byte.
FinalLineFeed,
/// A field count other than the statement's.
FieldCount,
/// An empty field.
EmptyField,
/// A domain other than the statement's separator.
Domain,
/// A namespace outside the SPEC-TRANSPORT-CONNECT §7.4 grammar.
Namespace,
/// A repository scope that is neither `<namespace>/<name>` (§7.4) nor
/// `<namespace>/*`.
RepositoryScope,
/// A repository scope in a namespace other than the `namespace` field.
ScopeNamespaceMismatch,
/// A decimal with a sign, a leading zero or a non-digit.
Decimal,
/// A decimal above its range (epoch `u64::MAX`, millis `i64::MAX`), or a
/// negative timestamp on encode.
DecimalOutOfRange,
/// Uppercase or wrong-length hexadecimal.
Hex,
/// An unknown capability, or capabilities not in canonical spelling.
Capabilities,
/// An audience that fails the auth v2 origin rules.
Audience,
/// A `*` anywhere in the audience list.
AudienceWildcard,
/// More than `MAX_AUDIENCES` audiences, or none (encode only).
AudienceCount,
/// Audiences out of ascending byte order, or duplicated.
AudiencesUnordered,
/// Ref scopes other than `-` on a `read` grant.
RefScopesOnRead,
/// Ref scopes `-` on a grant with `write`.
RefScopesMissing,
/// A ref-scope pattern outside §3.3 (or an entry without `=`).
RefPattern,
/// A pattern that is, or begins with, `refs/mkit/packmap/`.
PackmapPattern,
/// A flag other than `c`, `u`, `f`, `d`.
UnknownRefFlag,
/// Empty flags, or flags repeated or out of the `cufd` order.
RefFlagsNotCanonical,
/// More than `MAX_REF_SCOPES` entries, or none (encode only).
RefScopeCount,
/// Ref-scope entries out of ascending byte order, or duplicated.
RefScopesUnordered,
/// Two ref-scope entries with one pattern.
DuplicateRefPattern,
/// `expiry <= created`.
ExpiryNotAfterCreated,
/// `expiry - created` above the statement's maximum lifetime.
LifetimeTooLong,
/// A header value longer than `MAX_GRANT_HEADER_BYTES`.
HeaderTooLong,
/// A header value that is not three non-empty `.`-separated segments.
HeaderFormat,
/// A header segment that is not canonical unpadded base64url.
HeaderBase64,
/// A scheme token not defined in §4.
UnknownScheme,
/// A visibility statement's repository that is not a full
/// `<namespace>/<name>` identity (§9.1).
Repository,
/// A visibility other than `public` or `private` (§9.1).
Visibility,
/// §4: a scheme the deployment does not advertise.
SchemeNotAdvertised,
/// §4: a scheme not valid for the namespace form (`ed25519` needs an
/// `ed25519-` namespace; the ECDSA schemes need a `0x` namespace).
SchemeNamespaceMismatch,
/// §4: a signature blob of the wrong length for its scheme.
SignatureLength,
/// §4: the owner signature does not verify (including a non-canonical
/// Ed25519 signature or a small-order or invalid namespace key, and a
/// `secp256k1-eip191` signature from which no key recovers).
BadSignature,
/// §4: a `secp256k1-eip191` `v` other than 27 or 28.
SignatureRecoveryId,
/// §4.4: an ECDSA `r` or `s` outside `[1, n - 1]`.
SignatureScalar,
/// §4.4: an ECDSA `s` above `n / 2` (never normalized).
HighS,
/// §4.1, §7 step 4: the recovered or derived owner address is not the
/// namespace.
OwnerMismatch,
/// §4.1: a `webauthn-p256` public key that is not a valid P-256 point.
InvalidOwnerKey,
/// §4: a `webauthn-p256` blob that is not four length-prefixed fields
/// with a 64-byte public key and a 64-byte signature, and nothing after.
WebAuthnBlob,
/// §4.3 rule 1: `authenticatorData` shorter than 37 bytes.
AuthenticatorData,
/// §4.3 rule 1: the user-present flag is clear.
UserNotPresent,
/// §4.3 rule 4: the rpIdHash is not the SHA-256 of a configured relying
/// party id.
RelyingPartyMismatch,
/// §4.3 rule 2: `clientDataJSON` is not a JSON object, or has a
/// duplicate member name at some depth.
ClientData,
/// §4.3 rule 2: `type` is not the string `webauthn.get`.
ClientDataType,
/// §4.3 rule 2: `challenge` is not the unpadded base64url of the
/// statement's BLAKE3.
Challenge,
/// §4.3 rule 2: `crossOrigin` is present and not `false`.
CrossOrigin,
/// §4.3 rule 2: a `topOrigin` member is present.
TopOrigin,
/// §4.3 rule 4: `origin` is not an origin configured for the relying
/// party that signed.
OriginNotAllowed,
/// §7 step 2: the grant's namespace is not the request repository's.
NamespaceMismatch,
/// §9.1: a visibility statement's repository is not `X-Repository`.
RepositoryMismatch,
/// §7 step 5, §5.2 check 4, §9.1: the deployment's own audience is not
/// in `audiences` (byte comparison).
AudienceNotListed,
/// §7 step 6: the repository scope does not cover the repository.
RepositoryNotInScope,
/// §7 step 7: the capabilities do not cover the operation.
CapabilityNotGranted,
/// §7 step 9, §10: the grantee is not the signer or principal.
GranteeMismatch,
/// §7 step 10, §5.2 check 5, §9.1: `created > now + MAX_CLOCK_LEAD_MS`,
/// or a negative clock reading.
NotYetValid,
/// §7 step 10, §5.2 check 5, §9.1: `now >= expiry` (expiry is
/// exclusive).
Expired,
/// Verifier configuration: the deployment's own audience is a loopback
/// origin (§3.2, §10) and loopback audiences were not explicitly allowed.
LoopbackAudience,
/// Verifier configuration: `webauthn-p256` accepted without a configured
/// relying party (§4.3).
NoRelyingParty,
/// Verifier configuration: a relying party whose id is not a lowercase
/// DNS name, with no origins, an empty, non-ASCII or duplicate origin,
/// or an id configured twice.
RelyingParty,
/// Verifier configuration: a loopback relying party (id `localhost` or
/// `*.localhost`, or a loopback origin) and loopback was not explicitly
/// allowed.
LoopbackRelyingParty,
}
impl GrantError {
/// Stable reason text for logs and fixtures.
#[must_use]
pub fn reason(self) -> &'static str {
match self {
Self::StatementTooLong => "statement too long",
Self::CarriageReturn => "carriage return",
Self::ByteOutOfRange => "byte out of range",
Self::FinalLineFeed => "final line feed",
Self::FieldCount => "field count",
Self::EmptyField => "empty field",
Self::Domain => "domain",
Self::Namespace => "namespace",
Self::RepositoryScope => "repository scope",
Self::ScopeNamespaceMismatch => "scope namespace mismatch",
Self::Decimal => "noncanonical decimal",
Self::DecimalOutOfRange => "decimal out of range",
Self::Hex => "noncanonical hex",
Self::Capabilities => "capabilities",
Self::Audience => "audience",
Self::AudienceWildcard => "audience wildcard",
Self::AudienceCount => "audience count",
Self::AudiencesUnordered => "audiences unordered",
Self::RefScopesOnRead => "ref scopes on read grant",
Self::RefScopesMissing => "ref scopes missing",
Self::RefPattern => "ref pattern",
Self::PackmapPattern => "packmap pattern",
Self::UnknownRefFlag => "unknown ref flag",
Self::RefFlagsNotCanonical => "noncanonical ref flags",
Self::RefScopeCount => "ref scope count",
Self::RefScopesUnordered => "ref scopes unordered",
Self::DuplicateRefPattern => "duplicate ref pattern",
Self::ExpiryNotAfterCreated => "expiry not after created",
Self::LifetimeTooLong => "lifetime too long",
Self::HeaderTooLong => "header too long",
Self::HeaderFormat => "header format",
Self::HeaderBase64 => "header base64",
Self::UnknownScheme => "unknown scheme",
Self::Repository => "repository",
Self::Visibility => "visibility",
Self::SchemeNotAdvertised => "scheme not advertised",
Self::SchemeNamespaceMismatch => "scheme namespace mismatch",
Self::SignatureLength => "signature length",
Self::BadSignature => "bad signature",
Self::SignatureRecoveryId => "signature recovery id",
Self::SignatureScalar => "signature scalar",
Self::HighS => "high s",
Self::OwnerMismatch => "owner mismatch",
Self::InvalidOwnerKey => "invalid owner key",
Self::WebAuthnBlob => "webauthn blob",
Self::AuthenticatorData => "authenticator data",
Self::UserNotPresent => "user not present",
Self::RelyingPartyMismatch => "relying party mismatch",
Self::ClientData => "client data",
Self::ClientDataType => "client data type",
Self::Challenge => "challenge",
Self::CrossOrigin => "cross origin",
Self::TopOrigin => "top origin",
Self::OriginNotAllowed => "origin not allowed",
Self::NamespaceMismatch => "namespace mismatch",
Self::RepositoryMismatch => "repository mismatch",
Self::AudienceNotListed => "audience not listed",
Self::RepositoryNotInScope => "repository not in scope",
Self::CapabilityNotGranted => "capability not granted",
Self::GranteeMismatch => "grantee mismatch",
Self::NotYetValid => "not yet valid",
Self::Expired => "expired",
Self::LoopbackAudience => "loopback audience",
Self::NoRelyingParty => "no relying party",
Self::RelyingParty => "relying party",
Self::LoopbackRelyingParty => "loopback relying party",
}
}
}