mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-22 17:50:03 +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
76 lines
2.9 KiB
C#
76 lines
2.9 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
#nullable enable
|
|
|
|
using System;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
|
|
namespace Opc.Ua.Tests
|
|
{
|
|
/// <summary>
|
|
/// Shared deadline-based polling helpers for tests that await a
|
|
/// condition produced by background work. Use this instead of adding
|
|
/// another private <c>WaitUntilAsync</c> copy so CI timeout tuning
|
|
/// happens in one place.
|
|
/// </summary>
|
|
public static class TestPolling
|
|
{
|
|
/// <summary>
|
|
/// Polls <paramref name="condition"/> until it returns
|
|
/// <see langword="true"/> or the timeout elapses, failing the test
|
|
/// with <paramref name="timeoutMessage"/> on expiry.
|
|
/// </summary>
|
|
/// <param name="condition">The condition to await.</param>
|
|
/// <param name="timeout">
|
|
/// Maximum time to wait; defaults to 10 seconds.
|
|
/// </param>
|
|
/// <param name="timeoutMessage">
|
|
/// The assertion message used when the deadline expires.
|
|
/// </param>
|
|
public static async Task WaitUntilAsync(
|
|
Func<bool> condition,
|
|
TimeSpan? timeout = null,
|
|
string timeoutMessage = "Timed out waiting for the condition.")
|
|
{
|
|
DateTime deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(10));
|
|
while (!condition())
|
|
{
|
|
if (DateTime.UtcNow > deadline)
|
|
{
|
|
Assert.Fail(timeoutMessage);
|
|
}
|
|
|
|
await Task.Delay(10).ConfigureAwait(false);
|
|
}
|
|
}
|
|
}
|
|
}
|