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.
194 lines
10 KiB
XML
194 lines
10 KiB
XML
<Project Sdk="Microsoft.NET.Sdk">
|
|
|
|
<PropertyGroup>
|
|
<OutputType>WinExe</OutputType>
|
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
|
|
|
<!--
|
|
The project is named for its layer and the executable is named for the product. Users see the
|
|
executable — in Task Manager, in a shortcut's target, in the SmartScreen dialog an unsigned
|
|
installer raises — and DodoSSH.Client.App.exe reads as an implementation detail leaking out.
|
|
|
|
Safe to change now and not later: it becomes the main executable recorded in every Velopack
|
|
package, and changing it after a release means the updater looks for an executable that is no
|
|
longer there. Nothing else depends on the name — no avares://DodoSSH.Client.App URI exists, the
|
|
window icon is the assembly-relative /Assets/dodossh.ico, and WindowsDeviceKeyStore names its
|
|
CNG key with a literal.
|
|
-->
|
|
<AssemblyName>DodoSSH</AssemblyName>
|
|
|
|
<!--
|
|
No RuntimeIdentifiers here, and that is the considered answer rather than an omission.
|
|
|
|
Declaring win-x64 is the obvious way to let a self-contained publish restore, and it was tried:
|
|
it works, and it also writes a net10.0/win-x64 target into the lock file of every project this
|
|
one references transitively — which includes DodoSSH.Contracts and DodoSSH.Crypto, which the
|
|
server builds too. The API's Dockerfile then restores those projects with no RID and locked
|
|
mode, and fails NU1004 on a lock file that has grown a runtime identifier the server knows
|
|
nothing about. The desktop head's packaging would have broken the server's image build.
|
|
|
|
So the RID stays out of the committed state entirely, and the publish that needs one asks for
|
|
it unlocked — see scripts/release-windows.ps1 and the CI step that proves the same thing. The
|
|
locked restore that gates every ordinary build is untouched, which is the property worth
|
|
keeping. See docs/platform-flags.md.
|
|
-->
|
|
|
|
<!--
|
|
The icon on the executable itself — what Explorer, the Start menu and a pinned taskbar button
|
|
draw, all of which read it from the PE resource and never start the process. Window.Icon in
|
|
MainWindow.axaml is a separate thing that only exists once the application is running; both are
|
|
needed, and both point at this file.
|
|
-->
|
|
<ApplicationIcon>Assets/dodossh.ico</ApplicationIcon>
|
|
|
|
<!--
|
|
============ WHICH CHANNEL THIS BUILD BELONGS TO ============
|
|
|
|
The same split the Android head has, for the same reason and with one difference worth stating up
|
|
front: Android gets separation for free, because the platform refuses an update signed by a
|
|
different key, so its two channels cannot replace one another whatever anybody does. Nothing
|
|
refuses anything here. Velopack applies what its feed serves, so the separation has to be built:
|
|
two pack ids, two Velopack channels, two feeds, and a profile directory each.
|
|
|
|
release — DodoSSH.Desktop, Velopack channel win, read from the newest non-prerelease release.
|
|
Cut from a v* tag by scripts/release-windows.ps1, by a person. See ADR 0013 rule 3.
|
|
nightly — DodoSSH.Desktop.Nightly, Velopack channel win-nightly, read from a prerelease release
|
|
CI replaces on every push to main.
|
|
|
|
Default release, so an unqualified `dotnet build` is the real application and the nightly is the
|
|
one you have to ask for.
|
|
|
|
What this property does *not* decide is the pack id or the title. Those are arguments to vpk and
|
|
live where the packaging happens — in ci.yml for the nightly and in the release script for the
|
|
release. Putting them here would suggest the build knows which package it will end up inside,
|
|
and it does not.
|
|
-->
|
|
<DodoChannel Condition="'$(DodoChannel)' == ''">release</DodoChannel>
|
|
|
|
<!--
|
|
For the macOS keychain interop in Platform/, and for nothing else.
|
|
|
|
Set on this project rather than in Directory.Build.props deliberately. The frameworks that hold a
|
|
Secure Enclave key take CFDictionaries of raw pointers, so building one means pinning arrays and
|
|
taking their addresses — see MacDeviceKeyStore. Every other project here is managed code with no
|
|
business doing that, and a solution-wide flag would quietly permit it everywhere, including in the
|
|
crypto project where a stray pointer is the last thing anybody wants to have been allowed.
|
|
|
|
The alternative — GCHandle.Alloc with GCHandleType.Pinned — needs no flag and was considered. It
|
|
would replace each `fixed` with an allocate/free pair that has to be balanced by hand across the
|
|
early returns those methods are full of, which trades a compiler-checked scope for a manual one.
|
|
-->
|
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
|
|
<!--
|
|
False here, unlike every server project. The root Directory.Build.props sets it true because
|
|
the API is container-hosted, UTC-only and has no business formatting anything for a human.
|
|
This process does: it shows timestamps and host names to a person who expects their own
|
|
locale's conventions. Invariant globalization would quietly render every date in a format
|
|
that is correct for nobody.
|
|
-->
|
|
<InvariantGlobalization>false</InvariantGlobalization>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
Named rather than globbed as Assets/**, because the folder also holds the script that draws the
|
|
icon and a build has no reason to carry a copy of it around inside the binary.
|
|
-->
|
|
<AvaloniaResource Include="Assets/dodossh.ico" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
Which channel this build belongs to, carried in the assembly the same way the Android head carries
|
|
it. Metadata rather than a compile-time constant for the reason stated there: the updater needs the
|
|
string rather than a branch, and a value baked into the assembly is one a crash report can be asked
|
|
for. See DesktopChannel, which is the only thing that reads it.
|
|
-->
|
|
<AssemblyMetadata Include="DodoChannel" Value="$(DodoChannel)" />
|
|
</ItemGroup>
|
|
|
|
<!--
|
|
Two native symbol files, and they are the reason a self-contained publish weighed 227 MB.
|
|
|
|
Measured on a win-x64 publish: libSkiaSharp.pdb is 80.1 MB and libHarfBuzzSharp.pdb is 19.9 MB, so
|
|
the two of them are 100 MB of debug symbols for third-party native code nobody here will ever step
|
|
through. Dropping them takes the publish to about 127 MB, which is most of what a first install
|
|
costs somebody on a slow connection.
|
|
|
|
Our own symbols stay, and the distinction is the point rather than a compromise. All fifteen managed
|
|
PDBs together are 0.93 MB, and with them present an Exception.ToString() carries file names and line
|
|
numbers — which for a self-hosted product is the whole diagnostic channel, because the way a fault
|
|
gets reported is a user pasting a stack into an issue.
|
|
|
|
Named one by one instead of matched by a pattern. A rule like "drop every .pdb whose assembly is
|
|
native" would be shorter and would silently start dropping ours the day a managed library ships a
|
|
file this heuristic misreads, and a build that quietly stops carrying line numbers is not a thing
|
|
anybody notices until they need them.
|
|
-->
|
|
<Target Name="DropNativeSymbolsFromPublish" AfterTargets="ComputeResolvedFilesToPublishList">
|
|
<ItemGroup>
|
|
<ResolvedFileToPublish
|
|
Remove="@(ResolvedFileToPublish)"
|
|
Condition="'%(Filename)%(Extension)' == 'libSkiaSharp.pdb'
|
|
or '%(Filename)%(Extension)' == 'libHarfBuzzSharp.pdb'" />
|
|
</ItemGroup>
|
|
</Target>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="Avalonia" />
|
|
<PackageReference Include="Avalonia.Desktop" />
|
|
<PackageReference Include="Avalonia.Themes.Fluent" />
|
|
<PackageReference Include="Avalonia.Fonts.Inter" />
|
|
<PackageReference Include="Avalonia.Controls.WebView" />
|
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
|
<!--
|
|
Here and in no other project. The Android head must never reference it — that head's
|
|
distribution is settled by ADR 0011 and has no updater — and DodoSSH.Client.Shell is shared
|
|
between the two heads, so the one file that names Velopack lives in Platform/ beside
|
|
WindowsDeviceKeyStore, which is the same shape of thing: a Windows-only implementation of an
|
|
interface declared in DodoSSH.Client.Session.
|
|
-->
|
|
<PackageReference Include="Velopack" />
|
|
|
|
<!--
|
|
Here for the same reason and with the same boundary as Velopack above: LinuxDeviceKeyStore is a
|
|
Linux-only implementation of an interface declared in DodoSSH.Client.Session, and it lives in
|
|
Platform/ beside the Windows and macOS stores. The Android head has its own keystore and must
|
|
never see this one.
|
|
-->
|
|
<PackageReference Include="Microsoft.TSS" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
The shell's view models, the terminal renderer's files and the palette, all shared with the Android
|
|
head. This project is now the desktop *views* and the desktop platform integration, and nothing else.
|
|
-->
|
|
<ProjectReference Include="../DodoSSH.Client.Shell/DodoSSH.Client.Shell.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Import/DodoSSH.Client.Import.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Session/DodoSSH.Client.Session.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Ssh/DodoSSH.Client.Ssh.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.ObjectStore/DodoSSH.Client.ObjectStore.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Terminal/DodoSSH.Client.Terminal.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Transfer/DodoSSH.Client.Transfer.csproj" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
The view models are plain CommunityToolkit.Mvvm objects and need no Avalonia to run, so the shell's
|
|
state machine is testable as ordinary code. That is the whole reason the sign-in step is a delegate.
|
|
-->
|
|
<InternalsVisibleTo Include="DodoSSH.Client.App.Tests" />
|
|
|
|
<!--
|
|
The layout harness needs the views and the application class themselves, not just the view models:
|
|
its whole job is to lay out the real XAML and measure it.
|
|
-->
|
|
<InternalsVisibleTo Include="DodoSSH.Client.App.Layout.Tests" />
|
|
</ItemGroup>
|
|
|
|
</Project>
|
|
|