Public Access
Two things the RPM shipped without, and the reason they arrive together is
that both are the same question — what may this platform ask of the machine,
and what must it not ask of the user.
◆ UPDATES: THIS CLIENT CHECKS AND FETCHES, AND PACKAGEKIT INSTALLS.
ADR 0013 rule 11 said there was no updater at all, and that went too far. It
conflated noticing that a newer build exists with installing it: the second is
what the trust argument is about, and the first is not — a client that checks
a fixed address and tells you what it found has taken no capability from
anybody. Written as a flat refusal, the rule bought its safety by paying a cost
that was avoidable, and the ADR now says so in its own words.
So LinuxUpdateChannel reads the rolling nightly release, compares versions the
way rpm compares them, downloads the package, verifies its digest and hands the
file to PackageKit over gdbus. Polkit asks; the user may say no; nothing is
torn down before the answer, because ApplyingEndsTheProcess is false and the
shell already knows what that means — it is the phone's shape, and declining an
update must not cost somebody their open shells.
Three details that are load-bearing rather than incidental:
- The comparison is rpmvercmp and not SemVer, because rpm is what will accept
or refuse the install. Same argument ADR 0014 makes for versionCode. The
installed side is read out of the rpm database rather than derived from the
assembly, so the SemVer-to-rpm translation exists in exactly one place. All
28 expected values in RpmVersionTests came out of rpm itself rather than out
of a reading of the algorithm.
- The digest is checked and is not a signature. Velopack verifies its own
downloads and Android checks a signature; an RPM over HTTPS has neither. The
manifest names a SHA-256 and a mismatch deletes the file rather than leaving
something installable-by-hand in the profile directory.
- ONLY_TRUSTED is cleared, which is what routes the request to polkit's
package-install-untrusted action. The package is unsigned, so requiring
trust would fail every update at the last step.
◆ THE DEVICE KEY: SEALED IN THE TPM, BEHIND A PIN THE TPM CHECKS.
ADR 0007's amendment is the bar: a gate inside the process is not a gate. The
key is sealed into a TPM 2.0 keyedhash object whose userAuth comes from a PIN,
so unsealing is a command the chip refuses; the blob is encrypted to the storage
hierarchy and is useless on any other machine; nothing here ever compares a PIN.
The sealed object deliberately carries no NoDA attribute, and that one omission
is the whole reason four digits is enough: every wrong answer advances the TPM's
dictionary-attack counter and the chip stops answering entirely once it trips.
Measured at four wrong tries against a software TPM, after which the correct PIN
gets nothing either. It is asserted by a test rather than left to a reading of
the template, because removing the attribute leaves every other assertion
passing.
This is weaker than Hello and Touch ID and the difference is invisible, so it is
written down in three places rather than smoothed over: those raise their prompt
from inside the platform and this one is a window we draw. The prompt is
deliberately plain for that reason — teaching people that a convincing DodoSSH
PIN box is a system dialog is the one thing this design must not do.
Rejected, each for failing the amendment: the Secret Service API, which has no
per-use consent and is DPAPI's shape exactly; fprintd, which answers a question
and holds nothing, so skipping the call skips the gate; and a polkit-gated root
helper, which would work and puts a privileged component in a package that
installs nothing setuid.
Availability is narrower than "has a TPM": on Fedora /dev/tpmrm0 is root:tss and
desktop users are not in tss, so the ordinary case reports unavailable and keeps
asking for the passphrase. Shipping a udev rule to widen that is refused — it is
the administrator's decision, not an application package's.
◆ WHAT IS ACTUALLY VERIFIED, SINCE THAT IS THE PART THAT USUALLY ISN'T.
This is the only device key store in the repository with real coverage, and it
is only possible because the PIN is a delegate and the TPM is a device: seal,
unseal, wrong PIN, lockout, decline and a foreign blob all run unattended
against a software TPM. Windows and macOS cannot be tested at all — their
prompts block a suite forever, which WindowsDeviceKeyStoreTests records.
TSS.Net's own TcpTpmDevice cannot reach swtpm: it opens the platform socket too
and exchanges a handshake only the Microsoft reference simulator answers, so
Connect() blocks with no error and no timeout. SoftwareTpm speaks the data
channel and nothing else.
The suite also lied once and was fixed: the lockout test trips shared state, and
without a DictionaryAttackLockReset in Dispose every later test got null from a
correct PIN — six of them assert null and passed for entirely the wrong reason.
End to end on a real Fedora 44: a genuine linux-x64 publish packages, installs,
and every DT_NEEDED in the installed tree resolves except liblttng-ust, which is
the one this spec deliberately excludes. What no runner covers is /dev/tpmrm0,
PackageKit and polkit, and Phase 19 grew five checks for exactly those.
Also fixed: the magic-bytes literal picked up raw control characters, which made
LinuxDeviceKeyStore.cs binary to git — no diffs and no line-ending normalisation.
262 lines
16 KiB
XML
262 lines
16 KiB
XML
<Project>
|
|
<PropertyGroup>
|
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
|
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
|
</PropertyGroup>
|
|
<!--
|
|
Versions are pinned here for the whole solution. Packages are added per milestone
|
|
rather than all at once, so that every entry is one we have actually verified and
|
|
restored. See docs/adr/ for the choices behind the notable ones.
|
|
-->
|
|
<ItemGroup Label="ASP.NET Core">
|
|
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Endpoints">
|
|
<!--
|
|
FastEndpoints drags FluentValidation, JobQueues and Messaging in behind it. None of the
|
|
three are used: validation lives in the feature services and is banned from moving into a
|
|
Validator<T> (see BannedSymbols.txt and ADR 0008), and there is no message bus. They are
|
|
left as plain transitives rather than declared here, because declaring a transitive under
|
|
central transitive pinning is a standing promise to keep its version current, and these are
|
|
not ours to steer. Declare one only to force a version forward for an advisory, as the
|
|
group below does.
|
|
-->
|
|
<PackageVersion Include="FastEndpoints" Version="8.2.0" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Pinned transitive dependencies">
|
|
<!--
|
|
Microsoft.AspNetCore.OpenApi 10.0.10 resolves Microsoft.OpenApi 2.0.0, which is
|
|
covered by GHSA-v5pm-xwqc-g5wc (high: circular schema references can terminate
|
|
OpenAPI parsing; vulnerable <= 2.7.4, patched in 2.7.5). Pinned forward within the
|
|
2.x major that ASP.NET Core 10 targets. Revisit when the ASP.NET Core package
|
|
itself moves off 2.0.0.
|
|
-->
|
|
<PackageVersion Include="Microsoft.OpenApi" Version="2.11.0" />
|
|
<!--
|
|
Microsoft.EntityFrameworkCore.Sqlite 10.0.10 resolves SQLitePCLRaw 2.1.11, whose bundled
|
|
SQLite build is covered by GHSA-2m69-gcr7-jv3q (high). 2.1.12 is the fix and is a patch bump
|
|
inside the minor EF asks for, so nothing needs to move. Pinned as a family: the bundle, the
|
|
core, the provider and the native library ship in lockstep and a mixed set is a loader error
|
|
at runtime rather than a build failure.
|
|
|
|
SQLitePCLRaw 3.x exists and is deliberately not used here. EF Core 10 is built against 2.1.x,
|
|
and 3.0 is also where bundle_e_sqlcipher was deprecated — which is one of the reasons the
|
|
local cache does not use SQLCipher at all. See DodoSSH.Client.Storage.
|
|
-->
|
|
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.12" />
|
|
<PackageVersion Include="SQLitePCLRaw.core" Version="2.1.12" />
|
|
<PackageVersion Include="SQLitePCLRaw.lib.e_sqlite3" Version="2.1.12" />
|
|
<PackageVersion Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.12" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Persistence">
|
|
<!--
|
|
EF Core pinned explicitly. The Npgsql provider asks only for 10.0.4 while
|
|
Microsoft.EntityFrameworkCore.Design pulls 10.0.10, and because Design is
|
|
PrivateAssets=all that higher version does not flow to referencing projects — which
|
|
produces a CS1705 in any test project that references Infrastructure. Pinning here lifts
|
|
every project to one version via central transitive pinning.
|
|
-->
|
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
|
|
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
|
|
<!--
|
|
Verified compatible with EF 10 before adopting; the plan flagged this package as
|
|
historically lagging EF majors. Fallback if it ever blocks an upgrade is explicit
|
|
HasColumnName in every IEntityTypeConfiguration: more code, zero risk.
|
|
-->
|
|
<PackageVersion Include="EFCore.NamingConventions" Version="10.0.1" />
|
|
<!--
|
|
The client's local cache. Plain SQLite, deliberately not SQLCipher: the rows are already
|
|
ciphertext, so an encrypted database file would add a native dependency and a licence
|
|
obligation to protect bytes that are protected already. SQLitePCLRaw's own
|
|
bundle_e_sqlcipher is deprecated as of 3.0 besides. See DodoSSH.Client.Storage.
|
|
-->
|
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Cryptography">
|
|
<!--
|
|
NSec wraps libsodium. Chosen over the BCL because .NET has no X25519 or Ed25519, and
|
|
because ChaCha20Poly1305.IsSupported is false on macOS, which rules out the in-box
|
|
AEAD for a cross-platform client. NSec also holds key material in libsodium's
|
|
guarded, non-swappable memory, which a byte[] cannot do. See docs/crypto.md.
|
|
|
|
26.4.0 targets net9.0; net10.0 consumes it by forward compatibility. Native binaries
|
|
arrive via the libsodium package, pinned here because central transitive pinning
|
|
requires it to be declared.
|
|
-->
|
|
<PackageVersion Include="NSec.Cryptography" Version="26.4.0" />
|
|
<PackageVersion Include="libsodium" Version="1.0.22" />
|
|
<!--
|
|
Managed differential oracle for the crypto test suite only. Also what SSH.NET pulls in,
|
|
so central transitive pinning makes this pin its floor too: 2.7.0 is what SSH.NET
|
|
2026.0.0 asks for, and pinning below that is a downgrade error rather than a preference.
|
|
-->
|
|
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.7.0" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Desktop client">
|
|
<!--
|
|
SSH.NET already covers PTY shells, all three auth methods, ed25519/RSA/ECDSA, encrypted
|
|
keys including PuTTY .ppk, SFTP, and local/remote/dynamic forwarding. The gaps are
|
|
agent forwarding (needs an upstream change; de-scoped from v1) and being handed a
|
|
pre-connected Stream — it performs its own socket connect, which is why the relay and
|
|
ProxyJump both go through a loopback TCP bridge. See docs/adr/.
|
|
|
|
2026.0.0 is the fix for GHSA-q939-rpr3-3284, a path traversal in ScpClient.Download's
|
|
recursive mode that trusts server-supplied names (everything up to and including
|
|
2025.1.0 is affected). Nothing here uses ScpClient — transfers go through SftpClient —
|
|
so this was an upgrade on principle rather than an exposure, and the release notes
|
|
list no breaking changes over 2025.1.0.
|
|
-->
|
|
<PackageVersion Include="SSH.NET" Version="2026.0.0" />
|
|
<!--
|
|
The S3 client, for buckets as a remote in the file browser. First-party, Apache-2.0, and
|
|
managed only — no native assets — which is the bar this file sets for anything that gets
|
|
pinned. Taken rather than hand-rolled because the alternative here is implementing SigV4
|
|
request signing, and unlike the openssh-key-v1 container (which had no library at all) a
|
|
maintained implementation of this exists and is the one every S3-compatible service tests
|
|
against.
|
|
|
|
AWSSDK.Core is declared and pinned forward. What AWSSDK.S3 4.0.101.6 resolves on its own is
|
|
4.0.1, which is covered by GHSA-9cvc-h2w8-phrp — low severity, and this repository builds
|
|
with NuGet audit as errors, so "low" is not a reason to carry it. 4.0.100.9 is past it and
|
|
inside the same major. Same treatment as the OpenApi and SQLitePCLRaw entries above, and the
|
|
same standing obligation: this is now ours to keep current.
|
|
-->
|
|
<PackageVersion Include="AWSSDK.S3" Version="4.0.101.6" />
|
|
<PackageVersion Include="AWSSDK.Core" Version="4.0.100.9" />
|
|
<!--
|
|
Avalonia 12.1.0, with the WebView control on 12.0.1 — the latest it has shipped. Its
|
|
dependency is Avalonia >= 12.0.0 with no upper bound and it targets net10.0, so the skew
|
|
is fine. Checked rather than assumed, because a control package lagging the core version
|
|
is exactly where a silent runtime mismatch would hide.
|
|
-->
|
|
<PackageVersion Include="Avalonia" Version="12.1.1" />
|
|
<PackageVersion Include="Avalonia.Desktop" Version="12.1.1" />
|
|
<!--
|
|
The Android head. Same core version as the desktop one, which is not a courtesy: the two heads
|
|
share every view model, so a version skew between them would be a skew inside one object graph.
|
|
-->
|
|
<PackageVersion Include="Avalonia.Android" Version="12.1.1" />
|
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.1" />
|
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.1" />
|
|
<PackageVersion Include="Avalonia.Controls.WebView" Version="12.0.1" />
|
|
<!--
|
|
Lets a test lay out real XAML and measure it, which is the only way this repository can catch a
|
|
control clipped off the bottom of a column — the defect this window has already shipped once. Pinned
|
|
to the core version exactly rather than allowed to drift: the whole value of the harness is that the
|
|
numbers it measures are the numbers the application renders.
|
|
-->
|
|
<PackageVersion Include="Avalonia.Headless" Version="12.1.1" />
|
|
<!--
|
|
Source-generated MVVM, so there is no reflection and trimming stays viable. ReactiveUI's one
|
|
real advantage is observable composition over streams, and the place that would help — the
|
|
terminal data plane — is Pipelines and channel code rather than view models.
|
|
-->
|
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
|
<!--
|
|
Packaging and self-update for the Windows desktop head. MIT, and on net10.0 it declares no
|
|
dependencies at all — the whole package is one managed assembly, so it restores and compiles
|
|
on the Linux runner that builds the solution even though the thing it produces only runs on
|
|
Windows. That mattered enough to check: a per-OS conditional PackageReference is not available
|
|
here, because it would make packages.lock.json depend on the operating system and CI's locked
|
|
restore would then fail on whichever platform did not write it.
|
|
|
|
MSIX would have been the platform-native choice and is ruled out rather than deprioritised: a
|
|
packaged app runs WebView2 in an AppContainer where loopback is blocked, and the terminal data
|
|
plane is a loopback WebSocket. See docs/platform-flags.md.
|
|
|
|
The update feed this is pointed at is the project's own forge and never a DodoSSH deployment.
|
|
That is ADR 0011 rule 2, and it is the reason the repository URL in VelopackUpdateSource is a
|
|
constant rather than a setting: an operator who could answer the update check could pin a
|
|
chosen user to a known-vulnerable build. See docs/adr/0013-desktop-distribution-and-updates.md.
|
|
-->
|
|
<PackageVersion Include="Velopack" Version="1.2.0" />
|
|
|
|
<!--
|
|
Microsoft's own TPM 2.0 stack, TSS.MSR, and the only way LinuxDeviceKeyStore reaches the chip.
|
|
|
|
Windows needs nothing here: CNG is in the framework and the Platform Crypto Provider is the TPM
|
|
behind it, and macOS reaches the Secure Enclave through the Security framework that
|
|
MacSymbols.cs binds by hand. Linux has neither — the platform's own stack is tpm2-tss, a C
|
|
library with a large surface — so this is the one head that needs a package to talk to its own
|
|
hardware.
|
|
|
|
Chosen over P/Invoking libtss2-esys for two reasons that both hold at the same time. It is
|
|
managed all the way down, so there is no native dependency to add to the RPM and nothing to
|
|
resolve at load time; and it works against a software TPM over a socket exactly as it does
|
|
against /dev/tpmrm0, which is what makes LinuxDeviceKeyStoreTests able to run the whole seal,
|
|
unseal and lockout path unattended. Neither of the other two device key stores can be tested at
|
|
all — see WindowsDeviceKeyStoreTests for the ten minutes that took to find out.
|
|
|
|
It carries a runtimes/linux-x64 asset, like SkiaSharp and libsodium, so the RID-specific publish
|
|
picks it up and the RID-free locked restore does not. See the RuntimeIdentifiers note in
|
|
DodoSSH.Client.App.csproj for why that distinction is load-bearing here.
|
|
-->
|
|
<PackageVersion Include="Microsoft.TSS" Version="2.1.1" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Versioning">
|
|
<!--
|
|
One version for the whole repository, derived from the nearest v* git tag. The tag was already
|
|
the version of record — ci.yml's "work out the tags" step parses refs/tags/v* for the docker
|
|
image — and nothing set an assembly version at all, so every binary reported the SDK's default
|
|
1.0.0 and the API served that as its ServerVersion. Deriving from the tag makes those one
|
|
number instead of two that can disagree.
|
|
|
|
MinVer's one real failure mode is that it answers plausibly rather than failing: a shallow
|
|
clone with no tags yields 0.0.0-alpha.0.N. Here that is not cosmetic — Velopack compares the
|
|
version baked into a package against the one it is running, so a wrong answer is a client that
|
|
never updates. Hence two guards in ci.yml: fetch-depth 0 on every checkout, and a step on tag
|
|
builds that fails if the computed version and the tag disagree.
|
|
-->
|
|
<PackageVersion Include="MinVer" Version="7.0.0" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Analyzers">
|
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
|
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
|
|
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.137" />
|
|
</ItemGroup>
|
|
<ItemGroup Label="Testing">
|
|
<!--
|
|
xunit.v3 runs on Microsoft.Testing.Platform, not VSTest. Microsoft.NET.Test.Sdk and
|
|
coverlet.collector are VSTest components: referencing them alongside MTP raises
|
|
MTP0001 and their collector never runs, so neither is referenced.
|
|
|
|
No coverage collector yet. Microsoft.Testing.Extensions.CodeCoverage 18.9.0 pulls
|
|
Microsoft.Testing.Platform.MSBuild 1.9.1, which is built against MTP 1.x and throws
|
|
TypeLoadException on IDataConsumer against the MTP 2.3.x that xunit.v3 3.2.2 brings.
|
|
Coverage gates are an M3 concern (90% on Domain and Authorization); pick a version
|
|
aligned with MTP 2.x then rather than carrying a broken dependency until it matters.
|
|
-->
|
|
<PackageVersion Include="xunit.v3" Version="3.2.2" />
|
|
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
|
<PackageVersion Include="NSubstitute" Version="6.0.0" />
|
|
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.13.0" />
|
|
<!-- Generic container, for the OpenSSH server the SSH suite talks to. -->
|
|
<PackageVersion Include="Testcontainers" Version="4.13.0" />
|
|
<PackageVersion Include="Respawn" Version="7.0.0" />
|
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
|
|
<!--
|
|
Stands in for the identity provider so integration tests exercise the real JwtBearer
|
|
pipeline. A TestAuthHandler that bypasses it would hide exactly the claim-mapping
|
|
mistakes that cause real authorization holes.
|
|
-->
|
|
<PackageVersion Include="WireMock.Net" Version="2.13.0" />
|
|
<!--
|
|
A JavaScript interpreter, in managed code, so that terminal.js can be tested as the file that
|
|
ships rather than as a transcription of it into C#. Test-only and referenced by exactly one
|
|
project; nothing in src depends on it.
|
|
|
|
Chosen over shelling out to node, which is the obvious alternative and needs node present
|
|
wherever the suite runs — CI installs one already, but a test that only runs when a separate
|
|
command is remembered is a test that stops being run. Chosen over a headless browser for the
|
|
same reason several times over.
|
|
|
|
What it does not buy: Jint is not Chromium, so this proves the page's own logic and nothing
|
|
about how WebView2 or Android's WebView behave. That line is drawn in RendererPage's remark and
|
|
picked up by docs/manual-checks.md 1.10 and 11.12a.
|
|
-->
|
|
<PackageVersion Include="Jint" Version="4.16.0" />
|
|
</ItemGroup>
|
|
</Project> |