Skip to main content
After registration, every outbound request to the Synheart cloud must be signed by the device’s hardware key. This is the SDK-side reference for RFC-AUTH-MOBILE-0001 §8.

Public API

signRequest(
  appId: String,
  method: String,
  path: String,
  bodyBytes: ByteArray,
) -> SignedHeaders
Dart, Kotlin, and Swift expose the same shape. The SDK generates the timestamp and nonce internally.

Message construction (normative)

The signed message MUST be:
method          = uppercase HTTP method, ASCII   (e.g. "POST")
path            = request path, ASCII             (e.g. "/ingest/v1/hsi")
                  no query string
timestamp_str   = Unix seconds, ASCII decimal     (e.g. "1709312345")
raw_body_bytes  = exact HTTP request body bytes on the wire
                  (empty bytes for GET / no body)
The bytes signed:
message = method || "\n" || path || "\n" || timestamp_str || "\n" || raw_body_bytes
signature = ECDSA-P256-SIGN(private_key, SHA256(message))
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

HeaderValueNotes
X-App-IDstringThe app_id provisioned in the dashboard.
X-Device-IDUUIDIssued at registration.
X-Synheart-SignatureBase64(ASN.1 DER)ECDSA-P256 over SHA256(message).
X-Synheart-TimestampUnix secondsDecimal ASCII, e.g. "1709312345".
X-Synheart-NonceUUID v4Unique per request, never reused.
X-Synheart-Sig-Version"1"Signature scheme version. Future evolution may bump.
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 (RFC §8.4)

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

class SignedHeaders {
  final String appId;
  final String deviceId;
  final String signature;
  final String timestamp;
  final String nonce;
  final String signatureVersion;  // default "1"

  Map<String, String> toMap();
}
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.
HeaderUsed by
X-Synheart-Signature (this SDK)Direct API calls from your app to Synheart endpoints.
X-Synheart-Proof (runtime)HSI ingest from the Synheart runtime. See Cloud Protocol.

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

Allowed (prod)Never (prod)
app_idRaw signatures
device_id (truncated/hashed)Raw request bodies
Error codesAttestation proof payloads
Latency metricsPublic key material (debug builds only with opt-in)
Rotation eventsClock offset values
State transitions
  • Registration — how the public key reaches the server.
  • ErrorsCLOCK_SKEW, NONCE_REPLAY, KEY_INVALIDATED recovery.
  • Cloud ProtocolX-Synheart-Proof for HSI ingest.