mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 06:31:35 +08:00
5572107ab8
# Description
Fixes found by a manual run of the OPC 10000-12 *Push Model for Global
Certificate and TrustList Management* test plan (183 test cases,
TC0–TC182) against the Reference Server. After these changes no test
case fails; the remaining non-pass verdicts are test-plan expectations
that contradict Part 12/Part 4/Part 20 (documented in the run report).
### Server behaviour changes
- **UpdateCertificate requires the issuer in the TrustList**
(`ConfigurationNodeManager.PushValidation.cs`). The validator classifies
an incomplete chain as suppressible; OPC 10000-4 Table 100 does not ("An
error during the chain creation may not be suppressed") and §7.10.5
requires the issuers to already be in the group's TrustList. A
certificate of an unknown CA is now rejected with
`Bad_CertificateChainIncomplete`. Expiry / host-name errors stay ignored
as §7.10.5 requires.
- **TrustList audit events reach the Server Object.**
`TrustListUpdateRequested`/`TrustListUpdated` were reported on the
TrustList node (`EventNotifier=0`, not linked to the Server), so no
client could receive them. `TrustList.SetAuditEventServer` routes them
through `IAuditEventServer`; wired for the Push TrustLists and the GDS
CertificateGroup TrustLists. New `AuditEvents` overloads keep the
node-based ones.
- **Duplicate AddCertificate** no longer makes the following
ApplyChanges fail with a bare `Bad` (store refused the second copy); the
commit is idempotent and its rollback keeps the pre-existing
certificate.
- **CloseAndUpdate validates CRLs** (incl. CRL-only updates): every CRL
must be signed by a CA of the new TrustList (§7.8.2.5). Previously an
unknown/forged CRL passed CloseAndUpdate and only failed ApplyChanges.
- **X.509 v1 certificates are rejected** by
CloseAndUpdate/AddCertificate/UpdateCertificate (OPC 10000-6 §6.2.2; new
`X509Utils.IsX509Version3`).
- **Application setup state (Annex G.2):** new
`StandardServer.StartupServerState` (virtual); the DI host returns
`NoConfiguration` when `ServerConfigurationOptions.InApplicationSetup ==
true`. `IsRunning` and `NodeManagerLifecycle` treat `NoConfiguration` as
operational. The Reference Server `--provision` mode starts in
`NoConfiguration` and exposes `InApplicationSetup=TRUE`.
- **DefaultHttpsGroup:** the group is only created when Https stores are
configured — the Reference Server config had none, so its
`opc.https`/`opc.wss` endpoints used the RSA application certificate
with an empty TLS trust list and the Https certificate could not be
pushed. The sample config now declares
`HttpsIssuerCertificates`/`TrustedHttpsCertificates` and an
`HttpsCertificateType` certificate (new `CertificateTypeString` value
`Https`). An Https certificate is assigned to DefaultHttpsGroup only (no
longer also listed in DefaultApplicationGroup). Fixed
`AddSecurityConfigurationHttpsStore`, which built the trusted Https
store path from the store *type*.
- Minor: AddCertificate logs a warning when it adds a CA without a CRL
while `RejectUnknownRevocationStatus` is enabled; UpdateCertificate
refused for an insufficient SecureChannel raises
`CertificateUpdateRequestedAuditEvent` with Status=false.
### Kept on purpose
- PEM certificates are accepted (converted to DER).
- `Bad_NotWritable` when opening an already open TrustList (OPC 10000-20
§4.2.2) and for AddCertificate while a read handle is open (§7.8.2.6).
- No validation rules for DefaultUserTokenGroup / DefaultHttpsGroup
TrustLists (Part 12 defines them only for ApplicationCertificateType
groups).
### Notes for reviewers
- Existing tests in `ConfigurationNodeManagerPushTests` and
`PushCertificateRequestRegressionTests` signed pushed certificates with
CAs that were not in the TrustList; they now trust their CA first
(behaviour change above).
- Adding the Https certificate to
`Quickstarts.ReferenceServer.Config.xml` gives the Reference Server's
opc.https/opc.wss endpoints their own TLS certificate.
### Tests
-
`tests/Opc.Ua.Server.Tests/Configuration/PushComplianceRegressionTests.cs`
— duplicate add/rollback, PEM, v1/v3, CRL issuer/forged/CRL-only, audit
routing.
- `tests/Opc.Ua.Server.Tests/ApplicationSetupServerStateTests.cs` —
NoConfiguration startup state.
- `tests/Opc.Ua.Gds.Tests/PushComplianceTests.cs` — end-to-end over a
secure channel: TrustList transaction lifecycle, open-handle rules,
masked reads, duplicate/PEM add, unknown CRL issuer, unknown certificate
issuer, cross-Session CSR key, nonce rules, RemoveCertificate chain/CRL
handling. Mutation-checked: reverting the issuer, duplicate-add and CRL
fixes makes the corresponding tests fail.
- Additional cases in `PushCertificateRequestRegressionTests`,
`CertificateIdentifierTests`, `ApplicationConfigurationBuilderTests`.
Local runs (net10.0): Opc.Ua.Gds.Tests 1257 passed / 0 failed / 56
skipped; Opc.Ua.Server.Tests push/TrustList/audit/lifecycle subset 1209
passed; Core and Configuration test classes passed. net48 build of the
affected test projects succeeds; `dotnet format whitespace` clean.
## Related Issues
- None (findings from a manual Push Model compliance run).
## Checklist
- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [ ] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.2 KiB
C#
180 lines
7.2 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 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/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Server.Hosting;
|
|
using Opc.Ua.Server.TestFramework;
|
|
using Opc.Ua.Tests;
|
|
|
|
namespace Opc.Ua.Server.Tests
|
|
{
|
|
/// <summary>
|
|
/// OPC 10000-12 Annex G.2: a server in the application setup state
|
|
/// reports ServerState NoConfiguration and still serves requests so it can
|
|
/// be provisioned.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("Server")]
|
|
[SetCulture("en-us")]
|
|
[SetUICulture("en-us")]
|
|
[NonParallelizable]
|
|
public class ApplicationSetupServerStateTests
|
|
{
|
|
[Test]
|
|
public async Task ServerInApplicationSetupReportsNoConfigurationAndServesRequestsAsync()
|
|
{
|
|
var fixture = new ServerFixture<ApplicationSetupServer>(
|
|
telemetry => new ApplicationSetupServer(telemetry))
|
|
{
|
|
SecurityNone = true
|
|
};
|
|
await fixture.LoadConfigurationAsync().ConfigureAwait(false);
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
StandardServer server = fixture.Server;
|
|
Assert.That(server.CurrentInstance.CurrentState, Is.EqualTo(ServerState.NoConfiguration));
|
|
Assert.That(server.CurrentInstance.IsRunning, Is.True);
|
|
|
|
Assert.That(await ReadServerStateAsync(server).ConfigureAwait(false),
|
|
Is.EqualTo(ServerState.NoConfiguration));
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task ServerStartsRunningByDefaultAsync()
|
|
{
|
|
var fixture = new ServerFixture<StandardServer>(telemetry => new StandardServer(telemetry))
|
|
{
|
|
SecurityNone = true
|
|
};
|
|
await fixture.LoadConfigurationAsync().ConfigureAwait(false);
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
Assert.That(fixture.Server.CurrentInstance.CurrentState, Is.EqualTo(ServerState.Running));
|
|
Assert.That(await ReadServerStateAsync(fixture.Server).ConfigureAwait(false),
|
|
Is.EqualTo(ServerState.Running));
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The hosted server derives its startup state from the registered
|
|
/// <see cref="ServerConfigurationOptions.InApplicationSetup"/>.
|
|
/// </summary>
|
|
[TestCase(true, ServerState.NoConfiguration)]
|
|
[TestCase(false, ServerState.Running)]
|
|
[TestCase(null, ServerState.Running)]
|
|
public async Task DependencyInjectionServerMapsInApplicationSetupToServerStateAsync(
|
|
bool? inApplicationSetup,
|
|
ServerState expected)
|
|
{
|
|
var services = new ServiceCollection();
|
|
if (inApplicationSetup.HasValue)
|
|
{
|
|
services.AddSingleton(new ServerConfigurationOptions { InApplicationSetup = inApplicationSetup });
|
|
}
|
|
using ServiceProvider provider = services.BuildServiceProvider();
|
|
var fixture = new ServerFixture<DependencyInjectionStandardServer>(
|
|
telemetry => new DependencyInjectionStandardServer(provider, telemetry, TimeProvider.System))
|
|
{
|
|
SecurityNone = true
|
|
};
|
|
await fixture.LoadConfigurationAsync().ConfigureAwait(false);
|
|
await fixture.StartAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
Assert.That(fixture.Server.CurrentInstance.CurrentState, Is.EqualTo(expected));
|
|
Assert.That(await ReadServerStateAsync(fixture.Server).ConfigureAwait(false), Is.EqualTo(expected));
|
|
}
|
|
finally
|
|
{
|
|
await fixture.StopAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
private static async Task<ServerState> ReadServerStateAsync(StandardServer server)
|
|
{
|
|
(RequestHeader requestHeader, SecureChannelContext secureChannelContext) =
|
|
await server.CreateAndActivateSessionAsync(TestContext.CurrentContext.Test.Name)
|
|
.ConfigureAwait(false);
|
|
try
|
|
{
|
|
ReadResponse response = await server.ReadAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
0,
|
|
TimestampsToReturn.Neither,
|
|
[
|
|
new ReadValueId
|
|
{
|
|
NodeId = VariableIds.Server_ServerStatus_State,
|
|
AttributeId = Attributes.Value
|
|
}
|
|
],
|
|
RequestLifetime.None).ConfigureAwait(false);
|
|
DataValue value = response.Results[0];
|
|
Assert.That(StatusCode.IsGood(value.StatusCode), Is.True);
|
|
Assert.That(value.WrappedValue.TryGetValue(out ServerState state), Is.True);
|
|
return state;
|
|
}
|
|
finally
|
|
{
|
|
await server.CloseSessionAsync(secureChannelContext, requestHeader, CancellationToken.None)
|
|
.ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A server that starts in the application setup state.
|
|
/// </summary>
|
|
public sealed class ApplicationSetupServer : StandardServer
|
|
{
|
|
public ApplicationSetupServer(ITelemetryContext telemetry)
|
|
: base(telemetry)
|
|
{
|
|
}
|
|
|
|
protected override ServerState StartupServerState => ServerState.NoConfiguration;
|
|
}
|
|
}
|
|
}
|