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
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.
- Shared API keys across the fleet: Every device holds the same secret. Extracting it from one device gives access to all of them.
- Symmetric HMAC forgery: Any device that knows the shared HMAC key can forge messages as any other device. There is no sender authentication.
- No replay protection: Without nonces or timestamp windows, captured messages can be resubmitted indefinitely. The server cannot distinguish original from replayed.
- Revocation requires fleet-wide re-keying: When a device is compromised, new secrets must be pushed to every remaining device. This takes hours and creates a window of exposure.
- One device compromised = all devices compromised: The blast radius of a single breach is the entire fleet. Lateral movement is instant and unlimited.
Architecture
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.
Per-Device 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.
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
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.
// 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
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.
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
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
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
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
| 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
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).
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 }); });