/* ========================================================================
* 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
{
///
public class ApplicationInstance : IApplicationInstance
{
///
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);
}
///
/// Obsolete constructor
///
[Obsolete("Use ApplicationInstance(ITelemetryContext) instead.")]
public ApplicationInstance()
: this((ITelemetryContext?)null)
{
}
///
/// Obsolete constructor
///
[Obsolete("Use ApplicationInstance(ApplicationConfiguration, ITelemetryContext) instead.")]
public ApplicationInstance(ApplicationConfiguration applicationConfiguration)
: this(applicationConfiguration, null)
{
}
///
/// Initializes a new instance of the class.
///
public ApplicationInstance(ITelemetryContext? telemetry)
{
m_telemetry = telemetry;
m_logger = telemetry?.CreateLogger() ?? NullLogger.Instance;
DisableCertificateAutoCreation = false;
}
///
/// Initializes a new instance of the class.
///
/// The application configuration.
/// The telemetry context to use to create obvservability instruments
public ApplicationInstance(
ApplicationConfiguration applicationConfiguration,
ITelemetryContext? telemetry)
: this(telemetry)
{
ApplicationConfiguration = applicationConfiguration;
}
///
public string? ApplicationName { get; set; }
///
public ApplicationType ApplicationType { get; set; }
///
public string? ConfigSectionName { get; set; }
///
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type? ConfigurationType { get; set; }
///
public IServerBase? Server { get; private set; }
///
public ApplicationConfiguration? ApplicationConfiguration { get; set; }
///
/// Get or set the message dialog.
///
public static IApplicationMessageDlg? MessageDlg { get; set; }
///
public ICertificatePasswordProvider? CertificatePasswordProvider { get; set; }
///
public bool DisableCertificateAutoCreation { get; set; }
///
/// Gets the certificate manager for this application instance.
///
public CertificateManager? CertificateManager { get; private set; }
///
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);
}
///
public ValueTask StopAsync(CancellationToken ct = default)
{
return Server?.StopAsync(ct) ?? default;
}
///
/// Stops the UA server.
///
[Obsolete("Use StopAsync")]
public void Stop()
{
Server?.StopAsync().AsTask().GetAwaiter().GetResult();
}
///
public async Task 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;
}
///
public async ValueTask 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;
}
///
public ValueTask 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);
}
///
/// Helper to replace localhost with the hostname
/// in the application uri and base addresses of the
/// configuration.
///
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;
}
///
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);
}
///
public async ValueTask DeleteApplicationInstanceCertificateAsync(
string[]? profileIds = null,
CancellationToken ct = default)
{
// TODO: delete only selected profiles
if (ApplicationConfiguration == null)
{
throw new ArgumentException("Missing configuration.");
}
ArrayOf applicationCerts = ApplicationConfiguration
.SecurityConfiguration.ApplicationCertificates;
for (int ii = 0; ii < applicationCerts.Count; ii++)
{
CertificateIdentifier id = applicationCerts[ii];
await DeleteApplicationInstanceCertificateAsync(ApplicationConfiguration, id, ct)
.ConfigureAwait(false);
}
}
///
public async ValueTask 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;
}
///
/// 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.
///
///
private async Task 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();
}
}
///
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);
}
///
/// Loads the configuration.
///
internal async ValueTask 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;
}
}
///
/// Loads the configuration.
///
internal async ValueTask 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;
}
}
///
/// Creates an application instance certificate if one does not already exist.
///
///
private async Task CheckApplicationInstanceCertificateAsync(
ApplicationConfiguration configuration,
CertificateIdentifier id,
Certificate certificate,
bool silent,
ushort minimumKeySize,
CancellationToken ct)
{
if (certificate == null)
{
return false;
}
// set suppressible errors
HashSet 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 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;
}
///
/// Checks that the domains in the server addresses match the domains in the certificates.
///
private async Task CheckDomainsInCertificateAsync(
ApplicationConfiguration configuration,
Certificate certificate,
bool silent,
CancellationToken ct)
{
m_logger.LogInformation("Check domains in certificate.");
bool valid = true;
ArrayOf serverDomainNames = configuration.GetServerDomainNames();
ArrayOf 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;
}
///
/// Creates the application instance certificate.
///
/// The configuration.
/// The certificate identifier.
/// Minimum RSA key size to use when creating the certificate.
/// The lifetime in months.
/// Cancellation token to cancel operation with
/// The new certificate
///
private async Task 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 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;
}
///
/// Deletes an existing application instance certificate.
///
/// The configuration instance that stores the configurable information for a UA application.
/// The certificate identifier.
/// Cancellation token to cancel operation with
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();
}
}
///
/// Adds the certificate to the Trusted Certificate Store
///
/// The application's configuration which specifies the location of the TrustedStore.
/// The certificate to register.
/// The cancellation token.
/// is null.
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 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));
}
}
///
/// Show a message for approval and return result.
///
/// True if approved, false otherwise.
private async Task 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;
}
}