# The grid-API-abuse cure

A genuinely valid aggregator token can dispatch a whole fleet of DER. Nothing was breached: the token authenticates a *claim*, never the machine on the other end. OWASP has a name for it: **BOLA**, broken object-level authorization, here at grid scale.

Whisper cures it by making the address *be* the DER: a routable IPv6 `/128` derived from the key the inverter already holds, [DNSSEC](/docs/dnssec)-anchored and [DANE-pinned](/docs/dane), that no aggregator can forge and one call can revoke worldwide. A stolen token with no device key behind it authenticates to nothing. This page walks the abuse end-to-end, then the reframe, then the exact live calls: keyless to verify, one keyed call to provision, one more to revoke. It closes with the attribution companion that names the operator behind the sessions already in your logs.

## The abuse, at grid scale

It is not a zero-day. Your DER control plane (a vendor cloud, a DERMS head-end, an OpenADR VTN, an aggregator API) is used exactly as it was built, at fleet scale, by an operator who was never your customer. The mechanics repeat across the industry, and they follow the same six moves every time.

*Figure: the grid-API kill chain, six stages: **01 RECON** find the control plane (vendor cloud / API) → **02 TOKEN** mint or steal the bearer (= fleet authority) → **03 ENUMERATE** iterate the `EndDevice` LFDIs (one token → one fleet) → **04 COMMAND** dispatch legit-looking setpoints (disconnect / on-off) → **05 ROTATE** hop clouds + residential (last IP disposable) → **06 STRESS** swing the aggregate (<2% modeled = grid-relevant). Two invariants make it invisible at the network layer: ① a real operator is one head-end to a fleet it owns, the abuser is one token to a fleet it doesn't; ② every IP it ever shows you is disposable, so the last IP was never the attacker.*

**Recon and token.** A third party locates the control plane: an internet-exposed gateway (research found **~35,000** such solar devices across **42 vendors** on Shodan) or, higher value, the vendor cloud or aggregator API that fronts a whole fleet. It then acquires a credential the same way the legitimate app does: hitting an unauthenticated `/oauth2/token`, exploiting a broken-authorization endpoint, or lifting a hard-coded key from an app bundle. Now it holds a valid **OAuth bearer or JWT**, and that token *is* the fleet authority.

**Enumerate and command.** With one token it iterates account and org IDs and the `EndDevice` `LFDI`s behind them: "many small devices, one control plane" becomes "one token, thousands of inverters." Then it dispatches *legitimate-looking* control: active-power setpoints and curtailment, forced disconnect and reconnect, coordinated on/off, or malicious firmware. Every request is well-formed and the token is valid, so your IP allowlists pass it and no detection rule fires.

> The root cause has a name: **OWASP broken object-level authorization, BOLA (API1:2023)**, with its sibling **BOPLA (API3:2023)** when the fleet-scoping check is missing. The bearer token, key, or password authenticates a *claim* ("I am an authorized caller") but never proves *which machine* is on the other end. So a stolen credential is indistinguishable from the real one, and the `EndDevice` object it targets has no identity of its own to check the caller against.

**Rotate and stress.** Rate-limit the egress and a fresh IP appears, hopping across AWS, GCP and Azure, or a residential-proxy swarm, every few requests, so your OT SOC correlates a meaningless *last IP* and nothing else. Finally the fleet is synchronized to move real power: research models that under **<2%** of inverters (~4.5 GW against a ~270 GW EU installed base) an attacker could push frequency toward load-shedding, and peer-reviewed work on high-wattage IoT botnets (MadIoT) argues a few bots per megawatt can seed a cascade. Treat those magnitudes as *modeled, not observed*. The direction is not in dispute. What *is* observed: **~800** PV-monitoring devices were hijacked into a Mirai botnet through one unauthenticated command-injection flaw, the first publicly confirmed cyberattack on PV generation, and a single cloud-platform pair was shown at Black Hat to coordinate **~195 GW** of solar where a stealable token took over any account.

Detection will always be a step behind a credential that is genuinely valid. You can tune models forever and the abuser still looks exactly like a customer: to your backend, they are one. The strictly-stronger move is to change *what the backend trusts*.

## The reframe: the address is the DER

Whisper has one primitive: **the address is the identity**. A routable IPv6 `/128` out of `2a04:2a01::/32` (announced by **AS219419**), deterministically derived from a key, DNSSEC-signed to the IANA root, [DANE-EE](/docs/dane) pinned (`3 1 1`), and [RDAP](/docs/rdap)-registered. It is re-derivable and verifiable by anyone with `dig`.

Point it at the DER. Whisper derives each inverter's (or each battery gateway's, or EVSE's) `/128` from the **public key** behind the identifier it *already* carries: the public half of its IEEE 2030.5 `EndDevice` certificate, a SunSpec/Kyrio device cert, a TPM, or a secure element, with the **LFDI** (or a bare DER serial) as the domain separator. The IEEE 2030.5 `LFDI` is itself the leftmost 160 bits of a SHA-256 over that certificate: a genuinely good key-derived name the standard already computes, then locks in a private, non-revocable root. The derivation is deterministic and tenant-bound: the same device key and LFDI always yield the same `/128`, and the mapping is unlinkable across fleets to an outsider. The private key never leaves the device; only its public `SubjectPublicKeyInfo` is an input.

The backend then authorizes on the DER's *pinned identity*, not a stealable token: a state-changing dispatch either proves it is the inverter it claims to be, before a single detection rule runs, or it has no authority at all.

*Figure: two authorization models. **Today (authorize a claim, BOLA/BOPLA):** a valid token reaches the aggregator API, which authorizes the claim and fans out to every `EndDevice` in the fleet: reach any account, reach any DER. **Under identity (authorize the machine):** a state-changing dispatch must terminate mutually-authenticated to one DER's pinned `/128` with a DANE-EE `3 1 1` match; the inverter co-signs. A token with no device key behind it authenticates to nothing. The stolen token still passes the API, but the command must prove the inverter's key, which the attacker never holds, so reaching any account no longer reaches any DER.*

> **The LFDI is the public index: the `/128` is its cryptographic counterpart.** The LFDI flows through every CSIP deployment and is not a secret; that is what the attacker weaponizes when it enumerates a fleet. But the `/128` derives from the in-device key with the LFDI as the domain separator, so **the LFDI alone yields nothing**. You cannot go LFDI → `/128` without the key, there is no enumerable directory, and RDAP and reverse DNS return the registry object, never the DER's whereabouts. The same unit under two aggregators yields two unrelated `/128`s, and no outsider can link it across operators.

## What changes

Nothing here is a new detection rule. Each row is an abuse technique that stops being *possible*, not one you catch after the fact.

| The abuse today | Why it dies under identity |
|---|---|
| **One token → thousands of inverters** | You cannot present thousands of DER identities whose device keys you do not hold. Every forgery is a DNSSEC / DANE inconsistency any verifier catches with stock tools: `dig -x` names an identity whose `AAAA` and TLSA don't agree. |
| **Rotate egress across clouds / residential proxies** | Identity is not the source IP. The *last IP* was never the credential, so rotating it, across AWS, GCP, Azure, or a proxy swarm, changes nothing about whether the caller can prove the DER. |
| **Replay a minted or reused aggregator token** (BOLA / BOPLA) | The token has no device key behind it. A state-changing dispatch terminates mutually-authenticated to the target inverter's `/128` (the DER co-signs), so a valid-looking token that can't prove the identity never had authority. Reaching *any* account no longer reaches *any* DER. |
| **Synchronize the fleet to swing aggregate output** | You can't coordinate identities you can't forge. The mass-command precondition (one credential speaking for thousands of devices) is removed, and one compromised unit is cut off in a single call, not after a fleet-wide reset. |
| **Blast radius on compromise** | One `revoke` tears down the `/128`, its PTR, and its DANE pin worldwide at DNS-TTL speed. No fleet-wide credential reset, no CRL you hope every inverter fetched. Compromise one inverter and you have compromised *that inverter*, not the fleet. |

## Provision a DER identity

Provisioning is one control-plane call to `POST https://graph.whisper.security/api/query` with your `X-API-Key` header. You pass the DER's **public** key material (the base64 `SubjectPublicKeyInfo` of its `EndDevice` / TPM / secure-element key) and its LFDI as the `device_id`; you get back the deterministic `/128` and a WireGuard config to source the inverter's traffic from that address.

### The call

```
CALL whisper.agents({op:'connect', args:{
  tier:'wireguard',
  identity_public_key:'<base64 SPKI of the DER key>',   # public half of its 2030.5 EndDevice / TPM / SE cert
  device_id:'3F2504E04F8911D39A0C0305E82C33018B2E44F9'   # the IEEE 2030.5 LFDI (40 hex)
}}) YIELD op, ok, status, result, error
RETURN op, ok, status, result, error
```

**Over stock tools**: `jq` builds the JSON body so the Cypher's own quotes never fight the shell:

```bash
# the public key only: the private key never leaves the DER's secure element
Q="CALL whisper.agents({op:'connect', args:{tier:'wireguard', \
   identity_public_key:'MFkwEwYHKoZIzj0…SPKI', device_id:'3F2504E04F8911D39A0C0305E82C33018B2E44F9'}}) \
   YIELD op, ok, status, result, error RETURN op, ok, status, result, error"

curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  --data "$(jq -nc --arg q "$Q" '{query:$q}')"
```

### The response

```
# result carries the deterministic identity plus a ready-to-apply tunnel
address        2a04:2a01:5e0::50c
fqdn           lfdi-3f2504e0.der.<tenant>.agents.whisper.online
ptr            lfdi-3f2504e0.der.<tenant>.agents.whisper.online
state          active                       # DNSSEC + DANE-EE (3 1 1) live at provision time
wireguard_config   [Interface] …            # source the inverter's traffic from its own /128
```

The DER now has a name it can prove and an address it egresses from. Reverse DNS resolves the `/128` to that identity, a TLSA record pins the leaf key, and RDAP registers the object under `2a04:2a01::/32`. That's the full [seven-proof](/docs/verify) chain, published atomically with the allocation, and each mint recorded in the [Merkle transparency log](/docs/transparency).

### Idempotency and errors

The call is deterministic and honest about conflicts: conservative in what it emits, liberal in what it accepts.

| You send | You get |
|---|---|
| The **same** device key + LFDI again | The **same** `/128`: idempotent, safe to retry, safe to run on every boot. No duplicate identities. |
| The same device key with a **different LFDI** on your tenant | `409 Conflict`: a device key binds to exactly one identifier. The `error.detail` tells you which LFDI it is already bound to. |
| A **non-string** `device_id` (a number, an array, null) | `400` with an actionable `detail`, never an opaque `500`. Send the LFDI as a string. |

> **Shipped & live.** This provisioning path (a DER `/128` derived from the device's public key + its `device_id`) is in production today. The `device_id` argument is generic: pass an `LFDI`, an `SFDI`, or a bare DER serial. A first-class typed `--lfdi` flag is on the roadmap; today, provision through the control-plane call above, then drive it with `whisper verify` / `whisper policy` / `whisper logs` / `whisper kill --revoke`.

### Revoke worldwide, and govern in between

A compromised inverter, a module swap, a change of aggregator, a decommission: one call tears down the `/128`, its PTR, and its DANE pin everywhere at DNS-TTL speed, and it is provable with the same stock tools that proved the identity existed, no Whisper software required.

```bash
# the control-plane op…
CALL whisper.agents({op:'revoke', args:{agent:'2a04:2a01:5e0::50c'}})
# …or the CLI
whisper kill --revoke 2a04:2a01:5e0::50c

# now prove it, worldwide, at DNS-TTL speed:
dig -x 2a04:2a01:5e0::50c +short                       # -> nothing
curl -s https://whisper.online/verify-identity/2a04:2a01:5e0::50c
# -> {"is_whisper_agent": false, ...}
```

Revocation is the kill-switch, but the same control plane governs what a *live* DER may reach in between. Because egress is source-bound to the device's `/128`, policy is enforced by name and by address: **default-deny**, allow only the DERMS head-end and the OTA endpoint, cap the traffic, and kill it in one call:

```bash
# default-deny: this inverter may reach ONLY its DERMS head-end and OTA endpoint
whisper policy set --default deny --allow derms.example-vpp.com,ota.example-vpp.com

# per-device firewall (allow/deny by host, cidr or port) + a traffic budget with a kill-switch
CALL whisper.agents({op:'firewall', args:{agent:'2a04:2a01:5e0::50c', deny:['0.0.0.0/0','::/0'], allow:['derms.example-vpp.com:443']}})
CALL whisper.agents({op:'budget',   args:{agent:'2a04:2a01:5e0::50c', max_mb_per_day:50}})
```

Compromise one inverter and you've compromised *that inverter*, not the fleet. The fleet-takeover failure mode is structurally removed. Full policy surface: [Egress governance](/docs/egress-governance). And because each dispatch and telemetry stream can be [signed under](/docs/sign-outputs) the DER's forge-proof `/128`, the ISO, the utility, and your own market settlement can trust the numbers came from the real device, not a spoofed feed.

## Verify it: keyless, no account

The identity is public by design, so anyone (your PSIRT, a partner aggregator, an auditor, a regulator) can check a DER without your key and without taking Whisper's word for it. This is the keyless half of the two-tier surface: verify with no key, provision and govern with your key.

```bash
# no key, no account: re-derive and verify the DER's identity, trustless to the IANA root
whisper verify --trustless 2a04:2a01:5e0::50c

# or with only curl: the keyless full-chain verdict
curl -s https://whisper.online/verify-identity/2a04:2a01:5e0::50c
# { "is_whisper_agent": true, "dane_ok": true, "jws_ok": true, "evidence": { ... } }

# the address IS the inverter: forward-confirmed reverse DNS names it
dig -x 2a04:2a01:5e0::50c +short
# lfdi-3f2504e0.der.<tenant>.agents.whisper.online.

# the registry object for the /128: RDAP, typed JSON
curl -s https://whisper.online/ip/2a04:2a01:5e0::50c | jq '.handle, .parentHandle'
# "2A04:2A01:5E0::50C/128"
# "2A04:2A01::/32"
```

The `--trustless` flag is the point: nothing there calls back to Whisper's own API as an authority. The CLI re-derives the DNSSEC chain to the IANA root, on your machine, with your resolver. A regulator can verify an inverter *outside* your cloud's tenancy. Full mechanics: [Verify an agent](/docs/verify) and [DANE & TLSA](/docs/dane).

## See who's enumerating your fleet

An identity you can prove is also an identity you can *watch*. Because every DER's name resolves through Whisper's own authoritative DNS and RDAP, the owner can ask who looked: a reconnaissance tripwire the LFDI's private, out-of-band registry never gave you. Step 03 of the kill chain is fleet enumeration; `op:lookups` surfaces it *while it's happening*, not in the post-mortem:

```bash
# who resolved / RDAP-queried this DER's identity, and when
CALL whisper.agents({op:'lookups', args:{agent:'2a04:2a01:5e0::50c', window:'24h'}})

# the same reverse-observability view, keyless, per address
curl -s https://whisper.online/ip/2a04:2a01:5e0::50c/lookups | jq
# → one source resolved 3,000+ DER PTR/AAAA/TLSA records in 40 minutes: a fleet sweep, before any command lands
```

Paired with `op:logs` (the DER's *own* outbound activity) and `/ip/<addr>/transparency` (its ordered lifecycle), you have both halves: what an inverter reaches out to, and who is reaching in to enumerate it.

## Name what already got in

Identity stops the next forgery. It does not name the operator behind the sessions already in your logs, so the same platform back-traces them. The attribution *survives* the rotation because it fingerprints the operator's infrastructure and tooling, not the ephemeral egress IP.

*Figure: attribution survives rotation. A suspect controller's egress is rotated across AWS, GCP and Azure and a residential-proxy swarm (71.x Comcast, 82.x KPN, 99.x Orange). The graph collapses the cloud rotation into one infrastructure genealogy via shared ASN, hosting and certificate lineage (cyan), and collapses the residential swarm via a JA4 client fingerprint that travels with the tooling in the TLS handshake the proxy can't rewrite (violet), resolving both to a single operator and returning a reproducible, replayable evidence chain into your SIEM. The one input never relied on is the last IP.*

Take a suspect egress IP straight from your SOC logs and ask the graph who really operates it. This runs read-only over the same public graph API, with your key:

```bash
# who really operates a host, even behind a CDN or a cloud front
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\")"}'
# operator fingerprinted across AWS / GCP / Azure; residential swarm collapsed by JA4
```

The read-only verbs (`identify`, `origins`, `walk`, `variants`, `history`) run over that one endpoint against a live internet-infrastructure graph of fused BGP, DNS, WHOIS, TLS, hosting, and threat intelligence. Cloud rotation collapses through `origins` and `walk`, which cluster shared ASN, hosting, and certificate lineage into one infrastructure genealogy; `history` gives a timeline over a suspect operator. Every answer is reproducible, replayable JSON: the paper trail a NERC or NIS2 finding needs, not a screenshot.

> These graph verbs are the **API surface**: you call the endpoint directly, as above. There is no `whisper identify` / `graph` / `export` CLI subcommand; the CLI covers the control plane (`create`, `verify`, `policy`, `logs`, `kill`), and the graph is the query API. See [Graph & cognition](/docs/graph-api).

## Where it fits: standards, SIEM, integrations

Whisper is additive. It rides *on top of* the anchors you already ship and the SIEM you already run. It replaces none of them, and it adds no inline chokepoint on an OT path.

**Compliance.** The per-`/128` egress logs and the attribution graph are ready-made evidence for the vendor-remote-access controls the standards actually require. One-call revocation and attributable per-device identity map directly to **NERC CIP-013-2 R1.2.3** (notify / coordinate when access should no longer be granted) and **R1.2.6** (system-to-system remote access), and to **CIP-005-7 R3.1 / R3.2** (determine *and disable* active vendor remote-access sessions): enumerate the connected devices, then cut one instantly. It is evidentiary for **EU NIS2 Art.21** (access control, asset management, logging, supply chain) and **NISTIR 7628** SG.IA / SG.AU, and aligns with the zero-trust and ICT-lifecycle traceability direction of the **EU Network Code on Cybersecurity**. The mapping, clause by clause, lives in [NERC CIP · IEC 62351 · Rule 21](/docs/energy-compliance).

> **Two honest caveats.** NERC CIP applies to the **Bulk Electric System** (at the utility, aggregation, and vendor-access layer, not most behind-the-meter DER), so we position it at that boundary. And the EU NCCS binding control lists finalize around 2027. We map to the *defensible, direct-additive* fits and say where a claim is a stretch; we don't overclaim.

**Nothing issued in the dark.** Every identity mint and every revoke lands in a public, append-only [RFC 6962 Merkle transparency log](/docs/transparency): Ed25519-signed C2SP checkpoints, each root anchored to Bitcoin via OpenTimestamps, an auditable, non-repudiable issuance and revocation trail your regulator can replay. *Honest status:* it is tamper-evident and Bitcoin-anchored today, but **not yet independently witnessed** (our two servers co-signing is availability, not independence); the log already speaks the C2SP witness protocol, so any external witness can co-sign.

> **Shipped vs roadmap.** The **Splunk**, **Microsoft Sentinel** and **OpenCTI** connectors ship today; findings arrive as signed JSON mapped to CEF and ECS fields. **Roadmap**, labelled as such and not yet available: **STIX 2.1 over TAXII** export, an **E-ISAC** machine-readable JSON export, and the first-class typed `--lfdi` CLI/API argument.

**Integrations (proposed, not vendor-endorsed).** Whisper anchors the cloud and IP boundary. It never touches the IEC 61850 GOOSE/MMS substation bus, the DNP3 outstation link, or the ISO 15118 charging handshake:

- **IEEE 2030.5 / CSIP head-end.** Derive the `/128` from the public half of the `EndDevice` certificate, and [DANE-pin](/docs/dane) your 2030.5 endpoint's cert to DNSSEC to cut single-CA trust risk. Keeps the LFDI's key-derived property; adds public verifiability and DNS-TTL revocation. Complements the SERCA→MCA→MICA PKI; does not replace it.
- **DERMS & VPP platforms.** Your platform consumes the manufacturer's 2030.5 cert today. Whisper adds an *out-of-ecosystem*, publicly verifiable identity so the ISO and your market settlement can verify a DER outside your DERMS. Complements dispatch; does not replace it.
- **OpenADR 3.x VTN.** The VTN authenticates a `venID` plus a JWT bearer; bind the VEN's `/128` so a state-changing signal proves the resource, not just the token. Complements OAuth; never reaches into the program logic.
- **SunSpec / secure element (IEEE 802.1AR-style IDevID).** The cleanest bridge: derive the `/128` from the device's birth-certificate public key. The publicly verifiable, DNSSEC-anchored layer *on top* of the silicon identity you already stamp; no second PKI, no BOM cost, no re-flashing the fielded fleet.
- **AWS IoT Core / FleetWise.** Your X.509 + mTLS stays; the cloud remains the CA. Whisper adds the out-of-tenancy identity anchored in DNSSEC / DANE, so a peer can verify a DER outside that cloud. Complements IoT Core mTLS; does not replace it.

And it is built to **fail open**: a Whisper outage never bricks an inverter. Checks degrade to the anchors you already ship and connectivity is preserved. Anycast on AS219419, no single node in the path.

## Next

- [DER & inverter identity](/docs/der-identity): how the `/128` is derived from the device key and the LFDI, in depth
- [Control plane](/docs/control-plane): the full `whisper.agents` op set the provisioning call belongs to
- [NERC CIP · IEC 62351 · Rule 21](/docs/energy-compliance): the clause-by-clause evidence mapping
- [Telemetry · egress · attribution](/docs/energy-recipes): runnable recipes for signing a DER's telemetry, default-denying its egress, and back-tracing a controller
