/* ======================================================================== * 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; } }