Whisper · Docs
OT

Asset & PLC identity

Bind the OPC UA ApplicationUri an asset already carries to a routable IPv6 /128 derived from the device's own public key. The address is the asset: forge-proof, tenant-private, DNSSEC-anchored, DANE-pinned, and revocable worldwide in one call. That's the cross-org off-switch its local TrustList never had. And it gives the identity-less legacy floor (a bare Modbus PLC behind a gateway) a verifiable network identity for the first time.

This is the spine of the OT vertical, and it is shipped and live. Everything else (the OT-exposure cure, the platform integrations, the IEC 62443 and EU CRA evidence) builds on the one idea below: an asset's network address stops being a label a foothold inherits by owning an IP, and becomes a cryptographic fact only that device's silicon can stand behind. The striking part: OPC UA already computes a genuinely good key-bound name for every application instance, the ApplicationUri, cryptographically welded into the certificate. It just traps that name in a local, per-site trust store that no one outside the plant can validate and no one can revoke across an org boundary. Whisper keeps that property and gives it the two things it lacks.

Two tiers, per Postel's Law. With no API key, anyone (an asset owner, an integrator, an OEM certifier, an auditor) can verify an asset's identity from stock tools (dig, curl, RDAP), because the identity is public by design. With your key you provision and govern: mint the /128, source-bind and constrain its egress to its MUD-declared peers, pull its logs, watch who's enumerating it, and revoke it. Verification never needs an account; the control plane does.

The ApplicationUri: a key-bound name, in a local cage

Start with what OPC UA already has, because it is better than most people realize. Every application instance is named by its ApplicationUri, an ApplicationDescription member that OPC UA Part 4 defines as the "globally unique identifier for the application instance." It is an identity string, not a locator (the EndpointUrl, opc.tcp://host:4840, is the locator, discovered separately via GetEndpoints / FindServers). And it is not a value the operator is free to spoof at will: Part 6 welds it into the certificate. The ApplicationInstanceCertificate SAN "shall include a uniformResourceIdentifier equal to the applicationUri", exactly one URI. That binding is enforced on the wire: at OpenSecureChannel / CreateSession, a certificate whose SAN URI does not match the presented ApplicationUri is rejected with BadCertificateUriInvalid, and the session fails. In other words, the standard already binds a globally-unique name into the asset's key material, the same instinct Whisper is built on. That is a real head start.

The trouble is not the name. The trouble is the walls around it. OPC UA's trust model (Part 2) is deliberately local: trust is a per-site TrustList / private, operator-run CA, and the spec explicitly discourages commercial or public CAs. The reasoning: a public CA would let the CA, not the operator, decide which applications are trusted. Self-signed deployments have no CA and no CRL at all. Revocation is a local CRL or TrustList edit; it is invisible one hop outside the site. The Part 12 GDS / CertificateManager can push onboarding, renewal, TrustList updates and revocation, but every one of those actions is scoped to the local trust domain.

Per the OPC UA security model: trust is anchored in a local TrustList / private per-site CA; using a commercial or public CA is explicitly discouraged, and a self-signed deployment has "no CA or CRL." Revocation is a local edit, invisible across an organizational boundary.

So the ApplicationUri is a good key-bound name that is nonetheless boxed in on four sides at once:

The ApplicationUri already has The ApplicationUri still lacks
Key-bound identity: welded into the certificate SAN; a mismatch fails the session (BadCertificateUriInvalid) Not routable: an application-layer URN, never a network endpoint you can reach or authorize against on the IP fabric
Globally unique: a canonical identifier that flows through every OPC UA discovery exchange Not publicly verifiable: trust lives in a private, per-site TrustList; commercial CAs are discouraged; useless one org-hop away
Standards-mandated: the default identity of every OPC UA application instance Allow-listed by hand: the peer certificate is trusted by a local TrustList edit, per site, per integrator
Bound to a key only the device holds Non-revocable cross-org: revocation is a local CRL/TrustList change no one outside the site can see

Read the right-hand column as a to-do list. Whisper does exactly those four things, without touching the left-hand column and without re-flashing the asset.

The cure: a routable, revocable /128

Whisper keeps the ApplicationUri's one good property (identity bound to the device's own key) and adds the two the local TrustList withholds: public, cross-org verifiability and DNS-TTL, cross-org revocation. It does this by giving the asset a real, routable IPv6 /128 out of 2a04:2a01::/32 (announced by AS219419) that is a deterministic function of the device's public key with its ApplicationUri (or a bare serial, or an IEEE 802.1AR IDevID) as the domain separator, then anchoring that address into DNS so anyone can prove it and one call can retire it.

already in the asset public · cross-org verifiable & revocable: what Whisper adds Asset key ApplicationUri welded in cert SAN OPC UA cert · 802.1AR IDevID · TPM/SE private key sealed on-device public key + ApplicationUri /128 2a04:2a01:7a3::c1a routable · tenant-bound DNSSEC + DANE-EE 3 1 1 A name anyone can verify whisper verify --trustless RDAP-registered · DNSSEC to the IANA root our own API is not in the trust path op:'revoke' → gone worldwide at DNS-TTL
OPC UA already welds a globally-unique ApplicationUri into the asset's certificate: a good key-bound name, trapped in a local, non-revocable-cross-org TrustList. Whisper binds it to a routable, publicly verifiable /128 and gives it the cross-org off-switch the local trust store never had.

Because the address is derived from a key only that device holds, you cannot present an asset identity whose key you don't have. A foothold that owns an IP no longer inherits the asset's identity. A spoofed session off the wrong address is inert. Every forgery is a DNSSEC/DANE inconsistency any verifier catches for free. That is the whole point of moving trust from reachability on a flat segment to the machine on the other end.

How the derivation works

The /128 is not drawn from a pool and written into a database. It is computed, the same way on every node, from inputs the asset already has. Three things go in:

Input What it is Where it lives
Device public key the SubjectPublicKeyInfo (SPKI) of the asset's key: the public half of its OPC UA ApplicationInstanceCertificate, an 802.1AR IDevID, a TPM, or a secure element the public half is submitted; the private key never leaves the device
Device id the native identifier the asset already carries, used here as the domain separator: the OPC UA ApplicationUri, an IDevID serial, or a bare asset/PLC serial submitted with the request; the public index
Component serial (optional) a per-component domain separator, so one asset can hold many addressable identities (the PLC CPU, a comms module, a specific OPC UA server instance) optional; omit it for a single per-asset address

Those inputs are combined by a one-way derivation, with a Whisper-held secret mixed in, into a stable, unguessable interface identifier scoped to your tenant:

# inputs -> a stable, forge-proof interface identifier
derive( device public key,  ApplicationUri [, component serial],  your tenant )  -->  64 uniform bits

# the /64 prefix is your tenant block; the low 64 bits are the derived id
/128 = < your tenant /64 prefix > : < derived interface id >

Four properties fall straight out of that derivation, and each one is load-bearing:

The moment the address is derived it is published as a full identity, atomically: an AAAA, a forward-confirmed PTR, and a DANE-EE TLSA 3 1 1 record that pins the device's leaf key directly, all DNSSEC-signed to the IANA root and registered in RDAP. That TLSA pin is what turns "the address is derived from a key" into "the address is provable against that key by anyone, in any org." See DANE & TLSA for the byte-for-byte record and DNSSEC for the chain it hangs from.

The private key never moves. The device submits only its public SPKI, the same public half of the OPC UA / 802.1AR / TPM / secure-element key it already holds. The server derives a public address from public inputs plus a server-side secret; it never sees, holds, or derives the asset's private key. The asset proves ownership later by presenting its own key against the DANE pin.

You can pin the certificate you already speak. If your OPC UA server already terminates its ApplicationInstanceCertificate, you can publish a DANE TLSA record over DNSSEC that pins that endpoint certificate, turning the private, single-TrustList trust into something any relying party can validate against the IANA root, cross-org, with no commercial CA and no change to the device. This is exactly the gap OPC UA's own model leaves open: it discourages public CAs, so nothing off-site can validate the cert. A DANE pin closes it without contradicting the standard. Whisper's derived /128 and a DANE-pin of your existing OPC UA cert are complementary: one gives the asset a publicly verifiable address, the other makes its existing certificate publicly verifiable.

Even a bare PLC gets a verifiable identity

Not every asset is an OPC UA server. A large part of any plant is the identity-less legacy floor (Modbus/TCP, EtherNet/IP CIP, PROFINET, base DNP3), protocols insecure by design: no TLS, no authentication, plaintext and spoofable, obeying any station that can reach them on a flat segment. You cannot re-flash a controller into having a key it was never built with. This is precisely the case NIST SP 800-82r3 anticipates: where a device cannot do per-device identity itself, apply compensating controls (segmentation, protocol-aware firewalls, session logging). An overlay identity layer is the sanctioned pattern, not a workaround.

So put the identity at the edge. A protocol-aware gateway in front of the legacy asset holds the derived /128 and source-binds the asset's northbound traffic to it. For the first time, a dumb Modbus PLC has a verifiable network identity: an AAAA, a PTR, a DANE pin, an RDAP object, that a remote integrator or the plant's own SOC can resolve, verify cross-org, attribute, and revoke. What it does not do is add authentication to the Modbus wire; Whisper anchors strictly at the IP/transport boundary.

the identity-less legacy floor gets a first verifiable identity flat OT segment · IP-based trust Legacy PLC / RTU Modbus/TCP:502 · DNP3 · PROFINET no auth · plaintext obeys any reachable IP local link · unchanged Whisper edge gateway holds the asset's /128 source-binds egress · default-deny NIST 800-82r3 compensating control DNSSEC + DANE Verifiable identity AAAA · PTR · DANE-EE 3 1 1 RDAP-registered attributable · cross-org-revocable Whisper anchors at the IP / transport boundary. It adds no authentication to the Modbus wire itself. the last-inch protocol write must still be enforced in-path (at the PLC, a broker-gateway, or the EWS).
A protocol-aware gateway carries the derived /128 for an asset that could never hold a key of its own, turning a bare Modbus/DNP3 controller into something remotely verifiable, attributable, and cross-org-revocable. This is the compensating-control overlay NIST SP 800-82r3 prescribes, honest about where it stops: it does not authenticate the field protocol on the wire.

Provision an asset identity

Provisioning is one control-plane call: whisper.agents with op:'connect', tier:'wireguard', the device's public SPKI, and the ApplicationUri passed as device_id. It returns the deterministic /128 and a ready WireGuard configuration so the asset's traffic sources from its own identity. The endpoint is POST https://graph.whisper.security/api/query, authed with an X-API-Key header; no key ever travels in the body.

CALL whisper.agents({op:'connect', args:{
  tier:                'wireguard',
  identity_public_key: '<base64 SubjectPublicKeyInfo of the asset key>',
  device_id:           'urn:acme-plant.example:ACME:PLC:S71500:00A1B2C3'   // the OPC UA ApplicationUri
  // ecu_serial: 'CPU-1516-3PN'   // optional: a distinct /128 per component
}}) YIELD op, ok, status, result, error
   RETURN op, ok, status, result, error

With stock tools: just curl, no Whisper software. A quoted heredoc keeps the Cypher single-quotes intact so it pastes and runs as-is:

curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_xxx" \
  -H "content-type: application/json" \
  -d @- <<'JSON' | jq .
{"query":"CALL whisper.agents({op:'connect', args:{tier:'wireguard', identity_public_key:'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...<SPKI>...', device_id:'urn:acme-plant.example:ACME:PLC:S71500:00A1B2C3'}}) YIELD op, ok, status, result, error RETURN op, ok, status, result, error"}
JSON

The response is the standard envelope; result carries the derived address and the transport. Because the device holds its own key, no private key is ever returned. Only the public identity and the config that binds egress to it come back:

{
  "op": "connect", "ok": true, "status": 200,
  "result": {
    "tier":               "wireguard",
    "address":            "2a04:2a01:7a3::c1a",                     // the deterministic /128
    "fqdn":               "asset-9f4c2a1b.plc.<tenant>.agents.whisper.online",
    "server_public_key":  "…",
    "endpoint":           "…:51820",
    "dns":                "2a04:2a01:0:53::53",
    "wireguard_config":   "[Interface]\nAddress = 2a04:2a01:7a3::c1a/128\n…"
  },
  "error": null
}

Drop the returned wireguard_config onto the asset's comms module or its protocol-aware gateway (or feed it to wireproxy for a no-root, userspace tunnel) and every packet it sends now sources from its own /128. The backend (a SCADA head-end, an MES, a cloud historian) authorizes on that address, a forge-proof, attributable, revocable network identity, instead of a shared credential or bare reachability. For the full transport mechanics and the SOCKS5 / AnyIP alternatives, see Connect & egress; for every other op on this same endpoint, the Control plane reference.

The whisper CLI ships create --register, verify --trustless, policy, logs, and kill --revoke. A dedicated --application-uri (or --mud) flag is on the roadmap, not shipped. Provision assets today via the control-plane call above, which is live. When the flag lands it will be a thin wrapper over exactly this call. The device_id argument is generic on purpose: pass an ApplicationUri, an 802.1AR IDevID serial, or a bare PLC serial, whatever native identifier the asset carries.

Idempotent, with honest errors

Because the address is derived, provisioning is naturally idempotent, and the failure modes are clear rather than surprising: Postel all the way down.

You send You get
the same key + ApplicationUri again (same tenant) the same /128: a re-derivation, not a new allocation
the same key with a different device_id (same tenant) 409: the reused identity is never silently re-pinned to a mismatched address
a non-string device_id (or ecu_serial) 400 with a helpful detail, never an opaque 500
device_id without identity_public_key 400: an asset derives its address from its own key

Verify: keyless, no account

The identity half is public on purpose: anyone (an asset owner allow-listing a sanctioned integrator, an OEM certifier gathering CRA evidence, an MSSP reconciling a remote-maintenance session, a suspicious peer across a 62443 conduit) can prove an asset's /128 without a Whisper account and without trusting Whisper's word. Four independent checks, all from tools already on the machine:

# 1. Forward-confirmed reverse DNS: the address names the asset, the name resolves back
dig -x 2a04:2a01:7a3::c1a +short
asset-9f4c2a1b.plc.<tenant>.agents.whisper.online.

# 2. The keyless verdict endpoint (takes an address or an FQDN; ?ip=<target> also accepted)
curl -s https://whisper.online/verify-identity/2a04:2a01:7a3::c1a | jq .
{
  "is_whisper_agent": true,
  "dane_ok": true,
  "jws_ok": true,
  "evidence": { "aaaa": "...", "ptr": "...", "tlsa": "3 1 1 b653a4ef…fcb82d1d" }
}

# 3. The registry record: RDAP, IP-anchored to the /128
curl -s https://whisper.online/ip/2a04:2a01:7a3::c1a | jq '.handle, .parentHandle'

# 4. The full chain re-derived on YOUR machine, against the IANA root: Whisper NOT in the trust path
whisper verify --trustless asset-9f4c2a1b.plc.<tenant>.agents.whisper.online

A target that isn't a Whisper identity gets a clean 200 {"is_whisper_agent": false}. A negative verdict is a successful answer, not an error; only genuinely malformed input draws a 400, never a 500. --trustless is the strong form: it validates DNSSEC from the root in-process, on your resolver, so the proof holds even for a party across an org boundary that won't take Whisper's word for anything, the exact case OPC UA's local TrustList cannot serve. The full keyless walk lives in Verify an agent.

Revoke: worldwide, in one call

A compromised or decommissioned asset is one revoke away from having no network identity anywhere. The call tears down the /128, its PTR, and its DANE pin across both authoritative servers, and the change propagates at DNS-TTL speed:

CALL whisper.agents({op:'revoke', args:{agent:'2a04:2a01:7a3::c1a'}})

# prove it: zero Whisper software, the same stock tools that proved it existed:
dig -x 2a04:2a01:7a3::c1a +short                       # -> nothing
curl -s https://whisper.online/verify-identity/2a04:2a01:7a3::c1a
# -> {"is_whisper_agent": false, ...}

Revocation isn't a database flag you have to trust; it's provable the same way the identity was. The reverse lookup goes empty and the keyless verdict flips to false for everyone, everywhere, at once. This is exactly what a local OPC UA TrustList cannot do: a CRL edit there is invisible one org-hop away, and a shared credential rotation reaches only the callers you can still reach. Compromise one asset and you've compromised that asset, not the site, and you can cut it, or a whole compromised model, off across every organization that verifies it, in a single call.

The lifecycle, end to end

An asset identity is not a one-shot registration; it tracks the device through its whole service life. Because every /128 is derived, each stage is a plain control-plane call, and each mint or revoke is written to a public, tamper-evident log. Nothing is issued in the dark.

time → Factory IDevID born on-device ApplicationUri set Commissioning /128 + source-bound egress op:'connect' Re-flash / module swap re-key → new /128 connect + revoke old Incident revoke cut off worldwide, DNS-TTL op:'revoke' Decommission / resale retire → re-register to owner revoke (+ erase) public Merkle transparency log every mint & revoke · Ed25519-signed · Bitcoin-anchored
One identity, tracked from the factory IDevID to decommission: each stage a plain whisper.agents call. A re-flash or module swap re-keys to a new /128 and revokes the old; every mint and revoke is written to the public log.
Stage What happens Call
Factory IDevID The asset is born with its key; its ApplicationUri is welded into the resulting certificate. Nothing is minted yet. native, no Whisper call
Commissioning At commissioning, derive the /128 and source-bind egress; the head-end now authorizes the address, not bare reachability. op:'connect'
Re-flash / module swap A firmware re-key or a replaced comms module carries a new key, so it re-keys to a fresh /128; the old address is revoked in the same breath. op:'connect' + op:'revoke'
Incident revoke A compromised unit, or a whole compromised model, is cut off worldwide at DNS-TTL; verify flips to false everywhere. op:'revoke'
Decommission / resale An asset transfer or an end-of-life retire is one revoke and a re-register to the new owner; op:'erase' renders the log leaf's meaning unrecoverable while its proofs stay valid. op:'revoke' (+ op:'erase')

Nothing issued in the dark. Every mint and every revoke along that timeline lands in a public, append-only Merkle transparency log (RFC 6962 tlog-tiles with signed-note checkpoints), Ed25519-signed and anchored to Bitcoin via OpenTimestamps: an auditable, non-repudiable issuance-and-revocation trail your PSIRT and your regulator can replay against an EU CRA reporting obligation or an IEC 62443 audit. Honest status: the log is tamper-evident, signed, and Bitcoin-anchored today, but it is not yet independently witnessed. Our two authoritative nodes co-signing is availability, not independence. The log already speaks the C2SP tlog-witness protocol, so any external witness can co-sign; that step is on the roadmap, and we label it as such rather than overclaim.

Govern a live asset: MUD egress, budget, lookups

Revocation is the kill-switch. In between, the same control plane governs exactly what a live asset may reach, caps it, and tells you who is looking at it. This is where OT has a second native socket worth honoring: MUD (RFC 8520). A device already declares its intended peers: a from-device-policy and to-device-policy of ACLs with abstractions like my-controller, manufacturer, and local-networks, emitted via a DHCP option, an LLDP TLV, or an X.509 extension. The weakness of plain MUD is that it is fetched from a manufacturer domain and enforced by a local MUD manager against spoofable name abstractions. Whisper enforces that same declared intent as default-deny egress keyed to a cryptographically verifiable /128: a genuine strengthening, and an identity-keyed conduit per asset in IEC 62443-3-3 terms.

# enforce the asset's MUD manifest as default-deny egress, keyed to its verifiable /128
# "I talk only to my controller, my manufacturer's update service, and the local subnet"
whisper policy set --default deny \
  --allow scada.acme-plant.example,updates.acme.example,10.20.0.0/16

# per-asset firewall (allow/deny by host, cidr, or port) + a daily budget with a kill-switch
CALL whisper.agents({op:'firewall', args:{agent:'2a04:2a01:7a3::c1a',
     deny:['0.0.0.0/0'], allow:['scada.acme-plant.example:4840','updates.acme.example:443']}})
CALL whisper.agents({op:'budget',   args:{agent:'2a04:2a01:7a3::c1a', max_mb_per_day:20}})

That contains the exact surfaces the 2023-25 OT wave abused: the IT→OT convergence bridge and remote-access sprawl. A governed asset cannot beacon to a C2 host or pivot laterally, because everything but its declared peers is denied by default, at asset granularity, keyed to identity rather than to a VLAN a foothold already sits inside.

See who's enumerating your plant, before the command lands. An identity you can prove is also an identity you can watch. Because every asset's name resolves through Whisper's own authoritative DNS and RDAP, the owner can ask who looked: a reconnaissance tripwire a local TrustList never gave you. op:'lookups' returns who resolved or RDAP-queried an asset's identity, so you catch an attacker enumerating the asset inventory in recon, not in the post-mortem:

# who has been resolving / RDAP-querying this asset's identity, and when
CALL whisper.agents({op:'lookups', args:{agent:'2a04:2a01:7a3::c1a', window:'24h'}})

# the same reverse-observability view, keyless, per address
curl -s https://whisper.online/ip/2a04:2a01:7a3::c1a/lookups | jq .
# -> who resolved this asset's PTR/AAAA/TLSA and hit its RDAP object

Paired with op:'logs' (the asset's own outbound activity) you have both halves of the picture: what an asset reaches out to, and who is reaching in to look at it. That's the per-/128 egress record that maps to CRA Annex I 2(l) and CISA CPG monitoring. And because each OPC UA data change, historian record, or telemetry stream can be signed under the asset's forge-proof /128, the integrator, the regulator, and your own process-data lineage can trust the reading came from the real asset: non-repudiable process data, not a spoofed feed. The full policy surface is in Egress governance.

Attribution: name whoever already scanned you

Identity stops the next forgery. To name whoever already scanned or scraped your plant, across rotating clouds and residential proxies, the same API key opens the read-only attribution graph on the same endpoint. whisper.identify takes an address and returns the operator behind it, stitched across the Amazon → Google → Azure hops that a raw last-IP loses:

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(\"203.0.113.45\")"}' | jq .
# operator fingerprinted across AWS / GCP / Azure; residential swarm collapsed by JA4

The read-only Cypher surface (identify, origins, walk, variants, history) runs over the same POST https://graph.whisper.security/api/query with your key, and each verb returns a reproducible, replayable JSON evidence chain your OT SOC, your PSIRT, and a regulator can replay. There is no whisper identify CLI subcommand. This is the API call, and it is live. Full verbs and shapes in Graph & cognition.

Where this fits, and where it doesn't

Whisper anchors the asset's identity at the device↔network IP boundary: the endpoint a SCADA head-end, MES, cloud historian, or remote-maintenance session authorizes over IP. It is additive: it complements the roots of trust you already run and does not try to replace them, and it deliberately stops at the field bus.

For mapping these identities to IEC 62443-4-2 CR 1.2 and 62443-3-3 zones-and-conduits evidence, EU CRA Annex I 2(d)/2(j)/2(l), NIST SP 800-82r3, CISA CPG 2.0, and TSA / NIS2 obligations, see IEC 62443 · EU CRA · TSA. The Splunk, Microsoft Sentinel and OpenCTI connectors are shipped; STIX 2.1 over TAXII + ISAC machine-readable export is on the roadmap and labelled as such there. Whisper is additive throughout: it evidences the identity, attack-surface, and logging clauses; the asset still performs its own authentication handshake, and SBOM, host audit, and vulnerability remediation stay with your existing stack. No specific operator or vendor is named, endorsed, or implicated as a breach victim anywhere in these docs; the abuse patterns cited are the public, cross-industry ones.

Next