Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion moq-clock-ietf/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async fn main() -> anyhow::Result<()> {
// Depending on whether we are publishing or subscribing, create the appropriate session
if config.publish {
// Create the publisher session
let (session, mut publisher) = Publisher::connect(session)
let (session, mut publisher) = Publisher::connect(session, None)
.await
.context("failed to create MoQ Transport session")?;

Expand Down
1 change: 1 addition & 0 deletions moq-pub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ serde_json = "1"
rfc6381-codec = "0.2"
tracing = "0.1"
tracing-subscriber = "0.3"
ciborium = "0.2"
124 changes: 124 additions & 0 deletions moq-pub/src/cat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! Minimal unsigned CWT (CBOR Web Token) builder for CAT tokens.
//!
//! Builds a CBOR map with RFC 8392 claim keys that the relay's
//! `CatTokenParser` can decode directly (no COSE_Mac0 signature).
//!
//! Claim keys used:
//! - 2: sub (subject) — the JWT string for plugin-level auth
//! - 4: exp (expiration) — Unix seconds
//! - 6: iat (issued at) — Unix seconds
//! - 100: moqt (scope) — permissions array

use std::time::{SystemTime, UNIX_EPOCH};

use ciborium::Value;

/// ANNOUNCE (PUBLISH_NAMESPACE) action code.
const ACTION_ANNOUNCE: i64 = 2;
/// SUBSCRIBE action code.
const ACTION_SUBSCRIBE: i64 = 4;
/// PUBLISH action code.
const ACTION_PUBLISH: i64 = 6;

/// Build an unsigned CWT claims map as CBOR bytes.
///
/// The `subject` is typically a JWT string that the Red5Pro plugin will
/// validate at Layer 2. `ttl_minutes` controls the token lifetime.
///
/// The moqt scope grants ANNOUNCE, SUBSCRIBE, and PUBLISH with
/// match-all namespace and track patterns (empty maps).
pub fn build_cwt(subject: &str, ttl_minutes: u64) -> Vec<u8> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before epoch")
.as_secs();
let exp = now + (ttl_minutes * 60);

// Actions array: [ANNOUNCE, SUBSCRIBE, PUBLISH]
let actions = Value::Array(vec![
Value::Integer(ACTION_ANNOUNCE.into()),
Value::Integer(ACTION_SUBSCRIBE.into()),
Value::Integer(ACTION_PUBLISH.into()),
]);

// Empty maps = match-all for namespace and track name
let ns_match = Value::Map(vec![]);
let track_match = Value::Map(vec![]);

// Single scope: [actions, ns_match, track_match]
let scope = Value::Array(vec![actions, ns_match, track_match]);

// moqt claim: array of scopes
let moqt = Value::Array(vec![scope]);

// CWT claims map with integer keys (RFC 8392)
let claims = Value::Map(vec![
(Value::Integer(2i64.into()), Value::Text(subject.to_string())), // sub
(Value::Integer(4i64.into()), Value::Integer((exp as i64).into())), // exp
(Value::Integer(6i64.into()), Value::Integer((now as i64).into())), // iat
(Value::Integer(100i64.into()), moqt), // moqt scope
]);

let mut buf = Vec::new();
ciborium::into_writer(&claims, &mut buf).expect("CBOR encoding failed");
buf
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn cwt_round_trip() {
let cwt_bytes = build_cwt("my-jwt-token", 60);
// Should be valid CBOR
let decoded: Value =
ciborium::from_reader(&cwt_bytes[..]).expect("should decode as CBOR");
// Should be a map
if let Value::Map(entries) = decoded {
// Should have 4 entries (sub, exp, iat, moqt)
assert_eq!(entries.len(), 4);
// Check subject claim (key 2)
let sub = entries
.iter()
.find(|(k, _)| *k == Value::Integer(2i64.into()))
.expect("should have sub claim");
assert_eq!(sub.1, Value::Text("my-jwt-token".to_string()));
} else {
panic!("expected CBOR map");
}
}

#[test]
fn cwt_has_moqt_scope() {
let cwt_bytes = build_cwt("test", 30);
let decoded: Value =
ciborium::from_reader(&cwt_bytes[..]).expect("should decode as CBOR");
if let Value::Map(entries) = decoded {
let moqt = entries
.iter()
.find(|(k, _)| *k == Value::Integer(100i64.into()))
.expect("should have moqt claim");
// moqt is an array of scopes
if let Value::Array(scopes) = &moqt.1 {
assert_eq!(scopes.len(), 1);
// Each scope is [actions, ns_match, track_match]
if let Value::Array(scope) = &scopes[0] {
assert_eq!(scope.len(), 3);
// Actions should be [2, 4, 6]
if let Value::Array(actions) = &scope[0] {
assert_eq!(actions.len(), 3);
} else {
panic!("expected actions array");
}
} else {
panic!("expected scope array");
}
} else {
panic!("expected moqt array");
}
} else {
panic!("expected CBOR map");
}
}
}
7 changes: 7 additions & 0 deletions moq-pub/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ pub struct Config {
/// Fine for local development, but should be used in caution in production.
#[arg(long)]
pub tls_disable_verify: bool,

/// Authentication token to send in CLIENT_SETUP.
///
/// Sent as AUTHORIZATION_TOKEN (param 0x03) with Token Type OUT_OF_BAND (0x00).
/// This is validated by the relay's SimpleTokenValidator.
#[arg(long)]
pub auth_token: Option<String>,
}

fn moq_url(s: &str) -> Result<Url, String> {
Expand Down
1 change: 1 addition & 0 deletions moq-pub/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod cat;
mod media;
pub use media::*;
36 changes: 34 additions & 2 deletions moq-pub/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use tokio::io::AsyncReadExt;

use moq_native_ietf::quic;
use moq_pub::Media;
use moq_transport::{coding::TrackNamespace, serve, session::Publisher};
use moq_transport::{coding::TrackNamespace, serve, session::Publisher, setup::token};

#[derive(Parser, Clone)]
pub struct Cli {
Expand Down Expand Up @@ -37,6 +37,23 @@ pub struct Cli {
/// The TLS configuration.
#[command(flatten)]
pub tls: moq_native_ietf::tls::Args,

/// Authentication token to send in CLIENT_SETUP.
///
/// Sent as AUTHORIZATION_TOKEN (param 0x03) with Token Type OUT_OF_BAND (0x00).
/// This is validated by the relay's SimpleTokenValidator.
/// Mutually exclusive with --cat-subject.
#[arg(long, conflicts_with = "cat_subject")]
pub auth_token: Option<String>,

/// Subject for CAT token (e.g., a JWT string for plugin-level auth).
///
/// When provided, builds a CAT token (Type 0x10) with this value as the
/// CWT "sub" claim. The relay extracts the subject and passes it to the
/// plugin's MoqAuthenticator for JWT validation.
/// Mutually exclusive with --auth-token.
#[arg(long, conflicts_with = "auth_token")]
pub cat_subject: Option<String>,
}

#[tokio::main]
Expand Down Expand Up @@ -71,7 +88,22 @@ async fn main() -> anyhow::Result<()> {
connection_id
);

let (session, mut publisher) = Publisher::connect(session)
// Build the token bytes with proper Token structure wrapping
let auth_token = if let Some(ref subject) = cli.cat_subject {
let cwt_bytes = moq_pub::cat::build_cwt(subject, 60);
log::info!(
"built CAT token: sub={} ({} CWT bytes)",
subject.chars().take(32).collect::<String>(),
cwt_bytes.len()
);
Some(token::build_cat_token(&cwt_bytes))
} else if let Some(ref raw) = cli.auth_token {
log::info!("built simple OUT_OF_BAND token ({} bytes)", raw.len());
Some(token::build_simple_token(raw.as_bytes()))
} else {
None
};
let (session, mut publisher) = Publisher::connect(session, auth_token)
.await
.context("failed to create MoQ Transport publisher")?;

Expand Down
2 changes: 1 addition & 1 deletion moq-relay-ietf/src/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ impl Relay {

// Create the MoQ session over the connection
let (session, publisher, subscriber) =
moq_transport::session::Session::connect(session, None)
moq_transport::session::Session::connect(session, None, None)
.await
.context("failed to establish forward session")?;

Expand Down
5 changes: 5 additions & 0 deletions moq-transport/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl Session {
pub async fn connect(
mut session: web_transport::Session,
mlog_path: Option<PathBuf>,
auth_token: Option<Vec<u8>>,
) -> Result<(Session, Publisher, Subscriber), SessionError> {
let mlog = mlog_path.and_then(|path| {
mlog::MlogWriter::new(path)
Expand All @@ -118,6 +119,10 @@ impl Session {
// TODO SLG - make configurable?
let mut params = KeyValuePairs::default();
params.set_intvalue(setup::ParameterType::MaxRequestId.into(), 100);
if let Some(token) = auth_token {
log::info!("adding auth token to CLIENT_SETUP ({} bytes)", token.len());
params.set_bytesvalue(setup::ParameterType::AuthorizationToken.into(), token);
}

let client = setup::Client {
versions: versions.clone(),
Expand Down
3 changes: 2 additions & 1 deletion moq-transport/src/session/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ impl Publisher {

pub async fn connect(
session: web_transport::Session,
auth_token: Option<Vec<u8>>,
) -> Result<(Session, Publisher), SessionError> {
let (session, publisher, _) = Session::connect(session, None).await?;
let (session, publisher, _) = Session::connect(session, None, auth_token).await?;
Ok((session, publisher))
}

Expand Down
2 changes: 1 addition & 1 deletion moq-transport/src/session/subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ impl Subscriber {

/// Create an outbound/client QUIC connection, by opening a bi-directional QUIC stream for control messages.
pub async fn connect(session: web_transport::Session) -> Result<(Session, Self), SessionError> {
let (session, _, subscriber) = Session::connect(session, None).await?;
let (session, _, subscriber) = Session::connect(session, None, None).await?;
Ok((session, subscriber))
}

Expand Down
1 change: 1 addition & 0 deletions moq-transport/src/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
mod client;
mod param_types;
mod server;
pub mod token;
mod version;

pub use client::*;
Expand Down
Loading