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 CheckRevocationStatus to Client & Server #2499

Merged
merged 12 commits into from
Feb 28, 2024
30 changes: 30 additions & 0 deletions Libraries/Opc.Ua.Gds.Client.Common/GlobalDiscoveryServerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,36 @@ public NodeId RegisterApplication(ApplicationRecordDataType application)
return null;
}

/// <summary>
/// Checks the provided certificate for validity
/// </summary>
/// <param name="certificate">The DER encoded form of the Certificate to check.</param>
/// <param name="certificateStatus">The first error encountered when validating the Certificate.</param>
/// <param name="validityTime">When the result expires and should be rechecked. DateTime.MinValue if this is unknown.</param>
public void CheckRevocationStatus(byte[] certificate,
out StatusCode certificateStatus,
out DateTime validityTime)
{
certificateStatus = StatusCodes.Good;
validityTime = DateTime.MinValue;

if (!IsConnected)
{
Connect();
}

var outputArguments = Session.Call(
ExpandedNodeId.ToNodeId(Opc.Ua.Gds.ObjectIds.Directory, Session.NamespaceUris),
ExpandedNodeId.ToNodeId(Opc.Ua.Gds.MethodIds.CertificateDirectoryType_CheckRevocationStatus, Session.NamespaceUris),
certificate);

if (outputArguments.Count >= 2)
{
certificateStatus = (StatusCode)outputArguments[0];
validityTime = (DateTime)outputArguments[1];
}
}

/// <summary>
/// Updates the application.
/// </summary>
Expand Down
40 changes: 39 additions & 1 deletion Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public override NodeId New(ISystemContext context, NodeState node)
}
#endregion

#region Private methods
#region Private Methods
private NodeId GetTrustListId(NodeId certificateGroupId)
{

Expand Down Expand Up @@ -381,6 +381,8 @@ protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context

Opc.Ua.Gds.CertificateDirectoryState activeNode = new Opc.Ua.Gds.CertificateDirectoryState(passiveNode.Parent);

activeNode.CheckRevocationStatus = new CheckRevocationStatusMethodState(passiveNode.Parent);

activeNode.Create(context, passiveNode);
activeNode.QueryServers.OnCall = new QueryServersMethodStateMethodCallHandler(OnQueryServers);
activeNode.QueryApplications.OnCall = new QueryApplicationsMethodStateMethodCallHandler(OnQueryApplications);
Expand All @@ -395,6 +397,8 @@ protected override NodeState AddBehaviourToPredefinedNode(ISystemContext context
activeNode.GetTrustList.OnCall = new GetTrustListMethodStateMethodCallHandler(OnGetTrustList);
activeNode.GetCertificateStatus.OnCall = new GetCertificateStatusMethodStateMethodCallHandler(OnGetCertificateStatus);
activeNode.StartSigningRequest.OnCall = new StartSigningRequestMethodStateMethodCallHandler(OnStartSigningRequest);
activeNode.CheckRevocationStatus.OnCall = new CheckRevocationStatusMethodStateMethodCallHandler(OnCheckRevocationStatus);
romanett marked this conversation as resolved.
Show resolved Hide resolved

// TODO
//activeNode.RevokeCertificate.OnCall = new RevokeCertificateMethodStateMethodCallHandler(OnRevokeCertificate);

Expand Down Expand Up @@ -584,6 +588,40 @@ out nextRecordId
return ServiceResult.Good;
}

private ServiceResult OnCheckRevocationStatus(
ISystemContext context,
MethodState method,
NodeId objectId,
byte[] certificate,
ref StatusCode certificateStatus,
ref DateTime validityTime)
{
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);

validityTime = DateTime.MinValue;
romanett marked this conversation as resolved.
Show resolved Hide resolved

using (var x509 = new X509Certificate2(certificate))
{
try
{
certificateValidator.Validate(x509);
}
catch (ServiceResultException se)
{
certificateStatus = se.StatusCode;
}
}
return ServiceResult.Good;
}

private ServiceResult CheckHttpsDomain(ApplicationRecordDataType application, string commonName)
{
if (application.ApplicationType == ApplicationType.Client)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,95 +51,110 @@ internal static class AuthorizationHelper
/// <param name="roles">all allowed roles, if wanted include <see cref="GdsRole.ApplicationSelfAdmin"/></param>
/// <param name="applicationId">If <see cref="GdsRole.ApplicationSelfAdmin"/> is allowed specifies the id of the Application-Entry to access</param>
public static void HasAuthorization(ISystemContext context, IEnumerable<Role> roles, [Optional] NodeId applicationId)
{
if (context != null)
{
List<Role> allowedRoles = roles.ToList();
bool selfAdmin = allowedRoles.Remove(GdsRole.ApplicationSelfAdmin);

//if true access is allowed
if (HasRole(context.UserIdentity, allowedRoles))
return;

if (selfAdmin)
if (context != null)
{
//if true access to own application is allowed
if (CheckSelfAdminPrivilege(context.UserIdentity, applicationId))
List<Role> allowedRoles = roles.ToList();
bool selfAdmin = allowedRoles.Remove(GdsRole.ApplicationSelfAdmin);

//if true access is allowed
if (HasRole(context.UserIdentity, allowedRoles))
return;

if (selfAdmin)
{
//if true access to own application is allowed
if (CheckSelfAdminPrivilege(context.UserIdentity, applicationId))
return;
}
throw new ServiceResultException(StatusCodes.BadUserAccessDenied, $"At least one of the Roles {string.Join(", ", roles)} is required to call the method");
}
throw new ServiceResultException(StatusCodes.BadUserAccessDenied, $"At least one of the Roles {string.Join(", ", roles)} is required to call the method");
}
}
/// <summary>
/// Checks if the current session (context) is allowed to access the trust List (has roles CertificateAuthorityAdmin, SecurityAdmin or <see cref="GdsRole.ApplicationSelfAdmin"/>)
/// </summary>
/// <param name="context">the current <see cref="ISystemContext"/></param>
/// <param name="trustedStorePath">path of the trustList, needed to check for Application Self Admin priviledge</param>
/// <param name="certTypeMap">all supported cert types, needed to check for Application Self Admin priviledge </param>
/// <param name="applicationsDatabase">all registered applications <see cref="IApplicationsDatabase"/> , needed to check for Application Self Admin priviledge </param>
/// <exception cref="ServiceResultException"></exception>
public static void HasTrustListAccess(ISystemContext context, string trustedStorePath, Dictionary<NodeId, string> certTypeMap, IApplicationsDatabase applicationsDatabase)
{
var roles = new List<Role> { GdsRole.CertificateAuthorityAdmin, Role.SecurityAdmin };
if (HasRole(context.UserIdentity, roles))
return;

if (!string.IsNullOrEmpty(trustedStorePath) && certTypeMap != null && applicationsDatabase != null &&
CheckSelfAdminPrivilege(context.UserIdentity, trustedStorePath, certTypeMap, applicationsDatabase))
return;

throw new ServiceResultException(StatusCodes.BadUserAccessDenied, $"At least one of the Roles {string.Join(", ", roles)} or ApplicationSelfAdminPrivilege is required to use the TrustList");
}
/// <summary>
/// Checks if the current session (context) is allowed to access the trust List (has roles CertificateAuthorityAdmin, SecurityAdmin or <see cref="GdsRole.ApplicationSelfAdmin"/>)
/// </summary>
/// <param name="context">the current <see cref="ISystemContext"/></param>
/// <param name="trustedStorePath">path of the trustList, needed to check for Application Self Admin priviledge</param>
/// <param name="certTypeMap">all supported cert types, needed to check for Application Self Admin priviledge </param>
/// <param name="applicationsDatabase">all registered applications <see cref="IApplicationsDatabase"/> , needed to check for Application Self Admin priviledge </param>
/// <exception cref="ServiceResultException"></exception>
public static void HasTrustListAccess(ISystemContext context, string trustedStorePath, Dictionary<NodeId, string> certTypeMap, IApplicationsDatabase applicationsDatabase)
{
var roles = new List<Role> { GdsRole.CertificateAuthorityAdmin, Role.SecurityAdmin };
if (HasRole(context.UserIdentity, roles))
return;

private static bool HasRole(IUserIdentity userIdentity, IEnumerable<Role> roles)
{
RoleBasedIdentity identity = userIdentity as RoleBasedIdentity;
if (!string.IsNullOrEmpty(trustedStorePath) && certTypeMap != null && applicationsDatabase != null &&
CheckSelfAdminPrivilege(context.UserIdentity, trustedStorePath, certTypeMap, applicationsDatabase))
return;

if (identity != null)
throw new ServiceResultException(StatusCodes.BadUserAccessDenied, $"At least one of the Roles {string.Join(", ", roles)} or ApplicationSelfAdminPrivilege is required to use the TrustList");
}
/// <summary>
/// Checks if current session (context) is connected using a secure channel
/// </summary>
/// <param name="context">the current <see cref="ISystemContext"/></param>
/// <exception cref="ServiceResultException"></exception>
public static void HasAuthenticatedSecureChannel(ISystemContext context)
{
foreach (Role role in roles)
OperationContext operationContext = (context as SystemContext)?.OperationContext as OperationContext;
if (operationContext != null)
{
if ((identity.Roles.Contains(role)))
if (operationContext.ChannelContext?.EndpointDescription?.SecurityMode != MessageSecurityMode.SignAndEncrypt)
{
return true;
throw new ServiceResultException(StatusCodes.BadUserAccessDenied, "Method has to be called from an authenticated secure channel.");
}
}
}
return false;
}
private static bool HasRole(IUserIdentity userIdentity, IEnumerable<Role> roles)
{
RoleBasedIdentity identity = userIdentity as RoleBasedIdentity;

private static bool CheckSelfAdminPrivilege(IUserIdentity userIdentity, NodeId applicationId)
{
if (applicationId is null || applicationId.IsNullNodeId)
if (identity != null)
{
foreach (Role role in roles)
{
if ((identity.Roles.Contains(role)))
{
return true;
}
}
}
return false;
}

GdsRoleBasedIdentity identity = userIdentity as GdsRoleBasedIdentity;
if (identity != null)
private static bool CheckSelfAdminPrivilege(IUserIdentity userIdentity, NodeId applicationId)
{
//self Admin only has access to own application
if (identity.ApplicationId == applicationId)
if (applicationId is null || applicationId.IsNullNodeId)
return false;

GdsRoleBasedIdentity identity = userIdentity as GdsRoleBasedIdentity;
if (identity != null)
{
return true;
//self Admin only has access to own application
if (identity.ApplicationId == applicationId)
{
return true;
}
}
return false;
}
return false;
}

private static bool CheckSelfAdminPrivilege(IUserIdentity userIdentity, string trustedStorePath, Dictionary<NodeId, string> certTypeMap, IApplicationsDatabase applicationsDatabase)
{
GdsRoleBasedIdentity identity = userIdentity as GdsRoleBasedIdentity;
if (identity != null)
private static bool CheckSelfAdminPrivilege(IUserIdentity userIdentity, string trustedStorePath, Dictionary<NodeId, string> certTypeMap, IApplicationsDatabase applicationsDatabase)
{
foreach (var certType in certTypeMap.Values)
GdsRoleBasedIdentity identity = userIdentity as GdsRoleBasedIdentity;
if (identity != null)
{
applicationsDatabase.GetApplicationTrustLists(identity.ApplicationId, certType, out var trustListId);
if (trustedStorePath == trustListId)
foreach (var certType in certTypeMap.Values)
{
return true;
applicationsDatabase.GetApplicationTrustLists(identity.ApplicationId, certType, out var trustListId);
if (trustedStorePath == trustListId)
{
return true;
}
}
}
return false;
}
return false;
}
}
}
26 changes: 26 additions & 0 deletions Tests/Opc.Ua.Gds.Tests/ClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,19 @@ public void GetInvalidCertificateStatus()
}
}

[Test, Order(700)]
public void CheckGoodRevocationStatus()
{
AssertIgnoreTestWithoutGoodRegistration();
ConnectGDS(false);
foreach (var application in m_goodApplicationTestSet)
{
m_gdsClient.GDSClient.CheckRevocationStatus(application.Certificate, out StatusCode certificateStatus, out DateTime validityTime);
romanett marked this conversation as resolved.
Show resolved Hide resolved
Assert.AreEqual(StatusCodes.Good, certificateStatus.Code);
Assert.NotNull(validityTime);
}
}

[Test, Order(900)]
public void UnregisterGoodApplications()
{
Expand All @@ -1308,6 +1321,19 @@ public void UnregisterGoodApplications()
}
}

[Test, Order(910)]
public void CheckRevocationStatusUnregisteredApplications()
{
AssertIgnoreTestWithoutGoodRegistration();
ConnectGDS(false);
foreach (var application in m_goodApplicationTestSet)
{
m_gdsClient.GDSClient.CheckRevocationStatus(application.Certificate, out StatusCode certificateStatus, out DateTime validityTime);
Assert.AreEqual(StatusCodes.BadCertificateRevoked, certificateStatus.Code);
Assert.NotNull(validityTime);
}
}

[Test, Order(910)]
public void UnregisterInvalidApplications()
{
Expand Down
Loading