Files
UA-.NETStandard/tests/Opc.Ua.Gds.Tests/FinishRequestIssueFailureTests.cs
romanettandClaude Opus 5.5 c56d58d094 Fix GDS FindApplications authorization and CSR key type checks (#4587, #4588) (#4591)
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>
2026-09-29 14:29:39 +00:00

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)]
};
}
}
}