From 6146bedb0ba0910743d0420b8bdfe6970b31464a Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 21 Oct 2026 14:01:20 +0200 Subject: [PATCH 01/21] build(release): publish the macOS and Windows downloads under fixed names The dashboard downloads releases/latest/download/PoliNetwork-eSign-macos.dmg and PoliNetwork-eSign-windows-amd64.zip, so its button gets the right installer at once instead of opening a release page with a dozen files. The macOS .zip follows the same pattern. The versioned names go away rather than doubling every download on the release page: the version stays in the tag, the app and its pages. The release fails when either linked file is missing. releases/latest is the newest release that is not a pre-release, so release candidates never reach these links. Until the first release with these names, the links answer 404. --- .github/workflows/release.yml | 7 +++++++ README.md | 16 +++++++++++++--- scripts/build-macos.sh | 9 +++++---- scripts/build-windows.sh | 7 ++++--- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02d090d..7dfb356 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,13 @@ jobs: with: path: dist merge-multiple: true + - name: Check the downloads the dashboard links + # The dashboard downloads releases/latest/download/: without these files its buttons would answer 404. + # releases/latest is the newest release that is not a pre-release, so release candidates never reach it. + run: | + for name in PoliNetwork-eSign-macos.dmg PoliNetwork-eSign-windows-amd64.zip; do + if [ ! -f "dist/$name" ]; then echo "::error::dist/$name is missing"; exit 1; fi + done - name: Channel id: channel # v1.2.3-rc.1 and similar are pre-releases: GitHub only, never the package repositories or the AUR. diff --git a/README.md b/README.md index ad2fc18..e9cd837 100644 --- a/README.md +++ b/README.md @@ -51,14 +51,16 @@ read. The dashboard never sees the PIN. Download the file for your system from the [latest release](https://github.com/PoliNetworkOrg/esign/releases/latest). Installing needs no administrator rights: the agent is installed for your user only, starts when you log in, and lets -the dashboard start it with a `polinetwork-esign://open` link when it is not running. +the dashboard start it with a `polinetwork-esign://open` link when it is not running. To update on macOS or Windows, +download the latest version and open it the same way: it replaces the one installed. Until v0.1.x the agent was called PoliNetwork Sign Agent. Installing PoliNetwork eSign removes it (app, login item, link handler, Debian package) and keeps its configuration and card pairings. ### macOS (Apple Silicon and Intel) -1. Download `PoliNetwork-eSign--macos.dmg` (or the `.zip`) and open it. +1. Download [`PoliNetwork-eSign-macos.dmg`](https://github.com/PoliNetworkOrg/esign/releases/latest/download/PoliNetwork-eSign-macos.dmg) + (or the `.zip`) and open it. 2. Open **PoliNetwork eSign**. The app is not notarized yet, so macOS refuses the first time: open **System Settings > Privacy & Security**, choose **Open Anyway** next to the message about PoliNetwork eSign, and confirm. @@ -73,7 +75,9 @@ card pairing that Software CIE keeps in its own folder. ### Windows -1. Download `PoliNetwork-eSign--windows-amd64.zip`, extract it and double-click `esign.exe`. +1. Download + [`PoliNetwork-eSign-windows-amd64.zip`](https://github.com/PoliNetworkOrg/esign/releases/latest/download/PoliNetwork-eSign-windows-amd64.zip), + extract it and double-click `esign.exe`. 2. The program is not code-signed yet: if SmartScreen warns, choose **More info > Run anyway**. 3. It copies itself to `%LOCALAPPDATA%\PoliNetwork\eSign`, starts at login, appears in **Settings > Apps > Installed apps**, and opens `http://127.0.0.1:47321/` in your browser, which shows whether the reader, the CIE and @@ -284,6 +288,12 @@ Releases: push a `v*` tag; `.github/workflows/release.yml` builds everything, ad GitHub release (a pre-release for tags such as `v1.2.3-rc.1`). Releases that are not pre-releases also update the APT and DNF repositories and the AUR package: see [`docs/linux-packages.md`](docs/linux-packages.md). +The macOS and Windows files have the same names in every release (`PoliNetwork-eSign-macos.dmg`, +`PoliNetwork-eSign-macos.zip`, `PoliNetwork-eSign-windows-amd64.zip`), so +`https://github.com/PoliNetworkOrg/esign/releases/latest/download/` always downloads the newest release that is +not a pre-release. The dashboard links the `.dmg` and the Windows `.zip` that way, and the release fails if either is +missing. + ### Maintainer TODO before a public release - **macOS notarization.** The app is only signed ad hoc, so Gatekeeper blocks it until the user allows it. Sign with a diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh index f38ed8f..1a38147 100755 --- a/scripts/build-macos.sh +++ b/scripts/build-macos.sh @@ -2,8 +2,9 @@ # Builds the macOS app on a Mac: ./scripts/build-macos.sh [version] # # dist/PoliNetwork eSign.app holds a universal binary (arm64 + x86_64): the agent runs natively, and its x86_64 -# half runs the Intel-only official CIE middleware through Rosetta. Also writes -# dist/PoliNetwork-eSign--macos.zip and, when hdiutil works, a .dmg. +# half runs the Intel-only official CIE middleware through Rosetta. Also writes dist/PoliNetwork-eSign-macos.zip and, +# when hdiutil works, dist/PoliNetwork-eSign-macos.dmg. Their names carry no version, so that +# releases/latest/download/ always downloads the latest release: the dashboard links the .dmg that way. # # Signing: with MACOS_SIGN_IDENTITY set (a "Developer ID Application: …" identity in the keychain) the app is signed # with the hardened runtime; with APPLE_API_KEY_PATH, APPLE_API_KEY_ID and APPLE_API_ISSUER_ID also set, the zip and @@ -60,7 +61,7 @@ else fi codesign --verify --strict --verbose=2 "$app" -zip="dist/PoliNetwork-eSign-$version-macos.zip" +zip="dist/PoliNetwork-eSign-macos.zip" rm -f "$zip" ditto -c -k --sequesterRsrc --keepParent "$app" "$zip" if $notarize; then @@ -73,7 +74,7 @@ if $notarize; then fi echo "$zip" -dmg="dist/PoliNetwork-eSign-$version-macos.dmg" +dmg="dist/PoliNetwork-eSign-macos.dmg" rm -f "$dmg" staging="$work/dmg" mkdir -p "$staging" diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh index cf6113a..8363635 100755 --- a/scripts/build-windows.sh +++ b/scripts/build-windows.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Cross-builds the Windows release with MinGW-w64: ./scripts/build-windows.sh [version] -# Writes dist/PoliNetwork-eSign--windows-amd64.zip holding esign.exe, a GUI-subsystem program (no -# console window) that attaches to the console of the terminal it is started from. +# Writes dist/PoliNetwork-eSign-windows-amd64.zip holding esign.exe, a GUI-subsystem program (no console window) that +# attaches to the console of the terminal it is started from. The name carries no version, so that +# releases/latest/download/ always downloads the latest release: the dashboard links it that way. set -euo pipefail cd "$(dirname "$0")/.." version="${1:-$(git describe --tags --always --dirty 2>/dev/null || echo dev)}" @@ -11,7 +12,7 @@ mkdir -p "$work" GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC="${CC:-x86_64-w64-mingw32-gcc}" \ go build -trimpath -ldflags "-H=windowsgui -s -w -X main.version=$version" -o "$work/esign.exe" ./cmd/esign cp README.md LICENSE "$work/" -zip="dist/PoliNetwork-eSign-$version-windows-amd64.zip" +zip="dist/PoliNetwork-eSign-windows-amd64.zip" rm -f "$zip" (cd "$work" && zip -q -9 "../$(basename "$zip")" esign.exe README.md LICENSE) echo "$zip" From 12397129f6e0b27123d97d8da91aaf8da73d773d Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 21 Oct 2026 14:02:47 +0200 Subject: [PATCH 02/21] fix(cie): call the CIE software CIE ID wherever someone has to find it The installed app is "CIE ID" on every system; only its Mac App Store listing says "Software CIE", so Spotlight and Launchpad never found the name the README and the doctor used. Errors, the status pages, the doctor (which now labels each provider) and the README say CIE ID and mention the store listing once. On macOS the README makes pairing the card in CIE ID the setup check: opening the app installs Rosetta, and a card that pairs there signs here too. The doctor suggests pairing when it finds no paired card. --- README.md | 40 ++++++++++++++++------------ cmd/esign/app_darwin.go | 15 +++++++---- cmd/esign/doctor.go | 10 ++++++- internal/server/server.go | 4 +-- internal/server/web/assets/sign.js | 2 +- internal/server/web/assets/status.js | 2 +- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index e9cd837..be852c5 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,12 @@ finalized by the PoliNetwork backend (PAdES), the agent only signs a digest with - A CIE (Carta d'Identità Elettronica 3.0) and its 8-digit PIN. - A contactless (NFC) smart-card reader, for example a bit4id miniLector AIR NFC. Most readers work without drivers on Windows and macOS; on Linux they need `pcscd` (see below). -- Optionally, CIE software for your system. The agent tries, in order: - - `cie-mw`, the official middleware: **Software CIE** (Mac App Store) on macOS, **CIE ID** on Windows and Linux, - from [cartaidentita.interno.gov.it](https://www.cartaidentita.interno.gov.it/pa-e-imprese/documentazione-middleware-cie/). - It works only with cards "paired" on this computer, a one-time step with the full PIN. If the card is not paired - yet, the agent pairs it through the middleware the first time you sign. +- **CIE ID**, the official CIE app, from + [cartaidentita.interno.gov.it](https://www.cartaidentita.interno.gov.it/pa-e-imprese/documentazione-middleware-cie/) + (on the Mac App Store it is listed as "Software CIE"). The agent tries, in order: + - `cie-mw`, the middleware that comes with CIE ID. It works only with cards "paired" on this computer, a one-time + step with the full PIN. If the card is not paired yet, the agent pairs it through the middleware the first time + you sign; on macOS, pair it in CIE ID when you install it (see below). - `opencie`, [OpenCIE](https://github.com/M0Rf30/opencie-pkcs11): put its PKCS#11 module (`libopencie-pkcs11.so`, `.dylib` or `.dll`) next to the agent or in the agent's configuration folder. It also keeps a pairing cache, which the agent creates the same way. @@ -59,6 +60,10 @@ link handler, Debian package) and keeps its configuration and card pairings. ### macOS (Apple Silicon and Intel) +First install **CIE ID** from the Mac App Store, where it is listed as "Software CIE", and open it: on an Apple Silicon +Mac, accept when macOS offers to install Rosetta. Pair your card in it with the 8-digit PIN. If it pairs there, signing +works with PoliNetwork eSign too; if it does not, ask whoever sent you the signing request. Then: + 1. Download [`PoliNetwork-eSign-macos.dmg`](https://github.com/PoliNetworkOrg/esign/releases/latest/download/PoliNetwork-eSign-macos.dmg) (or the `.zip`) and open it. 2. Open **PoliNetwork eSign**. The app is not notarized yet, so macOS refuses the first time: open @@ -68,10 +73,10 @@ link handler, Debian package) and keeps its configuration and card pairings. Items**) and opens `http://127.0.0.1:47321/` in your browser. The page shows whether the reader, the CIE and the signing software are ready: with the CIE on the reader it says "Ready to sign". -The official Software CIE module is built for Intel Macs only. On Apple Silicon the agent runs it in a helper process -under Rosetta: if macOS offers to install Rosetta, accept (or run `softwareupdate --install-rosetta`). The first time you -sign, macOS asks whether **PoliNetwork eSign** may access data from other apps: allow it, so the agent can read the -card pairing that Software CIE keeps in its own folder. +CIE ID's middleware is built for Intel Macs only. On Apple Silicon the agent runs it in a helper process under Rosetta, +which opening CIE ID installs (or run `softwareupdate --install-rosetta`). The first time you sign, macOS asks whether +**PoliNetwork eSign** may access data from other apps: allow it, so the agent can read the card pairing that CIE ID +keeps in its own folder. ### Windows @@ -153,7 +158,7 @@ after the other, and shows which were signed. If the dashboard refuses one docum the others are still signed; if the card stops answering, the rest are left unsigned and you can try again. A wrong PIN uses one of the card's three attempts; the agent never retries a PIN by itself. After three wrong PINs the -card is blocked: unblock it with the PUK in the CIE software. +card is blocked: unblock it with the PUK in CIE ID. ## Troubleshooting @@ -172,14 +177,15 @@ file as above is easier. Common reports: -- **module: not found** — the CIE software is not installed, or is installed somewhere unusual: set its path in the - configuration file (`pkcs11Module` or `openCieModule`). +- **module: not found** under `cie-mw (CIE ID)` — CIE ID is not installed, or is installed somewhere unusual: set its + path in the configuration file (`pkcs11Module`; `openCieModule` for OpenCIE). - **loading: impossible … x86_64 only** — an Apple Silicon Mac running a build without Intel code; install the release app, which contains both. - **readers: none** — the reader is not connected, or (Linux) `pcscd` is not running. - **card: false** — put the CIE flat on the reader and keep it still. -- "This CIE is not set up on this computer yet" when signing — the middleware does not export its pairing function: - pair the card in Software CIE / CIE ID. +- **paired cards: none** (macOS) — open CIE ID and pair the card with its 8-digit PIN. +- "This CIE is not paired with this computer yet" when signing — CIE ID's middleware does not export its pairing + function: pair the card in CIE ID. - **agent: NOT running; another program uses port 47321** — close that program, or change `port` in the configuration (the dashboard expects 47321). @@ -203,7 +209,7 @@ eSign` on macOS, `%APPDATA%\PoliNetwork eSign` on Windows, `~/.config/PoliNetwor ``` `worker` says when a PKCS#11 module runs in a separate helper process: `auto` when needed (another CPU architecture, -or Software CIE's environment), `always` (a crashing middleware cannot stop the agent), or `never`. +or CIE ID's environment on macOS), `always` (a crashing middleware cannot stop the agent), or `never`. ## Commands @@ -300,11 +306,11 @@ missing. Developer ID Application certificate, with the hardened runtime and the `com.apple.security.cs.disable-library-validation` entitlement (the agent loads the CIE middleware, which another team signed), notarize with `xcrun notarytool submit --wait` and staple (`xcrun stapler staple`) both the app and the - `.dmg`. A stable signature also keeps macOS from asking again, after each update, for access to Software CIE's data. + `.dmg`. A stable signature also keeps macOS from asking again, after each update, for access to CIE ID's data. - **Windows code signing** (Authenticode) of `esign.exe`, to avoid SmartScreen warnings; an icon and version resource would help too. - **Test on real hardware**: Windows 10/11 x64 (and Windows on ARM, which runs the x64 build emulated), Intel and Apple - Silicon Macs with Software CIE, and the Linux distributions to support, each with a paired card. + Silicon Macs with CIE ID, and the Linux distributions to support, each with a paired card. ## Code signing diff --git a/cmd/esign/app_darwin.go b/cmd/esign/app_darwin.go index a67822e..ff2265e 100644 --- a/cmd/esign/app_darwin.go +++ b/cmd/esign/app_darwin.go @@ -42,15 +42,16 @@ func plistString(path, key string) string { return strings.TrimSpace(string(out)) } -// systemNotes reports the Software CIE app and its paired cards. +// systemNotes reports the CIE ID app and the cards paired in it. Pairing the card in CIE ID is the setup check on +// macOS: a card that pairs there signs here too. func systemNotes() []string { var notes []string app := "/Applications/CIE ID.app" if _, err := os.Stat(app); err == nil { info := filepath.Join(app, "Contents", "Info.plist") - notes = append(notes, fmt.Sprintf("Software CIE: %s %s (%s)", app, plistString(info, "CFBundleShortVersionString"), plistString(info, "CFBundleIdentifier"))) + notes = append(notes, fmt.Sprintf("CIE ID: %s %s (%s)", app, plistString(info, "CFBundleShortVersionString"), plistString(info, "CFBundleIdentifier"))) } else { - notes = append(notes, "Software CIE: not installed (Mac App Store, \"Software CIE\")") + notes = append(notes, "CIE ID: not installed (Mac App Store, where it is called \"Software CIE\")") } home, _ := os.UserHomeDir() cache := filepath.Join(home, "Library", "Group Containers", "group.it.ipzs.SoftwareCIE", "Library", "Caches", "CIEPKI") @@ -63,11 +64,15 @@ func systemNotes() []string { n++ } } - notes = append(notes, fmt.Sprintf("paired cards: %d in %s", n, cache)) + note := fmt.Sprintf("paired cards: %d in %s", n, cache) + if n == 0 { + note += "; pair the card in CIE ID" + } + notes = append(notes, note) case os.IsPermission(err): notes = append(notes, "paired cards: unknown, macOS did not let this program read "+cache) default: - notes = append(notes, "paired cards: none ("+cache+" is absent)") + notes = append(notes, "paired cards: none ("+cache+" is absent); pair the card in CIE ID") } return notes } diff --git a/cmd/esign/doctor.go b/cmd/esign/doctor.go index 775f02c..47d8774 100644 --- a/cmd/esign/doctor.go +++ b/cmd/esign/doctor.go @@ -17,6 +17,14 @@ import ( "github.com/PoliNetworkOrg/esign/internal/token/worker" ) +// providerNames name each provider as users know it, so the report names the app to install. +var providerNames = map[string]string{ + "cie-mw": "CIE ID", + "opencie": "OpenCIE", + "cie": "built in", + "soft": "development key", +} + // doctor prints what support needs to know: the build, installation, configuration, running agent, every // provider's module and architecture, and the readers and card each provider sees. It never asks for the PIN. func doctor(ctx context.Context, w io.Writer, cfg config.Config, configPath, dir string, specs []providerSpec) { @@ -63,7 +71,7 @@ func doctor(ctx context.Context, w io.Writer, cfg config.Config, configPath, dir fmt.Fprintln(w, "\nProviders") for _, s := range specs { - fmt.Fprintf(w, " %s\n", s.Name) + fmt.Fprintf(w, " %s (%s)\n", s.Name, providerNames[s.Name]) switch { case s.Module != "": marchs := strings.Join(s.Archs, "+") diff --git a/internal/server/server.go b/internal/server/server.go index a233c84..71df363 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -679,8 +679,8 @@ var messageOf = map[token.Code]string{ token.CodeNoReader: "No smart-card reader found. Connect the NFC reader.", token.CodeNoCard: "No card on the reader. Place your CIE on the reader.", token.CodeCardRemoved: "The card moved away from the reader. Keep the CIE still on the reader and try again.", - token.CodeMiddlewareMissing: "No CIE software found on this computer.", - token.CodeNotPaired: "This CIE is not set up on this computer yet.", + token.CodeMiddlewareMissing: "CIE ID is not set up on this computer.", + token.CodeNotPaired: "This CIE is not paired with this computer yet. Pair it in CIE ID, then sign again.", token.CodeCardUnsupported: "This card is not a supported CIE.", token.CodeCardUntrusted: "This card could not be verified as a genuine CIE, so the PIN was not sent to it.", token.CodeNotFound: "No signing certificate found on the card.", diff --git a/internal/server/web/assets/sign.js b/internal/server/web/assets/sign.js index d480a8d..caa956d 100644 --- a/internal/server/web/assets/sign.js +++ b/internal/server/web/assets/sign.js @@ -347,7 +347,7 @@ async function pollStatus() { card.dataset.state = status.ready ? "ready" : "waiting" if (status.ready) $("card-text").textContent = reader?.name ? `Card detected on ${reader.name}` : "Card detected" // Unavailable software lists no readers, so it comes first. - else if (!software.ok) $("card-text").textContent = "No CIE software found. See the setup guide in the dashboard." + else if (!software.ok) $("card-text").textContent = "CIE ID is not set up. See the setup guide in the dashboard." else if (!reader?.ok) $("card-text").textContent = "Connect the NFC card reader" else $("card-text").textContent = `Place your CIE on the reader (${reader.name})` } diff --git a/internal/server/web/assets/status.js b/internal/server/web/assets/status.js index aec6dc9..20cc684 100644 --- a/internal/server/web/assets/status.js +++ b/internal/server/web/assets/status.js @@ -3,7 +3,7 @@ // What an /api/status answer means for the user, shared by the signing page and the home page so they never disagree. const SOFTWARE = { - "cie-mw": "Official CIE software", + "cie-mw": "CIE ID", opencie: "OpenCIE", cie: "Built into PoliNetwork eSign", soft: "Development key (signatures not valid)", From 1d082f595af71bfed41cc8aa0afa2cfea8ec6b0c Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 21 Oct 2026 14:04:22 +0200 Subject: [PATCH 03/21] feat(sign): show the version at the bottom of the agent's pages "PoliNetwork eSign 0.4.0" under the panel answers support's first question from a screenshot, on the signing window too. The agent renders it into the page, so it shows before any script or status call, and the home page no longer fills it from the status. Release tags lose their "v". --- internal/server/server.go | 27 +++++++++++++++++++---- internal/server/server_test.go | 32 ++++++++++++++++++++++++++++ internal/server/web/assets/index.js | 1 - internal/server/web/assets/style.css | 5 +++-- internal/server/web/index.html | 2 +- internal/server/web/sign.html | 2 ++ 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 71df363..c9e1670 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -13,6 +13,7 @@ import ( "encoding/json" "errors" "fmt" + "html/template" "io/fs" "log/slog" "net" @@ -35,6 +36,23 @@ const APIHeader = "X-PN-eSign" //go:embed web var webFS embed.FS +// pages are the HTML pages, rendered with pageData; their assets are served as they are. +var pages = template.Must(template.ParseFS(webFS, "web/*.html")) + +// pageData is what the pages show before any script runs. +type pageData struct { + // Version is the agent's version as people read it: "0.4.0" for the release tagged v0.4.0, or "dev". + Version string +} + +// shownVersion drops the "v" of a release tag. +func shownVersion(version string) string { + if rest, ok := strings.CutPrefix(version, "v"); ok && rest != "" && rest[0] >= '0' && rest[0] <= '9' { + return rest + } + return version +} + // Options configures the server. type Options struct { // Port the agent listens on; Host headers must name it. @@ -217,13 +235,14 @@ func (s *Server) guard(next http.Handler) http.Handler { func (s *Server) page(name string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - data, err := webFS.ReadFile("web/" + name) - if err != nil { - http.NotFound(w, r) + var page bytes.Buffer + if err := pages.ExecuteTemplate(&page, name, pageData{Version: shownVersion(s.opts.Version)}); err != nil { + s.opts.Logger.Error("render page", "page", name, "error", err) + http.Error(w, "PoliNetwork eSign could not show this page.", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") - _, _ = w.Write(data) + _, _ = w.Write(page.Bytes()) } } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 0d030a7..979fa2a 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -232,6 +232,38 @@ func TestGuards(t *testing.T) { } } +func TestPagesShowTheVersion(t *testing.T) { + for version, footer := range map[string]string{ + "v1.2.3": "PoliNetwork eSign 1.2.3", + "": "PoliNetwork eSign <b>", + } { + s := New(Options{Port: 47321, Version: version}) + for _, path := range []string{"/", "/sign"} { + rec := do(t, s, req{method: "GET", path: path}) + if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), footer) { + t.Errorf("%s with version %q: %d, no %q in\n%s", path, version, rec.Code, footer, rec.Body) + } + } + } +} + +func TestShownVersion(t *testing.T) { + cases := map[string]string{ + "v0.4.0": "0.4.0", + "v1.2.3-rc.1": "1.2.3-rc.1", + "0.4.0": "0.4.0", + "dev": "dev", + "vnext": "vnext", + "v": "v", + "": "", + } + for version, want := range cases { + if got := shownVersion(version); got != want { + t.Errorf("shownVersion(%q) = %q, want %q", version, got, want) + } + } +} + type signResponse struct { Results []signResult `json:"results"` Signer *struct { diff --git a/internal/server/web/assets/index.js b/internal/server/web/assets/index.js index 7965a47..65b599d 100644 --- a/internal/server/web/assets/index.js +++ b/internal/server/web/assets/index.js @@ -33,7 +33,6 @@ function render(status) { setText($("software-detail"), software.detail || "") $("software-detail").hidden = !software.detail setHeading(ready ? "Ready to sign" : "PoliNetwork eSign is running", ready) - setText($("version"), `PoliNetwork eSign ${status.version}`) } /** Shows heading and text in place of the status rows. */ diff --git a/internal/server/web/assets/style.css b/internal/server/web/assets/style.css index fc628df..9733c56 100644 --- a/internal/server/web/assets/style.css +++ b/internal/server/web/assets/style.css @@ -236,8 +236,9 @@ h1 { } .version { - min-height: 1.5em; - margin: 0; + margin-top: 16px; + color: var(--fg-muted); + font-size: 12px; } label { diff --git a/internal/server/web/index.html b/internal/server/web/index.html index 3708c7f..576cf6f 100644 --- a/internal/server/web/index.html +++ b/internal/server/web/index.html @@ -34,8 +34,8 @@

PoliNetwork eSign is running

To sign a document, open it in the PoliNetwork dashboard and choose “Sign with CIE”.

-

+
PoliNetwork eSign {{.Version}}
diff --git a/internal/server/web/sign.html b/internal/server/web/sign.html index 57edf59..13ce0d3 100644 --- a/internal/server/web/sign.html +++ b/internal/server/web/sign.html @@ -107,6 +107,8 @@

Signed

+ +
PoliNetwork eSign {{.Version}}
From 3617b278701e1a897b291a0730cce256790c6851 Mon Sep 17 00:00:00 2001 From: Lorenzo Corallo Date: Wed, 21 Oct 2026 14:07:44 +0200 Subject: [PATCH 04/21] feat(sign): tell the dashboard when the window is ready or cannot go on The signing page posts {status: "ready", detail: } as soon as it runs, before any request, so the dashboard can close a window that stays silent (the agent is not running) within seconds instead of waiting for the job to be seen. Every error that ends the window (no valid link, jobs that could not be loaded or expired, a card that cannot be used) now also carries the text the window shows and fatal: true, so the dashboard can explain it in place of "isn't answering". Other messages keep their meaning. The first message leaves before the page has asked the agent anything, so the agent puts the dashboard origin in the page only after checking it against its allowlist, and the origin stays in the address for reloads. Other sites that open the page learn nothing from it. --- README.md | 11 +++++ internal/server/server.go | 8 +++- internal/server/server_test.go | 20 +++++++++ internal/server/web/assets/sign.js | 66 ++++++++++++++++++------------ internal/server/web/sign.html | 2 +- 5 files changed, 79 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index be852c5..b23511d 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,17 @@ repeats are dropped). The page talks only to the agent: The agent calls the dashboard's `/api/esign/agent/jobs/{ticket}[/prepare|/complete|/fail]` with each ticket. A backend older than protocol 2 sends no steps, and its jobs are signed once. +The page tells the dashboard what happens with `postMessage({type: "pn-esign", status, detail, …})`, only to the +`origin` of its address and only when the agent signs for it: the agent checks the origin when it serves the page, so +the page knows where it may post before making any request. + +- `ready`, first thing when the page runs, with the agent's version in `detail` (`0.4.0`, or `dev`). A dashboard that + gets no `ready` can tell that the agent is not running. +- `error`, with the error code in `detail`. When the window cannot go on (the link is not valid, the jobs could not be + loaded or expired, the card cannot be used, …) the message also has `message`, the text the window shows, and + `fatal: true`. +- `completed` when every document was signed, `cancelled` when the signer cancelled. + Builds: ```sh diff --git a/internal/server/server.go b/internal/server/server.go index c9e1670..9b5f554 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -43,6 +43,8 @@ var pages = template.Must(template.ParseFS(webFS, "web/*.html")) type pageData struct { // Version is the agent's version as people read it: "0.4.0" for the release tagged v0.4.0, or "dev". Version string + // Dashboard is the signing page's origin parameter when the agent signs for it: the page's messages go there only. + Dashboard string } // shownVersion drops the "v" of a release tag. @@ -235,8 +237,12 @@ func (s *Server) guard(next http.Handler) http.Handler { func (s *Server) page(name string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + data := pageData{Version: shownVersion(s.opts.Version)} + if origin := r.URL.Query().Get("origin"); OriginAllowed(s.opts.AllowedOrigins, origin) { + data.Dashboard = origin + } var page bytes.Buffer - if err := pages.ExecuteTemplate(&page, name, pageData{Version: shownVersion(s.opts.Version)}); err != nil { + if err := pages.ExecuteTemplate(&page, name, data); err != nil { s.opts.Logger.Error("render page", "page", name, "error", err) http.Error(w, "PoliNetwork eSign could not show this page.", http.StatusInternalServerError) return diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 979fa2a..b86d8b8 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -10,6 +10,7 @@ import ( "errors" "net/http" "net/http/httptest" + "net/url" "slices" "strconv" "strings" @@ -247,6 +248,25 @@ func TestPagesShowTheVersion(t *testing.T) { } } +// TestSignPageDashboard: the signing page posts its messages, the first one before any request, only to a dashboard +// the agent signs for, which the agent names in the page. +func TestSignPageDashboard(t *testing.T) { + s, _, _ := setup(t) + cases := map[string]string{ + "/sign?origin=" + url.QueryEscape(dashboard) + "&ticket=" + ticket: dashboard, + "/sign?origin=" + url.QueryEscape(dashboard): dashboard, + "/sign?origin=" + url.QueryEscape("https://evil.example"): "", + "/sign?origin=" + url.QueryEscape(`">