Files
Cristi Pogacean 2cdf0de97f Fix scoped certificate-store resolution in GDS push operations (#4726)
# Description

Fix the remaining GDS push certificate paths that bypass instance-scoped
certificate stores, while preserving OPC UA 1.05.07 trust and
transaction requirements.

- Use the configured resolver/registry for previous-key loading, slot
resolution, application-store commit/rollback, issuer
import/compensation, and rejected-store opening.
- Validate application-certificate uploads through the configured
manager and the group's registered trust scope, instead of a temporary
manager that cannot see custom providers.
- Isolate per-call `AcceptError` validation cores from normal cached
trust decisions. Part 12 installation-time acceptance must neither reuse
nor populate the connection-validation cache.
- Compensate the application-certificate swap if opening the issuer
store fails, not only if importing an issuer fails.
- Add scoped-provider, cross-group trust, cache-isolation, and
compensation regressions. Reuse the portable PEM writer and short
temporary PKI paths so the tests work on .NET Framework too.
- Use source-generated failure logging without private-key bytes, and
document custom-store/pending-key requirements.

The branch starts at master `7f14fb864fc93532f3dbab1e1b5e2ba9b5f605c5`.
It excludes the unrelated LocalizedText commits, the local handoff, PLC
changes, logs, keys, certificates, and build outputs. No public API,
dependency, package version, global provider registration, or analyzer
configuration is changed.

### Specification review

The online Part 12 reference identifies version **1.05.07**. This is a
focused review and regression validation of the changed behavior, **not
a claim of complete stack conformance or CTT certification**.

| Requirement | Review / evidence |
| --- | --- |
| [Part 12,
7.10.5](https://reference.opcfoundation.org/specs/OPC-10000-12/7.10.5):
validate application certificates using the associated TrustList; ignore
the supplied issuer list | Existing issuer-argument regressions
retained. New flat-layout scoped-provider cases accept a CA installed in
the selected trusted/issuer store and reject one installed only in
another scope. |
| [Part 4,
6.1.3](https://reference.opcfoundation.org/specs/OPC-10000-4/6.1.3) and
Part 12, 7.10.5: suppressible installation errors must not bypass normal
peer trust or non-suppressible failures | Missing-issuer,
signature/revocation, explicit-trust, and validation-core tests; new
two-way cache-isolation cases with `UseValidatedCertificates = true`;
normal peer validation remains untrusted after issuer-only push
acceptance. |
| Part 12, 7.10.5: matching private key, encrypted channel and
SecurityAdmin | Existing access/key/type tests retained; scoped
own-store upload cases cover no uploaded key and PEM. PFX and
regenerated-key cases remain in the selected suite. |
| [Part 12,
7.10.2](https://reference.opcfoundation.org/specs/OPC-10000-12/7.10.2),
[7.10.9](https://reference.opcfoundation.org/specs/OPC-10000-12/7.10.9),
and
[7.10.11](https://reference.opcfoundation.org/specs/OPC-10000-12/7.10.11):
staging, ApplyChanges, cancellation, ownership and compensation |
Push/transaction/rollback suites; new custom-provider compensation cases
cover both issuer-import and store-open failure, retaining the old
application certificate and pre-existing issuers. |
| [Part 12,
7.10.7](https://reference.opcfoundation.org/specs/OPC-10000-12/7.10.7):
do not delete endpoint-referenced certificates | Existing ApplyChanges
endpoint-reference and delete/create replacement tests retained. |
| [Part 12,
7.10.10](https://reference.opcfoundation.org/specs/OPC-10000-12/7.10.10):
CSR key handling and persistence | Existing-key scoped CSR test, SDK
pending-key/cancel cases, and supporting PLC encrypted round
trips/restarts. Arbitrary custom backends still require a durable
pending-key implementation; the separate PLC implementation is not
included here. |

### Validation

Windows, .NET SDK 10.0.401, Release, NUnit/VSTest. Build analyzers
remained enforced: **no `WarningsNotAsErrors`, suppressions, or analyzer
downgrades**. `NuGetAudit=false` was needed because the local nuget.org
TLS connection failed; no repository audit setting was changed.

| Selection | Result |
| --- | --- |
| SDK GDS push/trust/transaction/rollback, net10.0 | **192 passed, 0
failed** |
| SDK certificate-manager/provider/explicit-trust/validation-core,
net10.0 | **180 passed, 0 failed** |
| SDK certificate-manager/provider/explicit-trust/validation-core, net48
| **180 passed, 0 failed** |
| SDK GDS push/trust/transaction/rollback, net48 | **192 passed, 0
failed** |
| Final portable-PEM/compensation tests, net10.0 | **5 passed, 0
failed** |
| Supporting PLC GDS/pending-key/compatibility selection, net10.0 | **29
passed, 0 failed** against this SDK source |

The PLC tests exercise encrypted UA calls, signed PEM/CSR updates,
persistence, fresh connections and restarts across Directory,
FlatDirectory and KubernetesSecret providers. The Kubernetes API is an
in-memory double, **not a real Kubernetes deployment**. PLC tests and
its durable pending-key implementation are local companion-repository
changes, not SDK PR contents or independently reproducible CI evidence
from this branch alone.

Commands below use the same isolated artifact locations as the
validation run, expressed relative to the user profile:

```powershell
$files = Join-Path $env:USERPROFILE '.copilot\session-state\003a7438-0312-4c10-beed-cd95c0fe400e\files'
$sdk = Join-Path $files 'sdk-build'
$net48 = Join-Path $files 'solution-net48'
$push = 'FullyQualifiedName~ConfigurationNodeManagerPushTests|FullyQualifiedName~ConfigurationNodeManagerPushValidationTests|FullyQualifiedName~PushComplianceRegressionTests|FullyQualifiedName~PushRollbackCancellationRegressionTests|FullyQualifiedName~PushConfigurationTransactionCoordinatorTests'
$manager = 'FullyQualifiedName~CertificateManagerTests|FullyQualifiedName~CertificateManagerExplicitTrustTests|FullyQualifiedName~CertificateManagerProviderResolutionRegressionTests|FullyQualifiedName~CertificateValidationCoreTests'

# Passed: 192/192.
dotnet test .\tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c Release -f net10.0 -m:1 --no-restore -p:GeneratePackageOnBuild=false -p:UseArtifactsOutput=true "-p:ArtifactsPath=$sdk" -p:NuGetAudit=false --filter $push

# Passed: 180/180.
dotnet test .\tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -c Release -f net10.0 --no-build --no-restore -p:UseArtifactsOutput=true "-p:ArtifactsPath=$sdk" --filter $manager

# Passed: 180/180.
dotnet test .\tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -c Release -f net48 --no-build --no-restore -p:CustomTestTarget=net48 -p:UseArtifactsOutput=true "-p:ArtifactsPath=$net48" --filter $manager

# Passed: 192/192 after the portable PEM writer and shorter temporary PKI paths.
dotnet test .\tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c Release -f net48 -m:1 --no-restore -p:CustomTestTarget=net48 -p:GeneratePackageOnBuild=false -p:UseArtifactsOutput=true "-p:ArtifactsPath=$net48" -p:NuGetAudit=false --filter $push

# Passed: 5/5 after final test portability changes.
dotnet test .\tests\Opc.Ua.Server.Tests\Opc.Ua.Server.Tests.csproj -c Release -f net10.0 -m:1 --no-restore -p:GeneratePackageOnBuild=false -p:UseArtifactsOutput=true "-p:ArtifactsPath=$sdk" -p:NuGetAudit=false --filter 'FullyQualifiedName~CertificatePushUsesScopedOwnStoreProviderAsync|FullyQualifiedName~ApplyCertificateSlotChangeSelfCompensatesWhenIssuerImportFailsAfterAppCertSwapAsync'

# Passed: 29/29; the companion project and this SDK had been built together first.
dotnet test C:\src\iot-edge-opc-plc\tests\opc-plc-tests.csproj -c Release --no-build --no-restore -p:UseLocalOpcUaStack=true -p:OpcUaSourceRoot=C:\src\UA-.NETStandard -p:UseArtifactsOutput=true "-p:ArtifactsPath=$sdk" --filter 'FullyQualifiedName~GdsPush|FullyQualifiedName~PlcPendingCertificateKeyStoreTests|FullyQualifiedName~PreviewCompatibilityTests'
```

The `--no-restore` / `--no-build` invocations require prior
restoration/build. The initially empty artifact-directory `--no-restore`
invocation produced no test execution and is **not** counted. Initial
compile/test portability failures were fixed before the final results
above.

Full-solution attempts did **not** pass:

```powershell
# Failed/incomplete: AMB server startup timed out in two tests; stopped after failures.
dotnet test .\UA.slnx -c Release -f net48 -m:1 -p:CustomTestTarget=net48 -p:GeneratePackageOnBuild=false -p:UseArtifactsOutput=true "-p:ArtifactsPath=$net48" -p:NuGetAudit=false

# Failed: NU1301 restoring OpenUsd-related projects due to nuget.org TLS handshake failure.
dotnet test .\UA.slnx -c Release -f net10.0 -m:1 -p:CustomTestTarget=net10.0 -p:GeneratePackageOnBuild=false -p:UseArtifactsOutput=true "-p:ArtifactsPath=$(Join-Path $files 'solution-net10')" -p:NuGetAudit=false
```

A retry through an already configured local package source progressed
further, but the OneFuzz nested publish did not inherit the audit
override and caused **210 setup failures (NU1900)**; the broad run was
stopped. These runs are not a clean full-suite result. AMB startup
failures have not been baseline-qualified and are not claimed to be
fixed by this PR.

`git diff --check` passed. A changed-file whitespace check reported
seven pre-existing diagnostics at lines 371-424 of
`CertificateManagerTests.cs`, outside this change (new tests start at
line 563); no unrelated formatting sweep was made.

**Remaining gates:** successful full solution on net48/net10.0,
quantitative coverage, Linux, required CI/CodeQL, and applicable
CTT/interoperability qualification. Keep this PR draft until the
required gates are resolved; the tests above do not establish 100% OPC
UA compliance.

## Related Issues

_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._

_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._

- Fixes #4725.
- Follow-up to #4546, #4530 and #4531 for remaining certificate-push
consumers.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [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.
- [x] 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.

Tests were added and pass as recorded, but coverage was not measured.
CLA status is not asserted on behalf of the contributor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-09 21:41:58 +02:00
..