macOS Desktop client — Developer guide¶
The eID platform's macOS (SwiftUI) desktop client — a desktop application
that performs QR / registration-number push login via the e-ID Mongolia
mobile app and applies a qualified electronic signature to PDFs. Source:
desktop/macos-app/ (XcodeGen project eIDMongolia).
First-party client. Following exactly the same principle as the iOS app, this client is not an RP — the client holds no RP secret, RP UUID, or registration. Every call goes through its own web backend's public
/api/*routes (the exact same path a browser uses). The Go RP-API (/v3/*)'sRP_API_SECRETis held only by the web server (web/src/lib/rpclient.ts). This is therefore NOT an RP integration — the web server itself does the RP integration, and the desktop merely uses its public front end.
1. What it does¶
- Login — sends a push via QR code or by registration/civil-ID number, and
the mobile app confirms it with PIN1. The web server cryptographically
verifies the signature, extracts the name + civil-ID number +
documentNumberfrom the cert subject, and returns them — the desktop does not parse the cert itself. - PDF signing — pick a PDF, compute its SHA-256 digest locally, and sign
it with PIN2 in the mobile app. The stamped (PAdES/PKCS#7 + verification page)
PDF is saved to
~/Downloads. - Identity handle — there is no Bearer session; the
documentNumberobtained from login becomes an identity handle and is stored in the Keychain (Touch ID gate on restore).
2. Architecture¶
The client calls the web app's public /api/* routes (web/src/app/api/*).
The routes it uses, as listed from the source
(Core/Network/Endpoints.swift, Core/Network/APIClient.swift):
| Route | Method | Purpose |
|---|---|---|
/api/start |
POST | Start a QR session → {sessionId, qr, deviceLinkBase, vc, pollToken} |
/api/login-notify |
POST | Push by registration/civil-ID number → {sessionId, vc, pollToken} (rate limit 60s/3) |
/api/status |
GET | ?sessionId=&pollToken= long-poll — the server holds ~1s, the client retries |
/api/sign-pdf-start |
POST | {etsi, digestB64, fileName, callbackUrl} → PIN2 push → {sessionId, vc, pollToken} (rate limit 60s/3 per etsi) |
/api/sign-pdf-download |
POST | multipart file + sessionId + pollToken → stamped PDF bytes |
/api/health |
GET | Check server health |
Note.
pollTokencomes back in thestart/login-notify/sign-pdf-startresponses and is required for/api/statusand/api/sign-pdf-download.sessionIdis exposed in the QR, so on its own it does not grant the right to read PII —pollTokenis issued only to the session initiator (Endpoints.swift, lines 18-23).
The secret, RP UUID, and RP name are not present in the client
(Core/Network/AppConfig.swift). Source precedence for the server base URL
(the first non-empty one wins):
| Source | Description |
|---|---|
UserDefaults["API_BASE_URL_OVERRIDE"] |
Set via the Settings UI |
env API_BASE_URL |
Environment variable |
| Default | DEBUG: http://localhost:3000, Release: https://eidmongolia.mn |
3. Prerequisites¶
- macOS 14+ (deployment target 14.0)
- Xcode 16+, Swift 5.10
brew install xcodegen— generates the.xcodeprojfromproject.yml- SPM dependencies: Sparkle 2 (auto-update),
../gerege-token-kit(local path package)
4. Build and run¶
cd desktop/macos-app
xcodegen generate
xcodebuild -project eIDMongolia.xcodeproj -scheme eIDMongolia \
-configuration Debug -destination 'platform=macOS,arch=arm64' build
open eIDMongolia.xcodeproj # ⌘R
Important. Use
-scheme eIDMongolia, not-target— the scheme is required to resolve the localGeregeTokenKitSPM package.
Local testing¶
The DEBUG build's default server is web (:3000). Start both the Go API and web:
cd ../../server && SMARTID_RP_API_SECRET= go run ./cmd/smartid # Go API :8080
cd ../../web && npm run dev # web :3000
To point at a different address, use Settings → Server (or env API_BASE_URL).
The mobile app must also point at the same Go server.
5. Main flows¶
Login (Features/Login/LoginView.swift)¶
QR (initQR):
1. POST /api/start → {sessionId, qr, vc, pollToken}.
2. Render the qr value (= sessionId) as a QR with CoreImage; display the
vc verification code on screen.
3. Scan the QR with the phone → approve with PIN1.
Registration-number push (initiateLogin):
1. The user enters a registration / civil-ID number (register, uppercased).
2. POST /api/login-notify {register} → {sessionId, vc, pollToken}; a push is
sent to the phone.
Common finish (APIClient.waitForAuth):
- Retries GET /api/status?sessionId=&pollToken= at ~400ms intervals until
COMPLETE (the server side holds for 1s).
- On COMPLETE + OK, the web server extracts name / idNumber
(serialNumber → civil-ID number) from the cert subject.
- The result is stored in the Keychain as a StoredIdentity
(documentNumber, fullName, civilID, certificateLevel).
PDF signing (Features/Sign/SignView.swift)¶
The exact same flow as the web demo page (web/src/app/demo/page.tsx):
- Pick a PDF (up to 25 MB). The SHA-256 digest of the source PDF is computed
locally on the client (
CryptoKit.SHA256) — the SAME bytes that get stamped. POST /api/sign-pdf-start {etsi, digestB64, fileName, callbackUrl:""}→{sessionId, vc, pollToken}.etsi= the civil-ID number obtained from login (civilID, fallbacknationalID, uppercased). A PIN2 push is sent to the phone.callbackUrlis empty — since the phone is a separate device, there is no Web2App return.- Poll
GET /api/status?sessionId=&pollToken=(same path as auth) → COMPLETE/OK. POST /api/sign-pdf-download(multipartfile+sessionId+pollToken) → stamped PDF bytes →~/Downloads/<name>_signed.pdf(numeric suffix if the name collides).
SEC-3: before starting the signing flow,
SecurityGuard.enforce()— tamper/anti-debug checks (active in Release).
6. USB token kit¶
desktop/gerege-token-kit/ is a dependency-free local SPM package for
FEITIAN USB tokens (local PKCS#11/APDU, no server connection). The macOS app
automatically resolves ../gerege-token-kit as a path package and uses it in
the token Core/Token/ (TokenManager, TokenProvisioner) and the Tokens
feature. Details: USB token kit.
7. Source (links)¶
- macOS app:
https://github.com/gerege-systems/eid-platform-mn/tree/main/desktop/macos-app - Network layer:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Core/Network/Endpoints.swift - HTTP client:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Core/Network/APIClient.swift - Login:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Features/Login/LoginView.swift - Signing:
https://github.com/gerege-systems/eid-platform-mn/blob/main/desktop/macos-app/Features/Sign/SignView.swift - Component details:
desktop/macos-app/CLAUDE.md, security hardening:desktop/macos-app/SECURITY-HARDENING.md