Loading...

Machine-to-Machine Security

Per-device cryptographic identity for IoT fleets. No shared secrets, no key rotation, no propagation delays. Every device is its own trust boundary.

The Problem

Fleet Security Today

Most IoT deployments share a single API key or HMAC secret across every device in the fleet. This creates a catastrophic single point of failure: compromise one device, and the entire fleet is exposed.

Architecture

How It Works

Each device receives an Ed25519 keypair at commissioning. The public key is encoded as a Decentralized Identifier (DID) in the format did:key:z6Mk.... This DID is the device's identity — unique, self-certifying, and cryptographically unforgeable.

Messages are signed, not HMAC'd. The server verifies the Ed25519 signature against the sender's DID. It then checks the DID against a trust registry that maps DIDs to permission scopes. No shared secrets exist anywhere in the system.

Revocation is instant: remove the DID from the registry, and the next message from that device is rejected. No secrets to push, no propagation delay, no window of exposure.

Device sends telemetry
Device (Ed25519 DID)
↓ SIGN
Signed Envelope
↓ SEND
Server verifies signature
↓ CHECK
Trust Registry (DID → permissions)

Per-Device Identity

Identity

An Ed25519 keypair is generated at commissioning — during factory provisioning, QR code scanning, or first boot. The public key is encoded as a DID: did:key:z6Mk....

There are no shared API keys. No fleet-wide secrets. Each device is cryptographically unique. The private key never leaves the device. The DID is registered in the trust registry with a specific permission scope.

If a device is factory-reset or decommissioned, a new keypair is generated. The old DID is revoked. The new DID is registered. No other device is affected.

commissioning.ts
import { Agent } from '@private.me/agent-sdk';

// Commissioning: one keypair per device
const device = await Agent.create();

device.did  // did:key:z6MkhaXg...
// Unique to this device. Cannot be forged.
// No shared secrets. No API keys.

Signed Envelopes

Envelope

Asymmetric Ed25519 signatures replace symmetric HMAC. Every message carries cryptographic proof of which specific device sent it. The signature is bound to the payload — any modification invalidates it.

Unlike HMAC, where any holder of the shared key can forge a valid tag, Ed25519 signatures can only be produced by the device that holds the private key. Even devices on the same network, in the same building, running the same firmware — cannot forge each other's messages.

send-telemetry.ts
// Every message is signed by the device's Ed25519 key
const envelope = await device.send({
  to: serverDid,
  payload: {
    type: 'telemetry',
    unit: 'DEVICE-104',
    event: 'door.unlocked',
    timestamp: Date.now()
  }
});
// envelope.signature = Ed25519(device.privateKey, payload)
// Server verifies: Ed25519.verify(envelope.signature, device.did)

Replay Protection

Nonce Store

Every envelope includes a 128-bit cryptographic nonce generated via crypto.getRandomValues() and a timestamp. The server-side NonceStore — backed by Redis or an in-memory Map — records every nonce it has seen.

A 30-second timestamp window rejects messages that arrive too late, preventing delayed replay. The nonce is single-use: even within the valid window, a duplicate nonce is rejected immediately.

The combination of cryptographic nonce + timestamp window + single-use enforcement makes replay attacks impossible, even if an attacker captures legitimate traffic off the wire.

nonce-store.ts
import { RedisNonceStore } from '@private.me/agent-sdk';

const store = new RedisNonceStore(redisClient, {
  ttl: 60  // seconds
});

// Server-side: verify nonce is unique
const valid = await store.check(envelope.nonce);
// true  = first time seen, proceed
// false = replay attempt, reject

Trust Registry & Revocation

Trust

When a device is compromised, remove its DID from the trust registry. The effect is immediate — the very next message from that DID is rejected. No new secrets need to be pushed to any other device. No certificate revocation lists. No OCSP stapling. No propagation delay.

The trust registry supports three backends: MemoryTrustRegistry for development, HttpTrustRegistry for distributed deployments, and DidWebTrustRegistry for standards-compliant resolution. All three implement the same interface. All three enforce the same rule: if the DID is not in the registry, the message is rejected. Fail-secure by default.

Revocation latency: zero. Remove the DID, the device is locked out on the next message. No certificate propagation, no CRL distribution, no OCSP stapling.

“Three backends — in-memory for development, HTTP for production, and did:web for decentralized resolution — all behind the same interface. We started with HTTP, but knowing we can move to self-hosted did:web identities without changing application code gives us a migration path we didn't even know we wanted.”

— Vault Engineering Team

Scope Graph

Permissions

Each DID maps to a specific set of allowed operations. A sensor on Floor 3 has the scope floor3.sensor.* — it can report events for that zone and nothing else. A compromised sensor cannot read data from Floor 4, cannot send commands to the HVAC system, cannot access the building management network.

The blast radius of a compromised device is exactly one device's scope. Not the fleet. Not the building. One unit, with a maximum exposure window of 30 seconds before the nonce TTL expires.

Property Shared Secrets Per-Device DID
Permission model Flat (all or nothing) Granular (per-device scopes)
Blast radius Entire fleet Single device
Lateral movement Unlimited Impossible (scope-bound)
Time to contain Hours (re-key fleet) Seconds (revoke DID)

Non-Repudiable Audit

Audit Trail

Every message is Ed25519-signed. The audit log stores the envelope including the signature, the sender DID, and the payload hash. Even if the database is tampered with after the fact, the signatures remain independently verifiable against the device's public key.

This is non-repudiable: the device cannot deny sending the message, and no other entity could have produced a valid signature for that DID. For regulatory compliance (SOC 2, ISO 27001, NIST 800-53), the audit trail provides cryptographic proof of exactly which device performed which action, at what time, with what payload.

With shared HMAC keys, any device in the fleet could have generated the tag. The audit trail is meaningless — you know a valid key holder sent the message, but not which one. Ed25519 eliminates this ambiguity entirely.

Before vs After

Comparison
Property Before (Shared Secrets) After (PRIVATE.ME Agent SDK)
Secrets SHARED
One key for all devices
PER-DEVICE
Ed25519 keypair per device
Blast radius FLEET-WIDE
One device = all devices
SINGLE DEVICE
Scope-bound DID
Forgery POSSIBLE
Any device can forge
IMPOSSIBLE
Ed25519 non-repudiation
Replay UNPROTECTED
No nonce, no timestamp
PROTECTED
128-bit nonce + 30s window
Revocation HOURS
Push new secrets to fleet
INSTANT
Remove DID from registry

From a fleet security team that migrated from shared secrets:

What we eliminated in one integration:

  • Three shared secrets (API key, MQTT credentials, HMAC signing key) — replaced by per-device Ed25519 identity
  • Fleet-wide blast radius — a compromised device can now only act within its own scoped permissions
  • Hours-long revocation — now one registry call, effective on the next message
  • Forgeable audit logs — every entry is Ed25519-signed, non-repudiable
  • Custom replay protection — built-in nonce store with configurable timestamp windows

— Vault Engineering Team

Constrained Devices

Compatibility

The SDK requires Web Crypto API (Node 18+, Deno, browsers, Cloudflare Workers). For devices without Web Crypto — microcontrollers, embedded Linux < Node 18, or constrained runtimes — two patterns are recommended:

Gateway pattern: The device sends raw telemetry to a gateway service running the SDK. The gateway signs, encrypts, and delivers via Xail on behalf of the device. Use GatewayTransport from the SDK for this integration.

Sidecar pattern: A co-located process with Web Crypto handles all cryptographic operations. The device communicates with the sidecar via IPC (Unix socket, named pipe, or localhost HTTP).

gateway.ts
import { Agent, GatewayTransport } from '@private.me/agent-sdk';

// Gateway runs the SDK on behalf of constrained devices
const gateway = await Agent.create({
  name: 'sensor-gateway',
  registry, transport,
});

// Device sends raw data to gateway via HTTP/MQTT
app.post('/ingest', async (req, res) => {
  const payload = req.body;
  await gateway.send({
    to: backendDid,
    payload,
    scope: 'telemetry:write',
  });
  res.json({ ok: true });
});