Files
Marc Schier e1b9cea3ea Reduce sync-over-async in non-obsolete server APIs + post-merge code style cleanup (#3806)
## 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.
2026-05-27 06:48:30 +02:00

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