/* ======================================================================== * Copyright (c) 2005-2016 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.Text; using System.Reflection; using System.IO; using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using Windows.UI.Popups; using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.Storage; using System.Net; namespace Opc.Ua.Configuration { public abstract class IApplicationMessageDlg { public abstract void Message(string text, Boolean ask=false); public abstract Task ShowAsync(); } /// /// A class that install, configures and runs a UA application. /// public class ApplicationInstance { #region Ctors /// /// Initializes a new instance of the class. /// public ApplicationInstance() { } /// /// Initializes a new instance of the class. /// /// The application configuration. public ApplicationInstance(ApplicationConfiguration applicationConfiguration) { m_applicationConfiguration = applicationConfiguration; } #endregion #region Public Properties /// /// Gets or sets the name of the application. /// /// The name of the application. public string ApplicationName { get { return m_applicationName; } set { m_applicationName = value; } } /// /// Gets or sets the type of the application. /// /// The type of the application. public ApplicationType ApplicationType { get { return m_applicationType; } set { m_applicationType = value; } } /// /// Gets or sets the name of the config section containing the path to the application configuration file. /// /// The name of the config section. public string ConfigSectionName { get { return m_configSectionName; } set { m_configSectionName = value; } } /// /// Gets or sets the type of configuration file. /// /// The type of configuration file. public Type ConfigurationType { get { return m_configurationType; } set { m_configurationType = value; } } /// /// Gets or sets the installation configuration. /// /// The installation configuration. public InstalledApplication InstallConfig { get { return m_installConfig; } set { m_installConfig = value; } } /// /// Gets the server. /// /// The server. public ServerBase Server { get { return m_server; } } /// /// Gets the application configuration used when the Start() method was called. /// /// The application configuration. public ApplicationConfiguration ApplicationConfiguration { get { return m_applicationConfiguration; } set { m_applicationConfiguration = value; } } /// /// Gets or sets a flag that indicates whether the application will be set up for management with the GDS agent. /// /// If true the application will not be visible to the GDS local agent after installation. public bool NoGdsAgentAdmin { get; set; } public static IApplicationMessageDlg MessageDlg { get; set; } #endregion #region InstallConfig Handling /// /// Loads the installation configuration from a file. /// public InstalledApplication LoadInstallConfigFromFile(string filePath) { if (filePath == null) throw new ArgumentNullException("filePath"); Stream istrm = null; try { istrm = File.Open(filePath, FileMode.Open, FileAccess.Read); } catch (Exception e) { throw ServiceResultException.Create(StatusCodes.BadDecodingError, e, "Could not open file: {0}", filePath); } return LoadInstallConfigFromStream(istrm); } /// /// Loads the installation configuration from an embedded resource. /// public InstalledApplication LoadInstallConfigFromResource(string resourcePath, Assembly assembly) { if (resourcePath == null) throw new ArgumentNullException("resourcePath"); Stream istrm = assembly.GetManifestResourceStream(resourcePath); if (istrm == null) { throw ServiceResultException.Create(StatusCodes.BadDecodingError, "Could not find resource file: {0}", resourcePath); } return LoadInstallConfigFromStream(istrm); } /// /// Loads the installation configuration from a stream. /// public InstalledApplication LoadInstallConfigFromStream(Stream istrm) { try { DataContractSerializer serializer = new DataContractSerializer(typeof(InstalledApplication)); return (InstalledApplication)serializer.ReadObject(istrm); } catch (Exception e) { throw ServiceResultException.Create(StatusCodes.BadDecodingError, e, "Could not parse install configuration."); } } /// /// Loads the installation configuration. /// /// The config file (may be null). public virtual void LoadInstallConfig(string configFile) { // load configuration from command line. if (!String.IsNullOrEmpty(configFile)) { InstallConfig = LoadInstallConfigFromFile(configFile); } // load it from a resource if not already loaded. else if (InstallConfig == null) { foreach (string resourcePath in this.GetType().GetTypeInfo().Assembly.GetManifestResourceNames()) { if (resourcePath.EndsWith("InstallConfig.xml")) { InstallConfig = LoadInstallConfigFromResource(resourcePath, this.GetType().GetTypeInfo().Assembly); break; } } if (InstallConfig == null) { throw new ServiceResultException(StatusCodes.BadConfigurationError, "Could not load default installation config file."); } } // override the application name. if (String.IsNullOrEmpty(InstallConfig.ApplicationName)) { InstallConfig.ApplicationName = ApplicationName; } else { ApplicationName = InstallConfig.ApplicationName; } // update fixed fields in the installation config. InstallConfig.ApplicationType = (Opc.Ua.Security.ApplicationType)(int)ApplicationType; InstallConfig.ExecutableFile = ApplicationData.Current.LocalFolder.Path; if (InstallConfig.TraceConfiguration != null) { InstallConfig.TraceConfiguration.ApplySettings(); } } #endregion #region Public Methods /// /// Starts the UA server. /// /// The server. public async Task Start(ServerBase server) { m_server = server; if (m_applicationConfiguration == null) { await LoadApplicationConfiguration(false); } if (m_applicationConfiguration.SecurityConfiguration != null && m_applicationConfiguration.SecurityConfiguration.AutoAcceptUntrustedCertificates) { m_applicationConfiguration.CertificateValidator.CertificateValidation += CertificateValidator_CertificateValidation; } server.Start(m_applicationConfiguration); } /// /// Stops the UA server. /// public void Stop() { m_server.Stop(); } #endregion #region WindowsService Class /// /// Manages the interface between the UA server and the Windows SCM. /// protected class WindowsService { #region Constructors /// /// Initializes a new instance of the class. /// /// The server. /// Name of the config section. /// Type of the application. /// Type of the configuration. public WindowsService(ServerBase server, string configSectionName, ApplicationType applicationType, Type configurationType) { m_server = server; m_configSectionName = configSectionName; m_applicationType = applicationType; m_configurationType = configurationType; } #endregion #region Private Methods /// /// Runs the service in a background thread. /// private async Task OnBackgroundStart(object state) { string filePath = null; ApplicationConfiguration configuration = null; try { filePath = ApplicationConfiguration.GetFilePathFromAppConfig(m_configSectionName); configuration = await ApplicationInstance.LoadAppConfig(false, filePath, m_applicationType, m_configurationType, true); } catch (Exception e) { ServiceResult error = ServiceResult.Create(e, StatusCodes.BadConfigurationError, "Could not load UA Service configuration file.\r\nPATH={0}", filePath); } try { if (configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates) { configuration.CertificateValidator.CertificateValidation += CertificateValidator_CertificateValidation; } m_server.Start(configuration); } catch (Exception e) { ServiceResult error = ServiceResult.Create(e, StatusCodes.BadConfigurationError, "Could not start UA Service."); Utils.Trace((int)Utils.TraceMasks.Error, error.ToLongString()); } } #endregion #region Private Fields private ServerBase m_server; private string m_configSectionName; private ApplicationType m_applicationType; private Type m_configurationType; #endregion } #endregion #region ArgumentDescription Class /// /// Stores the description of an argument. /// protected class ArgumentDescription { /// /// The argument name. /// public string Name; /// /// The argument description. /// public string Description; /// /// Whether the argument requires a value. /// public bool ValueRequired; /// /// Whether the argument allows a value. /// public bool ValueAllowed; /// /// Initializes a new instance of the class. /// /// The name. /// if set to true a value is required. /// if set to true a value is allowed. /// The description. public ArgumentDescription( string name, bool valueRequired, bool valueAllowed, string description) { Name = name; ValueRequired = valueRequired; ValueAllowed = valueAllowed; Description = description; } } private static ArgumentDescription[] s_SupportedArguments = new ArgumentDescription[] { new ArgumentDescription("/start", false, false, "Starts the application as a service (/start [/silent] [/configFile:])."), new ArgumentDescription("/install", false, false, "Installs the application (/install [/silent] [/configFile:])."), new ArgumentDescription("/uninstall", false, false, "Uninstalls the application (/uninstall [/silent] [/configFile:])."), new ArgumentDescription("/silent", false, false, "Performs operations without prompting user to confirm or displaying errors."), new ArgumentDescription("/configFile", true, true, "Specifies the installation configuration file."), }; #endregion #region Protected Methods /// /// Gets the descriptions for the supported arguments. /// protected virtual ArgumentDescription[] GetArgumentDescriptions() { return s_SupportedArguments; } /// /// Gets the help string. /// protected virtual string GetHelpString(ArgumentDescription[] commands) { StringBuilder text = new StringBuilder(); text.Append("These are the supported arguments:\r\n"); for (int ii = 0; ii < commands.Length; ii++) { ArgumentDescription command = commands[ii]; text.Append("\r\n"); if (command.ValueRequired) { text.AppendFormat("{0}: {1}", command.Name, command.Description); } else if (command.ValueAllowed) { text.AppendFormat("{0}[:] {1}", command.Name, command.Description); } else { text.AppendFormat("{0} {1}", command.Name, command.Description); } } text.Append("\r\n"); return text.ToString(); } /// /// Validates the arguments. /// protected virtual string ValidateArguments(bool ignoreUnknownArguments, Dictionary args) { ArgumentDescription[] commands = GetArgumentDescriptions(); // check if help was requested. if (args.ContainsKey("/?")) { return GetHelpString(commands); } // validate the arguments. StringBuilder error = new StringBuilder(); foreach (KeyValuePair arg in args) { ArgumentDescription command = null; for (int ii = 0; ii < commands.Length; ii++) { if (String.Compare(commands[ii].Name, arg.Key, StringComparison.OrdinalIgnoreCase) == 0) { command = commands[ii]; break; } } if (command == null) { if (!ignoreUnknownArguments) { if (error.Length > 0) { error.Append("\r\n"); } error.AppendFormat("Unrecognized argument: {0}", arg.Key); } continue; } if (command.ValueRequired && String.IsNullOrEmpty(arg.Value)) { if (error.Length > 0) { error.Append("\r\n"); } error.AppendFormat("{0} requires a value to be specified (syntax {0}:).", arg.Key); continue; } if (!command.ValueAllowed && !String.IsNullOrEmpty(arg.Value)) { if (error.Length > 0) { error.Append("\r\n"); } error.AppendFormat("{0} does not allow a value to be specified.", arg.Key); continue; } } // return any error text. return error.ToString(); } /// /// Updates the application configuration with the values from the installation configuration. /// /// The configuration to update. protected virtual async Task UpdateAppConfigWithInstallConfig(ApplicationConfiguration configuration) { // override the application name. if (InstallConfig.ApplicationName != null) { if (configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.ApplicationCertificate != null) { if (configuration.SecurityConfiguration.ApplicationCertificate.SubjectName == configuration.ApplicationName) { configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = InstallConfig.ApplicationName; } } configuration.ApplicationName = InstallConfig.ApplicationName; } if (InstallConfig.ApplicationUri != null) { configuration.ApplicationUri = InstallConfig.ApplicationUri; } // replace localhost with the current machine name. if (configuration.ApplicationUri != null) { int index = configuration.ApplicationUri.IndexOf("localhost", StringComparison.OrdinalIgnoreCase); if (index != -1) { StringBuilder buffer = new StringBuilder(); buffer.Append(configuration.ApplicationUri.Substring(0, index)); buffer.Append(Utils.GetHostName()); buffer.Append(configuration.ApplicationUri.Substring(index + "localhost".Length)); configuration.ApplicationUri = buffer.ToString(); } } ServerBaseConfiguration serverConfiguration = null; if (configuration.ServerConfiguration != null) { serverConfiguration = configuration.ServerConfiguration; } else if (configuration.DiscoveryServerConfiguration != null) { serverConfiguration = configuration.DiscoveryServerConfiguration; } if (serverConfiguration != null) { if (InstallConfig.BaseAddresses != null && InstallConfig.BaseAddresses.Count > 0) { Dictionary addresses = new Dictionary(); serverConfiguration.BaseAddresses.Clear(); for (int ii = 0; ii < InstallConfig.BaseAddresses.Count; ii++) { Uri url = Utils.ParseUri(InstallConfig.BaseAddresses[ii]); if (url != null) { if (!addresses.ContainsKey(url.Scheme)) { serverConfiguration.BaseAddresses.Add(url.ToString()); addresses.Add(url.Scheme, String.Empty); } else { serverConfiguration.AlternateBaseAddresses.Add(url.ToString()); } } } } if (InstallConfig.SecurityProfiles != null && InstallConfig.SecurityProfiles.Count > 0) { ServerSecurityPolicyCollection securityPolicies = new ServerSecurityPolicyCollection(); for (int ii = 0; ii < InstallConfig.SecurityProfiles.Count; ii++) { for (int jj = 0; jj < serverConfiguration.SecurityPolicies.Count; jj++) { if (serverConfiguration.SecurityPolicies[jj].SecurityPolicyUri == InstallConfig.SecurityProfiles[ii].ProfileUri) { securityPolicies.Add(serverConfiguration.SecurityPolicies[jj]); } } } serverConfiguration.SecurityPolicies = securityPolicies; } } if (InstallConfig.ApplicationCertificate != null) { configuration.SecurityConfiguration.ApplicationCertificate.StoreType = InstallConfig.ApplicationCertificate.StoreType; configuration.SecurityConfiguration.ApplicationCertificate.StorePath = InstallConfig.ApplicationCertificate.StorePath; if (String.IsNullOrEmpty(InstallConfig.ApplicationCertificate.SubjectName)) { configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = InstallConfig.ApplicationCertificate.SubjectName; } } if (InstallConfig.RejectedCertificatesStore != null) { configuration.SecurityConfiguration.RejectedCertificateStore = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(InstallConfig.RejectedCertificatesStore); } if (InstallConfig.IssuerCertificateStore != null) { configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = InstallConfig.IssuerCertificateStore.StoreType; configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = InstallConfig.IssuerCertificateStore.StorePath; configuration.SecurityConfiguration.TrustedIssuerCertificates.ValidationOptions = (CertificateValidationOptions)(int)InstallConfig.IssuerCertificateStore.ValidationOptions; } if (InstallConfig.TrustedCertificateStore != null) { configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType = InstallConfig.TrustedCertificateStore.StoreType; configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = InstallConfig.TrustedCertificateStore.StorePath; configuration.SecurityConfiguration.TrustedPeerCertificates.ValidationOptions = (CertificateValidationOptions)(int)InstallConfig.TrustedCertificateStore.ValidationOptions; } await configuration.CertificateValidator.Update(configuration); } /// /// Installs the service. /// /// if set to true no dialogs such be displayed. /// Additional arguments provided on the command line. protected virtual async Task Install(bool silent, Dictionary args) { Utils.Trace(Utils.TraceMasks.Information, "Installing application."); // check the configuration. string filePath = Utils.GetAbsoluteFilePath(InstallConfig.ConfigurationFile, true, false, false); if (filePath == null) { Utils.Trace("WARNING: Could not load config file specified in the installation configuration: {0}", InstallConfig.ConfigurationFile); filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName); InstallConfig.ConfigurationFile = filePath; } ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, Opc.Ua.Security.SecuredApplication.FromApplicationType(InstallConfig.ApplicationType), ConfigurationType, false); if (configuration == null) { return; } // update the configuration. await UpdateAppConfigWithInstallConfig(configuration); ApplicationConfiguration = configuration; // update configuration with information form the install config. // check the certificate. X509Certificate2 certificate = await configuration.SecurityConfiguration.ApplicationCertificate.Find(true); if (certificate != null) { if (!silent) { bool result = await CheckApplicationInstanceCertificate(configuration, certificate, silent, InstallConfig.MinimumKeySize); if (!result) { certificate = null; } } } // create a new certificate. if (certificate == null) { certificate = await CreateApplicationInstanceCertificate(configuration, InstallConfig.MinimumKeySize, InstallConfig.LifeTimeInMonths); } // ensure the certificate is trusted. await AddToTrustedStore(configuration, certificate); // add to discovery server. if (configuration.ApplicationType == ApplicationType.Server || configuration.ApplicationType == ApplicationType.ClientAndServer) { try { await AddToDiscoveryServerTrustList(certificate, null, null, configuration.SecurityConfiguration.TrustedPeerCertificates); } catch (Exception e) { Utils.Trace(e, "Could not add certificate to LDS trust list."); } } // configure access to the executable, the configuration file and the private key. await ConfigureFileAccess(configuration); // update configuration file. ConfigUtils.UpdateConfigurationLocation(InstallConfig.ExecutableFile, InstallConfig.ConfigurationFile); try { // ensure the RawData does not get serialized. certificate = configuration.SecurityConfiguration.ApplicationCertificate.Certificate; configuration.SecurityConfiguration.ApplicationCertificate.Certificate = null; configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = certificate.Subject; configuration.SecurityConfiguration.ApplicationCertificate.Thumbprint = certificate.Thumbprint; configuration.SaveToFile(configuration.SourceFilePath); // restore the configuration. configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; } catch (Exception e) { Utils.Trace(e, "Could not save configuration file. FilePath={0}", configuration.SourceFilePath); } if (!NoGdsAgentAdmin) { try { // install the GDS agent configuration file string agentPath = Utils.GetAbsoluteDirectoryPath(ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\GDS\\Applications", false, false, true); if (agentPath != null) { Opc.Ua.Security.SecuredApplication export = new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(configuration.SourceFilePath); export.ExecutableFile = InstallConfig.ExecutableFile; DataContractSerializer serializer = new DataContractSerializer(typeof(Opc.Ua.Security.SecuredApplication)); using (FileStream ostrm = File.Open(agentPath + "\\" + configuration.ApplicationName + ".xml", FileMode.Create)) { serializer.WriteObject(ostrm, export); Utils.Trace(Utils.TraceMasks.Information, "Created GDS agent configuration file."); } } } catch (Exception e) { Utils.Trace(Utils.TraceMasks.Error, "Could not create GDS agent configuration file: {0}", e.Message); } } } /// /// Uninstalls the service. /// /// if set to true no dialogs such be displayed. /// Additional arguments provided on the command line. protected virtual async Task Uninstall(bool silent, Dictionary args) { // check the configuration. string filePath = Utils.GetAbsoluteFilePath(InstallConfig.ConfigurationFile, true, false, false); if (filePath == null) { Utils.Trace("WARNING: Could not load config file specified in the installation configuration: {0}", InstallConfig.ConfigurationFile); filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName); InstallConfig.ConfigurationFile = filePath; } ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, Opc.Ua.Security.SecuredApplication.FromApplicationType(InstallConfig.ApplicationType), ConfigurationType, false); ApplicationConfiguration = configuration; if (configuration != null) { // delete certificate. if (InstallConfig.DeleteCertificatesOnUninstall) { await DeleteApplicationInstanceCertificate(configuration); } } if (!NoGdsAgentAdmin) { try { string agentPath = Utils.GetAbsoluteDirectoryPath(ApplicationData.Current.LocalFolder.Path + "\\OPC Foundation\\GDS\\Applications", false, false, false); if (agentPath != null) { File.Delete(agentPath + "\\" + configuration.ApplicationName + ".xml"); } } catch (Exception e) { Utils.Trace(Utils.TraceMasks.Error, "Could not create GDS agent configuration file: {0}", e.Message); } } } #endregion #region Static Methods /// /// Loads the configuration. /// public static async Task LoadAppConfig( bool silent, string filePath, ApplicationType applicationType, Type configurationType, bool applyTraceSettings) { Utils.Trace(Utils.TraceMasks.Information, "Loading application configuration file. {0}", filePath); try { // load the configuration file. ApplicationConfiguration configuration = await ApplicationConfiguration.Load( new System.IO.FileInfo(filePath), applicationType, configurationType, applyTraceSettings); if (configuration == null) { return null; } return configuration; } catch (Exception e) { // warn user. if (!silent && MessageDlg != null) { MessageDlg.Message("Load Application Configuration: " + e.Message); await MessageDlg.ShowAsync(); } Utils.Trace(e, "Could not load configuration file. {0}", filePath); return null; } } /// /// Loads the application configuration. /// public async Task LoadApplicationConfiguration(string filePath, bool silent) { ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, ApplicationType, ConfigurationType, true); if (configuration == null) { throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Could not load configuration file."); } m_applicationConfiguration = configuration; return configuration; } /// /// Loads the application configuration. /// public async Task LoadApplicationConfiguration(bool silent) { string filePath = ApplicationConfiguration.GetFilePathFromAppConfig(ConfigSectionName); ApplicationConfiguration configuration = await LoadAppConfig(silent, filePath, ApplicationType, ConfigurationType, true); if (configuration == null) { throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Could not load configuration file."); } m_applicationConfiguration = configuration; return configuration; } /// /// Checks for a valid application instance certificate. /// /// if set to true no dialogs will be displayed. /// Minimum size of the key. public async Task CheckApplicationInstanceCertificate( bool silent, ushort minimumKeySize) { Utils.Trace(Utils.TraceMasks.Information, "Checking application instance certificate."); ApplicationConfiguration configuration = null; if (m_applicationConfiguration == null) { await LoadApplicationConfiguration(silent); } configuration = m_applicationConfiguration; bool dontCreateNewCertificate = true; // find the existing certificate. CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; if (id == null) { throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Configuration file does not specify a certificate."); } X509Certificate2 certificate = await id.Find(true); // check that it is ok. if (certificate != null) { dontCreateNewCertificate = await CheckApplicationInstanceCertificate(configuration, certificate, silent, minimumKeySize); } else { // check for missing private key. certificate = await id.Find(false); if (certificate != null) { throw ServiceResultException.Create(StatusCodes.BadConfigurationError, "Cannot access certificate private key. Subject={0}", certificate.Subject); } // check for missing thumbprint. if (!String.IsNullOrEmpty(id.Thumbprint)) { if (!String.IsNullOrEmpty(id.SubjectName)) { CertificateIdentifier id2 = new CertificateIdentifier(); id2.StoreType = id.StoreType; id2.StorePath = id.StorePath; id2.SubjectName = id.SubjectName; certificate = await id2.Find(true); } if (certificate != null) { string message = Utils.Format( "Thumbprint was explicitly specified in the configuration." + "\r\nAnother certificate with the same subject name was found." + "\r\nUse it instead?\r\n" + "\r\nRequested: {0}" + "\r\nFound: {1}", id.SubjectName, certificate.Subject); throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message); } else { string message = Utils.Format("Thumbprint was explicitly specified in the configuration. Cannot generate a new certificate."); throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message); } } } // create a new certificate. if (!dontCreateNewCertificate) { certificate = await CreateApplicationInstanceCertificate(configuration, minimumKeySize, 600); } else { if (certificate == null) { string message = Utils.Format( "There is no cert with subject {0} in the configuration." + "\r\n Copy cert to this location:" + "\r\n{1}", id.SubjectName, id.StorePath); throw ServiceResultException.Create(StatusCodes.BadConfigurationError, message); } // ensure it is trusted. await AddToTrustedStore(configuration, certificate); } // add to discovery server. if (configuration.ApplicationType == ApplicationType.Server || configuration.ApplicationType == ApplicationType.ClientAndServer) { try { await AddToDiscoveryServerTrustList(certificate, null, null, configuration.SecurityConfiguration.TrustedPeerCertificates); } catch (Exception e) { Utils.Trace(e, "Could not add certificate to LDS trust list."); } } return true; } #endregion #region HTTPS Support /// /// Uses the UA validation logic for HTTPS certificates. /// /// The validator. public static void SetUaValidationForHttps(CertificateValidator validator) { m_validator = validator; } /// /// Remotes the certificate validate. /// private static bool HttpsCertificateValidation( object sender, X509Certificate2 cert, System.Net.Security.SslPolicyErrors error) { try { m_validator.Validate(new X509Certificate2(cert.RawData)); return true; } catch (Exception e) { Utils.Trace(e, "Could not verify SSL certificate: {0}", cert.Subject); return false; } } private static CertificateValidator m_validator; #endregion #region Private Methods /// /// Handles a certificate validation error. /// private static void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e) { try { if (e.Error != null && e.Error.Code == StatusCodes.BadCertificateUntrusted) { e.Accept = true; Utils.Trace((int)Utils.TraceMasks.Security, "Automatically accepted certificate: {0}", e.Certificate.Subject); } } catch (Exception exception) { Utils.Trace(exception, "Error accepting certificate."); } } /// /// Creates an application instance certificate if one does not already exist. /// private static async Task CheckApplicationInstanceCertificate( ApplicationConfiguration configuration, X509Certificate2 certificate, bool silent, ushort minimumKeySize) { if (certificate == null) { return false; } Utils.Trace(Utils.TraceMasks.Information, "Checking application instance certificate. {0}", certificate.Subject); // validate certificate. configuration.CertificateValidator.Validate(certificate); // check key size. if (minimumKeySize > certificate.GetRSAPublicKey().KeySize) { bool valid = false; string message = Utils.Format( "The key size ({0}) in the certificate is less than the minimum provided ({1}). Update certificate?", certificate.GetRSAPublicKey().KeySize, minimumKeySize); if (!silent && MessageDlg!=null) { MessageDlg.Message(message, true); if (!await MessageDlg.ShowAsync()) { valid = true; } } Utils.Trace(message); if (!valid) { return false; } } // check domains. if (configuration.ApplicationType != ApplicationType.Client) { return await CheckDomainsInCertificate(configuration, certificate, silent); } // check uri. string applicationUri = Utils.GetApplicationUriFromCertficate(certificate); if (String.IsNullOrEmpty(applicationUri)) { bool valid = false; string message = "The Application URI could not be read from the certificate. Use certificate anyway?"; if (!silent && MessageDlg != null) { MessageDlg.Message(message, true); if (!await MessageDlg.ShowAsync()) { valid = true; } } Utils.Trace(message); if (!valid) { return false; } } // update configuration. configuration.ApplicationUri = applicationUri; configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; return true; } /// /// Checks that the domains in the server addresses match the domains in the certificates. /// private static async Task CheckDomainsInCertificate( ApplicationConfiguration configuration, X509Certificate2 certificate, bool silent) { Utils.Trace(Utils.TraceMasks.Information, "Checking domains in certificate. {0}", certificate.Subject); bool valid = true; IList serverDomainNames = configuration.GetServerDomainNames(); IList certificateDomainNames = Utils.GetDomainsFromCertficate(certificate); // get computer name. string computerName = Utils.GetHostName(); // get IP addresses. IPAddress[] addresses = await Utils.GetHostAddresses(computerName); for (int ii = 0; ii < serverDomainNames.Count; ii++) { if (Utils.FindStringIgnoreCase(certificateDomainNames, serverDomainNames[ii])) { continue; } if (String.Compare(serverDomainNames[ii], "localhost", StringComparison.OrdinalIgnoreCase) == 0) { if (Utils.FindStringIgnoreCase(certificateDomainNames, computerName)) { continue; } // check for aliases. bool found = false; // check for ip addresses. for (int jj = 0; jj < addresses.Length; jj++) { if (Utils.FindStringIgnoreCase(certificateDomainNames, addresses[jj].ToString())) { 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?", serverDomainNames[ii]); valid = false; if (!silent && MessageDlg != null) { MessageDlg.Message(message, true); if (await MessageDlg.ShowAsync()) { valid = true; continue; } } Utils.Trace(message); break; } return valid; } /// /// Creates the application instance certificate. /// /// The configuration. /// Size of the key. /// The lifetime in months. /// The new certificate private static async Task CreateApplicationInstanceCertificate( ApplicationConfiguration configuration, ushort keySize, ushort lifetimeInMonths) { Utils.Trace(Utils.TraceMasks.Information, "Creating application instance certificate. KeySize={0}, Lifetime={1}", keySize, lifetimeInMonths); // delete existing any existing certificate. await DeleteApplicationInstanceCertificate(configuration); CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; // get the domains from the configuration file. IList serverDomainNames = configuration.GetServerDomainNames(); if (serverDomainNames.Count == 0) { serverDomainNames.Add(Utils.GetHostName()); } // ensure the certificate store directory exists. if (id.StoreType == CertificateStoreType.Directory) { Utils.GetAbsoluteDirectoryPath(id.StorePath, true, true, true); } X509Certificate2 certificate = await Opc.Ua.CertificateFactory.CreateCertificate( id.StoreType, id.StorePath, configuration.ApplicationUri, configuration.ApplicationName, null, serverDomainNames, keySize, lifetimeInMonths); id.Certificate = certificate; await AddToTrustedStore(configuration, certificate); await configuration.CertificateValidator.Update(configuration.SecurityConfiguration); Utils.Trace(Utils.TraceMasks.Information, "Certificate created. Thumbprint={0}", certificate.Thumbprint); // reload the certificate from disk. return await configuration.SecurityConfiguration.ApplicationCertificate.LoadPrivateKey(null); } /// /// Deletes an existing application instance certificate. /// /// The configuration instance that stores the configurable information for a UA application. private static async Task DeleteApplicationInstanceCertificate(ApplicationConfiguration configuration) { Utils.Trace(Utils.TraceMasks.Information, "Deleting application instance certificate."); // create a default certificate id none specified. CertificateIdentifier id = configuration.SecurityConfiguration.ApplicationCertificate; if (id == null) { return; } // delete private key. X509Certificate2 certificate = await id.Find(); // delete trusted peer certificate. if (configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.TrustedPeerCertificates != null) { string thumbprint = id.Thumbprint; if (certificate != null) { thumbprint = certificate.Thumbprint; } using (ICertificateStore store = configuration.SecurityConfiguration.TrustedPeerCertificates.OpenStore()) { await store.Delete(thumbprint); } } // delete private key. if (certificate != null) { using (ICertificateStore store = id.OpenStore()) { await store.Delete(certificate.Thumbprint); } } } /// /// Adds the application certificate to the discovery server trust list. /// public static async Task AddToDiscoveryServerTrustList( X509Certificate2 certificate, string oldThumbprint, IList issuers, CertificateStoreIdentifier trustedCertificateStore) { Utils.Trace(Utils.TraceMasks.Information, "Adding certificate to discovery server trust list."); try { string configurationPath = Utils.GetAbsoluteFilePath(ApplicationData.Current.LocalFolder.Path + @"\OPC Foundation\Config\Opc.Ua.DiscoveryServer.Config.xml", true, false, false); if (configurationPath == null) { throw new ServiceResultException("Could not find the discovery server configuration file. Please confirm that it is installed."); } Opc.Ua.Security.SecuredApplication ldsConfiguration = new Opc.Ua.Security.SecurityConfigurationManager().ReadConfiguration(configurationPath); CertificateStoreIdentifier csid = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(ldsConfiguration.TrustedCertificateStore); await AddApplicationCertificateToStore(csid, certificate, oldThumbprint); if (issuers != null && ldsConfiguration.IssuerCertificateStore != null) { csid = Opc.Ua.Security.SecuredApplication.FromCertificateStoreIdentifier(ldsConfiguration.IssuerCertificateStore); AddIssuerCertificatesToStore(csid, issuers); } CertificateIdentifier cid = Opc.Ua.Security.SecuredApplication.FromCertificateIdentifier(ldsConfiguration.ApplicationCertificate); X509Certificate2 ldsCertificate = await cid.Find(false); // add LDS certificate to application trust list. if (ldsCertificate != null && trustedCertificateStore != null) { await AddApplicationCertificateToStore(csid, ldsCertificate, null); } } catch (Exception e) { Utils.Trace(e, "Could not add certificate to discovery server trust list."); } } /// /// Adds an application certificate to a store. /// private static async Task AddApplicationCertificateToStore( CertificateStoreIdentifier csid, X509Certificate2 certificate, string oldThumbprint) { ICertificateStore store = csid.OpenStore(); try { // delete the old certificate. if (oldThumbprint != null) { await store.Delete(oldThumbprint); } // delete certificates with the same application uri. if (store.FindByThumbprint(certificate.Thumbprint) == null) { string applicationUri = Utils.GetApplicationUriFromCertficate(certificate); // delete any existing certificates. X509Certificate2Collection collection = await store.Enumerate(); foreach (X509Certificate2 target in collection) { if (Utils.CompareDistinguishedName(target.Subject, certificate.Subject)) { if (Utils.GetApplicationUriFromCertficate(target) == applicationUri) { await store.Delete(target.Thumbprint); } } } // add new certificate. await store.Add(new X509Certificate2(certificate.RawData)); } } finally { store.Close(); } } /// /// Adds an application certificate to a store. /// private static void AddIssuerCertificatesToStore(CertificateStoreIdentifier csid, IList issuers) { ICertificateStore store = csid.OpenStore(); try { foreach (X509Certificate2 issuer in issuers) { if (store.FindByThumbprint(issuer.Thumbprint) == null) { store.Add(issuer); } } } finally { store.Close(); } } /// /// Adds the certificate to the Trusted Certificate Store /// /// The application's configuration which specifies the location of the TrustedStore. /// The certificate to register. private static async Task AddToTrustedStore(ApplicationConfiguration configuration, X509Certificate2 certificate) { if (certificate == null) throw new ArgumentNullException("certificate"); string storePath = null; if (configuration != null && configuration.SecurityConfiguration != null && configuration.SecurityConfiguration.TrustedPeerCertificates != null) { storePath = configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath; } if (String.IsNullOrEmpty(storePath)) { Utils.Trace(Utils.TraceMasks.Information, "WARNING: Trusted peer store not specified."); return; } try { ICertificateStore store = configuration.SecurityConfiguration.TrustedPeerCertificates.OpenStore(); if (store == null) { Utils.Trace("Could not open trusted peer store. StorePath={0}", storePath); return; } try { // check if it already exists. X509Certificate2Collection existingCertificates = await store.FindByThumbprint(certificate.Thumbprint); if (existingCertificates.Count > 0) { return; } Utils.Trace(Utils.TraceMasks.Information, "Adding certificate to trusted peer store. StorePath={0}", storePath); List subjectName = Utils.ParseDistinguishedName(certificate.Subject); // check for old certificate. X509Certificate2Collection certificates = await store.Enumerate(); for (int ii = 0; ii < certificates.Count; ii++) { if (Utils.CompareDistinguishedName(certificates[ii], subjectName)) { if (certificates[ii].Thumbprint == certificate.Thumbprint) { return; } await store.Delete(certificates[ii].Thumbprint); break; } } // add new certificate. X509Certificate2 publicKey = new X509Certificate2(certificate.RawData); await store.Add(publicKey); } finally { store.Close(); } } catch (Exception e) { Utils.Trace(e, "Could not add certificate to trusted peer store. StorePath={0}", storePath); } } /// /// Gets the access rules to use for the application. /// private List GetAccessRules() { List rules = new List(); // check for rules specified in the installer configuration. bool hasAdmin = false; if (InstallConfig.AccessRules != null) { for (int ii = 0; ii < InstallConfig.AccessRules.Count; ii++) { ApplicationAccessRule rule = InstallConfig.AccessRules[ii]; if (rule.Right == ApplicationAccessRight.Configure && rule.RuleType == AccessControlType.Allow) { hasAdmin = true; break; } } rules = InstallConfig.AccessRules; } // provide some default rules. if (rules.Count == 0) { // give user run access. ApplicationAccessRule rule = new ApplicationAccessRule(); rule.RuleType = AccessControlType.Allow; rule.Right = ApplicationAccessRight.Run; rule.IdentityName = WellKnownSids.Users; rules.Add(rule); // ensure service can access. if (InstallConfig.InstallAsService) { rule = new ApplicationAccessRule(); rule.RuleType = AccessControlType.Allow; rule.Right = ApplicationAccessRight.Run; rule.IdentityName = WellKnownSids.NetworkService; rules.Add(rule); rule = new ApplicationAccessRule(); rule.RuleType = AccessControlType.Allow; rule.Right = ApplicationAccessRight.Run; rule.IdentityName = WellKnownSids.LocalService; rules.Add(rule); } } // ensure someone can change the configuration later. if (!hasAdmin) { ApplicationAccessRule rule = new ApplicationAccessRule(); rule.RuleType = AccessControlType.Allow; rule.Right = ApplicationAccessRight.Configure; rule.IdentityName = WellKnownSids.Administrators; rules.Add(rule); } return rules; } /// /// Configures access to the executable, the configuration file and the private key. /// private async Task ConfigureFileAccess(ApplicationConfiguration configuration) { Utils.Trace(Utils.TraceMasks.Information, "Configuring file access."); List rules = GetAccessRules(); // apply access rules to the excutable file. try { if (InstallConfig.SetExecutableFilePermissions) { ApplicationAccessRule.SetAccessRules(InstallConfig.ExecutableFile, rules, true); } } catch (Exception e) { Utils.Trace(e, "Could not set executable file permissions."); } // apply access rules to the configuration file. try { if (InstallConfig.SetConfigurationFilePermisions) { ApplicationAccessRule.SetAccessRules(configuration.SourceFilePath, rules, true); } } catch (Exception e) { Utils.Trace(e, "Could not set configuration file permissions."); } // apply access rules to the private key file. try { X509Certificate2 certificate = await configuration.SecurityConfiguration.ApplicationCertificate.Find(true); if (certificate != null) { ICertificateStore store = configuration.SecurityConfiguration.ApplicationCertificate.OpenStore(); store.SetAccessRules(certificate.Thumbprint, rules, true); } } catch (Exception e) { Utils.Trace(e, "Could not set private key file permissions."); } } #endregion #region Private Fields private string m_applicationName; private ApplicationType m_applicationType; private string m_configSectionName; private Type m_configurationType; private InstalledApplication m_installConfig; private ServerBase m_server; private ApplicationConfiguration m_applicationConfiguration; #endregion } }