feat: punktfunk/1 — mid-stream mode renegotiation + PIN pairing ceremony
Renegotiation (no reconnect on resize): the handshake bi-stream stays open; the client
sends Reconfigure{mode} (typed post-handshake message), the host validates + acks
Reconfigured and rebuilds capture/encoder/virtual output at the new mode while the data
plane (keys, ports, FEC) runs untouched — the first new-mode AU is an IDR with in-band
parameter sets. NativeClient::request_mode / punktfunk_connection_request_mode; mode()
reflects the active mode. Validated live on KWin: one continuous stream, 225 frames
@1280x720 then 395 @1920x1080, ~90 ms pipeline rebuild (ffprobe shows both resolutions).
PIN pairing (mutual trust, kills TOFU MITM): clients get persistent self-signed
identities presented via QUIC client auth (generate_identity / client auth offered but
optional server-side — legacy clients still connect). Ceremony on the control stream:
PairRequest{name} → host shows a 4-digit PIN (log) + PairChallenge{salt} → client proves
with HMAC-SHA256(PIN‖salt, client_fp‖host_fp) — binding both certs means a MITM can't
forward a proof, single attempt per PIN, constant-time compare → PairResult; host
persists the fingerprint (~/.config/punktfunk/punktfunk1-paired.json), client pins the
host's. m3-host --require-pairing gates sessions on the paired set.
NativeClient::pair + punktfunk_pair/punktfunk_generate_identity in the ABI; reference
client: --pair PIN --name LABEL + auto-generated persistent identity, --remode for live
renegotiation testing. Swift wrapper: ClientIdentity/generateIdentity()/pair(),
requestMode()/currentMode(); README handoff updated.
Tested: reconfigure/pairing wire roundtrips, C-ABI mode switch ack, full in-process
ceremony (wrong PIN → Crypto, anonymous-vs-gate rejection, success → pinned session);
live wrong-PIN ceremony against the serving host (PIN logged, proof rejected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ crate-type = ["lib", "cdylib", "staticlib"]
|
||||
default = []
|
||||
# Control-plane QUIC (pairing, config, reverse audio). tokio is permitted ONLY here,
|
||||
# never on the per-frame hot path. Off by default so the core stays runtime-free.
|
||||
quic = ["dep:quinn", "dep:tokio", "dep:rustls", "dep:rcgen", "dep:rustls-pki-types", "dep:sha2"]
|
||||
quic = ["dep:quinn", "dep:tokio", "dep:rustls", "dep:rcgen", "dep:rustls-pki-types", "dep:sha2", "dep:hmac"]
|
||||
|
||||
[dependencies]
|
||||
reed-solomon-simd = "3.1" # GF(2^16) Leopard-RS, SIMD, O(n log n) — the wall-breaker (P2)
|
||||
@@ -38,9 +38,10 @@ zeroize = "1"
|
||||
|
||||
quinn = { version = "0.11", optional = true }
|
||||
rustls = { version = "0.23", optional = true, default-features = false, features = ["ring", "std"] }
|
||||
rcgen = { version = "0.13", optional = true, default-features = false, features = ["aws_lc_rs"] }
|
||||
rcgen = { version = "0.13", optional = true, default-features = false, features = ["aws_lc_rs", "pem"] }
|
||||
rustls-pki-types = { version = "1", optional = true }
|
||||
sha2 = { version = "0.10", optional = true }
|
||||
hmac = { version = "0.12", optional = true }
|
||||
tokio = { version = "1", optional = true, features = ["rt-multi-thread", "net", "sync", "macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -465,6 +465,18 @@ pub struct PunktfunkConnection {
|
||||
last_audio: std::sync::Mutex<Option<crate::client::AudioPacket>>,
|
||||
}
|
||||
|
||||
/// Read an optional NUL-terminated UTF-8 string parameter; `Err` = invalid pointer/UTF-8.
|
||||
#[cfg(feature = "quic")]
|
||||
unsafe fn opt_cstr<'a>(p: *const std::os::raw::c_char) -> std::result::Result<Option<&'a str>, ()> {
|
||||
if p.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
unsafe { std::ffi::CStr::from_ptr(p) }
|
||||
.to_str()
|
||||
.map(Some)
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
/// Connect to a `punktfunk/1` host and start a session at `width`x`height`@`refresh_hz`.
|
||||
/// Blocks up to `timeout_ms` for the handshake. Returns NULL on failure.
|
||||
///
|
||||
@@ -473,9 +485,15 @@ pub struct PunktfunkConnection {
|
||||
/// fingerprint written to `observed_sha256_out` (NULL or 32 bytes, filled on success) and
|
||||
/// pass it as the pin on every later connect.
|
||||
///
|
||||
/// Identity: `client_cert_pem`/`client_key_pem` (both NULL, or both NUL-terminated PEM
|
||||
/// strings — see [`punktfunk_generate_identity`]) are presented via TLS client auth so a
|
||||
/// host can recognize this client once paired ([`punktfunk_pair`]). NULL = anonymous;
|
||||
/// hosts running `--require-pairing` reject anonymous sessions.
|
||||
///
|
||||
/// # Safety
|
||||
/// `host` is a NUL-terminated UTF-8 string (IP or hostname resolvable by the platform);
|
||||
/// `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes.
|
||||
/// `pin_sha256`/`observed_sha256_out` are each NULL or valid for 32 bytes;
|
||||
/// `client_cert_pem`/`client_key_pem` are each NULL or NUL-terminated UTF-8.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_connect(
|
||||
@@ -486,6 +504,8 @@ pub unsafe extern "C" fn punktfunk_connect(
|
||||
refresh_hz: u32,
|
||||
pin_sha256: *const u8,
|
||||
observed_sha256_out: *mut u8,
|
||||
client_cert_pem: *const std::os::raw::c_char,
|
||||
client_key_pem: *const std::os::raw::c_char,
|
||||
timeout_ms: u32,
|
||||
) -> *mut PunktfunkConnection {
|
||||
let r = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
@@ -508,11 +528,19 @@ pub unsafe extern "C" fn punktfunk_connect(
|
||||
p.copy_from_slice(unsafe { std::slice::from_raw_parts(pin_sha256, 32) });
|
||||
Some(p)
|
||||
};
|
||||
let identity = match (unsafe { opt_cstr(client_cert_pem) }, unsafe {
|
||||
opt_cstr(client_key_pem)
|
||||
}) {
|
||||
(Ok(Some(c)), Ok(Some(k))) => Some((c.to_string(), k.to_string())),
|
||||
(Ok(None), Ok(None)) => None,
|
||||
_ => return std::ptr::null_mut(), // half an identity / bad UTF-8: fail closed
|
||||
};
|
||||
match crate::client::NativeClient::connect(
|
||||
host,
|
||||
port,
|
||||
mode,
|
||||
pin,
|
||||
identity,
|
||||
std::time::Duration::from_millis(timeout_ms as u64),
|
||||
) {
|
||||
Ok(c) => {
|
||||
@@ -534,6 +562,97 @@ pub unsafe extern "C" fn punktfunk_connect(
|
||||
r.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// Generate a persistent client identity: a self-signed certificate + private key, both
|
||||
/// PEM, NUL-terminated, written into the caller's buffers. Generate ONCE, store both
|
||||
/// strings (Keychain etc.), pass them to [`punktfunk_pair`] and every
|
||||
/// [`punktfunk_connect`] — the certificate's fingerprint is how hosts recognize this
|
||||
/// client. 4096-byte buffers are ample.
|
||||
///
|
||||
/// # Safety
|
||||
/// `cert_pem_out` is writable for `cert_cap` bytes; `key_pem_out` for `key_cap`.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_generate_identity(
|
||||
cert_pem_out: *mut std::os::raw::c_char,
|
||||
cert_cap: usize,
|
||||
key_pem_out: *mut std::os::raw::c_char,
|
||||
key_cap: usize,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
if cert_pem_out.is_null() || key_pem_out.is_null() {
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
let (cert, key) = match crate::quic::endpoint::generate_identity() {
|
||||
Ok(t) => t,
|
||||
Err(_) => return PunktfunkStatus::Io,
|
||||
};
|
||||
if cert.len() + 1 > cert_cap || key.len() + 1 > key_cap {
|
||||
return PunktfunkStatus::InvalidArg;
|
||||
}
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(cert.as_ptr(), cert_pem_out as *mut u8, cert.len());
|
||||
*cert_pem_out.add(cert.len()) = 0;
|
||||
std::ptr::copy_nonoverlapping(key.as_ptr(), key_pem_out as *mut u8, key.len());
|
||||
*key_pem_out.add(key.len()) = 0;
|
||||
}
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the PIN pairing ceremony against a host (see the protocol docs in punktfunk-core):
|
||||
/// the host displays a short PIN; the user types it into the client app, which passes it
|
||||
/// here. On success the host has stored this client's identity, the now-verified host
|
||||
/// fingerprint is written to `host_sha256_out` (32 bytes) — persist it and pass it as
|
||||
/// `pin_sha256` to [`punktfunk_connect`] from then on. Returns
|
||||
/// [`PunktfunkStatus::Crypto`] for a wrong PIN.
|
||||
///
|
||||
/// # Safety
|
||||
/// `host`/`client_cert_pem`/`client_key_pem`/`pin`/`name` are NUL-terminated UTF-8;
|
||||
/// `host_sha256_out` is writable for 32 bytes.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_pair(
|
||||
host: *const std::os::raw::c_char,
|
||||
port: u16,
|
||||
client_cert_pem: *const std::os::raw::c_char,
|
||||
client_key_pem: *const std::os::raw::c_char,
|
||||
pin: *const std::os::raw::c_char,
|
||||
name: *const std::os::raw::c_char,
|
||||
host_sha256_out: *mut u8,
|
||||
timeout_ms: u32,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let (Ok(Some(host)), Ok(Some(cert)), Ok(Some(key)), Ok(Some(pin)), Ok(Some(name))) = (
|
||||
unsafe { opt_cstr(host) },
|
||||
unsafe { opt_cstr(client_cert_pem) },
|
||||
unsafe { opt_cstr(client_key_pem) },
|
||||
unsafe { opt_cstr(pin) },
|
||||
unsafe { opt_cstr(name) },
|
||||
) else {
|
||||
return PunktfunkStatus::NullPointer;
|
||||
};
|
||||
if host_sha256_out.is_null() {
|
||||
return PunktfunkStatus::NullPointer;
|
||||
}
|
||||
match crate::client::NativeClient::pair(
|
||||
host,
|
||||
port,
|
||||
(cert, key),
|
||||
pin,
|
||||
name,
|
||||
std::time::Duration::from_millis(timeout_ms as u64),
|
||||
) {
|
||||
Ok(fp) => {
|
||||
unsafe {
|
||||
std::slice::from_raw_parts_mut(host_sha256_out, 32).copy_from_slice(&fp);
|
||||
}
|
||||
PunktfunkStatus::Ok
|
||||
}
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Pull the next reassembled access unit, waiting up to `timeout_ms`. Returns
|
||||
/// [`PunktfunkStatus::NoFrame`] on timeout and [`PunktfunkStatus::Closed`] once the session ended.
|
||||
/// On `Ok`, `*out` borrows connection memory **until the next `next_au` call** on this
|
||||
@@ -710,7 +829,8 @@ pub unsafe extern "C" fn punktfunk_connection_send_input(
|
||||
})
|
||||
}
|
||||
|
||||
/// The host-confirmed session mode (from the Welcome). Safe any time after connect.
|
||||
/// The currently active session mode — the Welcome's, until an accepted
|
||||
/// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` is a valid connection handle; out pointers are writable (NULLs are skipped).
|
||||
@@ -727,21 +847,55 @@ pub unsafe extern "C" fn punktfunk_connection_mode(
|
||||
Some(c) => c,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
let mode = c.inner.mode();
|
||||
unsafe {
|
||||
if !width.is_null() {
|
||||
*width = c.inner.mode.width;
|
||||
*width = mode.width;
|
||||
}
|
||||
if !height.is_null() {
|
||||
*height = c.inner.mode.height;
|
||||
*height = mode.height;
|
||||
}
|
||||
if !refresh_hz.is_null() {
|
||||
*refresh_hz = c.inner.mode.refresh_hz;
|
||||
*refresh_hz = mode.refresh_hz;
|
||||
}
|
||||
}
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
}
|
||||
|
||||
/// Ask the host to switch the live session to `width`x`height`@`refresh_hz` without
|
||||
/// reconnecting (window resized, refresh changed). Non-blocking enqueue: on acceptance the
|
||||
/// stream continues at the new mode — the first new-mode access unit is an IDR with
|
||||
/// in-band parameter sets (rebuild the decoder from it) — and
|
||||
/// [`punktfunk_connection_mode`] reflects the switch. A rejected request leaves the
|
||||
/// session unchanged.
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` is a valid connection handle.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_connection_request_mode(
|
||||
c: *const PunktfunkConnection,
|
||||
width: u32,
|
||||
height: u32,
|
||||
refresh_hz: u32,
|
||||
) -> PunktfunkStatus {
|
||||
guard(|| {
|
||||
let c = match unsafe { c.as_ref() } {
|
||||
Some(c) => c,
|
||||
None => return PunktfunkStatus::NullPointer,
|
||||
};
|
||||
match c.inner.request_mode(crate::config::Mode {
|
||||
width,
|
||||
height,
|
||||
refresh_hz,
|
||||
}) {
|
||||
Ok(()) => PunktfunkStatus::Ok,
|
||||
Err(e) => e.status(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Close the connection and free the handle (joins the internal threads). NULL is a no-op.
|
||||
///
|
||||
/// # Safety
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
use crate::config::{Mode, Role};
|
||||
use crate::error::{PunktfunkError, Result};
|
||||
use crate::input::InputEvent;
|
||||
use crate::quic::{endpoint, io, Hello, Start, Welcome};
|
||||
use crate::quic::{endpoint, io, Hello, Reconfigure, Reconfigured, Start, Welcome};
|
||||
use crate::session::{Frame, Session};
|
||||
use crate::transport::UdpTransport;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -50,10 +50,12 @@ pub struct NativeClient {
|
||||
audio: Receiver<AudioPacket>,
|
||||
rumble: Receiver<(u16, u16, u16)>,
|
||||
input_tx: tokio::sync::mpsc::UnboundedSender<InputEvent>,
|
||||
reconfig_tx: tokio::sync::mpsc::UnboundedSender<Mode>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
worker: Option<std::thread::JoinHandle<()>>,
|
||||
/// The host-confirmed session mode (from the Welcome).
|
||||
pub mode: Mode,
|
||||
/// The currently active session mode (the Welcome's, then updated by every accepted
|
||||
/// [`NativeClient::request_mode`]).
|
||||
mode: Arc<std::sync::Mutex<Mode>>,
|
||||
/// SHA-256 fingerprint of the certificate the host actually presented. A TOFU caller
|
||||
/// (`pin = None`) persists this and passes it as the pin from then on.
|
||||
pub host_fingerprint: [u8; 32],
|
||||
@@ -66,22 +68,30 @@ impl NativeClient {
|
||||
/// `pin`: expected SHA-256 of the host's certificate. `Some` and the host presents
|
||||
/// anything else → the handshake is rejected ([`PunktfunkError::Crypto`]). `None` = trust on
|
||||
/// first use; check [`NativeClient::host_fingerprint`] after connecting.
|
||||
///
|
||||
/// `identity`: this client's persistent self-signed identity (PEM cert + PKCS#8 key,
|
||||
/// see [`endpoint::generate_identity`]), presented via TLS client auth so a host can
|
||||
/// recognize a paired client. `None` = anonymous (rejected by hosts requiring pairing).
|
||||
pub fn connect(
|
||||
host: &str,
|
||||
port: u16,
|
||||
mode: Mode,
|
||||
pin: Option<[u8; 32]>,
|
||||
identity: Option<(String, String)>,
|
||||
timeout: Duration,
|
||||
) -> Result<NativeClient> {
|
||||
let (frame_tx, frame_rx) = std::sync::mpsc::sync_channel::<Frame>(FRAME_QUEUE);
|
||||
let (audio_tx, audio_rx) = std::sync::mpsc::sync_channel::<AudioPacket>(AUDIO_QUEUE);
|
||||
let (rumble_tx, rumble_rx) = std::sync::mpsc::sync_channel::<(u16, u16, u16)>(RUMBLE_QUEUE);
|
||||
let (input_tx, input_rx) = tokio::sync::mpsc::unbounded_channel::<InputEvent>();
|
||||
let (reconfig_tx, reconfig_rx) = tokio::sync::mpsc::unbounded_channel::<Mode>();
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(Mode, [u8; 32])>>();
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let mode_slot = Arc::new(std::sync::Mutex::new(mode));
|
||||
|
||||
let host = host.to_string();
|
||||
let shutdown_w = shutdown.clone();
|
||||
let mode_slot_w = mode_slot.clone();
|
||||
let worker = std::thread::Builder::new()
|
||||
.name("punktfunk-client".into())
|
||||
.spawn(move || {
|
||||
@@ -101,12 +111,15 @@ impl NativeClient {
|
||||
port,
|
||||
mode,
|
||||
pin,
|
||||
identity,
|
||||
frame_tx,
|
||||
audio_tx,
|
||||
rumble_tx,
|
||||
input_rx,
|
||||
reconfig_rx,
|
||||
ready_tx,
|
||||
shutdown: shutdown_w,
|
||||
mode_slot: mode_slot_w,
|
||||
}));
|
||||
})
|
||||
.map_err(PunktfunkError::Io)?;
|
||||
@@ -119,18 +132,100 @@ impl NativeClient {
|
||||
return Err(PunktfunkError::Timeout);
|
||||
}
|
||||
};
|
||||
*mode_slot.lock().unwrap() = negotiated;
|
||||
Ok(NativeClient {
|
||||
frames: frame_rx,
|
||||
audio: audio_rx,
|
||||
rumble: rumble_rx,
|
||||
input_tx,
|
||||
reconfig_tx,
|
||||
shutdown,
|
||||
worker: Some(worker),
|
||||
mode: negotiated,
|
||||
mode: mode_slot,
|
||||
host_fingerprint: fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
/// Run the PIN pairing ceremony against a host: connect (trust-on-first-use — the PIN
|
||||
/// proof is what authenticates the certificates), prove knowledge of the PIN the host
|
||||
/// is displaying, and return the host's now-verified fingerprint for pinning. The host
|
||||
/// persists this client's fingerprint in its paired set.
|
||||
///
|
||||
/// `identity` is this client's persistent PEM identity (cert, key) — the same one
|
||||
/// later passed to [`NativeClient::connect`]; `pin` is what the user read off the host
|
||||
/// (its log / UI); `name` is the label the host stores.
|
||||
pub fn pair(
|
||||
host: &str,
|
||||
port: u16,
|
||||
identity: (&str, &str),
|
||||
pin: &str,
|
||||
name: &str,
|
||||
timeout: Duration,
|
||||
) -> Result<[u8; 32]> {
|
||||
use crate::quic::{PairChallenge, PairRequest, PairResult};
|
||||
|
||||
let client_fp = endpoint::fingerprint_of_pem(identity.0)
|
||||
.map_err(|_| PunktfunkError::InvalidArg("client cert pem"))?;
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(PunktfunkError::Io)?;
|
||||
let pin = pin.to_string();
|
||||
let name = name.to_string();
|
||||
let remote: std::net::SocketAddr = format!("{host}:{port}")
|
||||
.parse()
|
||||
.map_err(|_| PunktfunkError::InvalidArg("host:port"))?;
|
||||
|
||||
rt.block_on(async move {
|
||||
// The quinn endpoint must be created inside the runtime (it spawns its driver).
|
||||
let (ep, observed) = endpoint::client_pinned_with_identity(None, Some(identity));
|
||||
let ep = ep.map_err(|e| PunktfunkError::Io(std::io::Error::other(e.to_string())))?;
|
||||
let ceremony = async {
|
||||
let conn = ep
|
||||
.connect(remote, "punktfunk")
|
||||
.map_err(|_| PunktfunkError::InvalidArg("connect"))?
|
||||
.await
|
||||
.map_err(|e| PunktfunkError::Io(std::io::Error::other(e.to_string())))?;
|
||||
let host_fp = observed.lock().unwrap().ok_or(PunktfunkError::Crypto)?;
|
||||
let (mut send, mut recv) = conn
|
||||
.open_bi()
|
||||
.await
|
||||
.map_err(|e| PunktfunkError::Io(std::io::Error::other(e.to_string())))?;
|
||||
|
||||
io::write_msg(&mut send, &PairRequest { name }.encode()).await?;
|
||||
let challenge = PairChallenge::decode(&io::read_msg(&mut recv).await?)?;
|
||||
let proof = crate::quic::pair_proof(&pin, &challenge.salt, &client_fp, &host_fp);
|
||||
io::write_msg(&mut send, &crate::quic::PairProof { hmac: proof }.encode()).await?;
|
||||
let result = PairResult::decode(&io::read_msg(&mut recv).await?)?;
|
||||
conn.close(0u32.into(), b"pair done");
|
||||
if result.ok {
|
||||
Ok(host_fp)
|
||||
} else {
|
||||
Err(PunktfunkError::Crypto) // wrong PIN (or refused)
|
||||
}
|
||||
};
|
||||
tokio::time::timeout(timeout, ceremony)
|
||||
.await
|
||||
.map_err(|_| PunktfunkError::Timeout)?
|
||||
})
|
||||
}
|
||||
|
||||
/// The currently active session mode — the Welcome's, until an accepted
|
||||
/// [`NativeClient::request_mode`] switches it.
|
||||
pub fn mode(&self) -> Mode {
|
||||
*self.mode.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Ask the host to switch the live session to `mode` (no reconnect). Non-blocking:
|
||||
/// the request is queued; on acceptance the stream continues at the new mode (next
|
||||
/// frames open with an IDR carrying new parameter sets) and [`NativeClient::mode`]
|
||||
/// reflects it. A rejected request leaves the session unchanged.
|
||||
pub fn request_mode(&self, mode: Mode) -> Result<()> {
|
||||
self.reconfig_tx
|
||||
.send(mode)
|
||||
.map_err(|_| PunktfunkError::Closed)
|
||||
}
|
||||
|
||||
/// Pull the next reassembled, FEC-recovered access unit; [`PunktfunkError::NoFrame`] on
|
||||
/// timeout, [`PunktfunkError::Closed`]-class errors once the session ended.
|
||||
///
|
||||
@@ -187,33 +282,43 @@ struct WorkerArgs {
|
||||
port: u16,
|
||||
mode: Mode,
|
||||
pin: Option<[u8; 32]>,
|
||||
identity: Option<(String, String)>,
|
||||
frame_tx: SyncSender<Frame>,
|
||||
audio_tx: SyncSender<AudioPacket>,
|
||||
rumble_tx: SyncSender<(u16, u16, u16)>,
|
||||
input_rx: tokio::sync::mpsc::UnboundedReceiver<InputEvent>,
|
||||
reconfig_rx: tokio::sync::mpsc::UnboundedReceiver<Mode>,
|
||||
ready_tx: std::sync::mpsc::Sender<Result<(Mode, [u8; 32])>>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
mode_slot: Arc<std::sync::Mutex<Mode>>,
|
||||
}
|
||||
|
||||
/// The worker: QUIC handshake, then the input/datagram tasks + the blocking data-plane pump.
|
||||
/// The worker: QUIC handshake, then the input/datagram/control tasks + the blocking
|
||||
/// data-plane pump.
|
||||
async fn worker_main(args: WorkerArgs) {
|
||||
let WorkerArgs {
|
||||
host,
|
||||
port,
|
||||
mode,
|
||||
pin,
|
||||
identity,
|
||||
frame_tx,
|
||||
audio_tx,
|
||||
rumble_tx,
|
||||
mut input_rx,
|
||||
mut reconfig_rx,
|
||||
ready_tx,
|
||||
shutdown,
|
||||
mode_slot,
|
||||
} = args;
|
||||
let setup = async {
|
||||
let remote: std::net::SocketAddr = format!("{host}:{port}")
|
||||
.parse()
|
||||
.map_err(|_| PunktfunkError::InvalidArg("host:port"))?;
|
||||
let (ep, observed) = endpoint::client_pinned(pin);
|
||||
let (ep, observed) = endpoint::client_pinned_with_identity(
|
||||
pin,
|
||||
identity.as_ref().map(|(c, k)| (c.as_str(), k.as_str())),
|
||||
);
|
||||
let ep = ep.map_err(|e| PunktfunkError::Io(std::io::Error::other(e.to_string())))?;
|
||||
let conn = ep
|
||||
.connect(remote, "punktfunk")
|
||||
@@ -264,16 +369,17 @@ async fn worker_main(args: WorkerArgs) {
|
||||
let transport =
|
||||
UdpTransport::connect(&format!("0.0.0.0:{udp_port}"), &host_udp.to_string())?;
|
||||
let session = Session::new(welcome.session_config(Role::Client), Box::new(transport))?;
|
||||
Ok::<_, PunktfunkError>((conn, session, welcome.mode, fingerprint))
|
||||
Ok::<_, PunktfunkError>((conn, session, send, recv, welcome.mode, fingerprint))
|
||||
};
|
||||
|
||||
let (conn, mut session, negotiated, fingerprint) = match setup.await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (conn, mut session, mut ctrl_send, mut ctrl_recv, negotiated, fingerprint) =
|
||||
match setup.await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = ready_tx.send(Ok((negotiated, fingerprint)));
|
||||
|
||||
// Input task: embedder events → QUIC datagrams.
|
||||
@@ -284,6 +390,35 @@ async fn worker_main(args: WorkerArgs) {
|
||||
}
|
||||
});
|
||||
|
||||
// Control task: the handshake stream stays open for mid-stream renegotiation. One
|
||||
// request at a time — write Reconfigure, await Reconfigured, publish the active mode.
|
||||
{
|
||||
let mode_slot = mode_slot.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(want) = reconfig_rx.recv().await {
|
||||
if io::write_msg(&mut ctrl_send, &Reconfigure { mode: want }.encode())
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
let ack = match io::read_msg(&mut ctrl_recv).await {
|
||||
Ok(b) => match Reconfigured::decode(&b) {
|
||||
Ok(a) => a,
|
||||
Err(_) => break, // protocol error — stop renegotiating
|
||||
},
|
||||
Err(_) => break, // stream closed
|
||||
};
|
||||
if ack.accepted {
|
||||
*mode_slot.lock().unwrap() = ack.mode;
|
||||
tracing::info!(mode = ?ack.mode, "host accepted mode switch");
|
||||
} else {
|
||||
tracing::warn!(requested = ?want, active = ?ack.mode, "host rejected mode switch");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Datagram demux: host → client audio/rumble (try_send: a lagging embedder drops the
|
||||
// newest packet rather than backing up the QUIC receive path).
|
||||
let dgram_conn = conn.clone();
|
||||
|
||||
@@ -58,6 +58,179 @@ pub struct Start {
|
||||
pub client_udp_port: u16,
|
||||
}
|
||||
|
||||
/// `client → host`, any time after [`Start`]: switch the session to a new display mode
|
||||
/// (window resized, refresh changed) without reconnecting. The host answers with
|
||||
/// [`Reconfigured`]; on acceptance it rebuilds its virtual output + encoder at the new
|
||||
/// mode and the stream continues over the unchanged data plane — the first new-mode frame
|
||||
/// is an IDR with in-band parameter sets, which is all a decoder needs to follow.
|
||||
///
|
||||
/// Post-handshake messages carry a type byte after the magic (the handshake itself is
|
||||
/// positional and stays untyped for wire compatibility).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Reconfigure {
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
/// `host → client`: answer to [`Reconfigure`]. `accepted = false` means the requested
|
||||
/// mode was rejected (e.g. exceeds encoder limits) and the session continues at `mode`
|
||||
/// (the still-active one); `true` means `mode` is now being switched to live.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Reconfigured {
|
||||
pub accepted: bool,
|
||||
pub mode: Mode,
|
||||
}
|
||||
|
||||
/// Type byte of [`Reconfigure`] (first byte after the magic).
|
||||
pub const MSG_RECONFIGURE: u8 = 0x01;
|
||||
/// Type byte of [`Reconfigured`].
|
||||
pub const MSG_RECONFIGURED: u8 = 0x02;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Pairing ceremony (typed messages 0x10..): instead of a session Hello, a client may open
|
||||
// the control stream with PairRequest. The host shows a short PIN out-of-band (log/UI);
|
||||
// the user types it into the client, which proves knowledge with an HMAC bound to BOTH
|
||||
// certificate fingerprints — a MITM would need the PIN within its single attempt, and any
|
||||
// substituted certificate changes the proof. On success the host persists the client's
|
||||
// fingerprint (presented via QUIC client auth) and the client pins the host's.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// Type byte of [`PairRequest`].
|
||||
pub const MSG_PAIR_REQUEST: u8 = 0x10;
|
||||
/// Type byte of [`PairChallenge`].
|
||||
pub const MSG_PAIR_CHALLENGE: u8 = 0x11;
|
||||
/// Type byte of [`PairProof`].
|
||||
pub const MSG_PAIR_PROOF: u8 = 0x12;
|
||||
/// Type byte of [`PairResult`].
|
||||
pub const MSG_PAIR_RESULT: u8 = 0x13;
|
||||
|
||||
/// `client → host`: begin pairing. `name` is the human label the host stores (e.g.
|
||||
/// "Enrico's Mac"), at most 64 bytes of UTF-8.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PairRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// `host → client`: a fresh random salt for the proof; the host has generated (and is
|
||||
/// displaying) the PIN. One proof attempt per challenge.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PairChallenge {
|
||||
pub salt: [u8; 16],
|
||||
}
|
||||
|
||||
/// `client → host`: the proof, see [`pair_proof`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PairProof {
|
||||
pub hmac: [u8; 32],
|
||||
}
|
||||
|
||||
/// `host → client`: ceremony outcome.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PairResult {
|
||||
pub ok: bool,
|
||||
}
|
||||
|
||||
impl PairRequest {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let name = self.name.as_bytes();
|
||||
let n = name.len().min(64);
|
||||
let mut b = Vec::with_capacity(6 + n);
|
||||
b.extend_from_slice(MAGIC);
|
||||
b.push(MSG_PAIR_REQUEST);
|
||||
b.push(n as u8);
|
||||
b.extend_from_slice(&name[..n]);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairRequest> {
|
||||
if b.len() < 6 || &b[0..4] != MAGIC || b[4] != MSG_PAIR_REQUEST {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairRequest"));
|
||||
}
|
||||
let n = b[5] as usize;
|
||||
if n > 64 || b.len() < 6 + n {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairRequest name"));
|
||||
}
|
||||
Ok(PairRequest {
|
||||
name: String::from_utf8_lossy(&b[6..6 + n]).into_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PairChallenge {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(21);
|
||||
b.extend_from_slice(MAGIC);
|
||||
b.push(MSG_PAIR_CHALLENGE);
|
||||
b.extend_from_slice(&self.salt);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairChallenge> {
|
||||
if b.len() < 21 || &b[0..4] != MAGIC || b[4] != MSG_PAIR_CHALLENGE {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairChallenge"));
|
||||
}
|
||||
let mut salt = [0u8; 16];
|
||||
salt.copy_from_slice(&b[5..21]);
|
||||
Ok(PairChallenge { salt })
|
||||
}
|
||||
}
|
||||
|
||||
impl PairProof {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(37);
|
||||
b.extend_from_slice(MAGIC);
|
||||
b.push(MSG_PAIR_PROOF);
|
||||
b.extend_from_slice(&self.hmac);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairProof> {
|
||||
if b.len() < 37 || &b[0..4] != MAGIC || b[4] != MSG_PAIR_PROOF {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairProof"));
|
||||
}
|
||||
let mut hmac = [0u8; 32];
|
||||
hmac.copy_from_slice(&b[5..37]);
|
||||
Ok(PairProof { hmac })
|
||||
}
|
||||
}
|
||||
|
||||
impl PairResult {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(6);
|
||||
b.extend_from_slice(MAGIC);
|
||||
b.push(MSG_PAIR_RESULT);
|
||||
b.push(self.ok as u8);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<PairResult> {
|
||||
if b.len() < 6 || &b[0..4] != MAGIC || b[4] != MSG_PAIR_RESULT {
|
||||
return Err(PunktfunkError::InvalidArg("bad PairResult"));
|
||||
}
|
||||
Ok(PairResult { ok: b[5] != 0 })
|
||||
}
|
||||
}
|
||||
|
||||
/// The pairing proof both sides compute: `HMAC-SHA256(key = PIN ‖ salt,
|
||||
/// msg = client_fp ‖ host_fp)`. Binding both fingerprints into the MAC means a
|
||||
/// man-in-the-middle (whose certificates differ on at least one side) cannot replay or
|
||||
/// forward a valid proof; the PIN is single-attempt on the host, so a 4-digit space
|
||||
/// cannot be searched online.
|
||||
pub fn pair_proof(
|
||||
pin: &str,
|
||||
salt: &[u8; 16],
|
||||
client_fp: &[u8; 32],
|
||||
host_fp: &[u8; 32],
|
||||
) -> [u8; 32] {
|
||||
use hmac::{Hmac, Mac};
|
||||
let mut key = Vec::with_capacity(pin.len() + 16);
|
||||
key.extend_from_slice(pin.as_bytes());
|
||||
key.extend_from_slice(salt);
|
||||
let mut mac = <Hmac<sha2::Sha256> as Mac>::new_from_slice(&key).expect("hmac key");
|
||||
mac.update(client_fp);
|
||||
mac.update(host_fp);
|
||||
mac.finalize().into_bytes().into()
|
||||
}
|
||||
|
||||
impl Hello {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(20);
|
||||
@@ -177,6 +350,62 @@ impl Start {
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconfigure {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
// magic[0..4] type[4] w[5..9] h[9..13] hz[13..17]
|
||||
let mut b = Vec::with_capacity(17);
|
||||
b.extend_from_slice(MAGIC);
|
||||
b.push(MSG_RECONFIGURE);
|
||||
b.extend_from_slice(&self.mode.width.to_le_bytes());
|
||||
b.extend_from_slice(&self.mode.height.to_le_bytes());
|
||||
b.extend_from_slice(&self.mode.refresh_hz.to_le_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<Reconfigure> {
|
||||
if b.len() < 17 || &b[0..4] != MAGIC || b[4] != MSG_RECONFIGURE {
|
||||
return Err(PunktfunkError::InvalidArg("bad Reconfigure"));
|
||||
}
|
||||
let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
|
||||
Ok(Reconfigure {
|
||||
mode: Mode {
|
||||
width: u32at(5),
|
||||
height: u32at(9),
|
||||
refresh_hz: u32at(13),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconfigured {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
// magic[0..4] type[4] accepted[5] w[6..10] h[10..14] hz[14..18]
|
||||
let mut b = Vec::with_capacity(18);
|
||||
b.extend_from_slice(MAGIC);
|
||||
b.push(MSG_RECONFIGURED);
|
||||
b.push(self.accepted as u8);
|
||||
b.extend_from_slice(&self.mode.width.to_le_bytes());
|
||||
b.extend_from_slice(&self.mode.height.to_le_bytes());
|
||||
b.extend_from_slice(&self.mode.refresh_hz.to_le_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
pub fn decode(b: &[u8]) -> Result<Reconfigured> {
|
||||
if b.len() < 18 || &b[0..4] != MAGIC || b[4] != MSG_RECONFIGURED {
|
||||
return Err(PunktfunkError::InvalidArg("bad Reconfigured"));
|
||||
}
|
||||
let u32at = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
|
||||
Ok(Reconfigured {
|
||||
accepted: b[5] != 0,
|
||||
mode: Mode {
|
||||
width: u32at(6),
|
||||
height: u32at(10),
|
||||
refresh_hz: u32at(14),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame a message for the control stream: `u16 LE length || payload`.
|
||||
pub fn frame(payload: &[u8]) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(2 + payload.len());
|
||||
@@ -293,11 +522,38 @@ pub mod endpoint {
|
||||
key_der: rustls::pki_types::PrivateKeyDer<'static>,
|
||||
addr: std::net::SocketAddr,
|
||||
) -> anyhow_result::Result<quinn::Endpoint> {
|
||||
let server_config = quinn::ServerConfig::with_single_cert(vec![cert_der], key_der)
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
// Client auth is OFFERED but optional: a client that presents its self-signed
|
||||
// identity is fingerprinted post-handshake (pairing / --require-pairing checks);
|
||||
// one that presents none still connects (and is rejected at the app layer when
|
||||
// pairing is required).
|
||||
let rustls_cfg = rustls::ServerConfig::builder()
|
||||
.with_client_cert_verifier(Arc::new(AcceptAnyClientCert))
|
||||
.with_single_cert(vec![cert_der], key_der)
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("server config: {e}")))?;
|
||||
let quic_cfg = quinn::crypto::rustls::QuicServerConfig::try_from(rustls_cfg)
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("quic server config: {e}")))?;
|
||||
let server_config = quinn::ServerConfig::with_crypto(Arc::new(quic_cfg));
|
||||
Ok(quinn::Endpoint::server(server_config, addr)?)
|
||||
}
|
||||
|
||||
/// Generate a fresh self-signed identity (certificate + PKCS#8 key, both PEM) — what a
|
||||
/// client persists once and presents on every connect so hosts can recognize it.
|
||||
pub fn generate_identity() -> anyhow_result::Result<(String, String)> {
|
||||
let cert = rcgen::generate_simple_self_signed(vec!["punktfunk-client".into()])
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("self-signed cert: {e}")))?;
|
||||
Ok((cert.cert.pem(), cert.key_pair.serialize_pem()))
|
||||
}
|
||||
|
||||
/// Fingerprint of the client certificate a connection presented (host side), if any.
|
||||
pub fn peer_fingerprint(conn: &quinn::Connection) -> Option<[u8; 32]> {
|
||||
let identity = conn.peer_identity()?;
|
||||
let certs = identity
|
||||
.downcast::<Vec<rustls::pki_types::CertificateDer<'static>>>()
|
||||
.ok()?;
|
||||
certs.first().map(|c| cert_fingerprint(c.as_ref()))
|
||||
}
|
||||
|
||||
/// SHA-256 of a certificate's DER encoding — the fingerprint clients pin.
|
||||
pub fn cert_fingerprint(cert_der: &[u8]) -> [u8; 32] {
|
||||
use sha2::Digest;
|
||||
@@ -332,16 +588,40 @@ pub mod endpoint {
|
||||
/// `None` accepts any (trust-on-first-use). Either way the observed fingerprint is
|
||||
/// written to the returned slot during the handshake, so a TOFU caller can persist it.
|
||||
pub fn client_pinned(pin: Option<[u8; 32]>) -> PinnedClient {
|
||||
client_pinned_with_identity(pin, None)
|
||||
}
|
||||
|
||||
/// [`client_pinned`], additionally presenting a client identity (PEM cert + PKCS#8
|
||||
/// key) via TLS client auth — how a paired client identifies itself to the host.
|
||||
pub fn client_pinned_with_identity(
|
||||
pin: Option<[u8; 32]>,
|
||||
identity: Option<(&str, &str)>,
|
||||
) -> PinnedClient {
|
||||
let observed = Arc::new(Mutex::new(None));
|
||||
let ep = (|| {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let rustls_cfg = rustls::ClientConfig::builder()
|
||||
let builder = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(PinVerify {
|
||||
pin,
|
||||
observed: observed.clone(),
|
||||
}))
|
||||
.with_no_client_auth();
|
||||
}));
|
||||
let rustls_cfg = match identity {
|
||||
None => builder.with_no_client_auth(),
|
||||
Some((cert_pem, key_pem)) => {
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
let cert =
|
||||
rustls::pki_types::CertificateDer::from_pem_slice(cert_pem.as_bytes())
|
||||
.map_err(|e| {
|
||||
anyhow_result::Error::msg(format!("client cert pem: {e}"))
|
||||
})?;
|
||||
let key = rustls::pki_types::PrivateKeyDer::from_pem_slice(key_pem.as_bytes())
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("client key pem: {e}")))?;
|
||||
builder
|
||||
.with_client_auth_cert(vec![cert], key)
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("client auth: {e}")))?
|
||||
}
|
||||
};
|
||||
let quic_cfg = quinn::crypto::rustls::QuicClientConfig::try_from(rustls_cfg)
|
||||
.map_err(|e| anyhow_result::Error::msg(format!("quic client config: {e}")))?;
|
||||
let mut ep = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())?;
|
||||
@@ -377,6 +657,69 @@ pub mod endpoint {
|
||||
/// Fingerprint-pinning verifier: trust is the SHA-256 of the host's (self-signed) leaf
|
||||
/// cert, not a CA chain. With no pin it accepts any cert (TOFU) but still records what
|
||||
/// it saw, so the embedder can persist the fingerprint and pin it from then on.
|
||||
/// Server-side client-cert verifier: accept any (self-signed) client certificate but
|
||||
/// verify the handshake signature for real — possession of the presented cert's key is
|
||||
/// what makes the post-handshake fingerprint ([`peer_fingerprint`]) meaningful.
|
||||
/// Authorization (is this fingerprint paired?) happens at the application layer.
|
||||
#[derive(Debug)]
|
||||
struct AcceptAnyClientCert;
|
||||
|
||||
impl rustls::server::danger::ClientCertVerifier for AcceptAnyClientCert {
|
||||
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn client_auth_mandatory(&self) -> bool {
|
||||
false // unpaired/legacy clients still connect; gating is per-feature
|
||||
}
|
||||
|
||||
fn verify_client_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> std::result::Result<rustls::server::danger::ClientCertVerified, rustls::Error>
|
||||
{
|
||||
Ok(rustls::server::danger::ClientCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
dss: &rustls::DigitallySignedStruct,
|
||||
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
|
||||
{
|
||||
rustls::crypto::verify_tls12_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
dss: &rustls::DigitallySignedStruct,
|
||||
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error>
|
||||
{
|
||||
rustls::crypto::verify_tls13_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&rustls::crypto::ring::default_provider().signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PinVerify {
|
||||
pin: Option<[u8; 32]>,
|
||||
@@ -492,6 +835,34 @@ mod tests {
|
||||
assert_eq!(Start::decode(&s.encode()).unwrap(), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconfigure_roundtrip() {
|
||||
let rq = Reconfigure {
|
||||
mode: Mode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
refresh_hz: 144,
|
||||
},
|
||||
};
|
||||
assert_eq!(Reconfigure::decode(&rq.encode()).unwrap(), rq);
|
||||
for accepted in [true, false] {
|
||||
let rs = Reconfigured {
|
||||
accepted,
|
||||
mode: rq.mode,
|
||||
};
|
||||
assert_eq!(Reconfigured::decode(&rs.encode()).unwrap(), rs);
|
||||
}
|
||||
// The type byte separates the post-handshake messages from each other.
|
||||
assert!(Reconfigure::decode(
|
||||
&Reconfigured {
|
||||
accepted: true,
|
||||
mode: rq.mode
|
||||
}
|
||||
.encode()
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_datagram_roundtrip() {
|
||||
let opus = [0x42u8; 97];
|
||||
|
||||
Reference in New Issue
Block a user