Files
UA-.NETStandard/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
Marc Schier 2966f693b2 2.0 Migration Analyzer and codefixer with Agent update (#3820)
Introduces `OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer` — a
single NuGet package that ships **three Roslyn components + a runtime
shim** to make the 1.5.378 → 2.0 upgrade as close to "install package,
run `dotnet format`, remove package" as possible:

| DLL | Where | Purpose |
|-----|-------|---------|
| `Opc.Ua.MigrationAnalyzer.dll` | `analyzers/dotnet/cs/` | 19 Roslyn
analyzers (`UA0001`–`UA0022`) covering every breaking-change pattern in
[`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md).
Built against the stable Roslyn 4.14 analyzer API — loads cleanly in
csc.exe (no `Microsoft.CodeAnalysis.Workspaces` reference). |
| `Opc.Ua.MigrationAnalyzer.CodeFixer.dll` | `analyzers/dotnet/cs/` | 14
companion `CodeFixProvider`s; loaded only by Workspaces-aware hosts
(Visual Studio, `dotnet format analyzers`). |
| `Opc.Ua.MigrationAnalyzer.Generator.dll` | `analyzers/dotnet/cs/` |
Roslyn `IIncrementalGenerator` that emits per-consumer `internal sealed
[Obsolete] class <Name>Collection : List<TElement>` shims for every
`<Type>Collection` wrapper the consumer references — including
**arbitrary model-compiled `<UserType>Collection` patterns**, not just
the 30 well-known built-in ones. |
| `Opc.Ua.MigrationAnalyzer.Core.dll` | `lib/<tfm>/` for `net472, net48,
netstandard2.1, net8.0, net9.0, net10.0` | 1.5.378 → 2.0 runtime
compatibility shim. Re-exposes the obsolete extension surface so
1.5.378-style call sites keep compiling against 2.0 with warnings
instead of errors. |

### Migration workflow the package enables

1. **Bump every `OPCFoundation.NetStandard.Opc.Ua.*` PackageReference to
`2.0.*-*`.**
2. **Add the migration package** as a build-only dependency:
   ```xml
<PackageReference
Include="OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer"
                     Version="2.0.*-*"
                     PrivateAssets="all" />
   ```
3. **Run `dotnet build`.** The Core shim DLL re-supplies obsolete
extension members; the source generator covers `<Type>Collection`
references; what remains are `[Obsolete]` warnings + `UA00xx` analyzer
diagnostics + (occasionally) `MIG01` for unresolvable model-compiled
element types.
4. **Apply auto-fixes** in the IDE or from the command line:
   ```bash
   dotnet format analyzers <solution>.sln \
       --diagnostics UA0002 UA0003 UA0004 UA0005 UA0006 UA0007 UA0008 \
                     UA0009 UA0010 UA0012 UA0014 UA0019 UA0020 UA0022 \
       --severity warn
   ```
5. **Walk the residual** `UA0001` / `UA0011` / `UA0015` / `UA0018` /
`UA0021` diagnostics manually — they cover patterns that require human
judgement.
6. **Remove the migration package.** You are now on clean 2.0 with no
shim dependency.

### Source generator (`Opc.Ua.MigrationAnalyzer.Generator.dll`)

Per-consumer Roslyn `IIncrementalGenerator` that emits `.g.cs` files for
every `<Name>Collection` reference in the consumer's compilation that
fails to bind:

- **Catalog override (30 entries)** — pins element types that renamed
across the 1.5.378 → 2.0 boundary: `DateTime`→`DateTimeUtc`,
`Guid`→`Uuid`, `byte[]`→`ByteString`, `XmlElement`→`Opc.Ua.XmlElement`,
plus the primitives and built-in opc types whose element name didn't
rename.
- **Semantic lookup** — for model-compiled `<UserType>Collection`
patterns the model compiler historically emitted for any user-defined
complex type. Strips the `Collection` suffix, runs
`Compilation.GetSymbolsWithName` against the unique element short name,
emits the shim with the fully-qualified type reference.
- **`MIG01` diagnostic** — fires when the element type can't be uniquely
resolved (zero or multiple candidates). User adds the missing `using` or
migrates the site manually.

Each generated file emits a class shaped exactly like the 1.5.378
wrapper:

```csharp
[global::System.Obsolete("'Int32Collection' was removed in 2.0. Use 'List<int>' or 'ArrayOf<int>' instead. (UA0002)")]
internal sealed class Int32Collection : global::System.Collections.Generic.List<int>
{
    public Int32Collection() { }
    public Int32Collection(int capacity) : base(capacity) { }
    public Int32Collection(global::System.Collections.Generic.IEnumerable<int> collection) : base(collection) { }
    public static implicit operator global::Opc.Ua.ArrayOf<int>(Int32Collection? value)
        => value is null ? default : value.ToArrayOf();
}
```

**Design choice — `internal` (not `public`):** the generated shim never
leaks through the consumer's public API surface. Consumers with *public*
methods or properties returning a `<Type>Collection` will hit `CS0050:
Inconsistent accessibility` — the intended signal that the public
surface must migrate to `List<T>` / `ArrayOf<T>` first; internal call
sites keep compiling under the shim so iteration is incremental.

### 19-rule analyzer reference

| Rule | Replaces | Default | Auto-fix |
|------|----------|---------|----------|
| `UA0001` | `Utils.Trace` / `Utils.LogX` | Info | No (telemetry
plumbing requires manual review) |
| `UA0002` | Removed `<Type>Collection` wrappers | Warning | Yes (→
`List<T>` / `ArrayOf<T>`); the source generator keeps the call site
compiling in the meantime |
| `UA0003` | `x == null` on now-struct built-ins | Warning | Yes (→
`x.IsNull`) |
| `UA0004` | `?.` on now-struct built-ins | Warning | Yes (drop the `?`)
|
| `UA0005` | `byte[]` where `ByteString` is expected | Warning | Yes
(append `.ToByteString()`) |
| `UA0006` | `new Variant(object\|DateTime\|Guid\|byte[])` | Warning |
Yes (→ `Variant.From(...)`) |
| `UA0007` | `new NodeId(string)` / `new ExpandedNodeId(string)` |
Warning | Yes (→ `Parse`) |
| `UA0008` | `Session.Call(..., params object[])` | Warning | Yes (wrap
with `Variant.From`) |
| `UA0009` | `[DataContract]` / `[DataMember]` on config extensions |
Warning | Yes (→ `[DataType]` / `[DataTypeField]`) |
| `UA0010` | `using` / `Dispose` on `CertificateIdentifier` /
`UserIdentity` / `IUserIdentityTokenHandler` | Warning | Yes (drop the
disposable) |
| `UA0011` | Sync
`IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}` | Info | No
(async promotion required) |
| `UA0012` | `CertificateFactory.*` static helpers | Warning | Yes (→
instance methods) |
| `UA0014` | `DataValue.IsGood(dv)` static helper | Warning | Yes (→
`dv.IsGood`) |
| `UA0015` | Sync / APM members on GDS / LDS clients | Info | No (async
promotion required) |
| `UA0018` | `CertificateIdentifier.Certificate` getter | Info | No (→
`LoadCertificate2Async`) |
| `UA0019` | `new DataValue(StatusCode[, ts])` | Warning | Yes (→
object-initializer form) |
| `UA0020` | `EncodeableFactory.GlobalFactory` / `Create()` | Warning |
Yes (→ `ServiceMessageContext.Factory` / `Fork()`) |
| `UA0021` | `CertificateValidator` / `CertificateValidationEventArgs`
(structural rename) | Info | No (see migration guide §Certificate
validation) |
| `UA0022` | `ApplicationConfiguration.CertificateValidator` /
`ServerBase.CertificateValidator` property rename | Warning | Yes (→
`.CertificateManager`) |

Plus one generator-only diagnostic:

| Rule | Source | Default | Triggers |
|------|--------|---------|----------|
| `MIG01` | Generator | Warning | Generator can't uniquely resolve the
element type for a `<Foo>Collection` reference (zero or multiple
candidates). The user adds a `using` for the namespace defining `Foo`,
or migrates the call site manually. |

### Runtime shim coverage (`Opc.Ua.MigrationAnalyzer.Core.dll`)

C# 14 `extension` members so 1.5.378 call sites continue to compile:

- **Moved obsolete extensions** the 2.0 libraries no longer carry
inline: `NodeId` / `Variant` / `DataValue` null-check helpers, `Session`
sync helpers, `Subscription` sync helpers, `ApplicationInstance`
helpers, `ServerBase.Start` / `Stop`, `TransportChannel` APM,
`ChannelBase` static factory methods, etc.
- **New shims for genuinely-removed members**:
`EncodeableFactory.GlobalFactory`, `CertificateIdentifier.Certificate`
(throws), sync wrappers for
`IUserIdentityTokenHandler.{Encrypt,Decrypt,Sign,Verify}`, sync + APM
wrappers for the GDS / LDS client APIs, `GlobalDiscoverySampleServer`
1.5.378-shape ctor (in-tree `Libraries/Opc.Ua.Gds.Server.Common/`).

> ⚠ The sync shims wrap their `*Async` counterparts via
`Task.Run(…).GetAwaiter().GetResult()`. Intended as a **migration aid
only** — port to `await` before shipping.

### Repo layout

```
Tools/Opc.Ua.MigrationAnalyzer/                  # analyzer DLL
  Analyzers/UA00xxAnalyzer.cs                    # 19 DiagnosticAnalyzer impls
  Diagnostics/{DiagnosticIds,Descriptors,WellKnownProperties}.cs
  Helpers/{SymbolExtensions,UaSymbols}.cs
  Opc.Ua.MigrationAnalyzer.nuspec                # ships all 3 analyzer DLLs + 6-TFM Core
  OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props

Tools/Opc.Ua.MigrationAnalyzer.CodeFixer/        # code-fix DLL
  CodeFixes/UA00xxCodeFix.cs                     # 14 CodeFixProvider impls

Tools/Opc.Ua.MigrationAnalyzer.Generator/        # source generator DLL  (NEW)
  MigrationGenerator.cs                          # IIncrementalGenerator
  CollectionShimCatalog.cs                       # 30-entry element-type overrides
  Diagnostics/GeneratorDiagnostics.cs            # MIG01

Tools/Opc.Ua.MigrationAnalyzer.Core/             # runtime shim (multi-TFM)
  Client/, Configuration/, Core/, Gds.Client.Common/, Types/, Marker/

Tools/Roslyn.slnx                                # source-gen + migration projects

Tests/Opc.Ua.MigrationAnalyzer.Tests/            # 142 tests
  Analyzers/UA00xxTests.cs                       # 107 analyzer + code-fix tests
  Generators/MigrationGeneratorTests.cs          # 35 generator tests (NEW)
Tests/Opc.Ua.MigrationAnalyzer.Core.Tests/       # 13 runtime-shim tests
```

### Documentation

-
[`Docs/MigrationGuide.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/Docs/MigrationGuide.md)
— §3 Collection Type Migration gains a tip pointing at the generator so
CS0246 is replaced with `[Obsolete]` warnings + UA0002.
-
[`.github/agents/opcua-v20-migration.agent.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/.github/agents/opcua-v20-migration.agent.md)
— Strategy section mentions the three Roslyn components + runtime shim
explicitly, plus the MIG01 diagnostic and the "internal-by-design"
CS0050 hint.

### Central package pinning

`Directory.Packages.props` pins `Microsoft.CodeAnalysis.*` (Common,
CSharp, CSharp.Workspaces, Workspaces.Common) to **4.14.0** — the stable
analyzer API surface. The earlier 5.3 pin was the *csc-internal*
version, which is not safe for analyzer hosts.

### Dogfood story

The full 42-sample dogfood against
`OPCFoundation/UA-.NETStandard-Samples` surfaced a **critical
analyzer-loader bug** in an earlier iteration: the analyzer DLL
initially co-shipped its code-fix providers in one assembly, which
transitively referenced `Microsoft.CodeAnalysis.Workspaces.dll`.
csc.exe's analyzer host ships only `Microsoft.CodeAnalysis.dll` +
`CSharp.dll` in its bincore and **silently swallows** load failures —
net result: zero diagnostics across all samples. Fix landed in commit
`861fa6ee1`:

1. **Split** the assembly: analyzer in `Opc.Ua.MigrationAnalyzer.dll`
(Workspaces-free, csc-safe), code-fixes in
`Opc.Ua.MigrationAnalyzer.CodeFixer.dll` (Workspaces-aware, IDE-only).
2. **Downgrade** to Roslyn 4.14 — the stable analyzer API.
3. `/p:ReportAnalyzer=true` confirms all 19 analyzers initialise and
execute on real consumer code.

This PR's new generator was smoke-tested against a minimal consumer that
exercises all 30 catalog entries + a model-compiled element type + the
unresolvable case:
- 30 catalog patterns + model-compiled `MyThingCollection` → all emit
shims, fire CS0618 with `(UA0002)` marker
- Element-type renames verified: `DateTimeCollection` → `DateTimeUtc`,
`ByteStringCollection` → `ByteString`, `GuidCollection` → `Uuid`, etc.
- `NeverHeardOfItCollection` → `MIG01` fires with help link to the
migration guide

Known compatibility gaps (documented in the migration agent):
- Legacy WinForms `.Net4` sample projects use pre-SDK MSBuild XML and
**ignore** `Directory.Build.targets` `<PackageReference>` injection —
consumers in this format must add the migration package to their csproj
inline.

### Packaging design notes

- Hand-authored nuspec ships 3 DLLs in `analyzers/dotnet/cs/` + 6 TFMs
of Core (shim) DLLs under `lib/`. All 6 OPC UA runtime package
dependencies are declared per-TFM-group.
- `build/OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer.props` is
auto-imported by NuGet (file name must match package id exactly) and
injects all three analyzer DLLs as `<Analyzer>` items.
- `DiagnosticIds.cs` / `DiagnosticDescriptors.cs` /
`WellKnownProperties.cs` / `Helpers/*.cs` are **shared via `<Compile
Link=…>`** between the analyzer and code-fixer projects to break what
would otherwise be a NuGet restore cycle. The generator has no compile
dependency on the analyzer; the 30-entry override table is duplicated in
`CollectionShimCatalog.cs` (the analyzer table also drives diagnostic
messages, so the duplication is small and intentional).
- `Opc.Ua.MigrationAnalyzer.Core` is multi-TFM (`net472, net48,
netstandard2.1, net8.0, net9.0, net10.0`) and **non-packable on its
own**; the parent migration package's nuspec consumes each TFM's DLL by
path.
2026-06-05 12:01:53 +02:00

1270 lines
51 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua.Security.Certificates;
namespace Opc.Ua.Configuration
{
/// <inheritdoc/>
public class ApplicationInstance : IApplicationInstance
{
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
if (Server != null)
{
try
{
await StopAsync().ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(e, "Error stopping server during dispose.");
}
Server.Dispose();
Server = null;
}
CertificateManager? localManager = CertificateManager;
localManager?.Dispose();
if (ApplicationConfiguration?.CertificateManager is CertificateManager configManager && !ReferenceEquals(configManager, localManager))
{
configManager.Dispose();
}
GC.SuppressFinalize(this);
}
/// <summary>
/// Obsolete constructor
/// </summary>
[Obsolete("Use ApplicationInstance(ITelemetryContext) instead.")]
public ApplicationInstance()
: this((ITelemetryContext?)null)
{
}
/// <summary>
/// Obsolete constructor
/// </summary>
[Obsolete("Use ApplicationInstance(ApplicationConfiguration, ITelemetryContext) instead.")]
public ApplicationInstance(ApplicationConfiguration applicationConfiguration)
: this(applicationConfiguration, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInstance"/> class.
/// </summary>
public ApplicationInstance(ITelemetryContext? telemetry)
{
m_telemetry = telemetry;
m_logger = telemetry?.CreateLogger<ApplicationInstance>() ?? NullLogger<ApplicationInstance>.Instance;
DisableCertificateAutoCreation = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInstance"/> class.
/// </summary>
/// <param name="applicationConfiguration">The application configuration.</param>
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
public ApplicationInstance(
ApplicationConfiguration applicationConfiguration,
ITelemetryContext? telemetry)
: this(telemetry)
{
ApplicationConfiguration = applicationConfiguration;
}
/// <inheritdoc/>
public string? ApplicationName { get; set; }
/// <inheritdoc/>
public ApplicationType ApplicationType { get; set; }
/// <inheritdoc/>
public string? ConfigSectionName { get; set; }
/// <inheritdoc/>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type? ConfigurationType { get; set; }
/// <inheritdoc/>
public IServerBase? Server { get; private set; }
/// <inheritdoc/>
public ApplicationConfiguration? ApplicationConfiguration { get; set; }
/// <summary>
/// Get or set the message dialog.
/// </summary>
public static IApplicationMessageDlg? MessageDlg { get; set; }
/// <inheritdoc/>
public ICertificatePasswordProvider? CertificatePasswordProvider { get; set; }
/// <inheritdoc/>
public bool DisableCertificateAutoCreation { get; set; }
/// <summary>
/// Gets the certificate manager for this application instance.
/// </summary>
public CertificateManager? CertificateManager { get; private set; }
/// <inheritdoc/>
public async Task StartAsync(IServerBase server, CancellationToken ct = default)
{
Server = server;
ApplicationConfiguration configuration = ApplicationConfiguration
?? await LoadApplicationConfigurationAsync(false, ct).ConfigureAwait(false);
await server.StartAsync(configuration, ct).ConfigureAwait(false);
}
/// <inheritdoc/>
public ValueTask StopAsync(CancellationToken ct = default)
{
return Server?.StopAsync(ct) ?? default;
}
/// <summary>
/// Stops the UA server.
/// </summary>
[Obsolete("Use StopAsync")]
public void Stop()
{
Server?.StopAsync().AsTask().GetAwaiter().GetResult();
}
/// <inheritdoc/>
public async Task<ApplicationConfiguration> LoadApplicationConfigurationAsync(
Stream stream,
bool silent,
CancellationToken ct = default)
{
ApplicationConfiguration? configuration = null;
try
{
configuration = await LoadAppConfigAsync(
silent,
stream,
ApplicationType,
ConfigurationType,
true,
CertificatePasswordProvider,
ct)
.ConfigureAwait(false);
}
catch (Exception) when (silent)
{
}
if (configuration == null)
{
throw ServiceResultException.ConfigurationError("Could not load configuration.");
}
ApplicationConfiguration = FixupAppConfig(configuration);
return configuration;
}
/// <inheritdoc/>
public async ValueTask<ApplicationConfiguration> LoadApplicationConfigurationAsync(
string filePath,
bool silent,
CancellationToken ct = default)
{
ApplicationConfiguration? configuration = null;
try
{
configuration = await LoadAppConfigAsync(
silent,
filePath,
ApplicationType,
ConfigurationType,
true,
CertificatePasswordProvider,
ct)
.ConfigureAwait(false);
}
catch (Exception) when (silent)
{
}
if (configuration == null)
{
throw ServiceResultException.ConfigurationError("Could not load configuration file.");
}
ApplicationConfiguration = FixupAppConfig(configuration);
return configuration;
}
/// <inheritdoc/>
public ValueTask<ApplicationConfiguration> LoadApplicationConfigurationAsync(
bool silent,
CancellationToken ct = default)
{
// GetFilePathFromAppConfig tolerates a null section name (string.Concat handles null operands).
string filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName!, m_logger);
return LoadApplicationConfigurationAsync(filePath, silent, ct);
}
/// <summary>
/// Helper to replace localhost with the hostname
/// in the application uri and base addresses of the
/// configuration.
/// </summary>
public static ApplicationConfiguration FixupAppConfig(
ApplicationConfiguration configuration)
{
configuration.ApplicationUri = Utils.ReplaceLocalhost(configuration.ApplicationUri);
if (configuration.ServerConfiguration != null)
{
string[] baseAddresses = new string[configuration.ServerConfiguration.BaseAddresses.Count];
for (int i = 0; i < configuration.ServerConfiguration.BaseAddresses.Count; i++)
{
// Base address strings in the collection are non-null; ReplaceLocalhost only returns
// null when its input is null, so the result here is non-null.
baseAddresses[i] =
Utils.ReplaceLocalhost(configuration.ServerConfiguration.BaseAddresses[i])!;
}
configuration.ServerConfiguration.BaseAddresses = baseAddresses;
}
return configuration;
}
/// <inheritdoc/>
public IApplicationConfigurationBuilderTypes Build(string applicationUri, string productUri)
{
// ApplicationConfiguration's ctor requires a non-null ITelemetryContext while this class
// tolerates a null telemetry context; passing null falls through to default telemetry.
ApplicationConfiguration = new ApplicationConfiguration(m_telemetry!)
{
ApplicationName = ApplicationName,
ApplicationType = ApplicationType,
ApplicationUri = applicationUri,
ProductUri = productUri,
TraceConfiguration = new TraceConfiguration { TraceMasks = Utils.TraceMasks.None },
TransportQuotas = new TransportQuotas()
};
// Trace off — legacy Utils pipeline; kept for 1.5.378 -> 2.0 migration.
#pragma warning disable CS0618 // Type or member is obsolete
ApplicationConfiguration.TraceConfiguration.ApplySettings();
#pragma warning restore CS0618 // Type or member is obsolete
return new ApplicationConfigurationBuilder(this);
}
/// <inheritdoc/>
public async ValueTask DeleteApplicationInstanceCertificateAsync(
string[]? profileIds = null,
CancellationToken ct = default)
{
// TODO: delete only selected profiles
if (ApplicationConfiguration == null)
{
throw new ArgumentException("Missing configuration.");
}
ArrayOf<CertificateIdentifier> applicationCerts = ApplicationConfiguration
.SecurityConfiguration.ApplicationCertificates;
for (int ii = 0; ii < applicationCerts.Count; ii++)
{
CertificateIdentifier id = applicationCerts[ii];
await DeleteApplicationInstanceCertificateAsync(ApplicationConfiguration, id, ct)
.ConfigureAwait(false);
}
}
/// <inheritdoc/>
public async ValueTask<bool> CheckApplicationInstanceCertificatesAsync(
bool silent,
ushort? lifeTimeInMonths = null,
CancellationToken ct = default)
{
lifeTimeInMonths ??= CertificateFactory.DefaultLifeTime;
m_logger.LogInformation("Checking application instance certificate.");
ApplicationConfiguration configuration = ApplicationConfiguration
?? await LoadApplicationConfigurationAsync(silent, ct).ConfigureAwait(false);
// find the existing certificates.
SecurityConfiguration securityConfiguration = configuration.SecurityConfiguration;
if (securityConfiguration.ApplicationCertificates.Count == 0)
{
throw ServiceResultException.ConfigurationError("Need at least one Application Certificate.");
}
// Initialize CertificateManager early so CheckApplicationInstanceCertificateAsync
// can use the new ICertificateValidatorEx pipeline (with CertificateValidationOptions.AcceptError)
// for per-certificate validation below.
CertificateManager ??= CertificateManagerFactory.Create(
securityConfiguration,
m_telemetry!);
// Make the manager visible via the configuration so consumers
// that only see ApplicationConfiguration (e.g. Session) can
// route validation through the new pipeline.
ApplicationConfiguration!.CertificateManager = CertificateManager;
// Note: The FindAsync method searches certificates in this order: thumbprint, subjectName, then applicationUri.
// When SubjectName or Thumbprint is specified, certificates may be loaded even if their ApplicationUri
// doesn't match ApplicationConfiguration.ApplicationUri, however each certificate is validated individually
// in CheckApplicationInstanceCertificateAsync (called via CheckOrCreateCertificateAsync) to ensure it contains
// the configuration's ApplicationUri.
bool result = true;
for (int ii = 0; ii < securityConfiguration.ApplicationCertificates.Count; ii++)
{
CertificateIdentifier certId = securityConfiguration.ApplicationCertificates[ii];
ushort minimumKeySize = certId.GetMinKeySize(securityConfiguration);
bool nextResult = await CheckOrCreateCertificateAsync(
configuration,
certId,
silent,
minimumKeySize,
lifeTimeInMonths.Value,
ct)
.ConfigureAwait(false);
result = result && nextResult;
}
return result;
}
/// <summary>
/// Checks, validates, and optionally creates an application certificate.
/// Loads the certificate, validates it against configured requirements (ApplicationUri, key size, domains),
/// and creates a new certificate if none exists and auto-creation is enabled.
/// Note: FindAsync searches certificates in order: thumbprint, subjectName, applicationUri.
/// The applicationUri parameter is only used if thumbprint and subjectName don't find a match.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private async Task<bool> CheckOrCreateCertificateAsync(
ApplicationConfiguration configuration,
CertificateIdentifier id,
bool silent,
ushort minimumKeySize,
ushort lifeTimeInMonths,
CancellationToken ct = default)
{
if (id == null)
{
throw ServiceResultException.ConfigurationError(
"Configuration file does not specify a certificate.");
}
// load the certificate (with private key if available).
ICertificatePasswordProvider? passwordProvider = configuration
.SecurityConfiguration
.CertificatePasswordProvider;
Certificate? certificate = await CertificateIdentifierResolver
.LoadPrivateKeyAsync(
id,
passwordProvider,
configuration.ApplicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
try
{
// check that it is ok.
if (certificate != null)
{
m_logger.LogInformation("Check certificate: {Certificate}", certificate);
bool certificateValid = await CheckApplicationInstanceCertificateAsync(
configuration,
id,
certificate,
silent,
minimumKeySize,
ct)
.ConfigureAwait(false);
if (!certificateValid)
{
throw ServiceResultException.ConfigurationError(
"The certificate with subject {0} in the configuration is invalid.\n" +
" Please update or delete the certificate from this location: {1}",
id.SubjectName!,
Utils.ReplaceSpecialFolderNames(id.StorePath)!);
}
}
else
{
// check for missing private key.
certificate = await CertificateIdentifierResolver
.ResolveAsync(
id,
registry: null,
needPrivateKey: false,
configuration.ApplicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
if (certificate != null)
{
throw ServiceResultException.ConfigurationError(
"Cannot access private key for certificate with thumbprint={0}",
certificate.Thumbprint);
}
// check for missing thumbprint.
if (!string.IsNullOrEmpty(id.Thumbprint))
{
if (!string.IsNullOrEmpty(id.SubjectName))
{
var id2 = new CertificateIdentifier
{
StoreType = id.StoreType,
StorePath = id.StorePath,
SubjectName = id.SubjectName
};
certificate = await CertificateIdentifierResolver
.LoadPrivateKeyAsync(
id2,
passwordProvider,
configuration.ApplicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
}
if (certificate != null)
{
var message = new StringBuilder();
message.AppendLine(
"Thumbprint was explicitly specified in the configuration.")
.AppendLine("Another certificate with the same subject name was found.")
.AppendLine("Use it instead?")
.AppendLine("Requested: {0}")
.AppendLine("Found: {1}");
if (!await ApproveMessageAsync(
Utils.Format(message.ToString(), id.SubjectName!, certificate.Subject), silent)
.ConfigureAwait(false))
{
throw ServiceResultException.ConfigurationError(
"Thumbprint for {0} was explicitly specified in the configuration but\n" +
"another certificate with the same subject name {1} was found.",
id.SubjectName!,
certificate.Subject);
}
}
else
{
throw ServiceResultException.ConfigurationError(
"Thumbprint was explicitly specified in the configuration. Cannot generate a new certificate.");
}
}
}
if (certificate == null)
{
if (!DisableCertificateAutoCreation)
{
certificate = await CreateApplicationInstanceCertificateAsync(
configuration,
id,
minimumKeySize,
lifeTimeInMonths,
ct)
.ConfigureAwait(false);
}
else
{
m_logger.LogWarning("Application Instance certificate auto creation is disabled.");
}
if (certificate == null)
{
throw ServiceResultException.ConfigurationError(
"There is no cert with subject {0} in the configuration.\n" +
"Please generate a cert for your application, then copy the new cert to this location: {1}",
id.SubjectName!,
id.StorePath!);
}
}
else if (configuration.SecurityConfiguration.AddAppCertToTrustedStore)
{
// ensure it is trusted.
await AddToTrustedStoreAsync(configuration, certificate, ct).ConfigureAwait(false);
}
return true;
}
finally
{
// The local 'certificate' variable is only used for validation /
// approval / trust-store warmup inside this method. Any cert that
// needs to outlive this call is reloaded via the CertificateManager
// registry by the caller. Dispose it here to balance the AddRef in
// Certificate.From / DirectoryCertificateStore.LoadPrivateKeyAsync.
certificate?.Dispose();
}
}
/// <inheritdoc/>
public async Task AddOwnCertificateToTrustedStoreAsync(
Certificate certificate,
CancellationToken ct)
{
// ApplicationConfiguration may be null; AddToTrustedStoreAsync checks defensively and
// logs a warning if no trusted store is configured, preserving prior behavior.
await AddToTrustedStoreAsync(ApplicationConfiguration!, certificate, ct).ConfigureAwait(
false);
}
/// <summary>
/// Loads the configuration.
/// </summary>
internal async ValueTask<ApplicationConfiguration?> LoadAppConfigAsync(
bool silent,
string filePath,
ApplicationType applicationType,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type? configurationType,
bool applyTraceSettings,
ICertificatePasswordProvider? certificatePasswordProvider = null,
CancellationToken ct = default)
{
m_logger.LogInformation("Loading application configuration file. {FilePath}", filePath);
try
{
// load the configuration file.
return await ApplicationConfiguration
.LoadAsync(
new FileInfo(filePath),
applicationType,
configurationType,
applyTraceSettings,
m_telemetry,
certificatePasswordProvider,
ct)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(e, "Could not load configuration file. {FilePath}", filePath);
// warn user.
if (!silent)
{
if (MessageDlg != null)
{
MessageDlg.Message("Load Application Configuration: " + e.Message);
await MessageDlg.ShowAsync().ConfigureAwait(false);
}
throw;
}
return null;
}
}
/// <summary>
/// Loads the configuration.
/// </summary>
internal async ValueTask<ApplicationConfiguration?> LoadAppConfigAsync(
bool silent,
Stream stream,
ApplicationType applicationType,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type? configurationType,
bool applyTraceSettings,
ICertificatePasswordProvider? certificatePasswordProvider = null,
CancellationToken ct = default)
{
m_logger.LogInformation("Loading application from stream.");
try
{
// load the configuration file.
return await ApplicationConfiguration
.LoadAsync(
stream,
applicationType,
configurationType,
applyTraceSettings,
m_telemetry,
certificatePasswordProvider,
ct)
.ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(e, "Could not load configuration from stream.");
// warn user.
if (!silent)
{
if (MessageDlg != null)
{
MessageDlg.Message("Load Application Configuration: " + e.Message);
await MessageDlg.ShowAsync().ConfigureAwait(false);
}
throw;
}
return null;
}
}
/// <summary>
/// Creates an application instance certificate if one does not already exist.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private async Task<bool> CheckApplicationInstanceCertificateAsync(
ApplicationConfiguration configuration,
CertificateIdentifier id,
Certificate certificate,
bool silent,
ushort minimumKeySize,
CancellationToken ct)
{
if (certificate == null)
{
return false;
}
// set suppressible errors
HashSet<StatusCode> approvedCodes =
[
StatusCodes.BadCertificateUntrusted,
StatusCodes.BadCertificateTimeInvalid,
StatusCodes.BadCertificateIssuerTimeInvalid,
StatusCodes.BadCertificateHostNameInvalid,
StatusCodes.BadCertificateRevocationUnknown,
StatusCodes.BadCertificateIssuerRevocationUnknown
];
m_logger.LogInformation(
"Check application instance certificate {Certificate}.",
certificate);
try
{
// validate certificate via the new CertificateManager pipeline,
// suppressing the same set of errors that the legacy
// CertificateValidation event handler used to accept.
var options = new Security.Certificates.CertificateValidationOptions
{
AcceptError = (cert, error) =>
{
if (approvedCodes.Contains(error.StatusCode))
{
m_logger.LogWarning(
"Application Certificate Validation suppressed {ErrorMessage}",
error.StatusCode);
return true;
}
return false;
}
};
using Certificate? publicKeyCert = certificate.HasPrivateKey
? Certificate.FromRawData(certificate.RawData)
: null;
using var chain = new CertificateCollection { publicKeyCert ?? certificate };
CertificateValidationResult result = await CertificateManager!
.ValidateAsync(
chain,
trustList: null,
options: options,
ct: ct)
.ConfigureAwait(false);
if (!result.IsValid)
{
throw new ServiceResultException(result.StatusCode);
}
}
catch (Exception ex)
{
string message = Utils.Format(
"Error validating certificate. Exception: {0}. Use certificate anyway?",
ex.Message);
if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
return false;
}
}
// check key size
int keySize = X509Utils.GetPublicKeySize(certificate);
if (minimumKeySize > keySize)
{
string message = Utils.Format(
"The key size ({0}) in the certificate is less than the minimum provided ({1}). Use certificate anyway?",
keySize,
minimumKeySize);
if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
return false;
}
}
// check domains.
if (configuration.ApplicationType != ApplicationType.Client &&
!await CheckDomainsInCertificateAsync(configuration, certificate, silent, ct)
.ConfigureAwait(false))
{
return false;
}
// Validate that the certificate contains the configuration's ApplicationUri
// ApplicationUri may be null; CompareApplicationUriWithCertificate's contract is non-null
// but its implementation returns false on null/empty, preserving prior behavior.
if (!X509Utils.CompareApplicationUriWithCertificate(
certificate,
configuration.ApplicationUri!,
out IReadOnlyList<string> certificateUris))
{
if (certificateUris.Count == 0)
{
const string message =
"The Application URI could not be found in the certificate. Use certificate anyway?";
if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
return false;
}
}
else
{
string message = Utils.Format(
"The certificate with subject '{0}' does not contain the ApplicationUri '{1}' " +
"from the configuration. Certificate contains: {2}. Use certificate anyway?",
certificate.Subject,
configuration.ApplicationUri!,
string.Join(", ", certificateUris));
if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
return false;
}
}
}
m_logger.LogInformation(
"Certificate {Certificate} validated for ApplicationUri: {ApplicationUri}",
certificate,
configuration.ApplicationUri);
// Sync the identifier metadata so subsequent resolver lookups
// by Thumbprint find the validated cert (the configured XML
// identifier may carry no Thumbprint).
id.Thumbprint = certificate.Thumbprint;
id.SubjectName = certificate.Subject;
if (id.CertificateType.IsNull)
{
id.CertificateType = CertificateIdentifier.GetCertificateType(certificate);
}
return true;
}
/// <summary>
/// Checks that the domains in the server addresses match the domains in the certificates.
/// </summary>
private async Task<bool> CheckDomainsInCertificateAsync(
ApplicationConfiguration configuration,
Certificate certificate,
bool silent,
CancellationToken ct)
{
m_logger.LogInformation("Check domains in certificate.");
bool valid = true;
ArrayOf<string> serverDomainNames = configuration.GetServerDomainNames();
ArrayOf<string> certificateDomainNames = X509Utils.GetDomainsFromCertificate(certificate);
m_logger.LogInformation("Server Domain names:");
foreach (string name in serverDomainNames)
{
m_logger.LogInformation(" {ServerDomainName}", name);
}
m_logger.LogInformation("Certificate Domain names:");
foreach (string name in certificateDomainNames)
{
m_logger.LogInformation(" {ClientDomainName}", name);
}
// get computer name.
string computerName = Utils.GetHostName();
// get IP addresses.
IPAddress[]? addresses = null;
for (int ii = 0; ii < serverDomainNames.Count; ii++)
{
if (certificateDomainNames.Contains(serverDomainNames[ii], StringComparer.OrdinalIgnoreCase))
{
continue;
}
if (string.Equals(
serverDomainNames[ii],
"localhost",
StringComparison.OrdinalIgnoreCase))
{
if (certificateDomainNames.Contains(computerName, StringComparer.OrdinalIgnoreCase))
{
continue;
}
// check for aliases.
bool found = false;
// get IP addresses only if necessary.
addresses ??= await Utils.GetHostAddressesAsync(computerName, ct).ConfigureAwait(false);
// check for ip addresses.
for (int jj = 0; jj < addresses.Length; jj++)
{
if (certificateDomainNames.Contains(addresses[jj].ToString(), StringComparer.OrdinalIgnoreCase))
{
found = true;
break;
}
}
if (found)
{
continue;
}
}
string message = Utils.Format(
"The server is configured to use domain '{0}' which does not appear in the certificate. Use certificate anyway?",
serverDomainNames[ii]);
valid = false;
if (await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
valid = true;
continue;
}
break;
}
return valid;
}
/// <summary>
/// Creates the application instance certificate.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="id">The certificate identifier.</param>
/// <param name="minimumKeySize">Minimum RSA key size to use when creating the certificate.</param>
/// <param name="lifeTimeInMonths">The lifetime in months.</param>
/// <param name="ct">Cancellation token to cancel operation with</param>
/// <returns>The new certificate</returns>
/// <exception cref="ServiceResultException"></exception>
private async Task<Certificate?> CreateApplicationInstanceCertificateAsync(
ApplicationConfiguration configuration,
CertificateIdentifier id,
ushort minimumKeySize,
ushort lifeTimeInMonths,
CancellationToken ct)
{
// delete any existing certificate.
await DeleteApplicationInstanceCertificateAsync(configuration, id, ct).ConfigureAwait(
false);
m_logger.LogInformation("Creating application instance certificate.");
// get the domains from the configuration file.
ArrayOf<string> serverDomainNames = configuration.GetServerDomainNames();
if (serverDomainNames.IsEmpty)
{
serverDomainNames = [Utils.GetHostName()];
}
// ensure the certificate store directory exists.
if (id.StoreType == CertificateStoreType.Directory)
{
// GetAbsoluteDirectoryPath requires non-null; StorePath is checked above by StoreType comparison.
Utils.GetAbsoluteDirectoryPath(id.StorePath!, true, true, true);
}
ICertificateBuilder builder = DefaultCertificateFactory.Instance
.CreateApplicationCertificate(
configuration.ApplicationUri!,
configuration.ApplicationName!,
id.SubjectName!,
serverDomainNames.ToList())
.SetLifeTime(lifeTimeInMonths);
Certificate newCertificate;
if (id.CertificateType.IsNull ||
id.CertificateType == ObjectTypeIds.ApplicationCertificateType ||
id.CertificateType == ObjectTypeIds.RsaMinApplicationCertificateType ||
id.CertificateType == ObjectTypeIds.RsaSha256ApplicationCertificateType)
{
ushort keySize = minimumKeySize == 0
? CertificateFactory.DefaultKeySize
: minimumKeySize;
newCertificate = builder.SetRSAKeySize(keySize).CreateForRSA();
m_logger.LogInformation(
"Certificate {Certificate} created for RSA with key size {KeySize} bits.",
newCertificate,
keySize);
}
else
{
ECCurve? curve =
CryptoUtils.GetCurveFromCertificateTypeId(id.CertificateType)
?? throw ServiceResultException.ConfigurationError(
"The Ecc certificate type is not supported.");
newCertificate = builder.SetECCurve(curve.Value).CreateForECDsa();
m_logger.LogInformation(
"Certificate {Certificate} created for {Curve}.",
newCertificate,
curve.Value.Oid.FriendlyName);
}
// Update the identifier metadata so subsequent resolver lookups
// (which key by Thumbprint / SubjectName) match the freshly
// generated cert. The identifier itself remains pure metadata.
id.SubjectName = newCertificate.Subject;
id.Thumbprint = newCertificate.Thumbprint;
if (id.CertificateType.IsNull)
{
id.CertificateType = CertificateIdentifier.GetCertificateType(newCertificate);
}
ICertificatePasswordProvider? passwordProvider = configuration
.SecurityConfiguration
.CertificatePasswordProvider;
await newCertificate.AddToStoreAsync(
id.StoreType!,
id.StorePath!,
passwordProvider?.GetPassword(id),
m_telemetry,
ct)
.ConfigureAwait(false);
// ensure the certificate is trusted.
if (configuration.SecurityConfiguration.AddAppCertToTrustedStore)
{
await AddToTrustedStoreAsync(configuration, newCertificate, ct).ConfigureAwait(
false);
}
// reload the certificate from disk to get the durable on-disk
// private-key handle (the in-memory cert from CreateForXxx is a
// builder-produced ephemeral instance).
Certificate? reloaded = await CertificateIdentifierResolver
.LoadPrivateKeyAsync(
id,
passwordProvider,
configuration.ApplicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
if (reloaded != null)
{
newCertificate.Dispose();
newCertificate = reloaded;
}
// Refresh CertificateManager so newly-created certificates are
// visible to subsequent ValidateAsync / GetInstanceCertificate
// callers (replaces the legacy validator.UpdateAsync hot-update
// path).
if (configuration.CertificateManager != null)
{
await configuration.CertificateManager.UpdateAsync(
configuration.SecurityConfiguration,
configuration.ApplicationUri,
ct)
.ConfigureAwait(false);
}
m_logger.LogInformation(
"Certificate {Certificate} created for {ApplicationUri}.",
newCertificate,
configuration.ApplicationUri);
// do not dispose temp cert, or X509Store certs become unusable
return newCertificate;
}
/// <summary>
/// Deletes an existing application instance certificate.
/// </summary>
/// <param name="configuration">The configuration instance that stores the configurable information for a UA application.</param>
/// <param name="id">The certificate identifier.</param>
/// <param name="ct">Cancellation token to cancel operation with</param>
private async Task DeleteApplicationInstanceCertificateAsync(
ApplicationConfiguration configuration,
CertificateIdentifier id,
CancellationToken ct)
{
if (id == null)
{
return;
}
// delete certificate and private key.
Certificate? certificate = await CertificateIdentifierResolver
.ResolveAsync(
id,
registry: null,
needPrivateKey: false,
configuration.ApplicationUri,
m_telemetry,
ct)
.ConfigureAwait(false);
if (certificate != null)
{
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Deleting application instance certificate {Certificate} and private key.",
certificate);
}
// delete trusted peer certificate.
if (configuration.SecurityConfiguration != null &&
configuration.SecurityConfiguration.TrustedPeerCertificates != null)
{
string? thumbprint = id.Thumbprint;
if (certificate != null)
{
thumbprint = certificate.Thumbprint;
}
if (!string.IsNullOrEmpty(thumbprint))
{
using ICertificateStore store = configuration.SecurityConfiguration
.TrustedPeerCertificates
.OpenStore(m_telemetry!);
if (store != null)
{
bool deleted = await store.DeleteAsync(thumbprint!, ct)
.ConfigureAwait(false);
if (deleted)
{
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Application Instance Certificate [{Thumbprint}] deleted from trusted store.",
thumbprint);
}
}
}
}
// delete certificate and private key from owner store.
if (certificate != null)
{
using ICertificateStore? store = CertificateIdentifierResolver
.OpenStore(id, m_telemetry);
if (store != null)
{
bool deleted = await store.DeleteAsync(certificate.Thumbprint, ct)
.ConfigureAwait(false);
if (deleted)
{
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Application certificate {Certificate} and private key deleted.",
certificate);
}
}
certificate.Dispose();
}
}
/// <summary>
/// Adds the certificate to the Trusted Certificate Store
/// </summary>
/// <param name="configuration">The application's configuration which specifies the location of the TrustedStore.</param>
/// <param name="certificate">The certificate to register.</param>
/// <param name="ct">The cancellation token.</param>
/// <exception cref="ArgumentNullException"><paramref name="certificate"/> is <c>null</c>.</exception>
private async Task AddToTrustedStoreAsync(
ApplicationConfiguration configuration,
Certificate certificate,
CancellationToken ct)
{
if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate));
}
if (configuration == null ||
configuration.SecurityConfiguration == null ||
configuration.SecurityConfiguration.TrustedPeerCertificates == null ||
string.IsNullOrEmpty(configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath))
{
m_logger.LogWarning("WARNING: Trusted peer store not specified.");
return;
}
try
{
using ICertificateStore? store = configuration.SecurityConfiguration
.TrustedPeerCertificates
.OpenStore(m_telemetry!);
if (store == null)
{
m_logger.LogWarning("Could not open trusted peer store.");
return;
}
// check if it already exists.
using CertificateCollection existingCertificates = await store
.FindByThumbprintAsync(certificate.Thumbprint, ct)
.ConfigureAwait(false);
if (existingCertificates.Count > 0)
{
return;
}
m_logger.LogInformation(
"Adding application certificate {Certificate} to trusted peer store.",
certificate);
List<string> subjectName = X509Utils.ParseDistinguishedName(
certificate.Subject);
// check for old certificate.
using CertificateCollection certificates = await store.EnumerateAsync(ct)
.ConfigureAwait(false);
for (int ii = 0; ii < certificates.Count; ii++)
{
if (X509Utils.CompareDistinguishedName(certificates[ii], subjectName))
{
if (certificates[ii].Thumbprint == certificate.Thumbprint)
{
return;
}
bool deleteCert = false;
if (X509Utils.IsECDsaSignature(certificates[ii]) &&
X509Utils.IsECDsaSignature(certificate))
{
if (X509Utils
.GetECDsaQualifier(certificates[ii])
.Equals(
X509Utils.GetECDsaQualifier(certificate),
StringComparison.Ordinal))
{
deleteCert = true;
}
}
else if (!X509Utils.IsECDsaSignature(certificates[ii]) &&
!X509Utils.IsECDsaSignature(certificate))
{
deleteCert = true;
}
if (deleteCert)
{
m_logger.LogInformation(
"Delete Certificate {Certificate} from trusted store.",
certificate);
await store.DeleteAsync(certificates[ii].Thumbprint, ct)
.ConfigureAwait(false);
break;
}
}
}
// add new certificate.
using var publicKey = Certificate.FromRawData(certificate.RawData);
await store.AddAsync(publicKey, ct: ct).ConfigureAwait(false);
m_logger.LogInformation("Added application certificate to trusted peer store.");
}
catch (Exception e)
{
m_logger.LogError(
"Could not add certificate to trusted peer store: {ErrorMessage}",
Redaction.Redact.Create(e));
}
}
/// <summary>
/// Show a message for approval and return result.
/// </summary>
/// <returns>True if approved, false otherwise.</returns>
private async Task<bool> ApproveMessageAsync(string message, bool silent)
{
if (!silent && MessageDlg != null)
{
MessageDlg.Message(message, true);
return await MessageDlg.ShowAsync().ConfigureAwait(false);
}
m_logger.LogError("Approve Message prompt: {Message} -> Rejected", message);
return false;
}
private readonly ITelemetryContext? m_telemetry;
private readonly ILogger m_logger;
}
}