use super::{GrantStreamConfig, StreamError, StreamState, http::Http};
use crate::auth_revocation::{
AuthenticatedBinding, BoundRightClaim, Digest32, GrantBindingSource, GrantId, GrantLogId,
Revision, RevocationRefusal, RightAction,
};
use lys_pass::binding::VerifiedBinding;
use lys_pass::{KeySet, VerifiedPass};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
use std::sync::Arc;
pub(super) struct BindingSource {
keys: KeySet,
issuer: String,
identity: Digest32,
state: Arc<StreamState>,
config: GrantStreamConfig,
http: Http,
handle: tokio::runtime::Handle,
}
impl std::fmt::Debug for BindingSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BindingSource")
.field("identity", &self.identity)
.finish_non_exhaustive()
}
}
#[derive(Serialize)]
struct Question<'a> {
binding: u32,
pass: String,
iss: &'a str,
sub: &'a str,
aud: &'a str,
iat: u64,
exp: u64,
grants: BTreeSet<&'a str>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Answer {
grant_binding: String,
revision: u64,
}
impl Drop for Answer {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.grant_binding);
}
}
impl BindingSource {
pub fn new(
lys: &crate::config::types::LysConfig,
config: GrantStreamConfig,
state: Arc<StreamState>,
credential: zeroize::Zeroizing<String>,
handle: tokio::runtime::Handle,
) -> Result<Self, StreamError> {
let key_json = super::http::read_file(&lys.keys_path, config.max_keyset_bytes)?;
let keys = KeySet::from_json(&key_json)
.map_err(|_| StreamError::named("grant_stream_keys_invalid"))?;
let identity = Digest32(Sha256::digest(key_json.as_bytes()).into());
let http = Http::new(
lys.api_url.as_deref().unwrap_or(&lys.issuer),
credential,
config.max_response_bytes,
state.cancellation(),
)?;
Ok(Self {
keys,
issuer: lys.issuer.clone(),
identity,
state,
config,
http,
handle,
})
}
pub const fn identity(&self) -> Digest32 {
self.identity
}
fn token<'a>(&self, credential: &'a [u8]) -> Result<&'a str, RevocationRefusal> {
if credential.len() > self.config.max_credential_bytes {
return Err(RevocationRefusal::BindingMissing);
}
let liminal_protocol::credential::WireCredential::LysPass(bytes) =
liminal_protocol::credential::WireCredential::classify(credential)
.map_err(|_| RevocationRefusal::BindingMissing)?
else {
return Err(RevocationRefusal::BindingMissing);
};
std::str::from_utf8(bytes).map_err(|_| RevocationRefusal::BindingMissing)
}
fn acquire(
&self,
token: &str,
pass: &VerifiedPass,
) -> Result<VerifiedBinding, RevocationRefusal> {
let claims = pass.claims();
if claims.rights.len() > self.config.max_rights || claims.rights_truncated.is_some() {
return Err(RevocationRefusal::BindingMissing);
}
let question = Question {
binding: lys_pass::binding::BINDING_VERSION,
pass: lys_pass::binding::pass_digest(token),
iss: &claims.iss,
sub: &claims.sub,
aud: &claims.aud,
iat: claims.iat,
exp: claims.exp,
grants: claims
.rights
.iter()
.map(|right| right.grant.as_str())
.collect(),
};
let answer = std::thread::scope(|scope| {
scope
.spawn(|| {
self.handle
.block_on(self.http.post::<_, Answer>("grants/bindings", &question))
})
.join()
})
.map_err(|_| RevocationRefusal::BindingMissing)?
.map_err(|_| RevocationRefusal::BindingMissing)?;
if answer.grant_binding.len() > self.config.max_credential_bytes {
return Err(RevocationRefusal::BindingMissing);
}
let binding = VerifiedBinding::verify(
&answer.grant_binding,
token,
pass,
&self.keys,
&self.state.log,
)
.map_err(|_| RevocationRefusal::BindingMissing)?;
if answer.revision != binding.revision()
|| binding
.claims()
.dependencies
.iter()
.any(|dependency| dependency.path.len() > self.config.max_chain_grants)
{
return Err(RevocationRefusal::BindingMissing);
}
Ok(binding)
}
fn rights(
&self,
pass: &VerifiedPass,
binding: &VerifiedBinding,
) -> Result<Vec<BoundRightClaim>, RevocationRefusal> {
let mut rights = Vec::new();
let mut admission = Vec::new();
for right in &pass.claims().rights {
if right.mode != lys_pass::rights::Mode::Outright {
continue;
}
let dependency = binding
.dependency(&right.grant)
.ok_or(RevocationRefusal::BindingMissing)?;
let chain = dependency
.path
.iter()
.map(|grant| GrantId::new(grant).map_err(|_| RevocationRefusal::BindingMissing))
.collect::<Result<Vec<_>, _>>()?;
let target = &self.config.admission;
if right.resource.kind == target.kind
&& right.resource.id == target.id
&& right.actions.contains(&target.action)
&& !admission.contains(&chain)
{
if admission.len() >= self.config.max_chains {
return Err(RevocationRefusal::BindingMissing);
}
admission.push(chain.clone());
}
for mapping in &self.config.subjects {
if mapping.kind != right.resource.kind || mapping.resource != right.resource.id {
continue;
}
for (word, action) in [
("publish", crate::auth_lys::HotAction::Publish),
("subscribe", crate::auth_lys::HotAction::Subscribe),
("call", crate::auth_lys::HotAction::Call),
("serve", crate::auth_lys::HotAction::Serve),
] {
if right.actions.iter().any(|granted| granted == word) {
self.merge(
&mut rights,
RightAction::Hot(action),
&mapping.prefix,
&chain,
)?;
}
}
}
}
if admission.is_empty() || rights.len() >= self.config.max_rights {
return Err(RevocationRefusal::BindingMissing);
}
rights.push(BoundRightClaim {
action: RightAction::Connect,
prefix: String::new(),
chains: admission,
});
Ok(rights)
}
fn merge(
&self,
rights: &mut Vec<BoundRightClaim>,
action: RightAction,
prefix: &str,
chain: &[GrantId],
) -> Result<(), RevocationRefusal> {
if let Some(known) = rights
.iter_mut()
.find(|known| known.action == action && known.prefix == prefix)
{
if !known.chains.iter().any(|known| known == chain) {
if known.chains.len() >= self.config.max_chains {
return Err(RevocationRefusal::BindingMissing);
}
known.chains.push(chain.to_vec());
}
} else {
if rights.len() >= self.config.max_rights {
return Err(RevocationRefusal::BindingMissing);
}
rights.push(BoundRightClaim {
action,
prefix: prefix.into(),
chains: vec![chain.to_vec()],
});
}
Ok(())
}
}
impl GrantBindingSource for BindingSource {
fn pass_digest(&self, credential: &[u8]) -> Result<Digest32, RevocationRefusal> {
Ok(Digest32(
Sha256::digest(self.token(credential)?.as_bytes()).into(),
))
}
fn binding_for(&self, credential: &[u8]) -> Result<AuthenticatedBinding, RevocationRefusal> {
let token = self.token(credential)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| RevocationRefusal::BindingMissing)?
.as_secs();
let pass = VerifiedPass::verify(token, &self.keys, &self.issuer, "liminal", now)
.map_err(|_| RevocationRefusal::BindingMissing)?;
let binding = self.acquire(token, &pass)?;
Ok(AuthenticatedBinding {
issuer: self.identity,
audience: "liminal".into(),
holder: pass.claims().holder.id.as_bytes().to_vec(),
pass_digest: self.pass_digest(credential)?,
log: GrantLogId::new(&binding.claims().log.identity, binding.claims().log.epoch)
.map_err(|_| RevocationRefusal::LogReset)?,
required: Revision::new(binding.revision()),
rights: self.rights(&pass, &binding)?,
})
}
}