Skip to content

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/*)'s RP_API_SECRET is 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 + documentNumber from 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 documentNumber obtained 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. pollToken comes back in the start / login-notify / sign-pdf-start responses and is required for /api/status and /api/sign-pdf-download. sessionId is exposed in the QR, so on its own it does not grant the right to read PII — pollToken is 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 .xcodeproj from project.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 local GeregeTokenKit SPM 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):

  1. 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.
  2. POST /api/sign-pdf-start {etsi, digestB64, fileName, callbackUrl:""}{sessionId, vc, pollToken}. etsi = the civil-ID number obtained from login (civilID, fallback nationalID, uppercased). A PIN2 push is sent to the phone. callbackUrl is empty — since the phone is a separate device, there is no Web2App return.
  3. Poll GET /api/status?sessionId=&pollToken= (same path as auth) → COMPLETE/OK.
  4. POST /api/sign-pdf-download (multipart file + 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.

  • 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