Give the seated user the TPM, so nobody has to join the tss group
ci / build and test (push) Successful in 3m0s
ci / android head (push) Successful in 3m24s
ci / desktop nightly (push) Successful in 1m4s
ci / api image (push) Successful in 24s

ADR 0007 decision 4 refused this a commit ago, on the grounds that widening
access to the machine's TPM is a system-wide decision an application package
should not make for an administrator. That refusal is overturned here, and the
reasoning it is replaced with is narrower rather than louder — because the
refusal had treated "widen access" as one thing when it is two.

Adding a person to `tss` is what the refusal was really about, and it stays the
wrong answer: permanent, effective over SSH, applying whether or not they are at
the keyboard, and needing an administrator and a fresh login before the feature
appears at all.

Tagging /dev/tpmrm0 with `uaccess` is a different thing. It is systemd's own
mechanism for this exact question — the one /dev/dri, webcams, scanners and FIDO
keys already use — and logind grants the ACL to the user of the *active local
session*, then takes it back when that session ends. Not over SSH, not on an
inactive VT, not to daemons. The rule changes neither the mode nor the group, so
everything that worked before still works and the ACL is layered on top.

tpmrm and never tpm, which is what makes it defensible at all: /dev/tpm0 is the
raw device and is exclusive, where /dev/tpmrm0 is the kernel's resource manager,
which virtualises handles and isolates callers precisely so that more than one
unprivileged program can use the chip.

◆ THE COST IS A DENIAL OF SERVICE AND NOT A DISCLOSURE, AND IT IS WRITTEN DOWN.

TPM objects are protected by their own auth values, so this lets nobody read
anybody else's sealed data. What it allows is *failed* attempts — and a TPM
answers enough of those by locking out everyone, which is the same anti-hammering
this project relies on to make a four-digit PIN safe. On a machine that unlocks
its disk with the TPM at boot, somebody sitting at it can trip that and stop the
next boot from unlocking until it recovers.

An administrator who does not accept that masks the file:

    sudo ln -s /dev/null /etc/udev/rules.d/71-dodossh-tpm.rules

which is the standard override, survives upgrades, and puts DodoSSH back to
behaving as though the machine had no TPM. That is in the rule's own comments,
because the person who needs it is reading the file rather than the ADR.

Numbered 71 because 60-tpm-udev.rules is what creates the node's ownership and
73-seat-late.rules is what turns the tag into an ACL; verified in that order on
Fedora 44, along with `udevadm verify` accepting the file and the mask being
reported as "masked by previous entry". %post reloads and replays it against the
tpmrm subsystem, because a rule that waits for a reboot is a feature that does
not work on the day it is installed.

Both channels install the same path deliberately: this is about the machine's
TPM rather than about either build, the file is byte-identical from both, and
giving each its own copy would mean removing one package silently revoked the
other's access.

Phase 19 grew a check that the ACL exists for the local session and does *not*
exist for an SSH one, and another that masking the rule really does put the
machine back — then tells you to undo it before the checks that follow.

Also recorded, after it cost five confusing failures: swtpm serves exactly one
client, so a shell readiness probe sitting on the port makes the whole device-key
suite fail with "The TPM closed the connection" and nothing pointing at why.
This commit is contained in:
jaap-jan committed 2026-08-22 20:38:22 +02:00
1 parent 4552538533
commit c2c860b4f2
10 files changed
+220 -48

No files matched your search

+4 -1
View File
@@ -22,9 +22,12 @@
# Files that must keep LF regardless of platform — shell scripts run in Linux # Files that must keep LF regardless of platform — shell scripts run in Linux
# containers, Dockerfiles are read by a Linux daemon, and an RPM spec is a shell # containers, Dockerfiles are read by a Linux daemon, and an RPM spec is a shell
# script in disguise: rpmbuild feeds its %install section to /bin/sh, where a # script in disguise: rpmbuild feeds its %install section to /bin/sh, where a
# carriage return at the end of a line is an argument rather than nothing. # carriage return at the end of a line is an argument rather than nothing. A udev
# rule is parsed by udev's own lexer, which treats a stray carriage return as part
# of the value it is reading.
*.sh text eol=lf *.sh text eol=lf
*.spec text eol=lf *.spec text eol=lf
*.rules text eol=lf
Dockerfile text eol=lf Dockerfile text eol=lf
*.Dockerfile text eol=lf *.Dockerfile text eol=lf
Caddyfile text eol=lf Caddyfile text eol=lf
+20 -6
View File
@@ -213,14 +213,26 @@ and stops responding after a few, which is what makes four digits enough and wha
useless. Two honest caveats, both in useless. Two honest caveats, both in
[ADR 0007](docs/adr/0007-device-key-protection.md) decision 4: [ADR 0007](docs/adr/0007-device-key-protection.md) decision 4:
- **On most Fedora installs this is not offered**, because `/dev/tpmrm0` belongs to `root:tss` and your
account is not in `tss`. DodoSSH will not widen that for you — granting every local user access to the
machine's TPM is the administrator's call. If you want it: `sudo usermod -aG tss $USER`, then log out and
back in.
- **It is weaker than Windows Hello or Touch ID**, and the difference does not show. Those draw their prompt - **It is weaker than Windows Hello or Touch ID**, and the difference does not show. Those draw their prompt
from inside the operating system; this one is a window DodoSSH draws, so something already running as you from inside the operating system; this one is a window DodoSSH draws, so something already running as you
could imitate it. It could imitate the passphrase screen too, which is worth far more — but the guarantee could imitate it. It could imitate the passphrase screen too, which is worth far more — but the guarantee
is not the same and this page will not pretend it is. is not the same and this page will not pretend it is.
- **The package grants your session access to the TPM, and you should know what that means.** Fedora leaves
`/dev/tpmrm0` readable only by the `tss` group, so this would otherwise need an administrator and a fresh
login. Instead the package installs a udev rule tagging the device `uaccess` — the same mechanism your
webcam and your FIDO key already use, which grants an ACL to **whoever is logged in at the machine** and
takes it away when they log out. Not over SSH, not to a user switched away on another VT, not to daemons.
The cost is not secrecy — TPM objects are protected by their own PINs — it is that anyone at the keyboard
can make failed attempts, and a TPM answers enough of those by locking out *everyone*. **If this machine
unlocks its disk with the TPM at boot**, that means somebody sitting at it could stop the next boot from
unlocking until the lockout recovers. If you would rather not have that, one command puts it back:
```bash
sudo ln -s /dev/null /etc/udev/rules.d/71-dodossh-tpm.rules
```
DodoSSH then behaves as though the machine had no TPM: no PIN offered, passphrase every time.
See [docs/unlocking-without-the-passphrase.md](docs/unlocking-without-the-passphrase.md). See [docs/unlocking-without-the-passphrase.md](docs/unlocking-without-the-passphrase.md).
@@ -997,8 +1009,10 @@ keychain plus a terminal — and the spike that gates all of it.
installs. Splitting it that way keeps the part that can put software on your machine where it already installs. Splitting it that way keeps the part that can put software on your machine where it already
was — with your package manager — while fixing the half `dnf` cannot do for a package that is in no was — with your package manager — while fixing the half `dnf` cannot do for a package that is in no
repository. The device key is sealed into the TPM behind a PIN **the TPM checks and counts failures repository. The device key is sealed into the TPM behind a PIN **the TPM checks and counts failures
against**, which is what makes four digits enough; on most Fedora installs it is not offered at all, against**, which is what makes four digits enough. Reaching the TPM needs no administrator and no group:
because an ordinary user may not open `/dev/tpmrm0` and this package will not widen that for them. See the package installs a udev rule tagging `/dev/tpmrm0` with `uaccess`, so the ACL follows whoever is
logged in at the machine and goes away with them — the same mechanism a webcam uses. What that costs, and
the one symlink that undoes it, is in the rule's own comments. See
[ADR 0007](docs/adr/0007-device-key-protection.md) decision 4, [ADR 0007](docs/adr/0007-device-key-protection.md) decision 4,
[ADR 0013](docs/adr/0013-desktop-distribution-and-updates.md) decision 11 and [ADR 0013](docs/adr/0013-desktop-distribution-and-updates.md) decision 11 and
[Installing on Linux](#installing-on-linux). [Installing on Linux](#installing-on-linux).
+54
View File
@@ -0,0 +1,54 @@
# Lets the person sitting at this machine use its TPM, without joining a group.
#
# ◆ WHAT THIS CHANGES, AND WHAT IT DELIBERATELY DOES NOT.
#
# Fedora's 60-tpm-udev.rules leaves /dev/tpmrm0 as root:tss mode 0660, so using it means being in
# the tss group — which means an administrator, a usermod and a fresh login before DodoSSH will
# offer to seal a device key. This adds the uaccess tag, and nothing else: the mode and the group
# are untouched, so everything that worked before still works and an ACL is layered on top.
#
# uaccess is systemd's own mechanism for exactly this question and is what /dev/dri, webcams,
# scanners and FIDO keys already use. logind grants a POSIX ACL to the user of the *active local
# session* and takes it away again when that session ends. So:
#
# - somebody logged in at the physical machine gets access;
# - somebody logged in over SSH does not;
# - a user switched away to another VT does not;
# - a system daemon running as its own user does not.
#
# That is a much narrower grant than adding a person to tss, which is permanent, applies over SSH,
# and applies whether or not they are the one at the keyboard.
#
# ◆ tpmrm AND NOT tpm, WHICH IS THE WHOLE REASON THIS IS DEFENSIBLE.
#
# /dev/tpm0 is the raw device and access to it is exclusive — two users cannot share it, and a
# process holding it can leave the chip in a state that breaks everything else. /dev/tpmrm0 is the
# kernel's resource manager: it virtualises handles, swaps contexts and isolates callers from each
# other, and it exists *because* the TPM is meant to be usable by more than one unprivileged
# program. Granting a seat's user the manager is the use it was built for. Granting the raw device
# would not be, and this rule never names it.
#
# ◆ THE COST, WHICH IS REAL AND IS NOT ABOUT SECRECY.
#
# TPM keys are protected by their own auth values, so this grants nobody the ability to read
# anybody else's sealed data. What it does grant is the ability to make failed authorisation
# attempts, and a TPM answers those by counting them and eventually refusing everyone — the same
# dictionary-attack lockout DodoSSH relies on to make a four-digit PIN safe.
#
# On a machine whose disk is unlocked by the TPM at boot (systemd-cryptenroll, Clevis), a local user
# who trips that lockout can stop the next boot from unlocking until it recovers. That is a denial
# of service against the machine's owner by somebody already sitting at the machine, and it is worth
# knowing rather than discovering. An administrator who does not accept it can mask this file:
#
# sudo ln -s /dev/null /etc/udev/rules.d/71-dodossh-tpm.rules
#
# which is the standard override and survives upgrades. DodoSSH then behaves exactly as it did
# before this file existed: no device key is offered, and unlocking asks for the passphrase.
#
# Numbered 71 because 73-seat-late.rules is what turns the tag into an ACL, and 60-tpm-udev.rules
# is what creates the device node's ownership in the first place. Anything outside that gap either
# never runs or runs too late to matter.
ACTION=="remove", GOTO="dodossh_tpm_end"
SUBSYSTEM=="tpmrm", KERNEL=="tpmrm[0-9]*", TAG+="uaccess"
LABEL="dodossh_tpm_end"
+32
View File
@@ -273,6 +273,25 @@ for size in 48 64 128 256; do
%{buildroot}%{_datadir}/icons/hicolor/${size}x${size}/apps/%{dodo_bin}.png %{buildroot}%{_datadir}/icons/hicolor/${size}x${size}/apps/%{dodo_bin}.png
done done
# ◆ THE UDEV RULE, WHICH IS THE ONE FILE THIS PACKAGE PUTS OUTSIDE ITS OWN TREE.
#
# It tags /dev/tpmrm0 with uaccess so the person at the machine can use the TPM without being put in
# the tss group by an administrator. The rule itself carries the whole argument, including what it
# costs and the one-line way to mask it.
#
# Installed for both channels, and that is not a mistake to be tidied up. The two packages own
# different paths for the same reason ADR 0013 rule 9 gives, but this file is about the machine's
# TPM rather than about either build — a nightly and a release on one machine want exactly the same
# access, and giving each its own copy would mean removing one package silently revoked the other's.
# rpm refuses to let two packages own one path with different content, which is what makes this
# safe: the file is byte-identical from both, so it is shared rather than duplicated.
#
# /usr/lib/udev/rules.d hardcoded rather than %{_udevrulesdir}, which comes from systemd-rpm-macros
# and is not present on the Alpine host CI builds this on. Hardcoding a path that has been stable
# for a decade beats a macro that expands to nothing and silently installs into the buildroot's root.
mkdir -p %{buildroot}/usr/lib/udev/rules.d
install -m 0644 %{dodo_udev} %{buildroot}/usr/lib/udev/rules.d/71-dodossh-tpm.rules
# Installed rather than listed straight out of the source tree. `%license` is a %doc-style marker and # Installed rather than listed straight out of the source tree. `%license` is a %doc-style marker and
# resolves its argument against the build directory, which this spec does not have — there is no # resolves its argument against the build directory, which this spec does not have — there is no
# %prep and no %build, because the payload arrives already published. Putting the file where it # %prep and no %build, because the payload arrives already published. Putting the file where it
@@ -288,10 +307,22 @@ install -m 0644 %{dodo_licence} %{buildroot}%{_licensedir}/%{name}/LICENSE
/usr/bin/update-desktop-database %{_datadir}/applications &>/dev/null || : /usr/bin/update-desktop-database %{_datadir}/applications &>/dev/null || :
/usr/bin/gtk-update-icon-cache -qtf %{_datadir}/icons/hicolor &>/dev/null || : /usr/bin/gtk-update-icon-cache -qtf %{_datadir}/icons/hicolor &>/dev/null || :
# Reloaded and replayed, because a rule that only takes effect at the next boot is a feature that
# does not work on the day it is installed — and "reboot and try again" is the worst possible answer
# to "why is the PIN option missing". The trigger is scoped to the tpmrm subsystem so this re-runs
# rules for one device rather than for every device on the machine.
/usr/bin/udevadm control --reload &>/dev/null || :
/usr/bin/udevadm trigger --subsystem-match=tpmrm --action=change &>/dev/null || :
%postun %postun
/usr/bin/update-desktop-database %{_datadir}/applications &>/dev/null || : /usr/bin/update-desktop-database %{_datadir}/applications &>/dev/null || :
/usr/bin/gtk-update-icon-cache -qtf %{_datadir}/icons/hicolor &>/dev/null || : /usr/bin/gtk-update-icon-cache -qtf %{_datadir}/icons/hicolor &>/dev/null || :
# On the way out too, so removing the package takes the ACL with it rather than leaving the last
# session's user holding access to the TPM until the next reboot.
/usr/bin/udevadm control --reload &>/dev/null || :
/usr/bin/udevadm trigger --subsystem-match=tpmrm --action=change &>/dev/null || :
%files %files
# The whole tree, owned by this package, which is what makes `dnf remove` take all of it. The user's # The whole tree, owned by this package, which is what makes `dnf remove` take all of it. The user's
# vault cache is not in here and is not touched by an uninstall — ClientPaths puts it under # vault cache is not in here and is not touched by an uninstall — ClientPaths puts it under
@@ -301,6 +332,7 @@ install -m 0644 %{dodo_licence} %{buildroot}%{_licensedir}/%{name}/LICENSE
%{_bindir}/%{dodo_bin} %{_bindir}/%{dodo_bin}
%{_datadir}/applications/%{dodo_bin}.desktop %{_datadir}/applications/%{dodo_bin}.desktop
%{_datadir}/icons/hicolor/*/apps/%{dodo_bin}.png %{_datadir}/icons/hicolor/*/apps/%{dodo_bin}.png
/usr/lib/udev/rules.d/71-dodossh-tpm.rules
%license %{_licensedir}/%{name}/LICENSE %license %{_licensedir}/%{name}/LICENSE
%changelog %changelog
+33 -7
View File
@@ -139,13 +139,39 @@ not claimed anywhere in the interface**, which is why `DesktopDeviceKeyStores` d
why the prompt is deliberately plain rather than dressed to look like a system dialog — teaching people why the prompt is deliberately plain rather than dressed to look like a system dialog — teaching people
that a convincing DodoSSH PIN box is a system dialog is the one thing this design must not do. that a convincing DodoSSH PIN box is a system dialog is the one thing this design must not do.
**Availability is narrower than "the machine has a TPM", and that is a deployment fact rather than a bug.** **Reaching the TPM at all is the other half, and the package now arranges it.** On Fedora `/dev/tpmrm0` is
On Fedora `/dev/tpmrm0` is `root:tss` mode 0660 and desktop users are not in `tss`, so the ordinary case is `root:tss` mode 0660 and desktop users are not in `tss`, so without help the ordinary case is a working TPM
a working TPM this process may not open. That reports unavailable and the passphrase path continues, this process may not open — which reports unavailable and keeps asking for the passphrase, exactly as a
exactly as a machine with no TPM does. **Shipping a udev rule to widen it is refused**: granting every local machine with no TPM does.
desktop user access to the machine's TPM is a system-wide security decision, some distributions restrict it
deliberately, and an application package has no business making it on the administrator's behalf. One line ◆ **This originally said shipping a udev rule to widen that was refused, on the grounds that it is a
of `usermod` is the answer, and the README says so. system-wide decision an application package should not make. That refusal was overturned deliberately, and
the reasoning it was replaced with is narrower rather than louder.** The refusal treated "widen access" as
one thing. It is two, and they are not close:
- *Adding a person to `tss`* is what the refusal was really about, and it remains the wrong answer. It is
permanent, it applies whether or not they are at the machine, it applies over SSH, and it needs an
administrator and a fresh login before the feature appears at all.
- *Tagging `/dev/tpmrm0` with `uaccess`* is the mechanism systemd already uses for `/dev/dri`, webcams,
scanners and FIDO keys: logind grants a POSIX ACL to the user of the **active local session** and removes
it when that session ends. Not over SSH, not on an inactive VT, not to daemons. `build/linux/71-dodossh-tpm.rules`
is that one line, and it changes neither the mode nor the group, so anything that worked before still does.
**`tpmrm` and never `tpm`, which is what makes it defensible at all.** `/dev/tpm0` is the raw device and is
exclusive; a process holding it can leave the chip in a state that breaks every other user. `/dev/tpmrm0` is
the kernel's resource manager, which virtualises handles and isolates callers — it exists *because* the TPM
is meant to be usable by more than one unprivileged program. Granting a seat's user the manager is the use
it was designed for.
**The cost is a denial of service and not a disclosure, and it is stated because it is easy to miss.** TPM
objects are protected by their own auth values, so this lets nobody read anybody else's sealed data. What it
does allow is *failed* authorisation attempts — and the chip answers those by counting them and eventually
refusing everyone, which is the same lockout decision 4 relies on. On a machine whose disk is unlocked by
the TPM at boot (`systemd-cryptenroll`, Clevis), somebody sitting at it can trip that lockout and stop the
next boot from unlocking until it recovers. An administrator who does not accept that masks the file with
`ln -s /dev/null /etc/udev/rules.d/71-dodossh-tpm.rules`, which is the standard override, survives upgrades,
and puts the behaviour back to the passphrase-only path. The rule says so in its own comments, because the
person who needs to know is reading the file rather than this document.
**What was rejected, and why each would have failed the amendment above.** The Secret Service API — **What was rejected, and why each would have failed the amendment above.** The Secret Service API —
gnome-keyring or KWallet — is out of process and OS-managed and has *no per-use consent*: once the session gnome-keyring or KWallet — is out of process and OS-managed and has *no per-use consent*: once the session
@@ -398,8 +398,9 @@ is what `ClientPaths` reads, and the packaging must not answer that question a s
platform had none and that the passphrase was asked for at every unlock. It has one: platform had none and that the passphrase was asked for at every unlock. It has one:
[ADR 0007](0007-device-key-protection.md) decision 4 adds a TPM-sealed key released against a PIN the TPM [ADR 0007](0007-device-key-protection.md) decision 4 adds a TPM-sealed key released against a PIN the TPM
itself checks and counts failures against. It is weaker than Hello and Touch ID in a way that ADR names itself checks and counts failures against. It is weaker than Hello and Touch ID in a way that ADR names
precisely, and it is available on fewer machines than have a TPM, because the ordinary Fedora desktop user precisely. Reaching the chip needs no group membership, because this package ships the udev rule that gives
may not open `/dev/tpmrm0`. the seated user an ACL on `/dev/tpmrm0` — a decision that ADR overturned from its own first answer, and
whose cost it states.
**What this platform does not have is a signature on the package**, which is not decision 8's "unsigned for **What this platform does not have is a signature on the package**, which is not decision 8's "unsigned for
now": nothing on this platform now": nothing on this platform
+53 -26
View File
@@ -2849,11 +2849,12 @@ because nothing on this platform refuses an unsigned package, which is a distinc
is fine"; `build/linux/dodossh.spec` says what stands in for a signature, which is that the package comes is fine"; `build/linux/dodossh.spec` says what stands in for a signature, which is that the package comes
from the project's own forge over TLS and from nowhere else. from the project's own forge over TLS and from nowhere else.
Everything else is here, and two things are here that neither other phase has. **The update does not Everything else is here, and three things are here that neither other phase has. **The update does not
install itself**, so 19.7 is about watching a polkit prompt appear and saying no to it, where phases 16 and install itself**, so 19.8 is about watching a polkit prompt appear and saying no to it, where phases 16 and
18 watch an application replace itself. And **the device key's gate is a PIN rather than a gesture**, with 18 watch an application replace itself. **The device key's gate is a PIN rather than a gesture**, with the
the TPM's own lockout behind it — so 19.6 is the one check in this document that deliberately breaks TPM's own lockout behind it — so 19.7 deliberately breaks something and then tells you how to put it back.
something and then tells you how to put it back. And **this package changes a permission on the machine**: 19.4 and 19.5 are about the udev rule that gives
the seated user the TPM, checking both that it works and that masking it undoes it completely.
What this phase is for is the set nobody else can answer. **CI builds this package and installs nothing**, What this phase is for is the set nobody else can answer. **CI builds this package and installs nothing**,
and every check in `scripts/ci-linux-rpm.sh` is a check on what the package *says* — its dependencies, its and every check in `scripts/ci-linux-rpm.sh` is a check on what the package *says* — its dependencies, its
@@ -2909,32 +2910,58 @@ are the package's binary name, so they can only disagree if one was edited. A bl
48-pixel file missing and the 256 being scaled down; `dodossh-icon.ps1` draws four sizes and the spec 48-pixel file missing and the 256 being scaled down; `dodossh-icon.ps1` draws four sizes and the spec
installs four. installs four.
### 19.4 With no access to the TPM, no device key is offered at all ### 19.4 The udev rule gives *this* session the TPM, and gives nobody else it · **needs a real TPM**
On a stock Fedora install your account is not in the `tss` group, so this is the state most people are in. Installing the package should be the whole setup — no group, no administrator, no fresh login. Check it
Confirm it — `id -nG | tr ' ' ' from a graphical session on the machine itself:
' | grep -x tss` prints nothing — then open the security settings page.
**Pass:** registering a device key is **not** offered, and unlocking asks for the passphrase every time. ```bash
getfacl /dev/tpmrm0 | grep "^user:"
id -nG | tr ' ' '
' | grep -x tss # expect nothing: you are not in the group
```
**Failure means:** being offered it is `LinuxDeviceKeyStore.IsSupported` having answered from the presence **Pass:** `getfacl` shows a `user:<you>:rw-` entry that is **not** there for any other account, and you are
of the device node rather than from opening it. Accepting the offer would put a wrap on the server that not in `tss`. Now `ssh` into the same machine from elsewhere and run the same `getfacl`: the entry for you
nothing can ever open — and the failure would arrive at the next unlock rather than now. is still the *local* session's, and a shell that is not on the seat cannot open the device — `head -c1
/dev/tpmrm0` answers "Permission denied".
### 19.5 With access to the TPM, the PIN unlocks and the TPM is what checks it **Failure means:** no ACL at all is the rule not having been applied. `udevadm test --action=change
/sys/class/tpmrm/tpmrm0 | grep uaccess` says whether the tag is being set, and the load order matters —
71 has to fall between `60-tpm-udev.rules` and `73-seat-late.rules`, which is what turns the tag into an
ACL. If the file is present and untagged, check nothing in `/etc/udev/rules.d` is masking it. **An ACL
granted to a remote session would be the serious one**, and would mean the tag had been put on `tpm` rather
than `tpmrm` or the mode widened instead — neither of which this rule does.
`sudo usermod -aG tss $USER`, log out and back in, then register a device key from the security settings ### 19.5 Masking the rule puts the machine back the way it was
page and choose a PIN. Lock the vault and unlock it again.
```bash
sudo ln -s /dev/null /etc/udev/rules.d/71-dodossh-tpm.rules
sudo udevadm control --reload && sudo udevadm trigger --subsystem-match=tpmrm --action=change
```
Log out and back in, then open the security settings page.
**Pass:** registering a device key is **not** offered, and unlocking asks for the passphrase every time —
the same behaviour as a machine with no TPM. `udevadm test` reports the file as "masked by previous entry".
**Failure means:** still being offered it means `LinuxDeviceKeyStore.IsSupported` answered from the presence
of the device node rather than from opening it. Accepting that offer would put a wrap on the server that
nothing can ever open, and the failure would arrive at the next unlock rather than now.
**Then undo it** — `sudo rm /etc/udev/rules.d/71-dodossh-tpm.rules` and reload — before 19.6.
### 19.6 The PIN unlocks, and the TPM is what checks it
Register a device key from the security settings page and choose a PIN. Lock the vault and unlock it again.
**Pass:** registering asks for a PIN twice and shows no other prompt. Unlocking asks for it once and opens **Pass:** registering asks for a PIN twice and shows no other prompt. Unlocking asks for it once and opens
the vault. Cancelling that prompt lands on the passphrase screen, which works. the vault. Cancelling that prompt lands on the passphrase screen, which works.
**Failure means:** no offer after joining the group is `/dev/tpmrm0` still unreadable — check `ls -l` on it **Failure means:** a PIN prompt that opens the vault whatever you type would mean the unseal is not reaching
and that the login really was restarted, because group membership is fixed at session start. A PIN prompt the TPM at all, which is the one failure that would make this whole feature decorative.
that opens the vault whatever you type would mean the unseal is not reaching the TPM at all, which is the
one failure that would make this whole feature decorative.
### 19.6 A wrong PIN gets nothing, and the TPM stops answering ### 19.7 A wrong PIN gets nothing, and the TPM stops answering
With a device key registered, lock the vault and enter a wrong PIN. Do it several times. With a device key registered, lock the vault and enter a wrong PIN. Do it several times.
@@ -2949,7 +2976,7 @@ assertion against real firmware, where the lockout is real and recovery takes ho
**Then put it back:** `sudo tpm2_dictionarylockout --clear-lockout`, or wait out the recovery time. Note **Then put it back:** `sudo tpm2_dictionarylockout --clear-lockout`, or wait out the recovery time. Note
which, because a user who trips this will ask — and the honest answer is that it needs an administrator. which, because a user who trips this will ask — and the honest answer is that it needs an administrator.
### 19.7 An update is noticed, fetched, and installed by PackageKit and not by DodoSSH ### 19.8 An update is noticed, fetched, and installed by PackageKit and not by DodoSSH
With the nightly installed, wait for a newer one to be published — or press **CHECK NOW** on With the nightly installed, wait for a newer one to be published — or press **CHECK NOW** on
PREFERENCES → UPDATES once there is one. PREFERENCES → UPDATES once there is one.
@@ -2967,7 +2994,7 @@ covers the algorithm. A message naming PackageKit means it is not installed; it
minimal install legitimately lacks it. **The vault locking or shells closing when you decline is the minimal install legitimately lacks it. **The vault locking or shells closing when you decline is the
serious one** — it means `ApplyingEndsTheProcess` has been set true, which punishes somebody for saying no. serious one** — it means `ApplyingEndsTheProcess` has been set true, which punishes somebody for saying no.
### 19.8 A tampered package is refused before PackageKit ever sees it ### 19.9 A tampered package is refused before PackageKit ever sees it
Not reachable without editing the feed, so this is the one to reason about rather than perform unless you Not reachable without editing the feed, so this is the one to reason about rather than perform unless you
are changing that code: the manifest's `sha256` is checked after the download and the file is deleted if it are changing that code: the manifest's `sha256` is checked after the download and the file is deleted if it
@@ -2978,7 +3005,7 @@ does not match.
**Failure means:** a leftover `.rpm` there after a failed update is a file somebody could reasonably install **Failure means:** a leftover `.rpm` there after a failed update is a file somebody could reasonably install
by hand, named as though the application had endorsed it. by hand, named as though the application had endorsed it.
### 19.9 The nightly installs beside the release rather than over it ### 19.10 The nightly installs beside the release rather than over it
With the release package installed, install the nightly one too. With the release package installed, install the nightly one too.
@@ -2991,7 +3018,7 @@ nightly that opens the release's vault already unlocked is the profile separatio
`DodoChannel=nightly` reaching `ClientPaths`, not from the packaging, so check `DodoChannel=nightly` reaching `ClientPaths`, not from the packaging, so check
`~/.local/share/dodossh.nightly` exists rather than looking at the spec. `~/.local/share/dodossh.nightly` exists rather than looking at the spec.
### 19.10 Nothing in the installed tree is writable by anyone but root ### 19.11 Nothing in the installed tree is writable by anyone but root
```bash ```bash
find /opt/DodoSSH ! -user root -o -perm -o+w find /opt/DodoSSH ! -user root -o -perm -o+w
@@ -3005,7 +3032,7 @@ model walked around rather than attacked. `ci-linux-rpm.sh` refuses to produce s
sets every mode absolutely, so a hit here means one of those two was changed — it cannot arrive by accident sets every mode absolutely, so a hit here means one of those two was changed — it cannot arrive by accident
from a publish directory. from a publish directory.
### 19.11 Removing it leaves the vault, and leaves the other channel alone ### 19.12 Removing it leaves the vault, and leaves the other channel alone
Register a device, sync something, then `sudo dnf remove dodossh`. Register a device, sync something, then `sudo dnf remove dodossh`.
+3 -1
View File
@@ -203,6 +203,7 @@ rpmbuild -bb "$repo_root/build/linux/dodossh.spec" \
--define "dodo_payload $payload" \ --define "dodo_payload $payload" \
--define "dodo_bundled $bundled" \ --define "dodo_bundled $bundled" \
--define "dodo_icons $repo_root/src/DodoSSH.Client.App/Assets" \ --define "dodo_icons $repo_root/src/DodoSSH.Client.App/Assets" \
--define "dodo_udev $repo_root/build/linux/71-dodossh-tpm.rules" \
--define "dodo_licence $repo_root/LICENSE" --define "dodo_licence $repo_root/LICENSE"
# rpmbuild writes into <_rpmdir>/<arch>/, and every caller wants the file rather than the tree. # rpmbuild writes into <_rpmdir>/<arch>/, and every caller wants the file rather than the tree.
@@ -275,7 +276,8 @@ for expected in \
"/opt/$dir/DodoSSH" \ "/opt/$dir/DodoSSH" \
"/usr/bin/$bin" \ "/usr/bin/$bin" \
"/usr/share/applications/$bin.desktop" \ "/usr/share/applications/$bin.desktop" \
"/usr/share/icons/hicolor/256x256/apps/$bin.png" "/usr/share/icons/hicolor/256x256/apps/$bin.png" \
"/usr/lib/udev/rules.d/71-dodossh-tpm.rules"
do do
if ! printf '%s\n' "$files" | grep -qxF "$expected"; then if ! printf '%s\n' "$files" | grep -qxF "$expected"; then
echo "The package was built and $expected is not in it." >&2 echo "The package was built and $expected is not in it." >&2
@@ -57,11 +57,17 @@ public enum DevicePinPurpose
/// parity. /// parity.
/// </para> /// </para>
/// <para> /// <para>
/// <b>Availability is narrower than "the machine has a TPM".</b> On Fedora <c>/dev/tpmrm0</c> is owned by /// <b>Availability still depends on reaching the device, and the package arranges that rather than asking
/// <c>root:tss</c> and mode 0660, and desktop users are not in <c>tss</c> — so the ordinary case is a /// the user to.</b> Fedora leaves <c>/dev/tpmrm0</c> as <c>root:tss</c> mode 0660, so without help this
/// machine with a working TPM this process may not open. That reports unavailable and the passphrase path /// process could not open it on an ordinary desktop account. The RPM installs
/// continues, exactly as a machine with no TPM does. Widening the device's permissions is deliberately not /// <c>build/linux/71-dodossh-tpm.rules</c>, which tags the device <c>uaccess</c> — logind then grants an
/// something this application's package does; see ADR 0007. /// ACL to whoever is logged in at the machine, and takes it away when they log out. Not over SSH and not
/// to a user on an inactive VT.
/// <para>
/// So a build run from a checkout, or one on a machine where an administrator has masked that rule, still
/// finds the device unreadable and reports unavailable — which keeps the passphrase path exactly as it was.
/// The rule carries its own reasoning, including the lockout denial-of-service it accepts; see ADR 0007
/// decision 4.
/// </para> /// </para>
/// </remarks> /// </remarks>
[SupportedOSPlatform("linux")] [SupportedOSPlatform("linux")]
@@ -30,6 +30,13 @@ namespace DodoSSH.Client.App.Tests;
/// DODOSSH_TPM_TCP=127.0.0.1 dotnet test tests/DodoSSH.Client.App.Tests /// DODOSSH_TPM_TCP=127.0.0.1 dotnet test tests/DodoSSH.Client.App.Tests
/// </code> /// </code>
/// <para> /// <para>
/// ◆ <b>swtpm serves exactly one client, and anything else holding the port makes this suite fail in a
/// way that reads like a bug in the store.</b> A second connection is accepted and then closed
/// immediately, so five tests fail at once with <c>IOException: The TPM closed the connection</c> and
/// nothing points at the cause. It was a shell readiness probe — <c>exec 3&lt;&gt;/dev/tcp/…</c> — sitting
/// on the socket. Wait for the container by looking at the container, not by opening the port.
/// </para>
/// <para>
/// <b>What this cannot cover is the transport.</b> Production opens <c>/dev/tpmrm0</c> through TSS.Net's /// <b>What this cannot cover is the transport.</b> Production opens <c>/dev/tpmrm0</c> through TSS.Net's
/// <c>LinuxTpmDevice</c> and these tests open a socket, so everything above the device is proved here and /// <c>LinuxTpmDevice</c> and these tests open a socket, so everything above the device is proved here and
/// the device itself is proved by Phase 19 of <c>docs/manual-checks.md</c> on a machine with a real chip. /// the device itself is proved by Phase 19 of <c>docs/manual-checks.md</c> on a machine with a real chip.