mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
feat(nixpak): pass FIDO2 keys to Firefox, Chromium and Chrome (#515)
* feat(nixpak): pass FIDO2 keys to Firefox, Chromium and Chrome Assisted-by: DeepSeek Harness (deepseek-flash) * feat(nixpak): support FIDO2 hotplug via udev-maintained node directory Replace launch-time hidraw discovery with host udev rules that hard-link FIDO-tagged hidraw nodes into /dev/fido-hidraw and remove them on unplug. The sandbox dev-binds that directory and gets static /dev/hidrawN symlinks into it, so keys plugged in after launch work and no stale node survives a reused hidraw minor. Narrow the extra mounts from whole /sys and /run/udev to /sys/class/hidraw, /run/udev/data and /run/udev/control; the policy suite now forbids the broad mounts and checks the wrapper and bind follow the fido2 option. Assisted-by: Claude Code (claude-opus-5-5) * refactor(nixpak): rewrite FIDO2 bwrap wrapper in Python The bash argument loop was hard to follow. Python inserts the /dev/hidrawN symlinks before "--" with a single list splice, and the header now explains the whole host-udev -> bind -> symlink chain step by step. Assisted-by: Claude Code (claude-opus-5-5) * docs(canokey): document phone-only management policy PCs only use the CanoKey; CanoKey Console management stays in the phone app and the Admin PIN is never typed on a PC. Record what the config enforces (sandboxed browsers get no raw USB/CCID, no vendor WebUSB udev rule) and what it does not (uaccess on the raw USB node, PC/SC access to the Admin applet, CTAP management from browsers). Assisted-by: Claude Code (claude-opus-5-5) * docs(canokey): separate management from use, link CanoKey docs Assisted-by: Claude Code (claude-opus-5-5) * fix(nixpak): create /dev/fido-hidraw from the udev rule NixOS applies /dev tmpfiles entries only at boot; switch-to-configuration runs systemd-tmpfiles-resetup with --exclude-prefix=/dev. After a switch without reboot the directory was missing and every `ln` failed with exit code 1, so no browser saw the key. The add rule now runs `mkdir -p` first. Also fix the documented trigger command: `udevadm trigger` defaults to `change`, which the add-only rule ignores. Assisted-by: Claude Code (claude-opus-5-5) * refactor(nixpak): keep tmpfiles as the only creator of /dev/fido-hidraw The /dev tmpfiles units run before systemd-udevd on every boot, so the directory always exists before coldplug. The mkdir in the udev rule only covered the first switch without reboot and duplicated the tmpfiles rule; drop it and document the one-time `systemd-tmpfiles --prefix` command. Assisted-by: Claude Code (claude-opus-5-5) * docs(nixpak): record FIDO2 hardware validation in Firefox and Chrome After a reboot on ai with the CanoKey present at boot, udev linked only the FIDO interface and WebAuthn works in Firefox and Chrome. Chromium and plugging the key in after launch remain pending. Assisted-by: Claude Code (claude-opus-5-5)
This commit is contained in:
1 parent
432de26764
commit
2cf3a58028
15 files changed
+171
-11
No files matched your search
+10
-2
@@ -332,6 +332,13 @@ isolation. Source review and socket metadata—not reads of private credentials
|
|||||||
Browser media and browserpass are enabled only for Firefox/Chromium/Chrome. Brave Origin and Edge
|
Browser media and browserpass are enabled only for Firefox/Chromium/Chrome. Brave Origin and Edge
|
||||||
are backup browsers with no media grants. browserpass exposes password-store/GPG data and agent
|
are backup browsers with no media grants. browserpass exposes password-store/GPG data and agent
|
||||||
operations.
|
operations.
|
||||||
|
- Firefox/Chromium/Chrome receive FIDO2 security-key passthrough for WebAuthn/passkeys. Host udev
|
||||||
|
rules hard-link only FIDO-tagged hidraw nodes into `/dev/fido-hidraw` and remove them on unplug;
|
||||||
|
the sandbox binds that directory plus read-only `/sys/class/hidraw` and the udev database. The
|
||||||
|
CanoKey KBD and CCID interfaces, other HID devices and raw USB stay outside the sandbox; Brave
|
||||||
|
Origin and Edge receive no FIDO2 passthrough. A compromised browser can use the key's FIDO
|
||||||
|
interface; the key's PIN and touch policy remain the authentication boundary. Hotplug works for
|
||||||
|
host-network Firefox/Chromium; pasta-isolated Chrome needs the key present at launch.
|
||||||
- Nixpak supplements Chromium's renderer sandbox, which remains enabled. Zoom/WeChat retain their
|
- Nixpak supplements Chromium's renderer sandbox, which remains enabled. Zoom/WeChat retain their
|
||||||
FHS runtimes inside the outer boundary; inner mounts cannot recover hidden host resources.
|
FHS runtimes inside the outer boundary; inner mounts cannot recover hidden host resources.
|
||||||
- Desktop access has limits: GPU drivers remain exposed, PulseAudio permits recording, read-only
|
- Desktop access has limits: GPU drivers remain exposed, PulseAudio permits recording, read-only
|
||||||
@@ -348,8 +355,9 @@ isolation. Source review and socket metadata—not reads of private credentials
|
|||||||
- Policy/build passes for all nine wrappers. Interactive validation passed for Firefox, Chrome,
|
- Policy/build passes for all nine wrappers. Interactive validation passed for Firefox, Chrome,
|
||||||
Chromium, Brave Origin, Edge, QQ, Telegram and Zoom; the Chromium smoke test also validates
|
Chromium, Brave Origin, Edge, QQ, Telegram and Zoom; the Chromium smoke test also validates
|
||||||
renderer sandboxing, environment clearing, bind scope and profile persistence. WeChat's
|
renderer sandboxing, environment clearing, bind scope and profile persistence. WeChat's
|
||||||
interactive Wayland check remains pending. See the
|
interactive Wayland check remains pending. FIDO2 browser passthrough passed WebAuthn with the
|
||||||
[runtime validation](../hardening/tests/runtime-validation.md).
|
CanoKey in Firefox and Chrome; Chromium and plugging the key in after launch still need
|
||||||
|
validation. See the [runtime validation](../hardening/tests/runtime-validation.md).
|
||||||
|
|
||||||
#### Agent-first diagnostics by host role
|
#### Agent-first diagnostics by host role
|
||||||
|
|
||||||
|
|||||||
+30
-7
@@ -18,8 +18,12 @@ workflow.
|
|||||||
browsers share an app-specific `/tmp` so external URLs reuse one browser instance.
|
browsers share an app-specific `/tmp` so external URLs reuse one browser instance.
|
||||||
- Read-only `/nix/store`; GPU and display; fonts and themes; LAN and Internet.
|
- Read-only `/nix/store`; GPU and display; fonts and themes; LAN and Internet.
|
||||||
- Portals for file chooser, URI opening, notifications, input methods and document exports.
|
- Portals for file chooser, URI opening, notifications, input methods and document exports.
|
||||||
- No whole host home, system D-Bus, SSH agent, USB/serial or raw camera. Raw PipeWire is granted
|
- No whole host home, system D-Bus, SSH agent, raw USB/serial or raw camera. For
|
||||||
only to MuseScore for its ALSA-via-PipeWire audio path; see [Limits](#limits).
|
Firefox/Chromium/Chrome, the only device passthrough is the FIDO HID interface of a security key:
|
||||||
|
host udev rules hard-link FIDO-tagged hidraw nodes into `/dev/fido-hidraw`, which the sandbox
|
||||||
|
binds together with read-only `/sys/class/hidraw` and the udev database, so keys can be plugged in
|
||||||
|
after launch. Other HID interfaces, CCID and raw USB stay hidden. Raw PipeWire is granted only to
|
||||||
|
MuseScore for its ALSA-via-PipeWire audio path; see [Limits](#limits).
|
||||||
- Host loopback only for Firefox/Chromium; all other apps use pasta-isolated loopback. Chromium is
|
- Host loopback only for Firefox/Chromium; all other apps use pasta-isolated loopback. Chromium is
|
||||||
an intentional development exception. Brave Origin and Edge are backup browsers and receive no
|
an intentional development exception. Brave Origin and Edge are backup browsers and receive no
|
||||||
media grants.
|
media grants.
|
||||||
@@ -31,9 +35,9 @@ explicitly; other files use the document portal. Every app also has private writ
|
|||||||
|
|
||||||
| App | Network | Writable paths | Grants |
|
| App | Network | Writable paths | Grants |
|
||||||
| ------------ | ------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
| ------------ | ------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
| Firefox | Host loopback | `~/.mozilla`, personal files, Trash, Firefox artwork | Audio, browserpass, MPRIS, idle; camera/screen sharing intentionally unavailable |
|
| Firefox | Host loopback | `~/.mozilla`, personal files, Trash, Firefox artwork | Audio, browserpass, MPRIS, idle, FIDO2 key; camera/screen sharing intentionally unavailable |
|
||||||
| Chromium | Host loopback | `~/.config/chromium`, personal files | Media, browserpass, MPRIS, idle, v12 Secret |
|
| Chromium | Host loopback | `~/.config/chromium`, personal files | Media, browserpass, MPRIS, idle, FIDO2 key, v12 Secret |
|
||||||
| Chrome | Isolated | `~/.config/google-chrome`, Downloads | Media, browserpass, MPRIS, idle, v12 Secret |
|
| Chrome | Isolated | `~/.config/google-chrome`, Downloads | Media, browserpass, MPRIS, idle, FIDO2 key, v12 Secret |
|
||||||
| Brave Origin | Isolated | `~/.config/BraveSoftware/Brave-Origin`, Downloads | v12 Secret |
|
| Brave Origin | Isolated | `~/.config/BraveSoftware/Brave-Origin`, Downloads | v12 Secret |
|
||||||
| Edge | Isolated | `~/.config/microsoft-edge`, Downloads | v12 Secret |
|
| Edge | Isolated | `~/.config/microsoft-edge`, Downloads | v12 Secret |
|
||||||
| QQ | Isolated | Personal files, private app state | Media, notifications, tray, MPRIS, idle, accessibility |
|
| QQ | Isolated | Personal files, private app state | Media, notifications, tray, MPRIS, idle, accessibility |
|
||||||
@@ -73,7 +77,22 @@ All browsers get read-only NSS certificates, printing, native notifications and
|
|||||||
- PulseAudio grants recording authority. Read-only GPG sockets still permit signing and decryption.
|
- PulseAudio grants recording authority. Read-only GPG sockets still permit signing and decryption.
|
||||||
- pasta isolates loopback, not LAN access. Chromecast discovery is unverified.
|
- pasta isolates loopback, not LAN access. Chromecast discovery is unverified.
|
||||||
- Brave Origin and Edge intentionally have no PulseAudio, Camera, ScreenCast or MPRIS grants. They
|
- Brave Origin and Edge intentionally have no PulseAudio, Camera, ScreenCast or MPRIS grants. They
|
||||||
are backup browsers, not media browsers.
|
are backup browsers, not media browsers. They also receive no FIDO2 passthrough.
|
||||||
|
- FIDO2 passthrough is limited to the hidraw interface whose report descriptor declares the FIDO
|
||||||
|
usage page. On the CanoKey that is the FIDO interface only: its KBD and CCID interfaces, other HID
|
||||||
|
devices and raw USB stay outside the sandbox. A compromised browser process can send raw CTAP to
|
||||||
|
the key; the key's PIN and touch policy remain the authentication boundary. The read-only udev
|
||||||
|
database exposes properties of all host devices, much of which is already readable via the sysfs
|
||||||
|
device binds.
|
||||||
|
- CanoKey management (CanoKey Console over WebUSB) is intentionally unavailable: Firefox has no
|
||||||
|
WebUSB and the sandbox exposes no raw USB. PCs only use the key; management happens in the phone
|
||||||
|
app. See [canokey.nix](../modules/nixos/desktop/canokey.nix) for the policy and its limits.
|
||||||
|
- FIDO2 hotplug relies on udev events, which only reach the host network namespace. Firefox and
|
||||||
|
Chromium therefore detect keys plugged in after launch; pasta-isolated Chrome needs the key
|
||||||
|
present before launch. `/dev/fido-hidraw` is created at boot. When first deploying with `switch`
|
||||||
|
instead of a reboot, run `sudo systemd-tmpfiles --create --prefix=/dev/fido-hidraw`, then replug
|
||||||
|
the key (or `sudo udevadm trigger --action=add -s hidraw`) and fully restart the browsers: a
|
||||||
|
browser started while the directory was missing has no bind for it.
|
||||||
- Firefox camera and remote screen sharing are intentionally disabled. Firefox does not support the
|
- Firefox camera and remote screen sharing are intentionally disabled. Firefox does not support the
|
||||||
XDG Camera/ScreenCast portal path used here, so granting camera or screen-capture access would
|
XDG Camera/ScreenCast portal path used here, so granting camera or screen-capture access would
|
||||||
require widening the sandbox with raw `/dev/video*` or direct PipeWire access. We choose not to
|
require widening the sandbox with raw `/dev/video*` or direct PipeWire access. We choose not to
|
||||||
@@ -108,6 +127,8 @@ All browsers get read-only NSS certificates, printing, native notifications and
|
|||||||
and user workflows still need confirmation.
|
and user workflows still need confirmation.
|
||||||
- Chromium smoke test passed: renderer sandbox, environment clearing, bind scope, disposable-profile
|
- Chromium smoke test passed: renderer sandbox, environment clearing, bind scope, disposable-profile
|
||||||
persistence and loopback policy.
|
persistence and loopback policy.
|
||||||
|
- FIDO2 browser passthrough is enabled for Firefox/Chromium/Chrome only. WebAuthn with the CanoKey
|
||||||
|
passed in Firefox and Chrome; Chromium and plugging the key in after launch still need validation.
|
||||||
- WeChat uses the native nixpkgs-master package with nixpak as the only bwrap layer. Build/policy
|
- WeChat uses the native nixpkgs-master package with nixpak as the only bwrap layer. Build/policy
|
||||||
pass; interactive Wayland validation is still pending.
|
pass; interactive Wayland validation is still pending.
|
||||||
- Evidence and repeat commands: [runtime validation](tests/runtime-validation.md),
|
- Evidence and repeat commands: [runtime validation](tests/runtime-validation.md),
|
||||||
@@ -121,7 +142,9 @@ All browsers get read-only NSS certificates, printing, native notifications and
|
|||||||
[desktop-integration.nix](nixpaks/modules/desktop-integration.nix): portal and desktop service
|
[desktop-integration.nix](nixpaks/modules/desktop-integration.nix): portal and desktop service
|
||||||
grants.
|
grants.
|
||||||
- [browser-permissions.nix](nixpaks/modules/browser-permissions.nix),
|
- [browser-permissions.nix](nixpaks/modules/browser-permissions.nix),
|
||||||
[chromium-browser.nix](nixpaks/modules/chromium-browser.nix): browser scopes and launcher.
|
[fido-bwrap.nix](nixpaks/modules/fido-bwrap.nix),
|
||||||
|
[chromium-browser.nix](nixpaks/modules/chromium-browser.nix): browser scopes, FIDO2 passthrough
|
||||||
|
wrapper and launcher.
|
||||||
- [nixpaks/wechat.nix](nixpaks/wechat.nix): WeChat package and data setup.
|
- [nixpaks/wechat.nix](nixpaks/wechat.nix): WeChat package and data setup.
|
||||||
- [kernel-hardening.nix](kernel-hardening.nix), [apparmor/](apparmor/): kernel sysctl and AppArmor
|
- [kernel-hardening.nix](kernel-hardening.nix), [apparmor/](apparmor/): kernel sysctl and AppArmor
|
||||||
profiles.
|
profiles.
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
desktopFile = "chromium-browser";
|
desktopFile = "chromium-browser";
|
||||||
browserpass = true;
|
browserpass = true;
|
||||||
media = true;
|
media = true;
|
||||||
|
fido2 = true;
|
||||||
hostLoopback = true;
|
hostLoopback = true;
|
||||||
personalFiles = true;
|
personalFiles = true;
|
||||||
secretPortal = true;
|
secretPortal = true;
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ let
|
|||||||
wrapper = _pkgs: path: (_pkgs.callPackage path callArgs);
|
wrapper = _pkgs: path: (_pkgs.callPackage path callArgs);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
# FIDO2 hotplug for sandboxed browsers (see modules/fido-bwrap.nix). The
|
||||||
|
# directory is created at boot, before udevd starts, so the browser bind
|
||||||
|
# succeeds even with no key present. NixOS applies /dev tmpfiles entries only
|
||||||
|
# at boot; after a switch without reboot, run
|
||||||
|
# `sudo systemd-tmpfiles --create --prefix=/dev/fido-hidraw` once.
|
||||||
|
# Hard links share the device inode, so logind's uaccess ACLs stay
|
||||||
|
# authoritative, and removal on unplug leaves no stale node for a reused
|
||||||
|
# hidraw minor. 60-fido-id.rules sets ID_FIDO_TOKEN from the report
|
||||||
|
# descriptor, so the CanoKey's keyboard interface is never linked.
|
||||||
|
systemd.tmpfiles.rules = [ "d /dev/fido-hidraw 0755 root root -" ];
|
||||||
|
services.udev.extraRules = ''
|
||||||
|
ACTION=="add", SUBSYSTEM=="hidraw", ENV{ID_FIDO_TOKEN}=="1", RUN+="${pkgs.coreutils}/bin/ln -f /dev/%k /dev/fido-hidraw/%k"
|
||||||
|
ACTION=="remove", SUBSYSTEM=="hidraw", RUN+="${pkgs.coreutils}/bin/rm -f /dev/fido-hidraw/%k"
|
||||||
|
'';
|
||||||
|
|
||||||
# Add nixpaked Apps into nixpkgs, and reference them in home-manager or other nixos modules
|
# Add nixpaked Apps into nixpkgs, and reference them in home-manager or other nixos modules
|
||||||
nixpkgs.overlays = [
|
nixpkgs.overlays = [
|
||||||
(_: super: {
|
(_: super: {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ let
|
|||||||
media = true;
|
media = true;
|
||||||
personalFiles = true;
|
personalFiles = true;
|
||||||
browserpass = true;
|
browserpass = true;
|
||||||
|
fido2 = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Firefox keeps the host network: local development servers and the
|
# Firefox keeps the host network: local development servers and the
|
||||||
|
|||||||
@@ -13,5 +13,6 @@
|
|||||||
desktopFile = "google-chrome";
|
desktopFile = "google-chrome";
|
||||||
browserpass = true;
|
browserpass = true;
|
||||||
media = true;
|
media = true;
|
||||||
|
fido2 = true;
|
||||||
secretPortal = true;
|
secretPortal = true;
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
# Shared browser capabilities. Network topology and application profiles are
|
# Shared browser capabilities. Network topology and application profiles are
|
||||||
# selected by the app; personal files, browserpass, Secret portal and media are explicit opt-ins.
|
# selected by the app; personal files, browserpass, Secret portal, media and
|
||||||
|
# FIDO2 security-key access are explicit opt-ins.
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
sloth,
|
sloth,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.browserPermissions;
|
cfg = config.browserPermissions;
|
||||||
passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store";
|
passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store";
|
||||||
|
# Maintained by the host udev rules in ../default.nix.
|
||||||
|
fidoDir = "/dev/fido-hidraw";
|
||||||
|
fidoBwrap = import ./fido-bwrap.nix { inherit pkgs fidoDir; };
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [ ./desktop-base.nix ];
|
imports = [ ./desktop-base.nix ];
|
||||||
@@ -17,6 +22,7 @@ in
|
|||||||
personalFiles = lib.mkEnableOption "personal Documents, media directories and Trash";
|
personalFiles = lib.mkEnableOption "personal Documents, media directories and Trash";
|
||||||
browserpass = lib.mkEnableOption "browserpass access to the password store and GPG agent";
|
browserpass = lib.mkEnableOption "browserpass access to the password store and GPG agent";
|
||||||
secretPortal = lib.mkEnableOption "the app-scoped Secret portal for v12 cookie encryption";
|
secretPortal = lib.mkEnableOption "the app-scoped Secret portal for v12 cookie encryption";
|
||||||
|
fido2 = lib.mkEnableOption "FIDO2 security-key (CanoKey/YubiKey) WebAuthn and passkey access";
|
||||||
};
|
};
|
||||||
config = {
|
config = {
|
||||||
portalGroups = {
|
portalGroups = {
|
||||||
@@ -36,6 +42,7 @@ in
|
|||||||
accessibility = true;
|
accessibility = true;
|
||||||
};
|
};
|
||||||
bubblewrap = {
|
bubblewrap = {
|
||||||
|
package = lib.mkIf cfg.fido2 fidoBwrap;
|
||||||
sockets.pulse = cfg.media;
|
sockets.pulse = cfg.media;
|
||||||
bind.rw = [
|
bind.rw = [
|
||||||
sloth.xdgDownloadDir
|
sloth.xdgDownloadDir
|
||||||
@@ -57,7 +64,17 @@ in
|
|||||||
(sloth.concat' sloth.homeDir "/.gnupg")
|
(sloth.concat' sloth.homeDir "/.gnupg")
|
||||||
passwordStore
|
passwordStore
|
||||||
(sloth.concat' sloth.runtimeDir "/gnupg")
|
(sloth.concat' sloth.runtimeDir "/gnupg")
|
||||||
|
]
|
||||||
|
# libudev enumeration and hotplug: hidraw class links (device directories
|
||||||
|
# come from desktop-base/GPU sysfs binds), the udev database without which
|
||||||
|
# devnode-backed devices count as uninitialized, and the control socket
|
||||||
|
# whose presence enables the udev monitor. Root owns the socket.
|
||||||
|
++ lib.optionals cfg.fido2 [
|
||||||
|
"/sys/class/hidraw"
|
||||||
|
"/run/udev/data"
|
||||||
|
"/run/udev/control"
|
||||||
];
|
];
|
||||||
|
bind.dev = lib.optional cfg.fido2 fidoDir;
|
||||||
env = lib.optionalAttrs cfg.browserpass { PASSWORD_STORE_DIR = passwordStore; };
|
env = lib.optionalAttrs cfg.browserpass { PASSWORD_STORE_DIR = passwordStore; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
secretPortal ? false,
|
secretPortal ? false,
|
||||||
hostLoopback ? false,
|
hostLoopback ? false,
|
||||||
personalFiles ? false,
|
personalFiles ? false,
|
||||||
|
fido2 ? false,
|
||||||
privateWindowFlag ? "--incognito",
|
privateWindowFlag ? "--incognito",
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -64,6 +65,7 @@ let
|
|||||||
personalFiles
|
personalFiles
|
||||||
media
|
media
|
||||||
secretPortal
|
secretPortal
|
||||||
|
fido2
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
bubblewrap.bind.rw = [
|
bubblewrap.bind.rw = [
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Replacement `bwrap` for browsers with browserPermissions.fido2 (Firefox,
|
||||||
|
# Chromium, Chrome), so they can use FIDO2 security keys.
|
||||||
|
#
|
||||||
|
# The problem: browsers find keys by listing /sys/class/hidraw, then open the
|
||||||
|
# FIDO ones at /dev/hidrawN (e.g. /dev/hidraw6). The sandbox /dev is a private
|
||||||
|
# tmpfs, so those nodes are missing. The number N is only known once a key is
|
||||||
|
# plugged in, which may be after the browser starts.
|
||||||
|
#
|
||||||
|
# How keys reach the sandbox:
|
||||||
|
# 1. Host (../default.nix): when a key is plugged in, udev hard-links its FIDO
|
||||||
|
# node into /dev/fido-hidraw/ and deletes it on unplug. Non-FIDO interfaces,
|
||||||
|
# such as the CanoKey keyboard, are never linked.
|
||||||
|
# 2. browser-permissions.nix binds /dev/fido-hidraw into the sandbox. It is a
|
||||||
|
# shared live directory, so later plug/unplug shows up inside immediately.
|
||||||
|
# 3. This script creates /dev/hidraw0 ... /dev/hidraw63 in the sandbox as
|
||||||
|
# symlinks into /dev/fido-hidraw/. A link works only while a FIDO key with
|
||||||
|
# that number is plugged in; the others fail to open with ENOENT.
|
||||||
|
#
|
||||||
|
# nixpak has no option for creating symlinks, so this script adds the
|
||||||
|
# `--symlink` options and then runs the real bwrap.
|
||||||
|
{ pkgs, fidoDir }:
|
||||||
|
pkgs.writers.writePython3Bin "bwrap"
|
||||||
|
{
|
||||||
|
# Interpolated store paths exceed the default line length.
|
||||||
|
flakeIgnore = [ "E501" ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
REAL_BWRAP = "${pkgs.bubblewrap}/bin/bwrap"
|
||||||
|
FIDO_DIR = "${fidoDir}"
|
||||||
|
# The kernel allows at most 64 hidraw devices (HIDRAW_MAX_DEVICES).
|
||||||
|
HIDRAW_COUNT = 64
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
# nixpak runs: bwrap <sandbox options> -- <browser command>
|
||||||
|
# Insert the symlinks just before "--": by then nixpak's "--dev /dev" has
|
||||||
|
# created the sandbox /dev they live in. nixpak's D-Bus proxy sandbox runs
|
||||||
|
# bwrap without "--" and stays unchanged.
|
||||||
|
if "--" in args:
|
||||||
|
symlinks = []
|
||||||
|
for n in range(HIDRAW_COUNT):
|
||||||
|
# e.g. --symlink /dev/fido-hidraw/hidraw6 /dev/hidraw6
|
||||||
|
symlinks += ["--symlink", f"{FIDO_DIR}/hidraw{n}", f"/dev/hidraw{n}"]
|
||||||
|
separator = args.index("--")
|
||||||
|
args[separator:separator] = symlinks
|
||||||
|
|
||||||
|
os.execv(REAL_BWRAP, [REAL_BWRAP, *args])
|
||||||
|
''
|
||||||
@@ -42,7 +42,9 @@ let
|
|||||||
|| lib.hasPrefix "/dev/video" path
|
|| lib.hasPrefix "/dev/video" path
|
||||||
|| lib.hasPrefix "/dev/bus/usb" path
|
|| lib.hasPrefix "/dev/bus/usb" path
|
||||||
|| path == "/run/dbus"
|
|| path == "/run/dbus"
|
||||||
|| path == "/run/dbus/system_bus_socket";
|
|| path == "/run/dbus/system_bus_socket"
|
||||||
|
|| path == "/sys"
|
||||||
|
|| path == "/run/udev";
|
||||||
|
|
||||||
hasHostFontconfig = app: lib.elem [ "/etc/static/fonts" "/etc/fonts" ] app.bubblewrap.bind.ro;
|
hasHostFontconfig = app: lib.elem [ "/etc/static/fonts" "/etc/fonts" ] app.bubblewrap.bind.ro;
|
||||||
|
|
||||||
@@ -95,6 +97,14 @@ let
|
|||||||
!(app.browserPermissions.secretPortal or false)
|
!(app.browserPermissions.secretPortal or false)
|
||||||
&& !(hasDbusFragment app "org.freedesktop.portal.Secret");
|
&& !(hasDbusFragment app "org.freedesktop.portal.Secret");
|
||||||
|
|
||||||
|
# FIDO2 needs both the udev-maintained node directory and the bwrap wrapper
|
||||||
|
# that links /dev/hidrawN into it; neither may leak to other apps.
|
||||||
|
fido2Check =
|
||||||
|
app: expected:
|
||||||
|
(app.browserPermissions.fido2 or false) == expected
|
||||||
|
&& lib.elem "/dev/fido-hidraw" app.bubblewrap.bind.dev == expected
|
||||||
|
&& (lib.getName app.bubblewrap.package == "bwrap") == expected;
|
||||||
|
|
||||||
trayCheck =
|
trayCheck =
|
||||||
app: expected:
|
app: expected:
|
||||||
if expected then
|
if expected then
|
||||||
@@ -130,6 +140,7 @@ let
|
|||||||
"${name}/tray" = trayCheck app (spec.tray or false);
|
"${name}/tray" = trayCheck app (spec.tray or false);
|
||||||
"${name}/browserpass" =
|
"${name}/browserpass" =
|
||||||
(app.browserPermissions.browserpass or false) == (spec.browserpass or false);
|
(app.browserPermissions.browserpass or false) == (spec.browserpass or false);
|
||||||
|
"${name}/fido2" = fido2Check app (spec.fido2 or false);
|
||||||
"${name}/personalFiles" =
|
"${name}/personalFiles" =
|
||||||
(app.browserPermissions.personalFiles or false) == (spec.personalFiles or false);
|
(app.browserPermissions.personalFiles or false) == (spec.personalFiles or false);
|
||||||
"${name}/secretPortal" = secretPortalCheck app (spec.secretPortal or false);
|
"${name}/secretPortal" = secretPortalCheck app (spec.secretPortal or false);
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ mkAppPolicy "chromium" {
|
|||||||
personalFiles = true;
|
personalFiles = true;
|
||||||
browserpass = true;
|
browserpass = true;
|
||||||
secretPortal = true;
|
secretPortal = true;
|
||||||
|
fido2 = true;
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ mkAppPolicy "firefox" {
|
|||||||
personalFiles = true;
|
personalFiles = true;
|
||||||
browserpass = true;
|
browserpass = true;
|
||||||
secretPortal = false;
|
secretPortal = false;
|
||||||
|
fido2 = true;
|
||||||
extraChecks = {
|
extraChecks = {
|
||||||
capabilitiesMatchChromium =
|
capabilitiesMatchChromium =
|
||||||
firefox.browserPermissions.media == chromium.browserPermissions.media
|
firefox.browserPermissions.media == chromium.browserPermissions.media
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ mkAppPolicy "google-chrome" {
|
|||||||
personalFiles = false;
|
personalFiles = false;
|
||||||
browserpass = true;
|
browserpass = true;
|
||||||
secretPortal = true;
|
secretPortal = true;
|
||||||
|
fido2 = true;
|
||||||
}
|
}
|
||||||
@@ -87,6 +87,20 @@ v10 storage; monitor `Local State.os_crypt.portal.prev_init_success`. Firefox us
|
|||||||
instead. Browserpass separately exposes the password store and GPG-agent socket to
|
instead. Browserpass separately exposes the password store and GPG-agent socket to
|
||||||
Firefox/Chromium/Chrome only.
|
Firefox/Chromium/Chrome only.
|
||||||
|
|
||||||
|
## FIDO2 passthrough
|
||||||
|
|
||||||
|
Passed after a reboot on `ai` (CanoKey Canary, PR #515 head): with the key present at boot, the host
|
||||||
|
linked only its FIDO interface into `/dev/fido-hidraw`, and WebAuthn works in Firefox and Chrome.
|
||||||
|
Chromium, plugging the key in after browser launch, and the in-sandbox keyboard-interface check are
|
||||||
|
still pending. Full checklist:
|
||||||
|
|
||||||
|
1. On the host, `ls -l /dev/fido-hidraw/` lists only the FIDO interface (`ID_FIDO_TOKEN=1`); it
|
||||||
|
follows plug and unplug.
|
||||||
|
2. Firefox/Chromium: start without the key, plug it in, then register and sign in on
|
||||||
|
<https://webauthn.io>. Unplug and replug without restarting, then sign in again.
|
||||||
|
3. Chrome: the same flow with the key present at launch; record whether a later plug is detected.
|
||||||
|
4. In each sandbox, the CanoKey keyboard interface's `/dev/hidrawN` fails to open with `ENOENT`.
|
||||||
|
|
||||||
## Zoom
|
## Zoom
|
||||||
|
|
||||||
Zoom keeps its upstream FHS runtime inside the outer nixpak boundary. The nested sandbox propagates
|
Zoom keeps its upstream FHS runtime inside the outer nixpak boundary. The nested sandbox propagates
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ in
|
|||||||
enable = lib.mkEnableOption "CanoKey hardware security key support";
|
enable = lib.mkEnableOption "CanoKey hardware security key support";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# CanoKey docs: https://docs.canokeys.org/userguide/
|
||||||
|
#
|
||||||
|
# Principle: management and use happen on different devices. Linux PCs only
|
||||||
|
# use the key (WebAuthn/passkeys, OpenPGP, SSH). CanoKey Console management
|
||||||
|
# (Admin PIN, application/factory reset, device settings) happens only in the
|
||||||
|
# phone app (https://docs.canokeys.org/userguide/admin/), so the Admin PIN is
|
||||||
|
# never typed on a PC.
|
||||||
|
#
|
||||||
|
# Sandboxed browsers back this up: they see only the FIDO HID interface
|
||||||
|
# (hardening/nixpaks), not raw USB or CCID, so the WebUSB console cannot reach
|
||||||
|
# the key, and the vendor's WebUSB udev rule (MODE 0666 on the raw USB node)
|
||||||
|
# is deliberately not installed. Unsandboxed software is trusted; if it is
|
||||||
|
# malicious, the PC is compromised regardless.
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# systemd tags FIDO/CCID devices uaccess (60-fido-id.rules); pcscd covers OpenPGP.
|
# systemd tags FIDO/CCID devices uaccess (60-fido-id.rules); pcscd covers OpenPGP.
|
||||||
services.pcscd.enable = true;
|
services.pcscd.enable = true;
|
||||||
|
|||||||
Reference in new issue
Block a user