mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 09:06:37 +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.
1305 lines
52 KiB
C#
1305 lines
52 KiB
C#
/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved.
|
|
The source code in this file is covered under a dual-license scenario:
|
|
- RCL: for OPC Foundation Corporate Members in good-standing
|
|
- GPL V2: everybody else
|
|
RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/
|
|
GNU General Public License as published by the Free Software Foundation;
|
|
version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2
|
|
This source code is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
*/
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Security.Certificates;
|
|
#if CURVE25519
|
|
using Org.BouncyCastle.Crypto.Modes;
|
|
using Org.BouncyCastle.Crypto.Parameters;
|
|
#endif
|
|
|
|
#nullable enable
|
|
|
|
namespace Opc.Ua
|
|
{
|
|
/// <summary>
|
|
/// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC).
|
|
/// </summary>
|
|
public class EncryptedSecret
|
|
{
|
|
private static readonly TimeSpan s_rsaEncryptedSecretMaxClockSkew = TimeSpan.FromMinutes(5);
|
|
private static readonly TimeSpan s_rsaEncryptedSecretMaxTokenAge = TimeSpan.FromHours(1);
|
|
|
|
/// <summary>
|
|
/// ECC encrypted secrets use the same one-hour age window as RSA encrypted secrets
|
|
/// to preserve consistent token replay tolerance across both encryption formats.
|
|
/// </summary>
|
|
private static readonly TimeSpan s_eccEncryptedSecretMaxTokenAge = TimeSpan.FromHours(1);
|
|
|
|
/// <summary>
|
|
/// Create secret
|
|
/// </summary>
|
|
public EncryptedSecret(
|
|
IServiceMessageContext context,
|
|
string securityPolicyUri,
|
|
CertificateCollection? senderIssuerCertificates,
|
|
Certificate receiverCertificate,
|
|
Nonce? receiverNonce,
|
|
Certificate? senderCertificate,
|
|
Nonce? senderNonce,
|
|
ICertificateValidatorEx? validator = null,
|
|
bool doNotEncodeSenderCertificate = false)
|
|
{
|
|
SenderCertificate = senderCertificate;
|
|
SenderIssuerCertificates = senderIssuerCertificates;
|
|
DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate;
|
|
SenderNonce = senderNonce;
|
|
ReceiverNonce = receiverNonce;
|
|
ReceiverCertificate = receiverCertificate;
|
|
Validator = validator;
|
|
SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri)
|
|
?? throw new ArgumentException(
|
|
$"Cannot resolve SecurityPolicy '{securityPolicyUri}'.",
|
|
nameof(securityPolicyUri));
|
|
Context = context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="EncryptedSecret"/> instance for RSAEncryptedSecret encryption/decryption.
|
|
/// </summary>
|
|
public static EncryptedSecret CreateForRsa(
|
|
IServiceMessageContext context,
|
|
string securityPolicyUri,
|
|
Certificate receiverCertificate,
|
|
Nonce? receiverNonce = null)
|
|
{
|
|
return new EncryptedSecret(
|
|
context: context,
|
|
securityPolicyUri: securityPolicyUri,
|
|
senderIssuerCertificates: null,
|
|
receiverCertificate: receiverCertificate,
|
|
receiverNonce: receiverNonce,
|
|
senderCertificate: null,
|
|
senderNonce: null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="EncryptedSecret"/> instance for ECC-based encryption and decryption operations.
|
|
/// </summary>
|
|
public static EncryptedSecret CreateForEcc(
|
|
IServiceMessageContext context,
|
|
string securityPolicyUri,
|
|
CertificateCollection senderIssuerCertificates,
|
|
Certificate receiverCertificate,
|
|
Nonce receiverNonce,
|
|
Certificate senderCertificate,
|
|
Nonce senderNonce,
|
|
ICertificateValidatorEx? validator = null,
|
|
bool doNotEncodeSenderCertificate = false)
|
|
{
|
|
return new EncryptedSecret(
|
|
context: context,
|
|
securityPolicyUri: securityPolicyUri,
|
|
senderIssuerCertificates: senderIssuerCertificates,
|
|
receiverCertificate: receiverCertificate,
|
|
receiverNonce: receiverNonce,
|
|
senderCertificate: senderCertificate,
|
|
senderNonce: senderNonce,
|
|
validator: validator,
|
|
doNotEncodeSenderCertificate: doNotEncodeSenderCertificate);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the X.509 certificate of the sender.
|
|
/// </summary>
|
|
public Certificate? SenderCertificate { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the collection of X.509 certificates of the sender's issuer.
|
|
/// </summary>
|
|
public CertificateCollection? SenderIssuerCertificates { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the sender's certificate should not be encoded.
|
|
/// </summary>
|
|
public bool DoNotEncodeSenderCertificate { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the nonce of the sender.
|
|
/// </summary>
|
|
public Nonce? SenderNonce { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the nonce of the receiver.
|
|
/// </summary>
|
|
public Nonce? ReceiverNonce { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the X.509 certificate of the receiver.
|
|
/// </summary>
|
|
public Certificate ReceiverCertificate { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the certificate validator.
|
|
/// </summary>
|
|
public ICertificateValidatorEx? Validator { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the security policy.
|
|
/// </summary>
|
|
public SecurityPolicyInfo SecurityPolicy { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Service message context to use
|
|
/// </summary>
|
|
public IServiceMessageContext Context { get; }
|
|
|
|
private static readonly byte[] s_secretLabel = System.Text.Encoding.UTF8.GetBytes("opcua-secret");
|
|
|
|
/// <summary>
|
|
/// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException"></exception>
|
|
private static void CreateKeysForEcc(
|
|
SecurityPolicyInfo securityPolicy,
|
|
Nonce localNonce,
|
|
Nonce remoteNonce,
|
|
bool forDecryption,
|
|
out byte[] encryptingKey,
|
|
out byte[] iv)
|
|
{
|
|
int encryptingKeySize = securityPolicy.SymmetricEncryptionKeyLength;
|
|
int blockSize = securityPolicy.InitializationVectorLength;
|
|
|
|
encryptingKey = new byte[encryptingKeySize];
|
|
iv = new byte[blockSize];
|
|
|
|
byte[] secret = localNonce.GenerateSecret(remoteNonce, null) ?? throw new InvalidOperationException("Failed to generate secret.");
|
|
byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize));
|
|
|
|
byte[] salt = Utils.Append(
|
|
keyLength,
|
|
s_secretLabel,
|
|
forDecryption ? remoteNonce.Data : localNonce.Data,
|
|
forDecryption ? localNonce.Data : remoteNonce.Data);
|
|
|
|
byte[] keyData = localNonce.DeriveKeyData(
|
|
secret!,
|
|
salt,
|
|
securityPolicy.KeyDerivationAlgorithm,
|
|
encryptingKeySize + blockSize);
|
|
|
|
Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length);
|
|
Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encrypts a secret using the specified nonce.
|
|
/// Dispatches to RSA encoding when <see cref="SecurityPolicyInfo.EphemeralKeyAlgorithm"/> is
|
|
/// <see cref="CertificateKeyAlgorithm.None"/>, otherwise dispatches to ECC encoding.
|
|
/// </summary>
|
|
/// <param name="secret">The secret to encrypt.</param>
|
|
/// <param name="nonce">The nonce to use for encryption.</param>
|
|
/// <returns>The encrypted secret.</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public byte[] Encrypt(byte[] secret, byte[] nonce)
|
|
{
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None)
|
|
{
|
|
return EncryptRsa(secret, nonce);
|
|
}
|
|
|
|
if (SenderCertificate == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Sender certificate is required for ECC encryption.");
|
|
}
|
|
|
|
int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate!);
|
|
|
|
using var encoder = new BinaryEncoder(Context);
|
|
|
|
// write header.
|
|
encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret);
|
|
encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary);
|
|
|
|
int lengthPosition = encoder.Position;
|
|
encoder.WriteUInt32(null, 0);
|
|
|
|
encoder.WriteString(null, SecurityPolicy.Uri);
|
|
|
|
byte[]? senderCertificate = null;
|
|
|
|
if (!DoNotEncodeSenderCertificate)
|
|
{
|
|
senderCertificate = SenderCertificate!.RawData;
|
|
|
|
if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0)
|
|
{
|
|
int blobSize = senderCertificate.Length;
|
|
|
|
foreach (Certificate issuer in SenderIssuerCertificates)
|
|
{
|
|
blobSize += issuer.RawData.Length;
|
|
}
|
|
|
|
byte[] blob = new byte[blobSize];
|
|
Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length);
|
|
|
|
int pos = senderCertificate.Length;
|
|
|
|
foreach (Certificate issuer in SenderIssuerCertificates)
|
|
{
|
|
byte[] data = issuer.RawData;
|
|
Buffer.BlockCopy(data, 0, blob, pos, data.Length);
|
|
pos += data.Length;
|
|
}
|
|
|
|
senderCertificate = blob;
|
|
}
|
|
}
|
|
|
|
encoder.WriteByteString(null, senderCertificate);
|
|
encoder.WriteDateTime(null, DateTime.UtcNow);
|
|
|
|
if (ReceiverNonce?.Data == null)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadArgumentsMissing,
|
|
"The receiver did not provide an ephemeral key.");
|
|
}
|
|
|
|
if (SenderNonce?.Data == null)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadArgumentsMissing,
|
|
"The sender nonce is required for ECC encryption.");
|
|
}
|
|
|
|
byte[] senderNonce = SenderNonce!.Data;
|
|
byte[] receiverNonce = ReceiverNonce!.Data;
|
|
|
|
encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8));
|
|
int senderNonceStart = encoder.Position;
|
|
encoder.WriteByteString(null, senderNonce);
|
|
int senderNonceEnd = encoder.Position;
|
|
encoder.WriteByteString(null, receiverNonce);
|
|
int receiverNonceEnd = encoder.Position;
|
|
|
|
// create keys.
|
|
CreateKeysForEcc(
|
|
SecurityPolicy,
|
|
SenderNonce,
|
|
ReceiverNonce,
|
|
false,
|
|
out byte[] encryptingKey,
|
|
out byte[] iv);
|
|
|
|
// reserves space for padding and tag that is added by SymmetricEncryptAndSign.
|
|
int startOfSecret = encoder.Position;
|
|
encoder.WriteByteString(null, nonce);
|
|
encoder.WriteByteString(null, secret);
|
|
|
|
int paddingCount = 0;
|
|
int tagLength = 0;
|
|
|
|
switch (SecurityPolicy.SymmetricEncryptionAlgorithm)
|
|
{
|
|
case SymmetricEncryptionAlgorithm.Aes128Cbc:
|
|
case SymmetricEncryptionAlgorithm.Aes256Cbc:
|
|
paddingCount = GetPaddingCount(SecurityPolicy.InitializationVectorLength, secret.Length, encoder.Position - startOfSecret);
|
|
tagLength = 0;
|
|
break;
|
|
case SymmetricEncryptionAlgorithm.Aes128Gcm:
|
|
case SymmetricEncryptionAlgorithm.Aes256Gcm:
|
|
case SymmetricEncryptionAlgorithm.ChaCha20Poly1305:
|
|
paddingCount = GetPaddingCount(16, secret.Length, encoder.Position - startOfSecret);
|
|
tagLength = SecurityPolicy.SymmetricSignatureLength;
|
|
break;
|
|
}
|
|
|
|
for (int ii = 0; ii < paddingCount; ii++)
|
|
{
|
|
encoder.WriteByte(null, (byte)paddingCount);
|
|
}
|
|
|
|
encoder.WriteByte(null, (byte)paddingCount);
|
|
encoder.WriteByte(null, 0);
|
|
|
|
int endOfSecret = encoder.Position;
|
|
|
|
// reserve space for the outer padding that SymmetricEncryptAndSign will add (CBC only).
|
|
int outerPaddingSize = 0;
|
|
if (SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc or SymmetricEncryptionAlgorithm.Aes256Cbc)
|
|
{
|
|
int blockSize = SecurityPolicy.InitializationVectorLength;
|
|
int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1;
|
|
int paddingSize = blockSize - ((endOfSecret - startOfSecret + paddingByteSize) % blockSize);
|
|
paddingSize %= blockSize;
|
|
outerPaddingSize = paddingSize + paddingByteSize;
|
|
|
|
for (int ii = 0; ii < outerPaddingSize; ii++)
|
|
{
|
|
encoder.WriteByte(null, 0xCD);
|
|
}
|
|
}
|
|
|
|
// save space for tag.
|
|
for (int ii = 0; ii < tagLength; ii++)
|
|
{
|
|
encoder.WriteByte(null, 0xAB);
|
|
}
|
|
|
|
// save space for signature.
|
|
for (int ii = 0; ii < signatureLength; ii++)
|
|
{
|
|
encoder.WriteByte(null, 0xDE);
|
|
}
|
|
|
|
byte[]? message = encoder.CloseAndReturnBuffer();
|
|
int length = message!.Length - lengthPosition - 4;
|
|
|
|
message[lengthPosition++] = (byte)(length & 0xFF);
|
|
message[lengthPosition++] = (byte)((length & 0xFF00) >> 8);
|
|
message[lengthPosition++] = (byte)((length & 0xFF0000) >> 16);
|
|
message[lengthPosition++] = (byte)((length & 0xFF000000) >> 24);
|
|
|
|
_ = CryptoUtils.SymmetricEncryptAndSign(
|
|
new ArraySegment<byte>(message, startOfSecret, endOfSecret - startOfSecret),
|
|
SecurityPolicy,
|
|
encryptingKey,
|
|
iv);
|
|
|
|
var dataToSign = new ArraySegment<byte>(message, 0, message.Length - signatureLength);
|
|
|
|
byte[]? signature = CryptoUtils.Sign(
|
|
dataToSign,
|
|
SenderCertificate,
|
|
SecurityPolicy.AsymmetricSignatureAlgorithm) ??
|
|
throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Failed to sign data.");
|
|
|
|
Buffer.BlockCopy(
|
|
signature!,
|
|
0,
|
|
message,
|
|
endOfSecret + outerPaddingSize + tagLength,
|
|
signatureLength);
|
|
|
|
return message;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encrypts a secret using RSAEncryptedSecret format.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public byte[] EncryptRsa(byte[] secret, byte[] nonce)
|
|
{
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
|
|
}
|
|
|
|
if (ReceiverCertificate == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadCertificateInvalid);
|
|
}
|
|
|
|
byte[]? signingKey = null;
|
|
byte[]? encryptingKey = null;
|
|
byte[]? iv = null;
|
|
byte[]? keyData = null;
|
|
byte[]? encryptedPayload = null;
|
|
|
|
try
|
|
{
|
|
signingKey = Nonce.CreateRandomNonceData(SecurityPolicy.DerivedSignatureKeyLength, false);
|
|
encryptingKey = Nonce.CreateRandomNonceData(SecurityPolicy.SymmetricEncryptionKeyLength, false);
|
|
iv = Nonce.CreateRandomNonceData(SecurityPolicy.InitializationVectorLength, false);
|
|
keyData = Utils.Append(signingKey, encryptingKey, iv);
|
|
|
|
ILogger logger = Context.Telemetry.CreateLogger<EncryptedSecret>();
|
|
byte[]? encryptedKeyData = SecurityPolicies.Encrypt(
|
|
ReceiverCertificate,
|
|
SecurityPolicy.Uri,
|
|
keyData,
|
|
logger).Data ??
|
|
throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Failed to encrypt key data.");
|
|
|
|
using var payloadEncoder = new BinaryEncoder(Context);
|
|
payloadEncoder.WriteByteString(null, nonce ?? []);
|
|
payloadEncoder.WriteByteString(null, secret);
|
|
byte[]? payload = payloadEncoder.CloseAndReturnBuffer();
|
|
|
|
int blockSize = SecurityPolicy.InitializationVectorLength;
|
|
int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1;
|
|
int paddingSize = blockSize - ((payload!.Length + paddingByteSize) % blockSize);
|
|
paddingSize %= blockSize;
|
|
|
|
encryptedPayload = new byte[payload.Length + paddingSize + paddingByteSize];
|
|
Buffer.BlockCopy(payload, 0, encryptedPayload, 0, payload.Length);
|
|
|
|
for (int ii = payload.Length; ii < payload.Length + paddingSize; ii++)
|
|
{
|
|
encryptedPayload[ii] = (byte)(paddingSize & 0xFF);
|
|
}
|
|
|
|
encryptedPayload[payload.Length + paddingSize] = (byte)(paddingSize & 0xFF);
|
|
if (paddingByteSize > 1)
|
|
{
|
|
encryptedPayload[payload.Length + paddingSize + 1] = (byte)((paddingSize >> 8) & 0xFF);
|
|
}
|
|
|
|
#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector
|
|
using var aes = Aes.Create();
|
|
aes.Mode = CipherMode.CBC;
|
|
aes.Padding = PaddingMode.None;
|
|
aes.Key = encryptingKey;
|
|
aes.IV = iv;
|
|
using ICryptoTransform encryptor = aes.CreateEncryptor();
|
|
#pragma warning restore CA5401
|
|
int bytesEncrypted = encryptor.TransformBlock(
|
|
encryptedPayload,
|
|
0,
|
|
encryptedPayload.Length,
|
|
encryptedPayload,
|
|
0);
|
|
if (bytesEncrypted != encryptedPayload.Length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadEncodingError);
|
|
}
|
|
|
|
ZeroMemory(payload);
|
|
|
|
using var encoder = new BinaryEncoder(Context);
|
|
encoder.WriteNodeId(null, DataTypeIds.RsaEncryptedSecret);
|
|
encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary);
|
|
int lengthPosition = encoder.Position;
|
|
encoder.WriteUInt32(null, 0);
|
|
encoder.WriteString(null, SecurityPolicy.Uri);
|
|
encoder.WriteByteString(null, ComputeSha1Hash(ReceiverCertificate.RawData));
|
|
encoder.WriteDateTime(null, DateTime.UtcNow);
|
|
encoder.WriteUInt16(null, (ushort)encryptedKeyData!.Length);
|
|
|
|
for (int ii = 0; ii < encryptedKeyData.Length; ii++)
|
|
{
|
|
encoder.WriteByte(null, encryptedKeyData[ii]);
|
|
}
|
|
|
|
for (int ii = 0; ii < encryptedPayload!.Length; ii++)
|
|
{
|
|
encoder.WriteByte(null, encryptedPayload[ii]);
|
|
}
|
|
|
|
for (int ii = 0; ii < SecurityPolicy.SymmetricSignatureLength; ii++)
|
|
{
|
|
encoder.WriteByte(null, 0);
|
|
}
|
|
|
|
byte[]? encodedSecret = encoder.CloseAndReturnBuffer();
|
|
int extensionObjectLength = encodedSecret!.Length - lengthPosition - 4;
|
|
encodedSecret[lengthPosition++] = (byte)(extensionObjectLength & 0xFF);
|
|
encodedSecret[lengthPosition++] = (byte)((extensionObjectLength >> 8) & 0xFF);
|
|
encodedSecret[lengthPosition++] = (byte)((extensionObjectLength >> 16) & 0xFF);
|
|
encodedSecret[lengthPosition] = (byte)((extensionObjectLength >> 24) & 0xFF);
|
|
|
|
int signatureStart = encodedSecret.Length - SecurityPolicy.SymmetricSignatureLength;
|
|
if (SecurityPolicy.SymmetricSignatureLength > 0)
|
|
{
|
|
using HMAC hmac = SecurityPolicy.CreateSignatureHmac(signingKey) ??
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadSecurityChecksFailed,
|
|
"The security policy does not support symmetric signatures required for RSAEncryptedSecret creation.");
|
|
byte[] signature = hmac.ComputeHash(encodedSecret, 0, signatureStart);
|
|
Buffer.BlockCopy(
|
|
signature,
|
|
0,
|
|
encodedSecret,
|
|
signatureStart,
|
|
Math.Min(signature.Length, SecurityPolicy.SymmetricSignatureLength));
|
|
}
|
|
|
|
return encodedSecret;
|
|
}
|
|
finally
|
|
{
|
|
if (signingKey != null)
|
|
{
|
|
ZeroMemory(signingKey);
|
|
}
|
|
|
|
if (encryptingKey != null)
|
|
{
|
|
ZeroMemory(encryptingKey);
|
|
}
|
|
|
|
if (iv != null)
|
|
{
|
|
ZeroMemory(iv);
|
|
}
|
|
|
|
if (keyData != null)
|
|
{
|
|
ZeroMemory(keyData);
|
|
}
|
|
|
|
ZeroMemory(encryptedPayload);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to decrypt an RSAEncryptedSecret payload.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public bool TryDecryptRsa(byte[] encodedSecret, byte[] expectedNonce, out byte[]? secret)
|
|
{
|
|
secret = null;
|
|
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ReceiverCertificate == null || encodedSecret == null || encodedSecret.Length < 8)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using var decoder = new BinaryDecoder(encodedSecret, Context);
|
|
NodeId typeId = decoder.ReadNodeId(null);
|
|
|
|
if (typeId != DataTypeIds.RsaEncryptedSecret)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null);
|
|
if (encoding != ExtensionObjectEncoding.Binary)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported);
|
|
}
|
|
|
|
int endOfSecret = checked((int)decoder.ReadUInt32(null) + decoder.Position);
|
|
if (endOfSecret > encodedSecret.Length || endOfSecret <= decoder.Position)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
string? encryptedSecretPolicyUri = decoder.ReadString(null);
|
|
if (!string.Equals(encryptedSecretPolicyUri, SecurityPolicy.Uri, StringComparison.Ordinal))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadSecurityPolicyRejected,
|
|
"Unexpected encrypted secret security policy: {0}",
|
|
encryptedSecretPolicyUri ?? "null");
|
|
}
|
|
|
|
ByteString certificateHash = decoder.ReadByteString(null);
|
|
if (certificateHash.Length > 0)
|
|
{
|
|
byte[] actualCertificateHash = ComputeSha1Hash(ReceiverCertificate.RawData);
|
|
if (!Utils.IsEqual(certificateHash.ToArray(), actualCertificateHash))
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadCertificateInvalid);
|
|
}
|
|
}
|
|
|
|
var signingTime = (DateTime)decoder.ReadDateTime(null);
|
|
DateTime now = DateTime.UtcNow;
|
|
// Accept tokens from the recent past to account for transit/processing delays while
|
|
// only allowing a small future clock skew to prevent replay with future-dated tokens.
|
|
if (signingTime < now - s_rsaEncryptedSecretMaxTokenAge ||
|
|
signingTime > now + s_rsaEncryptedSecretMaxClockSkew)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadInvalidTimestamp);
|
|
}
|
|
|
|
ushort keyDataLength = decoder.ReadUInt16(null);
|
|
if (keyDataLength == 0 || decoder.Position + keyDataLength > endOfSecret)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
int keyDataStart = decoder.Position;
|
|
_ = decoder.BaseStream.Seek(keyDataLength, SeekOrigin.Current);
|
|
|
|
int signatureLength = SecurityPolicy.SymmetricSignatureLength;
|
|
int signatureStart = endOfSecret - signatureLength;
|
|
if (signatureStart <= decoder.Position)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
byte[]? keyData = null;
|
|
byte[]? signingKey = null;
|
|
byte[]? encryptingKey = null;
|
|
byte[]? iv = null;
|
|
byte[]? encryptedPayload = null;
|
|
byte[]? payload = null;
|
|
|
|
try
|
|
{
|
|
ILogger logger = Context.Telemetry.CreateLogger<EncryptedSecret>();
|
|
keyData = RsaUtils.Decrypt(
|
|
new ArraySegment<byte>(encodedSecret, keyDataStart, keyDataLength),
|
|
ReceiverCertificate,
|
|
SecurityPolicy.AsymmetricEncryptionAlgorithm switch
|
|
{
|
|
AsymmetricEncryptionAlgorithm.RsaOaepSha1 => RsaUtils.Padding.OaepSHA1,
|
|
AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1 => RsaUtils.Padding.Pkcs1,
|
|
_ => RsaUtils.Padding.OaepSHA256
|
|
},
|
|
logger);
|
|
|
|
int expectedKeyDataLength =
|
|
SecurityPolicy.DerivedSignatureKeyLength +
|
|
SecurityPolicy.SymmetricEncryptionKeyLength +
|
|
SecurityPolicy.InitializationVectorLength;
|
|
|
|
if (keyData.Length < expectedKeyDataLength)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
signingKey = new byte[SecurityPolicy.DerivedSignatureKeyLength];
|
|
encryptingKey = new byte[SecurityPolicy.SymmetricEncryptionKeyLength];
|
|
iv = new byte[SecurityPolicy.InitializationVectorLength];
|
|
int keyMaterialOffset = signingKey.Length + encryptingKey.Length;
|
|
if (keyMaterialOffset + iv.Length > keyData.Length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
Buffer.BlockCopy(keyData, 0, signingKey, 0, signingKey.Length);
|
|
Buffer.BlockCopy(keyData, signingKey.Length, encryptingKey, 0, encryptingKey.Length);
|
|
Buffer.BlockCopy(keyData, keyMaterialOffset, iv, 0, iv.Length);
|
|
|
|
if (signatureLength > 0)
|
|
{
|
|
using HMAC hmac = SecurityPolicy.CreateSignatureHmac(signingKey) ??
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadSecurityChecksFailed,
|
|
"The security policy does not support symmetric signatures required for RSAEncryptedSecret validation.");
|
|
|
|
byte[] expectedSignature = hmac.ComputeHash(encodedSecret, 0, signatureStart);
|
|
int notValid = expectedSignature.Length == signatureLength ? 0 : 1;
|
|
|
|
for (int ii = 0; ii < signatureLength; ii++)
|
|
{
|
|
byte expectedByte = ii < expectedSignature.Length ? expectedSignature[ii] : (byte)0;
|
|
notValid |= expectedByte ^ encodedSecret[signatureStart + ii];
|
|
}
|
|
|
|
if (notValid != 0)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed);
|
|
}
|
|
}
|
|
|
|
int encryptedPayloadStart = decoder.Position;
|
|
int encryptedPayloadLength = signatureStart - encryptedPayloadStart;
|
|
encryptedPayload = new byte[encryptedPayloadLength];
|
|
Buffer.BlockCopy(encodedSecret, encryptedPayloadStart, encryptedPayload, 0, encryptedPayloadLength);
|
|
ArraySegment<byte> plainText = CryptoUtils.SymmetricDecryptAndVerify(
|
|
new ArraySegment<byte>(encryptedPayload!),
|
|
SecurityPolicy,
|
|
encryptingKey,
|
|
iv);
|
|
byte[] plainTextArray = plainText.Array ?? throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
payload = new byte[plainText.Count];
|
|
Buffer.BlockCopy(plainTextArray, plainText.Offset, payload, 0, payload.Length);
|
|
|
|
using var payloadDecoder = new BinaryDecoder(payload, Context);
|
|
|
|
ByteString actualNonce = payloadDecoder.ReadByteString(null);
|
|
if (expectedNonce != null && !Utils.IsEqual(actualNonce.ToArray(), expectedNonce))
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadNonceInvalid);
|
|
}
|
|
|
|
secret = payloadDecoder.ReadByteString(null).ToArray();
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
if (keyData != null)
|
|
{
|
|
ZeroMemory(keyData);
|
|
}
|
|
|
|
ZeroMemory(signingKey);
|
|
ZeroMemory(encryptingKey);
|
|
ZeroMemory(iv);
|
|
ZeroMemory(encryptedPayload);
|
|
ZeroMemory(payload);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to decrypt the encrypted secret and returns the plain secret.
|
|
/// </summary>
|
|
/// <param name="encryptedSecret">The encrypted secret bytes.</param>
|
|
/// <param name="expectedNonce">The expected nonce to validate.</param>
|
|
/// <param name="secret">The decrypted secret when decryption succeeds.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if decryption succeeds; otherwise <c>false</c>.
|
|
/// Routes to RSA or ECC decryption based on the configured security policy.
|
|
/// </returns>
|
|
public bool TryDecrypt(byte[] encryptedSecret, byte[] expectedNonce, out byte[]? secret)
|
|
{
|
|
secret = null;
|
|
|
|
if (encryptedSecret == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None)
|
|
{
|
|
return TryDecryptRsa(encryptedSecret, expectedNonce, out secret);
|
|
}
|
|
|
|
try
|
|
{
|
|
secret = Decrypt(
|
|
DateTime.UtcNow - s_eccEncryptedSecretMaxTokenAge,
|
|
expectedNonce,
|
|
encryptedSecret,
|
|
0,
|
|
encryptedSecret.Length,
|
|
Context.Telemetry);
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (ex is
|
|
ServiceResultException or
|
|
CryptographicException or
|
|
IOException or
|
|
FormatException or
|
|
ArgumentException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Async variant of <see cref="TryDecrypt"/>. Awaits the sender certificate validation
|
|
/// rather than blocking on it inside the cryptographic decode loop.
|
|
/// </summary>
|
|
public async ValueTask<(bool Success, byte[]? Secret)> TryDecryptAsync(
|
|
byte[] encryptedSecret,
|
|
byte[] expectedNonce,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (encryptedSecret == null)
|
|
{
|
|
return (false, null);
|
|
}
|
|
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None)
|
|
{
|
|
bool ok = TryDecryptRsa(encryptedSecret, expectedNonce, out byte[]? rsaSecret);
|
|
return (ok, rsaSecret);
|
|
}
|
|
|
|
try
|
|
{
|
|
byte[] secret = await DecryptAsync(
|
|
DateTime.UtcNow - s_eccEncryptedSecretMaxTokenAge,
|
|
expectedNonce,
|
|
encryptedSecret,
|
|
0,
|
|
encryptedSecret.Length,
|
|
Context.Telemetry,
|
|
cancellationToken).ConfigureAwait(false);
|
|
return (true, secret);
|
|
}
|
|
catch (Exception ex) when (ex is
|
|
ServiceResultException or
|
|
CryptographicException or
|
|
IOException or
|
|
FormatException or
|
|
ArgumentException)
|
|
{
|
|
return (false, null);
|
|
}
|
|
}
|
|
|
|
private static int GetPaddingCount(int blockSize, int secretLength, int dataLength)
|
|
{
|
|
dataLength += 2; // add padding size
|
|
|
|
int paddingCount =
|
|
dataLength % blockSize == 0
|
|
? 0
|
|
: blockSize - (dataLength % blockSize);
|
|
|
|
if (paddingCount + secretLength < blockSize)
|
|
{
|
|
paddingCount += blockSize;
|
|
}
|
|
|
|
return paddingCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the header for an ECC encrypted message and returns the encrypted data.
|
|
/// </summary>
|
|
/// <param name="dataToDecrypt">The data to decrypt.</param>
|
|
/// <param name="earliestTime">The earliest time allowed for the message signing time.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The encrypted data.</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
private async ValueTask<ArraySegment<byte>> VerifyHeaderForEccAsync(
|
|
ArraySegment<byte> dataToDecrypt,
|
|
DateTime earliestTime,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
byte[] decryptArray = dataToDecrypt.Array ?? throw new ArgumentNullException(nameof(dataToDecrypt));
|
|
|
|
using var decoder = new BinaryDecoder(
|
|
decryptArray,
|
|
dataToDecrypt.Offset,
|
|
dataToDecrypt.Count,
|
|
Context);
|
|
NodeId typeId = decoder.ReadNodeId(null);
|
|
|
|
if (typeId != DataTypeIds.EccEncryptedSecret)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown);
|
|
}
|
|
|
|
var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null);
|
|
|
|
if (encoding != ExtensionObjectEncoding.Binary)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported);
|
|
}
|
|
|
|
int length = (int)decoder.ReadUInt32(null) + decoder.Position;
|
|
|
|
SecurityPolicy = SecurityPolicies.GetInfo(decoder.ReadString(null)!) ?? throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
|
|
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
|
|
}
|
|
|
|
// extract the send certificate and any chain.
|
|
ByteString senderCertificate = decoder.ReadByteString(null);
|
|
|
|
if (senderCertificate.Length == 0)
|
|
{
|
|
if (SenderCertificate == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadCertificateInvalid);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using CertificateCollection senderCertificateChain = Utils.ParseCertificateChainBlob(
|
|
senderCertificate.ToArray(),
|
|
telemetry);
|
|
|
|
SenderCertificate = senderCertificateChain[0].AddRef();
|
|
SenderIssuerCertificates = [];
|
|
|
|
for (int ii = 1; ii < senderCertificateChain.Count; ii++)
|
|
{
|
|
SenderIssuerCertificates.Add(senderCertificateChain[ii]);
|
|
}
|
|
|
|
// validate the sender.
|
|
if (Validator != null)
|
|
{
|
|
await Validator.ValidateAsync(senderCertificateChain, ct: cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
// extract the send certificate and any chain.
|
|
var signingTime = (DateTime)decoder.ReadDateTime(null);
|
|
|
|
if (signingTime < earliestTime)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadInvalidTimestamp);
|
|
}
|
|
|
|
// extract the key data length.
|
|
ushort headerLength = decoder.ReadUInt16(null);
|
|
|
|
if (headerLength == 0 || headerLength > length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
// read the key data.
|
|
ByteString senderPublicKey = decoder.ReadByteString(null);
|
|
ByteString receiverPublicKey = decoder.ReadByteString(null);
|
|
|
|
if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadDecodingError,
|
|
"Unexpected key data length");
|
|
}
|
|
|
|
int startOfEncryption = decoder.Position;
|
|
|
|
SenderNonce = Nonce.CreateNonce(SecurityPolicy, senderPublicKey.ToArray());
|
|
|
|
if (!Utils.IsEqual(receiverPublicKey.ToArray(), ReceiverNonce?.Data))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadDecodingError,
|
|
"Unexpected receiver nonce.");
|
|
}
|
|
|
|
// check the signature.
|
|
int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate);
|
|
|
|
if (signatureLength >= length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
byte[] signature = new byte[signatureLength];
|
|
|
|
Buffer.BlockCopy(
|
|
decryptArray,
|
|
dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength,
|
|
signature,
|
|
0,
|
|
signatureLength);
|
|
|
|
var dataToSign = new ArraySegment<byte>(
|
|
decryptArray,
|
|
dataToDecrypt.Offset,
|
|
dataToDecrypt.Count - signatureLength);
|
|
|
|
if (!CryptoUtils.Verify(dataToSign, signature, SenderCertificate!, SecurityPolicy.AsymmetricSignatureAlgorithm))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadSecurityChecksFailed,
|
|
"Could not verify signature.");
|
|
}
|
|
|
|
// extract the encrypted data.
|
|
return new ArraySegment<byte>(
|
|
decryptArray,
|
|
dataToDecrypt.Offset + startOfEncryption,
|
|
dataToDecrypt.Count - startOfEncryption - signatureLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the header for an ECC encrypted message and returns the encrypted data.
|
|
/// </summary>
|
|
/// <param name="dataToDecrypt">The data to decrypt.</param>
|
|
/// <param name="earliestTime">The earliest time allowed for the message signing time.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <returns>The encrypted data.</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
/// <exception cref="ArgumentNullException"></exception>
|
|
private ArraySegment<byte> VerifyHeaderForEcc(
|
|
ArraySegment<byte> dataToDecrypt,
|
|
DateTime earliestTime,
|
|
ITelemetryContext telemetry)
|
|
{
|
|
byte[] decryptArray = dataToDecrypt.Array ?? throw new ArgumentNullException(nameof(dataToDecrypt));
|
|
|
|
using var decoder = new BinaryDecoder(
|
|
decryptArray,
|
|
dataToDecrypt.Offset,
|
|
dataToDecrypt.Count,
|
|
Context);
|
|
NodeId typeId = decoder.ReadNodeId(null);
|
|
|
|
if (typeId != DataTypeIds.EccEncryptedSecret)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown);
|
|
}
|
|
|
|
var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null);
|
|
|
|
if (encoding != ExtensionObjectEncoding.Binary)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported);
|
|
}
|
|
|
|
int length = (int)decoder.ReadUInt32(null) + decoder.Position;
|
|
|
|
SecurityPolicy = SecurityPolicies.GetInfo(decoder.ReadString(null)!) ?? throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
|
|
|
|
if (SecurityPolicy.EphemeralKeyAlgorithm == CertificateKeyAlgorithm.None)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
|
|
}
|
|
|
|
// extract the send certificate and any chain.
|
|
ByteString senderCertificate = decoder.ReadByteString(null);
|
|
|
|
if (senderCertificate.Length == 0)
|
|
{
|
|
if (SenderCertificate == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadCertificateInvalid);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using CertificateCollection senderCertificateChain = Utils.ParseCertificateChainBlob(
|
|
senderCertificate.ToArray(),
|
|
telemetry);
|
|
|
|
SenderCertificate = senderCertificateChain[0].AddRef();
|
|
SenderIssuerCertificates = [];
|
|
|
|
for (int ii = 1; ii < senderCertificateChain.Count; ii++)
|
|
{
|
|
SenderIssuerCertificates.Add(senderCertificateChain[ii]);
|
|
}
|
|
|
|
// validate the sender.
|
|
#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks
|
|
Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult();
|
|
#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks
|
|
}
|
|
|
|
// extract the send certificate and any chain.
|
|
var signingTime = (DateTime)decoder.ReadDateTime(null);
|
|
|
|
if (signingTime < earliestTime)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadInvalidTimestamp);
|
|
}
|
|
|
|
// extract the key data length.
|
|
ushort headerLength = decoder.ReadUInt16(null);
|
|
|
|
if (headerLength == 0 || headerLength > length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
// read the key data.
|
|
int senderNonceStart = decoder.Position;
|
|
ByteString senderPublicKey = decoder.ReadByteString(null);
|
|
int senderNonceEnd = decoder.Position;
|
|
ByteString receiverPublicKey = decoder.ReadByteString(null);
|
|
int receiverNonceEnd = decoder.Position;
|
|
|
|
if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadDecodingError,
|
|
"Unexpected key data length");
|
|
}
|
|
|
|
int startOfEncryption = decoder.Position;
|
|
|
|
SenderNonce = Nonce.CreateNonce(SecurityPolicy, senderPublicKey.ToArray());
|
|
|
|
if (!Utils.IsEqual(receiverPublicKey.ToArray(), ReceiverNonce?.Data))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadDecodingError,
|
|
"Unexpected receiver nonce.");
|
|
}
|
|
|
|
// check the signature.
|
|
int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate);
|
|
|
|
if (signatureLength >= length)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
byte[] signature = new byte[signatureLength];
|
|
|
|
Buffer.BlockCopy(
|
|
decryptArray,
|
|
dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength,
|
|
signature,
|
|
0,
|
|
signatureLength);
|
|
|
|
var dataToSign = new ArraySegment<byte>(
|
|
decryptArray,
|
|
dataToDecrypt.Offset,
|
|
dataToDecrypt.Count - signatureLength);
|
|
|
|
if (!CryptoUtils.Verify(dataToSign, signature, SenderCertificate!, SecurityPolicy.AsymmetricSignatureAlgorithm))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadSecurityChecksFailed,
|
|
"Could not verify signature.");
|
|
}
|
|
|
|
// extract the encrypted data.
|
|
return new ArraySegment<byte>(
|
|
decryptArray,
|
|
dataToDecrypt.Offset + startOfEncryption,
|
|
dataToDecrypt.Count - startOfEncryption - signatureLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrypts the specified data using the ECC algorithm.
|
|
/// </summary>
|
|
/// <param name="earliestTime">The earliest time allowed for the message.</param>
|
|
/// <param name="expectedNonce">The expected nonce value.</param>
|
|
/// <param name="data">The data to decrypt.</param>
|
|
/// <param name="offset">The offset of the data to decrypt.</param>
|
|
/// <param name="count">The number of bytes to decrypt.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The decrypted data.</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public async ValueTask<byte[]> DecryptAsync(
|
|
DateTime earliestTime,
|
|
byte[] expectedNonce,
|
|
byte[] data,
|
|
int offset,
|
|
int count,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
ArraySegment<byte> dataToDecrypt = await VerifyHeaderForEccAsync(
|
|
new ArraySegment<byte>(data, offset, count),
|
|
earliestTime,
|
|
telemetry,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return DecryptVerifiedEcc(dataToDecrypt, expectedNonce);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrypts the specified data using the ECC algorithm.
|
|
/// </summary>
|
|
/// <param name="earliestTime">The earliest time allowed for the message.</param>
|
|
/// <param name="expectedNonce">The expected nonce value.</param>
|
|
/// <param name="data">The data to decrypt.</param>
|
|
/// <param name="offset">The offset of the data to decrypt.</param>
|
|
/// <param name="count">The number of bytes to decrypt.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <returns>The decrypted data.</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public byte[] Decrypt(
|
|
DateTime earliestTime,
|
|
byte[] expectedNonce,
|
|
byte[] data,
|
|
int offset,
|
|
int count,
|
|
ITelemetryContext telemetry)
|
|
{
|
|
ArraySegment<byte> dataToDecrypt = VerifyHeaderForEcc(
|
|
new ArraySegment<byte>(data, offset, count),
|
|
earliestTime,
|
|
telemetry);
|
|
|
|
return DecryptVerifiedEcc(dataToDecrypt, expectedNonce);
|
|
}
|
|
|
|
private byte[] DecryptVerifiedEcc(ArraySegment<byte> dataToDecrypt, byte[] expectedNonce)
|
|
{
|
|
if (ReceiverNonce == null || SenderNonce == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadArgumentsMissing, "Receiver and sender nonces are required for ECC decryption.");
|
|
}
|
|
|
|
CreateKeysForEcc(
|
|
SecurityPolicy,
|
|
ReceiverNonce!,
|
|
SenderNonce!,
|
|
true,
|
|
out byte[] encryptingKey,
|
|
out byte[] iv);
|
|
|
|
ArraySegment<byte> plainText = CryptoUtils.SymmetricDecryptAndVerify(
|
|
dataToDecrypt,
|
|
SecurityPolicy,
|
|
encryptingKey,
|
|
iv);
|
|
|
|
using var decoder = new BinaryDecoder(
|
|
plainText.GetArray(),
|
|
plainText.Offset + dataToDecrypt.Offset,
|
|
plainText.Count - dataToDecrypt.Offset,
|
|
Context);
|
|
|
|
ByteString actualNonce = decoder.ReadByteString(null);
|
|
|
|
if (expectedNonce != null && expectedNonce.Length > 0)
|
|
{
|
|
int notvalid = expectedNonce.Length == actualNonce.Length ? 0 : 1;
|
|
|
|
for (int ii = 0; ii < expectedNonce.Length && ii < actualNonce.Length; ii++)
|
|
{
|
|
notvalid |= expectedNonce[ii] ^ actualNonce.Span[ii];
|
|
}
|
|
|
|
if (notvalid != 0)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadNonceInvalid);
|
|
}
|
|
}
|
|
|
|
ByteString key = decoder.ReadByteString(null);
|
|
byte paddingCount = decoder.ReadByte(null);
|
|
|
|
int error = 0;
|
|
|
|
for (int ii = 0; ii < paddingCount; ii++)
|
|
{
|
|
byte padding = decoder.ReadByte(null);
|
|
error |= padding & ~paddingCount;
|
|
}
|
|
|
|
byte highByte = decoder.ReadByte(null);
|
|
|
|
if (error != 0 || highByte != 0)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadDecodingError);
|
|
}
|
|
|
|
return key.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the SHA-1 hash required by the OPC UA RSAEncryptedSecret certificate hash field.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
|
|
private static byte[] ComputeSha1Hash(byte[] data)
|
|
{
|
|
if (data == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(data));
|
|
}
|
|
|
|
#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms
|
|
#if NET8_0_OR_GREATER
|
|
return SHA1.HashData(data);
|
|
#else
|
|
using var sha1 = SHA1.Create();
|
|
return sha1.ComputeHash(data);
|
|
#endif
|
|
#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms
|
|
}
|
|
|
|
private static void ZeroMemory(byte[]? buffer)
|
|
{
|
|
if (buffer == null)
|
|
{
|
|
return;
|
|
}
|
|
#if NET8_0_OR_GREATER
|
|
CryptographicOperations.ZeroMemory(buffer);
|
|
#else
|
|
Array.Clear(buffer, 0, buffer.Length);
|
|
#endif
|
|
}
|
|
}
|
|
}
|