Steps (normative)
1. Request a challenge
2. Generate the keypair
| Platform | Algorithm | Storage | Alias |
|---|---|---|---|
| iOS | ECDSA P-256 | Secure Enclave (SecKey with kSecAttrTokenIDSecureEnclave) | synheart_auth_{app_id} |
| Android | ECDSA P-256 | Android Keystore (KeyGenParameterSpec with hardware backing) | synheart_auth_{app_id} |
signRequest results.
3. Compute binding nonce
4. Attest with platform provider
| Platform | API | Input |
|---|---|---|
| iOS | DCAppAttestService.attestKey(...) | keyId from key generation, clientDataHash = binding_nonce |
| Android | IntegrityManager.requestIntegrityToken(...) | nonce = binding_nonce |
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:
| Field | iOS | Android |
|---|---|---|
device_id | Keychain | EncryptedSharedPreferences |
| Key alias / handle | Secure Enclave | Keystore |
platform | Local storage | Local storage |
registered_at | Local storage | Local storage |
key_rotated_at | Local storage | Local storage |
clock_offset_ms | Local storage | Local storage |
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
| Failure | SDK behavior | Recovery |
|---|---|---|
| Challenge request 5xx / network | NETWORK_ERROR with backoff | Retry. |
| Challenge expired | CHALLENGE_EXPIRED | Re-fetch challenge. |
| Attestation unsupported | ATTESTATION_UNAVAILABLE | Cannot proceed without dev-mode bypass. |
| Attestation API rejects | ATTESTATION_FAILED | Retry once, then surface. |
Register 4xx with INVALID_CHALLENGE | server says nonce mismatch | Re-fetch challenge and restart. |
Register 4xx with INVALID_ATTESTATION | server rejects proof | Surface as ATTESTATION_FAILED. |
| Register 5xx / network | NETWORK_ERROR | Backoff + retry; always with a fresh challenge. |
| Already registered | ALREADY_REGISTERED | No-op. |
| Registration in progress | REGISTRATION_IN_PROGRESS | Caller already racing — wait. |
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.