opcua-client-ext-transport-pubsub · master
Docs · Recipes

Rotating keys with an SKS

Fetch group keys from a Security Key Service, subscribe with encryption, and rotate the keys without restarting.

SksGroupKeyProvider pulls the current group keys from an OPC UA Security Key Service via GetSecurityKeys. You drive the rotation by calling refresh().

refresh() first

The key accessors throw until refresh() has succeeded at least once. Call refresh() before you start listening, then again on your rotation schedule.

Set up

php connect SKS + build provider
use PhpOpcua\Client\ClientBuilder;
use PhpOpcua\Client\ExtTransportPubSub\Security\PubSubSecurityMode;
use PhpOpcua\Client\ExtTransportPubSub\Security\PubSubSecurityOptions;
use PhpOpcua\Client\ExtTransportPubSub\Security\SksGroupKeyProvider;

$sksClient = ClientBuilder::create()
    ->connect('opc.tcp://sks.plant.local:4840');

$keys = new SksGroupKeyProvider(
    client: $sksClient,
    securityGroupId: 'group-1',
    securityPolicyUri: SksGroupKeyProvider::POLICY_AES256_CTR,
    requestedKeyCount: 1,
);

$keys->refresh();   // mandatory: fetch the first key set

$security = new PubSubSecurityOptions(
    mode: PubSubSecurityMode::SignAndEncrypt,
    keyProvider: $keys,
);

Subscribe and rotate

Run your own poll() loop so you can re-refresh() on a schedule:

php poll loop with rotation
$subscriber = SubscriberBuilder::create()
    ->onDataSetMessage($callback)
    ->listenUdp(
        endpoint: 'opc.udp://239.0.0.1:4840',
        readers: [$reader],
        security: $security,
    );

$nextRotation = time() + 3600;   // rotate hourly (match your SKS policy)

while ($running) {
    $subscriber->poll(timeoutMs: 200);

    if (time() >= $nextRotation) {
        $keys->refresh();        // pull the next key set from the SKS
        $nextRotation = time() + 3600;
    }
}

The codec reads the provider's signingKey() / encryptingKey() / keyNonce() / tokenId() per datagram, so a successful refresh() takes effect on the very next message — no restart, no gap.

Failed refresh

If refresh() can't reach the SKS it throws PubSubSecurityException. Decide your policy explicitly: keep using the last known-good keys for a grace period, or stop. Don't let an unhandled throw kill the loop silently.

Defaults

objectNodeId and methodNodeId default to the standard SKS nodes (i=14443 / i=15215). Override them only if your server exposes GetSecurityKeys elsewhere. Use POLICY_AES128_CTR instead of POLICY_AES256_CTR for a 128-bit group.