Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GDS] Add Method GetCertificates to GDS for Pull Support and ServerConfiguration for Push Support / Fix CheckRevocationStatus #2553

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 37 additions & 0 deletions Libraries/Opc.Ua.Gds.Client.Common/GlobalDiscoveryServerClient.cs
Expand Up @@ -584,6 +584,43 @@ public NodeId RegisterApplication(ApplicationRecordDataType application)
return null;
}

/// <summary>
/// Returns the Certificates assigned to Application and associated with the CertificateGroup.
/// </summary>
/// <param name="applicationId">The identifier assigned to the Application by the GDS.</param>
/// <param name="certificateGroupId">An identifier for the CertificateGroup that the Certificates belong to.
///If null, the CertificateManager shall return the Certificates for all CertificateGroups assigned to the Application.</param>
/// <param name="certificateTypeIds">The CertificateTypes that currently have a Certificate assigned.
/// The length of this list is the same as the length as certificates list.</param>
/// <param name="certificates">A list of DER encoded Certificates assigned to Application.
/// This list only includes Certificates that are currently valid.</param>
public void GetCertificates(
NodeId applicationId,
NodeId certificateGroupId,
out NodeId[] certificateTypeIds,
out byte[][] certificates)
{
certificateTypeIds = Array.Empty<NodeId>();
certificates = Array.Empty<byte[]>();

if (!IsConnected)
{
Connect();
}

var outputArguments = Session.Call(
ExpandedNodeId.ToNodeId(Opc.Ua.Gds.ObjectIds.Directory, Session.NamespaceUris),
ExpandedNodeId.ToNodeId(Opc.Ua.Gds.MethodIds.CertificateDirectoryType_GetCertificates, Session.NamespaceUris),
applicationId,
certificateGroupId);

if (outputArguments.Count >= 2)
{
certificateTypeIds = outputArguments[0] as NodeId[];
certificates = outputArguments[1] as byte[][];
}
}

/// <summary>
/// Checks the provided certificate for validity
/// </summary>
Expand Down
Expand Up @@ -561,6 +561,47 @@ public void RemoveCertificate(string thumbprint, bool isTrustedCertificate)
}
}

/// <summary>
/// returns the Certificates assigned to CertificateTypes associated with a CertificateGroup.
/// </summary>
/// <param name="certificateGroupId">The identifier for the CertificateGroup.</param>
/// <param name="certificateTypeIds">The CertificateTypes that currently have a Certificate assigned.
///The length of this list is the same as the length as certificates list.
///An empty list if the CertificateGroup does not have any CertificateTypes.</param>
/// <param name="certificates">A list of DER encoded Certificates assigned to CertificateGroup.
///The certificateType for the Certificate is specified by the corresponding element in the certificateTypes parameter.</param>
public void GetCertificates(
NodeId certificateGroupId,
out NodeId[] certificateTypeIds,
out byte[][] certificates)
{
certificateTypeIds = Array.Empty<NodeId>();
certificates = Array.Empty<byte[]>();
if (!IsConnected)
{
Connect();
}

IUserIdentity oldUser = ElevatePermissions();
try
{
var outputArguments = m_session.Call(
ExpandedNodeId.ToNodeId(Opc.Ua.ObjectIds.ServerConfiguration, m_session.NamespaceUris),
ExpandedNodeId.ToNodeId(Opc.Ua.MethodIds.ServerConfigurationType_GetCertificates, m_session.NamespaceUris),
certificateGroupId
);
if (outputArguments.Count >= 2)
{
certificateTypeIds = outputArguments[0] as NodeId[];
certificates = outputArguments[1] as byte[][];
}
}
finally
{
RevertPermissions(oldUser);
}
}

/// <summary>
/// Creates the CSR.
/// </summary>
Expand Down
92 changes: 76 additions & 16 deletions Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs
Expand Up @@ -31,12 +31,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Opc.Ua.Gds.Server.Database;
using Opc.Ua.Gds.Server.Diagnostics;
using Opc.Ua.Server;
using Org.BouncyCastle.Tls;

namespace Opc.Ua.Gds.Server
{
Expand Down Expand Up @@ -389,7 +391,8 @@ protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context
Opc.Ua.Gds.CertificateDirectoryState activeNode = new Opc.Ua.Gds.CertificateDirectoryState(passiveNode.Parent);

activeNode.RevokeCertificate = new RevokeCertificateMethodState(passiveNode);
activeNode.CheckRevocationStatus = new CheckRevocationStatusMethodState(passiveNode.Parent);
activeNode.CheckRevocationStatus = new CheckRevocationStatusMethodState(passiveNode);
activeNode.GetCertificates = new GetCertificatesMethodState(passiveNode);

activeNode.Create(context, passiveNode);
activeNode.QueryServers.OnCall = new QueryServersMethodStateMethodCallHandler(OnQueryServers);
Expand All @@ -407,6 +410,7 @@ protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context
activeNode.StartSigningRequest.OnCall = new StartSigningRequestMethodStateMethodCallHandler(OnStartSigningRequest);
activeNode.RevokeCertificate.OnCall = new RevokeCertificateMethodStateMethodCallHandler(OnRevokeCertificate);
activeNode.CheckRevocationStatus.OnCall = new CheckRevocationStatusMethodStateMethodCallHandler(OnCheckRevocationStatus);
activeNode.GetCertificates.OnCall = new GetCertificatesMethodStateMethodCallHandler(OnGetCertificates);

activeNode.CertificateGroups.DefaultApplicationGroup.CertificateTypes.Value = new NodeId[] { Opc.Ua.ObjectTypeIds.RsaSha256ApplicationCertificateType };
activeNode.CertificateGroups.DefaultApplicationGroup.TrustList.LastUpdateTime.Value = DateTime.UtcNow;
Expand Down Expand Up @@ -659,31 +663,87 @@ out nextRecordId
{
AuthorizationHelper.HasAuthenticatedSecureChannel(context);

//create CertificateValidator initialized with GDS CAs
var certificateValidator = new CertificateValidator();
var authorities = new CertificateTrustList() {
StorePath = m_globalDiscoveryServerConfiguration.AuthoritiesStorePath,
StoreType = CertificateStoreIdentifier.DetermineStoreType(m_globalDiscoveryServerConfiguration.AuthoritiesStorePath)
};
certificateValidator.Update(null, authorities, null);

//TODO return validityTime of Certificate once CertificateValidator supports it
//TODO return When the result expires and should be rechecked.
validityTime = DateTime.MinValue;

using (var x509 = new X509Certificate2(certificate))
try
{
try
//create chain to validate Certificate against it
var chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;

using (var x509 = new X509Certificate2(certificate))
{
certificateValidator.Validate(x509);
if (chain.Build(x509))
{
return ServiceResult.Good;
}
else
{
certificateStatus = StatusCodes.Bad;
}
}
catch (ServiceResultException se)
}
catch (CryptographicException)
{
certificateStatus = StatusCodes.Bad;
romanett marked this conversation as resolved.
Show resolved Hide resolved
}

return ServiceResult.Good;
}

private ServiceResult OnGetCertificates(
ISystemContext context,
MethodState method,
NodeId objectId,
NodeId applicationId,
NodeId certificateGroupId,
ref NodeId[] certificateTypeIds,
ref byte[][] certificates)
{
AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.CertificateAuthorityAdminOrSelfAdmin);

var certificateTypeIdsList = new List<NodeId>();
var certificatesList = new List<byte[]>();

if (m_database.GetApplication(applicationId) == null)
{
return new ServiceResult(StatusCodes.BadNotFound, "The ApplicationId does not refer to a registered application.");
}

//If CertificateGroupId is null, the CertificateManager shall return the Certificates for all CertificateGroups assigned to the Application.
if (certificateGroupId == null)
{
foreach (KeyValuePair<NodeId, string> certType in m_certTypeMap)
{
if (m_database.GetApplicationCertificate(applicationId, certType.Value, out byte[] certificate) && certificate != null)
{
certificateTypeIdsList.Add(certType.Key);
certificatesList.Add(certificate);
}
}
}
//get only Certificate of the provided CertificateGroup
else
{
if (!m_certificateGroups.TryGetValue(certificateGroupId, out ICertificateGroup certificateGroup)
|| !m_certTypeMap.TryGetValue(certificateGroup.CertificateType, out string certificateTypeId))
{
certificateStatus = se.StatusCode;
return new ServiceResult(StatusCodes.BadInvalidArgument, "The CertificateGroupId is not recognized or not valid for the Application.");
}
if (m_database.GetApplicationCertificate(applicationId, certificateTypeId, out byte[] certificate) && certificate != null)
{
certificateTypeIdsList.Add(certificateGroup.CertificateType);
certificatesList.Add(certificate);
}
}

certificates = certificatesList.ToArray();
certificateTypeIds = certificateTypeIdsList.ToArray();

return ServiceResult.Good;
}

private ServiceResult CheckHttpsDomain(ApplicationRecordDataType application, string commonName)
{
if (application.ApplicationType == ApplicationType.Client)
Expand Down
41 changes: 41 additions & 0 deletions Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs
Expand Up @@ -105,6 +105,9 @@
case ObjectTypes.ServerConfigurationType:
{
ServerConfigurationState activeNode = new ServerConfigurationState(passiveNode.Parent);

activeNode.GetCertificates = new GetCertificatesMethodState(activeNode);

activeNode.Create(context, passiveNode);

m_serverConfigurationNode = activeNode;
Expand Down Expand Up @@ -202,6 +205,7 @@
m_serverConfigurationNode.CreateSigningRequest.OnCall = new CreateSigningRequestMethodStateMethodCallHandler(CreateSigningRequest);
m_serverConfigurationNode.ApplyChanges.OnCallMethod = new GenericMethodCalledEventHandler(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
Expand All @@ -227,6 +231,8 @@
}
}



/// <summary>
/// Gets and returns the <see cref="NamespaceMetadataState"/> node associated with the specified NamespaceUri
/// </summary>
Expand Down Expand Up @@ -612,6 +618,41 @@
return StatusCodes.Good;
}

private ServiceResult GetCertificates(
ISystemContext context,
MethodState method,
NodeId objectId,
NodeId certificateGroupId,
ref NodeId[] certificateTypeIds,
ref byte[][] certificates)
{

Check warning on line 628 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L628

Added line #L628 was not covered by tests
HasApplicationSecureAdminAccess(context);

Check warning on line 630 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L630

Added line #L630 was not covered by tests
ServerCertificateGroup certificateGroup = m_certificateGroups.FirstOrDefault(group => Utils.IsEqual(group.NodeId, certificateGroupId));
if (certificateGroup == null)
{

Check warning on line 633 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L633

Added line #L633 was not covered by tests
throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Certificate group invalid.");
}

Check warning on line 636 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L636

Added line #L636 was not covered by tests
NodeId certificateTypeId = certificateGroup.CertificateTypes.FirstOrDefault();

//TODO support multiple Application Instance Certificates
if (certificateTypeId != null)
{
certificateTypeIds = new NodeId[1] {certificateTypeId };
certificates = new byte[1][];

Check warning on line 643 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L641-L643

Added lines #L641 - L643 were not covered by tests
certificates[0] = certificateGroup.ApplicationCertificate.Certificate.GetRawCertData();
}
else
{
certificateTypeIds = new NodeId[0];

Check warning on line 648 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L647-L648

Added lines #L647 - L648 were not covered by tests
certificates = new byte[0][];
}

Check warning on line 651 in Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs#L651

Added line #L651 was not covered by tests
return ServiceResult.Good;
}


private ServerCertificateGroup VerifyGroupAndTypeId(
NodeId certificateGroupId,
NodeId certificateTypeId
Expand Down
27 changes: 26 additions & 1 deletion Tests/Opc.Ua.Gds.Tests/ClientTest.cs
Expand Up @@ -853,6 +853,31 @@ public void FinishGoodSigningRequests()

}


[Test, Order(540)]
public void GetGoodCertificates()
{
AssertIgnoreTestWithoutGoodRegistration();
AssertIgnoreTestWithoutGoodNewKeyPairRequest();
ConnectGDS(true);

Assert.That(() => {
m_gdsClient.GDSClient.GetCertificates(null, null, out var _, out var _);
}, Throws.Exception);

foreach (var application in m_goodApplicationTestSet)
{
m_gdsClient.GDSClient.GetCertificates(application.ApplicationRecord.ApplicationId, null, out NodeId[] certificateTypeIds, out byte[][] certificates);
Assert.That(certificateTypeIds.Length == 1);
Assert.NotNull(certificates[0]);
Assert.AreEqual(certificates[0], application.Certificate);
m_gdsClient.GDSClient.GetCertificates(application.ApplicationRecord.ApplicationId, application.CertificateGroupId, out NodeId[] certificateTypeIds2, out byte[][] certificates2);
Assert.That(certificateTypeIds2.Length == 1);
Assert.NotNull(certificates2[0]);
Assert.AreEqual(certificates2[0], application.Certificate);
}
}

[Test, Order(550)]
public void StartGoodSigningRequestWithInvalidAppURI()
{
Expand Down Expand Up @@ -1327,7 +1352,7 @@ public void RevokeGoodCertificates()
}, Throws.Exception);
}
}

[Test, Order(900)]
public void UnregisterGoodApplications()
{
Expand Down
20 changes: 20 additions & 0 deletions Tests/Opc.Ua.Gds.Tests/PushTest.cs
Expand Up @@ -601,6 +601,25 @@ public void GetRejectedList()
Assert.NotNull(collection);
}

[Test, Order(610)]
public void GetCertificates()
{
ConnectPushClient(true);

Assert.That(() => {
m_pushClient.PushClient.GetCertificates(null, out var _, out var _);
}, Throws.Exception);

m_pushClient.PushClient.GetCertificates(m_pushClient.PushClient.DefaultApplicationGroup, out NodeId[] certificateTypeIds, out byte[][] certificates);

Assert.That(certificateTypeIds.Length == 1);
Assert.NotNull(certificates[0]);
using (var x509 = new X509Certificate2(certificates[0]))
{
Assert.NotNull(x509);
}
}

[Test, Order(700)]
public void ApplyChanges()
{
Expand All @@ -614,6 +633,7 @@ public void VerifyNoUserAccess()
ConnectPushClient(false);
Assert.That(() => { m_pushClient.PushClient.ApplyChanges(); }, Throws.Exception);
Assert.That(() => { m_pushClient.PushClient.GetRejectedList(); }, Throws.Exception);
Assert.That(() => { m_pushClient.PushClient.GetCertificates(null, out _, out _); }, Throws.Exception);
Assert.That(() => { m_pushClient.PushClient.UpdateCertificate(null, null, m_selfSignedServerCert.RawData, null, null, null); }, Throws.Exception);
Assert.That(() => { m_pushClient.PushClient.CreateSigningRequest(null, null, null, false, null); }, Throws.Exception);
Assert.That(() => { m_pushClient.PushClient.ReadTrustList(); }, Throws.Exception);
Expand Down