mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 08:41:51 +08:00
c56d58d094
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>
771 lines
49 KiB
Markdown
771 lines
49 KiB
Markdown
# OPC UA Global Discovery Server (GDS) — Developer Guide
|
||
|
||
This package implements the OPC UA Global Discovery Server (GDS) as
|
||
defined in OPC 10000-12 (Part 12). It provides application
|
||
registration, certificate lifecycle management (pull and push models),
|
||
KeyCredentialService, AuthorizationService, and role-based access
|
||
control.
|
||
|
||
## Quick Links
|
||
|
||
| Topic | Document |
|
||
|-------|----------|
|
||
| GDS core (this file) | Directory, certificates, push management, roles |
|
||
| [KeyCredentialService](KeyCredentialService.md) | Credential issuance for non-UA services |
|
||
| [AuthorizationService](AuthorizationService.md) | OAuth2-style access token issuance |
|
||
|
||
## Contents
|
||
|
||
- [Quick Links](#quick-links)
|
||
- [Packages](#packages)
|
||
- [1. Client API](#1-client-api)
|
||
- [GlobalDiscoveryServerClient](#globaldiscoveryserverclient)
|
||
- [ServerPushConfigurationClient](#serverpushconfigurationclient)
|
||
- [OPC 10000-21 OnboardingClient](#opc-10000-21-onboardingclient)
|
||
- [2. Server-Side: Building a GDS](#2-server-side-building-a-gds)
|
||
- [Minimal GDS Server](#minimal-gds-server)
|
||
- [Using GdsNodeManagerFactory](#using-gdsnodemanagerfactory)
|
||
- [Extension Points](#extension-points)
|
||
- [3. Implementing Providers](#3-implementing-providers)
|
||
- [IApplicationsDatabase](#iapplicationsdatabase)
|
||
- [ICertificateGroup](#icertificategroup)
|
||
- [IGdsUserDatabase](#igdsuserdatabase)
|
||
- [IConfigurationDataStore](#iconfigurationdatastore)
|
||
- [4. Roles and Authorization](#4-roles-and-authorization)
|
||
- [GDS Roles (OPC 10000-12 §7.2)](#gds-roles-opc-10000-12-72)
|
||
- [ApplicationSelfAdmin Privilege](#applicationselfadmin-privilege)
|
||
- [ApplicationAdmin Privilege](#applicationadmin-privilege)
|
||
- [Security: Fail-Closed Channel Validation](#security-fail-closed-channel-validation)
|
||
- [5. End-to-End Example](#5-end-to-end-example)
|
||
- [Conformance Matrix](#conformance-matrix)
|
||
- [Applicable Part 12 profiles and conformance units](#applicable-part-12-profiles-and-conformance-units)
|
||
- [Automated verification](#automated-verification)
|
||
- [GDS Directory (§6.5)](#gds-directory-65)
|
||
- [Certificate Management — Pull Model (§7.9)](#certificate-management--pull-model-79)
|
||
- [Roles and Privileges (§7.2)](#roles-and-privileges-72)
|
||
- [Push Management — ServerConfiguration (§7.10)](#push-management--serverconfiguration-710)
|
||
- [Transaction model, diagnostics and lifecycle (§7.10.2, §7.10.9, §7.10.11, §7.10.17)](#transaction-model-diagnostics-and-lifecycle-7102-7109-71011-71017)
|
||
- [Optional ServerConfiguration surface (§7.10.13, §7.10.14, §7.10.20)](#optional-serverconfiguration-surface-71013-71014-71020)
|
||
- [GDS-managed applications proxy (§7.10.14–§7.10.16)](#gds-managed-applications-proxy-7101471016)
|
||
- [TrustList (§7.8)](#trustlist-78)
|
||
- [Certificate and TrustList alarms (§7.8.3)](#certificate-and-trustlist-alarms-783)
|
||
- [TrustList-change effects on channels and Sessions (§7.10.9)](#trustlist-change-effects-on-channels-and-sessions-7109)
|
||
- [Audit Events](#audit-events)
|
||
- [KeyCredentialService (§8)](#keycredentialservice-8)
|
||
- [AuthorizationService (§9)](#authorizationservice-9)
|
||
- [Refresh tokens](#refresh-tokens)
|
||
- [Remaining optional / unsupported Part 12 items](#remaining-optional--unsupported-part-12-items)
|
||
- [LDS / LDS-ME (§4–5)](#lds--lds-me-45)
|
||
- [Extension Points (Phase 2 Abstractions)](#extension-points-phase-2-abstractions)
|
||
|
||
## Packages
|
||
|
||
| NuGet Package | Contents |
|
||
|---------------|----------|
|
||
| `Opc.Ua.Gds` | Part 12 and Part 21 model types, node states, identifiers, encodeable types |
|
||
| `Opc.Ua.Gds.Server` | Server-side node managers, providers, authorization, onboarding ticket-store binding |
|
||
| `Opc.Ua.Gds.Client` | Client proxies for GDS, push, KeyCredential, AuthorizationService, onboarding |
|
||
|
||
---
|
||
|
||
## 1. Client API
|
||
|
||
### GlobalDiscoveryServerClient
|
||
|
||
The primary client for interacting with a GDS. Manages its own
|
||
session and exposes the full GDS directory and certificate
|
||
management API.
|
||
|
||
```csharp
|
||
using Opc.Ua.Gds.Client;
|
||
|
||
// Create and connect
|
||
var gdsClient = new GlobalDiscoveryServerClient(appConfig);
|
||
gdsClient.AdminCredentials = new UserIdentity("admin", "password");
|
||
await gdsClient.ConnectAsync("opc.tcp://gds-host:4840");
|
||
|
||
// Register an application
|
||
var appRecord = new ApplicationRecordDataType
|
||
{
|
||
ApplicationUri = "urn:my-app",
|
||
ApplicationType = ApplicationType.Server,
|
||
ApplicationNames = new[] { new LocalizedText("My App") }.ToArrayOf(),
|
||
DiscoveryUrls = new[] { "opc.tcp://my-app:4850" }.ToArrayOf()
|
||
};
|
||
NodeId appId = await gdsClient.RegisterApplicationAsync(appRecord);
|
||
|
||
// Request a new certificate (pull model)
|
||
NodeId requestId = await gdsClient.StartNewKeyPairRequestAsync(
|
||
appId, default, ObjectTypeIds.RsaSha256ApplicationCertificateType,
|
||
"CN=My App", domainNames, "PFX", null);
|
||
|
||
// Poll for completion
|
||
ByteString cert, privateKey;
|
||
ArrayOf<ByteString> issuerCerts;
|
||
do
|
||
{
|
||
await Task.Delay(2000);
|
||
(cert, privateKey, issuerCerts) =
|
||
await gdsClient.FinishRequestAsync(requestId, default);
|
||
} while (cert.IsEmpty);
|
||
|
||
// Clean up
|
||
await gdsClient.DisconnectAsync();
|
||
```
|
||
|
||
### ServerPushConfigurationClient
|
||
|
||
Client for OPC 10000-12 §7.10 push certificate management on a
|
||
target server:
|
||
|
||
```csharp
|
||
var pushClient = new ServerPushConfigurationClient(appConfig);
|
||
await pushClient.ConnectAsync(serverEndpoint);
|
||
|
||
// Push a new certificate
|
||
bool restartNeeded = await pushClient.UpdateCertificateAsync(
|
||
pushClient.DefaultApplicationGroup,
|
||
ObjectTypeIds.RsaSha256ApplicationCertificateType,
|
||
newCertBlob, "PFX", privateKeyBlob, issuerCerts);
|
||
|
||
if (restartNeeded)
|
||
{
|
||
await pushClient.ApplyChangesAsync();
|
||
}
|
||
|
||
// Create a self-signed certificate on the server
|
||
await pushClient.CreateSelfSignedCertificateAsync(
|
||
pushClient.DefaultApplicationGroup,
|
||
ObjectTypeIds.RsaSha256ApplicationCertificateType,
|
||
"CN=NewSubject", domainNames);
|
||
```
|
||
|
||
### OPC 10000-21 OnboardingClient
|
||
|
||
`Opc.Ua.Gds` ships the generated `Opc.Ua.Onboarding` model,
|
||
including the well-known `DeviceRegistrar` instance and the typed
|
||
`DeviceRegistrarAdminTypeClient`. `OnboardingClient` is the DI-friendly
|
||
facade for loading and removing encoded tickets:
|
||
|
||
```csharp
|
||
NodeId registrarId = ExpandedNodeId.ToNodeId(
|
||
Opc.Ua.Onboarding.ObjectIds.DeviceRegistrar_Administration,
|
||
session.NamespaceUris);
|
||
|
||
var onboarding = new OnboardingClient(session, registrarId, telemetry);
|
||
ArrayOf<StatusCode> results = await onboarding.RegisterTicketsAsync(
|
||
[
|
||
new ByteString(firstTicket),
|
||
new ByteString(secondTicket)
|
||
]);
|
||
```
|
||
|
||
The public contract follows Part 21: tickets are
|
||
`ArrayOf<ByteString>` (`EncodedTicket` is a ByteString subtype), and
|
||
the per-ticket results are `ArrayOf<StatusCode>`.
|
||
|
||
On the server, load the generated model and bind the standard
|
||
administration object to an injected ticket store:
|
||
|
||
```csharp
|
||
NodeStateCollection nodes = new NodeStateCollection()
|
||
.AddOpcUaGds(context)
|
||
.AddOpcUaOnboarding(context);
|
||
DeviceRegistrarState registrar =
|
||
nodes.OfType<DeviceRegistrarState>().Single();
|
||
|
||
registrar.Administration!.BindToTicketStore(ticketStore);
|
||
```
|
||
|
||
The handlers are asynchronous, pass request cancellation to
|
||
`ITicketStore`, and populate the single declared `Results` output.
|
||
See the runnable
|
||
[`samples/Gds` onboarding demo](../samples/Gds/README.md) for a secure
|
||
registrar/client round trip.
|
||
|
||
---
|
||
|
||
## 2. Server-Side: Building a GDS
|
||
|
||
### Minimal GDS Server
|
||
|
||
Use `GlobalDiscoverySampleServer` with dependency injection of
|
||
your storage backends:
|
||
|
||
```csharp
|
||
// 1. Create storage backends
|
||
IApplicationsDatabase database = JsonApplicationsDatabase.Load(
|
||
"gds-applications.json", telemetry);
|
||
ICertificateGroup certGroup = new CertificateGroup(
|
||
gdsConfig.AuthoritiesStorePath, gdsConfig.CertificateGroups);
|
||
IUserDatabase userDb = JsonUserDatabase.Load(
|
||
"gds-users.json", telemetry);
|
||
|
||
// 2. Create the GDS server
|
||
// (database implements both IApplicationsDatabase and ICertificateRequest)
|
||
var gdsServer = new GlobalDiscoverySampleServer(
|
||
database, database, certGroup, userDb, telemetry,
|
||
autoApprove: true);
|
||
|
||
// 3. Start via ApplicationInstance
|
||
var app = new ApplicationInstance(telemetry)
|
||
{
|
||
ApplicationName = "My GDS",
|
||
ApplicationType = ApplicationType.Server,
|
||
ConfigSectionName = "Opc.Ua.GlobalDiscoveryServer"
|
||
};
|
||
var config = await app.LoadAsync("MyGds.Config.xml");
|
||
await app.CheckApplicationInstanceCertificatesAsync(false);
|
||
await app.StartAsync(gdsServer);
|
||
```
|
||
|
||
### Using GdsNodeManagerFactory
|
||
|
||
For embedding GDS into an existing server:
|
||
|
||
```csharp
|
||
// In your server's CreateMasterNodeManager:
|
||
var gdsConfig = configuration.ParseExtension<GlobalDiscoveryServerConfiguration>();
|
||
var factory = new GdsNodeManagerFactory(gdsConfig);
|
||
additionalNodeManagers.Add(factory.Create(server, configuration));
|
||
```
|
||
|
||
### Extension Points
|
||
|
||
Every pluggable interface and its purpose:
|
||
|
||
| Interface | Purpose | Default |
|
||
|-----------|---------|---------|
|
||
| `IApplicationsDatabase` | Application registration store | `LinqApplicationsDatabase` / `JsonApplicationsDatabase` |
|
||
| `ICertificateRequest` | Certificate request lifecycle | `LinqApplicationsDatabase` |
|
||
| `ICertificateGroup` | CA operations (sign, revoke) | `CertificateGroup` |
|
||
| `IGdsUserDatabase` | User store + ApplicationAdmin bindings | Implement over `LinqUserDatabase` |
|
||
| `IKeyCredentialRequestStore` | KeyCredential lifecycle | `InMemoryKeyCredentialRequestStore` |
|
||
| `ISecretStore` | Secret storage for credentials | `InMemorySecretStore` |
|
||
| `IAccessTokenProvider` | Token issuance (AuthorizationService) | `null` (Bad_NotSupported) |
|
||
| `IConfigurationDataStore` | ManagedApplications config persistence | `InMemoryConfigurationDataStore` |
|
||
| `IManagedApplicationsNodeManager` | ManagedApplications folder | `StubManagedApplicationsNodeManager` |
|
||
|
||
---
|
||
|
||
## 3. Implementing Providers
|
||
|
||
### IApplicationsDatabase
|
||
|
||
Stores registered application records. Must also implement
|
||
`ICertificateRequest` for certificate request lifecycle (or provide
|
||
a separate implementation):
|
||
|
||
```csharp
|
||
public class SqlApplicationsDatabase
|
||
: ApplicationsDatabaseBase, ICertificateRequest
|
||
{
|
||
// ApplicationsDatabaseBase provides Match() and ValidateApplication()
|
||
// Override the abstract CRUD methods:
|
||
|
||
public override NodeId RegisterApplication(
|
||
ApplicationRecordDataType application)
|
||
{
|
||
ValidateApplication(application); // call base validation
|
||
return _db.InsertApplication(application);
|
||
}
|
||
|
||
public override void UpdateApplication(
|
||
ApplicationRecordDataType application)
|
||
{
|
||
ValidateApplication(application);
|
||
_db.UpdateApplication(application);
|
||
}
|
||
// ... implement remaining abstract methods
|
||
}
|
||
```
|
||
|
||
### ICertificateGroup
|
||
|
||
Implement to use an external CA (EST, ACME, EJBCA, etc.):
|
||
|
||
```csharp
|
||
public class EstCertificateGroup : ICertificateGroup
|
||
{
|
||
public async Task<(Certificate, byte[], CertificateCollection)>
|
||
NewKeyPairRequestAsync(
|
||
ApplicationRecordDataType application,
|
||
string subjectName,
|
||
string[] domainNames,
|
||
string privateKeyFormat,
|
||
string privateKeyPassword)
|
||
{
|
||
// Call your EST server's /simpleenroll endpoint
|
||
// Return (signedCert, privateKey, issuerChain)
|
||
}
|
||
// ... implement remaining interface methods
|
||
}
|
||
```
|
||
|
||
### IGdsUserDatabase
|
||
|
||
Extend `IUserDatabase` to support the `ApplicationAdmin` role
|
||
(OPC 10000-12 §7.2). The GDS sample server automatically detects
|
||
`IGdsUserDatabase` during username authentication:
|
||
|
||
```csharp
|
||
public class MyGdsUserDatabase : LinqUserDatabase, IGdsUserDatabase
|
||
{
|
||
private readonly Dictionary<string, List<NodeId>> _appAdminMap = new();
|
||
|
||
public IReadOnlyList<NodeId>? GetAdministeredApplicationIds(
|
||
string userName)
|
||
{
|
||
return _appAdminMap.TryGetValue(userName, out var ids)
|
||
? ids : null;
|
||
}
|
||
|
||
// Admin API to grant ApplicationAdmin for specific apps:
|
||
public void GrantApplicationAdmin(
|
||
string userName, IEnumerable<NodeId> applicationIds)
|
||
{
|
||
_appAdminMap[userName] = applicationIds.ToList();
|
||
}
|
||
}
|
||
```
|
||
|
||
### IConfigurationDataStore
|
||
|
||
Persistence backend for ManagedApplications (§7.10.16). The
|
||
`DefaultManagedApplicationsNodeManager` uses this to populate
|
||
`ApplicationConfigurationState` nodes:
|
||
|
||
```csharp
|
||
public class FileConfigurationDataStore : IConfigurationDataStore
|
||
{
|
||
public async ValueTask<IReadOnlyList<ManagedApplicationInfo>>
|
||
GetManagedApplicationsAsync(CancellationToken ct)
|
||
{
|
||
// Read from config file / database
|
||
}
|
||
|
||
public async ValueTask<uint> WriteConfigurationAsync(
|
||
string applicationUri, byte[] data,
|
||
uint currentVersion, CancellationToken ct)
|
||
{
|
||
// Persist with optimistic concurrency check
|
||
}
|
||
|
||
public async ValueTask ConfirmUpdateAsync(
|
||
string applicationUri, uint configVersion,
|
||
CancellationToken ct)
|
||
{
|
||
// Mark the configuration as applied
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Roles and Authorization
|
||
|
||
### GDS Roles (OPC 10000-12 §7.2)
|
||
|
||
| Role | Purpose |
|
||
|------|---------|
|
||
| `DiscoveryAdmin` | Register/update/unregister any application |
|
||
| `CertificateAuthorityAdmin` | Manage certificate requests and trust lists |
|
||
| `RegistrationAuthorityAdmin` | Approve/reject certificate requests |
|
||
| `ApplicationSelfAdmin` | Manage own application's registration |
|
||
| `ApplicationAdmin` | Manage a configured set of applications |
|
||
|
||
### ApplicationSelfAdmin Privilege
|
||
|
||
`GdsApplicationSelfAdminProvider` grants `GdsRole.ApplicationSelfAdmin`
|
||
when the secure-channel client certificate matches a certificate stored
|
||
for the registered `ApplicationRecordDataType` found by the channel
|
||
ApplicationUri. The provider is an `IIdentityAugmenter`, so it layers
|
||
SelfAdmin on top of any accepted identity instead of using a custom
|
||
`ImpersonateUser` callback.
|
||
|
||
For dependency-injection-hosted GDS servers the provider is registered with the symmetric
|
||
builder extension:
|
||
|
||
```csharp
|
||
services.AddOpcUa()
|
||
.AddGdsServer(opt => opt.ApplicationName = "MyGds")
|
||
.AddDefaultIdentityAuthenticators(opt => opt.EnableJwt = false)
|
||
.AddGdsApplicationSelfAdminProvider();
|
||
```
|
||
|
||
`AddDefaultIdentityAuthenticators(...)` on `IGdsServerBuilder` also
|
||
adds this provider so standard GDS hosts get OPC 10000-12 §7.2
|
||
SelfAdmin behavior automatically.
|
||
|
||
### ApplicationAdmin Privilege
|
||
|
||
The `ApplicationAdmin` role allows a user to administer a specific
|
||
set of applications (not all applications like `DiscoveryAdmin`).
|
||
|
||
**How it works:**
|
||
|
||
1. Assign the `GdsRole.ApplicationAdmin` role to the user via
|
||
`IUserDatabase.CreateUser()`
|
||
2. Implement `IGdsUserDatabase.GetAdministeredApplicationIds()` to
|
||
return the `ApplicationId`s the user may administer
|
||
3. During username authentication, `GlobalDiscoverySampleServer` constructs a
|
||
`GdsRoleBasedIdentity` with `AdministeredApplicationIds` populated
|
||
4. `AuthorizationHelper.CheckApplicationAdminPrivilege()` verifies
|
||
the target application is in the user's administered set
|
||
|
||
```csharp
|
||
// Setup: create a user with ApplicationAdmin role
|
||
userDb.CreateUser("app-admin", passwordBytes,
|
||
[GdsRole.ApplicationAdmin]);
|
||
|
||
// If using IGdsUserDatabase:
|
||
((MyGdsUserDatabase)userDb).GrantApplicationAdmin(
|
||
"app-admin",
|
||
[registeredAppId1, registeredAppId2]);
|
||
```
|
||
|
||
### Security: Fail-Closed Channel Validation
|
||
|
||
`AuthorizationHelper.HasAuthenticatedSecureChannel()` enforces that
|
||
all GDS methods are called over a signed (or encrypted) secure
|
||
channel. The check **fails closed** — if the system context cannot
|
||
be validated, the method throws `BadSecurityModeInsufficient`.
|
||
|
||
---
|
||
|
||
## 5. End-to-End Example
|
||
|
||
Complete example: register an application, get a certificate, push
|
||
it to the server.
|
||
|
||
```csharp
|
||
// ── 1. Start the GDS Server ─────────────────────────
|
||
|
||
var database = JsonApplicationsDatabase.Load("apps.json", telemetry);
|
||
var certGroup = new CertificateGroup(authStorePath, certGroupConfigs);
|
||
var userDb = JsonUserDatabase.Load("users.json", telemetry);
|
||
|
||
var gds = new GlobalDiscoverySampleServer(
|
||
database, database, certGroup, userDb, telemetry);
|
||
|
||
var app = new ApplicationInstance(telemetry) { ... };
|
||
await app.StartAsync(gds);
|
||
|
||
// ── 2. Client: Register and Get Certificate ─────────
|
||
|
||
var gdsClient = new GlobalDiscoveryServerClient(clientConfig);
|
||
gdsClient.AdminCredentials = new UserIdentity("admin", "password");
|
||
await gdsClient.ConnectAsync("opc.tcp://localhost:58810");
|
||
|
||
// Register
|
||
var record = new ApplicationRecordDataType
|
||
{
|
||
ApplicationUri = "urn:my-server",
|
||
ApplicationType = ApplicationType.Server,
|
||
ApplicationNames = new[] { new LocalizedText("My Server") }.ToArrayOf(),
|
||
DiscoveryUrls = new[] { "opc.tcp://my-server:4840" }.ToArrayOf()
|
||
};
|
||
NodeId appId = await gdsClient.RegisterApplicationAsync(record);
|
||
|
||
// Request certificate
|
||
NodeId reqId = await gdsClient.StartNewKeyPairRequestAsync(
|
||
appId, default,
|
||
ObjectTypeIds.RsaSha256ApplicationCertificateType,
|
||
"CN=My Server,O=My Org",
|
||
new[] { "my-server", "localhost" }.ToArrayOf(),
|
||
"PFX", null);
|
||
|
||
// Wait for completion
|
||
ByteString cert, key;
|
||
ArrayOf<ByteString> issuers;
|
||
do
|
||
{
|
||
await Task.Delay(2000);
|
||
(cert, key, issuers) =
|
||
await gdsClient.FinishRequestAsync(reqId, default);
|
||
} while (cert.IsEmpty);
|
||
|
||
// ── 3. Push Certificate to Target Server ────────────
|
||
|
||
var pushClient = new ServerPushConfigurationClient(clientConfig);
|
||
await pushClient.ConnectAsync(serverEndpoint);
|
||
|
||
bool needsRestart = await pushClient.UpdateCertificateAsync(
|
||
pushClient.DefaultApplicationGroup,
|
||
ObjectTypeIds.RsaSha256ApplicationCertificateType,
|
||
cert, "PFX", key, issuers);
|
||
|
||
if (needsRestart)
|
||
{
|
||
await pushClient.ApplyChangesAsync();
|
||
}
|
||
|
||
// ── 4. Clean Up ─────────────────────────────────────
|
||
|
||
await pushClient.DisconnectAsync();
|
||
await gdsClient.DisconnectAsync();
|
||
```
|
||
|
||
---
|
||
|
||
## Conformance Matrix
|
||
|
||
Status key: ✅ Implemented · ⚠️ Partial · 🔲 Optional, provider-gated (exposed only when configured) · ❌ Not implemented · N/A Not applicable
|
||
|
||
This matrix links every implemented OPC 10000-12 (Part 12) requirement in the ServerConfiguration / Push Certificate Management, TrustList, certificate-alarm, KeyCredentialService and AuthorizationService areas to the source that implements it and to the automated tests that exercise it. Clause numbers are **OPC 10000-12 v1.05.07**. When Part 12 behaviour changes, keep the matrix truthful: every new Method, alarm or optional child gets a row with its source *and* at least one test, and any behaviour that regresses to partial/unsupported is downgraded here. The push/transaction behaviour summarised below is documented in depth in [CertificateManager.md → PushManagement Transactions](CertificateManager.md#pushmanagement-transactions-opc-ua-part-12-7102-71011).
|
||
|
||
### Applicable Part 12 profiles and conformance units
|
||
|
||
The server-side Part 12 features map to these Facets in the OPC UA profile registry (`profiles.opcfoundation.org`, profile group **UACore 1.05**). Conformance units are marked (M)andatory or (O)ptional as declared by the Facet.
|
||
|
||
| Facet (UACore 1.05) | Key conformance units | Repo status |
|
||
|---------------------|-----------------------|-------------|
|
||
| Global Certificate Management Server Facet (`Server/GlobalCertificateManagement`) | *Pull or Push Model* (M); *Push Model for Global Certificate and TrustList Management* (O); *Pull Model for Global Certificate and TrustList Management* (O) | ✅ Push model (`ServerConfiguration`); ✅ Pull model (GDS `CertificateDirectory`) |
|
||
| A & C CertificateExpiration 2022 Server Facet (`Server/ACCertificateExpiration2022`) | *A & C CertificateExpiration*, *A & C Alarm*, *A & C Acknowledge* (M); *A & C Comment / Confirm / Shelving* (O) | ✅ mandatory CUs; optional Comment/Confirm/Shelving inherited from the base condition model |
|
||
| KeyCredential Service Server Facet (`Server/KeyCredentialManagement`) | *Push Model for KeyCredential Service*, *KeyCredential Authentication Mechanism Support* (M); *ProfileURI – MQTT UserName / AMQP SASL Plain / UA transport UserName* (O) | ✅ push model + client proxy; ProfileURIs are provider-defined |
|
||
| Global Service Authorization Request Server Facet (`Server/GlobalServiceAuthorization`) | *GDS Authorization Service Server* (M) | ✅ token issuance (`AuthorizationServiceManager`) |
|
||
| Authorization Service Server Facet (`Server/AuthorizationServiceConfiguration`) | *Authorization Service Configuration Server* (M) | ⚠️ access-token validation is provided through pluggable [Identity Providers](IdentityProviders.md); the Part 12 `ServerConfiguration.AuthorizationServices` configuration folder is not exposed |
|
||
| Global Discovery and Certificate Mgmt 2022 Server (`Server/GlobalDiscoveryAndCertificateManagement2022`) | *GDS Application Directory*, *GDS Certificate Manager Pull Model*, *GDS Query Applications*, *GDS LDS-ME Connectivity* (M) | ✅ full GDS server |
|
||
|
||
### Automated verification
|
||
|
||
The automated conformance checks that run in CI are the NUnit suites: the unit suites in `Opc.Ua.Server.Tests` (address-space exposure, transaction coordinator, TrustList staging, certificate alarms, optional surface) and the end-to-end GDS integration suites in `Opc.Ua.Gds.Tests` — notably `PushTest.cs`, which drives a live push server through the `ServerConfiguration` Methods, and `TrustListValidationTest.cs`. The applicable Facets / conformance units above were identified with the OPC UA profile-graph tooling against `profiles.opcfoundation.org`.
|
||
|
||
There is no fully-automated CTT harness in this repository, and the formal **OPC UA Compliance Test Tool (UACTT / CTT)** — a licensed OPC Foundation corporate-member GUI application — was **not** run to produce this matrix; no CTT results are claimed here. Run it against the reference server (`samples/Reference/ConsoleReferenceServer`; see [samples/README.md](../samples/README.md)) for formal certification. Known UACTT *script* defects encountered in prior manual runs are catalogued in [`plans/ctt-issues.md`](../plans/ctt-issues.md).
|
||
|
||
## GDS Directory (§6.5)
|
||
|
||
| Section | Feature | Status | Source | Tests |
|
||
|---------|---------|--------|--------|-------|
|
||
| §6.5.3 | DirectoryType | ✅ | `ApplicationsNodeManager.cs` | `ClientTest.cs`, `GdsApplicationDirectoryTests.cs` |
|
||
| §6.5.4 | FindApplications | ✅ | `OnFindApplications` (any Client, §6.5.3) | `GdsApplicationDirectoryTests.cs`, `ClientTest.FindApplicationsAsSelfAdminAsync` |
|
||
| §6.5.5 | ApplicationRecordDataType / rcp+ rules | ✅ | `ApplicationsDatabaseBase.ValidateApplication` | `ApplicationsDatabaseBaseTests.cs`, `LinqApplicationsDatabaseTests.cs` |
|
||
| §6.5.6 | RegisterApplication | ✅ | `OnRegisterApplication` + `DiscoveryAdminOrAppAdmin` | `GdsApplicationDirectoryTests.cs`, `RegisteredApplicationTests.cs` |
|
||
| §6.5.7 | UpdateApplication | ✅ | `OnUpdateApplication` + `DiscoveryAdminOrSelfAdminOrAppAdmin` | `GdsApplicationDirectoryTests.cs` |
|
||
| §6.5.8 | UnregisterApplication | ✅ | `OnUnregisterApplicationAsync` + cert revocation | `GdsApplicationDirectoryTests.cs`, `ClientTest.cs` |
|
||
| §6.5.9 | GetApplication | ✅ | `OnGetApplication` | `GdsApplicationDirectoryTests.cs` |
|
||
| §6.5.10 | QueryApplications | ✅ | `OnQueryApplications` | `GdsQueryApplicationsTests.cs` |
|
||
| §6.5.11 | QueryServers | ✅ | `OnQueryServers` | `GdsQueryApplicationsTests.cs`, `ClientTest.cs` |
|
||
| §6.5.12 | ApplicationRegistrationChangedAuditEvent | ✅ | `AuditEvents.ReportApplicationRegistrationChangedAuditEvent` | `AuditEventsTests.cs` |
|
||
|
||
## Certificate Management — Pull Model (§7.9)
|
||
|
||
Pull Certificate Management moved from §7.6 (1.04) to **§7.9** (v1.05.07); the `CertificateDirectoryType` Methods below use the current clause numbers.
|
||
|
||
| Section | Feature | Status | Source | Tests |
|
||
|---------|---------|--------|--------|-------|
|
||
| §7.9.3 | StartSigningRequest | ✅ | `OnStartSigningRequestAsync` + CSR key / CertificateType check + audit | `GdsCertificateManagementTests.cs`, `CertificateGroupSigningRequestTests.cs`, `ClientTest.cs` |
|
||
| §7.9.4 | StartNewKeyPairRequest | ✅ | `OnStartNewKeyPairRequest` + audit | `GdsCertificateManagementTests.cs`, `ClientTest.cs` |
|
||
| §7.9.5 | FinishRequest | ✅ | `OnFinishRequestAsync` + issuer chain | `GdsCertificateManagementTests.cs`, `ClientTest.cs` |
|
||
| §7.9.6 | RevokeCertificate | ✅ | `OnRevokeCertificateAsync` + audit | `GdsCertificateManagementTests.cs`, `CertificateGroupTests.cs` |
|
||
| §7.9.7 | GetCertificateGroups | ✅ | `OnGetCertificateGroups` | `ClientTest.cs`, `CustomCertificateGroupIntegrationTest.cs` |
|
||
| §7.9.8 | GetCertificates | ✅ | `OnGetCertificates` | `GdsCertificateManagementTests.cs` |
|
||
| §7.9.9 | GetTrustList | ✅ | `OnGetTrustList` | `ClientTest.cs`, `CustomCertificateGroupIntegrationTest.cs` |
|
||
| §7.9.10 | GetCertificateStatus | ✅ | `OnGetCertificateStatus` | `ClientTest.cs` |
|
||
| §7.9.11 | CheckRevocationStatus | ✅ | `OnCheckRevocationStatusAsync` + ValidityTime | `GdsCertificateManagementTests.cs` |
|
||
|
||
## Roles and Privileges (§7.2)
|
||
|
||
| Feature | Status | Source | Tests |
|
||
|---------|--------|--------|-------|
|
||
| DiscoveryAdmin | ✅ | `GdsRole.DiscoveryAdmin` | `AuthorizationHelperTests.HasAuthorizationSucceedsWhenUserHasDiscoveryAdminRole` |
|
||
| CertificateAuthorityAdmin | ✅ | `GdsRole.CertificateAuthorityAdmin` | `AuthorizationHelperTests.HasAuthorizationSucceedsWithCertificateAuthorityAdminRole` |
|
||
| RegistrationAuthorityAdmin | ✅ | `GdsRole.RegistrationAuthorityAdmin` | `AuthorizationHelperTests.cs` |
|
||
| ApplicationSelfAdmin | ✅ | `GdsRole.ApplicationSelfAdmin` | `AuthorizationHelperTests.HasAuthorizationSucceedsWithSelfAdminForOwnApplication`, `...ThrowsWithSelfAdminForDifferentApplication` |
|
||
| ApplicationAdmin | ✅ | `GdsRole.ApplicationAdmin` + `AdministeredApplicationIds` | `AuthorizationHelperTests.cs`, `GdsApplicationSelfAdminProviderTests.cs` |
|
||
| BadSecurityModeInsufficient (channel) | ✅ | `AuthorizationHelper.HasAuthenticatedSecureChannel` | `AuthorizationHelperTests.cs` (asserts `BadSecurityModeInsufficient`), `RoleAuthorizationGateTests.cs` |
|
||
|
||
## Push Management — ServerConfiguration (§7.10)
|
||
|
||
`ConfigurationNodeManager` implements the full push transaction model: every certificate/TrustList Method **stages** work that only takes effect at `ApplyChanges`. Clause numbers are corrected to v1.05.07 (the 1.04 numbering was §7.7.x).
|
||
|
||
| Section | Feature | Status | Source | Tests |
|
||
|---------|---------|--------|--------|-------|
|
||
| §7.10.5 | UpdateCertificate | ✅ | `ConfigurationNodeManager.UpdateCertificateAsync` (staged; ApplicationCertificate TrustList validation; supplied issuers ignored for app groups) | `ConfigurationNodeManagerPushTests` (`UpdateCertificateWith*`, `...IgnoresSuppliedIssuerCertificatesAsync`), `PushTest` (`UpdateCertificate*`) |
|
||
| §7.10.6 | CreateSelfSignedCertificate | ✅ | `...CreateSelfSignedCertificateAsync` (occupied slot → `Bad_InvalidState`; subject/SAN/key-size validation) | `ConfigurationNodeManagerPushValidationTests` (`CreateDefaultApplicationCertificate*`, `ValidateKeySize*`), `ConfigurationNodeManagerPushTests` (`CreateSelfSignedCertificate*`) |
|
||
| §7.10.7 | DeleteCertificate | ✅ | `...DeleteCertificateAsync` — empty slot → `Bad_InvalidState`; endpoint-reference check enforced at `ApplyChanges` | `ConfigurationNodeManagerPushTests` (`DeleteCertificate*`, `DeleteCertificateReferencedByEndpointIsRejectedByApplyChangesAsync`, `RotatedCertificateBecomesEndpointReferenced*`) |
|
||
| §7.10.8 | GetCertificates | ✅ | `...GetCertificates` (null group → `DefaultApplicationGroup`; occupied slots only, aligned pairs) | `ConfigurationNodeManagerPushValidationTests.SelectOccupiedCertificateSlotsOmitsEmptySlotsAndPreservesOrder`, `ConfigurationNodeManagerPushTests.GetCertificates*`, `PushTest.GetCertificatesAsync` |
|
||
| §7.10.9 | ApplyChanges | ✅ | `...ApplyChanges` (ordered commit + reverse rollback) | `PushConfigurationTransactionCoordinatorTests` (`ApplyChanges*`), `PushTest` (`ApplyChangesAsync`, `ApplyChangesForcesChannelRenegotiateAsync`) |
|
||
| §7.10.10 | CreateSigningRequest | ✅ | `...CreateSigningRequestAsync` (nonce ≥ 32 bytes genuinely mixed in) | `ConfigurationNodeManagerPushTests` (`CreateSigningRequest*`, `...WithRegenerateAndShortNonceThrowsBadInvalidArgument`), `PushTest.CreateSigningRequest*` |
|
||
| §7.10.11 | CancelChanges | ✅ | `...CancelChanges` (discard; `Bad_RequestCancelledByClient` diagnostics) | `PushConfigurationTransactionCoordinatorTests.CancelChanges*`, `ConfigurationNodeManagerPushTests.CancelChanges*` |
|
||
| §7.10.12 | GetRejectedList | ✅ | `...GetRejectedList` | `ConfigurationNodeManagerPushTests.GetRejectedList*`, `PushTest.GetRejectedListAsync` |
|
||
|
||
Per-Method SecureChannel/Role enforcement (`Bad_SecurityModeInsufficient` for the channel vs `Bad_UserAccessDenied` for the Role), the §7.10.10 `Nonce` entropy requirement, and the §7.10.7 endpoint-reference rule are described in [CertificateManager.md → PushManagement Transactions → *Method security and validation*](CertificateManager.md#pushmanagement-transactions-opc-ua-part-12-7102-71011).
|
||
|
||
### Transaction model, diagnostics and lifecycle (§7.10.2, §7.10.9, §7.10.11, §7.10.17)
|
||
|
||
| Requirement | Status | Source | Tests |
|
||
|-------------|--------|--------|-------|
|
||
| §7.10.2 transaction lifecycle: single owning Session; other Sessions get `Bad_TransactionPending`; ownership cleared atomically on apply | ✅ | `PushConfigurationTransactionCoordinator` | `PushConfigurationTransactionCoordinatorTests` (`StageStartsTransactionOwnedByStagingSession`, `StageFromAnotherSessionThrowsBadTransactionPending`, `ApplyChangesClearsOwnershipAtomicallySoANewSessionCanImmediatelyStageAsync`) |
|
||
| §7.10.2 `SupportsTransactions = true` exposed | ✅ | `ConfigurationNodeManager.CreateServerConfiguration` (the manager is split into `ConfigurationNodeManager.*.cs` concern files) | `ConfigurationNodeManagerPushTests.SupportsTransactionsIsExposedAndTrue` |
|
||
| Staging blocked during an in-flight commit (`Bad_InvalidState` same-owner / `Bad_TransactionPending` other Session) | ✅ | coordinator commit guard | `PushConfigurationTransactionCoordinatorTests` (`StageFromOwningSessionDuringInFlightCommitReturnsBadInvalidStateThenSucceedsAsync`, `StagingFromAnotherSessionDuringAnInFlightCommit...`) |
|
||
| §7.10.9 ordered commit, reverse-order rollback, `Bad_NothingToDo`, open-writer `Bad_InvalidState` | ✅ | coordinator prepare/commit/compensate | `PushConfigurationTransactionCoordinatorTests` (`ApplyChangesCommitsOperationsInRequestOrderAsync`, `ApplyChangesWithFailingCommitRollsBackEarlierOperationsInReverseOrderAsync`, `ApplyChangesWithNoActiveTransactionReturnsBadNothingToDoAsync`, `ApplyChangesWithOpenTrustListWriterReturnsBadInvalidStateWithoutClearingTransactionAsync`) |
|
||
| §7.10.11 wrong-Session apply/cancel → `Bad_SessionIdInvalid`; Session-close cancels its transaction | ✅ | coordinator ownership checks | `PushConfigurationTransactionCoordinatorTests` (`ApplyChangesFromWrongSessionReturnsBadSessionIdInvalidAsync`, `CancelChangesFromWrongSessionReturnsBadSessionIdInvalid`, `CancelForSessionClose*`) |
|
||
| §7.10.17 `TransactionDiagnostics` DataValue status: `Bad_OutOfService` before any transaction, `Bad_InvalidState` while active, `Good` after completion | ✅ | `ConfigurationNodeManager` diagnostics binding + coordinator snapshot | `ConfigurationNodeManagerPushTests` (`TransactionDiagnosticsReportBadOutOfServiceBeforeAnyTransactionAsync`, `...BadInvalidStateWhileTransactionActiveAsync`, `...GoodResultAfterCompletedTransactionAsync`), `PushConfigurationTransactionCoordinatorTests.GetSnapshot*` |
|
||
| §7.10.10 nonce entropy genuinely mixed into regenerated key (HMAC-DRBG) | ✅ | `AdditionalEntropyCertificateKeyGenerator` | `AdditionalEntropyCertificateKeyGeneratorTests.cs` |
|
||
| §7.10.10 regenerated private key persisted across Sessions and restart | ✅ | `DirectoryPendingCertificateKeyStore` / `InMemoryPendingCertificateKeyStore` | `PendingCertificateKeyStoreTests.cs` |
|
||
| Coordinator scoped per server (no cross-server sharing); injected coordinator honored | ✅ | `ConfigurationNodeManager` private per-server default | `ConfigurationNodeManagerPushTests` (`EachConfigurationNodeManagerOwnsADistinctDefaultCoordinator`, `AnInjectedCoordinatorIsSharedAcrossConfigurationNodeManagers`) |
|
||
| Deferred post-apply effects drained/cancelled on shutdown | ✅ | `ConfigurationNodeManager.DeleteAddressSpaceAsync` / `Dispose` | `ConfigurationNodeManagerShutdownTests.cs` |
|
||
|
||
### Optional ServerConfiguration surface (§7.10.13, §7.10.14, §7.10.20)
|
||
|
||
These optional members are exposed **only when their backing option/provider is configured** (see [CertificateManager.md → Optional ServerConfiguration Surface](CertificateManager.md#optional-serverconfiguration-surface-opc-ua-part-12-7103-71013-71020)); otherwise the node is suppressed.
|
||
|
||
| Member (§) | Status | Source | Tests |
|
||
|------------|--------|--------|-------|
|
||
| Identity variables `ApplicationUri` / `ProductUri` / `ApplicationType` / `ApplicationNames` (§7.10.3) | ✅ always | `ServerConfigurationOptions` from `ApplicationConfiguration` | `ServerConfigurationSurfaceTests.IdentityPropertiesAlwaysExposedAndValuedAsync` |
|
||
| `HasSecureElement` / `InApplicationSetup` (§7.10.3) | 🔲 provider-gated | `ServerConfigurationOptions` | `ServerConfigurationSurfaceTests` (`HasSecureElementExposedWithValueWhenConfiguredAsync`, `InApplicationSetupExposedWithValueWhenConfiguredAsync`, `OptionalMembersSuppressedWhenNotConfiguredAsync`) |
|
||
| `ResetToServerDefaults` (§7.10.13) | 🔲 provider-gated | `IServerConfigurationResetProvider` | `ServerConfigurationSurfaceTests.ResetToServerDefaultsExposedOnlyWhenProviderConfiguredAsync`, `ServerConfigurationResetTests.cs` |
|
||
| `ConfigurationFile` / `ApplicationConfigurationFileType` incl. `CloseAndUpdate` / `ConfirmUpdate` (§7.10.20) | 🔲 provider-gated | `IApplicationConfigurationFileProvider` + `ApplicationConfigurationFile.cs` | `ServerConfigurationSurfaceTests` (`ConfigurationFileExposedWithChildrenWhenProviderConfiguredAsync`, `ConfigurationFilePropertiesSeededFromProviderAsync`, `ConfigurationFileReadReturnsProviderContentAsync`) |
|
||
|
||
### GDS-managed applications proxy (§7.10.14–§7.10.16)
|
||
|
||
The GDS-side proxy that exposes *other* applications' configurations under a `ManagedApplications` folder is a separate, partially-implemented feature (out of scope for a self-managed push Server).
|
||
|
||
| Section | Feature | Status | Source |
|
||
|---------|---------|--------|--------|
|
||
| §7.10.14 | `ApplicationConfigurationType` | ⚠️ | `DefaultManagedApplicationsNodeManager` creates `ApplicationConfigurationState` nodes; `IConfigurationDataStore` abstracts persistence |
|
||
| §7.10.16 | `ManagedApplications` folder | ⚠️ | `DefaultManagedApplicationsNodeManager`; optimistic concurrency via version tracking |
|
||
|
||
## TrustList (§7.8)
|
||
|
||
| Section | Feature | Status | Source | Tests |
|
||
|---------|---------|--------|--------|-------|
|
||
| §7.8.2.2 | TrustListType / Open / OpenWithMasks (Read + Write+EraseExisting only) | ✅ | Core `TrustList.cs` | `TrustListTests.OpenWithMasksSyncAllReturnsGood` |
|
||
| §7.8.2.5 | CloseAndUpdate — staged, applied at `ApplyChanges` | ✅ | `TrustList.cs` + coordinator; `TrustListUpdatedAuditEvent` post-commit | `TrustListTransactionTests` (`CloseAndUpdateStagesInsteadOfApplyingImmediatelyAsync`, `...RollsBackWhenALaterOperationFailsAsync`, `...SelfCompensatesWhenOnlyOneOfSeveralStoresFailsAsync`), `PushTest.UpdateTrustListAsync` |
|
||
| §7.8.2.6/.7 | AddCertificate / RemoveCertificate — staged | ✅ | `TrustList.cs` | `PushTest` (`AddRemoveCertAsync`, `AddRemoveCATrustedCertAsync`, `AddRemoveCAIssuerCertAsync`), `TrustListTransactionTests.cs` |
|
||
| §7.8.2 | Open-for-write blocked with `Bad_TransactionPending` while another Session's transaction is active | ✅ | `TrustList.cs` + coordinator | `TrustListTransactionTests.cs`, `PushConfigurationTransactionCoordinatorTests.ApplyChangesWithOpenTrustListWriter*` |
|
||
| §7.8.2 / Part 20 §4.2.2 | Several concurrent read handles; an open while a write handle is open → `Bad_NotReadable`/`Bad_NotWritable` (also for the owning Session); a write open while any handle is open → `Bad_NotWritable` | ✅ | `TrustList.cs` | `TrustListTests` (`OpenForReadSeveralTimesKeepsEveryReadHandle`, `SameSessionOpenWhileItsWriteHandleIsOpenIsRejected`) |
|
||
| §7.8.2 | Read/write access control | ✅ | `TrustList.cs` | `TrustListTests` (`OpenReadWithoutReadAccessThrowsBadUserAccessDenied`, `OpenWriteWithoutWriteAccessThrowsBadUserAccessDenied`) |
|
||
| §7.8.2 | `LastUpdateTime` set on init + after committed update | ✅ | `TrustList.cs` | `TrustListTransactionTests.cs` |
|
||
| §7.8.2 | Writable / UserWritable; DefaultValidationOptions | ✅ | Set to true for GDS groups; generated from model CSV | `TrustListTests.cs` |
|
||
| §7.8.2.1 | ActivityTimeout: a handle with no Method call for the node's `ActivityTimeout` (default 60 000 ms) is closed and its changes discarded; every Open/Read/Write restarts it | ✅ | `TrustList.cs` (per-handle timer on the server `TimeProvider`) | `TrustListTests` (`IdleWriteHandleIsClosedAfterActivityTimeout`, `ActivityTimeoutPropertyValueIsHonored`) |
|
||
| §8.4.5 | `MaxTrustListSize` advertised honestly + resource-protection safety ceiling; oversize `Write`/`AddCertificate` → `Bad_RequestTooLarge`; `Read` clamped to the server-encoded stream | ✅ | `TrustList.cs` + `ServerConfigurationOptions.MaxTrustListSizeSafetyCeiling` | `TrustListValidationTest` (`NormalSizeTrustListAsync`, `WriteTrustListExceedsSizeLimit`, `TrustListJustUnderLimitAsync`), `ConfigurationNodeManagerPushTests.MaxTrustListSizeAdvertisesHonestFiniteEffectiveLimit` |
|
||
|
||
### Certificate and TrustList alarms (§7.8.3)
|
||
|
||
Each `CertificateGroup` exposes the two optional standard alarm instances
|
||
with **full active/inactive transitions and events** (previously
|
||
property-only). `ConfigurationNodeManager` creates and wires them in
|
||
`CreateAddressSpace`. `StandardServer.OnServerStarted` starts periodic
|
||
monitoring every 60 s once the subscription infrastructure is ready. The
|
||
injected `TimeProvider` supplies all thresholds and timers. See
|
||
[CertificateManager.md → Certificate-Expiration and TrustList-Staleness Alarms](CertificateManager.md#certificate-expiration-and-trustlist-staleness-alarms-opc-ua-part-12-783).
|
||
|
||
| Alarm | Status | Source | Tests |
|
||
|-------|--------|--------|-------|
|
||
| `CertificateExpirationAlarmType` (`CertificateExpired`) — active within `ExpirationLimit` / expired, Medium/High severity, `Retain`, Acknowledge, cleared after certificate replacement | ✅ | `CertificateGroupAlarmMonitor` + `CertificateAlarmScheduler.UpdateAndEvaluate` / `StartAlarmMonitoring` | `CertificateAlarmMonitoringTests` (`CertificateExpiredActivatesWithMediumSeverity*`, `...HighSeverityWhenAlreadyExpired`, `...DeactivatesAfterCertificateReplacement`, `RepeatedEvaluationsDoNotEmitDuplicateTransitionEvents`, `AcknowledgedAlarmClearsRetainAfterDeactivation`, `ClientAcknowledgeMethodMarksAlarmAcknowledged`) |
|
||
| `TrustListOutOfDateAlarmType` (`TrustListOutOfDate`) — active when `UpdateFrequency` elapses, disabled when non-positive | ✅ | same | `CertificateAlarmMonitoringTests` (`TrustListOutOfDateActivatesWhenStale`, `TrustListOutOfDateStaysInactiveWhenFresh`, `TrustListOutOfDateDisabledWhenUpdateFrequencyIsZero`) |
|
||
|
||
### TrustList-change effects on channels and Sessions (§7.10.9)
|
||
|
||
| Requirement | Status | Source | Tests |
|
||
|-------------|--------|--------|-------|
|
||
| Committed TrustList change forces SecureChannel renegotiation (application group) and revalidates certificate user identities / closes invalidated Sessions (user-token group) | ✅ | `PushConfigurationTrustListEffectHandler` + `ConfigurationNodeManager` | `ConfigurationNodeManagerTrustListEffectsTests` (`BuildTrustListEffects*`, `ApplyChangesDispatches*EffectFor*`, `ApplyChangesUsesCommittedTargetsNotAConcurrentSessionsStagedTargetsAsync`), `PushConfigurationTrustListEffectHandlerTests.cs` |
|
||
| Origin-independent enforcement: any `TrustListUpdated`/`CrlUpdated` on the certificate manager (out-of-band store write, `WriteTrustListAsync`, `ITrustListTransaction` commit) triggers the same effect fan-out; a host running the legacy non-transactional TrustList must OPT IN by calling `SetTrustListChangeNotifier` — nothing in the stack wires it automatically | ✅ | `CertificateChangePump` consumed by `ConfigurationNodeManager` | `ConfigurationNodeManagerTrustListEffectsTests` (`OutOfBand*`, `BuildTrustListEffectsForScopes*`), `CertificateChangePumpTests` |
|
||
| A CRL pushed through `CloseAndUpdate` + `ApplyChanges` disconnects a connected client whose certificate it revokes, without waiting for the security-token renewal | ✅ | commit → validator-cache invalidation → §7.10.9 sweep | `PushTest` (`PushedCrlDisconnectsRevokedClientAsync`), `ConfigurationNodeManagerTrustListEffectsTests` (`PushedCrlIsEnforcedByTheLiveValidatorAfterApplyChangesAsync`), `CertificateValidationCoreTests` (`ValidatedCertificateFastPathDoesNotSkipRevocationAsync`) |
|
||
|
||
## Audit Events
|
||
|
||
| Event Type | Status | Source | Tests |
|
||
|------------|--------|--------|-------|
|
||
| ApplicationRegistrationChangedAuditEvent | ✅ | Register/Update/Unregister | `AuditEventsTests.cs` |
|
||
| CertificateRequestedAuditEvent | ✅ | StartNewKeyPair + StartSigning | `AuditEventsTests.cs` |
|
||
| CertificateDeliveredAuditEvent | ✅ | FinishRequest | `AuditEventsTests.cs` |
|
||
| CertificateRevokedAuditEvent | ✅ | RevokeCertificate | `AuditEventsTests.cs` |
|
||
| CertificateUpdateRequestedAuditEvent | ✅ | UpdateCertificate (push) | `AuditEventsTests.cs`, `PushTest.cs` |
|
||
| TrustListUpdatedAuditEvent | ✅ | Core TrustList.cs | `TrustListTransactionTests.cs`, `PushTest.cs` |
|
||
| KeyCredentialRequestedAuditEvent | ✅ | StartRequest | `AuditEventsTests.cs` |
|
||
| KeyCredentialDeliveredAuditEvent | ✅ | FinishRequest | `AuditEventsTests.cs` |
|
||
| KeyCredentialRevokedAuditEvent | ✅ | Revoke | `AuditEventsTests.cs` |
|
||
| AccessTokenIssuedAuditEvent | ✅ | RequestAccessToken | `AuthorizationService/RefreshTokenTests.cs` |
|
||
| Secrets redacted from audit payloads | ✅ | RedactedPrivateKeyPassword / RedactedPrivateKey | `AuditEventsTests.cs` |
|
||
|
||
## KeyCredentialService (§8)
|
||
|
||
| Feature | Status | Source | Tests |
|
||
|---------|--------|--------|-------|
|
||
| StartRequest | ✅ | `OnKeyCredentialStartRequest` | `KeyCredentialRequestStoreTests.StartRequestReturnsNonNullIdAsync` |
|
||
| FinishRequest | ✅ | `OnKeyCredentialFinishRequest` | `KeyCredentialRequestStoreTests` (`FinishRequestReturnsCredentialAsync`, `FinishRequestWithCancelRejectsRequestAsync`, `FinishRequestWithUnknownIdThrows`) |
|
||
| Revoke | ✅ | `OnKeyCredentialRevoke` | `KeyCredentialRequestStoreTests` (`RevokeMarksCredentialAsRejectedAsync`, `RevokeUnknownCredentialThrows`) |
|
||
| InMemoryKeyCredentialRequestStore | ✅ | `IKeyCredentialRequestStore.cs` | `KeyCredentialRequestStoreTests.cs` |
|
||
| Client proxy | ✅ | `KeyCredentialServiceClient.cs` | `KeyCredentialRequestStoreTests.cs` |
|
||
| KeyCredential Push binding | ✅ | `WithKeyCredentialPush` / `KeyCredentialPushSubject` | `ConfigurationNodeManagerPushTests` (`BindKeyCredentialPushWithExistingFolderBindsSubjectAsync`, `BindKeyCredentialPushWithNullSubjectThrowsArgumentNullException`) |
|
||
|
||
## AuthorizationService (§9)
|
||
|
||
| Feature | Status | Source | Tests |
|
||
|---------|--------|--------|-------|
|
||
| GetServiceDescription | ✅ | `OnGetServiceDescription` | `AuthorizationService/RefreshTokenTests.GetServiceDescriptionReportsConfiguredServiceUri` |
|
||
| RequestAccessToken (obsolete single-call) | ✅ | Legacy compatibility path via `IAccessTokenProvider.RequestAccessTokenAsync` | `AuthorizationService/StartRequestTokenTests.LegacyRequestAccessTokenIssuesAnonymousUnprivilegedToken` |
|
||
| StartRequestToken (RC) | ✅ | `OnStartRequestToken` / `AuthorizationServiceManager` | `AuthorizationService/StartRequestTokenTests.cs` |
|
||
| FinishRequestToken (RC) | ✅ | `OnFinishRequestToken` / `AuthorizationServiceManager` | `AuthorizationService/StartRequestTokenTests.cs` |
|
||
| RefreshToken (RC) | ✅ | `OnRefreshTokenAsync` / `AuthorizationServiceManager` | `AuthorizationService/RefreshTokenTests.cs`, `AuthorizationService/InMemoryAccessTokenProviderRefreshTests.cs` |
|
||
| SupportedRoles (RC) | ✅ | Model property exposed | — (model property; not separately unit-tested) |
|
||
| Client proxy | ✅ | `AuthorizationServiceClient.cs` | `AuthorizationService/StartRequestTokenTests.cs`, `AuthorizationService/RefreshTokenTests.cs` |
|
||
|
||
### Refresh tokens
|
||
|
||
The default in-memory AuthorizationService provider issues a refresh token from
|
||
`FinishRequestToken` and accepts it through `RefreshToken` (OPC 10000-12 §9.7). Refresh
|
||
tokens are opaque, single-use secrets: each successful refresh consumes the current token,
|
||
issues a new access token, and rotates to a new refresh token. The refresh-token lifetime
|
||
slides by `AuthorizationServiceOptions.DefaultRefreshTokenLifetime` (7 days by default).
|
||
Set `AuthorizationServiceOptions.EnableRefreshTokens = false` to preserve the legacy
|
||
behavior where `FinishRequestToken` returns no refresh token and `RefreshToken` returns
|
||
`Bad_NotSupported`.
|
||
|
||
```csharp
|
||
var (newJwt, newJwtExpiresAt, newRefreshToken, newRefreshExpiresAt) =
|
||
await authClient.RefreshTokenAsync(resourceId, refreshToken);
|
||
|
||
refreshToken = newRefreshToken;
|
||
```
|
||
|
||
Refresh-token audit events include only the `resourceId` input and the outcome. The
|
||
refresh token itself is a secret and is never placed in audit payloads.
|
||
|
||
## Remaining optional / unsupported Part 12 items
|
||
|
||
The following optional Part 12 members are **not** implemented (or only partially). None are required for the mandatory conformance units of the applicable Facets above; they are listed here so the matrix stays honest.
|
||
|
||
| Item (§) | Status | Notes |
|
||
|----------|--------|-------|
|
||
| `ServerConfiguration.AuthorizationServices` folder (§9.6.2 / *Authorization Service Configuration Server* CU) | ❌ not exposed | A push Server does not expose the configuration folder listing trusted Authorization Services. Client access-token validation is instead handled by the pluggable [Identity Providers](IdentityProviders.md). |
|
||
| `ManagedApplications` / `ApplicationConfigurationType` proxy (§7.10.14–§7.10.16) | ⚠️ partial | `DefaultManagedApplicationsNodeManager` creates the nodes with an `IConfigurationDataStore`; the full GDS-as-proxy update/confirm flow to *remote* managed applications is not complete. |
|
||
| KeyCredential authentication `ProfileUris` — AMQP SASL Plain / MQTT UserName / UA transport UserName (§8, optional CUs) | 🔲 provider-defined | The push binding is implemented; the concrete ProfileUri set advertised is defined by the credential provider, not fixed by the stack. |
|
||
| A & C CertificateExpiration optional CUs — *Comment* / *Confirm* / *Shelving* | 🔲 inherited | Available through the standard OPC 10000-9 condition model on the alarm instances; not separately exercised by the certificate-alarm tests. |
|
||
| Onboarding / OPC 10000-21 device setup interplay with `ResetToServerDefaults` / `InApplicationSetup` (§7.10.13, Annex G) | 🔲 provider-gated | Registrar ticket administration is implemented through `OnboardingClient`, `ITicketStore`, and `BindToTicketStore`; the stack does not itself implement the complete device-facing onboarding state machine. |
|
||
|
||
## LDS / LDS-ME (§4–5)
|
||
|
||
| Feature | Status | Source | Tests |
|
||
|---------|--------|--------|-------|
|
||
| FindServers | ✅ | `LdsServer.FindServersAsync` | `LocalDiscoveryTests.cs` |
|
||
| GetEndpoints | ✅ | `LdsServer.GetEndpointsAsync` | `LocalDiscoveryTests.cs` |
|
||
| RegisterServer | ✅ | `LdsServer.RegisterServerAsync` | `LocalDiscoveryTests.cs` |
|
||
| RegisterServer2 | ✅ | `LdsServer.RegisterServer2Async` | `LocalDiscoveryTests.cs` |
|
||
| FindServersOnNetwork | ✅ | `LdsServer.FindServersOnNetworkAsync` | `LocalDiscoveryTests.cs`, `LdsServerStaticTests.cs` |
|
||
| LDS-ME capability | ✅ | `ComputeServerCapabilities` | `ServerCapabilitiesTests.cs`, `ServerCapabilityTests.cs` |
|
||
| mDNS _opcua-tcp._tcp | ✅ | `MulticastDiscovery.OpcUaServiceType` | `LdsServerStaticTests.cs` |
|
||
| rcp+ reverse-connect | ✅ | `MulticastDiscovery.ReverseConnectScheme` | `LdsServerStaticTests.cs` |
|
||
| Annex C TXT keys | ✅ | path / caps / rc | `LdsServerStaticTests.cs` |
|
||
| Annex D identifiers | ✅ | LDS / LDS-ME | `LdsServerStaticTests.cs` |
|
||
|
||
## Extension Points (Phase 2 Abstractions)
|
||
|
||
Every extension point a user needs to provide or override:
|
||
|
||
| Interface | Purpose | Default impl | User replaces when... |
|
||
|-----------|---------|-------------|----------------------|
|
||
| `IApplicationsDatabase` | Application registration store | `LinqApplicationsDatabase` / `JsonApplicationsDatabase` | Using a SQL/NoSQL database |
|
||
| `ICertificateRequest` | Cert request lifecycle | `LinqApplicationsDatabase` | Custom approval workflow |
|
||
| `ICertificateGroup` | CA operations | `CertificateGroup` | External CA (EST, ACME, etc.) |
|
||
| `IKeyCredentialRequestStore` | KeyCredential lifecycle | `InMemoryKeyCredentialRequestStore` | Persistent store, external IdP |
|
||
| `ISecretStore` | Secret storage for credentials | `InMemorySecretStore` | Key Vault, DPAPI, K8s secrets |
|
||
| `IAccessTokenProvider` | Token issuance for AuthorizationService | `null` (Bad_NotSupported) | OAuth2/JWT provider |
|
||
| `IConfigurationDataStore` | ManagedApplications config persistence | `InMemoryConfigurationDataStore` | File-system, database |
|
||
| `IManagedApplicationsNodeManager` | ManagedApplications folder | `StubManagedApplicationsNodeManager` / `DefaultManagedApplicationsNodeManager` | Full push management |
|
||
| `IConfigurationNodeManager` | Alarm monitoring | `ConfigurationNodeManager.StartAlarmMonitoring` | Custom thresholds/sources |
|
||
| `IUserDatabase` | User/password store | Existing in `GlobalDiscoverySampleServer` | LDAP, database |
|