liminal-server 0.20.1

Standalone server for the liminal messaging bus
Documentation
//! ACCESS-002 R1: a link is a machine's grant. Two machines with export and
//! import grants exchange on the granted prefix only; a third machine is
//! refused at the handshake; a key derived any other way fails it.

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])))
}

/// A machine's pass as a table: the prefixes each link action reaches.
#[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);
    // The importer expects the exporter; the third machine presents itself.
    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);
    // The importer was told a wrong static key for the exporter: a second
    // derivation of the same identity is simply another key.
    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() {
    // y = 2 (little-endian, sign bit clear): (y^2 - 1) / (d y^2 + 1) is not
    // a square modulo 2^255 - 19, so no x satisfies the curve and the
    // encoding does not decompress. Checked by arithmetic, not by trial.
    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(())
}

/// The refusal a crossing came to, or an error naming the crossing that
/// should have been refused.
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(())
}

/// The real rights over a verified pass (ACCESS-002 R1): a machine whose pass
/// carries no link grant on the bound subject is refused by name, with the
/// subject, the link action and the issuer's request page in the refusal; it
/// is decided offline from the pass alone.
#[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(())
}

/// The same pass past its expiry refuses with the expiry as its cause, not a
/// missing grant: the cause is the pass's own.
#[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(())
}