Steps (normative)
1. Request a challenge
2. Generate the keypair
The key MUST be non-exportable. The Dart SDK never sees the private key; only
signRequest results.
3. Compute binding nonce
4. Attest with platform provider
Returns the attestation proof (Apple) or integrity token (Google). The SDK does not validate it — that’s the server’s job in step 7.
The Dart facade returns
AttestationUnavailable if the platform doesn’t support attestation.
5. Register with auth service
GETDELthe challenge from Redis (single-use, atomic).- Recompute
SHA256(challenge + base64(public_key))and compare with the attestation proof’s nonce field. - Verify proof against Apple/Google attestation APIs.
- Persist
(app_id, device_id, public_key, platform, status, registered_at). - Return
{ device_id, status }.
status is one of "registered" | "pending" | "rejected".
6. Persist locally
The SDK stores perapp_id:
Never persisted: private key bytes (live only in hardware), raw attestation tokens.
Idempotency
registerDevice(appId) is idempotent: subsequent calls return { status: alreadyRegistered, deviceId: <existing> } without touching the network. Call it at app start regardless — the SDK fast-paths when isRegistered(appId) is true.
Failure paths
Rotation flow (RFC §8.3)
rotateKey(appId) replaces the signing key while preserving the device_id:
Dev / QA bypass (RFC §14)
Attestation bypass exists for emulator/CI and is strictly controlled:- Server-side allowlist on
app_id+ build channel (dev/staging only). "development_integrity_allowed": trueon the platform-side app record.- Mobile SDK MUST set
X-Synheart-Dev-Mode: trueheader so the server tracks dev vs. prod traffic. - Bypass MUST be compile-time gated (
#if DEBUG/BuildConfig.DEBUG) — never runtime-toggleable.
X-Synheart-Dev-Mode: true is treated as a security incident.
Related
- Request Signing — the contract used after registration.
- State Machine — the states each step transitions through.
- Errors — full error code taxonomy.