Draft::select takes the most preferred draft a client offers (-22 first), as the relay's TLS does. The relay client takes extra Setup Options (AUTHORITY and PATH for a moqt:// URI), and the relay tests can run any session at -18 or -16. Co-Authored-By: Claude Opus 5.5 <[email protected]>
101 lines
5.2 KiB
Rust
101 lines
5.2 KiB
Rust
//! Proof of possession of a room grant's key (WAI live-rooms §10.5.2), over each transport:
|
|
//! the relay sends its SETUP first with a fresh challenge; a client presenting a grant in
|
|
//! its SETUP with a proof over that challenge, for the target it addressed, gets the
|
|
//! grant's rights; a grant with no proof, a proof by another key, over another session's
|
|
//! challenge or for another target closes the session with UNAUTHORIZED (§12.2); and a
|
|
//! grant presented later in a request, never proved, is refused that request.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use ed25519_dalek::SigningKey;
|
|
use joule_moq::draft22::{Parameter, Parameters, RequestErrorCode, SessionError, Token};
|
|
use joule_moq::grant_proof;
|
|
use joule_moq_relay::client::{Client, ClientOptions};
|
|
use joule_moq_relay::config::Grant;
|
|
use joule_moq_relay::transport::TransportError;
|
|
use joule_moq_relay::RelayServer;
|
|
use std::time::Duration;
|
|
use tokio::time::timeout;
|
|
|
|
/// A record shaped as a room grant (`WRG1`, version 1, a room, the principal), which is
|
|
/// all the relay reads of it: its rule names the record by its SHA-256.
|
|
fn grant_record(room: u8, principal: &SigningKey) -> Vec<u8> {
|
|
let mut g = b"WRG1\x01".to_vec();
|
|
g.extend_from_slice(&[room; 32]);
|
|
g.extend_from_slice(&principal.verifying_key().to_bytes());
|
|
g.extend_from_slice(&[0x5a; 160]);
|
|
g.extend_from_slice(&[0x77; 64]);
|
|
g
|
|
}
|
|
|
|
/// The `//authority/path` a test client addresses over `via`: the URL's over WebTransport
|
|
/// and the WebSocket binding; over native QUIC, with no AUTHORITY or PATH sent, `//`.
|
|
fn target_of(server: &RelayServer, via: Via) -> String {
|
|
let path = server.relay().config().path.clone();
|
|
match via {
|
|
Via::Quic | Via::QuicDraft(_) => "//".into(),
|
|
Via::WebTransport | Via::WtDraft(_) => format!("//127.0.0.1:{}{path}", server.quic_addr.unwrap().port()),
|
|
Via::WebSocket => format!("//{}{path}", server.websocket_addr.unwrap()),
|
|
}
|
|
}
|
|
|
|
async fn closed_unauthorized(c: &Client, what: &str) {
|
|
match timeout(STEP, c.conn.closed()).await.expect("closed in time") {
|
|
TransportError::Closed { code, .. } => assert_eq!(code, SessionError::UNAUTHORIZED.0, "{what}"),
|
|
other => panic!("{what}: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn a_grant_is_honoured_only_with_its_proof() {
|
|
timeout(Duration::from_secs(180), run()).await.expect("the test finishes in time");
|
|
}
|
|
|
|
async fn run() {
|
|
let host = SigningKey::from_bytes(&[0x41; 32]);
|
|
let stranger = SigningKey::from_bytes(&[0x42; 32]);
|
|
let record = grant_record(9, &host);
|
|
let hash = hex::encode(joule_moq_relay::auth::sha256(&record));
|
|
let server = relay(|c| {
|
|
c.auth.required = true;
|
|
c.auth.grants = vec![Grant { prefix: vec!["room".into()], token_sha256: hash, publish: true, subscribe: true, proof: true, unlimited: false, seek: false }];
|
|
})
|
|
.await;
|
|
let t = Tracks::new(ns(&["room", "host"]), "0", 10, 5, 128);
|
|
let with = |key: &SigningKey, target: String| ClientOptions { grants: vec![(record.clone(), key.clone())], target, ..ClientOptions::default() };
|
|
|
|
// With its proof, over each transport, the grant gives its rights; each session has a
|
|
// challenge of its own.
|
|
let mut challenges = Vec::new();
|
|
for via in [Via::WebSocket, Via::WebTransport, Via::Quic] {
|
|
let (c, _ep) = connect(&server, via, with(&host, target_of(&server, via))).await;
|
|
challenges.push(grant_proof::challenge_of(&c.peer_setup.options).expect("the relay's SETUP carries a challenge"));
|
|
let _held = publish_tracks(&c, &t, None).await;
|
|
c.close(0, "done");
|
|
}
|
|
challenges.sort();
|
|
challenges.dedup();
|
|
assert_eq!(challenges.len(), 3, "a challenge is drawn for each session");
|
|
|
|
// The grant as a bare token, a proof by another key, one for another target, and one
|
|
// over another session's challenge: each session refused.
|
|
let bare = ClientOptions { tokens: vec![Token::UseValue { token_type: 0, value: record.clone() }], ..ClientOptions::default() };
|
|
let (c, _) = connect(&server, Via::WebSocket, bare).await;
|
|
closed_unauthorized(&c, "a grant with no proof").await;
|
|
let (c, _) = connect(&server, Via::WebSocket, with(&stranger, target_of(&server, Via::WebSocket))).await;
|
|
closed_unauthorized(&c, "a proof by another key").await;
|
|
let (c, _) = connect(&server, Via::WebSocket, with(&host, "//other-relay.example/moq".into())).await;
|
|
closed_unauthorized(&c, "a proof for another relay").await;
|
|
let replayed = ClientOptions { sign_challenge: Some(challenges[0]), ..with(&host, target_of(&server, Via::WebSocket)) };
|
|
let (c, _) = connect(&server, Via::WebSocket, replayed).await;
|
|
closed_unauthorized(&c, "a proof over an earlier session's challenge").await;
|
|
|
|
// A grant presented only in a request, never proved in SETUP: refused that request.
|
|
let (c, _) = connect(&server, Via::WebSocket, ClientOptions::default()).await;
|
|
let late = Parameters::new().with(Parameter::AuthorizationToken(Token::UseValue { token_type: 0, value: record.clone() }));
|
|
let refused = c.subscribe(t.ns.clone(), t.media.clone(), late).await.expect_err("a grant not proved");
|
|
assert!(refused.contains(&RequestErrorCode::UNAUTHORIZED.to_string()), "{refused}");
|
|
c.close(0, "done");
|
|
server.shutdown().await;
|
|
}
|