use super::{CertificateRefusal, MachineIdentity};
use crate::auth_lys::{LysPassConsumer, lys_pass_adapter::LysPassAdapter};
use crate::config::peers::PeersConfig;
use lys_core::attestation::verify_attestation_bytes_by_signer;
use std::sync::Arc;
use std::time::Duration;
use zeroize::Zeroizing;
pub struct Standing {
base: reqwest::Url,
http: reqwest::Client,
handle: tokio::runtime::Handle,
adapter: Arc<LysPassAdapter>,
limits: PeersConfig,
}
impl std::fmt::Debug for Standing {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
out.debug_struct("CertificateStanding")
.finish_non_exhaustive()
}
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Answer {
pass: String,
expires_at: u64,
audience: String,
}
pub struct StandingPass {
token: Zeroizing<String>,
expires_at: u64,
}
impl std::fmt::Debug for StandingPass {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
out.debug_struct("StandingPass")
.field("bytes", &self.token.len())
.field("expires_at", &self.expires_at)
.finish_non_exhaustive()
}
}
impl StandingPass {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.token.as_bytes()
}
#[must_use]
pub const fn expires_at(&self) -> u64 {
self.expires_at
}
}
impl Standing {
pub fn new(
base: &str,
adapter: Arc<LysPassAdapter>,
handle: tokio::runtime::Handle,
limits: PeersConfig,
deadline: Duration,
) -> Result<Arc<Self>, CertificateRefusal> {
limits.validate().map_err(|_| CertificateRefusal::Bound)?;
if deadline.is_zero() {
return Err(CertificateRefusal::StatusUnavailable);
}
let mut base = super::service_key::endpoint(base)?;
let path = base
.path()
.strip_suffix("service-key")
.ok_or(CertificateRefusal::ServiceKey)?
.to_owned();
base.set_path(&path);
let http = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.timeout(deadline)
.build()
.map_err(|_| CertificateRefusal::StatusUnavailable)?;
Ok(Arc::new(Self {
base,
http,
handle,
adapter,
limits,
}))
}
pub fn endpoint(&self, holder: &str) -> Result<reqwest::Url, CertificateRefusal> {
if holder.is_empty()
|| !holder
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
return Err(CertificateRefusal::HolderMismatch);
}
self.base
.join(&format!("agents/{holder}/pass"))
.map_err(|_| CertificateRefusal::ServiceKey)
}
pub fn status_endpoint(&self) -> Result<reqwest::Url, CertificateRefusal> {
self.base
.join("mcp")
.map_err(|_| CertificateRefusal::ServiceKey)
}
#[must_use]
pub fn status_signature_path(endpoint: &reqwest::Url) -> &str {
endpoint
.path()
.strip_prefix("/api")
.unwrap_or_else(|| endpoint.path())
}
#[must_use]
pub const fn body() -> &'static [u8] {
br#"{"audience":"liminal"}"#
}
pub fn prove(
&self,
holder: &str,
machine: MachineIdentity,
der: &[u8],
header: &str,
now: u64,
) -> Result<StandingPass, CertificateRefusal> {
if header.len() > self.limits.max_proof_bytes.get()
|| der.len() > self.limits.max_certificate_bytes.get()
{
return Err(CertificateRefusal::Bound);
}
if der.is_empty() {
return Err(CertificateRefusal::Encoding);
}
let (pass_header, status_header) =
header.split_once('\n').ok_or(CertificateRefusal::Proof)?;
if status_header.contains(['\n', '\r']) {
return Err(CertificateRefusal::Proof);
}
let endpoint = self.endpoint(holder)?;
verify_proof(
holder,
machine,
pass_header,
endpoint.path(),
now,
self.limits.max_proof_bytes.get(),
self.limits.proof_window_ms.get(),
)?;
let status_endpoint = self.status_endpoint()?;
let status_body = super::status::request(holder)?;
verify_request(ProofRequest {
holder,
machine,
header: status_header,
path: Self::status_signature_path(&status_endpoint),
body: &status_body,
now,
bound: self.limits.max_proof_bytes.get(),
window_ms: self.limits.proof_window_ms.get(),
})?;
let token = std::thread::scope(|scope| {
scope
.spawn(|| {
self.handle.block_on(async {
self.ask_status(status_endpoint, status_header, status_body, holder, der)
.await?;
self.ask(endpoint, pass_header).await
})
})
.join()
})
.map_err(|_| CertificateRefusal::StatusUnavailable)??;
let observed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|_| CertificateRefusal::StatusUnavailable)?
.as_secs();
let now = now.max(observed);
let pass = self
.adapter
.verify_offline(token.as_bytes(), "liminal", now)
.map_err(|_| CertificateRefusal::Proof)?;
let claims = pass.verified().claims();
if claims.holder.id != holder
|| claims.holder.kind != "agent"
|| claims.exp <= now
|| claims.iat > now
|| claims.exp != token.expires_at
|| claims.exp.saturating_sub(claims.iat)
> self.limits.max_certificate_pass_lifetime_secs.get()
{
return Err(CertificateRefusal::Proof);
}
Ok(token)
}
async fn ask_status(
&self,
endpoint: reqwest::Url,
header: &str,
body: Vec<u8>,
holder: &str,
der: &[u8],
) -> Result<(), CertificateRefusal> {
let mut response = self
.http
.post(endpoint)
.header("lys-agent-signature", header)
.header("accept", "application/json, text/event-stream")
.header("content-type", "application/json")
.body(body)
.send()
.await
.map_err(|_| CertificateRefusal::StatusUnavailable)?;
let status = response.status();
let mut bytes = Zeroizing::new(Vec::new());
while let Some(chunk) = response
.chunk()
.await
.map_err(|_| CertificateRefusal::StatusUnavailable)?
{
if bytes
.len()
.checked_add(chunk.len())
.is_none_or(|length| length > self.limits.max_certificate_status_bytes.get())
{
return Err(CertificateRefusal::Bound);
}
bytes.extend_from_slice(&chunk);
}
if !status.is_success() {
return Err(issuer_refusal(&bytes)?);
}
super::status::response(&bytes, holder, der)
}
async fn ask(
&self,
endpoint: reqwest::Url,
header: &str,
) -> Result<StandingPass, CertificateRefusal> {
let mut response = self
.http
.post(endpoint)
.header("lys-agent-signature", header)
.header("content-type", "application/json")
.body(Self::body())
.send()
.await
.map_err(|_| CertificateRefusal::StatusUnavailable)?;
let status = response.status();
let mut bytes = Zeroizing::new(Vec::new());
while let Some(chunk) = response
.chunk()
.await
.map_err(|_| CertificateRefusal::StatusUnavailable)?
{
if bytes
.len()
.checked_add(chunk.len())
.is_none_or(|length| length > self.limits.max_pass_response_bytes.get())
{
return Err(CertificateRefusal::Bound);
}
bytes.extend_from_slice(&chunk);
}
if !status.is_success() {
return Err(issuer_refusal(&bytes)?);
}
let answer: Answer =
serde_json::from_slice(&bytes).map_err(|_| CertificateRefusal::StatusUnavailable)?;
let token = Zeroizing::new(answer.pass);
if answer.audience != "liminal" || answer.expires_at == 0 || token.is_empty() {
return Err(CertificateRefusal::Proof);
}
Ok(StandingPass {
token,
expires_at: answer.expires_at,
})
}
}
fn issuer_refusal(bytes: &[u8]) -> Result<CertificateRefusal, CertificateRefusal> {
let reason: serde_json::Value =
serde_json::from_slice(bytes).map_err(|_| CertificateRefusal::StatusUnavailable)?;
Ok(
if reason["refusal"] == "AgentSignatureRefused"
&& reason["reason"] == "AgentSignatureRefused: the signature does not verify"
{
CertificateRefusal::Withdrawn
} else {
CertificateRefusal::StatusUnavailable
},
)
}
pub fn verify_proof(
holder: &str,
machine: MachineIdentity,
header: &str,
path: &str,
now: u64,
bound: usize,
window_ms: u64,
) -> Result<(), CertificateRefusal> {
verify_request(ProofRequest {
holder,
machine,
header,
path,
body: Standing::body(),
now,
bound,
window_ms,
})
}
#[derive(Clone, Copy)]
struct ProofRequest<'a> {
holder: &'a str,
machine: MachineIdentity,
header: &'a str,
path: &'a str,
body: &'a [u8],
now: u64,
bound: usize,
window_ms: u64,
}
fn verify_request(proof: ProofRequest<'_>) -> Result<(), CertificateRefusal> {
let ProofRequest {
holder,
machine,
header,
path,
body,
now,
bound,
window_ms,
} = proof;
if bound == 0 || header.len() > bound {
return Err(CertificateRefusal::Bound);
}
let mut words = header.split_ascii_whitespace();
let agent = words.next().ok_or(CertificateRefusal::Proof)?;
let at: u64 = words
.next()
.ok_or(CertificateRefusal::Proof)?
.parse()
.map_err(|_| CertificateRefusal::Proof)?;
let nonce = words.next().ok_or(CertificateRefusal::Proof)?;
let signature = words.next().ok_or(CertificateRefusal::Proof)?;
let observed = now.checked_mul(1000).ok_or(CertificateRefusal::Proof)?;
if agent != holder
|| words.next().is_some()
|| window_ms == 0
|| observed.abs_diff(at) > window_ms
|| !hex::decode(nonce).is_ok_and(|bytes| bytes.len() == 16)
{
return Err(CertificateRefusal::Proof);
}
let cose = hex::decode(signature).map_err(|_| CertificateRefusal::Proof)?;
let payload = crate::link_pass::cose::payload(path, body, at, nonce);
verify_attestation_bytes_by_signer(&cose, &payload, machine.ed25519_public())
.map(|_| ())
.map_err(|_| CertificateRefusal::Proof)
}