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.
| Header | Value | Notes |
|---|
X-App-ID | string | The app_id provisioned in the dashboard. |
X-Device-ID | UUID | Issued at registration. |
X-Synheart-Signature | Base64(ASN.1 DER) | ECDSA-P256 over SHA256(message). |
X-Synheart-Timestamp | Unix seconds | Decimal ASCII, e.g. "1709312345". |
X-Synheart-Nonce | UUID v4 | Unique 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:
- Extract the six headers; reject if any required header is missing.
- Reject if
abs(server_time - X-Synheart-Timestamp) > 300.
- For write endpoints, reject if
(device_id, nonce) has been seen in the freshness window.
- Reconstruct the message:
method + "\n" + path + "\n" + timestamp + "\n" + body_bytes.
- Look up the public key by
(app_id, device_id).
- Verify ECDSA P-256 signature over
SHA256(message).
- Store the nonce with a 300-second TTL (write endpoints).
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:
- Core constructs a
DeviceAuthProvider over SynheartAuth.
- Core registers the auth provider’s crypto callbacks with the Synheart runtime.
- 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.
- For non-runtime traffic, hosts call
SynheartAuth.signRequest(...) directly and attach the six headers themselves.
| Header | Used 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_id | Raw signatures |
device_id (truncated/hashed) | Raw request bodies |
| Error codes | Attestation proof payloads |
| Latency metrics | Public key material (debug builds only with opt-in) |
| Rotation events | Clock offset values |
| State transitions | |
- Registration — how the public key reaches the server.
- Errors —
CLOCK_SKEW, NONCE_REPLAY, KEY_INVALIDATED recovery.
- Cloud Protocol —
X-Synheart-Proof for HSI ingest.