Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt

Use this file to discover all available pages before exploring further.

Registration is a one-time-per-(app_id, device) handshake that establishes a hardware-backed device identity with the auth service. After it completes, the SDK can sign every outgoing request without further server round-trips.

Steps (normative)

1. SDK         → POST /auth/v1/device/challenge { app_id }
2. SDK         ← { challenge, expires_at, ttl_seconds }
3. SDK gen     keypair in hardware (alias: synheart_auth_{app_id})
4. SDK compute binding_nonce = SHA256(challenge + base64(public_key))
5. SDK attest  with binding_nonce (App Attest / Play Integrity)
6. SDK         → POST /auth/v1/device/register
                  { app_id, public_key, challenge, platform, proof,
                    device_local_id? }
7. Server      verify challenge (GETDEL), recompute nonce, verify proof,
                  store (app_id, device_id, public_key, platform)
8. SDK         ← { device_id, status }
When used through Synheart Core, steps 1, 6, and 8 are performed by the Synheart runtime, which calls the SDK’s crypto callbacks for steps 3, 4, 5.

1. Request a challenge

POST /auth/v1/device/challenge
Content-Type: application/json

{ "app_id": "com.example.app" }
Response:
{
  "challenge": "<base64, ≥32 bytes>",
  "expires_at": "<ISO8601>",
  "ttl_seconds": 90
}
Challenge TTL is 90 seconds (RFC §7.3). Single-use, server-enforced. If the SDK fails to complete steps 2–7 within the TTL, it must request a new challenge.

2. Generate the keypair

PlatformAlgorithmStorageAlias
iOSECDSA P-256Secure Enclave (SecKey with kSecAttrTokenIDSecureEnclave)synheart_auth_{app_id}
AndroidECDSA P-256Android Keystore (KeyGenParameterSpec with hardware backing)synheart_auth_{app_id}
The key MUST be non-exportable. The Dart SDK never sees the private key; only signRequest results.

3. Compute binding nonce

binding_nonce = SHA256( challenge_bytes || base64(public_key_bytes) )
This binds the attestation proof to the specific public key the SDK is registering. The server recomputes the nonce in step 7 and rejects mismatches — preventing attestation reuse with a different key.

4. Attest with platform provider

PlatformAPIInput
iOSDCAppAttestService.attestKey(...)keyId from key generation, clientDataHash = binding_nonce
AndroidIntegrityManager.requestIntegrityToken(...)nonce = binding_nonce
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

POST /auth/v1/device/register
Content-Type: application/json

{
  "app_id":          "com.example.app",
  "public_key":      "<base64 P-256 public key, X.509 SPKI form>",
  "challenge":       "<the same challenge from step 1>",
  "platform":        "ios" | "android",
  "proof":           "<attestation token>",
  "device_local_id": "<optional UUID for local correlation, non-authoritative>"
}
Server verifies (RFC §7.2.6):
  1. GETDEL the challenge from Redis (single-use, atomic).
  2. Recompute SHA256(challenge + base64(public_key)) and compare with the attestation proof’s nonce field.
  3. Verify proof against Apple/Google attestation APIs.
  4. Persist (app_id, device_id, public_key, platform, status, registered_at).
  5. Return { device_id, status }.
status is one of "registered" | "pending" | "rejected".

6. Persist locally

The SDK stores per app_id:
FieldiOSAndroid
device_idKeychainEncryptedSharedPreferences
Key alias / handleSecure EnclaveKeystore
platformLocal storageLocal storage
registered_atLocal storageLocal storage
key_rotated_atLocal storageLocal storage
clock_offset_msLocal storageLocal storage
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

FailureSDK behaviorRecovery
Challenge request 5xx / networkNETWORK_ERROR with backoffRetry.
Challenge expiredCHALLENGE_EXPIREDRe-fetch challenge.
Attestation unsupportedATTESTATION_UNAVAILABLECannot proceed without dev-mode bypass.
Attestation API rejectsATTESTATION_FAILEDRetry once, then surface.
Register 4xx with INVALID_CHALLENGEserver says nonce mismatchRe-fetch challenge and restart.
Register 4xx with INVALID_ATTESTATIONserver rejects proofSurface as ATTESTATION_FAILED.
Register 5xx / networkNETWORK_ERRORBackoff + retry; always with a fresh challenge.
Already registeredALREADY_REGISTEREDNo-op.
Registration in progressREGISTRATION_IN_PROGRESSCaller already racing — wait.

Rotation flow (RFC §8.3)

rotateKey(appId) replaces the signing key while preserving the device_id:
1. Generate new keypair: synheart_auth_{app_id}_next
2. Sign rotation request with the CURRENT key:
     POST /auth/v1/device/rotate-key
     Body: { app_id, device_id, new_public_key }
3. Server verifies signature against current public key
4. Server atomically replaces stored public key
5. Server returns { status: "rotated", effective_at: <unix> }
6. SDK promotes:
   - delete old key from hardware
   - rename _next → primary alias
   - update key_rotated_at
7. On any failure, keep the old key active and retry later
Rotation policy (RFC §8.3): SDK SHOULD rotate every 90 days; MUST rotate immediately on platform key-compromise signals; server MAY reject keys older than its configured maximum age.

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": true in platform-service.
  • Mobile SDK MUST set X-Synheart-Dev-Mode: true header so the server tracks dev vs. prod traffic.
  • Bypass MUST be compile-time gated (#if DEBUG / BuildConfig.DEBUG) — never runtime-toggleable.
There are no hidden bypasses. A production build that emits X-Synheart-Dev-Mode: true is treated as a security incident.