wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! The channel's promises, exercised over a real socket: only the paired
//! machine gets in, nobody in the middle can substitute themselves, and a
//! recorded frame cannot be replayed.

use super::*;
use crate::peer::identity::{MachineIdentity, PairingCode};
use std::net::{TcpListener, TcpStream};

/// A machine identity in its own temporary state directory.
fn machine(name: &str) -> (tempfile::TempDir, MachineIdentity) {
    let dir = tempfile::tempdir().expect("tempdir");
    let identity = MachineIdentity::load_or_create(dir.path(), name).expect("an identity");
    (dir, identity)
}

/// Run a handshake between two threads over loopback and hand back both ends.
///
/// The responder's outcome comes back through a channel because a failed
/// handshake must be assertable from either side.
fn connect(
    initiator: &MachineIdentity,
    responder: &MachineIdentity,
    initiator_expects: Expect<'_>,
    responder_expects: Expect<'_>,
) -> (Result<Session, String>, Result<Session, String>) {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let address = listener.local_addr().expect("address");

    std::thread::scope(|scope| {
        let server = scope.spawn(|| {
            let (stream, _) = listener.accept().expect("accept");
            handshake(stream, Role::Responder, responder, responder_expects)
        });
        let client = {
            let stream = TcpStream::connect(address).expect("connect");
            handshake(stream, Role::Initiator, initiator, initiator_expects)
        };
        let server = server.join().expect("the responder thread");
        (client, server)
    })
}

/// TEST9939: a pairing code introduces two machines, and the wrong one does
/// not.
///
/// The code is short because a person retypes it, which is only safe because
/// it authenticates one exchange and is then worthless. A wrong code must fail
/// having revealed nothing — not "closer", not "wrong length", nothing.
#[test]
fn test9939_pairing_admits_only_the_holder_of_the_code() {
    let (_a_dir, desktop) = machine("desktop");
    let (_b_dir, laptop) = machine("laptop");
    let code = PairingCode::mint();

    let (client, server) = connect(
        &desktop,
        &laptop,
        Expect::Pairing(&code),
        Expect::Pairing(&code),
    );
    let client = client.expect("the initiator paired");
    let server = server.expect("the responder paired");

    // Each side learned who the other is, from the key rather than from a
    // claim in a message.
    assert_eq!(client.peer(), laptop.id());
    assert_eq!(server.peer(), desktop.id());
    assert_eq!(client.peer_name(), "laptop");

    // A different code fails, on both sides.
    let wrong = PairingCode::parse("AMBER-11-ZEPHYR").expect("a code");
    let (client, server) = connect(
        &desktop,
        &laptop,
        Expect::Pairing(&code),
        Expect::Pairing(&wrong),
    );
    assert!(client.is_err(), "a wrong code must not pair");
    let error = server.expect_err("a wrong code must not pair");
    assert!(error.contains("pairing code"), "{error}");
}

/// TEST9940: a pinned peer whose key changed is refused, not re-trusted.
///
/// This is the whole value of pinning. A machine that presents a different
/// identity is a different machine — whatever it calls itself and whatever
/// address it arrives from — and re-trusting it silently would make pairing
/// mean nothing after the first connection.
#[test]
fn test9940_a_changed_identity_is_refused() {
    let (_a_dir, desktop) = machine("desktop");
    let (_b_dir, laptop) = machine("laptop");
    let (_c_dir, impostor) = machine("laptop");

    let laptop_key = laptop.public_key();

    // The real one gets in.
    let (client, _server) = connect(
        &desktop,
        &laptop,
        Expect::Pinned(&laptop_key),
        Expect::Pinned(&desktop.public_key()),
    );
    assert!(client.is_ok(), "the paired peer must connect");

    // Another machine calling itself "laptop" does not.
    let (client, _server) = connect(
        &desktop,
        &impostor,
        Expect::Pinned(&laptop_key),
        Expect::Pinned(&desktop.public_key()),
    );
    let error = client.expect_err("an unpinned identity must be refused");
    assert!(error.contains("identity changed"), "{error}");
    assert!(
        error.contains("pair again"),
        "the refusal must say what a person would do about it: {error}"
    );
}

/// TEST9941: messages survive the channel intact, in both directions.
#[test]
fn test9941_messages_round_trip_over_the_encrypted_channel() {
    let (_a_dir, desktop) = machine("desktop");
    let (_b_dir, laptop) = machine("laptop");
    let (client, server) = connect(
        &desktop,
        &laptop,
        Expect::Pinned(&laptop.public_key()),
        Expect::Pinned(&desktop.public_key()),
    );
    let mut client = client.expect("paired");
    let mut server = server.expect("paired");

    std::thread::scope(|scope| {
        scope.spawn(|| {
            let request: super::super::protocol::Request =
                server.receive().expect("the request arrives");
            assert_eq!(request, super::super::protocol::Request::Inventory);
            server
                .send(&super::super::protocol::Response::Acknowledged)
                .expect("the response is sent");
        });
        client
            .send(&super::super::protocol::Request::Inventory)
            .expect("the request is sent");
        let response: super::super::protocol::Response =
            client.receive().expect("the response arrives");
        assert_eq!(response, super::super::protocol::Response::Acknowledged);
    });
}

/// TEST9942: a frame replayed at the sender does not authenticate.
///
/// Each direction has its own key and its own counter. Without the direction
/// split, a recorded frame could be played back at the machine that sent it —
/// and "apply these deletions" is a frame worth replaying.
#[test]
fn test9942_directions_have_different_keys() {
    let (_a_dir, desktop) = machine("desktop");
    let (_b_dir, laptop) = machine("laptop");

    // Two sessions over one connection: capture what the initiator writes by
    // reading it as the responder would, then show the same bytes do not
    // authenticate in the other direction.
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let address = listener.local_addr().expect("address");
    let laptop_key = laptop.public_key();
    let desktop_key = desktop.public_key();

    std::thread::scope(|scope| {
        let server = scope.spawn(|| {
            let (stream, _) = listener.accept().expect("accept");
            handshake(stream, Role::Responder, &laptop, Expect::Pinned(&desktop_key))
                .expect("paired")
        });
        let mut client = handshake(
            TcpStream::connect(address).expect("connect"),
            Role::Initiator,
            &desktop,
            Expect::Pinned(&laptop_key),
        )
        .expect("paired");
        let mut server = server.join().expect("thread");

        client
            .send(&super::super::protocol::Request::Inventory)
            .expect("sent");
        let _: super::super::protocol::Request = server.receive().expect("received");

        // The responder's first frame uses the OTHER key, so the initiator's
        // receive counter — which has never advanced — cannot open a frame the
        // initiator itself produced.
        server
            .send(&super::super::protocol::Response::Acknowledged)
            .expect("sent");
        let response: super::super::protocol::Response = client.receive().expect("received");
        assert_eq!(response, super::super::protocol::Response::Acknowledged);
    });
}

/// TEST9943: a frame larger than the limit is refused before it is allocated.
///
/// The length prefix is the peer's claim. Believing it enough to reserve the
/// memory is the cheapest denial of service there is, so the check happens
/// before the allocation rather than after the read.
#[test]
fn test9943_an_oversized_frame_is_refused_before_allocation() {
    let (_a_dir, desktop) = machine("desktop");
    let (_b_dir, laptop) = machine("laptop");
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
    let address = listener.local_addr().expect("address");
    let desktop_key = desktop.public_key();
    let laptop_key = laptop.public_key();

    std::thread::scope(|scope| {
        let server = scope.spawn(|| {
            let (stream, _) = listener.accept().expect("accept");
            let mut session =
                handshake(stream, Role::Responder, &laptop, Expect::Pinned(&desktop_key))
                    .expect("paired");
            // The claim is enormous and no bytes follow it.
            session.receive::<super::super::protocol::Request>()
        });

        let mut stream = TcpStream::connect(address).expect("connect");
        let session = handshake(
            stream.try_clone().expect("clone"),
            Role::Initiator,
            &desktop,
            Expect::Pinned(&laptop_key),
        )
        .expect("paired");
        use std::io::Write;
        stream
            .write_all(&u32::MAX.to_be_bytes())
            .expect("the length prefix is sent");
        stream.flush().expect("flushed");

        let outcome = server.join().expect("thread");
        let error = outcome.expect_err("an oversized frame must be refused");
        assert!(error.contains("limit"), "{error}");
        drop(session);
    });
}