/* ========================================================================
* 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.Linq;
using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua.Security.Certificates;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Opc.Ua.Server
{
///
/// The Server Configuration Node Manager.
///
public class ConfigurationNodeManager : DiagnosticsNodeManager, ICallAsyncNodeManager, IConfigurationNodeManager
{
///
/// Initializes the configuration and diagnostics manager.
///
public ConfigurationNodeManager(
IServerInternal server,
ApplicationConfiguration configuration)
: this(server, configuration, server.Telemetry.CreateLogger())
{
}
///
/// Initializes the configuration and diagnostics manager.
///
public ConfigurationNodeManager(
IServerInternal server,
ApplicationConfiguration configuration,
ILogger logger)
: base(server, configuration, logger)
{
string rejectedStorePath = configuration.SecurityConfiguration.RejectedCertificateStore?
.StorePath;
if (!string.IsNullOrEmpty(rejectedStorePath))
{
m_rejectedStore = new CertificateStoreIdentifier(rejectedStorePath);
}
m_certificateGroups = [];
m_configuration = configuration;
// TODO: configure cert groups in configuration
var defaultApplicationGroup = new ServerCertificateGroup
{
NodeId = ObjectIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup,
BrowseName = BrowseNames.DefaultApplicationGroup,
CertificateTypes = [],
ApplicationCertificates = [],
IssuerStore = new CertificateStoreIdentifier(
configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath
),
TrustedStore = new CertificateStoreIdentifier(
configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath)
};
m_certificateGroups.Add(defaultApplicationGroup);
if (configuration.SecurityConfiguration.UserIssuerCertificates != null &&
configuration.SecurityConfiguration.TrustedUserCertificates != null)
{
var defaultUserGroup = new ServerCertificateGroup
{
NodeId = ObjectIds.ServerConfiguration_CertificateGroups_DefaultUserTokenGroup,
BrowseName = BrowseNames.DefaultUserTokenGroup,
CertificateTypes = [],
ApplicationCertificates = [],
IssuerStore = new CertificateStoreIdentifier(
configuration.SecurityConfiguration.UserIssuerCertificates.StorePath
),
TrustedStore = new CertificateStoreIdentifier(
configuration.SecurityConfiguration.TrustedUserCertificates.StorePath)
};
m_certificateGroups.Add(defaultUserGroup);
}
ServerCertificateGroup defaultHttpsGroup = null;
if (configuration.SecurityConfiguration.HttpsIssuerCertificates != null &&
configuration.SecurityConfiguration.TrustedHttpsCertificates != null)
{
defaultHttpsGroup = new ServerCertificateGroup
{
NodeId = ObjectIds.ServerConfiguration_CertificateGroups_DefaultHttpsGroup,
BrowseName = BrowseNames.DefaultHttpsGroup,
CertificateTypes = [],
ApplicationCertificates = [],
IssuerStore = new CertificateStoreIdentifier(
configuration.SecurityConfiguration.HttpsIssuerCertificates.StorePath
),
TrustedStore = new CertificateStoreIdentifier(
configuration.SecurityConfiguration.TrustedHttpsCertificates.StorePath)
};
m_certificateGroups.Add(defaultHttpsGroup);
}
// For each certificate in ApplicationCertificates, add the certificate type to ServerConfiguration_CertificateGroups_DefaultApplicationGroup
// under the CertificateTypes field.
foreach (CertificateIdentifier cert in configuration.SecurityConfiguration
.ApplicationCertificates)
{
defaultApplicationGroup.CertificateTypes =
[
.. defaultApplicationGroup.CertificateTypes,
.. new NodeId[] { cert.CertificateType }
];
defaultApplicationGroup.ApplicationCertificates.Add(cert);
if (cert.CertificateType == ObjectTypeIds.HttpsCertificateType &&
defaultHttpsGroup != null)
{
defaultHttpsGroup.CertificateTypes =
[
.. defaultHttpsGroup.CertificateTypes,
.. new NodeId[] { cert.CertificateType }
];
defaultHttpsGroup.ApplicationCertificates.Add(cert);
}
}
}
///
/// Replaces the generic node with a node specific to the model.
///
protected override NodeState AddBehaviourToPredefinedNode(
ISystemContext context,
NodeState predefinedNode)
{
if (predefinedNode is BaseObjectState passiveNode)
{
NodeId typeId = passiveNode.TypeDefinitionId;
if (IsNodeIdInNamespace(typeId) && typeId.IdType == IdType.Numeric)
{
switch ((uint)typeId.Identifier)
{
case ObjectTypes.ServerConfigurationType:
{
var activeNode = new ServerConfigurationState(passiveNode.Parent);
activeNode.GetCertificates = new GetCertificatesMethodState(activeNode);
activeNode.Create(context, passiveNode);
m_serverConfigurationNode = activeNode;
// replace the node in the parent.
if (passiveNode.Parent != null)
{
passiveNode.Parent.ReplaceChild(context, activeNode);
}
else
{
NodeState serverNode = FindNodeInAddressSpace(ObjectIds.Server);
serverNode?.ReplaceChild(context, activeNode);
}
// remove the reference to server node because it is set as parent
activeNode.RemoveReference(
ReferenceTypeIds.HasComponent,
true,
ObjectIds.Server);
return activeNode;
}
case ObjectTypes.CertificateGroupFolderType:
{
var 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.
passiveNode.Parent?.ReplaceChild(context, activeNode);
return activeNode;
}
case ObjectTypes.CertificateGroupType:
{
ServerCertificateGroup result = m_certificateGroups
.FirstOrDefault(group =>
group.NodeId == passiveNode.NodeId);
if (result != null)
{
var activeNode = new CertificateGroupState(passiveNode.Parent);
activeNode.Create(context, passiveNode);
result.NodeId = activeNode.NodeId;
result.Node = activeNode;
// replace the node in the parent.
passiveNode.Parent?.ReplaceChild(context, activeNode);
return activeNode;
}
}
break;
}
}
}
return base.AddBehaviourToPredefinedNode(context, predefinedNode);
}
///
public void CreateServerConfiguration(
ServerSystemContext systemContext,
ApplicationConfiguration configuration)
{
// setup server configuration node
if (m_serverConfigurationNode.ApplicationUri != null)
{
m_serverConfigurationNode.ApplicationUri.Value = configuration.ApplicationUri;
}
if (m_serverConfigurationNode.ProductUri != null)
{
m_serverConfigurationNode.ProductUri.Value = configuration.ProductUri;
}
m_serverConfigurationNode.ServerCapabilities.Value =
[
.. configuration.ServerConfiguration.ServerCapabilities
];
m_serverConfigurationNode.ServerCapabilities.ValueRank = ValueRanks.OneDimension;
m_serverConfigurationNode.ServerCapabilities.ArrayDimensions
= new ReadOnlyList([0]);
m_serverConfigurationNode.SupportedPrivateKeyFormats.Value =
[
.. configuration.ServerConfiguration.SupportedPrivateKeyFormats
];
m_serverConfigurationNode.SupportedPrivateKeyFormats.ValueRank = ValueRanks
.OneDimension;
m_serverConfigurationNode.SupportedPrivateKeyFormats.ArrayDimensions
= new ReadOnlyList([0]);
m_serverConfigurationNode.MaxTrustListSize.Value = (uint)configuration
.ServerConfiguration
.MaxTrustListSize;
m_serverConfigurationNode.MulticastDnsEnabled.Value = configuration.ServerConfiguration
.MultiCastDnsEnabled;
m_serverConfigurationNode.UpdateCertificate.OnCallAsync
= new UpdateCertificateMethodStateMethodAsyncCallHandler(
UpdateCertificateAsync);
m_serverConfigurationNode.CreateSigningRequest.OnCallAsync =
new CreateSigningRequestMethodStateMethodAsyncCallHandler(CreateSigningRequestAsync);
m_serverConfigurationNode.ApplyChanges.OnCallMethod2
= new GenericMethodCalledEventHandler2(ApplyChanges);
m_serverConfigurationNode.GetRejectedList.OnCall
= new GetRejectedListMethodStateMethodCallHandler(
GetRejectedList);
m_serverConfigurationNode.GetCertificates.OnCall
= new GetCertificatesMethodStateMethodCallHandler(
GetCertificates);
m_serverConfigurationNode.ClearChangeMasks(systemContext, true);
// setup certificate group trust list handlers
foreach (ServerCertificateGroup certGroup in m_certificateGroups)
{
certGroup.Node.CertificateTypes.Value = certGroup.CertificateTypes;
certGroup.Node.TrustList.Handle = new TrustList(
certGroup.Node.TrustList,
certGroup.TrustedStore,
certGroup.IssuerStore,
new TrustList.SecureAccess(HasApplicationSecureAdminAccess),
new TrustList.SecureAccess(HasApplicationSecureAdminAccess),
Server.Telemetry,
m_configuration.ServerConfiguration.MaxTrustListSize);
certGroup.Node.ClearChangeMasks(systemContext, true);
}
// find ServerNamespaces node and subscribe to StateChanged
if (FindPredefinedNode(ObjectIds.Server_Namespaces)
is NamespacesState serverNamespacesNode)
{
serverNamespacesNode.StateChanged += ServerNamespacesChanged;
}
}
///
public NamespaceMetadataState GetNamespaceMetadataState(string namespaceUri)
{
if (namespaceUri == null)
{
return null;
}
if (m_namespaceMetadataStates.TryGetValue(
namespaceUri,
out NamespaceMetadataState value))
{
return value;
}
NamespaceMetadataState namespaceMetadataState = FindNamespaceMetadataState(
namespaceUri);
lock (Lock)
{
// remember the result for faster access.
m_namespaceMetadataStates[namespaceUri] = namespaceMetadataState;
}
return namespaceMetadataState;
}
///
public NamespaceMetadataState CreateNamespaceMetadataState(string namespaceUri)
{
NamespaceMetadataState namespaceMetadataState = FindNamespaceMetadataState(
namespaceUri);
if (namespaceMetadataState == null)
{
// find ServerNamespaces node
if (FindPredefinedNode(ObjectIds.Server_Namespaces)
is not NamespacesState serverNamespacesNode)
{
m_logger.LogError(
"Cannot create NamespaceMetadataState for namespace '{NamespaceUri}'.",
namespaceUri);
return null;
}
// create the NamespaceMetadata node
namespaceMetadataState = new NamespaceMetadataState(serverNamespacesNode)
{
BrowseName = new QualifiedName(namespaceUri, NamespaceIndex)
};
namespaceMetadataState.Create(
SystemContext,
default,
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;
}
///
public void HasApplicationSecureAdminAccess(ISystemContext context)
{
HasApplicationSecureAdminAccess(context, null);
}
///
public void HasApplicationSecureAdminAccess(
ISystemContext context,
CertificateStoreIdentifier trustedStore)
{
if (context is SessionSystemContext { OperationContext: OperationContext operationContext })
{
if (operationContext.ChannelContext?.EndpointDescription?.SecurityMode !=
MessageSecurityMode.SignAndEncrypt)
{
throw new ServiceResultException(
StatusCodes.BadUserAccessDenied,
"Access to this item is only allowed with MessageSecurityMode SignAndEncrypt.");
}
IUserIdentity identity = operationContext.UserIdentity;
// allow access to system configuration only with Role SecurityAdmin
if (identity == null ||
identity.TokenType == UserTokenType.Anonymous ||
!identity.GrantedRoleIds.Contains(ObjectIds.WellKnownRole_SecurityAdmin))
{
throw new ServiceResultException(
StatusCodes.BadUserAccessDenied,
"Security Admin Role required to access this item.");
}
}
}
private async ValueTask UpdateCertificateAsync(
ISystemContext context,
MethodState method,
NodeId objectId,
NodeId certificateGroupId,
NodeId certificateTypeId,
byte[] certificate,
byte[][] issuerCertificates,
string privateKeyFormat,
byte[] privateKey,
CancellationToken ct)
{
bool applyChangesRequired = false;
HasApplicationSecureAdminAccess(context);
object[] inputArguments =
[
certificateGroupId,
certificateTypeId,
certificate,
issuerCertificates,
privateKeyFormat,
privateKey
];
X509Certificate2 newCert = null;
X509Certificate2 certWithPrivateKey = null;
Server.ReportCertificateUpdateRequestedAuditEvent(
context,
objectId,
method,
inputArguments,
m_logger);
try
{
if (certificate == null)
{
throw new ArgumentNullException(nameof(certificate));
}
privateKeyFormat = privateKeyFormat?.ToUpperInvariant();
if (privateKeyFormat is not null and not "PEM" and not "PFX" and not "")
{
throw new ServiceResultException(
StatusCodes.BadNotSupported,
$"The private key format {privateKeyFormat} is not supported.");
}
ServerCertificateGroup certificateGroup = VerifyGroupAndTypeId(
certificateGroupId,
certificateTypeId);
certificateGroup.UpdateCertificate = null;
try
{
newCert = CertificateFactory.Create(certificate);
}
catch
{
throw new ServiceResultException(
StatusCodes.BadCertificateInvalid,
"Certificate data is invalid.");
}
// validate certificate type of new certificate
if (!CertificateIdentifier.ValidateCertificateType(newCert, certificateTypeId))
{
throw new ServiceResultException(
StatusCodes.BadCertificateInvalid,
"Certificate type of new certificate doesn't match the provided certificate type.");
}
// identify the existing certificate to be updated
// it should be of the same type and same subject name as the new certificate
CertificateIdentifier existingCertIdentifier =
(
certificateGroup.ApplicationCertificates.FirstOrDefault(cert =>
X509Utils.CompareDistinguishedName(cert.SubjectName, newCert.Subject) &&
cert.CertificateType == certificateTypeId)
?? certificateGroup.ApplicationCertificates.FirstOrDefault(cert =>
cert.Certificate != null &&
X509Utils.GetApplicationUrisFromCertificate(cert.Certificate)
.Any(uri => uri.Equals(m_configuration.ApplicationUri, StringComparison.Ordinal)) &&
cert.CertificateType == certificateTypeId))
?? throw new ServiceResultException(
StatusCodes.BadInvalidArgument,
"No existing certificate found for the specified certificate type and subject name.");
bool isApplicationCertificateGroup = Utils.IsEqual(
certificateGroup.NodeId,
ObjectIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup);
var newIssuerCollection = new X509Certificate2Collection();
try
{
// Application-group issuers must already be installed in the
// configured TrustList. Ignore supplied issuer bytes entirely.
if (!isApplicationCertificateGroup && issuerCertificates != null)
{
foreach (byte[] issuerRawCert in issuerCertificates)
{
newIssuerCollection.Add(CertificateFactory.Create(issuerRawCert));
}
}
}
catch
{
throw new ServiceResultException(
StatusCodes.BadCertificateInvalid,
"Issuer certificate data is invalid.");
}
// self signed
bool selfSigned = X509Utils.IsSelfSigned(newCert);
if (selfSigned && newIssuerCollection.Count != 0)
{
throw new ServiceResultException(
StatusCodes.BadCertificateInvalid,
"Issuer list not empty for self signed certificate.");
}
if (!selfSigned)
{
try
{
// Honor the server's certificate validation policy and use its
// configured trust and issuer stores for application certificates.
var certValidator = new CertificateValidator(Server.Telemetry);
await certValidator.UpdateAsync(
m_configuration.SecurityConfiguration,
m_configuration.ApplicationUri,
ct).ConfigureAwait(false);
if (newIssuerCollection.Count > 0)
{
// Preserve supplied-chain handling for other certificate groups.
var issuerStore = new CertificateTrustList();
var issuerCollection = new CertificateIdentifierCollection();
foreach (X509Certificate2 issuerCert in newIssuerCollection)
{
issuerCollection.Add(new CertificateIdentifier(issuerCert));
}
issuerStore.TrustedCertificates = issuerCollection;
certValidator.Update(issuerStore, issuerStore, null);
}
await certValidator.ValidateAsync(newCert, ct).ConfigureAwait(false);
}
catch (ServiceResultException)
{
throw;
}
catch (Exception ex)
{
m_logger.LogError(
Utils.TraceMasks.Security,
ex,
"Failed to verify integrity of the new certificate {Certificate} and the issuer list.",
newCert.AsLogSafeString());
throw new ServiceResultException(
StatusCodes.BadSecurityChecksFailed,
"Failed to verify integrity of the new certificate and the issuer list.",
ex);
}
}
var updateCertificate = new UpdateCertificateData
{
IssuerCollection = newIssuerCollection,
SessionId = (context as ISessionSystemContext)?.SessionId ?? default
};
try
{
ICertificatePasswordProvider passwordProvider = m_configuration
.SecurityConfiguration
.CertificatePasswordProvider;
switch (privateKeyFormat)
{
case null:
case "":
for (int attempt = 0; ; attempt++)
{
X509Certificate2 exportableKey;
// use the new generated private key if one exists and matches the provided public key
if (certificateGroup.TemporaryApplicationCertificate != null &&
X509Utils.VerifyKeyPair(
newCert,
certificateGroup.TemporaryApplicationCertificate))
{
exportableKey = X509Utils.CreateCopyWithPrivateKey(
certificateGroup.TemporaryApplicationCertificate,
false);
}
else
{
certWithPrivateKey = await existingCertIdentifier
.LoadPrivateKeyExAsync(
passwordProvider,
m_configuration.ApplicationUri,
Server.Telemetry,
ct)
.ConfigureAwait(false);
if (certWithPrivateKey == null)
{
throw new ServiceResultException(
StatusCodes.BadSecurityChecksFailed,
"A private key was not found");
}
exportableKey = X509Utils.CreateCopyWithPrivateKey(
certWithPrivateKey,
false);
}
// On macOS combine the new certificate with a private copy of
// the key. CopyWithPrivateKey shares the native key handle with
// its source; disposing the combined certificate later would
// invalidate that key for RSA-PSS, and the source is the
// certificate new channels use until ApplyChanges reloads it.
// Linux keys are in memory and Windows already has a detached
// copy, so those platforms keep using the source as is.
using (X509Certificate2 keyCopy = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? CreateDetachedKeyCopy(exportableKey)
: null)
{
updateCertificate.CertificateWithPrivateKey =
CertificateFactory.CreateCertificateWithPrivateKey(
newCert,
keyCopy ?? exportableKey);
try
{
await UpdateCertificateInternalAsync(
certificateGroup,
existingCertIdentifier,
updateCertificate, ct).ConfigureAwait(false);
break;
}
catch (Exception ex) when (ShouldRetry(attempt, ex))
{
// release the failed attempt's certificate; the retry creates a new one
updateCertificate.CertificateWithPrivateKey.Dispose();
updateCertificate.CertificateWithPrivateKey = null;
m_logger.LogDebug(
Utils.TraceMasks.Security,
ex,
"Failed to update certificate {Certificate}. Retrying...",
newCert.AsLogSafeString());
}
}
}
break;
case "PFX":
for (int attempt = 0; ; attempt++)
{
certWithPrivateKey = X509Utils.CreateCertificateFromPKCS12(
privateKey,
passwordProvider?.GetPassword(existingCertIdentifier),
#if !NET9_0_OR_GREATER
// https://github.com/OPCFoundation/UA-.NETStandard/commit/0b24d62b7c2bab2e5ed08e694103d49278e457af
// CopyWithPrivateKey apparently does not support ephimeralkeysets on windows
RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
#else // But it seems to work on .net 9 - and we prefer that over files
false);
#endif
updateCertificate.CertificateWithPrivateKey =
CertificateFactory.CreateCertificateWithPrivateKey(
newCert,
certWithPrivateKey);
try
{
await UpdateCertificateInternalAsync(
certificateGroup,
existingCertIdentifier,
updateCertificate, ct).ConfigureAwait(false);
break;
}
catch (Exception ex) when (ShouldRetry(attempt, ex))
{
// release the failed attempt's certificate; the retry creates a new one
updateCertificate.CertificateWithPrivateKey.Dispose();
updateCertificate.CertificateWithPrivateKey = null;
m_logger.LogDebug(
Utils.TraceMasks.Security,
ex,
"Failed to update certificate {Certificate} with PFX private key. Retrying...",
newCert.AsLogSafeString());
}
}
break;
case "PEM":
for (int attempt = 0; ; attempt++)
{
updateCertificate.CertificateWithPrivateKey =
CertificateFactory.CreateCertificateWithPEMPrivateKey(
newCert,
privateKey,
passwordProvider?.GetPassword(existingCertIdentifier));
try
{
await UpdateCertificateInternalAsync(
certificateGroup,
existingCertIdentifier,
updateCertificate, ct).ConfigureAwait(false);
break;
}
catch (Exception ex) when (ShouldRetry(attempt, ex))
{
// release the failed attempt's certificate; the retry creates a new one
updateCertificate.CertificateWithPrivateKey.Dispose();
updateCertificate.CertificateWithPrivateKey = null;
m_logger.LogDebug(
Utils.TraceMasks.Security,
ex,
"Failed to update certificate {Certificate} with PEM private key. Retrying...",
newCert.AsLogSafeString());
}
}
break;
}
}
catch (Exception ex) when (ex is not ServiceResultException)
{
throw new ServiceResultException(
StatusCodes.BadSecurityChecksFailed,
"Failed to verify integrity of the new certificate and the private key.", ex);
}
finally
{
// dispose temporary new private key as it is no longer needed
certificateGroup.TemporaryApplicationCertificate?.Dispose();
certificateGroup.TemporaryApplicationCertificate = null;
}
certificateGroup.UpdateCertificate = updateCertificate;
applyChangesRequired = true;
}
catch (Exception e)
{
// report the failure of UpdateCertificate via an audit event
Server.ReportCertificateUpdatedAuditEvent(
context,
objectId,
method,
inputArguments,
certificateGroupId,
certificateTypeId,
m_logger,
e);
// Raise audit certificate event
Server.ReportAuditCertificateEvent(newCert, e, m_logger);
throw;
}
return new UpdateCertificateMethodStateResult
{
ServiceResult = ServiceResult.Good,
ApplyChangesRequired = applyChangesRequired
};
static bool ShouldRetry(int attempt, Exception ex)
{
if (ex is ServiceResultException sre && sre.StatusCode == StatusCodes.BadConfigurationError)
{
return false;
}
const int maxAttempts = 3;
return attempt < maxAttempts;
}
// Handle the store update
async Task UpdateCertificateInternalAsync(
ServerCertificateGroup certificateGroup,
CertificateIdentifier existingCertIdentifier,
UpdateCertificateData updateCertificate,
CancellationToken ct)
{
try
{
using (ICertificateStore appStore = existingCertIdentifier.OpenStore(Server.Telemetry))
{
if (appStore == null)
{
throw ServiceResultException.ConfigurationError(
"Failed to open application certificate store.");
}
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Delete application certificate {Certificate}",
existingCertIdentifier.Certificate.AsLogSafeString());
await appStore.DeleteAsync(
existingCertIdentifier.Thumbprint,
ct)
.ConfigureAwait(false);
ICertificatePasswordProvider passwordProvider = m_configuration
.SecurityConfiguration
.CertificatePasswordProvider;
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Add new application certificate {Certificate}",
updateCertificate.CertificateWithPrivateKey.AsLogSafeString());
Debug.Assert(updateCertificate.CertificateWithPrivateKey.HasPrivateKey);
await appStore.AddAsync(
updateCertificate.CertificateWithPrivateKey,
passwordProvider?.GetPassword(existingCertIdentifier),
ct)
.ConfigureAwait(false);
// keep only track of cert without private key
X509Certificate2 certOnly = CertificateFactory.Create(
updateCertificate.CertificateWithPrivateKey.RawData);
updateCertificate.CertificateWithPrivateKey.Dispose();
updateCertificate.CertificateWithPrivateKey = certOnly;
// update certificate identifier with new certificate
await existingCertIdentifier.FindAsync(
m_configuration.ApplicationUri,
Server.Telemetry,
ct)
.ConfigureAwait(false);
}
ICertificateStore issuerStore = certificateGroup.IssuerStore.OpenStore(Server.Telemetry);
try
{
if (issuerStore == null)
{
throw ServiceResultException.ConfigurationError(
"Failed to open issuer certificate store.");
}
foreach (X509Certificate2 issuer in updateCertificate.IssuerCollection)
{
try
{
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Add new issuer certificate {Certificate}",
issuer.AsLogSafeString());
await issuerStore.AddAsync(issuer, ct: ct).ConfigureAwait(false);
}
catch (ArgumentException)
{
// ignore error if issuer cert already exists
}
}
}
finally
{
issuerStore?.Close();
}
Server.ReportCertificateUpdatedAuditEvent(
context,
objectId,
method,
inputArguments,
certificateGroupId,
certificateTypeId,
m_logger);
}
catch (Exception ex)
{
m_logger.LogError(
Utils.TraceMasks.Security,
ex,
"Failed to update certificate {Certificate}.",
newCert.AsLogSafeString());
throw new ServiceResultException(
StatusCodes.BadSecurityChecksFailed,
"Failed to update certificate.",
ex);
}
}
}
private async ValueTask CreateSigningRequestAsync(
ISystemContext context,
MethodState method,
NodeId objectId,
NodeId certificateGroupId,
NodeId certificateTypeId,
string subjectName,
bool regeneratePrivateKey,
byte[] nonce,
CancellationToken cancellationToken)
{
HasApplicationSecureAdminAccess(context);
ServerCertificateGroup certificateGroup = VerifyGroupAndTypeId(
certificateGroupId,
certificateTypeId);
// identify the existing certificate for which to CreateSigningRequest
// it should be of the same type
CertificateIdentifier existingCertIdentifier = certificateGroup.ApplicationCertificates
.FirstOrDefault(
cert => cert.CertificateType == certificateTypeId);
if (string.IsNullOrEmpty(subjectName))
{
subjectName = existingCertIdentifier.Certificate.Subject;
}
certificateGroup.TemporaryApplicationCertificate?.Dispose();
certificateGroup.TemporaryApplicationCertificate = null;
X509Certificate2 certWithPrivateKey;
if (regeneratePrivateKey)
{
IList domainNames = X509Utils.GetDomainsFromCertificate(existingCertIdentifier.Certificate);
certWithPrivateKey = GenerateTemporaryApplicationCertificate(
certificateTypeId,
certificateGroup,
subjectName,
domainNames);
}
else
{
ICertificatePasswordProvider passwordProvider = m_configuration
.SecurityConfiguration
.CertificatePasswordProvider;
certWithPrivateKey = await existingCertIdentifier
.LoadPrivateKeyExAsync(passwordProvider,
m_configuration.ApplicationUri,
Server.Telemetry,
cancellationToken)
.ConfigureAwait(false);
if (certWithPrivateKey == null)
{
throw ServiceResultException.Create(StatusCodes.BadInternalError, "Failed to load private key");
}
}
m_logger.LogInformation(
Utils.TraceMasks.Security,
"Create signing request {Certificate}",
certWithPrivateKey.AsLogSafeString());
byte[] certificateRequest = CertificateFactory.CreateSigningRequest(
certWithPrivateKey,
X509Utils.GetDomainsFromCertificate(certWithPrivateKey));
return new CreateSigningRequestMethodStateResult
{
ServiceResult = ServiceResult.Good,
CertificateRequest = certificateRequest
};
}
private X509Certificate2 GenerateTemporaryApplicationCertificate(
NodeId certificateTypeId,
ServerCertificateGroup certificateGroup,
string subjectName,
IList domainNames)
{
X509Certificate2 certificate;
ICertificateBuilder certificateBuilder = CertificateFactory
.CreateCertificate(m_configuration.ApplicationUri, m_configuration.ApplicationName, subjectName, domainNames)
.SetNotBefore(DateTime.Today.AddDays(-1))
.SetNotAfter(DateTime.Today.AddDays(14));
if (certificateTypeId == null ||
certificateTypeId == ObjectTypeIds.ApplicationCertificateType ||
certificateTypeId == ObjectTypeIds.RsaMinApplicationCertificateType ||
certificateTypeId == ObjectTypeIds.RsaSha256ApplicationCertificateType)
{
certificate = certificateBuilder.SetRSAKeySize(CertificateFactory.DefaultKeySize)
.CreateForRSA();
}
else
{
ECCurve? curve =
EccUtils.GetCurveFromCertificateTypeId(certificateTypeId)
?? throw new ServiceResultException(
StatusCodes.BadNotSupported,
"The Ecc certificate type is not supported.");
certificate = certificateBuilder.SetECCurve(curve.Value).CreateForECDsa();
}
certificateGroup.TemporaryApplicationCertificate = certificate;
return certificate;
}
///
/// Returns a copy of a certificate whose private key is independent
/// of the source, or null if the key cannot be exported.
///
///
/// X509Certificate2.CopyWithPrivateKey does not deep copy the
/// key: the result shares the native key handle with the certificate
/// the key came from. On macOS the stack loads keys into a temporary
/// keychain, and disposing a certificate that shares such a key leaves
/// the source unable to export key parameters, which RSA-PSS signing
/// needs (OSStatus -50). Round-tripping the source through an
/// in-memory PFX gives the copy a key of its own, the technique
/// already uses on
/// Windows. A key that cannot be exported (TPM, HSM, PKCS#11) is not
/// affected, so null is returned and the caller uses the source as is.
/// The caller decides on which platform a copy is needed and verifies
/// the key pair when it combines the copy with the new certificate.
///
private static X509Certificate2 CreateDetachedKeyCopy(
X509Certificate2 certificateWithPrivateKey)
{
char[] passcode = X509Utils.GeneratePasscode();
byte[] pfx = null;
try
{
using var securePasscode = new SecureString();
foreach (char c in passcode)
{
securePasscode.AppendChar(c);
}
securePasscode.MakeReadOnly();
try
{
pfx = certificateWithPrivateKey.Export(X509ContentType.Pfx, securePasscode);
}
catch (CryptographicException)
{
// not exportable: nothing to copy
return null;
}
return X509CertificateLoader.LoadPkcs12(
pfx,
passcode,
X509KeyStorageFlags.Exportable);
}
finally
{
if (pfx != null)
{
Array.Clear(pfx, 0, pfx.Length);
}
Array.Clear(passcode, 0, passcode.Length);
}
}
private ServiceResult ApplyChanges(
ISystemContext context,
MethodState method,
NodeId objectId,
IList