mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 00:56:28 +08:00
e1b9cea3ea
## Proposed changes Reduces sync-over-async in non-obsolete server APIs, removes stale `#region/#endregion` noise from the test suite, and fixes analyzer/test fallout from merging the Part 11 Historical Access framework (#3797). Follow-up review feedback is included: `FileSystemNodeManager` is now based on `AsyncCustomNodeManager`. ### Sync-over-async reduction (~24 `GetAwaiter().GetResult()` sites) **Server-side:** - `TrustList`: async `OnCallAsync` handlers added; sync `OnCall` handlers retained as compat shims so legacy `CustomNodeManager2` subclasses that host the TrustList but do not implement `ICallAsyncNodeManager` keep working through `MethodState.Call`. - `GDS ApplicationsNodeManager`: `RequestAccessToken`, `KeyCredentialStart/FinishRequest`, and `Revoke` moved to `OnCallAsync`. - `IKeyCredentialRequestStore`: interface + in-memory implementation converted to async; `FinishKeyCredentialRequestResult` record carries the previous out-set. Tests in `Tests/Opc.Ua.Gds.Tests/` updated to the new async API. - `FileSystem`: - `DirectoryObjectState` methods (`CreateDirectory` / `CreateFile` / `Delete` / `MoveOrCopy`) use `OnCallAsync`. - `FileSystemNodeManager` now derives from `AsyncCustomNodeManager` and overrides async address-space/node-resolution paths (`CreateAddressSpaceAsync`, `DeleteAddressSpaceAsync`, `GetManagerHandleAsync`, `ValidateNodeAsync`) to avoid sync-over-async in dynamic node validation. - `FileSystemNodeManagerFactory` now supports async creation (`IAsyncNodeManagerFactory`) while retaining sync factory compatibility. - Reference server registration of the FileSystem node manager is aligned to the async node manager list. - `ConfigurationNodeManager` namespace lookup uses sync `GetManagerHandle` to keep the remaining `GetResult` inside the documented `SyncNodeManagerAdapter` bridge. - `ServerBase`: new `OnServerStartingAsync`; `StartAsync` now awaits it. - `StandardServer.CreateMasterNodeManagerAsync` added; sync wrapper kept for backward compatibility. - `IServerInternal.CloseSession` marked `[Obsolete]` (`CloseSessionAsync` exists). - `IMasterNodeManager.RemoveReferences` marked `[Obsolete]`; in-tree sync callers remain with scoped `CS0618` pragmas + TODO. **Stack:** - `EncryptedSecret`: added `VerifyHeaderForEccAsync` / `DecryptAsync` / `TryDecryptAsync`; `UserNameIdentityTokenHandler.DecryptAsync` awaits ECC validation. - `CertificatePasswordProvider` ctor: `GetResult` fallback replaced with explicit `InvalidOperationException`. ### Code-style and post-merge cleanup - Removed unused `#region`/`#endregion` directives across test files. - Restored green build state after Part 11 merge fallout fixes (production + analyzer + test cleanup from earlier commits in this PR). ### CI / validation notes - This PR branch surfaced runtime regressions introduced by Part 11 after the build-breaker was fixed (outside scope of this async/code-style cleanup). - Follow-up feedback change validation for `FileSystemNodeManager`: - `dotnet build Libraries/Opc.Ua.Server/Opc.Ua.Server.csproj -f net10.0 --no-restore` ✅ - `dotnet test Tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj -f net10.0 --filter FileSystemNodeIdTests` ✅ - `dotnet test Tests/Opc.Ua.History.Tests/Opc.Ua.History.Tests.csproj -f net10.0 --filter FileSystemSmokeTests` ✅ (test skipped in environment) ## Related Issues - None. ## Types of changes - [ ] Bugfix (non-breaking change which fixes an issue) - [x] Enhancement (non-breaking change which adds functionality) - [x] Test enhancement (non-breaking change to increase test coverage) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages) - [ ] Documentation Update (if none of the other choices apply) ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf). - [x] I ran tests locally with my changes, all passed. - [ ] I fixed all failing tests in the CI pipelines. - [ ] I fixed all introduced issues with CodeQL and LGTM. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added necessary documentation (if appropriate). - [x] Any dependent changes have been merged and published in downstream modules.
1114 lines
41 KiB
C#
1114 lines
41 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Security.Certificates;
|
|
|
|
namespace Opc.Ua.Server
|
|
{
|
|
/// <summary>
|
|
/// The implementation of a server trustlist.
|
|
/// </summary>
|
|
public class TrustList
|
|
{
|
|
private const int kDefaultTrustListCapacity = 1 * 1024 * 1024;
|
|
|
|
/// <summary>
|
|
/// 1MB default max trust list size
|
|
/// </summary>
|
|
private const int kDefaultMaxTrustListSize = 1 * 1024 * 1024;
|
|
|
|
/// <summary>
|
|
/// Initialize the trustlist with default values.
|
|
/// </summary>
|
|
public TrustList(
|
|
TrustListState node,
|
|
CertificateStoreIdentifier trustedListStore,
|
|
CertificateStoreIdentifier issuerListStore,
|
|
SecureAccess readAccess,
|
|
SecureAccess writeAccess,
|
|
ITelemetryContext telemetry,
|
|
int maxTrustListSize = 0)
|
|
{
|
|
m_telemetry = telemetry;
|
|
m_logger = telemetry.CreateLogger<TrustList>();
|
|
m_node = node;
|
|
m_trustedStore = trustedListStore;
|
|
m_issuerStore = issuerListStore;
|
|
m_readAccess = readAccess;
|
|
m_writeAccess = writeAccess;
|
|
// If maxTrustListSize is 0 (unlimited), use a sensible default limit
|
|
m_maxTrustListSize = maxTrustListSize > 0 ? maxTrustListSize : kDefaultMaxTrustListSize;
|
|
|
|
// Register both sync and async handlers per MethodState. The async path is
|
|
// preferred (and is what the in-tree containers — ConfigurationNodeManager
|
|
// and ApplicationsNodeManager — dispatch through). The sync OnCall handlers
|
|
// are compat shims for legacy CustomNodeManager2 subclasses that host this
|
|
// TrustList but do not implement ICallAsyncNodeManager; those subclasses
|
|
// dispatch through MethodState.Call (sync) which would otherwise return
|
|
// BadNotImplemented because OnCallAsync is not consulted on the sync path.
|
|
node.Open!.OnCall = new OpenMethodStateMethodCallHandler(Open);
|
|
node.Open.OnCallAsync = new OpenMethodStateMethodAsyncCallHandler(OpenAsync);
|
|
node.OpenWithMasks!.OnCall
|
|
= new OpenWithMasksMethodStateMethodCallHandler(OpenWithMasks);
|
|
node.OpenWithMasks.OnCallAsync
|
|
= new OpenWithMasksMethodStateMethodAsyncCallHandler(OpenWithMasksAsync);
|
|
node.Read!.OnCall = new ReadMethodStateMethodCallHandler(Read);
|
|
node.Read.OnCallAsync = new ReadMethodStateMethodAsyncCallHandler(ReadAsync);
|
|
node.Write!.OnCall = new WriteMethodStateMethodCallHandler(Write);
|
|
node.Write.OnCallAsync = new WriteMethodStateMethodAsyncCallHandler(WriteAsync);
|
|
node.Close!.OnCall = new CloseMethodStateMethodCallHandler(Close);
|
|
node.Close.OnCallAsync = new CloseMethodStateMethodAsyncCallHandler(CloseAsync);
|
|
node.CloseAndUpdate!.OnCall
|
|
= new CloseAndUpdateMethodStateMethodCallHandler(CloseAndUpdate);
|
|
node.CloseAndUpdate.OnCallAsync
|
|
= new CloseAndUpdateMethodStateMethodAsyncCallHandler(CloseAndUpdateAsync);
|
|
node.AddCertificate!.OnCall
|
|
= new AddCertificateMethodStateMethodCallHandler(AddCertificate);
|
|
node.AddCertificate.OnCallAsync
|
|
= new AddCertificateMethodStateMethodAsyncCallHandler(AddCertificateAsync);
|
|
node.RemoveCertificate!.OnCall
|
|
= new RemoveCertificateMethodStateMethodCallHandler(RemoveCertificate);
|
|
node.RemoveCertificate.OnCallAsync
|
|
= new RemoveCertificateMethodStateMethodAsyncCallHandler(RemoveCertificateAsync);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delegate to validate the access to the trust list.
|
|
/// </summary>
|
|
/// <param name="context">System context</param>
|
|
/// <param name="trustedStore">the path to identify the trustList</param>
|
|
public delegate void SecureAccess(
|
|
ISystemContext context,
|
|
CertificateStoreIdentifier trustedStore);
|
|
|
|
private ServiceResult Open(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
byte mode,
|
|
ref uint fileHandle)
|
|
{
|
|
OpenMethodStateResult result = OpenAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
mode,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
fileHandle = result.FileHandle;
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private ValueTask<OpenMethodStateResult> OpenAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
byte mode,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return OpenCoreAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
(OpenFileMode)mode,
|
|
TrustListMasks.All,
|
|
cancellationToken);
|
|
}
|
|
|
|
private ServiceResult OpenWithMasks(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint masks,
|
|
ref uint fileHandle)
|
|
{
|
|
OpenWithMasksMethodStateResult result = OpenWithMasksAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
masks,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
fileHandle = result.FileHandle;
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private async ValueTask<OpenWithMasksMethodStateResult> OpenWithMasksAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint masks,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
OpenMethodStateResult result = await OpenCoreAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
OpenFileMode.Read,
|
|
(TrustListMasks)masks,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return new OpenWithMasksMethodStateResult
|
|
{
|
|
ServiceResult = result.ServiceResult,
|
|
FileHandle = result.FileHandle
|
|
};
|
|
}
|
|
|
|
private async ValueTask<OpenMethodStateResult> OpenCoreAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
OpenFileMode mode,
|
|
TrustListMasks masks,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (mode == OpenFileMode.Read)
|
|
{
|
|
HasSecureReadAccess(context);
|
|
}
|
|
else if ((int)mode == ((int)OpenFileMode.Write | (int)OpenFileMode.EraseExisting))
|
|
{
|
|
HasSecureWriteAccess(context);
|
|
}
|
|
else
|
|
{
|
|
return new OpenMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadNotWritable,
|
|
FileHandle = 0
|
|
};
|
|
}
|
|
|
|
uint fileHandle = 0;
|
|
MemoryStream? strm = null;
|
|
|
|
try
|
|
{
|
|
var trustList = new TrustListDataType { SpecifiedLists = (uint)masks };
|
|
|
|
ICertificateStore store = m_trustedStore.OpenStore(m_telemetry);
|
|
try
|
|
{
|
|
if (store == null)
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Failed to open trusted certificate store.");
|
|
}
|
|
|
|
if (((int)masks & (int)TrustListMasks.TrustedCertificates) != 0)
|
|
{
|
|
using CertificateCollection certificates = await store.EnumerateAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
trustList.TrustedCertificates = trustList.TrustedCertificates.AddItems(
|
|
certificates
|
|
.Select(certificate => certificate.RawData.ToByteString()));
|
|
}
|
|
|
|
if (((int)masks & (int)TrustListMasks.TrustedCrls) != 0)
|
|
{
|
|
X509CRLCollection crls = await store.EnumerateCRLsAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
trustList.TrustedCrls = trustList.TrustedCrls.AddItems(
|
|
crls.Select(crl => crl.RawData.ToByteString()));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
store.Close();
|
|
}
|
|
|
|
store = m_issuerStore.OpenStore(m_telemetry);
|
|
try
|
|
{
|
|
if (store == null)
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Failed to open issuer certificate store.");
|
|
}
|
|
|
|
if (((int)masks & (int)TrustListMasks.IssuerCertificates) != 0)
|
|
{
|
|
using CertificateCollection certificates = await store.EnumerateAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
trustList.IssuerCertificates = trustList.IssuerCertificates.AddItems(certificates
|
|
.Select(certificate => certificate.RawData.ToByteString()));
|
|
}
|
|
|
|
if (((int)masks & (int)TrustListMasks.IssuerCrls) != 0)
|
|
{
|
|
X509CRLCollection crls = await store.EnumerateCRLsAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
trustList.IssuerCrls = trustList.IssuerCrls.AddItems(crls
|
|
.Select(crl => crl.RawData.ToByteString()));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
store.Close();
|
|
}
|
|
|
|
if (mode == OpenFileMode.Read)
|
|
{
|
|
strm = EncodeTrustListData(context, trustList);
|
|
}
|
|
else
|
|
{
|
|
strm = new MemoryStream(kDefaultTrustListCapacity);
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
if (!m_sessionId.IsNull)
|
|
{
|
|
// to avoid deadlocks, last open always wins
|
|
m_sessionId = default;
|
|
m_strm?.Dispose();
|
|
m_strm = null;
|
|
m_node.OpenCount!.Value = 0;
|
|
}
|
|
|
|
m_sessionId = (context as ISessionSystemContext)?.SessionId ?? default;
|
|
fileHandle = ++m_fileHandle;
|
|
m_totalBytesProcessed = 0; // Reset counter for new file operation
|
|
m_strm = strm;
|
|
m_node.OpenCount!.Value = 1;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
strm?.Dispose();
|
|
throw;
|
|
}
|
|
|
|
return new OpenMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Good,
|
|
FileHandle = fileHandle
|
|
};
|
|
}
|
|
|
|
private ServiceResult Read(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
int length,
|
|
ref ByteString data)
|
|
{
|
|
ReadMethodStateResult result = ReadAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
fileHandle,
|
|
length,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
data = result.Data;
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private ValueTask<ReadMethodStateResult> ReadAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
int length,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
HasSecureReadAccess(context);
|
|
|
|
ByteString data;
|
|
|
|
lock (m_lock)
|
|
{
|
|
if (context is ISessionSystemContext session &&
|
|
m_sessionId != null! &&
|
|
!m_sessionId.Equals(session.SessionId))
|
|
{
|
|
return new ValueTask<ReadMethodStateResult>(new ReadMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Create(
|
|
StatusCodes.BadUserAccessDenied,
|
|
"Session not authorized"),
|
|
Data = default
|
|
});
|
|
}
|
|
|
|
if (m_fileHandle != fileHandle)
|
|
{
|
|
return new ValueTask<ReadMethodStateResult>(new ReadMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Create(
|
|
StatusCodes.BadInvalidArgument,
|
|
"Invalid file handle"),
|
|
Data = default
|
|
});
|
|
}
|
|
|
|
// Check if we would exceed the maximum trust list size
|
|
if (m_totalBytesProcessed + length > m_maxTrustListSize)
|
|
{
|
|
return new ValueTask<ReadMethodStateResult>(new ReadMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Create(
|
|
StatusCodes.BadEncodingLimitsExceeded,
|
|
"Trust list size exceeds maximum allowed size of {0} bytes",
|
|
m_maxTrustListSize),
|
|
Data = default
|
|
});
|
|
}
|
|
|
|
byte[] buffer = new byte[length];
|
|
int bytesRead = m_strm!.Read(buffer, 0, length);
|
|
Debug.Assert(bytesRead >= 0);
|
|
data = ByteString.From(buffer)[..bytesRead];
|
|
|
|
m_totalBytesProcessed += bytesRead;
|
|
}
|
|
|
|
return new ValueTask<ReadMethodStateResult>(new ReadMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Good,
|
|
Data = data
|
|
});
|
|
}
|
|
|
|
private ServiceResult Write(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
ByteString data)
|
|
{
|
|
WriteMethodStateResult result = WriteAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
fileHandle,
|
|
data,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private ValueTask<WriteMethodStateResult> WriteAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
ByteString data,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
HasSecureWriteAccess(context);
|
|
|
|
lock (m_lock)
|
|
{
|
|
if (context is ISessionSystemContext session &&
|
|
m_sessionId != null! &&
|
|
!m_sessionId.Equals(session.SessionId))
|
|
{
|
|
return new ValueTask<WriteMethodStateResult>(new WriteMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadUserAccessDenied
|
|
});
|
|
}
|
|
|
|
if (m_fileHandle != fileHandle)
|
|
{
|
|
return new ValueTask<WriteMethodStateResult>(new WriteMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadInvalidArgument
|
|
});
|
|
}
|
|
|
|
// Check if we would exceed the maximum trust list size
|
|
if (m_totalBytesProcessed + data.Length > m_maxTrustListSize)
|
|
{
|
|
return new ValueTask<WriteMethodStateResult>(new WriteMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Create(
|
|
StatusCodes.BadEncodingLimitsExceeded,
|
|
"Trust list size exceeds maximum allowed size of {0} bytes",
|
|
m_maxTrustListSize)
|
|
});
|
|
}
|
|
|
|
m_strm!.Write(data.ToArray(), 0, data.Length);
|
|
m_totalBytesProcessed += data.Length;
|
|
}
|
|
|
|
return new ValueTask<WriteMethodStateResult>(new WriteMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Good
|
|
});
|
|
}
|
|
|
|
private ServiceResult Close(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle)
|
|
{
|
|
CloseMethodStateResult result = CloseAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
fileHandle,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private ValueTask<CloseMethodStateResult> CloseAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
HasSecureReadAccess(context);
|
|
|
|
lock (m_lock)
|
|
{
|
|
if (context is ISessionSystemContext session &&
|
|
m_sessionId != null! &&
|
|
!m_sessionId.Equals(session.SessionId))
|
|
{
|
|
return new ValueTask<CloseMethodStateResult>(new CloseMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadUserAccessDenied
|
|
});
|
|
}
|
|
|
|
if (m_fileHandle != fileHandle)
|
|
{
|
|
return new ValueTask<CloseMethodStateResult>(new CloseMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadInvalidArgument
|
|
});
|
|
}
|
|
|
|
m_sessionId = default;
|
|
m_strm?.Dispose();
|
|
m_strm = null;
|
|
m_node.OpenCount!.Value = 0;
|
|
}
|
|
|
|
return new ValueTask<CloseMethodStateResult>(new CloseMethodStateResult
|
|
{
|
|
ServiceResult = ServiceResult.Good
|
|
});
|
|
}
|
|
|
|
private ServiceResult CloseAndUpdate(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
ref bool restartRequired)
|
|
{
|
|
CloseAndUpdateMethodStateResult result = CloseAndUpdateAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
fileHandle,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
restartRequired = result.ApplyChangesRequired;
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private async ValueTask<CloseAndUpdateMethodStateResult> CloseAndUpdateAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
uint fileHandle,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArrayOf<Variant> inputParameters = [fileHandle];
|
|
m_node.ReportTrustListUpdateRequestedAuditEvent(
|
|
context,
|
|
objectId,
|
|
"Method/CloseAndUpdate",
|
|
method.NodeId,
|
|
inputParameters,
|
|
m_logger);
|
|
HasSecureWriteAccess(context);
|
|
|
|
ServiceResult result = StatusCodes.Good;
|
|
|
|
MemoryStream? strm;
|
|
lock (m_lock)
|
|
{
|
|
if (context is ISessionSystemContext session &&
|
|
m_sessionId != null! &&
|
|
!m_sessionId.Equals(session.SessionId))
|
|
{
|
|
return new CloseAndUpdateMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadUserAccessDenied,
|
|
ApplyChangesRequired = false
|
|
};
|
|
}
|
|
|
|
if (m_fileHandle != fileHandle)
|
|
{
|
|
return new CloseAndUpdateMethodStateResult
|
|
{
|
|
ServiceResult = StatusCodes.BadInvalidArgument,
|
|
ApplyChangesRequired = false
|
|
};
|
|
}
|
|
|
|
strm = m_strm;
|
|
}
|
|
|
|
CertificateCollection? issuerCertificates = null;
|
|
CertificateCollection? trustedCertificates = null;
|
|
try
|
|
{
|
|
TrustListDataType trustList = DecodeTrustListData(context, strm!);
|
|
int masks = (int)trustList.SpecifiedLists;
|
|
|
|
X509CRLCollection? issuerCrls = null;
|
|
X509CRLCollection? trustedCrls = null;
|
|
|
|
// test integrity of all CRLs
|
|
if ((masks & (int)TrustListMasks.IssuerCertificates) != 0)
|
|
{
|
|
issuerCertificates = [];
|
|
foreach (ByteString cert in trustList.IssuerCertificates)
|
|
{
|
|
issuerCertificates.Add(Certificate.FromRawData(cert));
|
|
}
|
|
}
|
|
if ((masks & (int)TrustListMasks.IssuerCrls) != 0)
|
|
{
|
|
issuerCrls = [];
|
|
foreach (ByteString crl in trustList.IssuerCrls)
|
|
{
|
|
issuerCrls.Add(new X509CRL(crl.ToArray()));
|
|
}
|
|
}
|
|
if ((masks & (int)TrustListMasks.TrustedCertificates) != 0)
|
|
{
|
|
trustedCertificates = [];
|
|
foreach (ByteString cert in trustList.TrustedCertificates)
|
|
{
|
|
trustedCertificates.Add(Certificate.FromRawData(cert));
|
|
}
|
|
}
|
|
if ((masks & (int)TrustListMasks.TrustedCrls) != 0)
|
|
{
|
|
trustedCrls = [];
|
|
foreach (ByteString crl in trustList.TrustedCrls)
|
|
{
|
|
trustedCrls.Add(new X509CRL(crl.ToArray()));
|
|
}
|
|
}
|
|
|
|
// update store
|
|
int updateMasks = (int)TrustListMasks.None;
|
|
if ((masks & (int)TrustListMasks.IssuerCertificates) != 0 &&
|
|
await UpdateStoreCertificatesAsync(m_issuerStore, issuerCertificates!, cancellationToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
updateMasks |= (int)TrustListMasks.IssuerCertificates;
|
|
}
|
|
if ((masks & (int)TrustListMasks.IssuerCrls) != 0 &&
|
|
await UpdateStoreCrlsAsync(m_issuerStore, issuerCrls!, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
updateMasks |= (int)TrustListMasks.IssuerCrls;
|
|
}
|
|
if ((masks & (int)TrustListMasks.TrustedCertificates) != 0 &&
|
|
await UpdateStoreCertificatesAsync(m_trustedStore, trustedCertificates!, cancellationToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
updateMasks |= (int)TrustListMasks.TrustedCertificates;
|
|
}
|
|
if ((masks & (int)TrustListMasks.TrustedCrls) != 0 &&
|
|
await UpdateStoreCrlsAsync(m_trustedStore, trustedCrls!, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
updateMasks |= (int)TrustListMasks.TrustedCrls;
|
|
}
|
|
|
|
if (masks != updateMasks)
|
|
{
|
|
result = StatusCodes.BadCertificateInvalid;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
result = StatusCodes.BadCertificateInvalid;
|
|
}
|
|
finally
|
|
{
|
|
issuerCertificates?.Dispose();
|
|
trustedCertificates?.Dispose();
|
|
|
|
lock (m_lock)
|
|
{
|
|
m_sessionId = default;
|
|
m_strm?.Dispose();
|
|
m_strm = null;
|
|
m_node.LastUpdateTime!.Value = DateTime.UtcNow;
|
|
m_node.OpenCount!.Value = 0;
|
|
}
|
|
}
|
|
|
|
// report the TrustListUpdatedAuditEvent
|
|
m_node.ReportTrustListUpdatedAuditEvent(
|
|
context,
|
|
objectId,
|
|
"Method/CloseAndUpdate",
|
|
method.NodeId,
|
|
inputParameters,
|
|
result.StatusCode,
|
|
m_logger);
|
|
|
|
return new CloseAndUpdateMethodStateResult
|
|
{
|
|
ServiceResult = result,
|
|
ApplyChangesRequired = false
|
|
};
|
|
}
|
|
|
|
private ServiceResult AddCertificate(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
ByteString certificate,
|
|
bool isTrustedCertificate)
|
|
{
|
|
AddCertificateMethodStateResult result = AddCertificateAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
certificate,
|
|
isTrustedCertificate,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private async ValueTask<AddCertificateMethodStateResult> AddCertificateAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
ByteString certificate,
|
|
bool isTrustedCertificate,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArrayOf<Variant> inputParameters = [certificate, isTrustedCertificate];
|
|
m_node.ReportTrustListUpdateRequestedAuditEvent(
|
|
context,
|
|
objectId,
|
|
"Method/AddCertificate",
|
|
method.NodeId,
|
|
inputParameters,
|
|
m_logger);
|
|
HasSecureWriteAccess(context);
|
|
|
|
ServiceResult result = StatusCodes.Good;
|
|
|
|
bool isSessionOpen;
|
|
lock (m_lock)
|
|
{
|
|
isSessionOpen = !m_sessionId.IsNull;
|
|
}
|
|
|
|
if (isSessionOpen)
|
|
{
|
|
result = StatusCodes.BadInvalidState;
|
|
}
|
|
else if (certificate.IsEmpty)
|
|
{
|
|
result = StatusCodes.BadInvalidArgument;
|
|
}
|
|
else
|
|
{
|
|
Certificate? cert = null;
|
|
try
|
|
{
|
|
cert = Certificate.FromRawData(certificate);
|
|
}
|
|
catch
|
|
{
|
|
// note: a previous version of the sample code accepted also CRL,
|
|
// but the behaviour was not as specified and removed
|
|
// https://mantis.opcfoundation.org/view.php?id=6342
|
|
result = StatusCodes.BadCertificateInvalid;
|
|
}
|
|
|
|
if (cert != null)
|
|
{
|
|
CertificateStoreIdentifier storeIdentifier = isTrustedCertificate
|
|
? m_trustedStore
|
|
: m_issuerStore;
|
|
ICertificateStore store = storeIdentifier.OpenStore(m_telemetry);
|
|
try
|
|
{
|
|
if (store != null)
|
|
{
|
|
await store.AddAsync(cert, null, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
store?.Close();
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
m_node.LastUpdateTime!.Value = DateTime.UtcNow;
|
|
}
|
|
}
|
|
}
|
|
|
|
// report the TrustListUpdatedAuditEvent
|
|
m_node.ReportTrustListUpdatedAuditEvent(
|
|
context,
|
|
objectId,
|
|
"Method/AddCertificate",
|
|
method.NodeId,
|
|
inputParameters,
|
|
result.StatusCode,
|
|
m_logger);
|
|
|
|
return new AddCertificateMethodStateResult
|
|
{
|
|
ServiceResult = result
|
|
};
|
|
}
|
|
|
|
private ServiceResult RemoveCertificate(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
string thumbprint,
|
|
bool isTrustedCertificate)
|
|
{
|
|
RemoveCertificateMethodStateResult result = RemoveCertificateAsync(
|
|
context,
|
|
method,
|
|
objectId,
|
|
thumbprint,
|
|
isTrustedCertificate,
|
|
CancellationToken.None).AsTask().ConfigureAwait(false).GetAwaiter().GetResult();
|
|
return result.ServiceResult;
|
|
}
|
|
|
|
private async ValueTask<RemoveCertificateMethodStateResult> RemoveCertificateAsync(
|
|
ISystemContext context,
|
|
MethodState method,
|
|
NodeId objectId,
|
|
string thumbprint,
|
|
bool isTrustedCertificate,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArrayOf<Variant> inputParameters = [thumbprint, isTrustedCertificate];
|
|
m_node.ReportTrustListUpdateRequestedAuditEvent(
|
|
context,
|
|
objectId,
|
|
"Method/RemoveCertificate",
|
|
method.NodeId,
|
|
inputParameters,
|
|
m_logger);
|
|
|
|
HasSecureWriteAccess(context);
|
|
ServiceResult result = StatusCodes.Good;
|
|
|
|
bool isSessionOpen;
|
|
lock (m_lock)
|
|
{
|
|
isSessionOpen = !m_sessionId.IsNull;
|
|
}
|
|
|
|
if (isSessionOpen)
|
|
{
|
|
result = StatusCodes.BadInvalidState;
|
|
}
|
|
else if (string.IsNullOrEmpty(thumbprint))
|
|
{
|
|
result = StatusCodes.BadInvalidArgument;
|
|
}
|
|
else
|
|
{
|
|
CertificateStoreIdentifier storeIdentifier = isTrustedCertificate
|
|
? m_trustedStore
|
|
: m_issuerStore;
|
|
using (ICertificateStore store = storeIdentifier.OpenStore(m_telemetry))
|
|
{
|
|
if (store == null)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadConfigurationError,
|
|
"Failed to open certificate store.");
|
|
}
|
|
|
|
using CertificateCollection certCollection = await store
|
|
.FindByThumbprintAsync(thumbprint, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (certCollection.Count == 0)
|
|
{
|
|
result = StatusCodes.BadInvalidArgument;
|
|
}
|
|
else
|
|
{
|
|
// delete all CRLs signed by cert
|
|
var crlsToDelete = new X509CRLCollection();
|
|
X509CRLCollection crls = await store.EnumerateCRLsAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
foreach (X509CRL crl in crls)
|
|
{
|
|
foreach (Certificate cert in certCollection)
|
|
{
|
|
if (X509Utils.CompareDistinguishedName(
|
|
cert.SubjectName,
|
|
crl.IssuerName) &&
|
|
crl.VerifySignature(cert, false))
|
|
{
|
|
crlsToDelete.Add(crl);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!await store.DeleteAsync(thumbprint, cancellationToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
result = StatusCodes.BadInvalidArgument;
|
|
}
|
|
else
|
|
{
|
|
foreach (X509CRL crl in crlsToDelete)
|
|
{
|
|
if (!await store.DeleteCRLAsync(crl, cancellationToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
// intentionally ignore errors, try best effort
|
|
m_logger.LogError(
|
|
"RemoveCertificate: Failed to delete CRL {Crl}.",
|
|
crl.ToString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
m_node.LastUpdateTime!.Value = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
// report the TrustListUpdatedAuditEvent
|
|
m_node.ReportTrustListUpdatedAuditEvent(
|
|
context,
|
|
objectId,
|
|
"Method/RemoveCertificate",
|
|
method.NodeId,
|
|
inputParameters,
|
|
result.StatusCode,
|
|
m_logger);
|
|
|
|
return new RemoveCertificateMethodStateResult
|
|
{
|
|
ServiceResult = result
|
|
};
|
|
}
|
|
|
|
private static MemoryStream EncodeTrustListData(
|
|
ISystemContext context,
|
|
TrustListDataType trustList)
|
|
{
|
|
IServiceMessageContext messageContext =
|
|
new ServiceMessageContext(context.Telemetry, context.EncodeableFactory)
|
|
{
|
|
NamespaceUris = context.NamespaceUris,
|
|
ServerUris = context.ServerUris
|
|
};
|
|
var strm = new MemoryStream();
|
|
using (var encoder = new BinaryEncoder(strm, messageContext, true))
|
|
{
|
|
encoder.WriteEncodeable(null, trustList);
|
|
}
|
|
strm.Position = 0;
|
|
return strm;
|
|
}
|
|
|
|
private static TrustListDataType DecodeTrustListData(
|
|
ISystemContext context,
|
|
MemoryStream strm)
|
|
{
|
|
var trustList = new TrustListDataType();
|
|
IServiceMessageContext messageContext =
|
|
new ServiceMessageContext(context.Telemetry, context.EncodeableFactory)
|
|
{
|
|
NamespaceUris = context.NamespaceUris,
|
|
ServerUris = context.ServerUris
|
|
};
|
|
strm.Position = 0;
|
|
using (var decoder = new BinaryDecoder(strm, messageContext))
|
|
{
|
|
trustList.Decode(decoder);
|
|
}
|
|
return trustList;
|
|
}
|
|
|
|
private async Task<bool> UpdateStoreCrlsAsync(
|
|
CertificateStoreIdentifier storeIdentifier,
|
|
X509CRLCollection updatedCrls,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
bool result = true;
|
|
try
|
|
{
|
|
ICertificateStore store = storeIdentifier.OpenStore(m_telemetry);
|
|
try
|
|
{
|
|
if (store == null)
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Failed to open certificate store.");
|
|
}
|
|
|
|
X509CRLCollection storeCrls = await store.EnumerateCRLsAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
foreach (X509CRL crl in storeCrls)
|
|
{
|
|
if (!updatedCrls.Remove(crl) &&
|
|
!await store.DeleteCRLAsync(crl, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
result = false;
|
|
}
|
|
}
|
|
foreach (X509CRL crl in updatedCrls)
|
|
{
|
|
await store.AddCRLAsync(crl, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
store.Close();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
result = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private async Task<bool> UpdateStoreCertificatesAsync(
|
|
CertificateStoreIdentifier storeIdentifier,
|
|
CertificateCollection updatedCerts,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
bool result = true;
|
|
try
|
|
{
|
|
ICertificateStore store = storeIdentifier.OpenStore(m_telemetry);
|
|
try
|
|
{
|
|
if (store == null)
|
|
{
|
|
throw ServiceResultException.ConfigurationError(
|
|
"Failed to open certificate store.");
|
|
}
|
|
|
|
using CertificateCollection storeCerts = await store.EnumerateAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
foreach (Certificate cert in storeCerts)
|
|
{
|
|
if (!updatedCerts.Remove(cert) &&
|
|
!await store.DeleteAsync(cert.Thumbprint, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
result = false;
|
|
}
|
|
}
|
|
foreach (Certificate cert in updatedCerts)
|
|
{
|
|
await store.AddAsync(cert, null, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
store.Close();
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
result = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void HasSecureReadAccess(ISystemContext context)
|
|
{
|
|
if (m_readAccess != null)
|
|
{
|
|
m_readAccess.Invoke(context, m_trustedStore);
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadUserAccessDenied);
|
|
}
|
|
}
|
|
|
|
private void HasSecureWriteAccess(ISystemContext context)
|
|
{
|
|
if (m_writeAccess != null)
|
|
{
|
|
m_writeAccess.Invoke(context, m_trustedStore);
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadUserAccessDenied);
|
|
}
|
|
}
|
|
|
|
private readonly Lock m_lock = new();
|
|
private readonly SecureAccess m_readAccess;
|
|
private readonly SecureAccess m_writeAccess;
|
|
private NodeId m_sessionId;
|
|
private uint m_fileHandle;
|
|
private readonly CertificateStoreIdentifier m_trustedStore;
|
|
private readonly CertificateStoreIdentifier m_issuerStore;
|
|
private readonly ITelemetryContext m_telemetry;
|
|
private readonly ILogger m_logger;
|
|
private readonly TrustListState m_node;
|
|
private MemoryStream? m_strm;
|
|
private readonly int m_maxTrustListSize;
|
|
private long m_totalBytesProcessed;
|
|
}
|
|
}
|