Skip to content

Device integration

A device authenticates by signing every request with its private key — there is no login step and no stored session. Hiveron uses a deliberately strict subset of RFC 9421 (the "IoTNet v2" signature profile). The older base64 bearer-token scheme is no longer accepted.

The request signature

Every call to an /iot/* endpoint carries these three headers:

Content-Digest: sha-256=:BASE64_SHA256_OF_EXACT_BODY:
Signature-Input: sig1=("@method" "@target-uri" "content-digest");created=EPOCH;expires=EPOCH+300;nonce="32_LOWERCASE_HEX";keyid="device:ID:VERSION";alg="ecdsa-p256-sha256";tag="iotnet-v2"
Signature: sig1=:BASE64_64_BYTE_R_THEN_S:

keyid is device:<id>:<key_version> (both shown in the console). The signature is raw big-endian r‖s (32 bytes each), base64-encoded — not ASN.1 DER. GET requests still send the digest of an empty body.

What gets signed

The signature is computed over this exact, newline-separated base (no trailing newline). Component and parameter order is exact; append "idempotency-key" when that header is present, then "range" when a Range header is present:

"@method": POST
"@target-uri": https://api.hiveron.net/iot/telemetry
"content-digest": sha-256=:BASE64_DIGEST:
"idempotency-key": 3f8a1c72-...-uuid
"@signature-params": ("@method" "@target-uri" "content-digest" "idempotency-key");created=EPOCH;expires=EPOCH+300;nonce="32_LOWERCASE_HEX";keyid="device:ID:VERSION";alg="ecdsa-p256-sha256";tag="iotnet-v2"

Sign the UTF-8 base with ECDSA P-256 / SHA-256. The @target-uri is the public HTTPS URL plus the exact path and query — Host/forwarded headers can't select another origin, and redirects aren't supported.

How the server verifies it

  • The signature is checked against the device's registered public key at the given key version.
  • A fresh random 128-bit nonce is required per attempt; nonces are recorded and cannot be replayed (even across restarts).
  • created may be at most 30 seconds in the future; the signature expires within 5 minutes.
  • The device's key version and active state are checked in the same transaction as the operation.

The device needs a reasonably accurate clock. GET /time ({ "unix_ms": N }) is advisory — synchronize from trusted infrastructure, not solely from it.

The device loop

Devices poll: check for firmware, post telemetry, and drain queued commands. Commands are a peek / acknowledge queue — reading one does not consume it; acking does. Telemetry retries must reuse the same Idempotency-Key and identical body (a different body under the same key returns 409).

pseudocode
# The device SIGNS EVERY request (fresh nonce + timestamp each time).

# 1) firmware
GET  /iot/fw_check            # -> { fw_id, size, sha256 }  (all null if none)
if fw_id changed:
    GET /iot/fw_download      # signed; raw bytes -> flash & reboot

# 2) telemetry  (UUID Idempotency-Key required)
POST /iot/telemetry
  Idempotency-Key: <uuid>
  { "content": "<reading>" }  # -> 204   (keep the same UUID + body on retries)

# 3) commands  (peek / acknowledge mailbox)
n = GET /iot/command_check    # -> { count }
while n > 0:
    cmd = GET /iot/command    # -> { id, content }  (peek oldest, or 204)
    execute(cmd.content)
    POST /iot/command_ack { "id": cmd.id }   # -> 204   (409 if expired)
    n -= 1

Once the loop is running, the device's own page in the console shows its readings arriving and whether it is on the latest firmware:

app.hiveron.net/admin/devices/4
A device page: the firmware card with an “update available” badge, the keys card, the telemetry chart, and the recent-readings list.

Ready-made C helper

You don't have to implement the signing yourself. We provide a dependency-light C reference — create_request_headers() — that produces the three headers above. It bundles micro-ecc for P-256 (ESP32-friendly); you supply platform randomness, a trusted clock, and TLS validation. Using it is one call:

c
#include "device_token.h"

/* Provisioned on the device; the console shows dev_id and key_version. */
static const uint64_t dev_id      = 1;
static const uint64_t key_version = 1;
static const uint8_t  priv[32]    = { /* 32-byte P-256 private key */ };

void post_telemetry(void) {
    const uint8_t body[] = "{\"content\":\"21.4C\"}";

    /* Your platform supplies a trusted clock, a fresh 32-hex-char nonce,
       and a UUID idempotency key. */
    device_headers h;
    if (create_request_headers(
            dev_id, key_version, priv,
            now_epoch_seconds(), nonce_hex,
            "POST", "https://api.hiveron.net/iot/telemetry",
            body, sizeof(body) - 1,
            idempotency_uuid, NULL, &h) != 0) {
        return; /* invalid input */
    }

    /* Send these three as request headers over your TLS client: */
    /*   Content-Digest:  h.content_digest                        */
    /*   Signature-Input: h.signature_input                       */
    /*   Signature:       h.signature                             */
}