Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,15 @@ public IApplicationConfigurationBuilderSecurityOptions SetSuppressNonceValidatio
return this;
}

/// <inheritdoc/>
public IApplicationConfigurationBuilderSecurityOptions SetRejectCertificateUriMismatch(
bool rejectCertificateUriMismatch)
{
ApplicationConfiguration.SecurityConfiguration.RejectCertificateUriMismatch =
rejectCertificateUriMismatch;
return this;
}

/// <inheritdoc/>
public IApplicationConfigurationBuilderSecurityOptions SetSendCertificateChain(
bool sendCertificateChain)
Expand Down
53 changes: 51 additions & 2 deletions Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ public async ValueTask<bool> CheckApplicationInstanceCertificatesAsync(
"Need at least one Application Certificate.");
}

// Note: The FindAsync method searches certificates in this order: thumbprint, subjectName, then applicationUri.
// When SubjectName or Thumbprint is specified, certificates may be loaded even if their ApplicationUri
// doesn't match ApplicationConfiguration.ApplicationUri. Therefore, we must explicitly validate that
// all loaded certificates have the same ApplicationUri.
bool result = true;
foreach (CertificateIdentifier certId in securityConfiguration.ApplicationCertificates)
{
Expand All @@ -372,11 +376,47 @@ public async ValueTask<bool> CheckApplicationInstanceCertificatesAsync(
result = result && nextResult;
}

// When there are multiple certificates, validate they all have the same ApplicationUri.
// Note: Individual certificate URI validation against the configuration happens in
// CheckApplicationInstanceCertificateAsync (called via CheckCertificateTypeAsync above).
// This additional check ensures consistency across multiple certificates.
if (securityConfiguration.ApplicationCertificates.Count > 1)
{
string firstApplicationUri = null;
foreach (CertificateIdentifier certId in securityConfiguration.ApplicationCertificates)
{
if (certId.Certificate != null)
{
string certApplicationUri = X509Utils.GetApplicationUriFromCertificate(certId.Certificate);

if (!string.IsNullOrEmpty(certApplicationUri))
{
if (firstApplicationUri == null)
{
firstApplicationUri = certApplicationUri;
}
else if (!firstApplicationUri.Equals(certApplicationUri, StringComparison.Ordinal))
{
// Multiple certificates with different URIs - always reject
throw ServiceResultException.Create(
StatusCodes.BadCertificateUriInvalid,
"All application certificates must have the same ApplicationUri. Certificate with subject '{0}' has URI '{1}' but expected '{2}'.",
certId.Certificate.Subject,
certApplicationUri,
firstApplicationUri);
}
}
}
}
}

return result;
}

/// <summary>
/// Check certificate type
/// Check certificate type.
/// Note: FindAsync searches certificates in order: thumbprint, subjectName, applicationUri.
/// The applicationUri parameter is only used if thumbprint and subjectName don't find a match.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private async Task<bool> CheckCertificateTypeAsync(
Expand All @@ -402,7 +442,7 @@ private async Task<bool> CheckCertificateTypeAsync(
await id.LoadPrivateKeyExAsync(passwordProvider, configuration.ApplicationUri, m_telemetry, ct)
.ConfigureAwait(false);

// load the certificate
// load the certificate
X509Certificate2 certificate = await id.FindAsync(
true,
configuration.ApplicationUri,
Expand Down Expand Up @@ -752,6 +792,15 @@ await configuration
}
else if (!configuration.ApplicationUri.Equals(applicationUri, StringComparison.Ordinal))
{
if (configuration.SecurityConfiguration.RejectCertificateUriMismatch)
{
throw ServiceResultException.Create(
StatusCodes.BadCertificateUriInvalid,
"The URI specified in the ApplicationConfiguration {0} does not match the URI in the Certificate: {1}.",
configuration.ApplicationUri,
applicationUri);
}

m_logger.LogInformation(
"Updated the ApplicationUri: {PreviousApplicationUri} --> {NewApplicationUri}",
configuration.ApplicationUri,
Expand Down
10 changes: 10 additions & 0 deletions Libraries/Opc.Ua.Configuration/IApplicationConfigurationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,16 @@ IApplicationConfigurationBuilderSecurityOptions SetUseValidatedCertificates(
IApplicationConfigurationBuilderSecurityOptions SetSuppressNonceValidationErrors(
bool suppressNonceValidationErrors);

/// <summary>
/// Whether to reject certificates with an ApplicationUri that does not match the configuration.
/// If set to true, an exception will be thrown when the ApplicationUri in the configuration
/// does not match the ApplicationUri in the certificate.
/// If set to false (default), the configuration ApplicationUri will be updated to match the certificate for backward compatibility.
/// </summary>
/// <param name="rejectCertificateUriMismatch"><see langword="true"/> to reject certificates with mismatched ApplicationUri.</param>
IApplicationConfigurationBuilderSecurityOptions SetRejectCertificateUriMismatch(
bool rejectCertificateUriMismatch);

/// <summary>
/// Whether a certificate chain should be sent with the application certificate.
/// Only used if the application certificate is CA signed.
Expand Down
36 changes: 33 additions & 3 deletions Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ private void Initialize()
AddAppCertToTrustedStore = true;
SendCertificateChain = true;
SuppressNonceValidationErrors = false;
RejectCertificateUriMismatch = false;
IsDeprecatedConfiguration = false;
}

Expand Down Expand Up @@ -736,13 +737,31 @@ public CertificateIdentifierCollection ApplicationCertificates
}
}

// Remove any duplicates
// Remove any duplicates based on thumbprint or subject name
// also since CertificateType can be null (implicitly RSA)
for (int i = 0; i < m_applicationCertificates.Count; i++)
{
for (int j = m_applicationCertificates.Count - 1; j > i; j--)
{
if (m_applicationCertificates[i]
.CertificateType == m_applicationCertificates[j].CertificateType)
// Compare by thumbprint (should always be present), otherwise by subject name and store path
bool isDuplicate = false;
if (!string.IsNullOrEmpty(m_applicationCertificates[i].Thumbprint) &&
!string.IsNullOrEmpty(m_applicationCertificates[j].Thumbprint))
{
isDuplicate = m_applicationCertificates[i].Thumbprint.Equals(
m_applicationCertificates[j].Thumbprint,
StringComparison.OrdinalIgnoreCase);
}
else if (!string.IsNullOrEmpty(m_applicationCertificates[i].SubjectName) &&
!string.IsNullOrEmpty(m_applicationCertificates[j].SubjectName))
{
isDuplicate = m_applicationCertificates[i].SubjectName.Equals(
m_applicationCertificates[j].SubjectName,
StringComparison.OrdinalIgnoreCase) &&
m_applicationCertificates[i].StorePath == m_applicationCertificates[j].StorePath;
}

if (isDuplicate)
{
m_applicationCertificates.RemoveAt(j);
}
Expand Down Expand Up @@ -927,6 +946,17 @@ public CertificateTrustList TrustedHttpsCertificates
[DataMember(IsRequired = false, EmitDefaultValue = false, Order = 21)]
public bool SuppressNonceValidationErrors { get; set; }

/// <summary>
/// Gets or sets a value indicating whether certificate ApplicationUri mismatches should be rejected.
/// </summary>
/// <remarks>
/// If set to true, the application will throw an exception when the ApplicationUri in the configuration
/// does not match the ApplicationUri in the certificate, similar to the server behavior for client certificates.
/// If set to false (default), the configuration ApplicationUri will be updated to match the certificate for backward compatibility.
/// </remarks>
[DataMember(IsRequired = false, EmitDefaultValue = false, Order = 22)]
public bool RejectCertificateUriMismatch { get; set; }

/// <summary>
/// The type of Configuration (deprecated or not)
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions Stack/Opc.Ua.Core/Types/Utils/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2790,6 +2790,19 @@ public static bool FindStringIgnoreCase(IList<string> strings, string target)
/// <param name="certificateType">The certificate type to check.</param>
public static bool IsSupportedCertificateType(NodeId certificateType)
{
// Handle null or NodeId.Null certificate types
// This occurs when:
// 1. CertificateIdentifier.CertificateType was never set (property is null)
// 2. GetCertificateType() returned NodeId.Null for unknown signature algorithms
//
// Return true (don't reject) to allow further certificate validation to proceed.
// The actual certificate signature and validity checks will still happen elsewhere.
// Older code that doesn't set CertificateType should still work.
if (certificateType == null || NodeId.IsNull(certificateType))
{
return true;
}

if (certificateType.Identifier is uint identifier)
{
switch (identifier)
Expand Down
Loading
Loading