Files
UA-.NETStandard/tests/Opc.Ua.Test.Common
romanett f96ca6df0b Enforce trust-material changes on live channels regardless of origin (#4357)
## Proposed changes

When a CRL (or any other trust-material change) reaches the server,
connected clients whose certificates it revokes should be disconnected
immediately — not at their next security-token renewal (default lifetime
1 h). The `ApplyChanges` push path already ran the OPC 10000-12 §7.10.9
sweep, but three gaps kept a revoked client connected:

1. **The push path never invalidated the validator.** `TrustList` writes
certificates/CRLs directly to the stores;
`CertificateManager.NotifyTrustListChanged` (the only thing that drops
the cached validation cores) was never called. Freshness relied on the
directory store's `(name, length, mtime)` + 30 s cache heuristic, which
non-directory `ICertificateStore` implementations don't even guarantee.
Worse: with `SecurityConfiguration.UseValidatedCertificates = true`, the
thumbprint fast path returned **before the revocation check**, so a
pushed CRL was never enforced at all for a previously-seen client — not
at renewal, not even on a fresh channel.
2. **Out-of-band changes had no server-side consumer.**
`CertificateChangeKind.TrustListUpdated`/`CrlUpdated` (raised by
`WriteTrustListAsync`, `ITrustListTransaction` commits, HA shared-store
sync) were only observed by the client-side `ManagedSession`;
enforcement waited for the token renewal.
3. The legacy non-transactional `TrustList` (immediate apply,
`applyChangesRequired = false`) ran no effects at all.

### Changes

- `NotifyTrustListChanged` is now part of `ICertificateTrustListManager`
(public on `CertificateManager`). The `ApplyChanges` deferred apply
calls it for every committed scope after the response boundary and
**before** the §7.10.9 sweep, so the sweep provably observes the
committed stores. The legacy `TrustList` gets the same via an opt-in
`SetTrustListChangeNotifier`.
- The `UseValidatedCertificates` fast path in
`CertificateValidationCore` moved after the issuer walk: revocation is
always evaluated; a clean cache hit still skips the expensive chain
build (the performance intent is preserved, the security hole is not).
- New internal `CertificateChangePump<TState>`, extracted from
`ClientChannelManagerCertRotation` (which now runs on it, behavior
unchanged), coalesces `CertificateChanges` events.
`ConfigurationNodeManager` subscribes a scope-union pump so **any**
`TrustListUpdated`/`CrlUpdated` — whatever wrote the stores — triggers
the same `IPushConfigurationTrustListEffectHandler` fan-out. Self-raised
notifications are suppressed, so a push still runs exactly one sweep.
Session semantics are unchanged: peer revocation cuts only the
SecureChannel; only user-token-group changes close Sessions
(+Subscriptions).
- The per-group store identifiers preserve the configured `StoreType`
instead of re-inferring it from the path (fixes pushing through a
different store implementation than the validator reads for custom
providers, e.g. HA shared key-value stores).
- Integrated the caller-owned `CertificateStoreIdentifier.OpenStore`
contract from #4382: `TrustList` lazily holds its trusted/issuer store
instances for its lifetime, `ConfigurationNodeManager` owns its
rejected-store instance, and both node managers dispose the handlers
they own. This replaces the former interim identifier-cache cleanup
while keeping the shutdown leak gate clean.

### Behavior notes

- A server that previously kept revoked clients connected until token
renewal now cuts them as soon as an out-of-band CRL lands. Re-validation
failures fail open (a channel whose trust cannot be determined stays
open), and closed-channel counts are logged.
- Hosts using `UseValidatedCertificates` for throughput keep the
chain-build saving but lose a revocation-check skip they should never
have had.
- A bare file copied into a store directory raises no event and is still
only picked up at the next validation (bounded by the 30 s directory
cache for new channels).

### Tests

- `PushTest.PushedCrlDisconnectsRevokedClientAsync` — end-to-end: a
client with a CA-issued certificate connects, a CRL revoking it is
pushed via `CloseAndUpdate` + `ApplyChanges`, the server cuts its
channel right after the response and refuses its reconnects; the
unaffected admin client stays connected; trust material is restored
afterwards.
- `ConfigurationNodeManagerTrustListEffectsTests` — live-validator
enforcement after a pushed CRL, out-of-band pump dispatch per scope,
scope→effect mapping, store-type preservation.
-
`CertificateValidationCoreTests.ValidatedCertificateFastPathDoesNotSkipRevocationAsync`
— fails without the fast-path fix.
- `CertificateChangePumpTests` — coalescing, drain-until-empty, error
resilience, dispose semantics.
- `TrustListTests` — legacy notifier flags for
`CloseAndUpdate`/`AddCertificate`/`RemoveCertificate`.

Suites run locally (net10.0): GDS PushTest 314/314 (leak detector
clean), Server.Tests configuration/trust/push 416/416, Core.Tests
certificate/channel 556/556, Client.Tests 2161/2161; net48 build clean.

## Types of changes

- [x] Bugfix (non-breaking change which fixes an issue)
- [x] Enhancement (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

- [x] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [x] I ran tests locally with my changes.
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added necessary documentation (if appropriate).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Marc <marcschier@hotmail.com>
Copilot-Session: d043dc7d-5c08-4084-96be-1f64e5466525
2026-09-02 12:12:55 +00:00
..