Skip to content

[Server] Fix #3896: send full certificate chain when CA is in the issuer store#3897

Merged
marcschier merged 3 commits into
masterfrom
sendcertchain
Jun 21, 2026
Merged

[Server] Fix #3896: send full certificate chain when CA is in the issuer store#3897
marcschier merged 3 commits into
masterfrom
sendcertchain

Conversation

@marcschier

@marcschier marcschier commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes #3896. A server configured with SecurityConfiguration.SendCertificateChain = true, whose application instance certificate is issued by a CA placed in the issuer store (TrustedIssuerCertificates), sent only the leaf certificate instead of the full chain. Clients that rely on receiving the chain then failed to connect.

Root cause (2.0 line)

The 2.0 CertificateManager refactor dropped issuer-chain resolution from the application-certificate load path. CertificateManager.LoadApplicationCertificatesAsync constructed every CertificateEntry with an empty issuer chain ([]), so CertificateEntry.GetEncodedChainBlob() / LoadCertificateChainRaw always returned the leaf only — SendCertificateChain = true was effectively ignored regardless of where the CA lived. (The defect was masked by a unit test that manually injected the issuer chain.)

This is a regression from 1.5.374, where ServerBase resolved the chain at startup and appended every resolved issuer unconditionally.

Fix

  • New CertificateManager.ResolveIssuerChainAsync resolves the issuer chain from the configured trusted and issuer stores. It appends every resolved issuer regardless of trust state — GetIssuersAsync returns isTrusted (true only when the issuer is in the trusted store), which reports trust, not resolution success, so it is intentionally ignored. Genuine resolution failures are logged and swallowed so they never block certificate registration / server startup (a leaf-only chain is returned); caller-requested cancellation is propagated so shutdown / abort stays responsive.
  • LoadApplicationCertificatesAsync and UpdateApplicationCertificateAsync (cert rotation / GDS push) now populate CertificateEntry.IssuerChain.
  • All server send-paths derive from CertificateEntry.IssuerChain — the endpoint ServerCertificate (ServerBase, TcpTransportListener, HttpsServiceHost) and the secure-channel SenderCertificate (UaSCBinaryChannel) — so the full chain is now emitted everywhere.

The companion fix for the 1.5.x maintenance line (master378) is in a separate PR.

Tests

  • De-masked SendCertificateChainBlobResolvesFullChainFromIssuerStore (formerly SendCertificateChainBlobMatchesLeafOrFullChain): the CA is placed in the issuer store only — not the trusted-peer store — and the chain blob is asserted to be leaf || CA byte-for-byte, resolved entirely by the real load path (no manual injection).
  • New LoadApplicationCertificatesProducesLeafOnlyBlobWhenIssuerUnavailable: when the CA is in neither store, the blob falls back to leaf-only without throwing.
  • Verified locally on net48 and net10.0: full Security.Certificates suite (221 passed / 6 skipped), CertificateManager + cert-rotation categories (35 passed), and the ChannelManagerCertRotationIntegration test. Build is clean (no new analyzer warnings).

Related Issues

Fixes #3896.

Checklist

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation (no public API change; behavioural fix only).
  • 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. (Ran the certificate-related suites on net48 + net10.0; full-solution run pending CI.)
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

…uer store

CertificateManager.LoadApplicationCertificatesAsync built every CertificateEntry with an empty issuer chain, so SendCertificateChain=true had no effect and servers always sent the leaf certificate only. Resolve the issuer chain from the configured trusted/issuer stores when loading (and rotating) application certificates so the full chain is emitted in both the endpoint ServerCertificate and the secure-channel SenderCertificate.

- Add CertificateManager.ResolveIssuerChainAsync: appends every resolved issuer regardless of trust state (GetIssuersAsync's bool reports trust, not resolution); failures are logged and never block startup.

- LoadApplicationCertificatesAsync and UpdateApplicationCertificateAsync now populate CertificateEntry.IssuerChain.

- De-mask the SendCertificateChain test (no manual issuer injection) and add a leaf-only fallback test.
Copilot AI review requested due to automatic review settings June 20, 2026 05:39
@CLAassistant

CLAassistant commented Jun 20, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ marcschier
❌ agent


agent seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@marcschier

Copy link
Copy Markdown
Collaborator Author

Companion 1.5.x (master378) fix: #3898. Same symptom, different code path (the 2.0 CertificateManager drops the chain at load; the 1.5.x CertificateTypesProvider gates it on the isTrusted return).

Copilot AI left a comment

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.

Pull request overview

This PR fixes a regression where servers configured with SecurityConfiguration.SendCertificateChain = true would still send only the leaf application certificate when the issuing CA was placed in the issuer store (TrustedIssuerCertificates), causing some clients to fail to connect (issue #3896). The change restores issuer-chain resolution during application-certificate load/update so the server can emit the full DER chain blob (leaf || issuers) consistently.

Changes:

  • Populate CertificateEntry.IssuerChain by resolving issuers from trusted + issuer stores during LoadApplicationCertificatesAsync.
  • Ensure UpdateApplicationCertificateAsync (rotation/GDS push) also resolves and stores the issuer chain when the caller doesn’t supply one.
  • Update and add regression tests to validate full-chain emission from the issuer store and leaf-only fallback when issuers are unavailable.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
Stack/Opc.Ua.Core/Security/Certificates/CertificateManager/CertificateManager.cs Adds issuer-chain resolution during app cert load/update to restore correct chain blob emission.
Tests/Opc.Ua.Core.Tests/Security/Certificates/CertificateManager/CertificateManagerTests.cs Updates regression coverage for #3896 and adds a negative-path test for missing issuer resolution.

ResolveIssuerChainAsync caught all exceptions, swallowing OperationCanceledException from GetIssuersAsync and preventing cancellation-aware callers (LoadApplicationCertificatesAsync / UpdateApplicationCertificateAsync) from aborting -- risking unresponsive shutdown.

Rethrow OperationCanceledException when the caller's token is cancelled (disposing the empty chain to avoid a leak); only genuine resolution failures are still logged and swallowed. Adds UpdateApplicationCertificatePropagatesCancellation regression test.
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.77419% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 72.78%. Comparing base (76bceb5) to head (69f8166).

Files with missing lines Patch % Lines
...tificates/CertificateManager/CertificateManager.cs 96.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3897      +/-   ##
==========================================
+ Coverage   72.76%   72.78%   +0.01%     
==========================================
  Files         917      917              
  Lines      148994   149022      +28     
  Branches    25814    25816       +2     
==========================================
+ Hits       108412   108462      +50     
+ Misses      31162    31150      -12     
+ Partials     9420     9410      -10     
Files with missing lines Coverage Δ
...tificates/CertificateManager/CertificateManager.cs 75.09% <96.77%> (+1.55%) ⬆️

... and 20 files with indirect coverage changes

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

@marcschier

Copy link
Copy Markdown
Collaborator Author

/azp run OPCFoundation.UA-.NETStandard

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@marcschier
marcschier merged commit a3256ce into master Jun 21, 2026
136 of 139 checks passed
@marcschier
marcschier deleted the sendcertchain branch June 21, 2026 10:07
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.

SendCertificateChain set to true not working as expected for the opc ua server

4 participants