> ## 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 Flow

> Challenge → keygen → attestation → register, per RFC-AUTH-MOBILE-0001 §7

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)

```text theme={null}
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

```http theme={null}
POST /auth/v1/device/challenge
Content-Type: application/json

{ "app_id": "com.example.app" }
```

Response:

```json theme={null}
{
  "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

| 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}` |

The key MUST be **non-exportable**. The Dart SDK never sees the private key; only `signRequest` results.

## 3. Compute binding nonce

```text theme={null}
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

| Platform | API                                           | Input                                                         |
| -------- | --------------------------------------------- | ------------------------------------------------------------- |
| iOS      | `DCAppAttestService.attestKey(...)`           | `keyId` from key generation, `clientDataHash = binding_nonce` |
| Android  | `IntegrityManager.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

```http theme={null}
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`:

| 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              |

**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

| 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`:

```text theme={null}
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` on the platform-side app record.
* 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.

## Related

* [Request Signing](/synheart-auth/signing) — the contract used after registration.
* [State Machine](/synheart-auth/state-machine) — the states each step transitions through.
* [Errors](/synheart-auth/errors) — full error code taxonomy.
