Skip to content

Commit

Permalink
GDS: add Method CheckRevocationStatus to Client & Server (#2499)
Browse files Browse the repository at this point in the history
Implement the Method CheckRevocationStatus from : https://reference.opcfoundation.org/GDS/v105/docs/7.9.11
  • Loading branch information
romanett committed Feb 28, 2024
1 parent 988be41 commit 8d2be39
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 64 deletions.
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
41 changes: 40 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);

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

Expand Down Expand Up @@ -584,6 +588,41 @@ 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);

//TODO return validityTime of Certificate once CertificateValidator supports it
validityTime = DateTime.MinValue;

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);
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

0 comments on commit 8d2be39

Please sign in to comment.