Skip to main content
After registration, every outbound request to the Synheart cloud must be signed by the device’s hardware key.

Public API

Dart, Kotlin, and Swift expose the same shape. The SDK generates the timestamp and nonce internally.

Message construction (normative)

The signed message MUST be:
The bytes signed:
For Synheart Core’s cloud ingest, the runtime strips the /ingest/ prefix from POSTs to /ingest/v1/... so signatures cover /v1/.... If you build clients that talk to those endpoints from outside Core, mirror this prefix-stripping rule.
The signature is encoded as Base64(ASN.1 DER). The runtime native bridge accepts compact r||s (64 bytes) from its sign_bytes callback and the SDK wraps that in DER for the wire header.

Six required headers

The Dart SignedHeaders.toMap() returns exactly these six keys.

Nonce policy

  • UUID v4. Generated fresh per request; never reused across requests for the same (app_id, device_id).
  • Server enforces replay protection on write endpoints (POST, PUT, PATCH, DELETE) within the 300-second freshness window.
  • Replay protection is RECOMMENDED for read endpoints.
  • Server stores nonces with a 300-second TTL keyed by (device_id, nonce).

Timestamp policy

  • Server-side freshness window: 300 seconds.
  • SDK clock may drift; correctClockSkew(serverTimestamp) learns an offset from CLOCK_SKEW errors and applies it on subsequent signs.
  • The SDK persists clock_offset_ms in local storage.

Server-side verification

Backends that verify these signatures follow:
  1. Extract the six headers; reject if any required header is missing.
  2. Reject if abs(server_time - X-Synheart-Timestamp) > 300.
  3. For write endpoints, reject if (device_id, nonce) has been seen in the freshness window.
  4. Reconstruct the message: method + "\n" + path + "\n" + timestamp + "\n" + body_bytes.
  5. Look up the public key by (app_id, device_id).
  6. Verify ECDSA P-256 signature over SHA256(message).
  7. Store the nonce with a 300-second TTL (write endpoints).

SignedHeaders type

Kotlin and Swift mirror this.

Sig-Version

X-Synheart-Sig-Version is "1". The header is versioned so the signing scheme can evolve without breaking deployed clients.

Wiring through Synheart Core

When synheart-core consumes synheart-auth:
  1. Core constructs a DeviceAuthProvider over SynheartAuth.
  2. Core registers the auth provider’s crypto callbacks with the Synheart runtime.
  3. The runtime’s request-signing path builds X-Synheart-Proof (a compact JWS bound to the request URL+method) for HSI ingest. This is a different header from X-Synheart-Signature — both are hardware-backed by the same ECDSA P-256 key but cover different surfaces.
  4. For non-runtime traffic, hosts call SynheartAuth.signRequest(...) directly and attach the six headers themselves.

What is not signed

  • Query string components (signing is over path only). Servers that include sensitive parameters in query strings are responsible for separate validation.
  • Headers other than the six listed above. The body and the path/method are the only signed surface.

Logging policy

  • Registration — how the public key reaches the server.
  • Errors — CLOCK_SKEW, NONCE_REPLAY, KEY_INVALIDATED recovery.
  • Cloud Protocol — X-Synheart-Proof for HSI ingest.