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:
@@ -82,6 +82,36 @@
|
||||
// `shard_payload` so `HEADER_LEN + shard_payload + CRYPTO_OVERHEAD ≤ MAX_DATAGRAM_BYTES`.
|
||||
#define MAX_DATAGRAM_BYTES 2048
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Type byte of [`Reconfigure`] (first byte after the magic).
|
||||
#define MSG_RECONFIGURE 1
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Type byte of [`Reconfigured`].
|
||||
#define MSG_RECONFIGURED 2
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Type byte of [`PairRequest`].
|
||||
#define MSG_PAIR_REQUEST 16
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Type byte of [`PairChallenge`].
|
||||
#define MSG_PAIR_CHALLENGE 17
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Type byte of [`PairProof`].
|
||||
#define MSG_PAIR_PROOF 18
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Type byte of [`PairResult`].
|
||||
#define MSG_PAIR_RESULT 19
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Datagram wire tags. Video rides UDP; everything low-rate rides QUIC datagrams,
|
||||
// demultiplexed by the first byte: input = [`crate::input::INPUT_MAGIC`] (0xC8),
|
||||
@@ -324,9 +354,15 @@ PunktfunkStatus punktfunk_get_stats(PunktfunkSession *s, PunktfunkStats *out);
|
||||
// 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.
|
||||
PunktfunkConnection *punktfunk_connect(const char *host,
|
||||
uint16_t port,
|
||||
uint32_t width,
|
||||
@@ -334,9 +370,47 @@ PunktfunkConnection *punktfunk_connect(const char *host,
|
||||
uint32_t refresh_hz,
|
||||
const uint8_t *pin_sha256,
|
||||
uint8_t *observed_sha256_out,
|
||||
const char *client_cert_pem,
|
||||
const char *client_key_pem,
|
||||
uint32_t timeout_ms);
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// 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`.
|
||||
PunktfunkStatus punktfunk_generate_identity(char *cert_pem_out,
|
||||
uintptr_t cert_cap,
|
||||
char *key_pem_out,
|
||||
uintptr_t key_cap);
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// 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.
|
||||
PunktfunkStatus punktfunk_pair(const char *host,
|
||||
uint16_t port,
|
||||
const char *client_cert_pem,
|
||||
const char *client_key_pem,
|
||||
const char *pin,
|
||||
const char *name,
|
||||
uint8_t *host_sha256_out,
|
||||
uint32_t timeout_ms);
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Pull the next reassembled access unit, waiting up to `timeout_ms`. Returns
|
||||
// [`PunktfunkStatus::NoFrame`] on timeout and [`PunktfunkStatus::Closed`] once the session ended.
|
||||
@@ -391,7 +465,8 @@ PunktfunkStatus punktfunk_connection_send_input(PunktfunkConnection *c,
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// 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).
|
||||
@@ -401,6 +476,22 @@ PunktfunkStatus punktfunk_connection_mode(const PunktfunkConnection *c,
|
||||
uint32_t *refresh_hz);
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// 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.
|
||||
PunktfunkStatus punktfunk_connection_request_mode(const PunktfunkConnection *c,
|
||||
uint32_t width,
|
||||
uint32_t height,
|
||||
uint32_t refresh_hz);
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Close the connection and free the handle (joins the internal threads). NULL is a no-op.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user