Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions Stack/Opc.Ua.Core/Security/Certificates/CertificateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,11 @@ private static void SetSuitableDefaults(
subjectName = Utils.Format("CN={0}", subjectName);
}

if (!subjectName.Contains("O=", StringComparison.Ordinal))
{
subjectName += Utils.Format(", O={0}", "OPC Foundation");
}

if (domainNames != null && domainNames.Count > 0)
{
if (!subjectName.Contains("DC=", StringComparison.Ordinal) &&
Expand Down
96 changes: 89 additions & 7 deletions Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -364,6 +365,37 @@ private static string GetDisplayName(X509Certificate2 certificate)
return buffer.ToString();
}

/// <summary>
/// Picks the certificate with the longest duration.
/// If multiple certificates have the same duration, pick the one with the latest NotAfter date.
/// </summary>
/// <param name="collection"></param>
/// <returns></returns>
static X509Certificate2 PickLongestDurationValidCerts(X509Certificate2Collection collection)
{
X509Certificate2 bestMatch = null;
TimeSpan bestAvailability = TimeSpan.MinValue;
DateTime bestNotAfter = DateTime.MinValue;

// Filter Valid certificates by time
X509Certificate2Collection validCertificates = collection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);

foreach (X509Certificate2 certificate in validCertificates)
{
TimeSpan availability = certificate.NotAfter - certificate.NotBefore;

if (availability > bestAvailability ||
(availability == bestAvailability && certificate.NotAfter > bestNotAfter))
{
bestMatch = certificate;
bestAvailability = availability;
bestNotAfter = certificate.NotAfter;
}
}

return bestMatch;
}

/// <summary>
/// Finds a certificate in the specified collection.
/// </summary>
Expand Down Expand Up @@ -406,6 +438,9 @@ public static X509Certificate2 Find(

return null;
}

X509Certificate2Collection matchesOnCriteria = null;

// find by subject name.
if (!string.IsNullOrEmpty(subjectName))
{
Expand All @@ -418,19 +453,62 @@ public static X509Certificate2 Find(
{
if (!needPrivateKey || certificate.HasPrivateKey)
{
return certificate;
(matchesOnCriteria ??= new X509Certificate2Collection()).Add(certificate);
}
}
}
if (matchesOnCriteria?.Count > 0)
{
return PickLongestDurationValidCerts(matchesOnCriteria);
}

collection = collection.Find(X509FindType.FindBySubjectName, subjectName, false);
bool hasCommonName = subjectName.IndexOf("CN=", StringComparison.OrdinalIgnoreCase) >= 0;

foreach (X509Certificate2 certificate in collection)
if (hasCommonName)
{
if (ValidateCertificateType(certificate, certificateType) &&
(!needPrivateKey || certificate.HasPrivateKey))
string commonNameEntry = subjectName2
.FirstOrDefault(s => s.StartsWith("CN=", StringComparison.OrdinalIgnoreCase));
string commonName = commonNameEntry?.Length > 3
? commonNameEntry.Substring(3).Trim()
: null;

if (!string.IsNullOrEmpty(commonName))
{
foreach (X509Certificate2 certificate in collection)
{
if (ValidateCertificateType(certificate, certificateType) &&
(!needPrivateKey || certificate.HasPrivateKey) &&
string.Equals(
certificate.GetNameInfo(X509NameType.SimpleName, false),
commonName,
StringComparison.Ordinal))
{
(matchesOnCriteria ??= new X509Certificate2Collection()).Add(certificate);
}
}
if (matchesOnCriteria?.Count > 0)
{
return PickLongestDurationValidCerts(matchesOnCriteria);
}
}
}
else
{
X509Certificate2Collection fuzzyMatches = collection.Find(
X509FindType.FindBySubjectName,
subjectName,
false);
foreach (X509Certificate2 certificate in fuzzyMatches)
{
return certificate;
if (ValidateCertificateType(certificate, certificateType) &&
(!needPrivateKey || certificate.HasPrivateKey))
{
(matchesOnCriteria ??= new X509Certificate2Collection()).Add(certificate);
}
}
if (matchesOnCriteria?.Count > 0)
{
return PickLongestDurationValidCerts(matchesOnCriteria);
}
}
}
Expand All @@ -444,9 +522,13 @@ public static X509Certificate2 Find(
ValidateCertificateType(certificate, certificateType) &&
(!needPrivateKey || certificate.HasPrivateKey))
{
return certificate;
(matchesOnCriteria ??= new X509Certificate2Collection()).Add(certificate);
}
}
if (matchesOnCriteria?.Count > 0)
{
return PickLongestDurationValidCerts(matchesOnCriteria);
}
}

// certificate not found.
Expand Down
64 changes: 64 additions & 0 deletions Tests/Opc.Ua.Configuration.Tests/ApplicationInstanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,70 @@ await applicationInstance
Assert.IsTrue(storedCertificates.Contains(cert));
}

/// <summary>
/// This tests that instantiating two application instances the second with a SubjectName being a substring of the first one's CN,
/// succeeds without throwing exceptions.
/// </summary>
/// <returns></returns>
[Test]
public async Task TestAddTwoAppCertificatesToTrustedStoreAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();

var subjectName = SubjectName;
//Arrange Application Instance
var applicationInstance = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName };
ApplicationConfiguration configuration = await applicationInstance
.Build(ApplicationUri, ProductUri)
.AsClient()
.AddSecurityConfigurationStores(subjectName,
$"{m_pkiRoot}/pki/own",
$"{m_pkiRoot}/pki/rusted",
$"{m_pkiRoot}/pki/issuer",
$"{m_pkiRoot}/pki/rejected")
.CreateAsync()
.ConfigureAwait(false);

Assert.DoesNotThrowAsync(async () => await applicationInstance.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false));

subjectName = "UA";// UA is a substring of the previous certificate SubjectName CN
var applicationInstance2 = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName };
ApplicationConfiguration configuration2 = await applicationInstance2
.Build(ApplicationUri + "2", ProductUri + "2")
.AsClient()
.AddSecurityConfigurationStores(subjectName,
$"{m_pkiRoot}/pki/own",
$"{m_pkiRoot}/pki/trusted",
$"{m_pkiRoot}/pki/issuer",
$"{m_pkiRoot}/pki/rejected")
.CreateAsync()
.ConfigureAwait(false);

// Since the SubjectName is a substring of the first one's CN,
// the matching algorithm will find the first certificate because a fuzzy match is done on the SubjectName when SubjectName does not contain CN=.
// However, since the ApplicationUri is different, the certificate will be considered invalid
ServiceResultException exception = NUnit.Framework.Assert
.ThrowsAsync<ServiceResultException>(async () =>
await applicationInstance2.CheckApplicationInstanceCertificatesAsync(true)
.ConfigureAwait(false));
Assert.AreEqual(StatusCodes.BadConfigurationError, exception.StatusCode);
subjectName = "CN=UA";// UA is a substring of the previous certificate SubjectName CN
var applicationInstance3 = new ApplicationInstance(telemetry) { ApplicationName = ApplicationName };
var configuration3 = await applicationInstance3
.Build(ApplicationUri + "3", ProductUri + "3")
.AsClient()
.AddSecurityConfigurationStores(subjectName,
$"{m_pkiRoot}/pki/own",
$"{m_pkiRoot}/pki/trusted",
$"{m_pkiRoot}/pki/issuer",
$"{m_pkiRoot}/pki/rejected")
.CreateAsync()
.ConfigureAwait(false);

// Since the SubjectName contains CN=UA, the matching algorithm will not do a fuzzy match and will not find the first certificate.
Assert.DoesNotThrowAsync(async () => await applicationInstance3.CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false));
}

/// <summary>
/// Test to verify that a new cert is not recreated/replaced if DisableCertificateAutoCreation is set.
/// </summary>
Expand Down
Loading
Loading