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(); } ///