feat(host/mgmt): mTLS auth — a paired client's cert authorizes the REST API
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
apple / swift (push) Successful in 1m15s
ci / bench (push) Successful in 1m35s
deb / build-publish (push) Successful in 4m31s
ci / rust (push) Successful in 7m2s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m30s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m37s
docker / deploy-docs (push) Successful in 19s
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 30s
apple / swift (push) Successful in 1m15s
ci / bench (push) Successful in 1m35s
deb / build-publish (push) Successful in 4m31s
ci / rust (push) Successful in 7m2s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 6s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 3s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 5m30s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m37s
docker / deploy-docs (push) Successful in 19s
Phase 1 of moving the library off a manual mgmt token: the management API now serves over HTTPS with the host's persistent identity (the cert clients already pin) and OPTIONAL client-cert auth. A request is authorized if EITHER the peer presented a client certificate whose SHA-256 is in the punktfunk/1 paired store (the same trust the QUIC data plane uses — so a paired native client needs no token), OR it carries the bearer token (the web console / admin). `/health` stays open. axum-server can't surface the peer cert to a handler, so `serve_https` runs the rustls handshake itself (tokio-rustls), reads the verified peer certificate, and serves the axum Router over hyper with the fingerprint attached to each request; `require_auth` checks it against `NativePairing::is_paired`. The verifier reuses the GameStream AcceptAnyClientCert, parameterized to make client auth optional (a browser with no cert still completes the handshake and falls back to the token). Validated live: paired cert → 200, unpaired cert / no creds / bad token → 401, bearer → 200, /health open. (Note: the API is now HTTPS with a self-signed cert — a browser shows a one-time trust prompt; native clients pin by fingerprint.) Next: Apple client presents its identity over mTLS (drops the token field); embed the web console; enable HTTPS mgmt by default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ mod pairing;
|
||||
mod rtsp;
|
||||
mod serverinfo;
|
||||
mod stream;
|
||||
mod tls;
|
||||
pub(crate) mod tls;
|
||||
mod video;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
@@ -17,6 +17,9 @@ use std::sync::Arc;
|
||||
#[derive(Debug)]
|
||||
struct AcceptAnyClientCert {
|
||||
provider: Arc<CryptoProvider>,
|
||||
/// nvhttp/pairing REQUIRES the client cert (mandatory); the mgmt API REQUESTS it but lets a
|
||||
/// certless peer (a browser, falling back to a bearer token) through (optional).
|
||||
mandatory: bool,
|
||||
}
|
||||
|
||||
impl ClientCertVerifier for AcceptAnyClientCert {
|
||||
@@ -25,7 +28,7 @@ impl ClientCertVerifier for AcceptAnyClientCert {
|
||||
}
|
||||
|
||||
fn client_auth_mandatory(&self) -> bool {
|
||||
true
|
||||
self.mandatory
|
||||
}
|
||||
|
||||
fn root_hint_subjects(&self) -> &[DistinguishedName] {
|
||||
@@ -76,8 +79,24 @@ impl ClientCertVerifier for AcceptAnyClientCert {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a mutual-TLS `ServerConfig` presenting the host cert/key.
|
||||
/// Build a mutual-TLS `ServerConfig` presenting the host cert/key. nvhttp/pairing path: the
|
||||
/// client cert is **mandatory**.
|
||||
pub fn server_config(cert_pem: &str, key_pem: &str) -> Result<Arc<ServerConfig>> {
|
||||
build_server_config(cert_pem, key_pem, true)
|
||||
}
|
||||
|
||||
/// Like [`server_config`] but the client cert is **optional** — a certless peer (a browser using a
|
||||
/// bearer token) still completes the handshake. Used by the management API's mTLS auth: a paired
|
||||
/// client presents its cert (authorized by fingerprint), everyone else falls back to the token.
|
||||
pub fn server_config_optional_client(cert_pem: &str, key_pem: &str) -> Result<Arc<ServerConfig>> {
|
||||
build_server_config(cert_pem, key_pem, false)
|
||||
}
|
||||
|
||||
fn build_server_config(
|
||||
cert_pem: &str,
|
||||
key_pem: &str,
|
||||
mandatory: bool,
|
||||
) -> Result<Arc<ServerConfig>> {
|
||||
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
||||
let certs = rustls_pemfile::certs(&mut cert_pem.as_bytes())
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
@@ -88,6 +107,7 @@ pub fn server_config(cert_pem: &str, key_pem: &str) -> Result<Arc<ServerConfig>>
|
||||
|
||||
let verifier = Arc::new(AcceptAnyClientCert {
|
||||
provider: provider.clone(),
|
||||
mandatory,
|
||||
});
|
||||
let config = ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()
|
||||
|
||||
Reference in New Issue
Block a user