use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::Error;
use crate::membership::GrantLog;
use crate::verify::{KeySet, VerifiedPass, signed_payload};
pub const BINDING_VERSION: u32 = 1;
pub const BINDING_REQUEST: &str = "1";
pub const BINDING_TYPE: &str = "lys-grant-binding+jwt";
pub const BINDING_REQUIRED: &str = "grant_binding_required";
pub const BINDING_UNSUPPORTED: &str = "grant_binding_unsupported";
pub const BINDING_MISMATCH: &str = "grant_binding_mismatch";
pub const BINDING_LOG_MISMATCH: &str = "grant_binding_log_mismatch";
pub const BINDING_ANCESTRY: &str = "grant_binding_ancestry_refused";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = PassGrantDependency))]
pub struct Dependency {
pub grant: String,
pub path: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = PassGrantBinding))]
pub struct BindingClaims {
pub binding: u32,
pub iss: String,
pub sub: String,
pub aud: String,
pub iat: u64,
pub exp: u64,
pub pass: String,
pub log: GrantLog,
pub revision: u64,
pub dependencies: Vec<Dependency>,
}
fn refused(name: &'static str, reason: &'static str) -> Error {
Error::Binding { name, reason }
}
#[must_use]
pub fn pass_digest(token: &str) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let digest = Sha256::digest(token.as_bytes());
let mut text = String::with_capacity(digest.len() * 2);
for byte in digest {
text.push(char::from(DIGITS[usize::from(byte >> 4)]));
text.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
}
text
}
pub fn required(binding: Option<&str>) -> Result<&str, Error> {
binding
.filter(|binding| !binding.is_empty())
.ok_or_else(|| refused(BINDING_REQUIRED, "the pass carries no grant binding"))
}
#[derive(Debug, Clone)]
pub struct Bound<'a> {
pub pass: &'a str,
pub iss: &'a str,
pub sub: &'a str,
pub aud: &'a str,
pub iat: u64,
pub exp: u64,
pub grants: &'a BTreeSet<&'a str>,
}
#[derive(Debug)]
pub struct VerifiedBinding {
claims: BindingClaims,
by_grant: BTreeMap<String, usize>,
}
impl VerifiedBinding {
pub fn verify(
binding: &str,
token: &str,
pass: &VerifiedPass,
keys: &KeySet,
log: &GrantLog,
) -> Result<Self, Error> {
let signed = pass.claims();
let digest = pass_digest(token);
let grants: BTreeSet<&str> = signed
.rights
.iter()
.map(|right| right.grant.as_str())
.collect();
Self::verify_bound(
binding,
&Bound {
pass: &digest,
iss: &signed.iss,
sub: &signed.sub,
aud: &signed.aud,
iat: signed.iat,
exp: signed.exp,
grants: &grants,
},
keys,
log,
)
}
pub fn verify_bound(
binding: &str,
bound: &Bound<'_>,
keys: &KeySet,
log: &GrantLog,
) -> Result<Self, Error> {
let payload: serde_json::Value =
serde_json::from_slice(&signed_payload(binding, keys, BINDING_TYPE)?)?;
if payload.get("binding").and_then(serde_json::Value::as_u64)
!= Some(u64::from(BINDING_VERSION))
{
return Err(refused(
BINDING_UNSUPPORTED,
"the binding is not of a version this verifier reads",
));
}
let claims: BindingClaims = serde_json::from_value(payload)?;
if claims.pass != bound.pass {
return Err(refused(BINDING_MISMATCH, "the binding names another pass"));
}
if (
claims.iss.as_str(),
claims.sub.as_str(),
claims.aud.as_str(),
claims.iat,
claims.exp,
) != (bound.iss, bound.sub, bound.aud, bound.iat, bound.exp)
{
return Err(refused(
BINDING_MISMATCH,
"the binding names another issuer, holder, audience or lifetime",
));
}
if &claims.log != log {
return Err(refused(
BINDING_LOG_MISMATCH,
"the binding names another grant log or reset epoch",
));
}
let mut by_grant = BTreeMap::new();
for (position, dependency) in claims.dependencies.iter().enumerate() {
if dependency.path.first() != Some(&dependency.grant) {
return Err(refused(
BINDING_ANCESTRY,
"a dependency's path does not begin at its grant",
));
}
let mut seen = BTreeSet::new();
if dependency
.path
.iter()
.any(|grant| grant.is_empty() || !seen.insert(grant))
{
return Err(refused(
BINDING_ANCESTRY,
"a dependency's path is empty or names a grant twice",
));
}
if by_grant
.insert(dependency.grant.clone(), position)
.is_some()
{
return Err(refused(BINDING_ANCESTRY, "a grant has two dependencies"));
}
}
if bound.grants.len() != by_grant.len()
|| bound
.grants
.iter()
.any(|grant| !by_grant.contains_key(*grant))
{
return Err(refused(
BINDING_ANCESTRY,
"the dependencies are not exactly the grants the pass's rights name",
));
}
Ok(Self { claims, by_grant })
}
#[must_use]
pub const fn claims(&self) -> &BindingClaims {
&self.claims
}
#[must_use]
pub const fn revision(&self) -> u64 {
self.claims.revision
}
#[must_use]
pub fn dependency(&self, grant: &str) -> Option<&Dependency> {
self.by_grant
.get(grant)
.and_then(|position| self.claims.dependencies.get(*position))
}
}