mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 05:11:13 +08:00
f96ca6df0b
## 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