# Description Follow-up to the master378 fix in #4678 (CTT: application certificate renewal must use the configured trust stores). Master already ignores the supplied `issuerCertificates` for the default application group (OPC 10000-12 §7.10.5), but it had no regression tests for CA-signed renewals. While adding them I found a real gap: **The validator treated `Bad_CertificateChainIncomplete` as suppressible.** OPC 10000-4 Table 100 (Build Certificate Chain) says: "An error during the chain creation may not be suppressed." §6.1.3 adds: "Processing fails with Bad_SecurityChecksFailed if an element in the chain cannot be found." UpdateCertificate for the application group accepts every *suppressible* error (as §7.10.5 requires). As a result it accepted a renewal signed by a CA that is **not** in the TrustList and was only passed in the ignored issuer list, which is exactly what §7.10.5 rules out. ## Changes - `CertificateValidationCore`: remove `BadCertificateChainIncomplete` from the suppressible status codes. An accept callback (`AcceptError`) is no longer asked about it. - `TrustList`: remove the local workaround that filtered out this error; plain "accept suppressible errors" is now enough. - Docs (`Certificates.md`, `CertificateManager.md`): drop it from the suppressible list, and fix a sample that suppressed it. ## Tests - `ConfigurationNodeManagerPushTests`: - A CA-signed renewal with the CA installed succeeds with a missing, empty, unrelated or malformed issuer list, and the trust stores stay unchanged. - A renewal whose CA is only supplied as an issuer is rejected with `BadCertificateChainIncomplete`; stores and the current certificate are untouched. - End to end on an isolated server: a CA-signed renewal with no issuer list is committed through ApplyChanges. - `ConfigurationNodeManagerPushValidationTests`: the group TrustList validator accepts a CA from the trusted or issuer store and rejects an uninstalled CA. - `CertificateValidationCoreTests` / `CertificateValidatorTest`: - An incomplete chain is unsuppressible and never reaches the callback. - A leaf trusted directly **stays valid when the peer supplies its CA** (§6.1.3: chain certificates "may be provided with the application Certificate"). - One existing test attached its approver only after validating, so it never exercised suppression; it now validates with the approver attached. - Tests that only passed because of the bug (`PushCertificateRequestRegressionTests`, the regenerated-key helper and the rollback test) now install the signing CA in the TrustList first. The two new rejection tests fail on master and pass with this change. ## Reviewer notes / behavior change This was suppressible in 1.5.x as well. Application code whose accept handler approved a CA-signed peer certificate without its CA (CA neither installed nor sent by the peer) will now get a rejection. Without a handler such certificates were already rejected, and the stack's own handlers (`ApplicationInstance`, push validation) never accepted this error. Worth a release note. Local runs (net10.0), all passing: | Suite | Passed | Failed | |---|---|---| | Server.Tests | 8310 | 0 | | Core.Tests (security/transport filter) | 2039 | 0 | | Security.Certificates.Tests | 358 | 0 | | Core.Security.Tests | 558 | 0 | | Client.Tests | 2761 | 0 | | Gds.Tests (PushTest and TrustList tests) | 345 | 0 | The net48 builds of Server.Tests and Core.Tests also succeed. ## Related Issues - Related to #4678 (master378 counterpart) ## Checklist - [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. - [ ] 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. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
46 KiB
Certificates
The application creates its required OPC UA certificates in a directory or OS-level certificate store on first startup. They remain in use until an administrator deletes them from the store.
The stack also supports application certificates issued by a certificate authority (CA), plus remote certificate-store and trust-list management through a Global Discovery Server using Server Push.
Private keys do not have to live in a file or OS certificate store. A TPM,
smart card, hardware security module (HSM), or remote key service can hold a
key without exposing it to process memory. See CryptoProvider
for pluggable cryptography and hardware-held keys, or use the
OPCFoundation.NetStandard.Opc.Ua.Security.Pkcs11 package for PKCS#11 tokens.
Contents
- Certificate stores
- X509Store on Windows
- Certificate and CertificateCollection Types
- Opening a certificate store
- Windows .NET applications
- Windows UWP applications
- .NET Core applications on Windows, Linux, iOS etc
- Certificate Validation
- Validation Workflow
- Chain Building Process
- Certificate List Configuration
- Configuration Settings
- Suppressible Validation Errors
- Inspecting Certificate Validation Results
- Configuring a Custom Certificate Validator
- Best Practices
Certificate stores
File-based sample applications follow the certificate-store layout
recommended in the specification.
The root folder, called pki, contains a certs folder for certificates, a
private folder for private keys, and a crl folder for certificate
revocation lists (CRLs).
The UA .NET Standard stack supports the following certificate stores:
-
The Application store
<root>/ownwhich contains private keys used by the application. -
The Issuer store
<root>/issuerwhich contains certificates which are needed for validation, for example to complete the validation of a certificate chain. A certificate in the Issuer store is not trusted! -
The Trusted store
<root>/trustedwhich contains certificates which are trusted by the application. The certificates in this store can either be self signed, leaf, root CA or sub CA certificates. The most common use case is to add a self signed application certificate to the Trusted store to establish trust with that application. If the application certificate is the leaf of a chain, the trust can be established by adding the root CA, a sub CA or the leaf certificate itself to the Trusted store. Each of the options enables a different set of trusted certificates. A trusted Root CA or Sub CA certificate acts as the chain's trust anchor. The application trusts a leaf certificate when its chain contains that anchor. The specification still requires validation of the entire chain. For this validation, the Issuer store must contain every certificate in the chain except the leaf.If only the leaf certificate is in the Trusted store and the rest of the chain is stored in the Issuer store, then only the leaf certificate is trusted. As an example, to trust an application certificate that is issued by a Root CA, only the Root CA certificate is required in the Trusted store to establish trust to all application certificates issued by the CA. This option can greatly simplify the management of OPC UA Clients and Servers because only one certificate needs to be distributed across all systems.
-
The Rejected store
<root>/rejectedwhich contains certificates which have been rejected. This store is provided as a convenience for the administrator of an application to allow to copy an untrusted certificate from the Rejected to the Trusted store to establish trust with that application. -
The Issuer User store
<root>/issuerUserwhich contains user certificates which are used to validate user certificates. -
The Trusted User store
<root>/trustedUserwhich contains user certificates which are trusted by an application. To establish trust, the same rules apply as explained for the Trusted and the Issuer store. -
The Issuer Https store
<root>/issuerHttpswhich contains https certificates which are used to validate https connection certificates. -
The Trusted Https store
<root>/trustedHttpswhich contains https certificates which are trusted by an application. To establish trust, the same rules apply as explained for the Trusted and the Issuer store.
X509Store on Windows
On the Windows OS the X509Store supports the storage and retrieval of CRLs. This enables the usage of the X509Store instead of the Directory Store for stores requiring the use of crls, e.g. the issuer or the directory Store.
Certificate and CertificateCollection Types
The stack uses the Certificate wrapper type (in Opc.Ua.Security.Certificates) instead of X509Certificate2 directly. Certificate wraps X509Certificate2 with reference counting for safe shared ownership — the inner X509Certificate2 is disposed only when the last reference is released. Use Certificate.AddRef() before sharing and Dispose() to release.
CertificateCollection implements IList<Certificate> and IDisposable. Disposing a collection decrements the reference count on each member. Store methods like EnumerateAsync() call AddRef() on cached certificates, so disposing the returned collection is safe.
For interop with .NET APIs that require X509Certificate2, use certificate.AsX509Certificate2() which creates a copy that the caller must dispose.
See CertificateManager.md for details.
Opening a certificate store
A CertificateStoreIdentifier is only a description of a store (StoreType / StorePath) — the analogue of a CertificateIdentifier resolving to a Certificate. Every call to OpenStore creates and opens a new ICertificateStore instance that the caller owns and must dispose:
var identifier = new CertificateStoreIdentifier(storePath);
using ICertificateStore store = identifier.OpenStore(telemetry);
CertificateCollection certificates = await store.EnumerateAsync(ct);
A component that accesses a store repeatedly should open one instance and keep it for the component's lifetime. Dispose the store at shutdown instead of reopening it for each operation. The store refreshes its parsed-certificate cache when the backing data changes.
Windows .NET applications
By default the self signed certificates are stored in a X509Store called CurrentUser\UA_MachineDefault. The certificates can be viewed or deleted with the Windows Certificate Management Console (certmgr.msc). The trusted, issuer and rejected stores remain in a folder called OPC Foundation\pki with a root folder which is specified by the SpecialFolder variable %CommonApplicationData%. On Windows 7/8/8.1/10 this is usually the invisible folder C:\ProgramData.
Windows UWP applications
By default the self signed certificates are stored in a X509Store called CurrentUser\UA_MachineDefault. The certificates can be viewed or deleted with the Windows Certificate Management Console (certmgr.msc).
The trusted, issuer and rejected stores remain in a folder called OPC Foundation\pki in the LocalState folder of the installed universal windows package. Deleting the application state also deletes the certificate stores.
.NET Core applications on Windows, Linux, iOS etc
The self signed certificates are stored in a folder called OPC Foundation/pki/own with a root folder which is specified by the SpecialFolder variable %LocalApplicationData% or in a X509Store called CurrentUser\My, depending on the configuration. For best cross platform support the personal store CurrentUser\My was chosen to support all platforms with the same configuration. Some platforms, like macOS, do not support arbitrary certificate stores.
The trusted, issuer and rejected stores remain in a shared folder called OPC Foundation\pki with a root folder specified by the SpecialFolder variable %LocalApplicationData%. Depending on the target platform, this folder maps to a hidden locations under the user home directory.
Certificate Validation
The OPC UA .NET Standard Stack validates certificates according to the OPC UA
specification. CertificateManager provides centralized certificate
management with trust-list-scoped validation, lifecycle monitoring, and
pluggable store backends (see CertificateManager.md).
The legacy validator and adapter are removed. This section explains trust,
validation workflow, and configuration; the current API reference lives in
CertificateManager.
Validation Workflow
The certificate validation process follows these steps:
-
Structural Check: Any certificate in the chain whose subject or issuer is an empty distinguished name is rejected immediately with the non-suppressible
Bad_CertificateInvalid. An empty name is an emptyRDNSequence: it identifies nothing, and two unrelated issuers become indistinguishable, so such a certificate can never take part in a trust decision. RFC 5280 §4.1.2.4 requires a non-empty issuer, and §4.1.2.6 only permits an empty subject for an end entity carrying a criticalsubjectAltName— which a CA may never do. This check runs before the trust-list lookup and the chain build so the outcome is identical on every target framework, rather than depending on whether the platform's X.509 parser happens to accept the certificate. -
Validation Cache:
UseValidatedCertificatescan skip expensive chain-policy rebuilding for a previously accepted certificate. The issuer walk and revocation checks still run before a cached result is used, so a newly revoked certificate is not accepted solely because it was previously cached. -
Trust Check: The validator checks if the certificate is explicitly trusted by searching in:
- The trusted certificate list (
TrustedPeerCertificates.TrustedCertificates) - An in-memory collection of explicitly trusted certificates - The trusted certificate store (
TrustedPeerCertificates.StorePath) - A file system directory or X509Store containing trusted certificates - The application's own certificate collection (
ApplicationCertificates) - The certificates used by the application itself
See Certificate List Configuration for details on how these lists are populated.
- The trusted certificate list (
-
Issuer Chain Validation: For certificates issued by a CA, the validator builds and validates the complete certificate chain. See Chain Building Process for detailed technical documentation.
-
Certificate Properties Validation: The validator checks:
- Certificate expiration dates (NotBefore/NotAfter)
- Key usage flags (DigitalSignature for ECDSA, DataEncipherment for RSA)
- Minimum key size requirements
- Signature algorithm strength (e.g., rejecting SHA-1 if configured)
- Certificate signature validity
-
Domain Validation: If an endpoint is provided, the validator checks that the certificate contains the endpoint's domain name in its Subject Alternative Names.
-
Application URI Validation: Verifies that the certificate contains the expected Application URI in the Subject Alternative Name extension.
-
Error Handling: If validation errors occur, they are classified as either:
- Suppressible errors: Can be accepted via
CertificateValidationOptions.AcceptErroror the manager's globalAcceptErrorcallback - Non-suppressible errors: Always cause validation to fail
- Suppressible errors: Can be accepted via
-
Rejected Certificate Storage: Failed certificates are saved to the rejected certificate store for administrator review.
Certificates with an empty distinguished name are also skipped when a PEM file is read, so they never reach a trust list in the first place. PEMReader.ImportPublicKeysFromPEM drops only the offending entry and returns the remaining certificates, so one malformed certificate cannot empty an otherwise usable trust list. DistinguishedNameUtils.HasEmptyDistinguishedName exposes the same check to applications.
Chain Building Process
This section explains the chain-building and validation algorithm used by
CertificateManager. The implementation is in the internal
CertificateValidationCore;
applications use ICertificateValidatorEx, not that internal class.
Overview
The chain building process constructs a complete certificate chain from a leaf certificate up to a self-signed root CA certificate. The algorithm searches through multiple certificate stores in a specific priority order and performs revocation checking at each step.
Chain Building Algorithm Diagram
┌─────────────────────────────────────────────────────────────────┐
│ Start Chain Building │
│ Input: Certificate Chain │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Current = certificates[0] │
│ (Leaf Certificate) │
└────────────┬───────────────┘
│
▼
┌────────────────────────────┐
│ Create untrusted list │
│ from certificates[1..n] │
└────────────┬───────────────┘
│
┌────────────▼───────────────┐
│ Loop: Find Issuer Chain │
└────────────┬───────────────┘
│
┌────────────▼───────────────────────────┐
│ Is Current certificate self-signed? │
└─┬─────────────────────────────────┬────┘
│ YES │ NO
│ │
▼ ▼
┌────────────────┐ ┌─────────────────────────────┐
│ Chain Complete │ │ Search for Issuer (Step 1) │
│ Return Result │ │ Location: Trusted Store │
└────────────────┘ └──────────┬──────────────────┘
│
┌───────────▼──────────────┐
│ Issuer Found in Trusted? │
└─┬─────────────────────┬──┘
│ YES │ NO
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ Mark as Trusted │ │ Search for Issuer (Step 2)│
│ Check Revocation │ │ Location: Issuer Store │
└──────┬─────────────┘ └────────┬─────────────────┘
│ │
│ ┌───────────▼──────────────┐
│ │ Issuer Found in Issuer? │
│ └─┬─────────────────────┬──┘
│ │ YES │ NO
│ │ │
│ ▼ ▼
│ ┌──────────────────┐ ┌──────────────────────────┐
│ │ Check Revocation │ │ Search for Issuer (Step 3)│
│ └────────┬─────────┘ │ Location: Untrusted Certs │
│ │ └────────┬─────────────────┘
│ │ │
│ │ ┌───────────▼──────────────┐
│ │ │ Issuer Found in Chain? │
│ │ └─┬─────────────────────┬──┘
│ │ │ YES │ NO
│ │ │ │
│ │ ▼ ▼
│ │ ┌──────────────────┐ ┌──────────────┐
│ │ │ Check Revocation │ │ Chain Broken │
│ │ └────────┬─────────┘ │ Return Result│
│ │ │ └──────────────┘
│ │ │
└─────────────┴───────────┘
│
┌─────────────▼──────────────┐
│ Check for Circular Chain │
│ (Duplicate Thumbprint) │
└─┬────────────────────────┬─┘
│ DUPLICATE │ UNIQUE
│ │
▼ ▼
┌────────────────┐ ┌──────────────────────┐
│ Chain Complete │ │ Add Issuer to List │
│ Return Result │ │ Current = Issuer Cert│
└────────────────┘ └──────────┬───────────┘
│
│
┌───────────────────┘
│ Loop Back to Top
└──────────────────────┐
│
▼
(Continue Building Chain)
Detailed Algorithm Steps
Step 1: Initialization
INPUT:
- certificates: CertificateCollection (certificate chain from peer)
- issuers: List<CertificateIdentifier> (output list, initially empty)
- validationErrors: Dictionary<Certificate, ServiceResultException> (output)
INITIALIZE:
- isTrusted ← false
- current ← certificates[0] // Leaf certificate
- untrustedCollection ← certificates[1..n] // Additional certs from peer
Step 2: Iterative Chain Building Loop
WHILE (issuer is found) DO:
// Exit condition: Self-signed certificate reached
IF (IsSelfSigned(current)) THEN
BREAK // Chain is complete
END IF
// Step 2.1: Search in Trusted Certificate Store
issuer ← FindIssuer(
certificate: current,
location: TrustedCertificateList + TrustedCertificateStore,
checkRevocation: true
)
IF (issuer != null) THEN
isTrusted ← true // Chain ends in trusted store
revocationStatus ← CheckCRL(issuer, current)
validationErrors[current] ← revocationStatus
GOTO Step_2.4
END IF
// Step 2.2: Search in Issuer Certificate Store
issuer ← FindIssuer(
certificate: current,
location: IssuerCertificateList + IssuerCertificateStore,
checkRevocation: true
)
IF (issuer != null) THEN
revocationStatus ← CheckCRL(issuer, current)
validationErrors[current] ← revocationStatus
GOTO Step_2.4
END IF
// Step 2.3: Search in Untrusted Certificates (from peer)
issuer ← FindIssuer(
certificate: current,
location: untrustedCollection,
checkRevocation: true
)
IF (issuer == null) THEN
BREAK // Chain building failed - no issuer found
END IF
// Step 2.4: Circular Chain Detection
FOR EACH existingIssuer IN issuers DO
IF (existingIssuer.Thumbprint == issuer.Thumbprint) THEN
BREAK LOOP // Circular chain detected
END IF
END FOR
// Step 2.5: Add Issuer to Chain
issuers.Add(issuer)
current ← LoadCertificate(issuer)
END WHILE
RETURN isTrusted
Step 3: Issuer Matching Algorithm
The FindIssuer() function uses the following matching criteria:
FUNCTION FindIssuer(certificate, location, checkRevocation):
// Extract issuer information from certificate
subjectName ← certificate.IssuerName
authorityKeyId ← ExtractAuthorityKeyIdentifier(certificate)
serialNumber ← ExtractSerialNumber(certificate)
// Search all certificates in location
FOR EACH candidate IN location DO
// Basic matching criteria
IF (candidate.SubjectName != subjectName) THEN
CONTINUE // Subject name must match issuer name
END IF
// Check if issuer is allowed (not end-entity cert)
IF (NOT IsIssuerAllowed(candidate)) THEN
CONTINUE // Must have CA capabilities
END IF
// Optional: Match by serial number
IF (serialNumber != null AND candidate.SerialNumber != serialNumber) THEN
CONTINUE
END IF
// Optional: Match by Authority Key Identifier
IF (authorityKeyId != null) THEN
subjectKeyId ← candidate.SubjectKeyIdentifier
IF (subjectKeyId != authorityKeyId) THEN
CONTINUE
END IF
END IF
// Candidate matches - perform revocation check
IF (checkRevocation) THEN
revocationStatus ← CheckCRL(candidate, certificate)
IF (revocationStatus == Revoked) THEN
RETURN (candidate, RevocationError)
END IF
END IF
RETURN (candidate, revocationStatus)
END FOR
RETURN (null, null) // No matching issuer found
END FUNCTION
Step 4: Certificate Revocation List (CRL) Checking
FUNCTION CheckCRL(issuer, certificate):
// Only check if store supports CRL operations
IF (store.SupportsCRL()) THEN
crlStatus ← store.IsRevoked(issuer, certificate)
CASE crlStatus OF
StatusCodes.Good:
RETURN null // Not revoked
StatusCodes.BadCertificateRevoked:
IF (IsCertificateAuthority(certificate)) THEN
RETURN BadCertificateIssuerRevoked
ELSE
RETURN BadCertificateRevoked
END IF
StatusCodes.BadCertificateRevocationUnknown:
IF (IsCertificateAuthority(certificate)) THEN
statusCode ← BadCertificateIssuerRevocationUnknown
ELSE
statusCode ← BadCertificateRevocationUnknown
END IF
// Check if error should be suppressed
IF (RejectUnknownRevocationStatus AND
NOT HasValidationOption(SuppressRevocationStatusUnknown)) THEN
RETURN statusCode
END IF
RETURN null // Suppressed
StatusCodes.BadNotSupported:
RETURN null // CRL not supported by store
END CASE
END IF
RETURN null
END FUNCTION
X509Chain Validation
After building the issuer chain, the validator uses .NET's X509Chain to verify cryptographic signatures and certificate properties:
// Configure chain policy
policy ← new X509ChainPolicy()
policy.RevocationMode ← NoCheck // Already checked via CRL
policy.RevocationFlag ← EntireChain
policy.VerificationFlags ← ConfigureFromValidationOptions(issuers)
// Add all found issuers to extra store
FOR EACH issuer IN issuers DO
policy.ExtraStore.Add(issuer.Certificate)
END FOR
// Build and validate chain
chain ← new X509Chain()
chain.ChainPolicy ← policy
chain.Build(certificate)
// Process chain status
FOR EACH chainStatus IN chain.ChainStatus DO
ProcessChainStatus(chainStatus)
END FOR
// Verify chain matches issuers
IF (chain.ChainElements.Count != issuers.Count + 1) THEN
chainIncomplete ← true
END IF
// Validate each chain element
FOR EACH element IN chain.ChainElements DO
ValidateChainElement(element)
END FOR
Chain Validation Results
The chain building process returns:
- isTrusted:
trueif any issuer was found in the trusted store,falseotherwise - issuers: List of all issuer certificates in the chain (leaf to root, excluding the leaf itself)
- validationErrors: Dictionary mapping certificates to their revocation status errors
These results are then used by the main validation logic to determine if the certificate should be accepted or rejected.
Key Behaviors
- Search Priority: Trusted store → Issuer store → Untrusted collection (from peer)
- Trust Anchoring: If any issuer is found in the trusted store, the entire chain is considered to have a trusted anchor
- CRL Checking: Performed during chain building if the store supports it
- Circular Chain Detection: Prevents infinite loops by checking for duplicate thumbprints
- Partial Chains: If no issuer is found, the chain is marked incomplete but processing continues
- Self-Signed Detection: Chain building stops when a self-signed certificate is encountered
Certificate List Configuration
This section describes how certificate lists and stores are configured and populated in the OPC UA .NET Standard Stack.
Configuration Sources
Certificate lists are populated from two primary sources:
- Configuration File (XML): The
ApplicationConfigurationfile defines certificate store locations and optional explicit certificate lists - Runtime API: Applications can programmatically add certificates to trust lists using the
SecurityConfigurationAPI
Configuration File Structure
The application configuration file (*.Config.xml) defines certificate
stores in its SecurityConfiguration section:
<SecurityConfiguration>
<!-- Application's own certificates -->
<ApplicationCertificates>
<CertificateIdentifier>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC Foundation/pki/own</StorePath>
<SubjectName>CN=MyApplication, O=MyOrganization</SubjectName>
<CertificateTypeString>RsaSha256</CertificateTypeString>
</CertificateIdentifier>
</ApplicationCertificates>
<!-- Issuer certificates (Certificate Authorities) -->
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC Foundation/pki/issuer</StorePath>
<!-- Optional: Explicit certificate list -->
<TrustedCertificates>
<CertificateIdentifier>
<Thumbprint>1234567890ABCDEF...</Thumbprint>
</CertificateIdentifier>
</TrustedCertificates>
</TrustedIssuerCertificates>
<!-- Trusted peer certificates (applications) -->
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC Foundation/pki/trusted</StorePath>
<!-- Optional: Explicit certificate list -->
<TrustedCertificates>
<CertificateIdentifier>
<Thumbprint>FEDCBA0987654321...</Thumbprint>
</CertificateIdentifier>
</TrustedCertificates>
</TrustedPeerCertificates>
<!-- Rejected certificates -->
<RejectedCertificateStore>
<StoreType>Directory</StoreType>
<StorePath>%LocalApplicationData%/OPC Foundation/pki/rejected</StorePath>
</RejectedCertificateStore>
</SecurityConfiguration>
Certificate Store Types
Three store types are supported out of the box. Custom store types can be registered using the ICertificateStoreProvider interface (see CertificateManager.md).
-
Directory: File system-based certificate store
- Certificates stored as
.deror.crtfiles incerts/subdirectory - Private keys stored as
.pfxor.pemfiles inprivate/subdirectory - CRLs stored in
crl/subdirectory - Example:
%LocalApplicationData%/OPC Foundation/pki/trusted
- Certificates stored as
-
X509Store: Windows certificate store (on Windows platforms)
- Uses Windows Certificate Store API
- Example:
CurrentUser\MyorLocalMachine\Root - Supports CRL operations on Windows only
-
InMemory: In-memory certificate store for testing
- Prefix:
InMemory: - No persistence — certificates are lost when the store is disposed
- Prefix:
On Windows, private-key directories grant inheritable access to the application
account, SYSTEM, and Administrators, preserving access to existing keys during
upgrades. The .NET 8+ Unix implementation uses owner-only directory/file modes
(0700/0600). A permission-update failure aborts the write unless the path
already existed and its current permissions are verified to restrict key access
to the trusted Windows identities or the Unix owner. That exception is logged;
unverifiable or overly broad permissions are never silently accepted.
Certificate List Population
CertificateManager sources its well-known trust lists from the
SecurityConfiguration in ApplicationConfiguration. Hosts may also register
named trust lists for other purposes.
1. Initialization via ApplicationConfiguration
// Direct-construction alternative for an already loaded configuration.
using CertificateManager manager = CertificateManagerFactory.Create(
config.SecurityConfiguration,
telemetry);
await manager
.LoadApplicationCertificatesAsync(config.SecurityConfiguration, config.ApplicationUri)
.ConfigureAwait(false);
For a hosted application, use config.CertificateManager after application
certificate initialization instead of creating another manager. See
initialization ordering.
2. Internal Update Process
The factory registers the configured trusted and issuer stores. Validation uses
the selected trust list's certificate lists and stores; application code does
not populate private validator fields or call a legacy UpdateAsync method.
For changing a live trust list, use
trust-list transactions.
The manager coordinates change notifications and validation-cache invalidation.
3. Certificate Search Behavior
During validation, certificates are searched in the following order:
For Trusted Certificates:
- Search the configured explicit trusted-certificate list, if populated.
- Search the trusted store (file system or X509Store).
- Check the application's own certificates.
For Issuer Certificates:
- Search the configured explicit issuer-certificate list, if populated.
- Search the issuer store (file system or X509Store).
Runtime Certificate Management
Applications can dynamically add certificates to trust lists:
// Add a trusted peer certificate programmatically
byte[] certificateData = File.ReadAllBytes("peer-cert.der");
config.SecurityConfiguration.AddTrustedPeer(certificateData);
// Or use the CertificateManager transactional update API:
await using ITrustListTransaction tx = await manager
.BeginUpdateAsync(TrustListIdentifier.Peers)
.ConfigureAwait(false);
using Certificate trusted = Certificate.FromRawData(certificateData);
await tx.AddTrustedCertificateAsync(trusted).ConfigureAwait(false);
await tx.CommitAsync().ConfigureAwait(false);
Certificate Store Management
Certificates in file system stores are managed as follows:
-
Adding Certificates: Copy certificate files to the
certs/subdirectory of the store path- Format:
[Thumbprint].deror[Thumbprint].crt - Example:
%LocalApplicationData%/OPC Foundation/pki/trusted/certs/1234567890ABCDEF.der
- Format:
-
Adding CRLs: Copy CRL files to the
crl/subdirectory- Format:
[IssuerThumbprint].crl - Example:
%LocalApplicationData%/OPC Foundation/pki/issuer/crl/FEDCBA0987654321.crl
- Format:
-
Rejected Certificates: Automatically added by the validator when validation fails
- Stored in the rejected certificate store
- Can be manually moved to trusted store to establish trust
Dual-Mode Operation
The validator supports both explicit lists and certificate stores:
- Explicit List Only: Specify certificates in
<TrustedCertificates>without a store path - Store Only: Specify store path without explicit certificates (most common)
- Combined Mode: Use both explicit list and store for maximum flexibility
- Explicit list is searched first for performance
- Store is searched if not found in list
Configuration Best Practices
-
Store Path: Use environment variables for platform independence:
%LocalApplicationData%- Per-user application data%CommonApplicationData%- Machine-wide application data- Relative paths - Relative to application directory
-
Explicit Lists: Use for:
- Small, fixed set of trusted certificates
- Performance optimization (faster than store enumeration)
- Pre-deployment certificate distribution
-
Certificate Stores: Use for:
- Dynamic trust management
- Administrator-managed certificate stores
- Integration with OS certificate infrastructure
-
Separation: Keep different certificate types in separate stores:
- Application certificates:
pki/own - Trusted peers:
pki/trusted - Trusted CAs:
pki/issuer - Rejected:
pki/rejected
- Application certificates:
Configuration Settings
The certificate validation behavior is controlled by several configuration settings in the SecurityConfiguration class:
AutoAcceptUntrustedCertificates
- Type:
bool - Default:
false - Description: When
true, automatically accepts certificates that have theBadCertificateUntrustedstatus. This is useful for development environments but should not be used in production. - Example:
configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates = true;
RejectSHA1SignedCertificates
- Type:
bool - Default:
true(when default hash size >= 256) - Description: When
true, rejects certificates signed with SHA-1 algorithms as they are considered cryptographically weak. - Example:
configuration.SecurityConfiguration.RejectSHA1SignedCertificates = true;
RejectUnknownRevocationStatus
- Type:
bool - Default:
false - Description: When
true, rejects certificates when the revocation status cannot be determined (e.g., CRL is not available). - Example:
configuration.SecurityConfiguration.RejectUnknownRevocationStatus = true;
MinimumCertificateKeySize
- Type:
ushort - Default:
2048(CertificateFactory.DefaultKeySize) - Description: The minimum RSA key size in bits that will be accepted. Common values are 2048, 3072, or 4096.
- Example:
configuration.SecurityConfiguration.MinimumCertificateKeySize = 2048;
UseValidatedCertificates
- Type:
bool - Default:
false - Description: When
true, skips validation for certificates that have already been successfully validated in the current session. This improves performance by caching validation results. - Example:
configuration.SecurityConfiguration.UseValidatedCertificates = true;
MaxRejectedCertificates
- Type:
int - Default:
5 - Description: Limits the number of rejected certificates kept in history. A value of 0 means all rejected certificates are kept. A negative value means no history is kept.
- Example:
configuration.SecurityConfiguration.MaxRejectedCertificates = 10;
Suppressible Validation Errors
The following validation errors can be suppressed by handling the CertificateValidation event and setting e.Accept = true:
- BadCertificateUntrusted: The certificate is not trusted (not in the trusted store or chain).
- BadCertificateHostNameInvalid: The domain name in the endpoint URL does not match any domain in the certificate.
- BadCertificateIssuerRevocationUnknown: The revocation status of the issuer cannot be determined.
- BadCertificateIssuerTimeInvalid: The issuer certificate has expired or is not yet valid.
- BadCertificateIssuerUseNotAllowed: An issuer/CA certificate in the chain is not valid for use as a CA — for example it does not assert the
keyCertSignandcRLSignKeyUsage bits required for a CA (see CA (issuer) KeyUsage validation). - BadCertificateRevocationUnknown: The revocation status of the certificate cannot be determined.
- BadCertificateTimeInvalid: The certificate has expired or is not yet valid.
- BadCertificatePolicyCheckFailed: The certificate does not meet policy requirements (e.g., key size, signature algorithm).
- BadCertificateUseNotAllowed: The certificate is not valid for the intended use (missing key usage flags).
All other validation errors are non-suppressible and will always cause the validation to fail. This includes BadCertificateChainIncomplete (an issuer certificate is missing from the trusted and issuer stores): OPC 10000-4 Table 100 does not allow chain-building errors to be suppressed, so install the issuing CA instead of accepting the error.
CA (issuer) KeyUsage validation
OPC UA requires every CA (issuer) certificate in a chain to carry a KeyUsage
extension with both keyCertSign and cRLSign set (OPC 10000-6 §6.2.4,
Table 52 — Issuer Certificate). During chain validation, the stack checks
each issuer certificate. If a CA certificate lacks either bit or has no
KeyUsage extension, the stack reports the suppressible
BadCertificateIssuerUseNotAllowed error (OPC 10000-4 §6.1.3, Table 100,
"Certificate Usage").
Strict third-party OPC UA stacks also reject such CA certificates, typically
with BadCertificateInvalid. This stack's CertificateBuilder includes the
required bits in every CA certificate it creates. To interoperate with a
legacy CA that omits them, suppress the error as described above.
Inspecting Certificate Validation Results
The new ICertificateValidatorEx (composed in ICertificateManager)
returns a structured CertificateValidationResult describing the
outcome of each validation. Callers can examine the result to decide
whether to accept errors:
CertificateValidationResult result = await manager
.ValidateAsync(certificate, TrustListIdentifier.Peers)
.ConfigureAwait(false);
if (!result.IsValid)
{
Console.WriteLine($"Validation failed: {result.StatusCode}");
foreach (ServiceResult error in result.Errors)
{
Console.WriteLine($" • {error}");
}
// Accept specific suppressible errors (e.g. untrusted certificates
// in development) by inspecting the StatusCode.
bool autoAccept = false;
if (result.IsSuppressible &&
result.StatusCode == StatusCodes.BadCertificateUntrusted &&
autoAccept)
{
// Application-specific accept logic.
}
else
{
throw new ServiceResultException(result.StatusCode);
}
}
Per-call validation behavior (e.g. auto-accepting untrusted
certificates, or relaxing revocation checks) can also be overridden
via CertificateValidationOptions:
var options = new CertificateValidationOptions
{
AutoAcceptUntrustedCertificates = true,
RejectUnknownRevocationStatus = false
};
CertificateValidationResult result = await manager
.ValidateAsync(certificate, TrustListIdentifier.Peers, options)
.ConfigureAwait(false);
Subscribe to manager.CertificateChanges (an
IObservable<CertificateChangeEvent>) for lifecycle notifications such
as ApplicationCertificateUpdated, TrustListUpdated,
CrlUpdated, CertificateRejected, and CertificateExpiring. See
CertificateManager.md for the full reference.
The legacy CertificateValidator.CertificateValidation event and adapter are
removed. See certificate migration
for the old-to-new mapping.
Configuring a Custom Certificate Validator
Use ApplicationConfiguration.CertificateManager for the application's normal
validation path. It combines trust-list management, certificate validation, and
certificate lifecycle notifications. Keep those checks in place when adding
application-specific requirements; an always-successful validator bypasses trust,
validity, and revocation checks.
The current validation contract is ICertificateValidatorEx. Its
CertificateValidationOptions.AcceptError callback can accept individual
suppressible errors; accepting untrusted certificates is appropriate only for
controlled development environments. For the interface, initialization order,
callback precedence, and examples, see
CertificateManager validation.
There is no CertificateValidatorAdapter or legacy
ApplicationConfiguration.CertificateValidator property in the current API.
Best Practices
-
Production vs Development: Never use
AutoAcceptUntrustedCertificates = truein production environments. -
Certificate Store Management: Regularly review rejected certificates in the rejected store and move trusted certificates to the appropriate trust store.
-
Revocation Checking: Enable
RejectUnknownRevocationStatusfor high-security environments where CRL checking is critical. -
Minimum Key Size: Use at least 2048 bits for RSA keys. Consider 3072 or 4096 bits for long-term security.
-
SHA-1 Deprecation: Keep
RejectSHA1SignedCertificates = trueto ensure only certificates with strong signature algorithms are accepted. -
Validation Callback: Always log certificate validation events for security auditing purposes.
-
Custom Validators: When implementing a custom validator, ensure it complies with OPC UA security requirements and thoroughly test edge cases.