fix(dcl): OPC UA secure client presents a real application certificate (#121)

Sign/SignAndEncrypt sessions failed before activation with "ApplicationCertificate
for the security profile ... cannot be found" (BadConfigurationError): both
RealOpcUaClient.ConnectAsync and the verify probe built an empty CertificateIdentifier.

- New OpcUaApplicationIdentity builds the client ApplicationConfiguration for both
  paths. Secured modes load the application certificate from a configurable own
  store (subject + ApplicationUri), auto-creating a self-signed RSA-2048 cert only
  when none exists. A persisted cert is reused across reconnect/restart and never
  silently rotated: one that no longer validates (e.g. ApplicationUri mismatch)
  fails closed. First-time creation is serialised per own store and logged at
  Warning with the thumbprint. None mode is unchanged and never touches the store.
- New OpcUaGlobalOptions: ApplicationUri, ApplicationCertificateSubject,
  OwnCertificateStorePath, AutoCreateApplicationCertificate,
  ApplicationCertificateLifetimeMonths (validated).
- Owned resources: the private-key cert is disposed on disconnect, failed connect
  and at probe end; sessions are now disposed after close, and a reconnect on the
  same instance closes the previous session first.
- CertStoreActor wrote trusted server certs to the store ROOT, which the SDK never
  reads (it uses {store}/certs), so an operator Trust never affected validation and
  SDK-rejected certs never listed. It now uses certs/ and migrates legacy root files
  on start.
- Rig: all site nodes (docker/, docker-env2/) put the four PKI stores on the
  persistent /app/data volume with a pinned ApplicationUri.
- Tests: in-process strict-trust StandardServer (test-only Opc.Ua.Server 1.5.378.106)
  exercising the real adapter through ConnectAsync and VerifyEndpointAsync for Sign
  and SignAndEncrypt: allow + read + reconnect + restart on one identity, deny when
  the client doesn't trust the server (BadCertificateUntrusted), deny when the server
  doesn't trust the client (BadSecurityChecksFailed), None untouched, concurrent
  provisioning creates one cert, URI mismatch fails without rotating, auto-create off.
  Against the pre-fix client these tests fail with the exact #121 error.
This commit is contained in:
Joseph Doherty
2026-11-10 13:34:55 -05:00
parent c810bd0f7c
commit ff8354478c
22 changed files with 948 additions and 74 deletions
+7 -2
View File
@@ -259,11 +259,16 @@ All settings under `DataConnectionLayer` in `appsettings.json` bind to `DataConn
| Key | Default | Description |
|-----|---------|-------------|
| `ApplicationName` | `ScadaBridge-DCL` | Application name used in the OPC UA certificate and session negotiation. |
| `ApplicationUri` | `""` → `urn:{host}:{ApplicationName}` | ApplicationUri presented in the session and embedded in the application certificate. |
| `ApplicationCertificateSubject` | `""` → `CN={ApplicationName}` | Subject the application certificate is located by. |
| `OwnCertificateStorePath` | `""` | Own store holding the application certificate + private key (Sign / SignAndEncrypt only). |
| `AutoCreateApplicationCertificate` | `true` | Create a self-signed certificate when none exists; `false` fails a secured connect instead. |
| `ApplicationCertificateLifetimeMonths` | `24` | Lifetime of an auto-created certificate (1–600). |
| `TrustedIssuerStorePath` | `""` | File path to the trusted issuer certificate store. |
| `TrustedPeerStorePath` | `""` | File path to the trusted peer certificate store. |
| `TrustedPeerStorePath` | `""` | File path to the trusted peer certificate store (certificates live in its `certs/` sub-directory). |
| `RejectedCertificateStorePath` | `""` | File path to the rejected certificate store. |
Empty store paths fall back to a temp-path default so dev runs work without explicit configuration.
Empty store paths fall back to a temp-path default so dev runs work without explicit configuration — production nodes must use persistent paths, or the application certificate changes whenever temp is cleared. An existing application certificate is reused, never silently rotated; one that no longer matches the configured ApplicationUri fails the connect. See `docs/requirements/Component-DataConnectionLayer.md` → *OPC UA application identity* (Gitea #121).
### Per-connection settings (OPC UA)