Skip to content

Recover clients after server certificate rotation - #1949

Merged
kevinherron merged 5 commits into
fix/session-original-server-certificatefrom
t3code/client-certificate-rotation-recovery
Sep 9, 2026
Merged

Recover clients after server certificate rotation#1949
kevinherron merged 5 commits into
fix/session-original-server-certificatefrom
t3code/client-certificate-rotation-recovery

Conversation

@kevinherron

@kevinherron kevinherron commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1955, which carries the server half. Merge #1955 first; this PR's base will then move to integration/1.2.

Clients can recover after a server application certificate changes without replacing the client instance. An EndpointResolver repeats discovery and endpoint selection after SecureChannel establishment failures on a secured endpoint. URL-based client factories configure it automatically; directly constructed clients can supply their own resolver.

Refresh preserves the endpoint's URL, server identity, transport, security policy, security mode, and user token policies. Replacement certificates still pass the configured validator. Retained Sessions keep their original certificate inputs while reactivation uses the replacement channel's security bindings; the server side of that contract is in #1955.

Deferred requests have bounded channel waits and stay on the channel used to build their signatures. The refresh cooldown resets when a channel becomes ready, so a second rotation is not delayed by an earlier outage. The change includes rotation and recovery tests, a configuration example, and feature documentation.

@kevinherron
kevinherron force-pushed the t3code/client-certificate-rotation-recovery branch from 47584dd to e74c439 Compare September 8, 2026 18:02
@kevinherron
kevinherron force-pushed the t3code/client-certificate-rotation-recovery branch from 73dfe6a to c7855fc Compare September 8, 2026 23:45
@kevinherron
kevinherron changed the base branch from integration/1.2 to fix/session-original-server-certificate September 8, 2026 23:45
@kevinherron
kevinherron added this pull request to stack #1956 September 8, 2026 23:54
…nvalid

An OpenSecureChannel request naming a thumbprint the server no longer
holds, as happens after the server's application certificate is
replaced, describes a certificate problem rather than a generic security
check failure. Bad_CertificateInvalid tells the client which side is
stale.
Transports that implement ChannelStateObservable now report each failed
connection attempt to their listeners, and failures during SecureChannel
establishment carry a SecureChannelHandshakeException in the cause
chain. The marker identifies the handshake phase independently of the
server's status code or whether the server sent an error at all,
including timeouts and silent closure. TCP connection and
Hello/Acknowledge failures do not carry it. The SDK will use this
boundary to refresh discovery information without depending on
particular status codes.

The TCP transport also aborts an in-flight socket and handshake on
disconnect() instead of waiting for the peer or a handshake timeout,
since the ChannelFsm shelves Disconnect while an attempt is in progress.
The reverse-connect transport preserves the phase marker when the peer
closes the socket before channelInactive runs.
OpcClientTransport.sendRequestMessage awaits an in-progress reconnect
internally, so a request built eagerly could sign over a dead channel's
thumbprint and be sent on the channel that replaces it. Add a
sendRequestMessage overload that takes a request supplier and a bounded
channel wait; the TCP transport invokes the supplier only after its
ChannelFsm publishes the new channel, and other transports build
immediately. CreateSession and reactivation use it, so an expired wait
fails with Bad_Timeout instead of creating a Session later, and a
transport that throws synchronously still produces a failure event.

Sessions now retain the endpoint and client certificate they were
created with. Reactivation signs with those inputs and the replacement
channel's certificate and thumbprint, and requires a new Session when
the client's application certificate has changed (Part 4 6.7).
A client connecting to a secured endpoint pins the server certificate
advertised at discovery. When the server replaces its application
certificate, every reconnect fails until the application rediscovers
the endpoint and builds a new client.

Add an EndpointResolver that reruns discovery and endpoint selection, and
an EndpointRefresh that invokes it after any SecureChannel establishment
failure on a secured endpoint, classified by the transport's phase marker
rather than by status code. The transport's next attempt reads the
refreshed endpoint; the Session FSM reads the endpoint and discovery
list together once the channel is ready so CreateSession validation uses
one resolution result. Refresh runs at most one resolution at a time,
within the resolver's cooldown and timeout, and resets the cooldown when
a channel becomes ready.

A refresh must select the same endpoint: URL, server URI, transport,
security policy, mode, and user token policies. Only the server
certificate may differ. Token policies are pinned because GetEndpoints
is unauthenticated and a refresh is reachable by anyone able to disrupt
the handshake. Replacement certificates still pass the configured
validator; discovery grants no trust.

URL-based client factories configure the resolver automatically, and
the client example runner builds one explicitly.
Exercise the Java client against a Milo server that rotates its
application certificate while connected: recovery with and without a
certificate status from the server, recovery after a server restart,
bounded rediscovery during a discovery outage, rejection of untrusted
replacements, reactivation of retained Sessions with username and X509
credentials, unchanged renewal bindings on an established channel, and
a new Session when the client's own certificate changes.
@kevinherron
kevinherron force-pushed the t3code/client-certificate-rotation-recovery branch from c7855fc to 3cf936f Compare September 9, 2026 00:05
@kevinherron
kevinherron marked this pull request as ready for review September 9, 2026 00:12
@greptile-apps

greptile-apps Bot commented Sep 9, 2026

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking defect established in the changed paths.

Summary

  • Introduces retained endpoint resolvers, endpoint-equivalence validation, refresh cooldown and lifecycle coordination.
  • Binds deferred session-request construction to the channel used to send the request and adds bounded channel waits.
  • Preserves retained-session certificate inputs while allowing subsequent channels and newly created sessions to use the replacement certificate.
  • Adds certificate-rotation, refresh, reverse-transport, request-failure, and channel-readiness coverage plus client documentation and configuration examples.

Diagram

sequenceDiagram
    participant Session as Session FSM
    participant Client as OpcUaClient
    participant Refresh as EndpointRefresh
    participant Resolver as EndpointResolver
    participant Transport as Client Transport
    participant Server as OPC UA Server

    Session->>Transport: Reconnect using cached endpoint certificate
    Transport->>Server: Establish SecureChannel
    Server-->>Transport: Handshake failure for stale certificate
    Transport-->>Refresh: onConnectFailure(failure)
    Refresh->>Resolver: resolve()
    Resolver->>Server: GetEndpoints
    Server-->>Resolver: Endpoints with replacement certificate
    Resolver-->>Refresh: Equivalent endpoint configuration
    Refresh->>Refresh: Publish refreshed endpoint
    Session->>Transport: Retry channel establishment
    Transport->>Server: Establish SecureChannel with replacement binding
    Server-->>Transport: Channel ready
    Transport-->>Refresh: onConnected()
    Refresh->>Refresh: Reset cooldown
    Session->>Transport: Build and send reactivation request on ready channel
    Server-->>Session: Reactivated session
Loading

@kevinherron
kevinherron merged commit a2cf9e0 into integration/1.2 Sep 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant