Skip to content

Certificate manager and ref counted certificate type#3688

Open
marcschier wants to merge 182 commits intomasterfrom
x509
Open

Certificate manager and ref counted certificate type#3688
marcschier wants to merge 182 commits intomasterfrom
x509

Conversation

@marcschier
Copy link
Copy Markdown
Collaborator

@marcschier marcschier commented Apr 20, 2026

Proposed changes

See documentation included in PR

Related Issues

  • Fixes

#3657
#3562
#3339
#3279
#3160
#2765

Types of changes

What types of changes does your code introduce?

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Further comments

The CertificateManager design is documented in Docs/CertificateManager.md. Migration guidance for adopters of the legacy API surface is in Docs/MigrationGuide.md. The legacy CertificateValidator class, ICertificateValidator interface, CertificateTypesProvider class, and the ApplicationConfiguration.CertificateValidator / ServerBase.CertificateValidator / ServerBase.InstanceCertificateTypesProvider properties are all marked [Obsolete] with replacement messages, while remaining functional bridges into the new design for binary compatibility.

marcschier and others added 26 commits April 15, 2026 14:39
Add 'using var' to undisposed IDisposable local variables across test files:
- NodeStateTests.cs: 44 state objects (BaseObjectState, BaseEventState, etc.)
- ConditionStateTests.cs: 9 condition/alarm state objects
- NodeStateCollectionConcurrencyTests.cs: 3 test node states
- NodeStateHandlerConcurrencyTests.cs: 6 test node states
- BaseDataVariableTypeStateSerializationTest.cs: 6 state objects
- SecuredApplicationTests.cs: 2 XmlEncoder instances
- SecurityConfigurationManagerTests.cs: 2 XmlEncoder instances
- SecurityConfigManagerAdditionalTests.cs: 1 XmlEncoder instance
- HttpsTransportListenerTests.cs: 5 listeners + 1 MemoryStream
- NullChannelTests.cs: 1 NullChannel instance

Skipped: X509Certificate2/certificate-related, Security\Certificates\ folder,
property assignments owned by parent objects, and loop-created objects
shared across threads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address dispose-before-losing-scope (CA2000) warnings across 14 files
in the Libraries/ directory. X509Certificate2-related warnings are
excluded per project conventions.

Patterns applied:
- using var for temporary objects (event states, encoders, parsers)
- try/finally with temp variable null-out for ownership transfers
- try/finally with Dispose() after collection removal
- Field disposal (m_strm?.Dispose()) before setting to null
- (IDisposable)?.Dispose() cast for interface types

Files fixed:
- UaPubSubConfigurationHelper.cs: XmlEncoder, XmlParser
- Subscription.cs (Server): RefreshStartEventState, RefreshEndEventState
- MonitoredItem.cs: EventQueueOverflowEventState
- DiagnosticsNodeManager.cs: 5 node state ownership transfers
- SessionManager.cs: Nonce, UserIdentity, Session disposal
- Session.cs (Client): 3 UserIdentity ownership transfers
- DefaultSessionFactory.cs: UserIdentity ownership transfer
- GlobalDiscoverySampleServer.cs: 2 UserIdentity ownership transfers
- MasterNodeManager.cs: ContinuationPoint disposal
- SubscriptionManager.cs: SessionPublishQueue disposal after removal
- SamplingGroupManager.cs: SamplingGroup disposal
- CustomNodeManager.cs: 2 IMonitoredItem out param disposals
- AsyncCustomNodeManager.cs: NodeState + IMonitoredItem disposals
- TrustList.cs: 3 MemoryStream field disposals

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add using declarations to undisposed IDisposable objects across 22 test
files in Opc.Ua.PubSub.Tests to resolve CA2000 analyzer warnings:

- UaPubSubApplication.Create() return values
- UadpNetworkMessage constructor calls
- MqttPubSubConnection constructor calls
- MemoryStream constructor calls

Field assignments in SetUp methods that are already properly disposed
in TearDown methods are left unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace X509Certificate2 with Certificate in all public and internal APIs
within the Security.Certificates project:

- ICertificateBuilder: CreateForRSA/CreateForECDsa return Certificate;
  SetIssuer accepts Certificate
- CertificateBuilderBase: IssuerCAKeyCert field changed to Certificate
- CertificateBuilder: All Create methods wrap results with Certificate.From
- X509PfxUtils: VerifyKeyPair, VerifyRSAKeyPair, VerifyECDsaKeyPair,
  IsECDsaSignature, CreateCertificateFromPKCS12 use Certificate
- PEMWriter: Export methods accept Certificate
- CrlBuilder: CreateForRSA/CreateForECDsa, AddRevokedCertificate use Certificate
- X509Crl: VerifySignature, IsRevoked accept Certificate
- X509Extensions: FindExtension and BuildAuthorityKeyIdentifier use Certificate
- BouncyCastle X509Utils: Internal helpers use Certificate
- X509CertificateLoader left unchanged (system polyfill)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add 'using' to IDisposable objects (Certificate, CertificateCollection,
Activity) that were not being disposed in test code across:

- Tests/Opc.Ua.Security.Certificates.Tests (CertificateTestsForRSA.cs,
  CertificateTestsForECDsa.cs)
- Tests/Opc.Ua.Server.Tests (CreateSessionApplicationUriValidationTests.cs)
- Tests/Opc.Ua.Client.Tests (Session/ClientTest.cs)
- Tests/Opc.Ua.Gds.Tests (CertificateGroupTests.cs, X509TestUtils.cs)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The using declaration on senderCertificateChain caused premature disposal
of certificates that are stored in SenderCertificate and
SenderIssuerCertificates fields. When the using scope ends, each
certificate in the collection has its refcount decremented, potentially
disposing the inner X509Certificate2 while the fields still reference it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…management

- Certificate._refCount starts at 1, AddRef increments, Dispose decrements
- Inner X509Certificate2 disposed only when refcount reaches 0
- DirectoryCertificateStore.EnumerateAsync/FindByThumbprintAsync call AddRef
  on cached certs before returning in CertificateCollection
- Fixes shared cert disposal when CertificateCollection.Dispose is called

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nd tests

Phase 1: Core types (TrustListIdentifier, CertificateEntry, CertificateValidationResult,
CertificateChangeEvent) + segregated interfaces (ICertificateRegistry, ICertificateTrustListManager,
ICertificateValidatorEx, ICertificateLifecycle, ICertificateFactory, ICertificateIssuer,
ICertificateStoreProvider, ICertificateManager)

Phase 2: DefaultCertificateFactory, DefaultCertificateIssuer, DirectoryStoreProvider,
X509StoreProvider, InMemoryStoreProvider

Phase 3: CertificateManager core (trust-list registry, cert registry, validation pipeline,
lifecycle with IObservable notifications, Channel-based rejected cert processor,
transaction support, DI factory)

Phase 4A: CertificateValidatorAdapter backward compat

47 tests covering all components.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ibraries

- ServerBase: Initialize CertificateManager from SecurityConfiguration on startup
- StandardServer: Log CertificateManager trust list count
- ApplicationInstance: Create CertificateManager after cert validation
- CertificateGroup: Add opt-in ICertificateIssuer property
- ApplicationsNodeManager: Add opt-in ICertificateTrustListManager property
- Utils: Add ParseCertificateChainBlob overload accepting ICertificateFactory

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace ArgumentNullException.ThrowIfNull with manual checks (pre-.NET 6 compat)
- Replace ObjectDisposedException.ThrowIf with manual check (pre-.NET 7 compat)
- Replace ValueTask.CompletedTask with default (pre-.NET 5 compat)
- Remove all 14 #region/#endregion directives from CertificateCollection.cs
- Add 5 TrustListTransaction tests (commit, remove, rollback, disposed, CRL)
- Add 4 CertificateValidatorAdapter tests (success/failure for single and chain)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ess, deeper wiring, obsolete attrs

- CertificateLifecycleMonitor: periodic timer emitting CertificateExpiring events
- ITrustListFileAccess + TrustListData: read/write trust-lists as blobs (Part 12 §7.5)
- StandardServer subscribes to CertificateManager.CertificateChanges
- GDS CertificateGroup uses ICertificateIssuer when available
- [Obsolete] on CertificateFactory.CreateCertificate/CreateSigningRequest/RevokeCertificate
- [Obsolete] on CertificateStoreType.RegisterCertificateStoreType

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ry methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Docs/CertificateManager.md covers:
- Architecture overview with interface segregation diagram
- Quick start: creation, validation, subscriptions, trust-lists
- Full interface reference tables for all 7 interfaces
- Pluggable store backend guide
- Certificate reference counting explanation
- Backward compatibility notes
- OPC UA specification alignment table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…igration

Certificates.md:
- Add Certificate/CertificateCollection wrapper types section
- Update validation intro to reference CertificateManager
- Add InMemory store type to store types list
- Update pseudocode from X509Certificate2 to Certificate types
- Add cross-references to CertificateManager.md

MigrationGuide.md:
- Add Certificate Management section under 1.5.378 to 1.6.x migration
- Document Certificate/CertificateCollection wrapper migration
- Document CertificateManager interfaces and usage
- Document obsoleted APIs with replacement table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 70.94718% with 957 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.13%. Comparing base (bd4543e) to head (87f8558).

Files with missing lines Patch % Lines
...es/CertificateManager/CertificateValidationCore.cs 73.54% 185 Missing and 38 partials ⚠️
...tificates/CertificateManager/CertificateManager.cs 65.07% 135 Missing and 33 partials ⚠️
...raries/Opc.Ua.Configuration/ApplicationInstance.cs 69.75% 38 Missing and 11 partials ⚠️
Libraries/Opc.Ua.Server/Session/Session.cs 21.05% 45 Missing ⚠️
...CertificateManager/CertificateValidationHelpers.cs 44.00% 36 Missing and 6 partials ⚠️
...a.Server/Configuration/ConfigurationNodeManager.cs 73.33% 22 Missing and 18 partials ⚠️
...ficates/CertificateManager/TrustListTransaction.cs 52.00% 27 Missing and 9 partials ⚠️
...es/Opc.Ua.PubSub/Transport/MqttPubSubConnection.cs 30.23% 30 Missing ⚠️
...es/CertificateManager/DefaultCertificateFactory.cs 79.83% 9 Missing and 15 partials ⚠️
Libraries/Opc.Ua.Server/Server/StandardServer.cs 62.90% 19 Missing and 4 partials ⚠️
... and 48 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3688      +/-   ##
==========================================
+ Coverage   72.10%   72.13%   +0.02%     
==========================================
  Files         572      594      +22     
  Lines      120749   122184    +1435     
  Branches    20173    20559     +386     
==========================================
+ Hits        87068    88136    +1068     
- Misses      27839    28020     +181     
- Partials     5842     6028     +186     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs Outdated
marcschier and others added 2 commits April 21, 2026 05:48
…eCollection

ApplicationsNodeManager.cs: Add using to CertificateCollection from EnumerateAsync
before calling AsX509Certificate2Collection() for ExtraStore.AddRange. Also add
using to X509Chain (was missing dispose).

CertificateValidator.cs ExtraStore.Add(issuer.Certificate.X509): verified safe —
issuers list stays alive throughout the using X509Chain block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
marcschier and others added 6 commits May 7, 2026 07:47
TestSubscription.OnKeepAliveNotificationAsync was returning WaitAsync()

which blocks forever on a Block semaphore that no test ever releases.

Previously this was harmless because the tests leaked subscription

instances (no DisposeAsync). After the recent cleanup commits added

'await using' to all SubscriptionTests, the dispose path waits on the

message worker task, which itself is stuck inside the keep-alive

callback — hanging the entire CI Client job.

OnPublishReceivedAsyncShouldProcessNotificationAsync sends an empty

NotificationMessage which the worker classifies as a keep-alive, so

every dispose hits the trap.

Make the override return default. The Block field stays in case

specific tests opt in via WaitAsync().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	Stack/Opc.Ua.Core/Security/Certificates/CertificateManager/CertificateManager.cs
#	Stack/Opc.Ua.Core/Security/Certificates/CertificateManager/CertificateValidatorAdapter.cs
#	Stack/Opc.Ua.Core/Security/Certificates/CertificateTypesProvider.cs
#	Stack/Opc.Ua.Core/Security/Certificates/CertificateValidator.cs
#	Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs
#	Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateManager/CertificateManagerTests.cs
#	Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateManager/CertificateValidatorAdapterTests.cs
#	Tests/Opc.Ua.Core.Tests/Security/Certificates/TemporaryCertValidator.cs
#	Tests/Opc.Ua.Core.Tests/Stack/Configuration/ApplicationConfigurationTests.cs
#	Tests/Opc.Ua.Core.Tests/Stack/Transport/HttpsTransportListenerTests.cs
#	Tests/Opc.Ua.Gds.Tests/X509TestUtils.cs
Phase 0 of the editorconfig-driven cleanup pass requires a green
baseline build with /p:RunAnalyzersDuringBuild=true. Three error-level
analyzer diagnostics were blocking that build:

* RCS1174 in CertificateManager.GetIssuersAsync — the method awaited a
  single tail call. Convert it to a non-async method that returns the
  inner Task directly. Argument validation still throws synchronously.
* RCS1047 on ServerBase.OnCertificateUpdateAsync — the method is
  protected virtual and renaming would be a binary breaking change for
  derived classes. Wrap the declaration in a narrow #pragma disable
  with a one-line reason.
* NUnit2023 in X509IdentityTokenHandlerTests — Assert.That on a
  ByteString (struct) for Is.Not.Null is tautological; remove the
  redundant assertion.

Build now succeeds with 0 errors (1150 warnings) under
/p:RunAnalyzersDuringBuild=true. Subsequent commits will apply
dotnet format passes against this clean baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format whitespace UA.slnx'.

42 files, ~440 line changes auto-fixed (trailing whitespace, blank line
normalisation, missing final newlines).

Build clean with /p:RunAnalyzersDuringBuild=true. Tests green on
net10.0 (Core: 6615 passed, 86 skipped).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
{
m_logger.LogError(
"Certificate {Certificate} rejected. Reason={ServiceResult}.",
certificate,
serviceResult.StatusCode == StatusCodes.BadCertificateUntrusted)
{
accept = true;
m_logger.LogInformation("Auto accepted certificate {Certificate}", certificate);
{
m_logger.LogError(
"Certificate {Certificate} validation failed with suppressible errors but was rejected. Reason={ServiceResult}.",
certificate,
{
if (store == null)
{
m_logger.LogWarning("Failed to open issuer store: {CertificateStore}", certificateStore);
@@ -1201,17 +1278,19 @@
m_lastDirectoryCheck = DateTime.MinValue;
}

m_logger.LogInformation(Utils.TraceMasks.Security, "Certificate store reloaded from {Path}, {Count} entries.", StorePath, m_certificates.Count);
marcschier and others added 11 commits May 7, 2026 12:54
… ids on Update

Three coupled fixes for GDS UpdateCertificate (Push) regressions on x509:

1. ServerBase.StartAsync now reuses ApplicationConfiguration.CertificateManager

   when present instead of creating its own. Without this, ApplicationInstance

   and ServerBase ended up with two distinct CertificateManager instances:

   ConfigurationNodeManager.ApplyChanges updated one (the application config's),

   while StandardServer's CertificateManagerChangeObserver was subscribed to the

   other - so the cert change notification never fanned out to endpoint

   descriptions / transport listeners and the server kept serving the old cert.

2. CertificateManager.UpdateAsync calls LoadPrivateKeyExAsync on each

   application certificate identifier before reloading. This restores the

   pre-Phase 8 behaviour: CertificateIdentifier caches the last loaded cert in

   m_certificate, so FindAsync(true) inside the registry reload would otherwise

   return the stale cached instance even after the underlying store was mutated

   by the GDS push.

3. ConfigurationNodeManager.UpdateCertificateInternalAsync replaces the cached

   Certificate on the existing identifier with the new (private-key-bearing)

   cert immediately after writing it to the application store. The previous

   FindAsync-with-discarded-result call was a no-op because the identifier still

   referenced the disposed-from-store old cert via its cache.

Local Gds.Tests verification: 92/104 passed, 12 skipped, 0 failed (was 0 of 78

UpdateCertificate variants passing on this branch).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format style UA.slnx --diagnostics IDE0032 IDE0049'.

The IDE0032 (use auto property) fixer crashed mid-run with
'SyntaxTree is not part of the compilation, so it cannot be removed'
and produced no auto-property changes. The benign side effect of the
pass was to apply the dotnet_sort_system_directives_first ordering on
files where it had drifted; 40 files updated with using directive
reorderings only.

Build clean with /p:RunAnalyzersDuringBuild=true. IDE0032 candidates
will need to be revisited later (manual or with a working fixer).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics RCS1015
RCS1031 RCS1032 RCS1037 RCS1043 RCS1049 RCS1058 RCS1061 RCS1090
RCS1129'.

5 files updated:
* RCS1043 — removed redundant 'partial' modifier from CertificateManager
  (only one declaration exists).
* RCS1061 — merged nested 'if disposing { if m_ecdh != null }' into a
  single conjunction in Nonce.Dispose; collapsed redundant block scope
  in SecurityPolicies switch case.
* RCS1090 — replaced parenthesised lambda expression with a bare lambda
  in ManagedSessionComplianceTests.
* RCS1049 — simplified 'c.Final == false' to '!c.Final' in
  MonitoredItemTests.

Build clean with /p:RunAnalyzersDuringBuild=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics RCS1132
RCS1133 RCS1134 RCS1136 RCS1139 RCS1140 RCS1142 RCS1143 RCS1145
RCS1151'.

7 files updated:
* RCS1132 — merged BuiltInType.Number/Integer fall-through cases in
  TypeInfo.cs (consecutive cases with identical bodies).
* RCS1140 — added <exception> elements to xml-doc comments where
  ArgumentNullException is thrown for the named parameter.
* RCS1142 — added <typeparam> elements to xml-doc comments for generic
  members.
* RCS1141 — wrapped existing prose-style /// comments above test
  methods in <summary>...</summary> tags in ApplicationConfigurationTests.

Build clean with /p:RunAnalyzersDuringBuild=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stateless helpers that resolve a CertificateIdentifier into a Certificate

without mutating the identifier.

First step toward removing the embedded Certificate cache (and IDisposable)

from CertificateIdentifier. Resolution order:

  1. registry.ApplicationCertificates by thumbprint (when registry given)

  2. inline RawData materialization

  3. fallback to opening the identifier's store and Find()

LoadPrivateKeyAsync mirrors today's CertificateIdentifier.LoadPrivateKeyExAsync

(password provider, X509Store fallback, applicationUri fallback after rotation)

but never writes back into the identifier.

OpenStore is the registry-less store opener used by call sites that today

do id.OpenStore(telemetry).

No call sites migrated yet — purely additive.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics RCS1006
RCS1013 RCS1033 RCS1078 RCS1085 RCS1089 RCS1097 RCS1112'.

3 files updated:
* RCS1085 — replaced backing-field + expression-bodied property pairs
  with auto-implemented properties in OptionSet (TypeId,
  BinaryEncodingId, XmlEncodingId, ByteLength) and RoleBasedIdentity
  (InnerIdentity).
* RCS1049 — simplified 'c.Final == true' / 'c.ItemDisposed == true'
  comparisons in MonitoredItemTests.

Build clean with /p:RunAnalyzersDuringBuild=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics RCS1170
RCS1207 RCS1225 RCS1244 RCS1246 RCS1249 RCS1252 RCS1257 RCS1258
RCS1259'.

2 files updated:
* RCS1249 — removed unnecessary null-forgiving '!' suffix on
  'password' arg in CertificateIdentifier (compiler can already prove
  the value is non-null at the call site).
* RCS1252 — removed redundant try/empty-finally wrapper around the
  revoked-certificates loop in CertificateFactoryTest (the finally
  was empty so the try is redundant).

Build clean with /p:RunAnalyzersDuringBuild=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics RCS0010
RCS0011 RCS0021 RCS0023 RCS0025 RCS0027 RCS0029 RCS0031'.

7 files updated:
* RCS0027 — moved binary operator (&&, ||) from leading to trailing
  position when wrapping, in NodeManagerBuilder, HistoryAotTests,
  MonitoredItemTests, MockResolver, EncodeableFactoryExtensions.
* RCS0029 — added blank lines between property accessors and adjacent
  backing fields in CertificateValidationCore; expanded empty
  expression-bodied methods to braced empty blocks in
  CertificateManagerTests.

Build clean with /p:RunAnalyzersDuringBuild=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics RCS0033
RCS0034 RCS0041 RCS0042 RCS0046 RCS0049 RCS0050 RCS0051 RCS0055
RCS0059 RCS0061 RCS0063 RCS0056 RCS1007 RCS1047 RCS1077 RCS1156
RCS1160 RCS1166 RCS1174 RCS1202 RCS1227 RCS1229'.

6 files updated:
* RCS0050 — collapsed double blank lines (between using directives
  and namespace) in CertificateManager, ServerBase,
  ApplicationConfigurationTests, X509TestUtils, RoleBasedIdentity.
* RCS1186 — collapsed multi-line auto-property declaration to single
  line in DefaultSubscriptionEngineTests.
* RCS0033 / continuation indent — re-indented a collection-expression
  initializer in CertificateManager. Manual nudge to align the
  closing bracket with the opening (the formatter shifted one but
  not the other).

Build clean with /p:RunAnalyzersDuringBuild=true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 1b/c of the CertificateIdentifier-cache removal:

* CertificateManager.LoadApplicationCertificatesAsync now uses

  CertificateIdentifierResolver.LoadPrivateKeyAsync instead of

  certId.FindAsync(true, ...). The resolver opens the identifier's store

  directly without touching the (about-to-be-removed) m_certificate cache

  on CertificateIdentifier.

* CertificateManager.UpdateAsync drops the f444c09 LoadPrivateKeyExAsync

  invalidation loop. It existed solely to scrub the identifier cache

  before reload; the resolver path doesn't read the cache, so the loop

  is no longer needed.

* CertificateIdentifierResolver.LoadPrivateKeyAsync gains a third post-

  rotation fallback: when both (configured-thumbprint, configured-subject)

  and (configured-thumbprint, applicationUri) lookups miss, retry with

  (null thumbprint, null subject, applicationUri). This is the case

  exercised by GDS UpdateCertificate NewKeyPair tests where the pushed

  cert has a different subject (and obviously a different thumbprint).

* ConfigurationNodeManager.UpdateCertificateInternalAsync drops the f444c09

  existingCertIdentifier.Certificate = newCert poke. Replaced with:

  - read the current cert thumbprint from the registry (not the identifier

    cache) so the store DELETE targets the right blob;

  - call lifecycle.UpdateApplicationCertificateAsync to atomically install

    the new cert in the registry and fire the change notification.

Local Gds.Tests verification: PushTest UpdateCertificate suite 92/104 passed

(0 failures, 12 Assert.Ignore skips for unsupported key formats).

Both work-arounds shipped in f444c09 are now removed in favour of the

resolver. The CertificateIdentifier cache itself remains for now (still

consumed elsewhere) — its removal will follow once the rest of the call

graph is migrated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applied via 'dotnet format analyzers UA.slnx --diagnostics NUnit2001
NUnit2002 NUnit2003 NUnit2004 NUnit2005 NUnit2006 NUnit2007 NUnit2008
NUnit2009 NUnit2010'.

2 files updated:
* NUnit2010 — converted Assert.That(x.Equals(y), Is.True) to
  Assert.That(x, Is.EqualTo(y)) in CertificateWrapperTests
  (EqualsRoundTrip and EqualsObjectOverloadWorks).
* NUnit2009 — converted 'is 0 or 1' pattern assertion to
  Or-chained EqualConstraint in ManagedSessionComplianceTests.
* NUnit2010 — narrowly suppressed for the
  'EqualsSameReferenceReturnsTrue' test where the reflexive
  Equals(self) check is intentional; rewriting it as
  Is.EqualTo(self) trips NUnit2009 (same actual + expected).

Build clean with /p:RunAnalyzersDuringBuild=true. Tests green:
* Opc.Ua.Security.Certificates.Tests net10.0: 236 passed.
* Opc.Ua.Client.Tests net10.0: 2177 passed, 381 skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
try
{
using (ICertificateStore appStore = existingCertIdentifier.OpenStore(Server.Telemetry))
// Resolve the currently-loaded certificate so we can
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this whole delete into Certificate manager, imo No one Else should have to have write Access to the application Certificates

Copilot AI and others added 11 commits May 7, 2026 17:13
Captures the inventory, batching, and verification commands used during
the .editorconfig-driven cleanup pass, plus per-rule notes for
diagnostics that needed manual handling or narrow suppression. Records
the deferred CA-family warnings (CA2000, CA2007, CA1859, etc.) with
counts and reasons so a follow-up PR can pick them up one rule family
at a time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CertificateManagerTests.LoadApplicationCertificatesLoadsFromConfig and

SendCertificateChainBlobMatchesLeafOrFullChain construct identifiers via

new CertificateIdentifier(cert) without a StorePath. The resolver's store-

open path returns null for these (no store metadata), causing the registry

to come up empty.

Add a guarded fallback at the top of LoadPrivateKeyAsync: when StorePath

is empty AND the identifier carries an in-memory Certificate with a

private key, return that cert. The StorePath check matters — when a path

is configured, the store is the authoritative source and the cached cert

may be stale (e.g. after a GDS push that replaced the on-disk cert under

an identifier whose ApplicationInstance pre-cached the old version).

Local verification:

* Core CertificateManagerTests: 15/15 passed

* Gds PushTest UpdateCertificate suite: 92 passed, 12 skipped, 0 failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ient off identifier cache

Phase 1d:

* UserIdentity (.ctor and CreateAsync) loads via

  CertificateIdentifierResolver.LoadPrivateKeyAsync instead of

  certificateId.LoadPrivateKeyExAsync. Resolver returns the cert; the

  identifier is no longer mutated to cache it.

* GDS server CertificateGroup.LoadSigningKeyAsync and the issuer-key path

  in CreateRevocationListAsync stop wrapping a Certificate in a

  CertificateIdentifier(cert) just to call LoadPrivateKeyAsync. Build a

  metadata-only identifier (StorePath/StoreType/Thumbprint/SubjectName/

  CertificateType derived from the in-memory cert) and pass it to the

  resolver. The 'using' decl on the identifier is dropped — identifiers

  no longer own a Certificate.

* ConsoleReferenceClient.Program drops 'using' on the

  FindUserCertificateIdentifierAsync result. The returned identifier is

  consumed by UserIdentity.CreateAsync which loads via the resolver and

  doesn't take ownership of the identifier itself.

Local: PushTest RsaSha256/UpdateCertificateNewKeyPairPEM still passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eyAsync

CertificateIdentifierResolver.LoadPrivateKeyAsync replaced
CertificateIdentifier.FindAsync as the cert-loading entry point in
CertificateManager.LoadApplicationCertificatesAsync. The resolver
went straight to OpenStore(identifier, ...) which fails for any
CertificateIdentifier that was constructed in-memory from an existing
Certificate (no StorePath) — a documented usage of the
CertificateIdentifier(Certificate) constructor.

Restore the same fast-path FindAsync had:

    Certificate? attached = identifier.Certificate;
    if (attached != null && attached.HasPrivateKey)
    {
        return attached.AddRef();
    }

This unblocks the
CertificateManagerTests.LoadApplicationCertificatesLoadsFromConfig and
CertificateManagerTests.SendCertificateChainBlobMatchesLeafOrFullChain
test cases which build the chain in-memory.

Build clean with /p:RunAnalyzersDuringBuild=true. Core 6615/6701
(net10.0), 6567/6653 (net48); Server 920/925 (both TFMs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… into x509

# Conflicts:
#	Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifierResolver.cs
…h Resolver

Migrate the remaining production callers of CertificateIdentifier.FindAsync /
LoadPrivateKeyExAsync that don't change behavior to the new
CertificateIdentifierResolver:

* CertificateTrustList.GetCertificatesAsync - resolves explicit
  TrustedCertificates entries (inline RawData + store search) via the
  resolver instead of FindAsync.
* CertificateValidationCore: validator UpdateAsync's ApplicationCertificates
  loop, GetTrustedCertificateAsync explicit trusted list, and
  GetIssuerNoExceptionAsync explicit issuer list.
* SecurityConfiguration.FindApplicationCertificateAsync.
* ConfigurationNodeManager: 2 LoadPrivateKeyExAsync calls in
  CSR/UpdateCertificate flows.

CertificateIdentifierResolver.ResolveAsync gains a transitional fast
path that returns identifier.Certificate.AddRef() when set (mirrors the
legacy FindAsync first branch). This bridge will be removed in Phase 3
when m_certificate is stripped from CertificateIdentifier.

ApplicationInstance.CheckOrCreateCertificateAsync /
DeleteApplicationInstanceCertificateAsync /
ApplicationConfiguration.ValidateAsync are deferred to Phase 3 (they
need m_certificate semantics that can't be replicated without breaking
GDS PushTest UpdateCertificate variants).

Local verification:
- Build: green (UA.slnx, all TFMs)
- Core CertificateManagerTests: 19/19 pass
- Core Certificate tests: 270/276 pass (6 skipped, 0 failed)
- Configuration tests: 208/208 pass
- Gds PushTest UpdateCertificate suite: 92/104 pass, 12 skipped, 0 failed
  (matches f444c09 baseline)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… CertificateIssuerReference record

Introduce a public sealed record CertificateIssuerReference(Certificate, CertificateValidationOptions)
as the carrier for resolved chain elements in:

* CertificateValidationCore: GetTrustedCertificateAsync, GetIssuerAsync,
  GetIssuerNoExceptionAsync now return CertificateIssuerReference?
  (or (CertificateIssuerReference?, ServiceResultException?) for the
  no-exception variant).
* CertificateValidationCore.GetIssuersAsync (both overloads): IList type
  changes from CertificateIdentifier to CertificateIssuerReference.
  The chain-walk advance no longer goes through issuer.FindAsync(false)
  (which only worked because CertificateIdentifier cached m_certificate);
  it borrows the issuer cert directly from the record.
* CertificateValidationCore.CheckChainStatus: signature switches to
  (X509ChainStatus, CertificateIssuerReference target,
   CertificateIssuerReference? issuer, bool). Note: target options are
  used for SuppressCertificateExpired (lines 1611-1630 in legacy).
* CertificateValidationCore.InternalValidateAsync: target/issuer locals
  carry CertificateIssuerReference, fallback target uses
  CertificateValidationOptions.Default. Disposal moved from
  CertificateIdentifier.Dispose() to issuer.Certificate.Dispose() in the
  finally block (records are not disposable).
* ICertificateRegistry.GetIssuersAsync: public API change — return
  IList<CertificateIssuerReference>.
* CertificateManager.GetIssuersAsync (interface impl): same change.
* Session.LoadCertificateChainAsync: uses issuers[i].Certificate (the
  record's non-nullable Certificate field).
* CertificateManagerTests.GetIssuersAsyncReturnsEmptyForSelfSignedCertificate:
  test fixture switches list type.

Disposal model:
- Records do not own their Certificate field. The chain construction
  paths (resolver, store) AddRef when handing certs to the record, so
  the record holds an owned reference. Callers (validator, Session,
  GDS) explicitly dispose via record.Certificate.Dispose() in finally
  blocks.

Behaviour preserved:
- Issuer dedup uses Cert.Thumbprint (was CertificateIdentifier.Thumbprint
  which read from m_certificate).
- Validation options for SuppressCertificateExpired now correctly read
  from target (per-element), not just issuer.

Local verification:
- Build green (UA.slnx, all TFMs).
- Core Certificate tests: 270/276 (6 skipped, 0 failed).
- Configuration tests: 208/208 pass.
- Server cert/validation tests: 4/4 pass.
- Client chain/issuer tests: 1/1 pass.
- Gds PushTest UpdateCertificate: 92/104 (12 skipped, 0 failed) —
  matches f444c09 baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ectionStore + AddTrustedPeer

* CertificateIdentifierCollectionStore now stores certificates directly
  in a CertificateCollection instead of a List<CertificateIdentifier>.
  All operations (EnumerateAsync, AddAsync, DeleteAsync,
  FindByThumbprintAsync) become synchronous — no more FindAsync round
  trip through CertificateIdentifier. Disposal: m_certificates.Dispose()
  on Dispose(true) drops AddRef'd certs.
* Removed the (ArrayOf<CertificateIdentifier>, ITelemetryContext)
  constructor — unused in production (only the empty ctor is
  instantiated by CertificateStoreIdentifier.CreateStore and
  InMemoryStoreProvider).
* AddAsync AddRef's the input cert; DeleteAsync disposes on remove;
  EnumerateAsync / FindByThumbprintAsync return AddRef'd copies.
  Caller-owned reference semantics throughout.

* SecurityConfiguration.AddTrustedPeer(byte[]) switches from the
  CertificateIdentifier(byte[]) ctor to the RawData setter pattern.
  Resolves identically through CertificateIdentifierResolver's inline
  RawData branch. Public API preserved.

Local verification:
- Build green (UA.slnx, all TFMs)
- Core full tests: 6615/6701 (86 skipped, 0 failed)
- Cert-related tests: 96/98 (2 skipped, 0 failed)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…date docs

Phase 3 (strip): CertificateIdentifier becomes pure metadata.

Removed members:
* m_certificate field and Certificate get/set property
* IDisposable, Dispose(), DisposeCertificate()
* Constructors taking Certificate, (Certificate, options), or byte[]
* Instance methods FindAsync(...), LoadPrivateKeyAsync(char[], ...),
  LoadPrivateKeyExAsync(...), OpenStore(...)
* IOpenStore declaration on CertificateIdentifier

Behaviour:
* RawData is now backed by an explicit byte[] field. Setter still
  derives SubjectName/Thumbprint/CertificateType from the parsed cert.
* SubjectName/Thumbprint getters return the configured metadata
  (no fallback to a cached cert).
* Equals compares metadata only.

Production callers migrated off the cache:
* ApplicationInstance.CheckOrCreateCertificateAsync /
  CreateApplicationInstanceCertificateAsync /
  DeleteApplicationInstanceCertificateAsync /
  CheckApplicationInstanceCertificateAsync use local Certificate
  variables and CertificateIdentifierResolver instead of id.Certificate.
  CheckApplicationInstanceCertificateAsync now syncs the identifier
  metadata (Thumbprint/SubjectName/CertificateType) from the loaded
  cert so subsequent resolver lookups by Thumbprint find it.
* ApplicationConfiguration.ValidateAsync now eagerly calls
  CertificateManager.LoadApplicationCertificatesAsync to populate the
  registry instead of mutating each identifier's cached cert.
* SecurityConfiguration.FindApplicationCertificateAsync uses
  Resolver.LoadPrivateKeyAsync when privateKey is requested (the
  resolver's Find path enumerates only public certs from Directory
  stores). The ECC type filter now matches by CertificateType
  instead of inspecting the cached cert.
* SecuredApplicationHelpers (CertificateIdentifier wrapper for the
  schema namespace) delegates Find/OpenStore to the resolver.
* DiscoveryClient.CreateAsync uses the resolver.
* CertificateValidationCore: the untrustedList for chain construction
  now wraps each cert as a metadata-only CertificateIdentifier with
  RawData set (the resolver's inline RawData branch handles it).
  Removed the per-iteration ownedCertificates list and untrusted
  identifier disposal — both are no-ops now.
* ConfigurationNodeManager: post-rotation existing-cert lookup falls
  back to ICertificateRegistry.GetApplicationCertificate when the
  configured subject doesn't match the new cert's subject.
  CreateSigningRequest queries the registry for the active cert's
  subject/domain. GetCertificates returns blobs from the registry.

Phase 4 (tests):
* CertificateIdentifierTests: dropped tests for removed APIs (cert
  ctors, DisposeCertificate, Certificate property, OpenStore on
  identifier). Added RawDataSetterDerivesSubjectAndThumbprint to
  cover the metadata-derivation behaviour.
* ApplicationInstanceTests: switched `id.FindAsync` /
  `id.OpenStore` / `applicationCertificate.Certificate` reads to
  the resolver. Replaced
  `Assert.That(applicationCertificate.Certificate, Is.EqualTo(...))`
  with thumbprint-based assertions.
* CertificateStoreTest: switched to `Resolver.LoadPrivateKeyAsync`.
* CertificateManagerTests: tests now persist the cert to a temp dir
  before constructing the metadata identifier so the resolver can
  load it.
* ClientTests / CreateSessionApplicationUriValidationTests: dropped
  `using` on identifiers, dropped `Certificate = ...` writes,
  added explicit `CertificateType`.
* Gds.Tests: TestUtils.CleanupTrustListAsync gains a
  `CertificateIdentifier`-typed overload that uses
  `Resolver.OpenStore`. `CertificateGroupTests` switched to
  `Resolver.LoadPrivateKeyAsync`. `GlobalDiscoveryTestClient` /
  `GlobalDiscoveryTestServer` clean-up paths use the resolver.
  `ApplyNewApplicationInstanceCertificateAsync` builds a
  metadata-only identifier (Thumbprint+Subject+StoreType+StorePath
  from the previous identifier).

Phase 5 (docs):
* Docs/CertificateManager.md: added a callout near the top + a new
  `Migration: CertificateIdentifier is metadata-only` section with
  before/after tables, resolver examples, and registration guidance.
* Docs/MigrationGuide.md: added an equivalent
  `CertificateIdentifier is metadata-only` subsection under the
  CertificateManager block in the 1.5.378 → 1.6.x migration notes.

Local verification:
* Build green (UA.slnx, all TFMs).
* Core tests: 6611/6697 (86 skipped, 0 failed).
* Configuration tests: 208/208.
* Server tests: 920/925 (5 skipped, 0 failed).
* Gds tests: 517/542 (25 skipped, 0 failed) — full suite, including
  PushTest UpdateCertificate variants, all green locally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants