mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-24 02:58:36 +08:00
Fixes #4587 Fixes #4588 ## #4587: FindApplications rejects anonymous / ApplicationSelfAdmin sessions OPC 10000-12 §6.5.3 says FindApplications "can be called by any Client", and §6.5.4 names no Role. `OnFindApplications` required `AuthenticatedUser`, which anonymous identities never have. So a §7.6 pull client (Anonymous + ApplicationSelfAdmin) could not find its own ApplicationId, and got `Bad_UserAccessDenied` even for unregistered URIs. - Removed the role check from `OnFindApplications`. This matches `QueryApplications` / `QueryServers`, which also have no handler check. - Moved `GetApplication` to the `SelfAdministered` node wiring so it matches its handler check (`AuthenticatedUserOrSelfAdmin`). The handler still limits self-admin callers to their own record. ## #4588: CSR key vs. requested CertificateType; FinishRequest error mapping **StartSigningRequest (§7.9.3)** now compares the CSR's public key with the resolved CertificateTypeId before queuing the request: | Case | Result | |---|---| | Key algorithm doesn't match the type (RSA CSR for ECC type, ECC CSR for RSA type) | `Bad_InvalidArgument`; the text names both | | ECC curve doesn't match (e.g. P-384 key for `EccNistP256…`) | `Bad_InvalidArgument` | | RSA key size outside the type's range (`RsaMin…` 1024–2048, `RsaSha256…` 2048–4096; §7.8.4.8/9) | `Bad_NotSupported` | | ECC type the group can't issue from a CSR (e.g. Curve25519/448) | `Bad_NotSupported` | The check is a new `ICertificateGroup.VerifySigningRequestAsync(application, certificateType, request, ct)` overload, which `StartSigningRequest` calls through the interface. `CertificateGroup` implements it as a virtual method that runs the existing CSR checks and then the new static `CertificateGroup.VerifySigningRequestKey`. `SigningRequestAsync` runs the same check, so requests queued before this change also fail with the exact reason. **FinishRequest (§7.9.5)** no longer replaces signing and key-pair failures with `Bad_ConfigurationError`, which the spec doesn't list. It now uses `ServiceResult.Create(Exception, …)`: a `ServiceResultException` keeps its status, other exceptions fall back to `Bad_RequestNotAllowed`, and the exception stays attached. The failure is also logged at Warning (new event id `ApplicationsNodeManager + 21`). An empty `ApplicationNames` no longer throws while the message is being built. ## Behaviour changes - **API:** `ICertificateGroup` gains a member. Custom implementations that don't derive from `CertificateGroup` must add the typed `VerifySigningRequestAsync` overload; they can call `CertificateGroup.VerifySigningRequestKey` for the key check. - Anonymous sessions can call FindApplications. Per the spec it returns only public registration data, the same data QueryApplications already returned. - StartSigningRequest can now fail on requests that it used to accept and that later failed in FinishRequest. - An RSA CSR whose key size is outside the requested type's range is rejected with `Bad_NotSupported`. Before, it was signed anyway. - FinishRequest status codes change from `Bad_ConfigurationError` to the underlying code, or to `Bad_RequestNotAllowed`. ## Tests - `CertificateGroupSigningRequestTests`: 6 new unit tests covering algorithm mismatch in both directions, curve mismatch, RSA size bounds, an unsupported ECC type, matching keys, and an undecodable CSR. - `ClientTest.FindApplicationsAsSelfAdminAsync` / `StartSigningRequestWithMismatchedKeyTypeAsSelfAdminAsync`: end to end over an anonymous self-admin session. - Local, net10.0: full `Opc.Ua.Gds.Tests` 1205 passed / 0 failed / 56 skipped. `ClientTest` 112/112, and both new tests pass in both fixture variants. The net48 build is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.4 KiB
C#
109 lines
4.4 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/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using NUnit.Framework;
|
|
using Opc.Ua.Gds.Server;
|
|
|
|
namespace Opc.Ua.Gds.Tests
|
|
{
|
|
/// <summary>
|
|
/// Tests the FinishRequest result for a certificate the group failed to
|
|
/// issue (OPC 10000-12 §7.9.5).
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("GDS")]
|
|
[Parallelizable]
|
|
public sealed class FinishRequestIssueFailureTests
|
|
{
|
|
private static readonly NodeId s_applicationId = new(Guid.NewGuid(), 2);
|
|
|
|
[Test]
|
|
public void ServiceResultExceptionKeepsItsStatus()
|
|
{
|
|
var exception = new ServiceResultException(
|
|
StatusCodes.BadInvalidArgument,
|
|
"The CertificateRequest has an RSA public key.");
|
|
|
|
ServiceResult result = ApplicationsNodeManager.CreateIssueFailureResult(
|
|
exception,
|
|
"Certificate",
|
|
s_applicationId,
|
|
CreateApplication("App"));
|
|
|
|
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadInvalidArgument));
|
|
Assert.That(result.LocalizedText.Text, Does.Contain("RSA public key"));
|
|
Assert.That(result.LocalizedText.Text, Does.Contain("ApplicationName=App"));
|
|
}
|
|
|
|
[Test]
|
|
public void OtherExceptionMapsToBadRequestNotAllowed()
|
|
{
|
|
var exception = new InvalidOperationException("CA key unavailable.");
|
|
|
|
ServiceResult result = ApplicationsNodeManager.CreateIssueFailureResult(
|
|
exception,
|
|
"New Key Pair Certificate",
|
|
s_applicationId,
|
|
CreateApplication("App"));
|
|
|
|
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadRequestNotAllowed));
|
|
Assert.That(result.StatusCode, Is.Not.EqualTo(StatusCodes.BadConfigurationError));
|
|
Assert.That(
|
|
result.LocalizedText.Text,
|
|
Does.Contain("New Key Pair Certificate=CA key unavailable."));
|
|
}
|
|
|
|
[Test]
|
|
public void EmptyApplicationNamesDoNotMaskTheFailure()
|
|
{
|
|
var exception = new ServiceResultException(StatusCodes.BadNotSupported, "Unsupported key.");
|
|
var application = new ApplicationRecordDataType { ApplicationUri = "urn:test:app" };
|
|
|
|
ServiceResult result = ApplicationsNodeManager.CreateIssueFailureResult(
|
|
exception,
|
|
"Certificate",
|
|
s_applicationId,
|
|
application);
|
|
|
|
Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadNotSupported));
|
|
Assert.That(result.LocalizedText.Text, Does.Contain("Unsupported key."));
|
|
Assert.That(result.LocalizedText.Text, Does.Contain("ApplicationUri=urn:test:app"));
|
|
}
|
|
|
|
private static ApplicationRecordDataType CreateApplication(string name)
|
|
{
|
|
return new ApplicationRecordDataType
|
|
{
|
|
ApplicationUri = "urn:test:app",
|
|
ApplicationNames = [new LocalizedText(name)]
|
|
};
|
|
}
|
|
}
|
|
} |