3526517eb1
ci / rust (push) Failing after 45s
apple / swift (push) Successful in 57s
ci / web (push) Successful in 39s
ci / docs-site (push) Successful in 38s
windows-host / package (push) Successful in 3m26s
android / android (push) Successful in 3m40s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m24s
deb / build-publish (push) Successful in 2m10s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m22s
decky / build-publish (push) Successful in 25s
ci / bench (push) Successful in 4m44s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m4s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m7s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m37s
flatpak / build-publish (push) Successful in 4m17s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m30s
docker / deploy-docs (push) Successful in 23s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 7m53s
Two strands, entangled in punktfunk1.rs, committed together (one builds-green tree). HDR pipeline Step 0 — glass-to-glass colour-metadata transport (docs/hdr-pipeline-plan.md): - Protocol/ABI: ColorInfo on the Welcome + a 0xCE HdrMeta datagram carry the source colour space + HDR10 static mastering metadata (quic.rs, abi.rs connect_ex5 fixing caps=0). - New platform-independent, unit-tested HDR static-metadata helpers (hdr.rs): chromaticities (1/50000), mastering luminance (0.0001 cd/m2), MaxCLL/MaxFALL in HDR10/ST.2086 units. - Capture/encode hooks (capture.rs, encode.rs set_hdr_meta) + Linux client / probe plumbing. Security-audit hardening — top 3 from docs/security-review.md, each adversarially verified: - #1 [HIGH] Secret file permissions. The host key.pem/cert.pem and both trust stores are now written owner-only: 0600 + dir 0700 on Unix (mirrors mgmt_token), best-effort SYSTEM/Administrators/OWNER-only icacls DACL on Windows (%ProgramData% is Users-readable). Closes a local key-disclosure -> host-impersonation gap. New gamestream::{create_private_dir, write_secret_file} + a 0600 regression test. - #2 [HIGH] Native SPAKE2 PIN is single-use. The PIN is consumed the moment the host sends its key-confirmation (which lets the client test its one guess), before reading the proof, so any completed attempt -- right OR wrong -- disarms the window. A wrong PIN isn't observable host-side (the client aborts before sending its proof), so consuming on first attempt is what delivers the documented "one online guess" instead of an unbounded brute-force of the static 4-digit PIN. Test verifies single-use. - #3 [MEDIUM] RTSP packetSize is bounded ([64,2048] in stream_config) and VideoPacketizer::new uses saturating .max(1), killing a PRE-AUTH div-by-zero/underflow panic of the video thread. Tests for {0,15,16,17} + out-of-range rejection. fmt + clippy -D warnings clean; full workspace test suite green (93 host tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.9 KiB
Rust
91 lines
3.9 KiB
Rust
//! The host's self-signed RSA-2048 identity: the cert returned to clients as `plaincert`
|
|
//! during pairing AND presented as the TLS server cert on 47984 (Moonlight pins it). The
|
|
//! cert's own X.509 signature bytes are an input to the pairing hashes, so we extract them.
|
|
|
|
use super::config_dir;
|
|
use anyhow::{anyhow, Context, Result};
|
|
use rsa::pkcs1v15::SigningKey;
|
|
use rsa::pkcs8::DecodePrivateKey;
|
|
use rsa::RsaPrivateKey;
|
|
use sha2::Sha256;
|
|
use std::fs;
|
|
|
|
pub struct ServerIdentity {
|
|
/// PEM of the cert (returned hex-encoded as `plaincert`; also the TLS server cert).
|
|
pub cert_pem: String,
|
|
/// PKCS#8 PEM of the private key (TLS server key).
|
|
pub key_pem: String,
|
|
/// The cert's X.509 `signatureValue` bytes — bound into the pairing challenge hashes.
|
|
pub signature: Vec<u8>,
|
|
/// RSA-PKCS1v15-SHA256 signer over the host key (the pairing `sign256`).
|
|
pub signing_key: SigningKey<Sha256>,
|
|
}
|
|
|
|
impl ServerIdentity {
|
|
pub fn load_or_create() -> Result<ServerIdentity> {
|
|
let dir = config_dir();
|
|
let cert_path = dir.join("cert.pem");
|
|
let key_path = dir.join("key.pem");
|
|
let (cert_pem, key_pem) = match (
|
|
fs::read_to_string(&cert_path),
|
|
fs::read_to_string(&key_path),
|
|
) {
|
|
(Ok(c), Ok(k)) if !c.trim().is_empty() && !k.trim().is_empty() => (c, k),
|
|
_ => {
|
|
let (c, k) = generate()?;
|
|
// The private key is the trust root for EVERY surface (TLS server cert, pairing
|
|
// signing, the QUIC identity clients pin) — write it owner-only (0600 / SYSTEM-only
|
|
// DACL) so a local user can't read it and impersonate the host. The dir is 0700.
|
|
super::create_private_dir(&dir).ok();
|
|
super::write_secret_file(&key_path, k.as_bytes())
|
|
.with_context(|| format!("write {}", key_path.display()))?;
|
|
// The cert is public (handed to clients), but write it owner-only too for consistency.
|
|
super::write_secret_file(&cert_path, c.as_bytes())
|
|
.with_context(|| format!("write {}", cert_path.display()))?;
|
|
tracing::info!(path = %cert_path.display(), "generated punktfunk host certificate (RSA-2048, key 0600)");
|
|
(c, k)
|
|
}
|
|
};
|
|
Self::from_pems(cert_pem, key_pem)
|
|
}
|
|
|
|
/// Build an identity from PEMs (no I/O).
|
|
pub fn from_pems(cert_pem: String, key_pem: String) -> Result<ServerIdentity> {
|
|
let priv_key = RsaPrivateKey::from_pkcs8_pem(&key_pem).context("parse host private key")?;
|
|
let signing_key = SigningKey::<Sha256>::new(priv_key);
|
|
let signature = cert_signature(&cert_pem)?;
|
|
Ok(ServerIdentity {
|
|
cert_pem,
|
|
key_pem,
|
|
signature,
|
|
signing_key,
|
|
})
|
|
}
|
|
|
|
/// Throwaway in-memory identity — nothing touches the config dir (used by tests).
|
|
pub fn ephemeral() -> Result<ServerIdentity> {
|
|
let (cert_pem, key_pem) = generate()?;
|
|
Self::from_pems(cert_pem, key_pem)
|
|
}
|
|
}
|
|
|
|
fn generate() -> Result<(String, String)> {
|
|
let key = rcgen::KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256).context("rcgen RSA keygen")?;
|
|
let mut params = rcgen::CertificateParams::new(Vec::<String>::new()).context("cert params")?;
|
|
params
|
|
.distinguished_name
|
|
.push(rcgen::DnType::CommonName, "punktfunk");
|
|
params.not_before = rcgen::date_time_ymd(2020, 1, 1);
|
|
params.not_after = rcgen::date_time_ymd(2040, 1, 1);
|
|
let cert = params.self_signed(&key).context("self-sign cert")?;
|
|
Ok((cert.pem(), key.serialize_pem()))
|
|
}
|
|
|
|
/// Extract the X.509 `signatureValue` bytes from a cert PEM.
|
|
fn cert_signature(cert_pem: &str) -> Result<Vec<u8>> {
|
|
let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
|
|
.map_err(|e| anyhow!("parse cert pem: {e}"))?;
|
|
let x509 = pem.parse_x509().context("parse x509")?;
|
|
Ok(x509.signature_value.data.to_vec())
|
|
}
|