Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 8 additions & 2 deletions Libraries/Opc.Ua.Client/ReverseConnectManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,21 @@ public Registration(
}

/// <summary>
/// Register with the server certificate.
/// Register with the server certificate to extract the application Uri.
/// </summary>
/// <remarks>
/// The first Uri in the subject alternate name field is considered the application Uri.
/// </remarks>
/// <param name="serverCertificate">The server certificate with the application Uri.</param>
/// <param name="endpointUrl">The endpoint Url of the server.</param>
/// <param name="onConnectionWaiting">The connection to use.</param>
public Registration(
X509Certificate2 serverCertificate,
Uri endpointUrl,
EventHandler<ConnectionWaitingEventArgs> onConnectionWaiting)
: this(endpointUrl, onConnectionWaiting)
{
ServerUri = X509Utils.GetApplicationUriFromCertificate(serverCertificate);
ServerUri = X509Utils.GetApplicationUrisFromCertificate(serverCertificate).FirstOrDefault();
}

private Registration(
Expand Down
43 changes: 16 additions & 27 deletions Libraries/Opc.Ua.Client/Session/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1707,8 +1707,6 @@ public async Task OpenAsync(

if (requireEncryption)
{
// validation skipped until IOP isses are resolved.
// ValidateServerCertificateApplicationUri(serverCertificate);
if (checkDomain)
{
await m_configuration
Expand Down Expand Up @@ -1836,6 +1834,8 @@ await m_configuration

ValidateServerEndpoints(serverEndpoints);

ValidateServerCertificateApplicationUri(serverCertificate, m_endpoint);

ValidateServerSignature(
serverCertificate,
serverSignature,
Expand Down Expand Up @@ -6352,31 +6352,6 @@ private void OpenValidateIdentity(
}
}

#if UNUSED
/// <summary>
/// Validates the ServerCertificate ApplicationUri to match the ApplicationUri of the Endpoint
/// for an open call (Spec Part 4 5.4.1)
/// </summary>
private void ValidateServerCertificateApplicationUri(X509Certificate2 serverCertificate)
{
string applicationUri = m_endpoint?.Description?.Server?.ApplicationUri;
//check is only neccessary if the ApplicatioUri is specified for the Endpoint
if (string.IsNullOrEmpty(applicationUri))
{
throw ServiceResultException.Create(
StatusCodes.BadSecurityChecksFailed,
"No ApplicationUri is specified for the server in the EndpointDescription.");
}
string certificateApplicationUri = X509Utils.GetApplicationUriFromCertificate(serverCertificate);
if (!string.Equals(certificateApplicationUri, applicationUri, StringComparison.Ordinal))
{
throw ServiceResultException.Create(
StatusCodes.BadSecurityChecksFailed,
"Server did not return a Certificate matching the ApplicationUri specified in the EndpointDescription.");
}
}
#endif

private void BuildCertificateData(
out byte[] clientCertificateData,
out byte[] clientCertificateChainData)
Expand Down Expand Up @@ -6489,6 +6464,20 @@ private void ValidateServerSignature(
}
}

/// <summary>
/// Validates the ServerCertificate ApplicationUri to match the ApplicationUri
/// of the Endpoint (Spec Part 4 5.4.1) returned by the CreateSessionResponse.
/// Ensure the endpoint was matched in <see cref="ValidateServerEndpoints"/>
/// with the applicationUri of the server description before the validation.
/// </summary>
private void ValidateServerCertificateApplicationUri(X509Certificate2 serverCertificate, ConfiguredEndpoint endpoint)
{
if (serverCertificate != null)
{
m_configuration.CertificateValidator.ValidateApplicationUri(serverCertificate, endpoint);
}
}

/// <summary>
/// Validates the server endpoints returned.
/// </summary>
Expand Down
60 changes: 39 additions & 21 deletions Libraries/Opc.Ua.Configuration/ApplicationInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,16 @@ 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, however each certificate is validated individually
// in CheckApplicationInstanceCertificateAsync (called via CheckOrCreateCertificateAsync) to ensure it contains
// the configuration's ApplicationUri.
bool result = true;
foreach (CertificateIdentifier certId in securityConfiguration.ApplicationCertificates)
{
ushort minimumKeySize = certId.GetMinKeySize(securityConfiguration);
bool nextResult = await CheckCertificateTypeAsync(
bool nextResult = await CheckOrCreateCertificateAsync(
certId,
silent,
minimumKeySize,
Expand All @@ -376,10 +381,14 @@ public async ValueTask<bool> CheckApplicationInstanceCertificatesAsync(
}

/// <summary>
/// Check certificate type
/// Checks, validates, and optionally creates an application certificate.
/// Loads the certificate, validates it against configured requirements (ApplicationUri, key size, domains),
/// and creates a new certificate if none exists and auto-creation is enabled.
/// Note: FindAsync searches certificates in order: thumbprint, subjectName, applicationUri.
/// The applicationUri parameter is only used if thumbprint and subjectName don't find a match.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private async Task<bool> CheckCertificateTypeAsync(
private async Task<bool> CheckOrCreateCertificateAsync(
CertificateIdentifier id,
bool silent,
ushort minimumKeySize,
Expand All @@ -402,7 +411,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 @@ -738,28 +747,37 @@ await configuration
return false;
}

// check uri.
string applicationUri = X509Utils.GetApplicationUriFromCertificate(certificate);

if (string.IsNullOrEmpty(applicationUri))
// Validate that the certificate contains the configuration's ApplicationUri
if (!X509Utils.CompareApplicationUriWithCertificate(certificate, configuration.ApplicationUri, out var certificateUris))
{
const string message =
"The Application URI could not be read from the certificate. Use certificate anyway?";
if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
if (certificateUris.Count == 0)
{
return false;
const string message =
"The Application URI could not be found in the certificate. Use certificate anyway?";
if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
return false;
}
}
else
{
string message = Utils.Format(
"The certificate with subject '{0}' does not contain the ApplicationUri '{1}' from the configuration. Certificate contains: {2}. Use certificate anyway?",
certificate.Subject,
configuration.ApplicationUri,
string.Join(", ", certificateUris));

if (!await ApproveMessageAsync(message, silent).ConfigureAwait(false))
{
return false;
}
}
}
else if (!configuration.ApplicationUri.Equals(applicationUri, StringComparison.Ordinal))
{
m_logger.LogInformation(
"Updated the ApplicationUri: {PreviousApplicationUri} --> {NewApplicationUri}",
configuration.ApplicationUri,
applicationUri);
configuration.ApplicationUri = applicationUri;
}

m_logger.LogInformation("Using the ApplicationUri: {ApplicationUri}", applicationUri);
m_logger.LogInformation(
"Certificate {Certificate} validated for ApplicationUri: {ApplicationUri}",
certificate.AsLogSafeString(),
configuration.ApplicationUri);

// update configuration.
id.Certificate = certificate;
Expand Down
3 changes: 2 additions & 1 deletion Libraries/Opc.Ua.Gds.Client.Common/CertificateWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Security.Cryptography.X509Certificates;

Expand Down Expand Up @@ -202,7 +203,7 @@ public string ApplicationUri
{
try
{
return X509Utils.GetApplicationUriFromCertificate(Certificate);
return X509Utils.GetApplicationUrisFromCertificate(Certificate).FirstOrDefault();
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,23 @@ public X509SubjectAltNameExtension(string applicationUri, IEnumerable<string> do
{
Oid = new Oid(SubjectAltName2Oid, kFriendlyName);
Critical = false;
Initialize(applicationUri, domainNames);
Initialize(new string[] { applicationUri }, domainNames);
RawData = Encode();
m_decoded = true;
}

/// <summary>
/// Build the Subject Alternative name extension (for OPC UA application certs).
/// </summary>
/// <param name="applicationUris">The application Uri</param>
/// <param name="domainNames">The domain names. DNS Hostnames, IPv4 or IPv6 addresses</param>
public X509SubjectAltNameExtension(
IEnumerable<string> applicationUris,
IEnumerable<string> domainNames)
{
Oid = new Oid(SubjectAltName2Oid, kFriendlyName);
Critical = false;
Initialize(applicationUris, domainNames);
RawData = Encode();
m_decoded = true;
}
Expand Down Expand Up @@ -408,14 +424,17 @@ private void Decode(byte[] data)
/// <summary>
/// Initialize the Subject Alternative name extension.
/// </summary>
/// <param name="applicationUri">The application Uri</param>
/// <param name="applicationUris">The application Uris</param>
/// <param name="generalNames">The general names. DNS Hostnames, IPv4 or IPv6 addresses</param>
private void Initialize(string applicationUri, IEnumerable<string> generalNames)
private void Initialize(IEnumerable<string> applicationUris, IEnumerable<string> generalNames)
{
var uris = new List<string>();
var domainNames = new List<string>();
var ipAddresses = new List<string>();
uris.Add(applicationUri);
foreach (string applicationUri in applicationUris)
{
uris.Add(applicationUri);
}
foreach (string generalName in generalNames)
{
switch (Uri.CheckHostName(generalName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,8 @@ private async ValueTask<UpdateCertificateMethodStateResult> UpdateCertificateAsy
cert.CertificateType == certificateTypeId)
?? certificateGroup.ApplicationCertificates.FirstOrDefault(cert =>
cert.Certificate != null &&
m_configuration.ApplicationUri ==
X509Utils.GetApplicationUriFromCertificate(cert.Certificate) &&
X509Utils.GetApplicationUrisFromCertificate(cert.Certificate)
.Any(uri => uri.Equals(m_configuration.ApplicationUri, StringComparison.Ordinal)) &&
cert.CertificateType == certificateTypeId))
?? throw new ServiceResultException(
StatusCodes.BadInvalidArgument,
Expand Down
40 changes: 18 additions & 22 deletions Libraries/Opc.Ua.Server/Server/StandardServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,31 +380,27 @@ X509Certificate2Collection clientCertificateChain

if (context.SecurityPolicyUri != SecurityPolicies.None)
{
string certificateApplicationUri = X509Utils
.GetApplicationUriFromCertificate(
parsedClientCertificate);

// verify if applicationUri from ApplicationDescription matches the applicationUri in the client certificate.
if (!string.IsNullOrEmpty(certificateApplicationUri) &&
!string.IsNullOrEmpty(clientDescription.ApplicationUri) &&
certificateApplicationUri != clientDescription.ApplicationUri)
// verify if applicationUri from ApplicationDescription matches the applicationUris in the client certificate.
if (!string.IsNullOrEmpty(clientDescription.ApplicationUri))
{
// report the AuditCertificateDataMismatch event for invalid uri
ServerInternal?.ReportAuditCertificateDataMismatchEvent(
parsedClientCertificate,
null,
clientDescription.ApplicationUri,
StatusCodes.BadCertificateUriInvalid,
m_logger);
if (!X509Utils.CompareApplicationUriWithCertificate(parsedClientCertificate, clientDescription.ApplicationUri))
{
// report the AuditCertificateDataMismatch event for invalid uri
ServerInternal?.ReportAuditCertificateDataMismatchEvent(
parsedClientCertificate,
null,
clientDescription.ApplicationUri,
StatusCodes.BadCertificateUriInvalid,
m_logger);

throw ServiceResultException.Create(
StatusCodes.BadCertificateUriInvalid,
"The URI specified in the ApplicationDescription {0} does not match the URIs in the Certificate.",
clientDescription.ApplicationUri);
}

throw ServiceResultException.Create(
StatusCodes.BadCertificateUriInvalid,
"The URI specified in the ApplicationDescription {0} does not match the URI in the Certificate: {1}.",
clientDescription.ApplicationUri,
certificateApplicationUri);
CertificateValidator.Validate(clientCertificateChain);
}

CertificateValidator.Validate(clientCertificateChain);
}
}
catch (Exception e)
Expand Down
51 changes: 38 additions & 13 deletions Stack/Opc.Ua.Core/Schema/ApplicationConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -722,33 +722,58 @@ public CertificateIdentifierCollection ApplicationCertificates
get => m_applicationCertificates;
set
{
m_applicationCertificates = value ?? [];
if (value == null || value.Count == 0)
{
m_applicationCertificates = new CertificateIdentifierCollection();
return;
}

IsDeprecatedConfiguration = false;
var newCertificates = new CertificateIdentifierCollection(value);

// Remove any unsupported certificate types.
for (int i = m_applicationCertificates.Count - 1; i >= 0; i--)
// Remove unsupported certificate types
for (int i = newCertificates.Count - 1; i >= 0; i--)
{
if (!Utils.IsSupportedCertificateType(
m_applicationCertificates[i].CertificateType))
if (!Utils.IsSupportedCertificateType(newCertificates[i].CertificateType))
{
m_applicationCertificates.RemoveAt(i);
// TODO: Log when ITelemetry instance is available
newCertificates.RemoveAt(i);
}
}

// Remove any duplicates
for (int i = 0; i < m_applicationCertificates.Count; i++)
// Remove any duplicates based on thumbprint
// Only perform duplicate detection if we have actual loaded certificates
for (int i = 0; i < newCertificates.Count; i++)
{
for (int j = m_applicationCertificates.Count - 1; j > i; j--)
for (int j = newCertificates.Count - 1; j > i; j--)
{
if (m_applicationCertificates[i]
.CertificateType == m_applicationCertificates[j].CertificateType)
bool isDuplicate = false;

// Only check for duplicates if both certificates are actually loaded
if (newCertificates[i].Certificate != null && newCertificates[j].Certificate != null)
{
// Compare by actual certificate thumbprint
isDuplicate = newCertificates[i].Certificate.Thumbprint.Equals(
newCertificates[j].Certificate.Thumbprint,
StringComparison.OrdinalIgnoreCase);
}
// If certificates aren't loaded yet, compare by explicit thumbprint configuration
else if (!string.IsNullOrEmpty(newCertificates[i].Thumbprint) &&
!string.IsNullOrEmpty(newCertificates[j].Thumbprint))
{
isDuplicate = newCertificates[i].Thumbprint.Equals(
newCertificates[j].Thumbprint,
StringComparison.OrdinalIgnoreCase);
}

if (isDuplicate)
{
m_applicationCertificates.RemoveAt(j);
newCertificates.RemoveAt(j);
}
}
}

m_applicationCertificates = newCertificates;

SupportedSecurityPolicies = BuildSupportedSecurityPolicies();
}
}
Expand Down
Loading
Loading