Whisper · Docs
Energy · Recipes

Telemetry · egress · attribution

Six copy-paste recipes put a forge-proof, revocable network identity under a DER: bind it to the LFDI it already carries, prove it keyless, back-trace whoever is dispatching it, govern exactly what it may reach, see who is enumerating your fleet, and sign its telemetry so the ISO trusts the numbers. Each runs today against shipped primitives.

They share one primitive: a routable IPv6 /128 that a DER derives from a key it already holds, DNSSEC-signed and DANE-pinned so anyone can verify it and nobody can forge it. Everything below is built from shipped parts: the device /128, keyless verify, the attribution graph, per-device egress governance, the transparency log, and one-call revoke. Where a step is a pattern you assemble rather than a single command, or a connector still on the roadmap, it says so in plain words.

What's shipped, stated honestly. The DER /128 derivation, keyless verify / RDAP, the attribution graph (whisper.identify), per-device egress governance (policy · firewall · budget · revoke), the reverse-observability op:lookups, and the Merkle transparency log are all live. Two caveats these recipes are written around: there is no first-class --lfdi CLI flag yet, so DER provisioning is shown via the control-plane API (which is live) with the LFDI passed as the generic device_id; and the SIEM/threat-intel export connectors named at the end are roadmap beyond Splunk, which ships. Whisper anchors the cloud/IP boundary: it is not on the DNP3 / IEC 61850 substation bus, not inside the SunSpec-Kyrio device PKI, and not in the ISO 15118 charging handshake. Each recipe names exactly where it stops.

The shared primitive: an identity a DER derives, not one you hand it

An inverter, a battery gateway, or an EVSE already carries a hardware key from the factory: the SunSpec/Kyrio device certificate behind its IEEE 2030.5 LFDI, a secure-element key, or a TPM. Whisper takes only its public SubjectPublicKeyInfo (SPKI) and, together with the LFDI as a domain separator, deterministically derives a /128 under 2a04:2a01::/32 (AS219419). The private key never leaves the device; the server only ever computes a public address.

The LFDI is a genuinely good starting point: it is the leftmost 160 bits of SHA-256 over the device's own X.509 certificate, so the grid already hashes each device's key material into a unique name. The trouble is that name lives in a private SERCA→MCA→MICA root that is life-long, no CRL, no OCSP: not addressable, not publicly verifiable, never revocable. The derivation below keeps the one good property and adds the four the private root withholds:

The LFDI is the public fingerprint; the /128 is its cryptographic counterpart. Because the address is bound to the device's key and the LFDI, the LFDI alone yields nothing: you cannot go LFDI → /128 without the key, and RDAP/reverse-DNS return the registry object, never a directory of your fleet. Every check in the recipes below that doesn't provision is keyless: no account, just DNS and TLS any counterparty already has.

Recipe 1: Bind a DER to the LFDI it carries

Give an inverter the identity it should have been born with, derived from the key it already holds and named by the LFDI that already flows through every CSIP deployment. One call; the reply is the address, and a stock dig confirms the name.

Boundary. This complements the SunSpec-Kyrio managed PKI and the 2030.5 EndDevice certificate. It does not replace them or touch the DNP3 / IEC 61850 control bus. It takes the public half of the key that certificate is built on and makes it addressable, publicly verifiable, and revocable. You can even DANE-pin your existing 2030.5 head-end certificate to DNSSEC alongside it.

Pass the LFDI as the generic device_id. Re-running with the same device key + LFDI returns the same /128: it is idempotent, so it is safe to run from a provisioning loop:

# Bind an inverter to the IEEE 2030.5 LFDI it already carries. device_id = the LFDI.
# Re-running with the same device key + LFDI returns the SAME /128 (idempotent).
curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'connect'"'"', args:{tier:'"'"'wireguard'"'"', identity_public_key:'"'"'<base64 SPKI of the device key>'"'"', device_id:'"'"'3F2504E04F8911D39A0C0305E82C33016BA7F3C2'"'"'}}) YIELD op, ok, status, result, error RETURN op, ok, status, result, error"}' | jq .
{ "op":"connect", "ok":true, "status":200,
  "result":{ "address":"2a04:2a01:5e0::50c",
             "name":"lfdi-3f2504e0.der.example-vpp.whisper.online",
             "dane":"3 1 1", "dnssec":"signed", "wireguard":"<peer config>" } }

Confirm the /128 and its name from the other side, keyless: the reverse DNS is forward-confirmed (PTR ⇄ AAAA), so nothing but dig is required:

dig +short -x 2a04:2a01:5e0::50c
lfdi-3f2504e0.der.example-vpp.whisper.online.

# …and the forward AAAA resolves straight back: the round trip that makes it forge-proof:
dig +short AAAA lfdi-3f2504e0.der.example-vpp.whisper.online
2a04:2a01:5e0::50c

The mint is not issued in the dark. Every /128 minted and every one revoked lands in a public, append-only Merkle transparency log, so you and your regulator can audit the full issuance history of your fleet:

# The identity's ordered lifecycle (mint, any rotations, revoke), keyless:
curl -s https://whisper.online/ip/2a04:2a01:5e0::50c/transparency
i

A supplied LFDI is checked against a reused identity: the same device key with a different device_id on your tenant is a 409, and a non-string device_id is a clear 400. That's liberal in what it accepts, strict and unambiguous in what it commits. Honest status of the ledger: it is tamper-evident, Ed25519-signed, and Bitcoin-anchored via OpenTimestamps today; independent third-party witnessing is the next step. It already speaks the C2SP tlog-witness protocol so any external witness can co-sign. A first-class typed --lfdi argument is on the roadmap; today the LFDI rides the generic device_id, which is shipped.

Recipe 2: Prove a DER's identity, keyless

Once bound, any counterparty (a DERMS head-end before it accepts telemetry, an ISO before it dispatches, a peer aggregator settling a market position) proves the DER is real without an account, without a shared CA list, and without trusting Whisper as an authority. The proof re-derives against the IANA DNSSEC root.

Boundary. This authenticates the DER's network identity at the IP/transport boundary. It sits alongside the 2030.5 application-layer session, which keeps doing its job; it does not replace the CSIP handshake or the SunSpec PKI. What it adds is a proof that is publicly verifiable off-ecosystem: something the private SERCA→MCA→MICA root cannot give a counterparty who isn't inside the utility's own trust store.

The trustless check walks the full chain (DNSSEC to the IANA root, the DANE-EE pin, and the transparency-log entry) and prints exactly what it trusted:

# Prove any DER identity, trustless: re-derived against the IANA DNSSEC root,
# with no Whisper API trusted as an authority anywhere in the chain.
whisper verify --trustless 2a04:2a01:5e0::50c
dnssec   pass   DNSSEC-root   AAAA, PTR and TLSA(3 1 1) all DNSSEC-validated to the IANA root
dane     pass   DNSSEC-root   served leaf SPKI-SHA256 == TLSA pin
ledger   pass   DNSSEC-root   transparency-log entry present, signature verifies
CRYPTOGRAPHICALLY PROVEN · trust anchor: DNSSEC root (IANA) + DANE-EE · Whisper API NOT trusted

The same fact is reachable with tools already in every ops toolbox: one keyless HTTPS call, or plain dig:

# Is this address a real, DANE-anchored DER identity? One keyless call, no install:
curl -s https://whisper.online/verify-identity/2a04:2a01:5e0::50c
{"is_whisper_agent":true,"dane_ok":true,"jws_ok":true,
 "evidence":{"address":"2a04:2a01:5e0::50c",
   "ptr":"lfdi-3f2504e0.der.example-vpp.whisper.online.",
   "forward_aaaa":"2a04:2a01:5e0::50c"}}

# A counterfeit or cloned unit: no registered /128 for its key, no DANE pin it can present:
curl -s https://whisper.online/verify-identity/2a04:2a01::1
{"is_whisper_agent":false,"detail":"no Whisper agent identity anchors this address"}

A stolen 2030.5 session token buys an attacker nothing here. The identity is a network fact bound to a key, not a bearer string that can be lifted off the wire and replayed from another host.

Recipe 3: Back-trace a suspicious controller

Detection tools tell you that a DER is being dispatched oddly. When the source rotates across clouds and residential proxies, they lose who at the firewall. The attribution graph doesn't: it fingerprints the operator, not the disposable last IP. This is the keyed half: pass the suspicious egress address to whisper.identify over the public graph API.

Boundary. This is an enrichment query over Whisper's internet-infrastructure graph (BGP, DNS, WHOIS, TLS, hosting and threat-intel), not a tap on your OT network. It complements your OT sensor and SIEM; the egress IP is the one thing it never relies on.

what your OT SOC sees: a rotating, meaningless “last IP” Stolen token = fleet authority AWS eu-central 3.68.x.x GCP europe-w4 34.90.x.x Azure westeu 20.61.x.x residential-proxy swarm 71.x · Comcast 82.x · KPN 99.x · Orange JA4-identical tooling ASN · hosting · cert genealogy JA4 fingerprint One operator infra genealogy + JA4 / JA3 fingerprint evidence chain → your SIEM what the graph sees: one operator
Attribution survives rotation because it tracks the infrastructure and the tooling, not the ephemeral egress. Infrastructure genealogy collapses the cloud hops; a JA4 client fingerprint collapses the residential swarm.

The call itself is the graph API: there is no whisper identify CLI verb. Passing the address to whisper.identify over POST https://graph.whisper.security/api/query is the interface:

# Who really operates the controller behind a suspicious dispatch? The graph API.
curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.identify(\"34.90.x.x\")"}' | jq .
  operator:  <fingerprinted> · seen across AWS / GCP / Azure
  residential swarm collapsed by JA4: same tooling, 41 exit IPs → 1 operator
  evidence:  reproducible, replayable JSON for your SOC and an auditor

Cluster the rotation into one genealogy, then express the fleet-scale question, one source touching N distinct DER identities in a window, as read-only Cypher rather than a ticket:

# Cluster the rotating egress into one infrastructure genealogy, and timeline it:
curl -s https://graph.whisper.security/api/query -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.origins(\"34.90.0.0/16\") YIELD asn, prefix, first_seen RETURN asn, prefix, first_seen"}' | jq .
# whisper.walk(node, depth) expands the genealogy; whisper.history() gives the timeline.

Recipe 4: Govern a DER's egress

An identity you can prove is also one you can fence. A DER should talk to its DERMS head-end and its OTA endpoint and nothing else. Make that the policy, cap what it can move, and keep the one-call kill-switch ready. Because egress source-binds to the DER's own /128, these controls apply to the device, not to a fragile IP allowlist.

Boundary. This governs the DER's internet-side egress via the graph-first resolver and source-bound egress. It is additive and availability-safe: it adds no inline OT chokepoint on the control bus, and if the head-end authorizes against the DANE/verify path that plane is built to fail open. It maps directly to NERC CIP-005-7 R3 (determine & disable active vendor remote access) and CIP-013-2 R1.2.3/R1.2.6.

DER /128 2a04:2a01:5e0::50c egress source-bound policy default-deny firewall · budget by host / cidr / port DERMS head-end derms.example-vpp.com ✓ allow OTA endpoint ota.example-vpp.com ✓ allow everything else C2 · exfil · unknown ✗ block op:revoke → /128 + PTR + DANE gone worldwide at DNS-TTL
Default-deny per device: allow the DERMS head-end and the OTA endpoint by name, block the rest, cap the throughput, and keep one revoke that cuts a compromised inverter off worldwide.

Set the resolution policy to default-deny and allow only the two names the DER legitimately needs, with the CLI or over the control plane with op:policy:

# Default-deny the DER's egress; allow only its DERMS head-end and OTA endpoint.
whisper policy set --agent 2a04:2a01:5e0::50c \
  --default deny --allow derms.example-vpp.com,ota.example-vpp.com

# The same, over the control plane (op:policy): allow by name or subdomain:
curl -s https://graph.whisper.security/api/query -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'policy'"'"', args:{agent:'"'"'2a04:2a01:5e0::50c'"'"', default:'"'"'deny'"'"', allow:['"'"'derms.example-vpp.com'"'"','"'"'ota.example-vpp.com'"'"']}})"}'

Tighten it further with a per-device firewall by host, cidr or port, cap what the device can move with a budget, and keep the kill-switch one verb away:

# Per-device firewall (op:firewall): allow the OTA CDN on 443, deny all else:
curl -s https://graph.whisper.security/api/query -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'firewall'"'"', args:{agent:'"'"'2a04:2a01:5e0::50c'"'"', rules:[{allow:'"'"'host'"'"', host:'"'"'ota.example-vpp.com'"'"', port:443},{deny:'"'"'all'"'"'}]}})"}'

# Budget + kill-switch (op:budget): cap egress volume, trip the switch past the cap:
curl -s https://graph.whisper.security/api/query -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'budget'"'"', args:{agent:'"'"'2a04:2a01:5e0::50c'"'"', egress_mb_per_day:50, on_exceed:'"'"'kill'"'"'}})"}'

# A compromised inverter: cut it off worldwide in one call (op:revoke):
whisper kill --revoke 2a04:2a01:5e0::50c   # worldwide, at DNS-TTL

After the revoke, every keyless check from Recipe 2 flips to is_whisper_agent:false for that address within the DNS TTL. No CRL to distribute, no per-vendor blocklist to push, no fleet-wide firmware flash. Containment is one call, and it lands in the transparency log.

Recipe 5: See who enumerated your fleet

The kill-chain starts with reconnaissance: someone resolving and RDAP-querying your DER identities to map the fleet before the dispatch. Because every DER's name resolves through Whisper's own authoritative DNS and RDAP, the owner sees exactly who looked: a tripwire the LFDI's private, out-of-band registry never gave you. This is reverse observability: not the DER's own outbound (op:logs), but who has been checking it.

Boundary. op:lookups reports resolution and RDAP accesses against your identities' authoritative records: the PTR / AAAA / TLSA queries and /ip reads. It is an early-warning signal, not a claim to see traffic inside someone else's network; pair it with Recipe 3 to turn a suspicious source into an attributed operator.

recon, before the command, and now you can see it Enumerator mapping your fleet dig -x · TLSA RDAP /ip Whisper authoritative DNS · RDAP for your /128s lfdi-3f2504e0… lfdi-a71c93b8… …812 distinct names 812 lookups / 90s op:lookups who checked this identity ⚠ recon tripwire early warning → your SOC
One source resolving hundreds of distinct DER identity names in seconds is the enumeration signature: surfaced by op:lookups as an early warning, not a post-mortem.

Ask who has been checking a single DER's identity (keyless, for that address) or the whole fleet with your key:

# Who resolved or RDAP-queried this DER's identity? A recon tripwire, keyless per-address:
curl -s https://whisper.online/ip/2a04:2a01:5e0::50c/lookups
{"address":"2a04:2a01:5e0::50c","window":"24h",
 "lookups":[{"kind":"PTR","count":6},{"kind":"TLSA","count":2},
            {"kind":"rdap","count":3}]}

# Fleet-wide, with your key (op:lookups): the single source walking your whole fleet:
curl -s https://graph.whisper.security/api/query -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'lookups'"'"', args:{scope:'"'"'fleet'"'"', window:'"'"'90s'"'"'}})"}' | jq .
  source 34.90.x.x: resolved 812 distinct DER identities in 90s   ⚠ enumeration

Feed that source straight into Recipe 3's whisper.identify and the anonymous recon becomes an attributed operator, before a single setpoint is dispatched.

Recipe 6: Non-repudiable DER telemetry

Market settlement, ISO dispatch, and grid-services payments all rest on trusting that a telemetry frame came from the real DER, not a stolen token spoofing numbers. Bind each frame to the DER's forge-proof /128 by signing it with the same key whose public half anchors that address, and any consumer verifies it against the DANE pin already in DNS. See Sign agent outputs for the full pattern.

Boundary. This complements IEC 62351 / 2030.5 payload security. It does not replace them. What it adds is a signature bound to a publicly verifiable identity: the utility, the ISO, and your market counterparty can check the numbers off-ecosystem, against the same DANE-EE key that anchors the /128, with no shared secret and no private CA store to trust.

The DER signs each telemetry frame with its device private key (which never leaves the device). The consumer needs nothing but the DANE pin (shipped, keyless) to verify the signer is the genuine /128:

# On the DER: sign the telemetry frame with the device key that anchors its /128.
# (The private key stays on the device; only the signature and frame go on the wire.)
openssl dgst -sha256 -sign device-key.pem -out frame.sig telemetry-frame.json

# On the consumer (ISO / utility / settlement): first prove the identity. Verify
# confirms the DER's served leaf key == the DANE pin for this /128 (shipped, trustless):
whisper verify --trustless 2a04:2a01:5e0::50c    # DANE-EE 3 1 1 matches → key is the DER's
dig +short TLSA _443._tcp.lfdi-3f2504e0.der.example-vpp.whisper.online
3 1 1 b653a4ef…fcb82d1d

# …then check the frame's signature against that same device public key with stock openssl.
# A stolen token can't produce it: it doesn't hold the key the /128 is derived from.
openssl dgst -sha256 -verify der-pubkey.pem -signature frame.sig telemetry-frame.json
Verified OK   # signer == the DANE-pinned /128 → telemetry is non-repudiable

Because the identity's mint and any revoke are in the transparency log, a settlement dispute can be replayed against an auditable record: the frame was signed by a key that was a live, registered DER identity at that timestamp. Revoke the DER and its future frames stop verifying worldwide within the DNS TTL. That's the same mechanism as counterfeit-detection and containment.

Where each recipe stops, and what's roadmap

Honest scoping, one row per recipe. Whisper anchors the cloud/IP boundary; the substation bus, the device PKI, and the charging handshake stay exactly where they are.

RecipeComplementsDoes not touch / replace
Bind a DER to its LFDI SunSpec-Kyrio managed PKI, IEEE 2030.5 EndDevice cert the device certificate itself; the DNP3 / IEC 61850 control bus
Prove keyless · Sign telemetry CSIP session security, IEC 62351 payload protection the 2030.5 application handshake; SecOC-style message auth
Back-trace · Lookups your OT sensor, SIEM and threat-intel a tap on your OT network: it enriches, it doesn't monitor the bus
Govern egress NERC CIP-005 R3 / CIP-013 R1.2 vendor-access controls an inline OT chokepoint; the ISO 15118 charging handshake

Roadmap, clearly labelled. Streaming this evidence into a SIEM ships today for Splunk, Microsoft Sentinel and OpenCTI (signed JSON → CEF/ECS). A STIX 2.1 over TAXII feed and an E-ISAC machine-readable export for sharing revocation and attribution evidence are proposed, not yet available. And the first-class typed --lfdi argument is roadmap: provision DERs via the control-plane API shown above, passing the LFDI as the generic device_id, which is live. The transparency log is tamper-evident, Ed25519-signed and Bitcoin-anchored today; independent witnessing is the next step. Nothing on this list is required for the six recipes; they run on shipped primitives alone.

Next