jaap-jan e65d738912 Add the client key hierarchy: bundle, master key, vault and item keys
Everything crypto.md section 3 describes below the identity key, which is
what the desktop client needs before it can enroll or store anything.

DshAad gives every descriptor in the specification a named constructor. The
AAD binding is the most valuable structural property in the design -- it is
what stops a server holding every ciphertext from pasting one row's bytes
onto another, rolling a row back to a superseded generation, or replaying a
revoked grant -- and all of it depends on callers getting purpose, resource
type and ids right at every single call site. Hand-constructing descriptors
makes that a matter of care; picking a method name makes it a matter of
spelling.

UserSecretBundle holds private keys in libsodium's guarded, mlocked
allocations rather than a byte[], so they are not paged out and do not land
in a core dump. They are created exportable, deliberately: re-wrapping the
same bundle for a passphrase change or a new device needs to re-encode it,
and the alternative -- a long-lived managed array so the keys need not be
exportable -- keeps the identical secret in strictly worse memory. Every
export is into a buffer zeroed before the method returns.

Two spec changes, both found by implementing it, which is the argument for
writing code before calling a spec frozen:

- MK is 64 bytes, not 32. Skipping HKDF-Extract is correct for an Argon2id
  output (RFC 5869 3.3), but it means MK *is* the PRK, and .NET's
  HKDF.Expand rejects a PRK shorter than the hash output -- so a 32-byte MK
  cannot be expanded with SHA-512 at all. Widening it keeps the specified
  primitive; the alternatives were dropping to SHA-256 or adding an Extract
  step that conditions nothing.
- The bundle encoding is a fixed 92-byte layout rather than canonical CBOR.
  Canonicality is not load-bearing here -- unlike a key statement the bundle
  is never hashed or signed, only encrypted -- so CBOR's one advantage does
  not apply, while its canonicalisation rules are a real source of
  cross-implementation disagreement. It also costs a dependency
  System.Formats.Cbor is not in the shared framework. Safe to change now
  and not later: no bundle has ever been stored.

53 new tests. The encoding is checked against an independent codec written
in the test rather than by round-tripping production code against itself --
a round trip passes just as happily when both directions are wrong the same
way, and this format cannot change after one bundle is stored. The pinned
92-byte hex constant is the golden vector for the layout.

Most of the rest are negative, because a binding is only demonstrated by
the substitutions that fail: a wrap for another user, a grant from a
superseded generation, a payload pasted onto another item, a metadata blob
offered as a payload, a version rolled back.
2026-07-28 21:02:52 +02:00

DodoSSH

A self-hosted, team-oriented SSH client with an end-to-end encrypted vault.

Manage hosts, credentials and keys in a desktop app; sync them across your devices and share them with teammates through a server you run yourself. The server stores ciphertext and never holds a key — the operator cannot read the credentials it stores.

Status: early development. See the milestone plan for what exists today.

Why

Teams either scatter SSH credentials across individual ~/.ssh directories with no sharing story, or pay per-seat for a hosted product that holds their infrastructure credentials. DodoSSH keeps the convenience of a synced, shareable vault while remaining self-hostable and zero-knowledge.

Architecture

Component Choice
Backend ASP.NET Core on .NET 10, PostgreSQL + EF Core
Client Avalonia (C#) for Windows/Linux/macOS; terminal pane is a WebView running xterm.js
Auth OIDC, provider-agnostic (Entra ID, Keycloak, Auth0, Authentik)
Vault End-to-end encrypted; X25519 + Ed25519 + XChaCha20-Poly1305, Argon2id unlock
Connections Client-direct SSH by default, with an optional raw-TCP server relay

Two consequences worth knowing before you read further:

  • Revocation is not retroactive. A removed member keeps what they already downloaded. The real remediation is rotating the SSH credential, so offboarding is built around a rotation checklist rather than a button that implies more than it delivers.
  • No session recording in relay mode. The relay forwards SSH ciphertext, so it cannot see commands. That is the cost of the relay not being able to read your traffic.

The reasoning behind each major decision is recorded in docs/adr/, starting with the E2EE trust model.

Repository layout

src/
  DodoSSH.Contracts       DTOs shared with the client — the real API contract
  DodoSSH.Crypto          DSH1 envelope, AAD derivation, key wrapping
  DodoSSH.Domain          entities and invariants, no EF
  DodoSSH.Infrastructure  DbContext, configurations, migrations
  DodoSSH.Api             the host
tests/                    one test project per source project
docs/adr/                 architecture decision records

Building

Requires the .NET SDK pinned in global.json (10.0.x).

dotnet build DodoSSH.slnx
dotnet test DodoSSH.slnx

Run the API locally:

dotnet run --project src/DodoSSH.Api

It listens on http://localhost:5233, serving /healthz/live, /healthz/ready and — in Development — /openapi/v1.json.

Development and testing are currently Windows-only. Anything known or suspected to differ on Linux and macOS is tracked in docs/platform-flags.md, along with the deployment gotchas that have already cost time once. Read it before assuming something works off-Windows.

Conventions the build enforces

  • Warnings are errors. dotnet format --verify-no-changes gates CI.
  • Package versions are centralised in Directory.Packages.props; packages.lock.json is committed and CI restores in locked mode.
  • BannedSymbols.txt bans DateTime.UtcNow (use TimeProvider), Guid.NewGuid (use CreateVersion7), sync-over-async, MD5/SHA1 and PBKDF2.
  • Public members of DodoSSH.Contracts must be declared in PublicAPI.Unshipped.txt, so a contract change is a build error rather than a client-side surprise.

Milestones

  • M0 — foundation. Repo structure, build conventions, CI, ADRs. Done.
  • M1 — vertical slice. OIDC login → enroll → create a host → open a shell. Backend done: the DSH1 crypto core, the data model, sync push/pull for hosts, /me, and enrollment with the identity-provider key binding. Remaining: the desktop client, and the two spikes that gate it — the Linux WebView and SSH.NET's window-change. Both need a Linux and a macOS machine, so neither has run yet.
  • M2 — full personal vault, robust sync, relay.
  • M3 — teams, sharing, ACLs.
  • M4 — hardening and ops, packaging, self-hosting guide.
  • M5 — multi-provider OIDC, key rotation, per-item content keys.

Licence

MIT.

S
Description
No description provided
Readme MIT
8.7 MiB
Languages
C# 98%
PowerShell 0.7%
Shell 0.5%
JavaScript 0.5%
Dockerfile 0.2%