From 3dd5773fe1b72fbb01d120089f47061520285a8d Mon Sep 17 00:00:00 2001 From: Andrew Chen Date: Thu, 27 Aug 2026 16:14:49 -0700 Subject: [PATCH 1/7] Replace pre-shared auth tokens with ed25519 public-key auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt ezvpn's public-key handshake, matching ezvpn-apple and ezvpn-android: the client authenticates with an ed25519 keypair in the shared FlexAccess format, and the server accepts it only if the public half is on its authorized_keys file. The ezvpn_start config's auth_token becomes auth_key. Keys are generated and derived exclusively through the FFI (ezvpn_generate_client_key / ezvpn_client_public_key, wrapped by Core/Interop/AuthKey), so the key format stays owned by flexaccess-keys and is never reimplemented in .NET. AuthKeyStore keeps the app's shared, named key list — the same model the other apps use: profiles reference a key by id (TunnelProfile.AuthKeyId) and, on save, copy that key's secret into the profile's own credential, which is what the tunnel is handed. So a key deleted from the list doesn't break profiles already saved with it. Public halves are re-derived on load rather than stored, and a record whose secret no longer parses is dropped from the list without being deleted from storage (a transient native failure must not destroy a key). Storage diverges from the siblings deliberately: they keep the whole list as one Keychain / encrypted-preferences document, but a Credential Manager blob caps out at 2560 bytes (about nine keys), so each key is its own ezvpn-key: credential. Per-record writes also mean an add, rename or delete never rewrites the other keys. TokenStore becomes SecretStore throughout, and its reads now distinguish "not stored" from "couldn't be read" instead of returning null for both. The new Auth keys dialog generates, imports, renames, copies and deletes keys; secrets never render, and copying one is behind a confirmation. Its actions are all inline (rename in place, confirmation flyouts) because WinUI allows only one ContentDialog at a time. The profile editor's password box becomes a key picker that shows the selected key's public half, warns when a profile's key has been removed from the list, and reaches the key manager by hiding and re-showing itself so the half-filled form survives. Pins ezvpn.dll to v0.0.43: v0.0.36 predates the key FFI symbols. Per the repo's no-backward-compatibility rule there is no migration — profiles saved with a pre-shared token keep loading but fail the handshake until re-saved with a key. Verified on the CI VM (ci/windows/remote.ps1): build, 43 Core tests, publish and MSI all green, plus a throwaway harness against the pinned DLL confirming generate/derive round-trips and that ezvpn_start wants exactly auth_key. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CWMkFDzsTTQVQsUxpzF2wN --- .github/workflows/verify-ezvpn-commit.yml | 2 +- CLAUDE.md | 16 +- Directory.Build.props | 2 +- README.md | 22 +- native/native.targets | 4 +- src/Ezvpn.App/MainWindow.xaml | 35 ++- src/Ezvpn.App/MainWindow.xaml.cs | 53 +++- .../Services/CredentialAuthKeyStore.cs | 60 ++++ src/Ezvpn.App/Services/ProfileStore.cs | 5 +- src/Ezvpn.App/Services/SecretStore.cs | 211 ++++++++++++++ src/Ezvpn.App/Services/TokenStore.cs | 136 --------- src/Ezvpn.App/ViewModels/TunnelViewModel.cs | 12 + src/Ezvpn.App/ViewModels/TunnelsManager.cs | 112 +++++--- src/Ezvpn.App/Views/AuthKeysDialog.xaml | 112 ++++++++ src/Ezvpn.App/Views/AuthKeysDialog.xaml.cs | 238 ++++++++++++++++ src/Ezvpn.App/Views/TunnelEditDialog.xaml | 22 +- src/Ezvpn.App/Views/TunnelEditDialog.xaml.cs | 100 ++++++- src/Ezvpn.Core/AuthKeyStore.cs | 258 ++++++++++++++++++ src/Ezvpn.Core/EzvpnConfig.cs | 20 +- src/Ezvpn.Core/Interop/AuthKey.cs | 78 ++++++ src/Ezvpn.Core/Interop/EzvpnNative.cs | 33 ++- src/Ezvpn.Core/Interop/EzvpnSession.cs | 17 +- src/Ezvpn.Core/TunnelProfile.cs | 16 +- src/Ezvpn.Core/TunnelValidation.cs | 23 +- tests/Ezvpn.Core.Tests/AuthKeyStoreTests.cs | 215 +++++++++++++++ tests/Ezvpn.Core.Tests/EzvpnConfigTests.cs | 20 +- .../Ezvpn.Core.Tests/TunnelValidationTests.cs | 21 +- 27 files changed, 1576 insertions(+), 267 deletions(-) create mode 100644 src/Ezvpn.App/Services/CredentialAuthKeyStore.cs create mode 100644 src/Ezvpn.App/Services/SecretStore.cs delete mode 100644 src/Ezvpn.App/Services/TokenStore.cs create mode 100644 src/Ezvpn.App/Views/AuthKeysDialog.xaml create mode 100644 src/Ezvpn.App/Views/AuthKeysDialog.xaml.cs create mode 100644 src/Ezvpn.Core/AuthKeyStore.cs create mode 100644 src/Ezvpn.Core/Interop/AuthKey.cs create mode 100644 tests/Ezvpn.Core.Tests/AuthKeyStoreTests.cs diff --git a/.github/workflows/verify-ezvpn-commit.yml b/.github/workflows/verify-ezvpn-commit.yml index 52288a9..3c0bdb2 100644 --- a/.github/workflows/verify-ezvpn-commit.yml +++ b/.github/workflows/verify-ezvpn-commit.yml @@ -17,7 +17,7 @@ on: ezvpn_ref: description: "ezvpn git ref (commit SHA / branch / tag) to build against" required: true - default: "b41e190b130251cd20230d020bc79c6e7285b7a4" + default: "6f7d3475cb4032be31c7ededbb606faba60e3547" ezvpn_repo: description: "ezvpn repository (owner/name)" required: true diff --git a/CLAUDE.md b/CLAUDE.md index 9c8c533..54fa5c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,8 +36,20 @@ The Rust core + C FFI live in the `../ezvpn` repo (`src/ffi_windows.rs`, `EzvpnConfig.Build` produces it. Keep them in sync. - `ClientStatus` mirrors the Rust `ClientStatus` (snake_case) from `../ezvpn/src/control.rs`. -- Auth tokens live in Windows Credential Manager (`TokenStore`), never in the - profile JSON. +- The client authenticates with an **ed25519 keypair**, not a pre-shared token: + the config's `auth_key` is the client's `ed25519-sec:…` secret, and its public + half goes on the server's `authorized_keys` file. Keys are generated and parsed + only through the FFI (`ezvpn_generate_client_key` / `ezvpn_client_public_key`, + wrapped by `Core/Interop/AuthKey`) — never reimplement the key format in .NET. +- Like the Apple and Android apps, the app keeps one shared list of **named** + keys (`AuthKeyStore`) that profiles reference by id (`TunnelProfile.AuthKeyId`); + saving a profile copies that key's secret into the profile's own credential, + which is what `ezvpn_start` is handed. So a key deleted from the list doesn't + break profiles already saved with it. +- All secrets live in Windows Credential Manager (`SecretStore`), never in the + profile JSON: `ezvpn:` (the profile's auth key), `ezvpn-relay:` + (the optional relay token) and `ezvpn-key:` (one record per named key — + one credential each, because a credential blob caps out at 2560 bytes). - Installer uses **WiX v5** (v6/v7 require accepting the paid OSMF EULA). The MSI is unsigned by design; code signing and MSIX/Store packaging are out of scope. - Use classic `[DllImport]` (not `[LibraryImport]`) for the `advapi32` diff --git a/Directory.Build.props b/Directory.Build.props index 403e222..d726483 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ monotonically increasing build-timestamp value so MajorUpgrade keeps working (see installer/Ezvpn.Installer.wixproj and .github/workflows/release.yml). --> - 0.0.36 + 0.0.43 diff --git a/README.md b/README.md index 74c73a5..c58951b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See `docs/Windows-App.md` in the `ezvpn` repo for the FFI contract. | Project | What | |---|---| | `src/Ezvpn.Core` | Pure model + config JSON builder + validation + status DTOs + the `ezvpn.dll` P/Invoke wrapper (`EzvpnSession`). No WinUI; unit-tested. | -| `src/Ezvpn.App` | The WinUI 3 app: profile list/detail/edit, connect/disconnect, live status polling. Stores profiles under `%ProgramData%\ezvpn\profiles` and the auth token in Windows Credential Manager. | +| `src/Ezvpn.App` | The WinUI 3 app: profile list/detail/edit, the auth-key manager, connect/disconnect, live status polling. Stores profiles under `%ProgramData%\ezvpn\profiles` and every secret in Windows Credential Manager. | | `tests/Ezvpn.Core.Tests` | xUnit tests for the pure logic. | | `installer/` | WiX v5 MSI that bundles the published app + `ezvpn.dll` + `wintun.dll`. | @@ -164,8 +164,20 @@ involved. ## Usage 1. Launch ezvpn (accept the UAC prompt). -2. **+** to add a profile: give it a name, the server's iroh node id, the auth - token (required), and optional split-tunnel routes (`10.0.0.0/8`, `fd00::/8`, …). -3. Select it and **Connect**. The status panel shows the assigned IP, gateway, +2. Add an **auth key** (the key button in the left toolbar, or *Manage keys…* in + the profile editor): name it and leave the secret blank to generate a fresh + ed25519 keypair — or paste an `ed25519-sec:…` secret from another device to + reuse that identity. Copy the key's **public** half onto the server's + `authorized_keys` file; the secret never leaves the machine except through the + explicit copy action. +3. **+** to add a profile: give it a name, the server's iroh node id, the auth key + to authenticate with (required), and optional split-tunnel routes + (`10.0.0.0/8`, `fd00::/8`, …). +4. Select it and **Connect**. The status panel shows the assigned IP, gateway, routes, and the live iroh connection path once connected. -4. **Disconnect** tears down the tunnel and routes. +5. **Disconnect** tears down the tunnel and routes. + +Keys are shared across profiles: several profiles can authenticate with the same +device identity. A profile keeps its own copy of the secret it was saved with, so +deleting a key from the list doesn't break profiles already using it — but they +can't be re-saved until a key is picked again. diff --git a/native/native.targets b/native/native.targets index e33014e..af238e7 100644 --- a/native/native.targets +++ b/native/native.targets @@ -29,9 +29,9 @@ once an ezvpn release that includes the build-windows-lib asset exists. scripts\bump-dll.ps1 also mirrors this tag (minus "v") into EzvpnVersion in Directory.Build.props, which is the version the app UI + MSI report. --> - v0.0.36 + v0.0.43 https://github.com/flexaccessdev/ezvpn/releases/download/$(EzvpnReleaseTag)/ezvpn-windows.dll.zip - d51ecf7b0df94ad3347121c220c4735caf52fd2b7a0d5b77e7c81436b20c8d78 + 792d01e125058aebe0f73b3af54f8e6bab3d29246b51ec1abb40162bb2516f75 + + + + + + + + + + + + + + + + + + + + + + + + + +