# 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](/docs/dane) so anyone can verify it and nobody can forge it. Everything below is built from shipped parts: the device `/128`, keyless [verify](/docs/verify), the attribution graph, per-device egress governance, the [transparency log](/docs/transparency), and one-call [revoke](/docs/control-plane). 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](/docs/verify) / RDAP, the attribution graph (`whisper.identify`), per-device [egress governance](/docs/egress-governance) (policy · firewall · budget · revoke), the reverse-observability `op:lookups`, and the Merkle [transparency log](/docs/transparency) 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:

- **Deterministic & idempotent**: the same device key + `LFDI` always yields the same `/128`. Re-provisioning a replacement communication card for the same `EndDevice` returns the same address; there is no registry to keep in sync.
- **Tenant-bound & fleet-unlinkable**: the derivation folds in your tenant, so the same device under two aggregators yields two *unrelated* `/128`s. No one can link a unit across VPPs by address suffix, and there is no enumeration oracle.
- **Forge-proof**: the `/128` is [DANE-EE `3 1 1`](/docs/dane)-pinned to that device's key and has DNSSEC-signed reverse DNS. A cloned inverter with a different key derives a different address and can't present the pinned key.
- **Revocable worldwide**: one control-plane call tears the `/128`, its PTR, and its DANE pin down everywhere at DNS-TTL speed. IEEE 2030.5's certificates never had that off-switch.

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:

```bash
# 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:

```bash
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](/docs/transparency), so you and your regulator can audit the full issuance history of your fleet:

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

> 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:

```bash
# 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`:

```bash
# 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.

*Figure: Attribution survives rotation because it tracks the infrastructure and the tooling, not the ephemeral egress. Infrastructure genealogy (shared ASN, hosting and certificate lineage) collapses the cloud hops across AWS / GCP / Azure; a `JA4` client fingerprint collapses the residential-proxy swarm (41 exit IPs down to one operator) and returns a replayable evidence chain into your SIEM.*

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:

```bash
# 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:

```bash
# 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**.

*Figure: Default-deny per device: allow the DERMS head-end (`derms.example-vpp.com`) and the OTA endpoint (`ota.example-vpp.com`) by name, block everything else, cap the throughput with a budget, and keep one `revoke` that tears the `/128` + PTR + DANE pin down worldwide at DNS-TTL.*

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`:

```bash
# 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:

```bash
# 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.

*Figure: One source resolving hundreds of distinct DER identity names (`dig -x`, TLSA, RDAP `/ip`) in seconds is the enumeration signature. Those lookups land on Whisper's authoritative DNS and RDAP, and `op:lookups` surfaces the single source touching 812 distinct DER identities in 90s as an early-warning tripwire to your SOC, before any command is dispatched.*

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

```bash
# 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](/docs/sign-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`:

```bash
# 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](/docs/transparency), 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.

| Recipe | Complements | Does *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

- [DER & inverter identity](/docs/industries/energy/der-identity): the full derivation the `/128` above comes from, key + `LFDI` to address
- [Grid-API-abuse cure](/docs/industries/energy/grid-api-abuse-cure): these primitives applied to the exact fleet-takeover / BOLA problem they were built for
- [DANE & TLSA](/docs/dane): the `3 1 1` pin every check here rests on, byte for byte
- [NERC CIP · IEC 62351 · Rule 21](/docs/industries/energy/energy-compliance): where these recipes map into CIP-013/005 evidence
