use super::{Authority, Current, MachineConfig, MachinePass, PassRefusal, Renewal, renewal_at};
use crate::auth_lys::lys_pass_adapter::{
LiveAnswer, LiveLysAsk, LysPassAdapter, MachineLinkRights,
};
use crate::auth_lys::{LysPassConsumer, ResourceKind};
use lys_core::keys::Ed25519Identity;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use zeroize::Zeroizing;
pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
pub const ISSUED_AT: u64 = 1_700_000_000;
pub const LIFETIME: u64 = 600;
pub const NOW: u64 = ISSUED_AT + 3601;
pub const LINK: &str = "seats-to-peer";
fn encoded(bytes: &[u8]) -> String {
const ABC: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut output = String::new();
for chunk in bytes.chunks(3) {
let a = chunk[0];
let b = chunk.get(1).copied().unwrap_or(0);
let c = chunk.get(2).copied().unwrap_or(0);
output.push(char::from(ABC[usize::from(a >> 2)]));
output.push(char::from(ABC[usize::from((a & 3) << 4 | b >> 4)]));
if chunk.len() > 1 {
output.push(char::from(ABC[usize::from((b & 15) << 2 | c >> 6)]));
}
if chunk.len() > 2 {
output.push(char::from(ABC[usize::from(c & 63)]));
}
}
output
}
pub fn token(holder: &str, iat: u64, exp: u64, grant: bool) -> Result<Zeroizing<String>> {
let signer = Ed25519Identity::from_seed(&Zeroizing::new([9; 32]));
let header = encoded(br#"{"alg":"EdDSA","typ":"JWT","kid":"fixture"}"#);
let rights = if grant {
serde_json::json!([{"resource":{"kind":"liminal.link","id":LINK},
"actions":["link_import","link_export"],"mode":"outright","grant":"link-grant"}])
} else {
serde_json::json!([])
};
let claims = serde_json::json!({"iss":"https://lys.ablative.com.au","aud":"liminal",
"sub":holder,"iat":iat,"exp":exp,
"holder":{"kind":"agent","id":holder,"responsible":"owner"},"rights":rights});
let signing_input = format!("{header}.{}", encoded(&serde_json::to_vec(&claims)?));
Ok(Zeroizing::new(format!(
"{signing_input}.{}",
encoded(&signer.sign(signing_input.as_bytes()))
)))
}
#[derive(Debug)]
struct Ask;
impl LiveLysAsk for Ask {
fn ask(
&self,
_: &str,
_: &str,
_: &lys_pass::Target,
_: u64,
) -> std::result::Result<LiveAnswer, lys_pass::Error> {
Ok(LiveAnswer::Outright {
grant: "live-grant".to_owned(),
})
}
}
pub fn adapter() -> Result<Arc<LysPassAdapter>> {
let signer = Ed25519Identity::from_seed(&Zeroizing::new([9; 32]));
let keys = serde_json::json!({"keys":[{"kty":"OKP","crv":"Ed25519","kid":"fixture",
"x":encoded(&signer.public_key_bytes()),"alg":"EdDSA","use":"sig"}]})
.to_string();
Ok(Arc::new(
LysPassAdapter::from_keys_json(&keys, "https://lys.ablative.com.au".to_owned())?
.with_live(Arc::new(Ask)),
))
}
fn current(
adapter: &LysPassAdapter,
token: &[u8],
now: u64,
) -> std::result::Result<Current, PassRefusal> {
let pass = adapter
.verify_offline(token, "liminal", now)
.map_err(|error| super::named(error.name()))?;
let expiry = pass.verified().claims().exp;
Ok(Current {
rights: MachineLinkRights::new(adapter, pass),
expiry,
renew_at: renewal_at(expiry, now),
renewed: false,
})
}
struct Source {
token: Zeroizing<String>,
calls: Arc<AtomicUsize>,
refusal: bool,
hold: Option<Gate>,
}
type Gate = Mutex<(mpsc::Sender<()>, mpsc::Receiver<()>)>;
pub struct Hold {
pub entered: mpsc::Receiver<()>,
pub release: mpsc::Sender<()>,
}
impl std::fmt::Debug for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Source").finish_non_exhaustive()
}
}
impl Renewal for Source {
fn renew(
&self,
adapter: &LysPassAdapter,
now: u64,
) -> std::result::Result<(Zeroizing<String>, Current), PassRefusal> {
self.calls.fetch_add(1, Ordering::Relaxed);
if let Some(gate) = &self.hold {
let gate = gate
.lock()
.map_err(|_| PassRefusal("pass_holder_refused"))?;
gate.0
.send(())
.map_err(|_| PassRefusal("lys_could_not_be_asked"))?;
gate.1
.recv()
.map_err(|_| PassRefusal("lys_could_not_be_asked"))?;
drop(gate);
}
if self.refusal {
return Err(PassRefusal("ProviderUnavailable"));
}
Ok((
self.token.clone(),
current(adapter, self.token.as_bytes(), now)?,
))
}
}
pub struct Fixture {
pub(super) authority: Arc<Authority>,
pub(crate) calls: Arc<AtomicUsize>,
pub(crate) renewed: Zeroizing<String>,
root: tempfile::TempDir,
}
impl Fixture {
pub(crate) fn session(&self) -> Result<Box<dyn super::LysSession>> {
let pass = token("exporter", NOW, NOW + LIFETIME, true)?;
let admitter = super::ConsumerAdmitter::new(self.authority.adapter.clone());
Ok(Box::new(super::Session {
inner: super::LysAdmitter::admit(&admitter, pass.as_bytes(), NOW)?,
authority: self.authority.clone(),
}))
}
pub(crate) fn new(grant: bool, refusal: bool) -> Result<Self> {
let root = tempfile::tempdir()?;
let machine: MachineConfig = toml::from_str(&format!(
"identity_path = '{}'\nagent_id = 'importer'\naudience = 'liminal'",
root.path().join("identity").display()
))?;
let adapter = adapter()?;
let first = token("importer", ISSUED_AT, ISSUED_AT + LIFETIME, true)?;
let renewed = token("importer", NOW, NOW + LIFETIME, grant)?;
let calls = Arc::new(AtomicUsize::new(0));
let authority = Arc::new(Authority {
current: Mutex::new(Some(current(&adapter, first.as_bytes(), ISSUED_AT)?)),
pass: MachinePass::hold(&machine, Some(first))?,
dial: Some(Box::new(Source {
token: renewed.clone(),
calls: calls.clone(),
refusal,
hold: None,
})),
adapter,
boot_pending: std::sync::atomic::AtomicBool::new(false),
});
authority.pass.attach(&authority)?;
Ok(Self {
authority,
calls,
renewed,
root,
})
}
pub(crate) fn unissued(refusal: bool) -> Result<Self> {
Self::unissued_with(refusal, None)
}
pub(crate) fn unissued_held() -> Result<(Self, Hold)> {
let (entered_send, entered) = mpsc::channel();
let (release, release_receive) = mpsc::channel();
let fixture =
Self::unissued_with(false, Some(Mutex::new((entered_send, release_receive))))?;
Ok((fixture, Hold { entered, release }))
}
fn unissued_with(refusal: bool, hold: Option<Gate>) -> Result<Self> {
let root = tempfile::tempdir()?;
let machine: MachineConfig = toml::from_str(&format!(
"identity_path = '{}'\nagent_id = 'importer'\naudience = 'liminal'",
root.path().join("identity").display()
))?;
let adapter = adapter()?;
let renewed = token("importer", NOW, NOW + LIFETIME, true)?;
let calls = Arc::new(AtomicUsize::new(0));
let authority = Arc::new(Authority {
current: Mutex::new(None),
pass: MachinePass::hold(&machine, None)?,
dial: Some(Box::new(Source {
token: renewed.clone(),
calls: calls.clone(),
refusal,
hold,
})),
adapter,
boot_pending: std::sync::atomic::AtomicBool::new(false),
});
authority.pass.attach(&authority)?;
Ok(Self {
authority,
calls,
renewed,
root,
})
}
pub(crate) fn link() -> crate::auth_lys::AccessResource {
crate::auth_lys::AccessResource {
kind: ResourceKind::Link,
id: LINK.to_owned(),
}
}
pub(crate) fn finish(self) -> Result {
drop(self.authority);
self.root.close()?;
Ok(())
}
}