use liminal_server::auth_lys::{
AccessAction, AccessDecisionError, AccessRefusal, AccessResource, GrantReceipt,
LysPassConsumer, RefusalCause, ResourceKind, SubjectBinding,
lys_pass_adapter::{LysPassAdapter, MachineLinkRights},
};
use liminal_server::link::{Link, LinkAction, LinkError, LinkRefusal, LinkRights};
use liminal_server::link_handshake::{
LinkHandshake, LinkHandshakeError, LocalMachine, MachineIdentity, NOISE_OVERHEAD,
};
use lys_core::keys::Ed25519Identity;
use zeroize::Zeroizing;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn machine(seed: u8) -> LocalMachine {
LocalMachine::new(Ed25519Identity::from_seed(&Zeroizing::new([seed; 32])))
}
#[derive(Debug)]
struct Rights {
export: &'static [&'static str],
import: &'static [&'static str],
}
impl LinkRights for Rights {
fn holds(
&self,
action: LinkAction,
subject: &SubjectBinding,
_now: u64,
) -> Result<GrantReceipt, AccessDecisionError> {
let prefixes = match action {
LinkAction::Export => self.export,
LinkAction::Import => self.import,
};
let reached = prefixes.iter().find(|prefix| {
subject.subject() == **prefix || subject.subject().starts_with(&format!("{prefix}."))
});
reached.map_or_else(
|| {
Err(AccessDecisionError {
cause: RefusalCause::MissingGrant,
refusal: AccessRefusal::new(
AccessResource {
kind: ResourceKind::Subject,
id: subject.resource().id.clone(),
},
action.access_action(),
format!("https://lys.example/ask/{}", subject.subject()),
),
})
},
|prefix| {
Ok(GrantReceipt {
grant: format!("{prefix}:{action:?}"),
})
},
)
}
}
fn exchange(
initiator: &mut LinkHandshake,
responder: &mut LinkHandshake,
) -> Result<(), LinkHandshakeError> {
let mut first = [0_u8; NOISE_OVERHEAD];
let written = initiator.write(&mut first)?;
responder.read(&first[..written])?;
let mut second = [0_u8; NOISE_OVERHEAD];
let written = responder.write(&mut second)?;
initiator.read(&second[..written])
}
#[test]
fn two_machines_keyed_from_their_lys_identities_complete_the_handshake() -> TestResult {
let exporter = machine(1);
let importer = machine(2);
let mut from_exporter = LinkHandshake::initiator(&exporter, importer.machine())?;
let mut at_importer = LinkHandshake::responder(&importer, exporter.machine())?;
exchange(&mut from_exporter, &mut at_importer)?;
assert!(from_exporter.is_finished() && at_importer.is_finished());
let mut sending = from_exporter.finish()?;
let mut receiving = at_importer.finish()?;
assert_eq!(receiving.peer(), &exporter.machine());
let mut frame = [0_u8; 64 + NOISE_OVERHEAD];
let sealed = sending.seal(b"orders.created", &mut frame)?;
let mut opened = [0_u8; 64];
let length = receiving.open(&frame[..sealed], &mut opened)?;
assert_eq!(&opened[..length], b"orders.created");
Ok(())
}
#[test]
fn a_third_machine_is_refused_at_the_handshake() -> TestResult {
let exporter = machine(1);
let importer = machine(2);
let third = machine(3);
let mut from_third = LinkHandshake::initiator(&third, importer.machine())?;
let mut at_importer = LinkHandshake::responder(&importer, exporter.machine())?;
let mut first = [0_u8; NOISE_OVERHEAD];
let written = from_third.write(&mut first)?;
assert_eq!(
at_importer.read(&first[..written]),
Err(LinkHandshakeError::PeerNotTheExpectedMachine)
);
assert!(!at_importer.is_finished());
assert_eq!(
at_importer.finish().map(|_| ()),
Err(LinkHandshakeError::NotFinished)
);
Ok(())
}
#[test]
fn a_key_derived_any_other_way_fails_the_handshake() -> TestResult {
let exporter = machine(1);
let importer = machine(2);
let wrong = MachineIdentity::new(machine(9).machine().ed25519_public().to_owned());
let mut from_exporter = LinkHandshake::initiator(&exporter, importer.machine())?;
let mut at_importer = LinkHandshake::responder(&importer, wrong)?;
let mut first = [0_u8; NOISE_OVERHEAD];
let written = from_exporter.write(&mut first)?;
assert_eq!(
at_importer.read(&first[..written]),
Err(LinkHandshakeError::PeerNotTheExpectedMachine)
);
Ok(())
}
#[test]
fn an_identity_that_is_not_an_ed25519_point_cannot_key_a_link() {
let mut bytes = [0_u8; 32];
bytes[0] = 2;
let not_a_point = MachineIdentity::new(bytes);
assert_eq!(
not_a_point.x25519_public(),
Err(LinkHandshakeError::InvalidIdentity)
);
}
#[test]
fn messages_cross_only_on_the_prefix_both_ends_hold() -> TestResult {
let link = Link::new(
"network-a".to_owned(),
"link-7".to_owned(),
machine(1).machine(),
machine(2).machine(),
)?;
let exporter = Rights {
export: &["orders", "invoices"],
import: &[],
};
let importer = Rights {
export: &[],
import: &["orders"],
};
let crossing = link.crossing(&exporter, &importer, "orders.created", 5)?;
assert_eq!(crossing.export.grant, "orders:Export");
assert_eq!(crossing.import.grant, "orders:Import");
let refused = refusal_of(link.crossing(&exporter, &importer, "invoices.paid", 5))?;
assert!(
matches!(&refused, LinkRefusal::Import(error) if error.cause == RefusalCause::MissingGrant),
"refused at the importing end by name: {refused}"
);
let refused = refusal_of(link.crossing(&exporter, &importer, "shipments.sent", 5))?;
assert!(
matches!(&refused, LinkRefusal::Export(_)),
"the exporting end refuses first: {refused}"
);
let refused = refusal_of(link.crossing(&exporter, &importer, "orders.*", 5))?;
assert!(matches!(&refused, LinkRefusal::Binding(_)), "{refused}");
Ok(())
}
fn refusal_of<T: std::fmt::Debug>(
crossing: Result<T, LinkRefusal>,
) -> Result<LinkRefusal, Box<dyn std::error::Error>> {
match crossing {
Ok(crossed) => Err(format!("the crossing was not refused: {crossed:?}").into()),
Err(refusal) => Ok(refusal),
}
}
#[test]
fn a_link_joins_two_different_machines_under_a_network() -> TestResult {
let same = machine(1).machine();
assert_eq!(
Link::new("network-a".to_owned(), "link-1".to_owned(), same, same).map(|_| ()),
Err(LinkError::SameMachine)
);
assert_eq!(
Link::new(
String::new(),
"link-1".to_owned(),
same,
machine(2).machine()
)
.map(|_| ()),
Err(LinkError::Identifier("network"))
);
let link = Link::new(
"network-a".to_owned(),
"link-1".to_owned(),
same,
machine(2).machine(),
)?;
assert_eq!(link.resource().kind, ResourceKind::Link);
assert_eq!(link.resource().id, "link-1");
Ok(())
}
#[test]
fn a_machine_pass_holding_no_link_grant_is_refused_by_name() -> TestResult {
use lys_pass::conformance::{AUDIENCE, ISSUER, KEYS, NOW, cases};
let adapter = LysPassAdapter::from_keys_json(KEYS, ISSUER.to_owned())?;
let token = cases()
.into_iter()
.find(|case| case.name == "valid")
.and_then(|case| case.token)
.ok_or("the shared valid pass is missing")?;
let pass = adapter.verify_offline(token.as_bytes(), AUDIENCE, NOW)?;
let rights = MachineLinkRights::new(&adapter, pass);
let subject = SubjectBinding::new(
"network-a".to_owned(),
"orders".to_owned(),
"orders".to_owned(),
)?;
for (action, access) in [
(LinkAction::Export, AccessAction::LinkExport),
(LinkAction::Import, AccessAction::LinkImport),
] {
let refused = rights
.holds(action, &subject, NOW)
.err()
.ok_or("a pass without link grants answered a receipt")?;
assert_eq!(refused.cause, RefusalCause::MissingGrant);
assert_eq!(
refused.refusal,
AccessRefusal::new(
AccessResource {
kind: ResourceKind::Subject,
id: "orders".to_owned(),
},
access,
adapter.request_url(),
)
);
}
assert!(
!format!("{rights:?}").contains(token),
"the Debug carries no pass bytes"
);
Ok(())
}
#[test]
fn an_expired_machine_pass_refuses_a_crossing_by_its_expiry() -> TestResult {
use lys_pass::conformance::{AUDIENCE, ISSUER, KEYS, NOW, cases};
let adapter = LysPassAdapter::from_keys_json(KEYS, ISSUER.to_owned())?;
let token = cases()
.into_iter()
.find(|case| case.name == "valid")
.and_then(|case| case.token)
.ok_or("the shared valid pass is missing")?;
let pass = adapter.verify_offline(token.as_bytes(), AUDIENCE, NOW)?;
let rights = MachineLinkRights::new(&adapter, pass);
let subject = SubjectBinding::new(
"network-a".to_owned(),
"orders".to_owned(),
"orders".to_owned(),
)?;
let refused = rights
.holds(LinkAction::Export, &subject, u64::MAX)
.err()
.ok_or("an expired pass answered a receipt")?;
assert_eq!(refused.cause, RefusalCause::Expired);
Ok(())
}