fix(security): anti-replay, 0600 client key, open redirect, supply-chain

Address findings from a repo security review:

- core: add a sliding-window anti-replay filter over the AEAD-authenticated
  sequence in Session (poll_input/poll_frame), closing the input-replay gap the
  data plane previously left to the LAN/VPN trust assumption. 4096-deep window,
  unit-tested; the encrypted loopback suite confirms no false drops.
- clients: write the mTLS client private key 0600 and lock the config dir 0700
  on Unix (it was world-readable at the umask default), re-locking existing
  stores on load. pf-client-core::trust plus the probe's own identity writer.
  Windows keeps the %APPDATA% ACL; Android/Apple already wrap the key.
- web: fix a post-login open redirect — resolve `next` via URL and require it to
  stay same-origin, rejecting `/\evil.com` and tab/encoding variants the old
  `!startsWith("//")` guard missed. Also fixes the dead safeNextPath helper.
- ci: SHA-256-pin the BtbN FFmpeg DLLs bundled into the signed Windows installer
  (were fetched from the rolling `latest` tag unverified); fails closed on a
  re-roll, matching the VB-CABLE gate.
- ci: fail-open fork-guard on the Windows/Apple host-mode PR build jobs that
  share runner labels with the signing jobs. Definitive fix stays server-side
  (Gitea outside-collaborator approval / isolated PR runners) — see the notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 10:04:23 +02:00
parent ef39050dbc
commit e707a962b6
9 changed files with 338 additions and 18 deletions
+24 -1
View File
@@ -145,11 +145,34 @@ fn load_or_create_identity() -> Result<(String, String)> {
let dir = std::path::PathBuf::from(home).join(".config/punktfunk");
let (cp, kp) = (dir.join("client-cert.pem"), dir.join("client-key.pem"));
if let (Ok(c), Ok(k)) = (std::fs::read_to_string(&cp), std::fs::read_to_string(&kp)) {
// Re-lock a store an older build left world-readable (this key is shared with the other
// clients' `~/.config/punktfunk/client-key.pem`); best-effort.
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
let _ = std::fs::set_permissions(&kp, std::fs::Permissions::from_mode(0o600));
}
return Ok((c, k));
}
let (c, k) = endpoint::generate_identity().map_err(|e| anyhow!("generate identity: {e}"))?;
std::fs::create_dir_all(&dir)?;
std::fs::write(&cp, &c)?;
std::fs::write(&cp, &c)?; // the certificate is public
// The key is the mTLS credential a paired host authorizes for full remote control, so it must
// not be world-readable — create it 0600 (a plain `fs::write` honors the umask → typically 0644).
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&kp)?;
f.write_all(k.as_bytes())?;
}
#[cfg(not(unix))]
std::fs::write(&kp, &k)?;
tracing::info!(cert = %cp.display(), "generated client identity");
Ok((c, k))