/* ======================================================================== * Copyright (c) 2005-2020 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.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using System.Xml; namespace Opc.Ua.Server { /// /// Priviledged identity which can access the system configuration. /// public class SystemConfigurationIdentity : IUserIdentity { private IUserIdentity m_identity; /// /// Create a user identity with the priviledge /// to modify the system configuration. /// /// The user identity. public SystemConfigurationIdentity(IUserIdentity identity) { m_identity = identity; } #region IUserIdentity /// public string DisplayName { get { return m_identity.DisplayName; } } /// public string PolicyId { get { return m_identity.PolicyId; } } /// public UserTokenType TokenType { get { return m_identity.TokenType; } } /// public XmlQualifiedName IssuedTokenType { get { return m_identity.IssuedTokenType; } } /// public bool SupportsSignatures { get { return m_identity.SupportsSignatures; } } /// public NodeIdCollection GrantedRoleIds { get { return m_identity.GrantedRoleIds; } set { m_identity.GrantedRoleIds = value; } } /// public UserIdentityToken GetIdentityToken() { return m_identity.GetIdentityToken(); } #endregion } /// /// The Server Configuration Node Manager. /// public class ConfigurationNodeManager : DiagnosticsNodeManager { #region Constructors /// /// Initializes the configuration and diagnostics manager. /// public ConfigurationNodeManager( IServerInternal server, ApplicationConfiguration configuration ) : base(server, configuration) { m_rejectedStorePath = configuration.SecurityConfiguration.RejectedCertificateStore.StorePath; m_certificateGroups = new List(); m_configuration = configuration; // TODO: configure cert groups in configuration ServerCertificateGroup defaultApplicationGroup = new ServerCertificateGroup { BrowseName = Opc.Ua.BrowseNames.DefaultApplicationGroup, CertificateTypes = new NodeId[] { ObjectTypeIds.RsaSha256ApplicationCertificateType }, ApplicationCertificate = configuration.SecurityConfiguration.ApplicationCertificate, IssuerStorePath = configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath, TrustedStorePath = configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath }; m_certificateGroups.Add(defaultApplicationGroup); } #endregion #region INodeManager Members /// /// Replaces the generic node with a node specific to the model. /// protected override NodeState AddBehaviourToPredefinedNode( ISystemContext context, NodeState predefinedNode) { BaseObjectState passiveNode = predefinedNode as BaseObjectState; if (passiveNode != null) { NodeId typeId = passiveNode.TypeDefinitionId; if (IsNodeIdInNamespace(typeId) && typeId.IdType == IdType.Numeric) { switch ((uint)typeId.Identifier) { case ObjectTypes.ServerConfigurationType: { ServerConfigurationState activeNode = new ServerConfigurationState(passiveNode.Parent); activeNode.Create(context, passiveNode); m_serverConfigurationNode = activeNode; // replace the node in the parent. if (passiveNode.Parent != null) { passiveNode.Parent.ReplaceChild(context, activeNode); } return activeNode; } case ObjectTypes.CertificateGroupFolderType: { CertificateGroupFolderState activeNode = new CertificateGroupFolderState(passiveNode.Parent); activeNode.Create(context, passiveNode); // delete unsupported groups if (m_certificateGroups.All(group => group.BrowseName != activeNode.DefaultHttpsGroup?.BrowseName)) { activeNode.DefaultHttpsGroup = null; } if (m_certificateGroups.All(group => group.BrowseName != activeNode.DefaultUserTokenGroup?.BrowseName)) { activeNode.DefaultUserTokenGroup = null; } if (m_certificateGroups.All(group => group.BrowseName != activeNode.DefaultApplicationGroup?.BrowseName)) { activeNode.DefaultApplicationGroup = null; } // replace the node in the parent. if (passiveNode.Parent != null) { passiveNode.Parent.ReplaceChild(context, activeNode); } return activeNode; } case ObjectTypes.CertificateGroupType: { var result = m_certificateGroups.FirstOrDefault(group => group.BrowseName == passiveNode.BrowseName); if (result != null) { CertificateGroupState activeNode = new CertificateGroupState(passiveNode.Parent); activeNode.Create(context, passiveNode); result.NodeId = activeNode.NodeId; result.Node = activeNode; // replace the node in the parent. if (passiveNode.Parent != null) { passiveNode.Parent.ReplaceChild(context, activeNode); } return activeNode; } } break; } } } return base.AddBehaviourToPredefinedNode(context, predefinedNode); } #endregion #region Public methods /// /// Creates the configuration node for the server. /// public void CreateServerConfiguration( ServerSystemContext systemContext, ApplicationConfiguration configuration) { // setup server configuration node m_serverConfigurationNode.ServerCapabilities.Value = configuration.ServerConfiguration.ServerCapabilities.ToArray(); m_serverConfigurationNode.ServerCapabilities.ValueRank = ValueRanks.OneDimension; m_serverConfigurationNode.ServerCapabilities.ArrayDimensions = new ReadOnlyList(new List { 0 }); m_serverConfigurationNode.SupportedPrivateKeyFormats.Value = configuration.ServerConfiguration.SupportedPrivateKeyFormats.ToArray(); m_serverConfigurationNode.SupportedPrivateKeyFormats.ValueRank = ValueRanks.OneDimension; m_serverConfigurationNode.SupportedPrivateKeyFormats.ArrayDimensions = new ReadOnlyList(new List { 0 }); m_serverConfigurationNode.MaxTrustListSize.Value = (uint)configuration.ServerConfiguration.MaxTrustListSize; m_serverConfigurationNode.MulticastDnsEnabled.Value = configuration.ServerConfiguration.MultiCastDnsEnabled; m_serverConfigurationNode.UpdateCertificate.OnCall = new UpdateCertificateMethodStateMethodCallHandler(UpdateCertificate); m_serverConfigurationNode.CreateSigningRequest.OnCall = new CreateSigningRequestMethodStateMethodCallHandler(CreateSigningRequest); m_serverConfigurationNode.ApplyChanges.OnCallMethod = new GenericMethodCalledEventHandler(ApplyChanges); m_serverConfigurationNode.GetRejectedList.OnCall = new GetRejectedListMethodStateMethodCallHandler(GetRejectedList); m_serverConfigurationNode.ClearChangeMasks(systemContext, true); // setup certificate group trust list handlers foreach (var certGroup in m_certificateGroups) { certGroup.Node.CertificateTypes.Value = certGroup.CertificateTypes; certGroup.Node.TrustList.Handle = new TrustList( certGroup.Node.TrustList, certGroup.TrustedStorePath, certGroup.IssuerStorePath, new TrustList.SecureAccess(HasApplicationSecureAdminAccess), new TrustList.SecureAccess(HasApplicationSecureAdminAccess) ); certGroup.Node.ClearChangeMasks(systemContext, true); } // find ServerNamespaces node and subscribe to StateChanged NamespacesState serverNamespacesNode = FindPredefinedNode(ObjectIds.Server_Namespaces, typeof(NamespacesState)) as NamespacesState; if (serverNamespacesNode != null) { serverNamespacesNode.StateChanged += ServerNamespacesChanged; } } /// /// Gets and returns the node associated with the specified NamespaceUri /// /// /// public NamespaceMetadataState GetNamespaceMetadataState(string namespaceUri) { if (namespaceUri == null) { return null; } if (m_namespaceMetadataStates.ContainsKey(namespaceUri)) { return m_namespaceMetadataStates[namespaceUri]; } NamespaceMetadataState namespaceMetadataState = FindNamespaceMetadataState(namespaceUri); lock (Lock) { // remember the result for faster access. m_namespaceMetadataStates[namespaceUri] = namespaceMetadataState; } return namespaceMetadataState; } /// /// Gets or creates the node for the specified NamespaceUri. /// /// /// public NamespaceMetadataState CreateNamespaceMetadataState(string namespaceUri) { NamespaceMetadataState namespaceMetadataState = FindNamespaceMetadataState(namespaceUri); if (namespaceMetadataState == null) { // find ServerNamespaces node NamespacesState serverNamespacesNode = FindPredefinedNode(ObjectIds.Server_Namespaces, typeof(NamespacesState)) as NamespacesState; if (serverNamespacesNode == null) { Utils.Trace("Cannot create NamespaceMetadataState for namespace '{0}'.", namespaceUri); return null; } // create the NamespaceMetadata node namespaceMetadataState = new NamespaceMetadataState(serverNamespacesNode); namespaceMetadataState.BrowseName = new QualifiedName(namespaceUri, NamespaceIndex); namespaceMetadataState.Create(SystemContext, null, namespaceMetadataState.BrowseName, null, true); namespaceMetadataState.DisplayName = namespaceUri; namespaceMetadataState.SymbolicName = namespaceUri; namespaceMetadataState.NamespaceUri.Value = namespaceUri; // add node as child of ServerNamespaces and in predefined nodes serverNamespacesNode.AddChild(namespaceMetadataState); serverNamespacesNode.ClearChangeMasks(Server.DefaultSystemContext, true); AddPredefinedNode(SystemContext, namespaceMetadataState); } return namespaceMetadataState; } /// /// Determine if the impersonated user has admin access. /// /// /// /// public void HasApplicationSecureAdminAccess(ISystemContext context) { OperationContext operationContext = (context as SystemContext)?.OperationContext as OperationContext; if (operationContext != null) { if (operationContext.ChannelContext?.EndpointDescription?.SecurityMode != MessageSecurityMode.SignAndEncrypt) { throw new ServiceResultException(StatusCodes.BadUserAccessDenied, "Secure Application Administrator access required."); } // allow access to system configuration only through special identity SystemConfigurationIdentity user = context.UserIdentity as SystemConfigurationIdentity; if (user == null || user.TokenType == UserTokenType.Anonymous) { throw new ServiceResultException(StatusCodes.BadUserAccessDenied, "System Configuration Administrator access required."); } } } #endregion #region Private Methods private ServiceResult UpdateCertificate( ISystemContext context, MethodState method, NodeId objectId, NodeId certificateGroupId, NodeId certificateTypeId, byte[] certificate, byte[][] issuerCertificates, string privateKeyFormat, byte[] privateKey, ref bool applyChangesRequired) { HasApplicationSecureAdminAccess(context); if (certificate == null) { throw new ArgumentNullException(nameof(certificate)); } privateKeyFormat = privateKeyFormat?.ToUpper(); if (!(String.IsNullOrEmpty(privateKeyFormat) || privateKeyFormat == "PEM" || privateKeyFormat == "PFX")) { throw new ServiceResultException(StatusCodes.BadNotSupported, "The private key format is not supported."); } ServerCertificateGroup certificateGroup = VerifyGroupAndTypeId(certificateGroupId, certificateTypeId); certificateGroup.UpdateCertificate = null; X509Certificate2Collection newIssuerCollection = new X509Certificate2Collection(); X509Certificate2 newCert; try { // build issuer chain if (issuerCertificates != null) { foreach (byte[] issuerRawCert in issuerCertificates) { var newIssuerCert = new X509Certificate2(issuerRawCert); newIssuerCollection.Add(newIssuerCert); } } newCert = new X509Certificate2(certificate); } catch { throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Certificate data is invalid."); } // validate new subject matches the previous subject if (!X509Utils.CompareDistinguishedName(certificateGroup.ApplicationCertificate.SubjectName, newCert.SubjectName.Name)) { throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Subject Name of new certificate doesn't match the application."); } // self signed bool selfSigned = X509Utils.CompareDistinguishedName(newCert.Subject, newCert.Issuer); if (selfSigned && newIssuerCollection.Count != 0) { throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Issuer list not empty for self signed certificate."); } if (!selfSigned) { try { // verify cert with issuer chain CertificateValidator certValidator = new CertificateValidator(); CertificateTrustList issuerStore = new CertificateTrustList(); CertificateIdentifierCollection issuerCollection = new CertificateIdentifierCollection(); foreach (var issuerCert in newIssuerCollection) { issuerCollection.Add(new CertificateIdentifier(issuerCert)); } issuerStore.TrustedCertificates = issuerCollection; certValidator.Update(issuerStore, issuerStore, null); certValidator.Validate(newCert); } catch { throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Failed to verify integrity of the new certificate and the issuer list."); } } var updateCertificate = new UpdateCertificateData(); try { var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; switch (privateKeyFormat) { case null: case "": { X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPrivateKey(newCert, certWithPrivateKey); break; } case "PFX": { X509Certificate2 certWithPrivateKey = X509Utils.CreateCertificateFromPKCS12(privateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate)); updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPrivateKey(newCert, certWithPrivateKey); break; } case "PEM": { updateCertificate.CertificateWithPrivateKey = CertificateFactory.CreateCertificateWithPEMPrivateKey(newCert, privateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate)); break; } } updateCertificate.IssuerCollection = newIssuerCollection; updateCertificate.SessionId = context.SessionId; } catch { throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Failed to verify integrity of the new certificate and the private key."); } certificateGroup.UpdateCertificate = updateCertificate; applyChangesRequired = true; if (updateCertificate != null) { try { using (ICertificateStore appStore = CertificateStoreIdentifier.OpenStore(certificateGroup.ApplicationCertificate.StorePath)) { Utils.Trace(Utils.TraceMasks.Security, "Delete application certificate {0}", certificateGroup.ApplicationCertificate.Thumbprint); appStore.Delete(certificateGroup.ApplicationCertificate.Thumbprint).Wait(); Utils.Trace(Utils.TraceMasks.Security, "Add new application certificate {0}", updateCertificate.CertificateWithPrivateKey); var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; appStore.Add(updateCertificate.CertificateWithPrivateKey, passwordProvider?.GetPassword(certificateGroup.ApplicationCertificate)).Wait(); // keep only track of cert without private key var certOnly = new X509Certificate2(updateCertificate.CertificateWithPrivateKey.RawData); updateCertificate.CertificateWithPrivateKey.Dispose(); updateCertificate.CertificateWithPrivateKey = certOnly; } using (ICertificateStore issuerStore = CertificateStoreIdentifier.OpenStore(certificateGroup.IssuerStorePath)) { foreach (var issuer in updateCertificate.IssuerCollection) { try { Utils.Trace(Utils.TraceMasks.Security, "Add new issuer certificate {0}", issuer); issuerStore.Add(issuer).Wait(); } catch (ArgumentException) { // ignore error if issuer cert already exists } } } } catch (Exception ex) { Utils.Trace(Utils.TraceMasks.Security, ServiceResult.BuildExceptionTrace(ex)); throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Failed to update certificate.", ex); } } return ServiceResult.Good; } private ServiceResult CreateSigningRequest( ISystemContext context, MethodState method, NodeId objectId, NodeId certificateGroupId, NodeId certificateTypeId, string subjectName, bool regeneratePrivateKey, byte[] nonce, ref byte[] certificateRequest) { HasApplicationSecureAdminAccess(context); ServerCertificateGroup certificateGroup = VerifyGroupAndTypeId(certificateGroupId, certificateTypeId); if (!String.IsNullOrEmpty(subjectName)) { throw new ArgumentException(nameof(subjectName)); } // TODO: implement regeneratePrivateKey // TODO: use nonce for generating the private key var passwordProvider = m_configuration.SecurityConfiguration.CertificatePasswordProvider; X509Certificate2 certWithPrivateKey = certificateGroup.ApplicationCertificate.LoadPrivateKeyEx(passwordProvider).Result; certificateRequest = CertificateFactory.CreateSigningRequest(certWithPrivateKey, X509Utils.GetDomainsFromCertficate(certWithPrivateKey)); return ServiceResult.Good; } private ServiceResult ApplyChanges( ISystemContext context, MethodState method, IList inputArguments, IList outputArguments) { HasApplicationSecureAdminAccess(context); bool disconnectSessions = false; foreach (var certificateGroup in m_certificateGroups) { try { var updateCertificate = certificateGroup.UpdateCertificate; if (updateCertificate != null) { disconnectSessions = true; Utils.Trace((int)Utils.TraceMasks.Security, $"Apply Changes for certificate {updateCertificate.CertificateWithPrivateKey}"); } } finally { certificateGroup.UpdateCertificate = null; } } if (disconnectSessions) { Task.Run(async () => { Utils.Trace((int)Utils.TraceMasks.Security, $"Apply Changes for application certificate update."); // give the client some time to receive the response // before the certificate update may disconnect all sessions await Task.Delay(1000).ConfigureAwait(false); await m_configuration.CertificateValidator.UpdateCertificate(m_configuration.SecurityConfiguration); } ); } return StatusCodes.Good; } private ServiceResult GetRejectedList( ISystemContext context, MethodState method, NodeId objectId, ref byte[][] certificates) { HasApplicationSecureAdminAccess(context); using (ICertificateStore store = CertificateStoreIdentifier.OpenStore(m_rejectedStorePath)) { X509Certificate2Collection collection = store.Enumerate().Result; List rawList = new List(); foreach (var cert in collection) { rawList.Add(cert.RawData); } certificates = rawList.ToArray(); } return StatusCodes.Good; } private ServerCertificateGroup VerifyGroupAndTypeId( NodeId certificateGroupId, NodeId certificateTypeId ) { // verify typeid must be set if (NodeId.IsNull(certificateTypeId)) { throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Certificate type not specified."); } // verify requested certificate group if (NodeId.IsNull(certificateGroupId)) { certificateGroupId = ObjectIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup; } ServerCertificateGroup certificateGroup = m_certificateGroups.FirstOrDefault(group => Utils.IsEqual(group.NodeId, certificateGroupId)); if (certificateGroup == null) { throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Certificate group invalid."); } // verify certificate type bool foundCertType = certificateGroup.CertificateTypes.Any(t => Utils.IsEqual(t, certificateTypeId)); if (!foundCertType) { throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Certificate type not valid for certificate group."); } return certificateGroup; } /// /// Finds the node for the specified NamespaceUri. /// /// /// private NamespaceMetadataState FindNamespaceMetadataState(string namespaceUri) { try { // find ServerNamespaces node NamespacesState serverNamespacesNode = FindPredefinedNode(ObjectIds.Server_Namespaces, typeof(NamespacesState)) as NamespacesState; if (serverNamespacesNode == null) { Utils.Trace("Cannot find ObjectIds.Server_Namespaces node."); return null; } IList serverNamespacesChildren = new List(); serverNamespacesNode.GetChildren(SystemContext, serverNamespacesChildren); foreach (var namespacesReference in serverNamespacesChildren) { // Find NamespaceMetadata node of NamespaceUri in Namespaces children NamespaceMetadataState namespaceMetadata = namespacesReference as NamespaceMetadataState; if (namespaceMetadata == null) { continue; } if (namespaceMetadata.NamespaceUri.Value == namespaceUri) { return namespaceMetadata; } else { continue; } } IList serverNamespacesReferencs = new List(); serverNamespacesNode.GetReferences(SystemContext, serverNamespacesReferencs); foreach (IReference serverNamespacesReference in serverNamespacesReferencs) { if (serverNamespacesReference.IsInverse == false) { // Find NamespaceMetadata node of NamespaceUri in Namespaces references NodeId nameSpaceNodeId = ExpandedNodeId.ToNodeId(serverNamespacesReference.TargetId, Server.NamespaceUris); NamespaceMetadataState namespaceMetadata = FindNodeInAddressSpace(nameSpaceNodeId) as NamespaceMetadataState; if (namespaceMetadata == null) { continue; } if (namespaceMetadata.NamespaceUri.Value == namespaceUri) { return namespaceMetadata; } } } return null; } catch (Exception ex) { Utils.Trace(ex, "Error searching NamespaceMetadata for namespaceUri {0}.", namespaceUri); return null; } } /// /// Clear NamespaceMetadata nodes cache in case nodes are added or deleted /// private void ServerNamespacesChanged(ISystemContext context, NodeState node, NodeStateChangeMasks changes) { if ((changes & NodeStateChangeMasks.Children) != 0 || (changes & NodeStateChangeMasks.References) != 0) { try { lock (Lock) { m_namespaceMetadataStates.Clear(); } } catch { // ignore errors } } } #endregion #region Private Fields private class UpdateCertificateData { public NodeId SessionId; public X509Certificate2 CertificateWithPrivateKey; public X509Certificate2Collection IssuerCollection; } private class ServerCertificateGroup { public string BrowseName; public NodeId NodeId; public CertificateGroupState Node; public NodeId[] CertificateTypes; public CertificateIdentifier ApplicationCertificate; public string IssuerStorePath; public string TrustedStorePath; public UpdateCertificateData UpdateCertificate; } private ServerConfigurationState m_serverConfigurationNode; private ApplicationConfiguration m_configuration; private IList m_certificateGroups; private readonly string m_rejectedStorePath; private Dictionary m_namespaceMetadataStates = new Dictionary(); #endregion } }