Security / App identity & attestation
App identity & attestation
For native and desktop apps, the publishable key is bound to your app two ways: a Bundle ID ties the key to your app's identity, and attestation cryptographically proves each request came from a genuine, unmodified build of it. The SDK attaches the proof automatically — you write none of it — but there are a couple of invisible requirements that trip everyone up. This page is the model, the gotchas, and how to register.
The model
The SDK sends an X-Bundle-Id header on every request, plus a platform-specific proof token. Onelo verifies both against what you've registered.
Web apps aren't attested — they're gated by Allowed domains instead. A server (Python/Node/PHP) does no client attestation at all — its trust is the secret key, kept server-side.
Availability by platform
Attestation is live and verified end-to-end on Apple platforms today. The other native platforms are on the way — until then, don't rely on client attestation for them.
| Platform | Mechanism | Status |
|---|---|---|
| iOS | Apple App Attest (X-Attest-Token) | Available |
| macOS | Apple App Attest (with the entitlement), else a codesign fingerprint (X-Codesign-Fingerprint) | Available |
| Android | Google Play Integrity (X-Integrity-Token) — full server-side verdict + signing-cert checks, enforced | Available |
| React Native · Flutter | App Attest (iOS side) + Play Integrity (Android side) — in progress, shipping soon | Coming soon |
| Electron (Windows / macOS) | A codesign fingerprint (X-Codesign-Fingerprint) per signed OS. Linux is unsigned → falls back to the Bundle ID + secret key. | Available |
| Web | — (use Allowed domains) | N/A |
| Server (Python/Node/PHP) | — (trust is the secret key) | N/A |
The gotchas that waste the most hours
App Attest needs the Secure Enclave, which the Simulator doesn’t have: DCAppAttestService.isSupported == false there, so the SDK silently skips it and nothing registers. Run on a physical iPhone/iPad. You also need the App Attest capability on the target (Xcode → Signing & Capabilities).
Most macOS apps (SwiftPM / Developer ID / adhoc) don’t carry the App Attest entitlement, so they fall back to codesign — which needs a properly signed build (Developer ID or Apple Development). An unsigned / adhoc build produces no fingerprint → no attestation, silently.
Registration only happens while a discovery window is open (see below) and the app runs on a real device. No window → nothing registers, even though the app runs fine.
The SDK never throws for a missing/unsupported attestation — it just sends what it can. That’s why “it doesn’t work” is usually one of the three above, not a code bug.
Registering your app — the discovery window
Registration is trust-on-first-use. You open a short window, run the app once, and the bundle registers itself — no hand-typing (a typed Bundle ID doesn't count for going live).
| Step | What you do |
|---|---|
| 1 | In the dashboard, open your app → Security → App Identity and open a discovery window. |
| 2 | Run your app on a real device (iOS) or a signed build (macOS) with the SDK initialised. |
| 3 | The SDK submits its attestation; Onelo verifies it and auto-registers the bundle / fingerprint. The App Identity panel updates live. |
Multi-target apps — register each platform separately
A multi-target app (an Electron app shipping macOS and Windows) registers each platform on its own — from that platform's machine, at its own time. You do not need them all running in one window (you rarely have a Mac and a Windows box open at once).
| When | What happens |
|---|---|
| Today, on your Mac | Open a discovery window → run the macOS build → the macOS signing cert auto-registers → the window closes. |
| Later, from Windows | Re-open the window → run the Windows build → the Windows cert co-registers alongside the macOS one (added to the set — it never replaces it). |
Electron across Windows, macOS & Linux
A common question: shipping one Electron app to Windows, macOS and Linux — do you need a separate app or key per OS? No. One app, one key, everywhere. Only one thing is per-OS, and it isn't what most people expect.
| Aspect | How it works across OS |
|---|---|
| One key = every module | Auth, store, customer portal, waitlist, features, feedback, roadmap and monitor are OS-agnostic HTTPS calls. One publishable key, one OneloConfig, the same code — works on Windows, macOS and Linux with nothing per-OS. |
| Identity = the install, not the OS | Each installation gets a per-install UUID (X-Onelo-Instance-Id). A Windows install, a Mac install and a Linux install are three different instances — the backend binds realtime, feature discovery (trust-on-first-use per app + instance) and monitor by instance, not by OS. |
| Users are cross-instance | Subscription, entitlement and feature targeting follow the account, not the install — a purchase on Windows unlocks on the same account’s Mac. |
| Codesign is the only per-OS part | macOS (Developer ID) and Windows (Authenticode) sign with different certificates → different fingerprints. Linux is unsigned, so codesign is inert (fail-open) and integrity rests on the Bundle ID + your backend’s secret key. |
Your codesign fingerprints are a set — one app, one fingerprint per signed OS, and enforcement is simply “is this cert in the set?”. Your Bundle ID (the Electron appId, e.g. com.company.app) is the same on every OS, so it's registered once.
bundleId in your Electron config — this trips people up. The SDK only sends X-Bundle-Id when you pass bundleId. On a signed build under enforcement, if you omit it every bundle-gated request gets bundle_id_mismatch (403) and auth silently fails. Two separate layers: (a) registering the bundle on Onelo's side is automatic (no manual entry in the dashboard), but (b) you still must provide bundleId in the SDK config. (On iOS/macOS/Android the id comes from native attestation automatically; Electron reads it from your config.) SDK ≥ 0.36.5 warns at startup if a signed build has no bundleId.const auth = new OneloElectronAuth({
publishableKey: 'onelo_pk_live_YOUR_KEY',
apiUrl: '…', // your Onelo API URL, from the dashboard SDK page
protocol: 'myapp',
bundleId: 'com.company.app', // REQUIRED for X-Bundle-Id — must match your signed appId
})In the dashboard (App → Security), the Codesign fingerprints section is grouped per OS — a macOS card and a Windows card, each with its certs, status, and the exact command to extract a fingerprint. An Unassigned card holds any cert registered before OS tagging, with a “Set OS” dropdown. The OS is filled automatically when the SDK reports it (via X-Codesign-Platform), or by hand when you add a cert.
| OS | Extract the signing-cert fingerprint |
|---|---|
| macOS | codesign -d --extract-certificates … then shasum -a 256 |
| Windows | Get-AuthenticodeSignature … .SignerCertificate.GetCertHashString() |
| Linux | — unsigned; no codesign attestation (relies on Bundle ID + secret key). |
How Apple App Attest works
For the curious — this is Apple's three-step model, which Onelo verifies server-side. You don't call any of it; the SDK does.
| Step | When | What happens |
|---|---|---|
generateKey() | Once per install | Generates a key pair; the private key stays in the device’s Secure Enclave (hardware) and never leaves. |
attestKey() | Once per key | Sends the attestation; the server validates the certificate chain up to Apple’s App Attestation Root CA, reconstructs the nonce from the challenge, and extracts the public key — then registers the bundle (Onelo: trust-on-first-use, in the discovery window). |
generateAssertion() | Each later request | Signed on-device (Apple’s servers no longer involved); the server verifies the signature with the stored public key plus a counter, to stop replay. |
generateAssertion proof on every request, and the server verifies it (signature, single-use challenge, replay counter). You don't wire anything — it rides along with the calls you already make.