1use std::collections::{BTreeMap, BTreeSet};
20
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23
24use crate::Error;
25use crate::membership::GrantLog;
26use crate::verify::{KeySet, VerifiedPass, signed_payload};
27
28pub const BINDING_VERSION: u32 = 1;
30pub const BINDING_REQUEST: &str = "1";
32pub const BINDING_TYPE: &str = "lys-grant-binding+jwt";
34
35pub const BINDING_REQUIRED: &str = "grant_binding_required";
37pub const BINDING_UNSUPPORTED: &str = "grant_binding_unsupported";
39pub const BINDING_MISMATCH: &str = "grant_binding_mismatch";
41pub const BINDING_LOG_MISMATCH: &str = "grant_binding_log_mismatch";
43pub const BINDING_ANCESTRY: &str = "grant_binding_ancestry_refused";
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = PassGrantDependency))]
50pub struct Dependency {
51 pub grant: String,
53 pub path: Vec<String>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = PassGrantBinding))]
61pub struct BindingClaims {
62 pub binding: u32,
64 pub iss: String,
66 pub sub: String,
68 pub aud: String,
70 pub iat: u64,
72 pub exp: u64,
74 pub pass: String,
76 pub log: GrantLog,
78 pub revision: u64,
81 pub dependencies: Vec<Dependency>,
83}
84
85fn refused(name: &'static str, reason: &'static str) -> Error {
86 Error::Binding { name, reason }
87}
88
89#[must_use]
91pub fn pass_digest(token: &str) -> String {
92 const DIGITS: &[u8; 16] = b"0123456789abcdef";
93 let digest = Sha256::digest(token.as_bytes());
94 let mut text = String::with_capacity(digest.len() * 2);
95 for byte in digest {
96 text.push(char::from(DIGITS[usize::from(byte >> 4)]));
97 text.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
98 }
99 text
100}
101
102pub fn required(binding: Option<&str>) -> Result<&str, Error> {
104 binding
105 .filter(|binding| !binding.is_empty())
106 .ok_or_else(|| refused(BINDING_REQUIRED, "the pass carries no grant binding"))
107}
108
109#[derive(Debug, Clone)]
112pub struct Bound<'a> {
113 pub pass: &'a str,
115 pub iss: &'a str,
117 pub sub: &'a str,
119 pub aud: &'a str,
121 pub iat: u64,
123 pub exp: u64,
125 pub grants: &'a BTreeSet<&'a str>,
127}
128
129#[derive(Debug)]
132pub struct VerifiedBinding {
133 claims: BindingClaims,
134 by_grant: BTreeMap<String, usize>,
135}
136
137impl VerifiedBinding {
138 pub fn verify(
141 binding: &str,
142 token: &str,
143 pass: &VerifiedPass,
144 keys: &KeySet,
145 log: &GrantLog,
146 ) -> Result<Self, Error> {
147 let signed = pass.claims();
148 let digest = pass_digest(token);
149 let grants: BTreeSet<&str> = signed
150 .rights
151 .iter()
152 .map(|right| right.grant.as_str())
153 .collect();
154 Self::verify_bound(
155 binding,
156 &Bound {
157 pass: &digest,
158 iss: &signed.iss,
159 sub: &signed.sub,
160 aud: &signed.aud,
161 iat: signed.iat,
162 exp: signed.exp,
163 grants: &grants,
164 },
165 keys,
166 log,
167 )
168 }
169
170 pub fn verify_bound(
174 binding: &str,
175 bound: &Bound<'_>,
176 keys: &KeySet,
177 log: &GrantLog,
178 ) -> Result<Self, Error> {
179 let payload: serde_json::Value =
180 serde_json::from_slice(&signed_payload(binding, keys, BINDING_TYPE)?)?;
181 if payload.get("binding").and_then(serde_json::Value::as_u64)
182 != Some(u64::from(BINDING_VERSION))
183 {
184 return Err(refused(
185 BINDING_UNSUPPORTED,
186 "the binding is not of a version this verifier reads",
187 ));
188 }
189 let claims: BindingClaims = serde_json::from_value(payload)?;
190 if claims.pass != bound.pass {
191 return Err(refused(BINDING_MISMATCH, "the binding names another pass"));
192 }
193 if (
194 claims.iss.as_str(),
195 claims.sub.as_str(),
196 claims.aud.as_str(),
197 claims.iat,
198 claims.exp,
199 ) != (bound.iss, bound.sub, bound.aud, bound.iat, bound.exp)
200 {
201 return Err(refused(
202 BINDING_MISMATCH,
203 "the binding names another issuer, holder, audience or lifetime",
204 ));
205 }
206 if &claims.log != log {
207 return Err(refused(
208 BINDING_LOG_MISMATCH,
209 "the binding names another grant log or reset epoch",
210 ));
211 }
212 let mut by_grant = BTreeMap::new();
213 for (position, dependency) in claims.dependencies.iter().enumerate() {
214 if dependency.path.first() != Some(&dependency.grant) {
215 return Err(refused(
216 BINDING_ANCESTRY,
217 "a dependency's path does not begin at its grant",
218 ));
219 }
220 let mut seen = BTreeSet::new();
221 if dependency
222 .path
223 .iter()
224 .any(|grant| grant.is_empty() || !seen.insert(grant))
225 {
226 return Err(refused(
227 BINDING_ANCESTRY,
228 "a dependency's path is empty or names a grant twice",
229 ));
230 }
231 if by_grant
232 .insert(dependency.grant.clone(), position)
233 .is_some()
234 {
235 return Err(refused(BINDING_ANCESTRY, "a grant has two dependencies"));
236 }
237 }
238 if bound.grants.len() != by_grant.len()
239 || bound
240 .grants
241 .iter()
242 .any(|grant| !by_grant.contains_key(*grant))
243 {
244 return Err(refused(
245 BINDING_ANCESTRY,
246 "the dependencies are not exactly the grants the pass's rights name",
247 ));
248 }
249 Ok(Self { claims, by_grant })
250 }
251
252 #[must_use]
254 pub const fn claims(&self) -> &BindingClaims {
255 &self.claims
256 }
257
258 #[must_use]
260 pub const fn revision(&self) -> u64 {
261 self.claims.revision
262 }
263
264 #[must_use]
267 pub fn dependency(&self, grant: &str) -> Option<&Dependency> {
268 self.by_grant
269 .get(grant)
270 .and_then(|position| self.claims.dependencies.get(*position))
271 }
272}