diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs
index 29dc0e7d5..93d7b07e2 100644
--- a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs
+++ b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs
@@ -66,18 +66,7 @@ public static bool IsEccPolicy(string securityPolicyUri)
{
if (securityPolicyUri != null)
{
- switch (securityPolicyUri)
- {
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP256r1:
- case SecurityPolicies.ECC_brainpoolP384r1:
- case SecurityPolicies.ECC_curve25519:
- case SecurityPolicies.ECC_curve448:
- return true;
- default:
- return false;
- }
+ return securityPolicyUri.Contains("#ECC_", StringComparison.Ordinal);
}
return false;
@@ -497,767 +486,505 @@ public static bool Verify(
signature,
algorithm);
}
- }
- ///
- /// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC).
- ///
- public class EncryptedSecret
- {
///
- /// Create secret
+ /// Adds padding to a buffer. Input: buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding.
///
- public EncryptedSecret(
- IServiceMessageContext context,
- string securityPolicyUri,
- X509Certificate2Collection senderIssuerCertificates,
- X509Certificate2 receiverCertificate,
- Nonce receiverNonce,
- X509Certificate2 senderCertificate,
- Nonce senderNonce,
- CertificateValidator validator = null,
- bool doNotEncodeSenderCertificate = false)
+ /// buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding.
+ ///
+ /// Output: buffer with unencrypted data starting at 0; plaintext data starting at offset; padding added.
+ private static ArraySegment AddPadding(ArraySegment data, int blockSize)
{
- SenderCertificate = senderCertificate;
- SenderIssuerCertificates = senderIssuerCertificates;
- DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate;
- SenderNonce = senderNonce;
- ReceiverNonce = receiverNonce;
- ReceiverCertificate = receiverCertificate;
- Validator = validator;
- SecurityPolicyUri = securityPolicyUri;
- Context = context;
- }
-
- ///
- /// Gets or sets the X.509 certificate of the sender.
- ///
- public X509Certificate2 SenderCertificate { get; private set; }
-
- ///
- /// Gets or sets the collection of X.509 certificates of the sender's issuer.
- ///
- public X509Certificate2Collection SenderIssuerCertificates { get; private set; }
-
- ///
- /// Gets or sets a value indicating whether the sender's certificate should not be encoded.
- ///
- public bool DoNotEncodeSenderCertificate { get; }
+ int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1;
+ int paddingSize = blockSize - ((data.Count + paddingByteSize) % blockSize);
+ paddingSize %= blockSize;
- ///
- /// Gets or sets the nonce of the sender.
- ///
- public Nonce SenderNonce { get; private set; }
-
- ///
- /// Gets or sets the nonce of the receiver.
- ///
- public Nonce ReceiverNonce { get; }
-
- ///
- /// Gets or sets the X.509 certificate of the receiver.
- ///
- public X509Certificate2 ReceiverCertificate { get; }
-
- ///
- /// Gets or sets the certificate validator.
- ///
- public CertificateValidator Validator { get; }
-
- ///
- /// Gets or sets the security policy URI.
- ///
- public string SecurityPolicyUri { get; private set; }
-
- ///
- /// Service message context to use
- ///
- public IServiceMessageContext Context { get; }
+ int endOfData = data.Offset + data.Count;
+ int endOfPaddedData = data.Offset + data.Count + paddingSize + paddingByteSize;
- ///
- /// Encrypts a secret using the specified nonce, encrypting key, and initialization vector (IV).
- ///
- /// The secret to encrypt.
- /// The nonce to use for encryption.
- /// The key to use for encryption.
- /// The initialization vector to use for encryption.
- /// The encrypted secret.
- ///
- private byte[] EncryptSecret(
- byte[] secret,
- byte[] nonce,
- byte[] encryptingKey,
- byte[] iv)
- {
-#if CURVE25519
- bool useAuthenticatedEncryption = false;
- if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters
- || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters)
+ for (int ii = endOfData; ii < endOfPaddedData - paddingByteSize && ii < data.Array.Length; ii++)
{
- useAuthenticatedEncryption = true;
+ data.Array[ii] = (byte)(paddingSize & 0xFF);
}
-#endif
- byte[] dataToEncrypt = null;
- using (var encoder = new BinaryEncoder(Context))
- {
- encoder.WriteByteString(null, nonce);
- encoder.WriteByteString(null, secret);
-
- // add padding.
- int paddingSize = iv.Length - ((encoder.Position + 2) % iv.Length);
- paddingSize %= iv.Length;
-
- if (secret.Length + paddingSize < iv.Length)
- {
- paddingSize += iv.Length;
- }
-
- for (int ii = 0; ii < paddingSize; ii++)
- {
- encoder.WriteByte(null, (byte)(paddingSize & 0xFF));
- }
+ data.Array[endOfData + paddingSize] = (byte)(paddingSize & 0xFF);
- encoder.WriteUInt16(null, (ushort)paddingSize);
-
- dataToEncrypt = encoder.CloseAndReturnBuffer();
- }
-#if CURVE25519
- if (useAuthenticatedEncryption)
- {
- return EncryptWithChaCha20Poly1305(encryptingKey, iv, dataToEncrypt);
- }
-#endif
- using (var aes = Aes.Create())
+ if (blockSize > byte.MaxValue)
{
- aes.Mode = CipherMode.CBC;
- aes.Padding = PaddingMode.None;
- aes.Key = encryptingKey;
- aes.IV = iv;
-
-#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable
- using ICryptoTransform encryptor = aes.CreateEncryptor();
-#pragma warning restore CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable
- if (dataToEncrypt.Length % encryptor.InputBlockSize != 0)
- {
- throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- "Input data is not an even number of encryption blocks.");
- }
-
- encryptor.TransformBlock(dataToEncrypt, 0, dataToEncrypt.Length, dataToEncrypt, 0);
+ data.Array[endOfData + paddingSize + 1] = (byte)((paddingSize & 0xFF) >> 8);
}
- return dataToEncrypt;
+ return new ArraySegment(data.Array, data.Offset, data.Count + paddingSize + paddingByteSize);
}
-#if CURVE25519
///
- /// Encrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV).
+ /// Removes padding from a buffer. Input: buffer with unencrypted data starting at 0; plaintext including padding starting at offset; signature removed.
///
- /// The key used for encryption.
- /// The initialization vector used for encryption.
- /// The data to be encrypted.
- /// The encrypted data.
- private static byte[] EncryptWithChaCha20Poly1305(byte[] encryptingKey, byte[] iv, byte[] dataToEncrypt)
+ /// Input: buffer with unencrypted data starting at 0; plaintext including padding starting at offset; signature removed.
+ ///
+ /// Output: buffer with unencrypted data starting at 0; plaintext starting at offset; padding excluded.
+ ///
+ private static ArraySegment RemovePadding(ArraySegment data, int blockSize)
{
- Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}");
- Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}");
+ int paddingSize = data.Array[data.Offset + data.Count - 1];
+ int paddingByteSize = 1;
- int signatureLength = 16;
+ if (blockSize > byte.MaxValue)
+ {
+ paddingSize <<= 8;
+ paddingSize += data.Array[data.Offset + data.Count - 2];
+ paddingByteSize = 2;
+ }
- AeadParameters parameters = new AeadParameters(
- new KeyParameter(encryptingKey),
- signatureLength * 8,
- iv,
- null);
+ int notvalid = paddingSize < data.Count ? 0 : 1;
+ int start = data.Offset + data.Count - paddingSize - paddingByteSize;
- ChaCha20Poly1305 encryptor = new ChaCha20Poly1305();
- encryptor.Init(true, parameters);
+ for (int ii = data.Offset; ii < data.Count - paddingByteSize && ii < paddingSize; ii++)
+ {
+ if (start < 0 || start + ii >= data.Count)
+ {
+ notvalid |= 1;
+ continue;
+ }
- byte[] ciphertext = new byte[encryptor.GetOutputSize(dataToEncrypt.Length)];
- int length = encryptor.ProcessBytes(dataToEncrypt, 0, dataToEncrypt.Length, ciphertext, 0);
- length += encryptor.DoFinal(ciphertext, length);
+ notvalid |= data.Array[start + ii] ^ (paddingSize & 0xFF);
+ }
- if (ciphertext.Length != length)
+ if (notvalid != 0)
{
- throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- $"CipherText not the expected size. [{ciphertext.Length} != {length}]");
+ throw new CryptographicException("Invalid padding.");
}
- return ciphertext;
+ return new ArraySegment(data.Array, 0, data.Offset + data.Count - paddingSize - paddingByteSize);
}
///
- /// Decrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV).
+ /// Encrypts the buffer using the algorithm specified by the security policy.
///
- /// The key used for encryption.
- /// The initialization vector used for encryption.
- /// The data to be decrypted.
- /// The offset in the data to start decrypting from.
- /// The number of bytes to decrypt.
- /// An containing the decrypted data.
- /// Thrown if the plaintext is not the expected size or too short, or if the nonce is invalid.
- private ArraySegment DecryptWithChaCha20Poly1305(
+ /// The data to encrypt.
+ /// The security policy to use.
+ /// The key to use for encryption.
+ /// The initialization vector to use for encryption.
+ /// The key to use for signing.
+ /// If TRUE, the data is not encrypted.
+ /// The encrypted buffer.
+ ///
+ public static ArraySegment SymmetricEncryptAndSign(
+ ArraySegment data,
+ SecurityPolicyInfo securityPolicy,
byte[] encryptingKey,
byte[] iv,
- byte[] dataToDecrypt,
- int offset,
- int count)
+ byte[] signingKey = null,
+ bool signOnly = false)
{
- Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}");
- Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}");
-
- int signatureLength = 16;
-
- AeadParameters parameters = new AeadParameters(
- new KeyParameter(encryptingKey),
- signatureLength * 8,
- iv,
- null);
+ SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm;
- ChaCha20Poly1305 decryptor = new ChaCha20Poly1305();
- decryptor.Init(false, parameters);
-
- byte[] plaintext = new byte[decryptor.GetOutputSize(count)];
- int length = decryptor.ProcessBytes(dataToDecrypt, offset, count, plaintext, 0);
- length += decryptor.DoFinal(plaintext, length);
-
- if (plaintext.Length != length || plaintext.Length < iv.Length)
+ if (algorithm == SymmetricEncryptionAlgorithm.None)
{
- throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- $"PlainText not the expected size or too short. [{count} != {length}]");
+ return data;
}
- ushort paddingSize = plaintext[length - 1];
- paddingSize <<= 8;
- paddingSize += plaintext[length - 2];
-
- int notvalid = (paddingSize < length) ? 0 : 1;
- int start = length - paddingSize - 2;
-
- for (int ii = 0; ii < length - 2 && ii < paddingSize; ii++)
+ if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm)
{
- if (start < 0 || start + ii >= plaintext.Length)
- {
- notvalid |= 1;
- continue;
- }
-
- notvalid |= plaintext[start + ii] ^ (paddingSize & 0xFF);
+#if NET8_0_OR_GREATER
+ return EncryptWithAesGcm(encryptingKey, iv, signOnly, data);
+#else
+ throw new NotSupportedException("AES-GCM requires .NET 8 or greater.");
+#endif
}
- if (notvalid != 0)
+ if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305)
{
- throw new ServiceResultException(StatusCodes.BadNonceInvalid);
- }
-
- return new ArraySegment(plaintext, 0, start);
- }
+#if NET8_0_OR_GREATER
+ return EncryptWithChaCha20Poly1305(
+ data,
+ encryptingKey,
+ iv,
+ signOnly,
+ true);
+#else
+ throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater.");
#endif
+ }
- ///
- /// Decrypts the specified data using the provided encrypting key and initialization vector (IV).
- ///
- /// The data to decrypt.
- /// The offset in the data to start decrypting from.
- /// The number of bytes to decrypt.
- /// The key to use for decryption.
- /// The initialization vector to use for decryption.
- /// The decrypted data.
- /// Thrown if the input data is not an even number of encryption blocks or if the nonce is invalid.
- private static ArraySegment DecryptSecret(
- byte[] dataToDecrypt,
- int offset,
- int count,
- byte[] encryptingKey,
- byte[] iv)
- {
-#if CURVE25519
- bool useAuthenticatedEncryption = false;
- if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters
- || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters)
+ if (!signOnly)
{
- useAuthenticatedEncryption = true;
+ data = AddPadding(data, iv.Length);
}
- if (useAuthenticatedEncryption)
+
+ if (signingKey != null)
{
- return DecryptWithChaCha20Poly1305(encryptingKey, iv, dataToDecrypt, offset, count);
+ using HMAC hmac = securityPolicy.CreateSignatureHmac(signingKey);
+ byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count);
+
+ Buffer.BlockCopy(
+ hash,
+ 0,
+ data.Array,
+ data.Offset + data.Count,
+ hash.Length);
+
+ data = new ArraySegment(
+ data.Array,
+ data.Offset,
+ data.Count + hash.Length);
}
-#endif
- using (var aes = Aes.Create())
+
+ if (!signOnly)
{
+ using var aes = Aes.Create();
+
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.Key = encryptingKey;
aes.IV = iv;
- using ICryptoTransform decryptor = aes.CreateDecryptor();
- if (count % decryptor.InputBlockSize != 0)
- {
- throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- "Input data is not an even number of encryption blocks.");
- }
+ using ICryptoTransform encryptor = aes.CreateEncryptor();
- decryptor.TransformBlock(dataToDecrypt, offset, count, dataToDecrypt, offset);
+ encryptor.TransformBlock(
+ data.Array,
+ data.Offset,
+ data.Count,
+ data.Array,
+ data.Offset);
}
- ushort paddingSize = dataToDecrypt[offset + count - 1];
- paddingSize <<= 8;
- paddingSize += dataToDecrypt[offset + count - 2];
+ return new ArraySegment(data.Array, 0, data.Offset + data.Count);
+ }
- int notvalid = paddingSize < count ? 0 : 1;
- int start = offset + count - paddingSize - 2;
+#if NET8_0_OR_GREATER
+ private const int kChaChaPolyIvLength = 12;
+ private const int kChaChaPolyTagLength = 16;
- for (int ii = 0; ii < count - 2 && ii < paddingSize; ii++)
+ private static ArraySegment EncryptWithChaCha20Poly1305(
+ ArraySegment data,
+ byte[] encryptingKey,
+ byte[] iv,
+ bool signOnly,
+ bool noPadding)
+ {
+ if (encryptingKey == null || encryptingKey.Length != 32)
{
- if (start < 0 || start + ii >= dataToDecrypt.Length)
- {
- notvalid |= 1;
- continue;
- }
+ throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey));
+ }
- notvalid |= dataToDecrypt[start + ii] ^ (paddingSize & 0xFF);
+ if (iv == null || iv.Length != kChaChaPolyIvLength)
+ {
+ throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv));
}
- if (notvalid != 0)
+ if (!noPadding && !signOnly)
{
- throw new ServiceResultException(StatusCodes.BadNonceInvalid);
+ data = AddPadding(data, iv.Length);
}
- return new ArraySegment(dataToDecrypt, offset, count - paddingSize);
- }
+ byte[] ciphertext = new byte[signOnly ? 0 : data.Count];
+ byte[] tag = new byte[kChaChaPolyTagLength]; // ChaCha20-Poly1305/AES-GCM uses 128-bit authentication tag
- private static readonly byte[] s_label = System.Text.Encoding.UTF8.GetBytes("opcua-secret");
+ var extraData = new ReadOnlySpan(
+ data.Array,
+ 0,
+ signOnly ? data.Offset + data.Count : data.Offset);
- ///
- /// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption.
- ///
- /// The security policy URI.
- /// The sender nonce.
- /// The receiver nonce.
- /// if set to true, creates the keys for decryption; otherwise, creates the keys for encryption.
- /// The encrypting key.
- /// The initialization vector (IV).
- private static void CreateKeysForEcc(
- string securityPolicyUri,
- Nonce senderNonce,
- Nonce receiverNonce,
- bool forDecryption,
- out byte[] encryptingKey,
- out byte[] iv)
- {
- int encryptingKeySize;
- int blockSize;
- HashAlgorithmName algorithmName;
+ using var chacha = new ChaCha20Poly1305(encryptingKey);
- switch (securityPolicyUri)
+ chacha.Encrypt(
+ iv,
+ signOnly ? Array.Empty() : data,
+ ciphertext,
+ tag,
+ extraData);
+
+ // Return layout: [associated data | ciphertext | tag]
+ if (!signOnly)
{
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_brainpoolP256r1:
- blockSize = 16;
- encryptingKeySize = 16;
- algorithmName = HashAlgorithmName.SHA256;
- break;
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP384r1:
- encryptingKeySize = 32;
- blockSize = 16;
- algorithmName = HashAlgorithmName.SHA384;
- break;
- case SecurityPolicies.ECC_curve25519:
- case SecurityPolicies.ECC_curve448:
- encryptingKeySize = 32;
- blockSize = 12;
- algorithmName = HashAlgorithmName.SHA256;
- break;
- default:
- encryptingKeySize = 32;
- blockSize = 16;
- algorithmName = HashAlgorithmName.SHA256;
- break;
+ Buffer.BlockCopy(ciphertext, 0, data.Array, data.Offset, ciphertext.Length);
}
- encryptingKey = new byte[encryptingKeySize];
- iv = new byte[blockSize];
+ Buffer.BlockCopy(tag, 0, data.Array, data.Offset + data.Count, tag.Length);
- byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize));
- byte[] salt = Utils.Append(keyLength, s_label, senderNonce.Data, receiverNonce.Data);
+ return new ArraySegment(
+ data.Array,
+ 0,
+ data.Offset + data.Count + kChaChaPolyTagLength);
+ }
+#endif
- byte[] keyData;
- if (forDecryption)
+#if NET8_0_OR_GREATER
+ private static ArraySegment DecryptWithChaCha20Poly1305(
+ ArraySegment data,
+ byte[] encryptingKey,
+ byte[] iv,
+ bool signOnly,
+ bool noPadding)
+ {
+ if (encryptingKey == null || encryptingKey.Length != 32)
{
- keyData = receiverNonce.DeriveKey(
- senderNonce,
- salt,
- algorithmName,
- encryptingKeySize + blockSize);
+ throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey));
}
- else
+
+ if (iv == null || iv.Length != kChaChaPolyIvLength)
{
- keyData = senderNonce.DeriveKey(
- receiverNonce,
- salt,
- algorithmName,
- encryptingKeySize + blockSize);
+ throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv));
}
- Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length);
- Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length);
- }
-
- ///
- /// Encrypts a secret using the specified nonce.
- ///
- /// The secret to encrypt.
- /// The nonce to use for encryption.
- /// The encrypted secret.
- public byte[] Encrypt(byte[] secret, byte[] nonce)
- {
- byte[] encryptingKey = null;
- byte[] iv = null;
- byte[] message = null;
- int lengthPosition = 0;
-
- int signatureLength = EccUtils.GetSignatureLength(SenderCertificate);
-
- using (var encoder = new BinaryEncoder(Context))
+ if (data.Count < kChaChaPolyTagLength) // Must at least contain tag
{
- // write header.
- encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret);
- encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary);
-
- lengthPosition = encoder.Position;
- encoder.WriteUInt32(null, 0);
-
- encoder.WriteString(null, SecurityPolicyUri);
-
- byte[] senderCertificate = null;
+ throw new ArgumentException("Ciphertext too short.", nameof(data));
+ }
- if (!DoNotEncodeSenderCertificate)
- {
- senderCertificate = SenderCertificate.RawData;
+ byte[] plaintext = new byte[data.Count - kChaChaPolyTagLength];
- if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0)
- {
- int blobSize = senderCertificate.Length;
+ var encryptedData = new ArraySegment(
+ data.Array,
+ data.Offset,
+ signOnly ? 0 : data.Count - kChaChaPolyTagLength);
- foreach (X509Certificate2 issuer in SenderIssuerCertificates)
- {
- blobSize += issuer.RawData.Length;
- }
+ var tag = new ArraySegment(
+ data.Array,
+ data.Offset + data.Count - kChaChaPolyTagLength,
+ kChaChaPolyTagLength);
- byte[] blob = new byte[blobSize];
- Buffer.BlockCopy(senderCertificate, 0, blob, 0, senderCertificate.Length);
+ var extraData = new ReadOnlySpan(
+ data.Array,
+ 0,
+ signOnly ? data.Offset + data.Count - kChaChaPolyTagLength : data.Offset);
- int pos = senderCertificate.Length;
+ using var chacha = new ChaCha20Poly1305(encryptingKey);
- foreach (X509Certificate2 issuer in SenderIssuerCertificates)
- {
- byte[] data = issuer.RawData;
- Buffer.BlockCopy(data, 0, blob, pos, data.Length);
- pos += data.Length;
- }
+ chacha.Decrypt(
+ iv,
+ encryptedData,
+ tag,
+ signOnly ? [] : plaintext,
+ extraData);
- senderCertificate = blob;
- }
- }
+ // Return layout: [associated data | plaintext]
+ if (!signOnly)
+ {
+ Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count);
+ }
- encoder.WriteByteString(null, senderCertificate);
- encoder.WriteDateTime(null, DateTime.UtcNow);
+ if (!noPadding && !signOnly)
+ {
+ return RemovePadding(new ArraySegment(data.Array, data.Offset, data.Count - kChaChaPolyTagLength), iv.Length);
+ }
- byte[] senderNonce = SenderNonce.Data;
- byte[] receiverNonce = ReceiverNonce.Data;
+ return new ArraySegment(data.Array, 0, data.Offset + data.Count - kChaChaPolyTagLength);
+ }
+#endif
- encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8));
- encoder.WriteByteString(null, senderNonce);
- encoder.WriteByteString(null, receiverNonce);
+#if NET8_0_OR_GREATER
+ private const int kAesGcmIvLength = 12;
+ private const int kAesGcmTagLength = 16;
- // create keys.
- if (EccUtils.IsEccPolicy(SecurityPolicyUri))
- {
- CreateKeysForEcc(
- SecurityPolicyUri,
- SenderNonce,
- ReceiverNonce,
- false,
- out encryptingKey,
- out iv);
- }
+ private static ArraySegment EncryptWithAesGcm(
+ byte[] encryptingKey,
+ byte[] iv,
+ bool signOnly,
+ ArraySegment data)
+ {
+ if (encryptingKey == null)
+ {
+ throw new ArgumentNullException(nameof(encryptingKey));
+ }
- // encrypt secret,
- byte[] encryptedData = EncryptSecret(secret, nonce, encryptingKey, iv);
+ if (iv == null || iv.Length != kAesGcmIvLength)
+ {
+ throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv));
+ }
- // append encrypted secret.
- for (int ii = 0; ii < encryptedData.Length; ii++)
- {
- encoder.WriteByte(null, encryptedData[ii]);
- }
+ if (!signOnly)
+ {
+ data = AddPadding(data, iv.Length);
+ }
- // save space for signature.
- for (int ii = 0; ii < signatureLength; ii++)
- {
- encoder.WriteByte(null, 0);
- }
+ byte[] ciphertext = new byte[signOnly ? 0 : data.Count];
+ byte[] tag = new byte[kAesGcmTagLength]; // AES-GCM uses 128-bit authentication tag
- message = encoder.CloseAndReturnBuffer();
- }
+ var extraData = new ReadOnlySpan(
+ data.Array,
+ 0,
+ signOnly ? data.Offset + data.Count : data.Offset);
- int length = message.Length - lengthPosition - 4;
+ using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength);
- message[lengthPosition++] = (byte)(length & 0xFF);
- message[lengthPosition++] = (byte)((length & 0xFF00) >> 8);
- message[lengthPosition++] = (byte)((length & 0xFF0000) >> 16);
- message[lengthPosition++] = (byte)((length & 0xFF000000) >> 24);
+ aesGcm.Encrypt(
+ iv,
+ signOnly ? Array.Empty() : data,
+ ciphertext,
+ tag,
+ extraData);
- // get the algorithm used for the signature.
- HashAlgorithmName signatureAlgorithm;
- switch (SecurityPolicyUri)
+ // Return layout: [associated data | ciphertext | tag]
+ if (!signOnly)
{
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP384r1:
- signatureAlgorithm = HashAlgorithmName.SHA384;
- break;
- default:
- signatureAlgorithm = HashAlgorithmName.SHA256;
- break;
+ Buffer.BlockCopy(ciphertext, 0, data.Array, data.Offset, ciphertext.Length);
}
- var dataToSign = new ArraySegment(message, 0, message.Length - signatureLength);
- byte[] signature = EccUtils.Sign(dataToSign, SenderCertificate, signatureAlgorithm);
- Buffer.BlockCopy(
- signature,
+ Buffer.BlockCopy(tag, 0, data.Array, data.Offset + data.Count, tag.Length);
+
+ return new ArraySegment(
+ data.Array,
0,
- message,
- message.Length - signatureLength,
- signatureLength);
- return message;
+ data.Offset + data.Count + kAesGcmTagLength);
}
+#endif
- ///
- /// Verifies the header for an ECC encrypted message and returns the encrypted data.
- ///
- /// The data to decrypt.
- /// The earliest time allowed for the message signing time.
- /// The telemetry context to use to create obvservability instruments
- /// The encrypted data.
- ///
- private ArraySegment VerifyHeaderForEcc(
- ArraySegment dataToDecrypt,
- DateTime earliestTime,
- ITelemetryContext telemetry)
+#if NET8_0_OR_GREATER
+ private static ArraySegment DecryptWithAesGcm(
+ ArraySegment data,
+ byte[] encryptingKey,
+ byte[] iv,
+ bool signOnly)
{
- using var decoder = new BinaryDecoder(
- dataToDecrypt.Array,
- dataToDecrypt.Offset,
- dataToDecrypt.Count,
- Context);
- NodeId typeId = decoder.ReadNodeId(null);
-
- if (typeId != DataTypeIds.EccEncryptedSecret)
+ if (encryptingKey == null)
{
- throw new ServiceResultException(StatusCodes.BadDataTypeIdUnknown);
+ throw new ArgumentNullException(nameof(encryptingKey));
}
- var encoding = (ExtensionObjectEncoding)decoder.ReadByte(null);
+ if (iv == null || iv.Length != kAesGcmIvLength)
+ {
+ throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv));
+ }
- if (encoding != ExtensionObjectEncoding.Binary)
+ if (data.Count < kAesGcmTagLength) // Must at least contain tag
{
- throw new ServiceResultException(StatusCodes.BadDataEncodingUnsupported);
+ throw new ArgumentException("Ciphertext too short.", nameof(data));
}
- uint length = decoder.ReadUInt32(null);
+ byte[] plaintext = new byte[data.Count - kAesGcmTagLength];
- // get the start of data.
- int startOfData = decoder.Position + dataToDecrypt.Offset;
+ var encryptedData = new ArraySegment(
+ data.Array,
+ data.Offset,
+ signOnly ? 0 : data.Count - kAesGcmTagLength);
- SecurityPolicyUri = decoder.ReadString(null);
+ var tag = new ArraySegment(
+ data.Array,
+ data.Offset + data.Count - kAesGcmTagLength,
+ kAesGcmTagLength);
- if (!EccUtils.IsEccPolicy(SecurityPolicyUri))
- {
- throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
- }
+ var extraData = new ReadOnlySpan(
+ data.Array,
+ 0,
+ signOnly ? data.Offset + data.Count - kAesGcmTagLength : data.Offset);
- // get the algorithm used for the signature.
- HashAlgorithmName signatureAlgorithm;
+ using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength);
- switch (SecurityPolicyUri)
+ aesGcm.Decrypt(
+ iv,
+ encryptedData,
+ tag,
+ signOnly ? [] : plaintext,
+ extraData);
+
+ // Return layout: [associated data | plaintext]
+ if (!signOnly)
{
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP384r1:
- signatureAlgorithm = HashAlgorithmName.SHA384;
- break;
- default:
- signatureAlgorithm = HashAlgorithmName.SHA256;
- break;
+ Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count);
}
- // extract the send certificate and any chain.
- byte[] senderCertificate = decoder.ReadByteString(null);
-
- if (senderCertificate == null || senderCertificate.Length == 0)
+ if (!signOnly)
{
- if (SenderCertificate == null)
- {
- throw new ServiceResultException(StatusCodes.BadCertificateInvalid);
- }
+ return RemovePadding(new ArraySegment(data.Array, data.Offset, data.Count - kAesGcmTagLength), iv.Length);
}
- else
- {
- X509Certificate2Collection senderCertificateChain = Utils.ParseCertificateChainBlob(
- senderCertificate,
- telemetry);
- SenderCertificate = senderCertificateChain[0];
- SenderIssuerCertificates = [];
+ return new ArraySegment(data.Array, 0, data.Offset + data.Count - kAesGcmTagLength);
+ }
+#endif
- for (int ii = 1; ii < senderCertificateChain.Count; ii++)
- {
- SenderIssuerCertificates.Add(senderCertificateChain[ii]);
- }
+ ///
+ /// Decrypts the buffer using the algorithm specified by the security policy.
+ ///
+ ///
+ ///
+ public static ArraySegment SymmetricDecryptAndVerify(
+ ArraySegment data,
+ SecurityPolicyInfo securityPolicy,
+ byte[] encryptingKey,
+ byte[] iv,
+ byte[] signingKey = null,
+ bool signOnly = false)
+ {
+ SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm;
- // validate the sender.
- Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult();
+ if (algorithm == SymmetricEncryptionAlgorithm.None)
+ {
+ return data;
}
- // extract the send certificate and any chain.
- DateTime signingTime = decoder.ReadDateTime(null);
-
- if (signingTime < earliestTime)
+ if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm)
{
- throw new ServiceResultException(StatusCodes.BadInvalidTimestamp);
+#if NET8_0_OR_GREATER
+ return DecryptWithAesGcm(data, encryptingKey, iv, signOnly);
+#else
+ throw new NotSupportedException("AES-GCM requires .NET 8 or greater.");
+#endif
}
- // extract the policy header.
- ushort headerLength = decoder.ReadUInt16(null);
-
- if (headerLength == 0 || headerLength > length)
+ if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305)
{
- throw new ServiceResultException(StatusCodes.BadDecodingError);
+#if NET8_0_OR_GREATER
+ return DecryptWithChaCha20Poly1305(
+ data,
+ encryptingKey,
+ iv,
+ signOnly,
+ true);
+#else
+ throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater.");
+#endif
}
- // read the policy header.
- byte[] senderPublicKey = decoder.ReadByteString(null);
- byte[] receiverPublicKey = decoder.ReadByteString(null);
-
- if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8)
+ if (!signOnly)
{
- throw new ServiceResultException(
- StatusCodes.BadDecodingError,
- "Unexpected policy header length");
- }
+ using var aes = Aes.Create();
- int startOfEncryption = decoder.Position;
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.None;
+ aes.Key = encryptingKey;
+ aes.IV = iv;
- SenderNonce = Nonce.CreateNonce(SecurityPolicyUri, senderPublicKey);
+ using ICryptoTransform decryptor = aes.CreateDecryptor();
- if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data))
- {
- throw new ServiceResultException(
- StatusCodes.BadDecodingError,
- "Unexpected receiver nonce.");
+ decryptor.TransformBlock(
+ data.Array,
+ data.Offset,
+ data.Count,
+ data.Array,
+ data.Offset);
}
- // check the signature.
- int signatureLength = EccUtils.GetSignatureLength(SenderCertificate);
+ int isNotValid = 0;
- if (signatureLength >= length)
+ if (signingKey != null)
{
- throw new ServiceResultException(StatusCodes.BadDecodingError);
- }
-
- byte[] signature = new byte[signatureLength];
- Buffer.BlockCopy(
- dataToDecrypt.Array,
- startOfData + (int)length - signatureLength,
- signature,
- 0,
- signatureLength);
+ using HMAC hmac = securityPolicy.CreateSignatureHmac(signingKey);
+ byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count - (hmac.HashSize / 8));
+ for (int ii = 0; ii < hash.Length; ii++)
+ {
+ int index = data.Offset + data.Count - hash.Length + ii;
+ isNotValid |= data.Array[index] != hash[ii] ? 1 : 0;
+ }
- var dataToSign = new ArraySegment(
- dataToDecrypt.Array,
- 0,
- startOfData + (int)length - signatureLength);
+ data = new ArraySegment(
+ data.Array,
+ data.Offset,
+ data.Count - hash.Length);
+ }
- if (!EccUtils.Verify(dataToSign, signature, SenderCertificate, signatureAlgorithm))
+ if (!signOnly)
{
- throw new ServiceResultException(
- StatusCodes.BadSecurityChecksFailed,
- "Could not verify signature.");
+ data = RemovePadding(data, iv.Length);
}
- // extract the encrypted data.
- return new ArraySegment(
- dataToDecrypt.Array,
- startOfEncryption,
- (int)length - (startOfEncryption - startOfData + signatureLength));
- }
-
- ///
- /// Decrypts the specified data using the ECC algorithm.
- ///
- /// The earliest time allowed for the message.
- /// The expected nonce value.
- /// The data to decrypt.
- /// The offset of the data to decrypt.
- /// The number of bytes to decrypt.
- /// The telemetry context to use to create obvservability instruments
- /// The decrypted data.
- ///
- public byte[] Decrypt(
- DateTime earliestTime,
- byte[] expectedNonce,
- byte[] data,
- int offset,
- int count,
- ITelemetryContext telemetry)
- {
- ArraySegment dataToDecrypt = VerifyHeaderForEcc(
- new ArraySegment(data, offset, count),
- earliestTime,
- telemetry);
-
- CreateKeysForEcc(
- SecurityPolicyUri,
- SenderNonce,
- ReceiverNonce,
- true,
- out byte[] encryptingKey,
- out byte[] iv);
-
- ArraySegment plainText = DecryptSecret(
- dataToDecrypt.Array,
- dataToDecrypt.Offset,
- dataToDecrypt.Count,
- encryptingKey,
- iv);
-
- using var decoder = new BinaryDecoder(
- plainText.Array,
- plainText.Offset,
- plainText.Count,
- Context);
- byte[] actualNonce = decoder.ReadByteString(null);
-
- if (expectedNonce != null && expectedNonce.Length > 0)
+ if (isNotValid != 0)
{
- int notvalid = expectedNonce.Length == actualNonce.Length ? 0 : 1;
-
- for (int ii = 0; ii < expectedNonce.Length && ii < actualNonce.Length; ii++)
- {
- notvalid |= expectedNonce[ii] ^ actualNonce[ii];
- }
-
- if (notvalid != 0)
- {
- throw new ServiceResultException(StatusCodes.BadNonceInvalid);
- }
+ throw new CryptographicException("Invalid signature.");
}
- return decoder.ReadByteString(null);
+ return new ArraySegment(data.Array, 0, data.Offset + data.Count);
}
}
}
diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs
new file mode 100644
index 000000000..049c97af0
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs
@@ -0,0 +1,784 @@
+/* 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.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+#if CURVE25519
+using Org.BouncyCastle.Crypto.Modes;
+using Org.BouncyCastle.Crypto.Parameters;
+#endif
+
+namespace Opc.Ua
+{
+ ///
+ /// Utility class for encrypting and decrypting secrets using Elliptic Curve Cryptography (ECC).
+ ///
+ public class EncryptedSecret
+ {
+ ///
+ /// Create secret
+ ///
+ public EncryptedSecret(
+ IServiceMessageContext context,
+ string securityPolicyUri,
+ X509Certificate2Collection senderIssuerCertificates,
+ X509Certificate2 receiverCertificate,
+ Nonce receiverNonce,
+ X509Certificate2 senderCertificate,
+ Nonce senderNonce,
+ CertificateValidator validator = null,
+ bool doNotEncodeSenderCertificate = false)
+ {
+ SenderCertificate = senderCertificate;
+ SenderIssuerCertificates = senderIssuerCertificates;
+ DoNotEncodeSenderCertificate = doNotEncodeSenderCertificate;
+ SenderNonce = senderNonce;
+ ReceiverNonce = receiverNonce;
+ ReceiverCertificate = receiverCertificate;
+ Validator = validator;
+ SecurityPolicyUri = securityPolicyUri;
+ Context = context;
+ }
+
+ ///
+ /// Gets or sets the X.509 certificate of the sender.
+ ///
+ public X509Certificate2 SenderCertificate { get; private set; }
+
+ ///
+ /// Gets or sets the collection of X.509 certificates of the sender's issuer.
+ ///
+ public X509Certificate2Collection SenderIssuerCertificates { get; private set; }
+
+ ///
+ /// Gets or sets a value indicating whether the sender's certificate should not be encoded.
+ ///
+ public bool DoNotEncodeSenderCertificate { get; }
+
+ ///
+ /// Gets or sets the nonce of the sender.
+ ///
+ public Nonce SenderNonce { get; private set; }
+
+ ///
+ /// Gets or sets the nonce of the receiver.
+ ///
+ public Nonce ReceiverNonce { get; }
+
+ ///
+ /// Gets or sets the X.509 certificate of the receiver.
+ ///
+ public X509Certificate2 ReceiverCertificate { get; }
+
+ ///
+ /// Gets or sets the certificate validator.
+ ///
+ public CertificateValidator Validator { get; }
+
+ ///
+ /// Gets or sets the security policy URI.
+ ///
+ public string SecurityPolicyUri { get; private set; }
+
+ ///
+ /// Service message context to use
+ ///
+ public IServiceMessageContext Context { get; }
+
+ ///
+ /// Encrypts a secret using the specified nonce, encrypting key, and initialization vector (IV).
+ ///
+ /// The secret to encrypt.
+ /// The nonce to use for encryption.
+ /// The key to use for encryption.
+ /// The initialization vector to use for encryption.
+ /// The encrypted secret.
+ ///
+ private byte[] EncryptSecret(
+ byte[] secret,
+ byte[] nonce,
+ byte[] encryptingKey,
+ byte[] iv)
+ {
+#if CURVE25519
+ bool useAuthenticatedEncryption = false;
+ if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters
+ || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters)
+ {
+ useAuthenticatedEncryption = true;
+ }
+#endif
+ byte[] dataToEncrypt = null;
+
+ using (var encoder = new BinaryEncoder(Context))
+ {
+ encoder.WriteByteString(null, nonce);
+ encoder.WriteByteString(null, secret);
+
+ // add padding.
+ int paddingSize = iv.Length - ((encoder.Position + 2) % iv.Length);
+ paddingSize %= iv.Length;
+
+ if (secret.Length + paddingSize < iv.Length)
+ {
+ paddingSize += iv.Length;
+ }
+
+ for (int ii = 0; ii < paddingSize; ii++)
+ {
+ encoder.WriteByte(null, (byte)(paddingSize & 0xFF));
+ }
+
+ encoder.WriteUInt16(null, (ushort)paddingSize);
+
+ dataToEncrypt = encoder.CloseAndReturnBuffer();
+ }
+#if CURVE25519
+ if (useAuthenticatedEncryption)
+ {
+ return EncryptWithChaCha20Poly1305(encryptingKey, iv, dataToEncrypt);
+ }
+#endif
+ using (var aes = Aes.Create())
+ {
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.None;
+ aes.Key = encryptingKey;
+ aes.IV = iv;
+
+#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable
+ using ICryptoTransform encryptor = aes.CreateEncryptor();
+#pragma warning restore CA5401 // Symmetric encryption uses non-default initialization vector, which could be potentially repeatable
+ if (dataToEncrypt.Length % encryptor.InputBlockSize != 0)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityChecksFailed,
+ "Input data is not an even number of encryption blocks.");
+ }
+
+ encryptor.TransformBlock(dataToEncrypt, 0, dataToEncrypt.Length, dataToEncrypt, 0);
+ }
+
+ return dataToEncrypt;
+ }
+
+#if CURVE25519
+ ///
+ /// Encrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV).
+ ///
+ /// The key used for encryption.
+ /// The initialization vector used for encryption.
+ /// The data to be encrypted.
+ /// The encrypted data.
+ private static byte[] EncryptWithChaCha20Poly1305(byte[] encryptingKey, byte[] iv, byte[] dataToEncrypt)
+ {
+ Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}");
+ Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}");
+
+ int signatureLength = 16;
+
+ AeadParameters parameters = new AeadParameters(
+ new KeyParameter(encryptingKey),
+ signatureLength * 8,
+ iv,
+ null);
+
+ ChaCha20Poly1305 encryptor = new ChaCha20Poly1305();
+ encryptor.Init(true, parameters);
+
+ byte[] ciphertext = new byte[encryptor.GetOutputSize(dataToEncrypt.Length)];
+ int length = encryptor.ProcessBytes(dataToEncrypt, 0, dataToEncrypt.Length, ciphertext, 0);
+ length += encryptor.DoFinal(ciphertext, length);
+
+ if (ciphertext.Length != length)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityChecksFailed,
+ $"CipherText not the expected size. [{ciphertext.Length} != {length}]");
+ }
+
+ return ciphertext;
+ }
+
+ ///
+ /// Decrypts the given data using the ChaCha20Poly1305 algorithm with the provided key and initialization vector (IV).
+ ///
+ /// The key used for encryption.
+ /// The initialization vector used for encryption.
+ /// The data to be decrypted.
+ /// The offset in the data to start decrypting from.
+ /// The number of bytes to decrypt.
+ /// An containing the decrypted data.
+ /// Thrown if the plaintext is not the expected size or too short, or if the nonce is invalid.
+ private ArraySegment DecryptWithChaCha20Poly1305(
+ byte[] encryptingKey,
+ byte[] iv,
+ byte[] dataToDecrypt,
+ int offset,
+ int count)
+ {
+ Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}");
+ Utils.Trace($"EncryptIV={Utils.ToHexString(iv)}");
+
+ int signatureLength = 16;
+
+ AeadParameters parameters = new AeadParameters(
+ new KeyParameter(encryptingKey),
+ signatureLength * 8,
+ iv,
+ null);
+
+ ChaCha20Poly1305 decryptor = new ChaCha20Poly1305();
+ decryptor.Init(false, parameters);
+
+ byte[] plaintext = new byte[decryptor.GetOutputSize(count)];
+ int length = decryptor.ProcessBytes(dataToDecrypt, offset, count, plaintext, 0);
+ length += decryptor.DoFinal(plaintext, length);
+
+ if (plaintext.Length != length || plaintext.Length < iv.Length)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityChecksFailed,
+ $"PlainText not the expected size or too short. [{count} != {length}]");
+ }
+
+ ushort paddingSize = plaintext[length - 1];
+ paddingSize <<= 8;
+ paddingSize += plaintext[length - 2];
+
+ int notvalid = (paddingSize < length) ? 0 : 1;
+ int start = length - paddingSize - 2;
+
+ for (int ii = 0; ii < length - 2 && ii < paddingSize; ii++)
+ {
+ if (start < 0 || start + ii >= plaintext.Length)
+ {
+ notvalid |= 1;
+ continue;
+ }
+
+ notvalid |= plaintext[start + ii] ^ (paddingSize & 0xFF);
+ }
+
+ if (notvalid != 0)
+ {
+ throw new ServiceResultException(StatusCodes.BadNonceInvalid);
+ }
+
+ return new ArraySegment(plaintext, 0, start);
+ }
+#endif
+
+ ///
+ /// Decrypts the specified data using the provided encrypting key and initialization vector (IV).
+ ///
+ /// The data to decrypt.
+ /// The offset in the data to start decrypting from.
+ /// The number of bytes to decrypt.
+ /// The key to use for decryption.
+ /// The initialization vector to use for decryption.
+ /// The decrypted data.
+ /// Thrown if the input data is not an even number of encryption blocks or if the nonce is invalid.
+ private ArraySegment DecryptSecret(
+ byte[] dataToDecrypt,
+ int offset,
+ int count,
+ byte[] encryptingKey,
+ byte[] iv)
+ {
+#if CURVE25519
+ bool useAuthenticatedEncryption = false;
+ if (SenderCertificate.BcCertificate.GetPublicKey() is Ed25519PublicKeyParameters
+ || SenderCertificate.BcCertificate.GetPublicKey() is Ed448PublicKeyParameters)
+ {
+ useAuthenticatedEncryption = true;
+ }
+ if (useAuthenticatedEncryption)
+ {
+ return DecryptWithChaCha20Poly1305(encryptingKey, iv, dataToDecrypt, offset, count);
+ }
+#endif
+ using (var aes = Aes.Create())
+ {
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.None;
+ aes.Key = encryptingKey;
+ aes.IV = iv;
+
+ using ICryptoTransform decryptor = aes.CreateDecryptor();
+ if (count % decryptor.InputBlockSize != 0)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityChecksFailed,
+ "Input data is not an even number of encryption blocks.");
+ }
+
+ decryptor.TransformBlock(dataToDecrypt, offset, count, dataToDecrypt, offset);
+ }
+
+ ushort paddingSize = dataToDecrypt[offset + count - 1];
+ paddingSize <<= 8;
+ paddingSize += dataToDecrypt[offset + count - 2];
+
+ int notvalid = paddingSize < count ? 0 : 1;
+ int start = offset + count - paddingSize - 2;
+
+ for (int ii = 0; ii < count - 2 && ii < paddingSize; ii++)
+ {
+ if (start < 0 || start + ii >= dataToDecrypt.Length)
+ {
+ notvalid |= 1;
+ continue;
+ }
+
+ notvalid |= dataToDecrypt[start + ii] ^ (paddingSize & 0xFF);
+ }
+
+ if (notvalid != 0)
+ {
+ throw new ServiceResultException(StatusCodes.BadNonceInvalid);
+ }
+
+ return new ArraySegment(dataToDecrypt, offset, count - paddingSize);
+ }
+
+ private static readonly byte[] s_label = System.Text.Encoding.UTF8.GetBytes("opcua-secret");
+
+ ///
+ /// Creates the encrypting key and initialization vector (IV) for Elliptic Curve Cryptography (ECC) encryption or decryption.
+ ///
+ /// The security policy URI.
+ /// The sender nonce.
+ /// The receiver nonce.
+ /// if set to true, creates the keys for decryption; otherwise, creates the keys for encryption.
+ /// The encrypting key.
+ /// The initialization vector (IV).
+ private static void CreateKeysForEcc(
+ string securityPolicyUri,
+ Nonce senderNonce,
+ Nonce receiverNonce,
+ bool forDecryption,
+ out byte[] encryptingKey,
+ out byte[] iv)
+ {
+ int encryptingKeySize;
+ int blockSize;
+ HashAlgorithmName algorithmName;
+
+ switch (securityPolicyUri)
+ {
+ case SecurityPolicies.ECC_nistP256:
+ case SecurityPolicies.ECC_brainpoolP256r1:
+ blockSize = 16;
+ encryptingKeySize = 16;
+ algorithmName = HashAlgorithmName.SHA256;
+ break;
+ case SecurityPolicies.ECC_nistP384:
+ case SecurityPolicies.ECC_brainpoolP384r1:
+ encryptingKeySize = 32;
+ blockSize = 16;
+ algorithmName = HashAlgorithmName.SHA384;
+ break;
+ case SecurityPolicies.ECC_curve25519:
+ case SecurityPolicies.ECC_curve448:
+ encryptingKeySize = 32;
+ blockSize = 12;
+ algorithmName = HashAlgorithmName.SHA256;
+ break;
+ default:
+ encryptingKeySize = 32;
+ blockSize = 16;
+ algorithmName = HashAlgorithmName.SHA256;
+ break;
+ }
+
+ encryptingKey = new byte[encryptingKeySize];
+ iv = new byte[blockSize];
+
+ byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize));
+ byte[] salt = Utils.Append(keyLength, s_label, senderNonce.Data, receiverNonce.Data);
+
+ byte[] keyData;
+ if (forDecryption)
+ {
+ keyData = receiverNonce.DeriveKey(
+ senderNonce,
+ salt,
+ algorithmName,
+ encryptingKeySize + blockSize);
+ }
+ else
+ {
+ keyData = senderNonce.DeriveKey(
+ receiverNonce,
+ salt,
+ algorithmName,
+ encryptingKeySize + blockSize);
+ }
+
+ Buffer.BlockCopy(keyData, 0, encryptingKey, 0, encryptingKey.Length);
+ Buffer.BlockCopy(keyData, encryptingKeySize, iv, 0, iv.Length);
+ }
+
+ ///
+ /// Encrypts a secret using the specified nonce.
+ ///
+ /// The secret to encrypt.
+ /// The nonce to use for encryption.
+ /// The encrypted secret.
+ public byte[] Encrypt(byte[] secret, byte[] nonce)
+ {
+ byte[] encryptingKey = null;
+ byte[] iv = null;
+ byte[] message = null;
+ int lengthPosition = 0;
+
+ int signatureLength = EccUtils.GetSignatureLength(SenderCertificate);
+
+ using (var encoder = new BinaryEncoder(Context))
+ {
+ // write header.
+ encoder.WriteNodeId(null, DataTypeIds.EccEncryptedSecret);
+ encoder.WriteByte(null, (byte)ExtensionObjectEncoding.Binary);
+
+ lengthPosition = encoder.Position;
+ encoder.WriteUInt32(null, 0);
+
+ encoder.WriteString(null, SecurityPolicyUri);
+
+ byte[] senderCertificate = null;
+
+ if (!DoNotEncodeSenderCertificate)
+ {
+ senderCertificate = SenderCertificate.RawData;
+
+ if (SenderIssuerCertificates != null && SenderIssuerCertificates.Count > 0)
+ {
+ int blobSize = senderCertificate.Length;
+
+ foreach (X509Certificate2 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 (X509Certificate2 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);
+
+ byte[] senderNonce = SenderNonce.Data;
+ byte[] receiverNonce = ReceiverNonce.Data;
+
+ encoder.WriteUInt16(null, (ushort)(senderNonce.Length + receiverNonce.Length + 8));
+ encoder.WriteByteString(null, senderNonce);
+ encoder.WriteByteString(null, receiverNonce);
+
+ // create keys.
+ if (EccUtils.IsEccPolicy(SecurityPolicyUri))
+ {
+ CreateKeysForEcc(
+ SecurityPolicyUri,
+ SenderNonce,
+ ReceiverNonce,
+ false,
+ out encryptingKey,
+ out iv);
+ }
+
+ // encrypt secret,
+ byte[] encryptedData = EncryptSecret(secret, nonce, encryptingKey, iv);
+
+ // append encrypted secret.
+ for (int ii = 0; ii < encryptedData.Length; ii++)
+ {
+ encoder.WriteByte(null, encryptedData[ii]);
+ }
+
+ // save space for signature.
+ for (int ii = 0; ii < signatureLength; ii++)
+ {
+ encoder.WriteByte(null, 0);
+ }
+
+ 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);
+
+ // get the algorithm used for the signature.
+ HashAlgorithmName signatureAlgorithm;
+ switch (SecurityPolicyUri)
+ {
+ case SecurityPolicies.ECC_nistP384:
+ case SecurityPolicies.ECC_brainpoolP384r1:
+ signatureAlgorithm = HashAlgorithmName.SHA384;
+ break;
+ default:
+ signatureAlgorithm = HashAlgorithmName.SHA256;
+ break;
+ }
+
+ var dataToSign = new ArraySegment(message, 0, message.Length - signatureLength);
+ byte[] signature = EccUtils.Sign(dataToSign, SenderCertificate, signatureAlgorithm);
+ Buffer.BlockCopy(
+ signature,
+ 0,
+ message,
+ message.Length - signatureLength,
+ signatureLength);
+ return message;
+ }
+
+ ///
+ /// Verifies the header for an ECC encrypted message and returns the encrypted data.
+ ///
+ /// The data to decrypt.
+ /// The earliest time allowed for the message signing time.
+ /// The telemetry context to use to create obvservability instruments
+ /// The encrypted data.
+ ///
+ private ArraySegment VerifyHeaderForEcc(
+ ArraySegment dataToDecrypt,
+ DateTime earliestTime,
+ ITelemetryContext telemetry)
+ {
+ using var decoder = new BinaryDecoder(
+ dataToDecrypt.Array,
+ 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);
+ }
+
+ uint length = decoder.ReadUInt32(null);
+
+ // get the start of data.
+ int startOfData = decoder.Position + dataToDecrypt.Offset;
+
+ SecurityPolicyUri = decoder.ReadString(null);
+
+ if (!EccUtils.IsEccPolicy(SecurityPolicyUri))
+ {
+ throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
+ }
+
+ // get the algorithm used for the signature.
+ HashAlgorithmName signatureAlgorithm;
+
+ switch (SecurityPolicyUri)
+ {
+ case SecurityPolicies.ECC_nistP384:
+ case SecurityPolicies.ECC_brainpoolP384r1:
+ signatureAlgorithm = HashAlgorithmName.SHA384;
+ break;
+ default:
+ signatureAlgorithm = HashAlgorithmName.SHA256;
+ break;
+ }
+
+ // extract the send certificate and any chain.
+ byte[] senderCertificate = decoder.ReadByteString(null);
+
+ if (senderCertificate == null || senderCertificate.Length == 0)
+ {
+ if (SenderCertificate == null)
+ {
+ throw new ServiceResultException(StatusCodes.BadCertificateInvalid);
+ }
+ }
+ else
+ {
+ X509Certificate2Collection senderCertificateChain = Utils.ParseCertificateChainBlob(
+ senderCertificate,
+ telemetry);
+
+ SenderCertificate = senderCertificateChain[0];
+ SenderIssuerCertificates = [];
+
+ for (int ii = 1; ii < senderCertificateChain.Count; ii++)
+ {
+ SenderIssuerCertificates.Add(senderCertificateChain[ii]);
+ }
+
+ // validate the sender.
+ Validator?.ValidateAsync(senderCertificateChain, default).GetAwaiter().GetResult();
+ }
+
+ // extract the send certificate and any chain.
+ DateTime signingTime = decoder.ReadDateTime(null);
+
+ if (signingTime < earliestTime)
+ {
+ throw new ServiceResultException(StatusCodes.BadInvalidTimestamp);
+ }
+
+ // extract the policy header.
+ ushort headerLength = decoder.ReadUInt16(null);
+
+ if (headerLength == 0 || headerLength > length)
+ {
+ throw new ServiceResultException(StatusCodes.BadDecodingError);
+ }
+
+ // read the policy header.
+ byte[] senderPublicKey = decoder.ReadByteString(null);
+ byte[] receiverPublicKey = decoder.ReadByteString(null);
+
+ if (headerLength != senderPublicKey.Length + receiverPublicKey.Length + 8)
+ {
+ throw new ServiceResultException(
+ StatusCodes.BadDecodingError,
+ "Unexpected policy header length");
+ }
+
+ int startOfEncryption = decoder.Position;
+
+ SenderNonce = Nonce.CreateNonce(SecurityPolicyUri, senderPublicKey);
+
+ if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data))
+ {
+ throw new ServiceResultException(
+ StatusCodes.BadDecodingError,
+ "Unexpected receiver nonce.");
+ }
+
+ // check the signature.
+ int signatureLength = EccUtils.GetSignatureLength(SenderCertificate);
+
+ if (signatureLength >= length)
+ {
+ throw new ServiceResultException(StatusCodes.BadDecodingError);
+ }
+
+ byte[] signature = new byte[signatureLength];
+ Buffer.BlockCopy(
+ dataToDecrypt.Array,
+ startOfData + (int)length - signatureLength,
+ signature,
+ 0,
+ signatureLength);
+
+ var dataToSign = new ArraySegment(
+ dataToDecrypt.Array,
+ 0,
+ startOfData + (int)length - signatureLength);
+
+ if (!EccUtils.Verify(dataToSign, signature, SenderCertificate, signatureAlgorithm))
+ {
+ throw new ServiceResultException(
+ StatusCodes.BadSecurityChecksFailed,
+ "Could not verify signature.");
+ }
+
+ // extract the encrypted data.
+ return new ArraySegment(
+ dataToDecrypt.Array,
+ startOfEncryption,
+ (int)length - (startOfEncryption - startOfData + signatureLength));
+ }
+
+ ///
+ /// Decrypts the specified data using the ECC algorithm.
+ ///
+ /// The earliest time allowed for the message.
+ /// The expected nonce value.
+ /// The data to decrypt.
+ /// The offset of the data to decrypt.
+ /// The number of bytes to decrypt.
+ /// The telemetry context to use to create obvservability instruments
+ /// The decrypted data.
+ ///
+ public byte[] Decrypt(
+ DateTime earliestTime,
+ byte[] expectedNonce,
+ byte[] data,
+ int offset,
+ int count,
+ ITelemetryContext telemetry)
+ {
+ ArraySegment dataToDecrypt = VerifyHeaderForEcc(
+ new ArraySegment(data, offset, count),
+ earliestTime,
+ telemetry);
+
+ CreateKeysForEcc(
+ SecurityPolicyUri,
+ SenderNonce,
+ ReceiverNonce,
+ true,
+ out byte[] encryptingKey,
+ out byte[] iv);
+
+ ArraySegment plainText = DecryptSecret(
+ dataToDecrypt.Array,
+ dataToDecrypt.Offset,
+ dataToDecrypt.Count,
+ encryptingKey,
+ iv);
+
+ using var decoder = new BinaryDecoder(
+ plainText.Array,
+ plainText.Offset,
+ plainText.Count,
+ Context);
+ byte[] 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[ii];
+ }
+
+ if (notvalid != 0)
+ {
+ throw new ServiceResultException(StatusCodes.BadNonceInvalid);
+ }
+ }
+
+ return decoder.ReadByteString(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs
index c16dd613b..a2920d6e8 100644
--- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs
+++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs
@@ -103,6 +103,12 @@ public static class SecurityPolicies
private static bool IsPlatformSupportedName(string name)
{
+ // If name contains BaseUri trim the BaseUri part
+ if (name.StartsWith(BaseUri, StringComparison.Ordinal))
+ {
+ name = name.Substring(BaseUri.Length);
+ }
+
// all RSA
if (name.Equals(nameof(None), StringComparison.Ordinal) ||
name.Equals(nameof(Basic256), StringComparison.Ordinal) ||
@@ -149,6 +155,29 @@ private static bool IsPlatformSupportedName(string name)
return false;
}
+ ///
+ /// Returns the info object associated with the SecurityPolicyUri.
+ /// Supports both full URI and short name (without BaseUri prefix).
+ ///
+ public static SecurityPolicyInfo GetInfo(string securityPolicyUri)
+ {
+ // Try full URI lookup first (e.g., "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256")
+ if (s_securityPolicyUriToInfo.Value.TryGetValue(securityPolicyUri, out SecurityPolicyInfo info) &&
+ IsPlatformSupportedName(info.Name))
+ {
+ return info;
+ }
+
+ // Try short name lookup (e.g., "Basic256Sha256")
+ if (s_securityPolicyNameToInfo.Value.TryGetValue(securityPolicyUri, out info) &&
+ IsPlatformSupportedName(info.Name))
+ {
+ return info;
+ }
+
+ return null;
+ }
+
///
/// Returns the uri associated with the display name. This includes http and all
/// other supported platform security policies.
@@ -652,6 +681,63 @@ public static bool Verify(
return keyValuePairs.ToFrozenDictionary();
#else
return new ReadOnlyDictionary(keyValuePairs);
+#endif
+ });
+
+ ///
+ /// Creates a dictionary of uris to SecurityPolicyInfo excluding base uri
+ ///
+ private static readonly Lazy> s_securityPolicyUriToInfo =
+ new(() =>
+ {
+#if NET8_0_OR_GREATER
+ return s_securityPolicyNameToInfo.Value.ToFrozenDictionary(k => k.Value.Uri, k => k.Value);
+#else
+ return new ReadOnlyDictionary(
+ s_securityPolicyNameToInfo.Value.ToDictionary(k => k.Value.Uri, k => k.Value));
+#endif
+ });
+
+ ///
+ /// Creates a dictionary for names to SecurityPolicyInfo excluding base uri
+ ///
+ private static readonly Lazy> s_securityPolicyNameToInfo =
+ new(() =>
+ {
+ FieldInfo[] policyFields = typeof(SecurityPolicies).GetFields(
+ BindingFlags.Public | BindingFlags.Static);
+
+ FieldInfo[] infoFields = typeof(SecurityPolicyInfo).GetFields(
+ BindingFlags.Public | BindingFlags.Static);
+
+ var keyValuePairs = new Dictionary();
+ foreach (FieldInfo field in policyFields)
+ {
+ string policyUri = (string)field.GetValue(typeof(SecurityPolicies));
+ if (field.Name == nameof(BaseUri) ||
+ field.Name == nameof(Https) ||
+ !policyUri.StartsWith(BaseUri, StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ // Find the corresponding SecurityPolicyInfo field by name
+ FieldInfo infoField = Array.Find(infoFields, f => f.Name == field.Name);
+ if (infoField != null && infoField.FieldType == typeof(SecurityPolicyInfo))
+ {
+ SecurityPolicyInfo info = (SecurityPolicyInfo)infoField.GetValue(null);
+ keyValuePairs.Add(field.Name, info);
+ }
+ else
+ {
+ // Fallback to creating a minimal instance for unknown policies
+ keyValuePairs.Add(field.Name, new SecurityPolicyInfo(policyUri, field.Name));
+ }
+ }
+#if NET8_0_OR_GREATER
+ return keyValuePairs.ToFrozenDictionary();
+#else
+ return new ReadOnlyDictionary(keyValuePairs);
#endif
});
}
diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs
new file mode 100644
index 000000000..af75058c8
--- /dev/null
+++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs
@@ -0,0 +1,708 @@
+/* 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.Security.Cryptography;
+
+namespace Opc.Ua
+{
+ ///
+ /// Defines constants for key security policies.
+ ///
+ public class SecurityPolicyInfo
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// The unique identifier.
+ /// The display name.
+ ///
+ public SecurityPolicyInfo(string uri, string name = null)
+ {
+ if (string.IsNullOrEmpty(uri))
+ {
+ throw new ArgumentException("The URI is not a valid security policy.", nameof(uri));
+ }
+
+ Uri = uri;
+ Name = name ?? SecurityPolicies.GetDisplayName(uri) ?? uri;
+ }
+
+ ///
+ /// Short name for the policy.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The unique identifier for the policy.
+ ///
+ public string Uri { get; }
+
+ ///
+ /// Returns true if the policy is considered deprecated and should not be used for new deployments.
+ ///
+ public bool IsDeprecated { get; private set; }
+
+ ///
+ /// The symmetric signature algorithm to use.
+ ///
+ public SymmetricSignatureAlgorithm SymmetricSignatureAlgorithm { get; private set; }
+
+ ///
+ /// The symmetric encryption algorithm to use.
+ ///
+ public SymmetricEncryptionAlgorithm SymmetricEncryptionAlgorithm { get; private set; }
+
+ ///
+ /// The asymmetric signature algorithm to use.
+ ///
+ public AsymmetricSignatureAlgorithm AsymmetricSignatureAlgorithm { get; private set; }
+
+ ///
+ /// The symmetric encryption algorithm to use.
+ ///
+ public AsymmetricEncryptionAlgorithm AsymmetricEncryptionAlgorithm { get; private set; }
+
+ ///
+ /// The minimum length, in bits, for an asymmetric key.
+ ///
+ public int MinAsymmetricKeyLength { get; private set; }
+
+ ///
+ /// The maximum length, in bits, for an asymmetric key.
+ ///
+ public int MaxAsymmetricKeyLength { get; private set; }
+
+ ///
+ /// The key derivation algorithm to use.
+ ///
+ public KeyDerivationAlgorithm KeyDerivationAlgorithm { get; private set; }
+
+ ///
+ /// The length in bytes of the derived key used for message authentication.
+ ///
+ public int DerivedSignatureKeyLength { get; private set; }
+
+ ///
+ /// The asymmetric signature algorithm used to sign certificates.
+ ///
+ public AsymmetricSignatureAlgorithm CertificateSignatureAlgorithm { get; private set; }
+
+ ///
+ /// The algorithm used to create asymmetric key pairs used with Certificates.
+ ///
+ public CertificateKeyAlgorithm CertificateKeyAlgorithm { get; private set; }
+
+ ///
+ /// The algorithm used to create asymmetric key pairs used for EphemeralKeys.
+ ///
+ public CertificateKeyAlgorithm EphemeralKeyAlgorithm { get; private set; }
+
+ ///
+ /// The length, in bytes, of the Nonces used when opening a SecureChannel.
+ ///
+ public int SecureChannelNonceLength { get; private set; }
+
+ ///
+ /// The length, in bytes, of the data used to initialize the symmetric algorithm.
+ ///
+ public int InitializationVectorLength { get; private set; }
+
+ ///
+ /// The length, in bytes, of the symmetric signature.
+ ///
+ public int SymmetricSignatureLength { get; private set; }
+
+ ///
+ /// The length, in bytes, of the symmetric encryption key.
+ ///
+ public int SymmetricEncryptionKeyLength { get; private set; }
+
+ ///
+ /// If TRUE, the 1024 based SequenceNumber rules apply to the SecurityPolicy.
+ /// If FALSE, the 0 based SequenceNumber rules apply.
+ ///
+ public bool LegacySequenceNumbers { get; private set; }
+
+ ///
+ /// Whether the padding is required with symmetric encryption.
+ ///
+ public bool NoSymmetricEncryptionPadding =>
+ SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305;
+
+ ///
+ /// Returns the derived key data length in bytes as a little endian UInt16.
+ ///
+ public byte[] KeyDataLength =>
+ BitConverter.GetBytes(DerivedSignatureKeyLength + SymmetricEncryptionKeyLength + InitializationVectorLength);
+
+ ///
+ /// Returns the derived key data length for an EncryptedSecret in bytes as a little endian UInt16.
+ ///
+ public byte[] KeyDataLengthForEncryptedSecret =>
+ BitConverter.GetBytes(SymmetricEncryptionKeyLength + InitializationVectorLength);
+
+ ///
+ /// Returns a HMAC based on the symmetric signature algorithm.
+ ///
+ public HMAC CreateSignatureHmac(byte[] signingKey)
+ {
+ return SymmetricSignatureAlgorithm switch
+ {
+ SymmetricSignatureAlgorithm.HmacSha1 => new HMACSHA1(signingKey),
+ SymmetricSignatureAlgorithm.HmacSha256 => new HMACSHA256(signingKey),
+ SymmetricSignatureAlgorithm.HmacSha384 => new HMACSHA384(signingKey),
+ _ => null
+ };
+ }
+
+ ///
+ /// Returns a HashAlgorithmName based on the KeyDerivationAlgorithm.
+ ///
+ public HashAlgorithmName GetKeyDerivationHashAlgorithmName()
+ {
+ return KeyDerivationAlgorithm switch
+ {
+ KeyDerivationAlgorithm.PSha1 => HashAlgorithmName.SHA1,
+ KeyDerivationAlgorithm.PSha256 => HashAlgorithmName.SHA256,
+ KeyDerivationAlgorithm.HKDFSha256 => HashAlgorithmName.SHA256,
+ KeyDerivationAlgorithm.HKDFSha384 => HashAlgorithmName.SHA384,
+ _ => HashAlgorithmName.SHA256
+ };
+ }
+
+ ///
+ /// The security policy that does not provide any security.
+ ///
+ public static readonly SecurityPolicyInfo None = new(SecurityPolicies.None)
+ {
+ DerivedSignatureKeyLength = 0,
+ SymmetricEncryptionKeyLength = 0,
+ InitializationVectorLength = 0,
+ SymmetricSignatureLength = 0,
+ MinAsymmetricKeyLength = 0,
+ MaxAsymmetricKeyLength = 0,
+ SecureChannelNonceLength = 0,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.None,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.None,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.None,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.None,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.None,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.None
+ };
+
+ ///
+ /// The security policy that uses SHA1 and 128 bit encryption. This policy is considered insecure and should not be used for new deployments.
+ ///
+ public static readonly SecurityPolicyInfo Basic128Rsa15 = new(SecurityPolicies.Basic128Rsa15)
+ {
+ DerivedSignatureKeyLength = 128 / 8,
+ SymmetricEncryptionKeyLength = 128 / 8,
+ // HMAC-SHA1 produces a 160-bit MAC
+ SymmetricSignatureLength = 160 / 8,
+ InitializationVectorLength = 128 / 8,
+ MinAsymmetricKeyLength = 1024,
+ MaxAsymmetricKeyLength = 2048,
+ SecureChannelNonceLength = 16,
+ LegacySequenceNumbers = true,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1,
+ IsDeprecated = true
+ };
+
+ ///
+ /// The security policy that uses SHA1 and 256 bit encryption. This policy is considered insecure and should not be used for new deployments.
+ ///
+ public static readonly SecurityPolicyInfo Basic256 = new(SecurityPolicies.Basic256)
+ {
+ DerivedSignatureKeyLength = 192 / 8,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ // HMAC-SHA1 produces a 160-bit MAC
+ SymmetricSignatureLength = 160 / 8,
+ InitializationVectorLength = 128 / 8,
+ MinAsymmetricKeyLength = 1024,
+ MaxAsymmetricKeyLength = 2048,
+ SecureChannelNonceLength = 32,
+ LegacySequenceNumbers = true,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1,
+ IsDeprecated = true
+ };
+
+ ///
+ /// Aes128_Sha256_RsaOaep is a required minimum security policy. It uses SHA256 and 128 bit encryption.
+ ///
+ public static readonly SecurityPolicyInfo Aes128_Sha256_RsaOaep = new(SecurityPolicies.Aes128_Sha256_RsaOaep)
+ {
+ DerivedSignatureKeyLength = 256 / 8,
+ SymmetricEncryptionKeyLength = 128 / 8,
+ SymmetricSignatureLength = 256 / 8,
+ InitializationVectorLength = 128 / 8,
+ MinAsymmetricKeyLength = 2048,
+ MaxAsymmetricKeyLength = 4096,
+ SecureChannelNonceLength = 32,
+ LegacySequenceNumbers = true,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256,
+ IsDeprecated = false
+ };
+
+ ///
+ /// Basic256Sha256 is a required minimum security policy. It uses SHA256 and 256 bit encryption.
+ ///
+ public static readonly SecurityPolicyInfo Basic256Sha256 = new(SecurityPolicies.Basic256Sha256)
+ {
+ DerivedSignatureKeyLength = 256 / 8,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ SymmetricSignatureLength = 256 / 8,
+ InitializationVectorLength = 128 / 8,
+ MinAsymmetricKeyLength = 2048,
+ MaxAsymmetricKeyLength = 4096,
+ SecureChannelNonceLength = 32,
+ LegacySequenceNumbers = true,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256,
+ IsDeprecated = false
+ };
+
+ ///
+ /// Aes256_Sha256_RsaPss is a optional high security policy. It uses SHA256 and 256 bit encryption.
+ ///
+ public static readonly SecurityPolicyInfo Aes256_Sha256_RsaPss = new(SecurityPolicies.Aes256_Sha256_RsaPss)
+ {
+ DerivedSignatureKeyLength = 256 / 8,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ MinAsymmetricKeyLength = 2048,
+ MaxAsymmetricKeyLength = 4096,
+ SecureChannelNonceLength = 32,
+ LegacySequenceNumbers = true,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha256,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPssSha256,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256,
+ InitializationVectorLength = 128 / 8,
+ SymmetricSignatureLength = 256 / 8,
+ IsDeprecated = false
+ };
+
+ ///
+ /// ECC_curve25519 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption.
+ ///
+ public static readonly SecurityPolicyInfo ECC_curve25519 = new(SecurityPolicies.ECC_curve25519)
+ {
+ DerivedSignatureKeyLength = 0,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ InitializationVectorLength = 96 / 8,
+ SymmetricSignatureLength = 128 / 8,
+ MinAsymmetricKeyLength = 256,
+ MaxAsymmetricKeyLength = 256,
+ SecureChannelNonceLength = 32,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305,
+ IsDeprecated = false
+ };
+
+ ///
+ /// ECC_curve448 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption.
+ ///
+ public static readonly SecurityPolicyInfo ECC_curve448 = new(SecurityPolicies.ECC_curve448)
+ {
+ DerivedSignatureKeyLength = 0,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ InitializationVectorLength = 96 / 8,
+ SymmetricSignatureLength = 128 / 8,
+ MinAsymmetricKeyLength = 456,
+ MaxAsymmetricKeyLength = 456,
+ SecureChannelNonceLength = 56,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305,
+ IsDeprecated = false
+ };
+
+ ///
+ /// ECC nistP256 is a required minimum security policy.
+ ///
+ public static readonly SecurityPolicyInfo ECC_nistP256 = new(SecurityPolicies.ECC_nistP256)
+ {
+ DerivedSignatureKeyLength = 256 / 8,
+ SymmetricEncryptionKeyLength = 128 / 8,
+ InitializationVectorLength = 128 / 8,
+ SymmetricSignatureLength = 256 / 8,
+ MinAsymmetricKeyLength = 256,
+ MaxAsymmetricKeyLength = 256,
+ SecureChannelNonceLength = 64,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256,
+ IsDeprecated = false
+ };
+
+ ///
+ /// ECC nistP384 is an optional high security policy.
+ ///
+ public static readonly SecurityPolicyInfo ECC_nistP384 = new(SecurityPolicies.ECC_nistP384)
+ {
+ DerivedSignatureKeyLength = 384 / 8,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ InitializationVectorLength = 128 / 8,
+ SymmetricSignatureLength = 384 / 8,
+ MinAsymmetricKeyLength = 384,
+ MaxAsymmetricKeyLength = 384,
+ SecureChannelNonceLength = 96,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384,
+ IsDeprecated = false
+ };
+
+ ///
+ /// ECC brainpoolP256r1 is a required minimum security policy.
+ ///
+ public static readonly SecurityPolicyInfo ECC_brainpoolP256r1 = new(SecurityPolicies.ECC_brainpoolP256r1)
+ {
+ DerivedSignatureKeyLength = 256 / 8,
+ SymmetricEncryptionKeyLength = 128 / 8,
+ InitializationVectorLength = 128 / 8,
+ SymmetricSignatureLength = 256 / 8,
+ MinAsymmetricKeyLength = 256,
+ MaxAsymmetricKeyLength = 256,
+ SecureChannelNonceLength = 64,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256,
+ IsDeprecated = false
+ };
+
+ ///
+ /// ECC brainpoolP384r1 is an optional high security policy.
+ ///
+ public static readonly SecurityPolicyInfo ECC_brainpoolP384r1 = new(SecurityPolicies.ECC_brainpoolP384r1)
+ {
+ DerivedSignatureKeyLength = 384 / 8,
+ SymmetricEncryptionKeyLength = 256 / 8,
+ InitializationVectorLength = 128 / 8,
+ SymmetricSignatureLength = 384 / 8,
+ MinAsymmetricKeyLength = 384,
+ MaxAsymmetricKeyLength = 384,
+ SecureChannelNonceLength = 96,
+ LegacySequenceNumbers = false,
+ AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None,
+ AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384,
+ CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1,
+ CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384,
+ EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1,
+ KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384,
+ SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc,
+ SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384,
+ IsDeprecated = false
+ };
+ }
+
+ ///
+ /// The algorithm used to generate key pairs.
+ ///
+ public enum CertificateKeyAlgorithm
+ {
+ ///
+ /// Does not apply.
+ ///
+ None,
+
+ ///
+ /// The RSA algorithm.
+ ///
+ RSA,
+
+ ///
+ /// The Diffie-Hellman algorith with RSA public keys.
+ ///
+ RSADH,
+
+ ///
+ /// The NIST P-256 ellipic curve algorithm.
+ ///
+ NistP256,
+
+ ///
+ /// The NIST P-384 ellipic curve algorithm.
+ ///
+ NistP384,
+
+ ///
+ /// The non-twisted Brainpool P-256 ellipic curve algorithm.
+ ///
+ BrainpoolP256r1,
+
+ ///
+ /// The non-twisted Brainpool P-384 ellipic curve algorithm.
+ ///
+ BrainpoolP384r1,
+
+ ///
+ /// The Edward Curve25519 ellipic curve algorithm.
+ ///
+ Curve25519,
+
+ ///
+ /// The Edward Curve25519 ellipic curve algorithm.
+ ///
+ Curve448
+ }
+
+ ///
+ /// The symmetric key derivation algorithm used to create shared keys.
+ ///
+ public enum KeyDerivationAlgorithm
+ {
+ ///
+ /// Does not apply.
+ ///
+ None,
+
+ ///
+ /// The P_SHA pseudo-random function with SHA1. This algorithm is considered insecure.
+ ///
+ PSha1,
+
+ ///
+ /// The P_SHA pseudo-random function with SHA256.
+ ///
+ PSha256,
+
+ ///
+ /// The HKDF pseudo-random function with SHA256.
+ ///
+ HKDFSha256,
+
+ ///
+ /// The HKDF pseudo-random function with SHA384.
+ ///
+ HKDFSha384
+ }
+
+ ///
+ /// The asymmetric encryption algorithm used to encrypt messages.
+ ///
+ public enum AsymmetricEncryptionAlgorithm
+ {
+ ///
+ /// Does not apply.
+ ///
+ None,
+
+ ///
+ /// RSA PKCS #1 v1.5. This algorithm is considered insecure.
+ ///
+ RsaPkcs15Sha1,
+
+ ///
+ /// RSA with OAEP padding with SHA1. This algorithm is considered insecure.
+ ///
+ RsaOaepSha1,
+
+ ///
+ /// RSA with OAEP padding with SHA256 .
+ ///
+ RsaOaepSha256
+ }
+
+ ///
+ /// The asymmetric signature algorithm used to sign messages.
+ ///
+ public enum AsymmetricSignatureAlgorithm
+ {
+ ///
+ /// Does not apply.
+ ///
+ None,
+
+ ///
+ /// RSA PKCS #1 v1.5 with SHA1. This algorithm is considered insecure.
+ ///
+ RsaPkcs15Sha1,
+
+ ///
+ /// RSA PKCS #1 v1.5 with SHA256.
+ ///
+ RsaPkcs15Sha256,
+
+ ///
+ /// RSA PSS with SHA256.
+ ///
+ RsaPssSha256,
+
+ ///
+ /// ECDSA with SHA256.
+ ///
+ EcdsaSha256,
+
+ ///
+ /// ECDSA with SHA384.
+ ///
+ EcdsaSha384,
+
+ ///
+ /// ECDSA with Curve 25519.
+ ///
+ EcdsaPure25519,
+
+ ///
+ /// ECDSA with Curve 448.
+ ///
+ EcdsaPure448
+ }
+
+ ///
+ /// The symmetric signature algorithm used to sign messages.
+ ///
+ public enum SymmetricSignatureAlgorithm
+ {
+ ///
+ /// Does not apply.
+ ///
+ None,
+
+ ///
+ /// HMAC with SHA1
+ ///
+ HmacSha1,
+
+ ///
+ /// HMAC with SHA256
+ ///
+ HmacSha256,
+
+ ///
+ /// HMAC with SHA384
+ ///
+ HmacSha384,
+
+ ///
+ /// ChaCha20Poly1305
+ ///
+ ChaCha20Poly1305,
+
+ ///
+ /// AES GCM with 128 bit tag
+ ///
+ Aes128Gcm
+ }
+
+ ///
+ /// The symmetric ecryption algorithm used to encrypt messages.
+ ///
+ public enum SymmetricEncryptionAlgorithm
+ {
+ ///
+ /// Does not apply.
+ ///
+ None,
+
+ ///
+ /// AES 128 bit in CBC mode
+ ///
+ Aes128Cbc,
+
+ ///
+ /// AES 256 bit in CBC mode
+ ///
+ Aes256Cbc,
+
+ ///
+ /// AES 128 bit in counter mode
+ ///
+ Aes128Ctr,
+
+ ///
+ /// AES 256 bit in counter mode
+ ///
+ Aes256Ctr,
+
+ ///
+ /// ChaCha20Poly1305
+ ///
+ ChaCha20Poly1305,
+
+ ///
+ /// AES 128 in GCM mode
+ ///
+ Aes128Gcm,
+
+ ///
+ /// AES 256 in GCM mode
+ ///
+ Aes256Gcm
+ }
+}
diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs
index f60c516fe..e2e8cbf83 100644
--- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs
+++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs
@@ -11,7 +11,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
using System;
-using System.Security.Cryptography;
namespace Opc.Ua.Bindings
{
@@ -20,6 +19,8 @@ namespace Opc.Ua.Bindings
///
public sealed class ChannelToken : IDisposable
{
+ private bool m_disposed;
+
///
/// Creates an object with default values.
///
@@ -34,17 +35,6 @@ private void Dispose(bool disposing)
{
if (!m_disposed)
{
- if (disposing)
- {
- Utils.SilentDispose(ClientHmac);
- Utils.SilentDispose(ServerHmac);
- Utils.SilentDispose(ClientEncryptor);
- Utils.SilentDispose(ServerEncryptor);
- }
- ClientHmac = null;
- ServerHmac = null;
- ClientEncryptor = null;
- ServerEncryptor = null;
m_disposed = true;
}
}
@@ -106,6 +96,11 @@ public void Dispose()
(HiResClock.TickCount - CreatedAtTickCount) >
(int)Math.Round(Lifetime * TcpMessageLimits.TokenActivationPeriod);
+ ///
+ /// The SecurityPolicy used to encrypt and sign the messages.
+ ///
+ public SecurityPolicyInfo SecurityPolicy { get; set; }
+
///
/// The nonce provided by the client.
///
@@ -119,53 +114,31 @@ public void Dispose()
///
/// The key used to sign messages sent by the client.
///
- public byte[] ClientSigningKey { get; set; }
+ internal byte[] ClientSigningKey { get; set; }
///
/// The key used to encrypt messages sent by the client.
///
- public byte[] ClientEncryptingKey { get; set; }
+ internal byte[] ClientEncryptingKey { get; set; }
///
/// The initialization vector by the client when encrypting a message.
///
- public byte[] ClientInitializationVector { get; set; }
+ internal byte[] ClientInitializationVector { get; set; }
///
/// The key used to sign messages sent by the server.
///
- public byte[] ServerSigningKey { get; set; }
+ internal byte[] ServerSigningKey { get; set; }
///
/// The key used to encrypt messages sent by the server.
///
- public byte[] ServerEncryptingKey { get; set; }
+ internal byte[] ServerEncryptingKey { get; set; }
///
/// The initialization vector by the server when encrypting a message.
///
- public byte[] ServerInitializationVector { get; set; }
-
- ///
- /// The SymmetricAlgorithm object used by the client to encrypt messages.
- ///
- public SymmetricAlgorithm ClientEncryptor { get; set; }
-
- ///
- /// The SymmetricAlgorithm object used by the server to encrypt messages.
- ///
- public SymmetricAlgorithm ServerEncryptor { get; set; }
-
- ///
- /// The HMAC object used by the client to sign messages.
- ///
- public HMAC ClientHmac { get; set; }
-
- ///
- /// The HMAC object used by the server to sign messages.
- ///
- public HMAC ServerHmac { get; set; }
-
- private bool m_disposed;
+ internal byte[] ServerInitializationVector { get; set; }
}
}
diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs
index 23751c8bb..562272fbf 100644
--- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs
+++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs
@@ -119,11 +119,6 @@ protected void DiscardTokens()
OnTokenActivated?.Invoke(null, null);
}
- ///
- /// Indicates that an explicit signature is not present.
- ///
- private bool AuthenticatedEncryption { get; set; }
-
///
/// The byte length of the MAC (a.k.a signature) attached to each message.
///
@@ -137,71 +132,25 @@ protected void DiscardTokens()
///
/// Calculates the symmetric key sizes based on the current security policy.
///
+ ///
protected void CalculateSymmetricKeySizes()
{
- AuthenticatedEncryption = false;
-
- switch (SecurityPolicyUri)
+ var securityPolicyUri = SecurityPolicyUri;
+ if (securityPolicyUri.StartsWith(SecurityPolicies.BaseUri, StringComparison.Ordinal))
{
- case SecurityPolicies.Basic128Rsa15:
- SymmetricSignatureSize = 20;
- m_signatureKeySize = 16;
- m_encryptionKeySize = 16;
- EncryptionBlockSize = 16;
- break;
- case SecurityPolicies.Basic256:
- SymmetricSignatureSize = 20;
- m_signatureKeySize = 24;
- m_encryptionKeySize = 32;
- EncryptionBlockSize = 16;
- break;
- case SecurityPolicies.Basic256Sha256:
- SymmetricSignatureSize = 32;
- m_signatureKeySize = 32;
- m_encryptionKeySize = 32;
- EncryptionBlockSize = 16;
- break;
- case SecurityPolicies.Aes128_Sha256_RsaOaep:
- SymmetricSignatureSize = 32;
- m_signatureKeySize = 32;
- m_encryptionKeySize = 16;
- EncryptionBlockSize = 16;
- break;
- case SecurityPolicies.Aes256_Sha256_RsaPss:
- SymmetricSignatureSize = 32;
- m_signatureKeySize = 32;
- m_encryptionKeySize = 32;
- EncryptionBlockSize = 16;
- break;
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_brainpoolP256r1:
- SymmetricSignatureSize = 32;
- m_signatureKeySize = 32;
- m_encryptionKeySize = 16;
- EncryptionBlockSize = 16;
- break;
- case SecurityPolicies.ECC_curve25519:
- case SecurityPolicies.ECC_curve448:
- AuthenticatedEncryption = true;
- SymmetricSignatureSize = 16;
- m_signatureKeySize = 32;
- m_encryptionKeySize = 32;
- EncryptionBlockSize = 12;
- break;
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP384r1:
- SymmetricSignatureSize = 48;
- m_signatureKeySize = 48;
- m_encryptionKeySize = 32;
- EncryptionBlockSize = 16;
- break;
- default:
- SymmetricSignatureSize = 0;
- m_signatureKeySize = 0;
- m_encryptionKeySize = 0;
- EncryptionBlockSize = 1;
- break;
+ securityPolicyUri = securityPolicyUri.Substring(SecurityPolicies.BaseUri.Length);
}
+
+ SecurityPolicyInfo info = SecurityPolicies.GetInfo(securityPolicyUri)
+ ?? throw ServiceResultException.Create(
+ StatusCodes.BadSecurityPolicyRejected,
+ "Unsupported security policy: {0}",
+ SecurityPolicyUri);
+
+ SymmetricSignatureSize = info.SymmetricSignatureLength;
+ m_signatureKeySize = info.DerivedSignatureKeyLength;
+ m_encryptionKeySize = info.SymmetricEncryptionKeyLength;
+ EncryptionBlockSize = info.InitializationVectorLength != 0 ? info.InitializationVectorLength : 1;
}
private void DeriveKeysWithPSHA(
@@ -239,22 +188,28 @@ private void DeriveKeysWithPSHA(
}
private void DeriveKeysWithHKDF(
- HashAlgorithmName algorithmName,
- byte[] salt,
ChannelToken token,
+ byte[] salt,
bool isServer)
{
- int length = m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize;
+ int length =
+ token.SecurityPolicy.DerivedSignatureKeyLength +
+ token.SecurityPolicy.SymmetricEncryptionKeyLength +
+ token.SecurityPolicy.InitializationVectorLength;
- byte[] output = m_localNonce.DeriveKey(m_remoteNonce, salt, algorithmName, length);
+ byte[] prk = m_localNonce.DeriveKey(
+ m_remoteNonce,
+ salt,
+ token.SecurityPolicy.GetKeyDerivationHashAlgorithmName(),
+ length);
byte[] signingKey = new byte[m_signatureKeySize];
byte[] encryptingKey = new byte[m_encryptionKeySize];
byte[] iv = new byte[EncryptionBlockSize];
- Buffer.BlockCopy(output, 0, signingKey, 0, signingKey.Length);
- Buffer.BlockCopy(output, m_signatureKeySize, encryptingKey, 0, encryptingKey.Length);
- Buffer.BlockCopy(output, m_signatureKeySize + m_encryptionKeySize, iv, 0, iv.Length);
+ Buffer.BlockCopy(prk, 0, signingKey, 0, signingKey.Length);
+ Buffer.BlockCopy(prk, m_signatureKeySize, encryptingKey, 0, encryptingKey.Length);
+ Buffer.BlockCopy(prk, m_signatureKeySize + m_encryptionKeySize, iv, 0, iv.Length);
if (isServer)
{
@@ -275,177 +230,69 @@ private void DeriveKeysWithHKDF(
///
protected void ComputeKeys(ChannelToken token)
{
- if (SecurityMode == MessageSecurityMode.None)
+ // Strip BaseUri prefix to get short name for dictionary lookup
+ var securityPolicyUri = SecurityPolicyUri;
+ if (securityPolicyUri.StartsWith(SecurityPolicies.BaseUri, StringComparison.Ordinal))
{
- return;
+ securityPolicyUri = securityPolicyUri.Substring(SecurityPolicies.BaseUri.Length);
}
- byte[] serverSecret = token.ServerNonce;
- byte[] clientSecret = token.ClientNonce;
+ token.SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri);
- HashAlgorithmName algorithmName = HashAlgorithmName.SHA256;
- switch (SecurityPolicyUri)
+ if (token.SecurityPolicy == null)
{
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_brainpoolP256r1:
- {
- algorithmName = HashAlgorithmName.SHA256;
- byte[] length =
- SecurityMode == MessageSecurityMode.Sign
- ? s_hkdfAes128SignOnlyKeyLength
- : s_hkdfAes128SignAndEncryptKeyLength;
- byte[] serverSalt = Utils.Append(
- length,
- s_hkdfServerLabel,
- serverSecret,
- clientSecret);
- byte[] clientSalt = Utils.Append(
- length,
- s_hkdfClientLabel,
- clientSecret,
- serverSecret);
+ throw ServiceResultException.Create(
+ StatusCodes.BadSecurityPolicyRejected,
+ "Unsupported security policy: {0}",
+ SecurityPolicyUri);
+ }
-#if DEBUG
- m_logger.LogDebug("Length={Length}", Utils.ToHexString(length));
- m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret));
- m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret));
- m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt));
- m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt));
-#endif
+ if (SecurityMode == MessageSecurityMode.None)
+ {
+ return;
+ }
- DeriveKeysWithHKDF(algorithmName, serverSalt, token, true);
- DeriveKeysWithHKDF(algorithmName, clientSalt, token, false);
- break;
- }
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP384r1:
- {
- algorithmName = HashAlgorithmName.SHA384;
- byte[] length =
- SecurityMode == MessageSecurityMode.Sign
- ? s_hkdfAes256SignOnlyKeyLength
- : s_hkdfAes256SignAndEncryptKeyLength;
- byte[] serverSalt = Utils.Append(
- length,
- s_hkdfServerLabel,
- serverSecret,
- clientSecret);
- byte[] clientSalt = Utils.Append(
- length,
- s_hkdfClientLabel,
- clientSecret,
- serverSecret);
+ byte[] serverSecret = token.ServerNonce;
+ byte[] clientSecret = token.ClientNonce;
-#if DEBUG
- m_logger.LogDebug("Length={Length}", Utils.ToHexString(length));
- m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret));
- m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret));
- m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt));
- m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt));
-#endif
+ m_logger?.LogInformation(
+ "[ComputeKeys] KeyDerivationAlgorithm: {Algo}",
+ token.SecurityPolicy.KeyDerivationAlgorithm);
- DeriveKeysWithHKDF(algorithmName, serverSalt, token, true);
- DeriveKeysWithHKDF(algorithmName, clientSalt, token, false);
- break;
- }
- case SecurityPolicies.ECC_curve25519:
- case SecurityPolicies.ECC_curve448:
- {
- algorithmName = HashAlgorithmName.SHA256;
- byte[] length = s_hkdfChaCha20Poly1305KeyLength;
- byte[] serverSalt = Utils.Append(
- length,
- s_hkdfServerLabel,
- serverSecret,
- clientSecret);
- byte[] clientSalt = Utils.Append(
- length,
- s_hkdfClientLabel,
- clientSecret,
- serverSecret);
+ if (token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha1 ||
+ token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha256)
+ {
+ HashAlgorithmName algorithmName = token.SecurityPolicy.GetKeyDerivationHashAlgorithmName();
+ DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false);
+ DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true);
+ }
+ else
+ {
+ byte[] keyData = SecurityMode == MessageSecurityMode.Sign
+ ? token.SecurityPolicy.KeyDataLength
+ : token.SecurityPolicy.KeyDataLength;
+
+ byte[] serverSalt = Utils.Append(
+ keyData,
+ s_hkdfServerLabel,
+ serverSecret,
+ clientSecret);
+ byte[] clientSalt = Utils.Append(
+ keyData,
+ s_hkdfClientLabel,
+ clientSecret,
+ serverSecret);
#if DEBUG
- m_logger.LogDebug("Length={Length}", Utils.ToHexString(length));
- m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret));
- m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(clientSecret));
- m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt));
- m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt));
+ m_logger.LogDebug("KeyData={KeyData}", Utils.ToHexString(keyData));
+ m_logger.LogDebug("ClientSecret={ClientSecret}", Utils.ToHexString(clientSecret));
+ m_logger.LogDebug("ServerSecret={ServerSecret}", Utils.ToHexString(serverSecret));
+ m_logger.LogDebug("ServerSalt={ServerSalt}", Utils.ToHexString(serverSalt));
+ m_logger.LogDebug("ClientSalt={ClientSalt}", Utils.ToHexString(clientSalt));
#endif
- DeriveKeysWithHKDF(algorithmName, serverSalt, token, true);
- DeriveKeysWithHKDF(algorithmName, clientSalt, token, false);
- break;
- }
- case SecurityPolicies.Basic128Rsa15:
- case SecurityPolicies.Basic256:
- algorithmName = HashAlgorithmName.SHA1;
- goto default;
- default:
- DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false);
- DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true);
- break;
- }
-
- switch (SecurityPolicyUri)
- {
- case SecurityPolicies.Basic128Rsa15:
- case SecurityPolicies.Basic256:
- case SecurityPolicies.Basic256Sha256:
- case SecurityPolicies.Aes128_Sha256_RsaOaep:
- case SecurityPolicies.Aes256_Sha256_RsaPss:
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP256r1:
- case SecurityPolicies.ECC_brainpoolP384r1:
- // create encryptors.
- var aesCbcEncryptorProvider = Aes.Create();
- aesCbcEncryptorProvider.Mode = CipherMode.CBC;
- aesCbcEncryptorProvider.Padding = PaddingMode.None;
- aesCbcEncryptorProvider.Key = token.ClientEncryptingKey;
- aesCbcEncryptorProvider.IV = token.ClientInitializationVector;
- token.ClientEncryptor = aesCbcEncryptorProvider;
-
- var aesCbcDecryptorProvider = Aes.Create();
- aesCbcDecryptorProvider.Mode = CipherMode.CBC;
- aesCbcDecryptorProvider.Padding = PaddingMode.None;
- aesCbcDecryptorProvider.Key = token.ServerEncryptingKey;
- aesCbcDecryptorProvider.IV = token.ServerInitializationVector;
- token.ServerEncryptor = aesCbcDecryptorProvider;
- break;
- default:
- // TODO: is this even legal or should we throw? What are the implications
- token.ClientEncryptor = null;
- token.ServerEncryptor = null;
- break;
- }
-
- switch (SecurityPolicyUri)
- {
- case SecurityPolicies.Basic128Rsa15:
- case SecurityPolicies.Basic256:
-#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms
- token.ServerHmac = new HMACSHA1(token.ServerSigningKey);
- token.ClientHmac = new HMACSHA1(token.ClientSigningKey);
-#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms
- break;
- case SecurityPolicies.Basic256Sha256:
- case SecurityPolicies.Aes128_Sha256_RsaOaep:
- case SecurityPolicies.Aes256_Sha256_RsaPss:
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_brainpoolP256r1:
- token.ServerHmac = new HMACSHA256(token.ServerSigningKey);
- token.ClientHmac = new HMACSHA256(token.ClientSigningKey);
- break;
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP384r1:
- token.ServerHmac = new HMACSHA384(token.ServerSigningKey);
- token.ClientHmac = new HMACSHA384(token.ClientSigningKey);
- break;
- default:
- // TODO: is this even legal or should we throw? What are the implications
- token.ServerHmac = null;
- token.ClientHmac = null;
- break;
+ DeriveKeysWithHKDF(token, serverSalt, true);
+ DeriveKeysWithHKDF(token, clientSalt, false);
}
}
@@ -478,8 +325,8 @@ protected BufferCollection WriteSymmetricMessage(
const int headerSize = TcpMessageLimits.SymmetricHeaderSize +
TcpMessageLimits.SequenceHeaderSize;
- // no padding byte.
- if (AuthenticatedEncryption)
+ // no padding byte for authenticated encryption.
+ if (token.SecurityPolicy.NoSymmetricEncryptionPadding)
{
maxPayloadSize++;
}
@@ -597,7 +444,7 @@ protected BufferCollection WriteSymmetricMessage(
int padding = 0;
if (SecurityMode == MessageSecurityMode.SignAndEncrypt &&
- !AuthenticatedEncryption)
+ !token.SecurityPolicy.NoSymmetricEncryptionPadding)
{
// reserve one byte for the padding size.
count++;
@@ -631,7 +478,7 @@ protected BufferCollection WriteSymmetricMessage(
// write padding.
if (SecurityMode == MessageSecurityMode.SignAndEncrypt &&
- !AuthenticatedEncryption)
+ !token.SecurityPolicy.NoSymmetricEncryptionPadding)
{
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
if (padding > 1)
@@ -653,7 +500,7 @@ protected BufferCollection WriteSymmetricMessage(
// calculate and write signature.
if (SecurityMode != MessageSecurityMode.None)
{
- if (AuthenticatedEncryption)
+ if (token.SecurityPolicy.NoSymmetricEncryptionPadding)
{
strm.Seek(SymmetricSignatureSize, SeekOrigin.Current);
}
@@ -672,8 +519,8 @@ protected BufferCollection WriteSymmetricMessage(
}
if ((SecurityMode == MessageSecurityMode.SignAndEncrypt &&
- !AuthenticatedEncryption) ||
- (SecurityMode != MessageSecurityMode.None && AuthenticatedEncryption))
+ !token.SecurityPolicy.NoSymmetricEncryptionPadding) ||
+ (SecurityMode != MessageSecurityMode.None && token.SecurityPolicy.NoSymmetricEncryptionPadding))
{
// encrypt the data.
var dataToEncrypt = new ArraySegment(
@@ -786,10 +633,11 @@ protected ArraySegment ReadSymmetricMessage(
int headerSize = decoder.Position;
+ int decryptedCount = buffer.Count - headerSize;
if (SecurityMode == MessageSecurityMode.SignAndEncrypt)
{
// decrypt the message.
- Decrypt(
+ decryptedCount = Decrypt(
token,
new ArraySegment(
buffer.Array,
@@ -799,9 +647,14 @@ protected ArraySegment ReadSymmetricMessage(
}
int paddingCount = 0;
- if (SecurityMode != MessageSecurityMode.None)
+ if (SecurityMode != MessageSecurityMode.None &&
+ !token.SecurityPolicy.NoSymmetricEncryptionPadding)
{
- int signatureStart = buffer.Offset + buffer.Count - SymmetricSignatureSize;
+ int signatureStart =
+ buffer.Offset +
+ headerSize +
+ decryptedCount -
+ SymmetricSignatureSize;
// extract signature.
byte[] signature = new byte[SymmetricSignatureSize];
@@ -814,7 +667,7 @@ protected ArraySegment ReadSymmetricMessage(
new ArraySegment(
buffer.Array,
buffer.Offset,
- buffer.Count - SymmetricSignatureSize),
+ headerSize + decryptedCount - SymmetricSignatureSize),
isRequest))
{
m_logger.LogError("ChannelId {Id}: Could not verify signature on message.", Id);
@@ -843,6 +696,11 @@ protected ArraySegment ReadSymmetricMessage(
paddingCount++;
}
}
+ else if (SecurityMode != MessageSecurityMode.None)
+ {
+ // AEAD algorithms are verified during decrypt.
+ paddingCount = 0;
+ }
// extract request id and sequence number.
sequenceNumber = decoder.ReadUInt32(null);
@@ -854,11 +712,13 @@ protected ArraySegment ReadSymmetricMessage(
TcpMessageLimits.SymmetricHeaderSize +
TcpMessageLimits.SequenceHeaderSize;
int sizeOfBody =
- buffer.Count -
- TcpMessageLimits.SymmetricHeaderSize -
+ decryptedCount -
TcpMessageLimits.SequenceHeaderSize -
paddingCount -
- SymmetricSignatureSize;
+ (SecurityMode != MessageSecurityMode.None &&
+ !token.SecurityPolicy.NoSymmetricEncryptionPadding
+ ? SymmetricSignatureSize
+ : 0);
return new ArraySegment(buffer.Array, startOfBody, sizeOfBody);
}
@@ -915,7 +775,7 @@ protected bool Verify(
}
///
- /// Decrypts the data in a buffer using symmetric encryption.
+ /// Encrypts and signs the data in a buffer using symmetric encryption.
///
///
protected void Encrypt(
@@ -923,93 +783,119 @@ protected void Encrypt(
ArraySegment dataToEncrypt,
bool useClientKeys)
{
- switch (SecurityPolicyUri)
+ byte[] encryptingKey = useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey;
+ byte[] iv = useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector;
+ byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey;
+
+ bool signOnly = SecurityMode == MessageSecurityMode.Sign;
+
+ if (SecurityPolicyUri == SecurityPolicies.None)
{
- case SecurityPolicies.None:
- break;
- case SecurityPolicies.Basic256:
- case SecurityPolicies.Basic256Sha256:
- case SecurityPolicies.Basic128Rsa15:
- case SecurityPolicies.Aes128_Sha256_RsaOaep:
- case SecurityPolicies.Aes256_Sha256_RsaPss:
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP256r1:
- case SecurityPolicies.ECC_brainpoolP384r1:
- SymmetricEncrypt(token, dataToEncrypt, useClientKeys);
- break;
+ return;
+ }
-#if CURVE25519
- case SecurityPolicies.ECC_curve25519:
- case SecurityPolicies.ECC_curve448:
+ // For CBC based policies the caller already applied padding and signatures.
+ if (token.SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc
+ or SymmetricEncryptionAlgorithm.Aes256Cbc)
+ {
+ if (signOnly)
{
- if (SecurityMode == MessageSecurityMode.SignAndEncrypt)
- {
- // narowing conversion can safely be done on m_localSequenceNumber
- SymmetricEncryptWithChaCha20Poly1305(
- token,
- (uint)m_localSequenceNumber,
- dataToEncrypt,
- useClientKeys);
- break;
- }
- // narowing conversion can safely be done on m_localSequenceNumber
- SymmetricSignWithPoly1305(token, (uint)m_localSequenceNumber, dataToEncrypt, useClientKeys);
- break;
+ return;
}
-#endif
- default:
- throw new NotSupportedException(SecurityPolicyUri);
+
+ using var aes = Aes.Create();
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.None;
+ aes.Key = encryptingKey;
+ aes.IV = iv;
+
+ using ICryptoTransform encryptor = aes.CreateEncryptor();
+ encryptor.TransformBlock(
+ dataToEncrypt.Array,
+ dataToEncrypt.Offset,
+ dataToEncrypt.Count,
+ dataToEncrypt.Array,
+ dataToEncrypt.Offset);
+ return;
+ }
+
+ ArraySegment result = EccUtils.SymmetricEncryptAndSign(
+ dataToEncrypt,
+ token.SecurityPolicy,
+ encryptingKey,
+ iv,
+ signingKey,
+ signOnly);
+
+ // Copy result back to original buffer if different
+ if (result.Array != dataToEncrypt.Array || result.Offset != dataToEncrypt.Offset)
+ {
+ Buffer.BlockCopy(result.Array, result.Offset, dataToEncrypt.Array, dataToEncrypt.Offset, result.Count);
}
}
///
- /// Decrypts the data in a buffer using symmetric encryption.
+ /// Decrypts and verifies the data in a buffer using symmetric encryption.
///
///
- protected void Decrypt(
+ protected int Decrypt(
ChannelToken token,
ArraySegment dataToDecrypt,
bool useClientKeys)
{
- switch (SecurityPolicyUri)
+ if (SecurityPolicyUri == SecurityPolicies.None)
{
- case SecurityPolicies.None:
- break;
- case SecurityPolicies.Basic256:
- case SecurityPolicies.Basic256Sha256:
- case SecurityPolicies.Basic128Rsa15:
- case SecurityPolicies.ECC_nistP256:
- case SecurityPolicies.ECC_nistP384:
- case SecurityPolicies.ECC_brainpoolP256r1:
- case SecurityPolicies.ECC_brainpoolP384r1:
- case SecurityPolicies.Aes128_Sha256_RsaOaep:
- case SecurityPolicies.Aes256_Sha256_RsaPss:
- SymmetricDecrypt(token, dataToDecrypt, useClientKeys);
- break;
+ return dataToDecrypt.Count;
+ }
-#if CURVE25519
- case SecurityPolicies.ECC_curve25519:
- case SecurityPolicies.ECC_curve448:
- {
- if (SecurityMode == MessageSecurityMode.SignAndEncrypt)
- {
- SymmetricDecryptWithChaCha20Poly1305(
- token,
- m_remoteSequenceNumber,
- dataToDecrypt,
- useClientKeys);
- break;
- }
+ byte[] encryptingKey = useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey;
+ byte[] iv = useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector;
+ byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey;
- SymmetricVerifyWithPoly1305(token, m_remoteSequenceNumber, dataToDecrypt, useClientKeys);
- break;
+ bool signOnly = SecurityMode == MessageSecurityMode.Sign;
+
+ // For CBC based policies the caller will verify signatures and remove padding.
+ if (token.SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc
+ or SymmetricEncryptionAlgorithm.Aes256Cbc)
+ {
+ if (signOnly)
+ {
+ return dataToDecrypt.Count;
}
-#endif
- default:
- throw new NotSupportedException(SecurityPolicyUri);
+ using var aes = Aes.Create();
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.None;
+ aes.Key = encryptingKey;
+ aes.IV = iv;
+
+ using ICryptoTransform decryptor = aes.CreateDecryptor();
+ decryptor.TransformBlock(
+ dataToDecrypt.Array,
+ dataToDecrypt.Offset,
+ dataToDecrypt.Count,
+ dataToDecrypt.Array,
+ dataToDecrypt.Offset);
+
+ return dataToDecrypt.Count;
}
+
+ ArraySegment result = EccUtils.SymmetricDecryptAndVerify(
+ dataToDecrypt,
+ token.SecurityPolicy,
+ encryptingKey,
+ iv,
+ signingKey,
+ signOnly);
+
+ // Copy result back to original buffer if different
+ if (result.Array != dataToDecrypt.Array || result.Offset != dataToDecrypt.Offset)
+ {
+ Buffer.BlockCopy(result.Array, result.Offset, dataToDecrypt.Array, dataToDecrypt.Offset, result.Count);
+ }
+
+ // return the decrypted size (without authentication tag/padding)
+ return result.Count - dataToDecrypt.Offset;
}
#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER
@@ -1021,8 +907,9 @@ private static byte[] SymmetricSign(
ReadOnlySpan dataToSign,
bool useClientKeys)
{
- // get HMAC object.
- HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac;
+ byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey;
+
+ using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey);
// compute hash.
int hashSizeInBytes = hmac.HashSize >> 3;
@@ -1049,8 +936,10 @@ private static byte[] SymmetricSign(
ArraySegment dataToSign,
bool useClientKeys)
{
- // get HMAC object.
- HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac;
+ byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey;
+
+ using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey);
+
// compute hash.
var istrm = new MemoryStream(
dataToSign.Array,
@@ -1075,8 +964,9 @@ private bool SymmetricVerify(
ReadOnlySpan dataToVerify,
bool useClientKeys)
{
- // get HMAC object.
- HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac;
+ byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey;
+
+ using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey);
// compute hash.
int hashSizeInBytes = hmac.HashSize >> 3;
@@ -1103,8 +993,9 @@ private bool SymmetricVerify(
ArraySegment dataToVerify,
bool useClientKeys)
{
- // get HMAC object.
- HMAC hmac = useClientKeys ? token.ClientHmac : token.ServerHmac;
+ byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey;
+
+ using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey);
var istrm = new MemoryStream(
dataToVerify.Array,
@@ -1140,67 +1031,7 @@ private bool SymmetricVerify(
return true;
}
- ///
- /// Encrypts a message using a symmetric algorithm.
- ///
- ///
- private static void SymmetricEncrypt(
- ChannelToken token,
- ArraySegment dataToEncrypt,
- bool useClientKeys)
- {
- SymmetricAlgorithm encryptingKey =
- (useClientKeys ? token.ClientEncryptor : token.ServerEncryptor)
- ?? throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- "Token missing symmetric key object.");
-
- using ICryptoTransform encryptor = encryptingKey.CreateEncryptor();
- byte[] blockToEncrypt = dataToEncrypt.Array;
-
- int start = dataToEncrypt.Offset;
- int count = dataToEncrypt.Count;
-
- if (count % encryptor.InputBlockSize != 0)
- {
- throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- "Input data is not an even number of encryption blocks.");
- }
- encryptor.TransformBlock(blockToEncrypt, start, count, blockToEncrypt, start);
- }
-
- ///
- /// Decrypts a message using a symmetric algorithm.
- ///
- ///
- private static void SymmetricDecrypt(
- ChannelToken token,
- ArraySegment dataToDecrypt,
- bool useClientKeys)
- {
- // get the decrypting key.
- SymmetricAlgorithm decryptingKey =
- (useClientKeys ? token.ClientEncryptor : token.ServerEncryptor)
- ?? throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- "Token missing symmetric key object.");
- using ICryptoTransform decryptor = decryptingKey.CreateDecryptor();
- byte[] blockToDecrypt = dataToDecrypt.Array;
-
- int start = dataToDecrypt.Offset;
- int count = dataToDecrypt.Count;
-
- if (count % decryptor.InputBlockSize != 0)
- {
- throw ServiceResultException.Create(
- StatusCodes.BadSecurityChecksFailed,
- "Input data is not an even number of encryption blocks.");
- }
-
- decryptor.TransformBlock(blockToDecrypt, start, count, blockToDecrypt, start);
- }
#if CURVE25519
///
diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs
index 07ccd2e25..6a95ecc2a 100644
--- a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs
+++ b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs
@@ -655,7 +655,14 @@ public static IPAddress[] GetHostAddresses(string hostNameOrAddress)
/// If the platform returns a FQDN, only the host name is returned.
public static string GetHostName()
{
- return Dns.GetHostName().Split('.')[0].ToLowerInvariant();
+ var hostName = Dns.GetHostName();
+ // If platform returns an IPv4 or IPv6 address return it as is
+ if (IPAddress.TryParse(hostName, out _))
+ {
+ return hostName;
+ }
+
+ return hostName.Split('.')[0].ToLowerInvariant();
}
///