From 688e2b892ca54accc4da19b80599ebcbf4f1539f Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 19 Nov 2025 16:23:30 +0200 Subject: [PATCH 01/42] merged security changes --- .../Security/Certificates/EccUtils.cs | 977 +++++++----------- .../Security/Certificates/EncryptedSecret.cs | 784 ++++++++++++++ .../Security/Constants/SecurityPolicies.cs | 57 + .../Security/Constants/SecurityPolicyInfo.cs | 706 +++++++++++++ Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs | 53 +- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 495 +++------ 6 files changed, 2029 insertions(+), 1043 deletions(-) create mode 100644 Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs create mode 100644 Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs index 29dc0e7d55..93d7b07e29 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 0000000000..049c97af01 --- /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 c16dd613b5..a2526cab58 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -149,6 +149,20 @@ private static bool IsPlatformSupportedName(string name) return false; } + /// + /// Returns the info object associated with the SecurityPolicyUri. + /// + public static SecurityPolicyInfo GetInfo(string securityPolicyUri) + { + if (s_securityPolicyUriToInfo.Value.TryGetValue(securityPolicyUri, out SecurityPolicyInfo 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 +666,49 @@ 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[] fields = typeof(SecurityPolicies).GetFields( + BindingFlags.Public | BindingFlags.Static); + + var keyValuePairs = new Dictionary(); + foreach (FieldInfo field in fields) + { + string policyUri = (string)field.GetValue(typeof(SecurityPolicies)); + if (field.Name == nameof(BaseUri) || + field.Name == nameof(Https) || + !policyUri.StartsWith(BaseUri, StringComparison.Ordinal)) + { + continue; + } + + keyValuePairs.Add(field.Name, new SecurityPolicyInfo(field.Name, policyUri)); + } +#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 0000000000..fcbb7f2712 --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -0,0 +1,706 @@ +/* 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, + SymmetricSignatureLength = 128 / 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, + SymmetricSignatureLength = 256 / 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 f60c516fec..e2e8cbf835 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 23751c8bb1..0abac6fd51 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,19 @@ protected void DiscardTokens() /// /// Calculates the symmetric key sizes based on the current security policy. /// + /// protected void CalculateSymmetricKeySizes() { - AuthenticatedEncryption = false; - - switch (SecurityPolicyUri) - { - 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; - } + 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 +182,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) { @@ -280,172 +229,45 @@ protected void ComputeKeys(ChannelToken token) return; } + token.SecurityPolicy = SecurityPolicies.GetInfo(SecurityPolicyUri); + byte[] serverSecret = token.ServerNonce; byte[] clientSecret = token.ClientNonce; - HashAlgorithmName algorithmName = HashAlgorithmName.SHA256; - switch (SecurityPolicyUri) + if (token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha1 || + token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha256) { - 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); - -#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 - - 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); - -#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 - - 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); + 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 +300,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 +419,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 +453,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 +475,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 +494,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( @@ -915,7 +737,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,48 +745,34 @@ protected void Encrypt( ArraySegment dataToEncrypt, bool useClientKeys) { - switch (SecurityPolicyUri) + 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: - { - 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; - } -#endif - default: - throw new NotSupportedException(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; + + 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( @@ -972,43 +780,29 @@ protected void Decrypt( 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; + } -#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; - } -#endif + bool signOnly = SecurityMode == MessageSecurityMode.Sign; - default: - throw new NotSupportedException(SecurityPolicyUri); + 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); } } @@ -1021,8 +815,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 +844,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 +872,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 +901,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 +939,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 /// From 45b07a224f6cd5f0d89f2f6ed7c4de125e57e03c Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 20 Nov 2025 14:40:58 +0200 Subject: [PATCH 02/42] Tailor SecurityPolicyUri for format expected in s_securityPolicyUriToInfo --- Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs | 6 ++++++ .../Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 8 +++++++- Stack/Opc.Ua.Core/Types/Utils/Utils.cs | 9 ++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index a2526cab58..77629234da 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) || diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 0abac6fd51..760f9c0163 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -135,7 +135,13 @@ protected void DiscardTokens() /// protected void CalculateSymmetricKeySizes() { - SecurityPolicyInfo info = SecurityPolicies.GetInfo(SecurityPolicyUri) + var securityPolicyUri = SecurityPolicyUri; + if (securityPolicyUri.StartsWith(SecurityPolicies.BaseUri, StringComparison.Ordinal)) + { + securityPolicyUri = securityPolicyUri.Substring(SecurityPolicies.BaseUri.Length); + } + + SecurityPolicyInfo info = SecurityPolicies.GetInfo(securityPolicyUri) ?? throw ServiceResultException.Create( StatusCodes.BadSecurityPolicyRejected, "Unsupported security policy: {0}", diff --git a/Stack/Opc.Ua.Core/Types/Utils/Utils.cs b/Stack/Opc.Ua.Core/Types/Utils/Utils.cs index 07ccd2e251..6a95ecc2ae 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(); } /// From cfbebe7962c709bd8c462636c3da754485586134 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 21 Nov 2025 20:12:00 +0200 Subject: [PATCH 03/42] Enhance security policy handling and key computation logic --- .../Security/Constants/SecurityPolicies.cs | 29 ++++- .../Security/Constants/SecurityPolicyInfo.cs | 6 +- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 116 +++++++++++++++--- 3 files changed, 131 insertions(+), 20 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index 77629234da..a2920d6e80 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -157,15 +157,24 @@ private static bool IsPlatformSupportedName(string name) /// /// 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; } @@ -695,11 +704,14 @@ public static bool Verify( private static readonly Lazy> s_securityPolicyNameToInfo = new(() => { - FieldInfo[] fields = typeof(SecurityPolicies).GetFields( + 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 fields) + foreach (FieldInfo field in policyFields) { string policyUri = (string)field.GetValue(typeof(SecurityPolicies)); if (field.Name == nameof(BaseUri) || @@ -709,7 +721,18 @@ public static bool Verify( continue; } - keyValuePairs.Add(field.Name, new SecurityPolicyInfo(field.Name, policyUri)); + // 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(); diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs index fcbb7f2712..af75058c8f 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -210,7 +210,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() { DerivedSignatureKeyLength = 128 / 8, SymmetricEncryptionKeyLength = 128 / 8, - SymmetricSignatureLength = 128 / 8, + // HMAC-SHA1 produces a 160-bit MAC + SymmetricSignatureLength = 160 / 8, InitializationVectorLength = 128 / 8, MinAsymmetricKeyLength = 1024, MaxAsymmetricKeyLength = 2048, @@ -234,7 +235,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() { DerivedSignatureKeyLength = 192 / 8, SymmetricEncryptionKeyLength = 256 / 8, - SymmetricSignatureLength = 256 / 8, + // HMAC-SHA1 produces a 160-bit MAC + SymmetricSignatureLength = 160 / 8, InitializationVectorLength = 128 / 8, MinAsymmetricKeyLength = 1024, MaxAsymmetricKeyLength = 2048, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 760f9c0163..562272fbf7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -230,16 +230,35 @@ private void DeriveKeysWithHKDF( /// protected void ComputeKeys(ChannelToken token) { + // Strip BaseUri prefix to get short name for dictionary lookup + var securityPolicyUri = SecurityPolicyUri; + if (securityPolicyUri.StartsWith(SecurityPolicies.BaseUri, StringComparison.Ordinal)) + { + securityPolicyUri = securityPolicyUri.Substring(SecurityPolicies.BaseUri.Length); + } + + token.SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + if (token.SecurityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + SecurityPolicyUri); + } + if (SecurityMode == MessageSecurityMode.None) { return; } - token.SecurityPolicy = SecurityPolicies.GetInfo(SecurityPolicyUri); - byte[] serverSecret = token.ServerNonce; byte[] clientSecret = token.ClientNonce; + m_logger?.LogInformation( + "[ComputeKeys] KeyDerivationAlgorithm: {Algo}", + token.SecurityPolicy.KeyDerivationAlgorithm); + if (token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha1 || token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha256) { @@ -614,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, @@ -627,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]; @@ -642,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); @@ -671,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); @@ -682,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); } @@ -751,16 +783,41 @@ protected void Encrypt( ArraySegment dataToEncrypt, bool useClientKeys) { + 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) { return; } - byte[] encryptingKey = useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey; - byte[] iv = useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector; - byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey; + // For CBC based policies the caller already applied padding and signatures. + if (token.SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc + or SymmetricEncryptionAlgorithm.Aes256Cbc) + { + if (signOnly) + { + return; + } - bool signOnly = SecurityMode == MessageSecurityMode.Sign; + 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, @@ -781,14 +838,14 @@ protected void Encrypt( /// Decrypts and verifies the data in a buffer using symmetric encryption. /// /// - protected void Decrypt( + protected int Decrypt( ChannelToken token, ArraySegment dataToDecrypt, bool useClientKeys) { if (SecurityPolicyUri == SecurityPolicies.None) { - return; + return dataToDecrypt.Count; } byte[] encryptingKey = useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey; @@ -797,6 +854,32 @@ protected void Decrypt( 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; + } + + 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, @@ -810,6 +893,9 @@ protected void Decrypt( { 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 From d0c63213528a35f2e631fe37a7b6c7d7517a5a87 Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Tue, 25 Nov 2025 22:52:12 -0800 Subject: [PATCH 04/42] Merge SecurityEnhancements. --- .../ConsoleReferenceClient/Program.cs | 16 +- .../Quickstarts.ReferenceClient.Config.xml | 24 +- .../ConsoleReferenceClient/RunTest.cs | 654 ++++++++++++ .../generate_user_certificate.ps1 | 80 ++ .../ConsoleReferenceServer/Program.cs | 14 +- .../Quickstarts.ReferenceServer.Config.xml | 68 +- Libraries/Opc.Ua.Client/Session/Session.cs | 186 +++- .../ApplicationInstance.cs | 2 +- .../Configuration/ConfigurationNodeManager.cs | 2 +- .../Opc.Ua.Server/Server/StandardServer.cs | 25 +- Libraries/Opc.Ua.Server/Session/Session.cs | 53 +- .../Stack/Https/HttpsTransportListener.cs | 13 +- .../Schema/SecuredApplicationHelpers.cs | 29 +- .../Certificates/CertificateIdentifier.cs | 33 +- .../{EccUtils.cs => CryptoUtils.cs} | 264 +++-- .../Security/Certificates/EncryptedSecret.cs | 439 ++------ .../Security/Certificates/Nonce.cs | 198 ++-- .../Security/Certificates/X509Utils.cs | 2 +- .../Constants/AdditionalParameterNames.cs | 34 + .../Security/Constants/SecurityPolicies.cs | 624 +++++++---- .../Security/Constants/SecurityPolicyInfo.cs | 570 +++++++++- .../Stack/Client/ClientChannelManager.cs | 12 + .../Configuration/EndpointDescription.cs | 16 +- .../Stack/Https/HttpsTransportChannel.cs | 23 +- .../Stack/Server/SecureChannelContext.cs | 34 +- Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs | 15 + Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs | 45 + .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 40 +- .../Stack/Tcp/TcpTransportListener.cs | 6 +- .../Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs | 297 +++--- .../Stack/Tcp/UaSCBinaryChannel.Rsa.cs | 18 +- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 979 ++++-------------- .../Stack/Tcp/UaSCBinaryChannel.cs | 24 +- .../Stack/Tcp/UaSCBinaryClientChannel.cs | 39 +- .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 13 +- .../Stack/Transport/ITransportChannel.cs | 25 +- .../Stack/Transport/NullChannel.cs | 16 +- .../Stack/Types/UserNameIdentityToken.cs | 6 +- .../Stack/Types/X509IdentityToken.cs | 16 +- .../Diagnostics/TelemetryUtils.cs | 2 +- Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs | 3 +- UA Reference.sln | 15 + 42 files changed, 3020 insertions(+), 1954 deletions(-) create mode 100644 Applications/ConsoleReferenceClient/RunTest.cs create mode 100644 Applications/ConsoleReferenceClient/generate_user_certificate.ps1 rename Stack/Opc.Ua.Core/Security/Certificates/{EccUtils.cs => CryptoUtils.cs} (83%) create mode 100644 Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs index 7f60afa397..5d47b107ac 100644 --- a/Applications/ConsoleReferenceClient/Program.cs +++ b/Applications/ConsoleReferenceClient/Program.cs @@ -75,8 +75,8 @@ public static async Task Main(string[] args) byte[] userpassword = null; string userCertificateThumbprint = null; byte[] userCertificatePassword = null; - bool logConsole = false; - bool appLog = false; + bool logConsole = true; + bool appLog = true; bool fileLog = false; bool renewCertificate = false; bool loadTypes = false; @@ -184,7 +184,7 @@ public static async Task Main(string[] args) } }, { - "f|fetchall", + "fa|fetchall", "Fetch all nodes", f => { @@ -333,7 +333,7 @@ public static async Task Main(string[] args) logConsole, fileLog, appLog, - LogLevel.Information); + LogLevel.Warning); // delete old certificate if (renewCertificate) @@ -370,6 +370,14 @@ await application.DeleteApplicationInstanceCertificateAsync() CancellationToken ct = quitCTS.Token; ManualResetEvent quitEvent = ConsoleUtils.CtrlCHandler(quitCTS); + // insert security tester. + var tester = new SecurityTestClient.RunTest(config, telemetry); + + if (await tester.RunAsync(quitEvent, ct).ConfigureAwait(false)) + { + return; + } + var userIdentity = new UserIdentity(); // set user identity of type username/pw diff --git a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml index aa3a8ae264..48028419bf 100644 --- a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml +++ b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml @@ -13,35 +13,35 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -49,17 +49,17 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/issuer + ./pki/issuer Directory - %LocalApplicationData%/OPC Foundation/pki/trusted + ./pki/trusted Directory - %LocalApplicationData%/OPC Foundation/pki/rejected + ./pki/rejected 5 Directory - %LocalApplicationData%/OPC Foundation/pki/userIssuer + ./pki/userIssuer Directory - %LocalApplicationData%/OPC Foundation/pki/trustedUser + ./pki/trustedUser @@ -92,7 +92,7 @@ 4194304 65535 300000 - 3600000 + 30000 60000 @@ -120,7 +120,7 @@ - %LocalApplicationData%/OPC Foundation/Logs/Quickstarts.ReferenceClient.log.txt + ./Logs/Quickstarts.ReferenceClient.log.txt true diff --git a/Applications/ConsoleReferenceClient/RunTest.cs b/Applications/ConsoleReferenceClient/RunTest.cs new file mode 100644 index 0000000000..a129e6dbb6 --- /dev/null +++ b/Applications/ConsoleReferenceClient/RunTest.cs @@ -0,0 +1,654 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +namespace SecurityTestClient +{ + internal sealed class RunTest + { + private readonly Lock m_lock = new(); + private SessionReconnectHandler m_reconnectHandler; + private ILogger m_logger; + private ITelemetryContext m_context; + private ApplicationConfiguration m_configuration; + private ISession m_session; + + const string ServerUrl = "opc.tcp://localhost:62541"; + const int kMaxSearchDepth = 128; + const int ReconnectPeriod = 1000; + const int ReconnectPeriodExponentialBackoff = 15000; + + public RunTest(ApplicationConfiguration configuration, ITelemetryContext context) + { + m_context = context; + m_configuration = configuration; + m_logger = context.CreateLogger("Test"); + + m_reconnectHandler = new SessionReconnectHandler( + context, + true, + ReconnectPeriodExponentialBackoff); + } + + private string GetUserCertificateFile(string securityPolicyUri) + { + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + switch (securityPolicy.CertificateKeyAlgorithm) + { + default: + case CertificateKeyAlgorithm.RSA: + case CertificateKeyAlgorithm.RSADH: + return $"iama.tester.rsa.der"; + case CertificateKeyAlgorithm.BrainpoolP256r1: + return $"iama.tester.brainpoolP256r1.der"; + case CertificateKeyAlgorithm.BrainpoolP384r1: + return $"iama.tester.brainpoolP384r1.der"; + case CertificateKeyAlgorithm.NistP256: + return $"iama.tester.nistP256.der"; + case CertificateKeyAlgorithm.NistP384: + return $"iama.tester.nistP384.der"; + } + } + + public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken ct) + { + try + { + m_logger.LogInformation("OPC UA Security Test Client"); + + // The application name and config file names + const string applicationName = "ConsoleReferenceClient"; + const string configSectionName = "Quickstarts.ReferenceClient"; + + // Define the UA Client application + var passwordProvider = new CertificatePasswordProvider([]); + + var application = new ApplicationInstance(m_context) + { + ApplicationName = applicationName, + ApplicationType = ApplicationType.Client, + ConfigSectionName = configSectionName, + CertificatePasswordProvider = passwordProvider + }; + + // load the application configuration. + var configuration = m_configuration = await application + .LoadApplicationConfigurationAsync(silent: false, ct: ct) + .ConfigureAwait(false); + + m_configuration.CertificateValidator.CertificateValidation += CertificateValidation; + + // check the application certificate. + bool haveAppCertificate = await application + .CheckApplicationInstanceCertificatesAsync(false, ct: ct) + .ConfigureAwait(false); + + if (!haveAppCertificate) + { + throw new InvalidOperationException("Application instance certificate invalid!"); + } + + m_logger.LogInformation("Connecting to... {ServerUrl}", ServerUrl); + + var endpoints = await GetEndpoints( + m_configuration, + ServerUrl, + ct).ConfigureAwait(false); + + var endpointConfiguration = EndpointConfiguration.Create(m_configuration); + var sessionFactory = new DefaultSessionFactory(m_context); + var userNameidentity = new UserIdentity("sysadmin", new UTF8Encoding(false).GetBytes("demo")); + + foreach (var ii in endpoints) + { + var userCertificateFile = GetUserCertificateFile(ii.SecurityPolicyUri); + var x509 = X509CertificateLoader.LoadCertificateFromFile(Path.Combine(".\\pki\\trustedUser\\certs", userCertificateFile)); + var thumbprint = x509.Thumbprint; + + var certificateIdentity = await LoadUserCertificateAsync(thumbprint, "password", ct).ConfigureAwait(false); + + foreach (var identity in new UserIdentity[] { userNameidentity, certificateIdentity }) + { + try + { + m_logger.LogWarning("{Line}", new string('=', 80)); + + m_logger.LogWarning( + "SECURITY-POLICY={SecurityPolicyUri} {SecurityMode}", + SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri), + ii.SecurityMode); + + m_logger.LogWarning( + "IDENTITY={DisplayName} {TokenType}", + identity.DisplayName, + identity.TokenType); + + ISession session = await RunTestAsync( + endpointConfiguration, + sessionFactory, + ii, + identity, + ct).ConfigureAwait(false); + + m_logger.LogWarning("Waiting for SecureChannel renew"); + + for (int count = 0; count < 10; count++) + { + var result = await session.ReadAsync( + null, + 0, + TimestampsToReturn.Neither, + new ReadValueIdCollection() + { + new ReadValueId() + { + NodeId = Opc.Ua.VariableIds.Server_ServerStatus_CurrentTime, + AttributeId = Attributes.Value + } + }, + ct).ConfigureAwait(false); + + m_logger.LogWarning( + "CurrentTime: {CurrentTime}", + result.Results[0].GetValueOrDefault().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture)); + + await Task.Delay(5000, ct).ConfigureAwait(false); + } + + await session.CloseAsync(true, ct: ct).ConfigureAwait(false); + } + catch (Exception e) + { + Console.WriteLine("Exception: {0}", e.Message); + Console.WriteLine("StackTrace: {0}", e.StackTrace); + quitEvent.WaitOne(20000); + } + + m_logger.LogWarning( + "TEST COMPLETE: {SecurityPolicyUri} {SecurityMode}", + SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri), + ii.SecurityMode); + + m_logger.LogWarning("{Line}", new string('=', 80)); + } + } + + Console.WriteLine("Ctrl-C to stop."); + quitEvent.WaitOne(); + } + catch (Exception e) + { + m_logger.LogError("Exception: {Message}", e.Message); + m_logger.LogTrace("StackTrace: {StackTrace}", e.StackTrace); + } + + return true; + } + + private async Task LoadUserCertificateAsync(string thumbprint, string password, CancellationToken ct) + { +#if NET8_0_OR_GREATER + var store = m_configuration.SecurityConfiguration.TrustedUserCertificates; + + // get user certificate with matching thumbprint + var hit = ( + await store.GetCertificatesAsync(m_context, ct).ConfigureAwait(false) + ).Find(X509FindType.FindByThumbprint, thumbprint, false).FirstOrDefault(); + + // create Certificate Identifier + var cid = new CertificateIdentifier(hit) + { + StorePath = store.StorePath, + StoreType = store.StoreType + }; + + return await UserIdentity.CreateAsync( + cid, + new CertificatePasswordProvider(new UTF8Encoding(false).GetBytes(password)), + m_context, + ct + ).ConfigureAwait(false); +#else + await Task.Delay(1, ct).ConfigureAwait(false); + throw new NotSupportedException("User certificate identity is only supported on .NET 8 or greater."); +#endif + } + + public async Task RunTestAsync( + EndpointConfiguration endpointConfiguration, + DefaultSessionFactory sessionFactory, + EndpointDescription endpointDescription, + UserIdentity identity, + CancellationToken ct) + { + var endpoint = new ConfiguredEndpoint( + null, + endpointDescription, + endpointConfiguration); + + // Create the session + ISession session = await sessionFactory + .CreateAsync( + m_configuration, + endpoint, + false, + false, + m_configuration.ApplicationName, + 600000, + //new UserIdentity(), + (endpointDescription.SecurityMode != MessageSecurityMode.None) ? identity : new UserIdentity(), + null, + ct + ) + .ConfigureAwait(false); + + // Assign the created session + if (session == null || !session.Connected) + { + throw new InvalidOperationException("Could not connect to server at " + ServerUrl); + } + + session.KeepAliveInterval = 10000; + session.KeepAlive += Session_KeepAlive; + + var nodes = await BrowseFullAddressSpaceAsync( + session, + ObjectIds.ObjectsFolder, + null, + ct).ConfigureAwait(false); + + return session; + } + private async ValueTask> GetEndpoints( + ApplicationConfiguration application, + string discoveryUrl, + CancellationToken ct = default) + { + var endpointConfiguration = EndpointConfiguration.Create(application); + + using var client = await DiscoveryClient.CreateAsync( + application, + new Uri(discoveryUrl), + endpointConfiguration, + ct: ct).ConfigureAwait(false); + + return await client.GetEndpointsAsync(null, ct).ConfigureAwait(false); + } + + private void CertificateValidation( + CertificateValidator sender, + CertificateValidationEventArgs e) + { + bool certificateAccepted = false; + + // **** + // Implement a custom logic to decide if the certificate should be + // accepted or not and set certificateAccepted flag accordingly. + // The certificate can be retrieved from the e.Certificate field + // *** + + ServiceResult error = e.Error; + m_logger.LogInformation("{ServiceResult}", error); + if (error.StatusCode == StatusCodes.BadCertificateUntrusted) + { + certificateAccepted = true; + } + + if (certificateAccepted) + { + m_logger.LogInformation( + "Untrusted Certificate accepted. Subject = {Subject}", + e.Certificate.Subject); + e.Accept = true; + } + else + { + m_logger.LogInformation( + "Untrusted Certificate rejected. Subject = {Subject}", + e.Certificate.Subject); + } + } + + private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) + { + try + { + // check for events from discarded sessions. + if (m_session == null || !m_session.Equals(session)) + { + return; + } + + // start reconnect sequence on communication error. + if (ServiceResult.IsBad(e.Status)) + { + SessionReconnectHandler.ReconnectState state = m_reconnectHandler + .BeginReconnect( + m_session, + null, + ReconnectPeriod, + Client_ReconnectComplete); + + if (state == SessionReconnectHandler.ReconnectState.Triggered) + { + m_logger.LogInformation( + "KeepAlive status {Status}, reconnect status {State}, reconnect period {ReconnectPeriod}ms.", + e.Status, + state, + ReconnectPeriod + ); + } + else + { + m_logger.LogInformation( + "KeepAlive status {Status}, reconnect status {State}.", + e.Status, + state); + } + + // cancel sending a new keep alive request, because reconnect is triggered. + e.CancelKeepAlive = true; + } + } + catch (Exception exception) + { + m_logger.LogError(exception, "Error in OnKeepAlive."); + } + } + private void Client_ReconnectComplete(object sender, EventArgs e) + { + // ignore callbacks from discarded objects. + if (!ReferenceEquals(sender, m_reconnectHandler)) + { + return; + } + + lock (m_lock) + { + // if session recovered, Session property is null + if (m_reconnectHandler.Session != null) + { + // ensure only a new instance is disposed + // after reactivate, the same session instance may be returned + if (!ReferenceEquals(m_session, m_reconnectHandler.Session)) + { + m_logger.LogInformation( + "--- RECONNECTED TO NEW SESSION --- {SessionId}", + m_reconnectHandler.Session.SessionId + ); + ISession session = m_session; + m_session = m_reconnectHandler.Session; + Utils.SilentDispose(session); + } + else + { + m_logger.LogInformation( + "--- REACTIVATED SESSION --- {SessionId}", + m_reconnectHandler.Session.SessionId); + } + } + else + { + m_logger.LogInformation("--- RECONNECT KeepAlive recovered ---"); + } + } + } + + private async Task BrowseFullAddressSpaceAsync( + ISession session, + NodeId startingNode = null, + BrowseDescription browseDescription = null, + CancellationToken ct = default) + { + var stopWatch = new Stopwatch(); + stopWatch.Start(); + + // Browse template + const int kMaxReferencesPerNode = 1000; + BrowseDescription browseTemplate = + browseDescription + ?? new BrowseDescription + { + NodeId = startingNode ?? ObjectIds.RootFolder, + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, + IncludeSubtypes = true, + NodeClassMask = 0, + ResultMask = (uint)BrowseResultMask.All + }; + BrowseDescriptionCollection browseDescriptionCollection + = CreateBrowseDescriptionCollectionFromNodeId( + [.. new NodeId[] { startingNode ?? ObjectIds.RootFolder }], + browseTemplate); + + // Browse + var referenceDescriptions = new Dictionary(); + var random = new Random(11211); // use a fixed seed for test reproducibility + + int searchDepth = 0; + uint maxNodesPerBrowse = session.OperationLimits.MaxNodesPerBrowse; + while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth) + { + searchDepth++; + m_logger.LogInformation( + "{SearchDepth}: Browse {Count} nodes after {ElapsedMilliseconds}ms", + searchDepth, + browseDescriptionCollection.Count, + stopWatch.ElapsedMilliseconds); + + var allBrowseResults = new BrowseResultCollection(); + bool repeatBrowse; + var browseResultCollection = new BrowseResultCollection(); + var unprocessedOperations = new BrowseDescriptionCollection(); + DiagnosticInfoCollection diagnosticsInfoCollection; + do + { + BrowseDescriptionCollection browseCollection = + maxNodesPerBrowse == 0 + ? browseDescriptionCollection + : browseDescriptionCollection.Take((int)maxNodesPerBrowse).ToArray(); + repeatBrowse = false; + try + { + RequestHeader requestHeader = null; + + // a random pattern to obscure the message size + // (only useful for application running over untrusted networks). + if (session.ConfiguredEndpoint.Description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + // a real application needs to use secure randomness +#pragma warning disable CA5394 // Do not use insecure randomness + var padding = new byte[random.Next() % 4096]; + random.NextBytes(padding); +#pragma warning restore CA5394 // Do not use insecure randomness + + requestHeader = new RequestHeader + { + AdditionalHeader = new ExtensionObject(new Opc.Ua.AdditionalParametersType() + { + Parameters = new KeyValuePairCollection([ + new Opc.Ua.KeyValuePair() { + Key = AdditionalParameterNames.Padding, + Value = new Variant(padding) + } + ]) + }) + }; + } + + BrowseResponse browseResponse = await + session.BrowseAsync( + requestHeader, + null, + kMaxReferencesPerNode, + browseCollection, + ct) + .ConfigureAwait(false); + browseResultCollection = browseResponse.Results; + diagnosticsInfoCollection = browseResponse.DiagnosticInfos; + ClientBase.ValidateResponse(browseResultCollection, browseCollection); + ClientBase.ValidateDiagnosticInfos( + diagnosticsInfoCollection, + browseCollection); + + // separate unprocessed nodes for later + int ii = 0; + foreach (BrowseResult browseResult in browseResultCollection) + { + // check for error. + StatusCode statusCode = browseResult.StatusCode; + if (StatusCode.IsBad(statusCode)) + { + // this error indicates that the server does not have enough simultaneously active + // continuation points. This request will need to be resent after the other operations + // have been completed and their continuation points released. + if (statusCode == StatusCodes.BadNoContinuationPoints) + { + unprocessedOperations.Add(browseCollection[ii++]); + continue; + } + } + + // save results. + allBrowseResults.Add(browseResult); + ii++; + } + } + catch (ServiceResultException sre) + { + if (sre.StatusCode is StatusCodes.BadEncodingLimitsExceeded or StatusCodes + .BadResponseTooLarge) + { + // try to address by overriding operation limit + maxNodesPerBrowse = + maxNodesPerBrowse == 0 + ? (uint)browseCollection.Count / 2 + : maxNodesPerBrowse / 2; + repeatBrowse = true; + } + else + { + m_logger.LogError("Browse error: {Message}", sre.Message); + throw; + } + } + } while (repeatBrowse); + + if (maxNodesPerBrowse == 0) + { + browseDescriptionCollection.Clear(); + } + else + { + browseDescriptionCollection = browseDescriptionCollection + .Skip(browseResultCollection.Count) + .ToArray(); + } + + // Browse next + ByteStringCollection continuationPoints = PrepareBrowseNext(browseResultCollection); + while (continuationPoints.Count > 0) + { + m_logger.LogInformation("BrowseNext {Count} continuation points.", continuationPoints.Count); + BrowseNextResponse browseNextResult = await + session.BrowseNextAsync(null, false, continuationPoints, ct) + .ConfigureAwait(false); + BrowseResultCollection browseNextResultCollection = browseNextResult.Results; + diagnosticsInfoCollection = browseNextResult.DiagnosticInfos; + ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints); + ClientBase.ValidateDiagnosticInfos( + diagnosticsInfoCollection, + continuationPoints); + allBrowseResults.AddRange(browseNextResultCollection); + continuationPoints = PrepareBrowseNext(browseNextResultCollection); + } + + // Build browse request for next level + var browseTable = new NodeIdCollection(); + int duplicates = 0; + foreach (BrowseResult browseResult in allBrowseResults) + { + foreach (ReferenceDescription reference in browseResult.References) + { + if (!referenceDescriptions.ContainsKey(reference.NodeId)) + { + referenceDescriptions[reference.NodeId] = reference; + if (reference.ReferenceTypeId != ReferenceTypeIds.HasProperty) + { + browseTable.Add( + ExpandedNodeId.ToNodeId( + reference.NodeId, + session.NamespaceUris)); + } + } + else + { + duplicates++; + } + } + } + if (duplicates > 0) + { + m_logger.LogInformation("Browse Result {Duplicates} duplicate nodes were ignored.", duplicates); + } + browseDescriptionCollection.AddRange( + CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate)); + + // add unprocessed nodes if any + browseDescriptionCollection.AddRange(unprocessedOperations); + } + + stopWatch.Stop(); + + var result = new ReferenceDescriptionCollection(referenceDescriptions.Values); + result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); + + m_logger.LogWarning( + "BrowseFullAddressSpace found {Count} references on server in {ElapsedMilliseconds}ms.", + referenceDescriptions.Count, + stopWatch.ElapsedMilliseconds); + + return result; + } + + private static BrowseDescriptionCollection CreateBrowseDescriptionCollectionFromNodeId( + NodeIdCollection nodeIdCollection, + BrowseDescription template) + { + var browseDescriptionCollection = new BrowseDescriptionCollection(); + foreach (NodeId nodeId in nodeIdCollection) + { + var browseDescription = (BrowseDescription)template.MemberwiseClone(); + browseDescription.NodeId = nodeId; + browseDescriptionCollection.Add(browseDescription); + } + return browseDescriptionCollection; + } + + private static ByteStringCollection PrepareBrowseNext( + BrowseResultCollection browseResultCollection) + { + var continuationPoints = new ByteStringCollection(); + foreach (BrowseResult browseResult in browseResultCollection) + { + if (browseResult.ContinuationPoint != null) + { + continuationPoints.Add(browseResult.ContinuationPoint); + } + } + return continuationPoints; + } + } +} diff --git a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 new file mode 100644 index 0000000000..bb3a12a470 --- /dev/null +++ b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 @@ -0,0 +1,80 @@ +# 1. Ensure directories exist +$certDir = "./pki/trustedUser/certs" +$privateDir = "./pki/trustedUser/private" + +$curves = @( + 'nistP256', + 'nistP384', + 'brainpoolP256r1', + 'brainpoolP384r1' +) + +foreach ($d in @($certDir, $privateDir)) { + if (-not (Test-Path $d)) { + New-Item -ItemType Directory -Path $d -Force | Out-Null + } +} + +# 2. Create a self-signed ECC certificate (NIST P-256) +# $cert = New-SelfSignedCertificate ` +# -Subject "CN=iama.tester@example.com" ` +# -CertStoreLocation "Cert:\CurrentUser\My" ` +# -KeyExportPolicy Exportable ` +# -KeySpec Signature ` +# -KeyAlgorithm ECDSA_nistP256 ` +# -Curve 'CurveName' ` +# -HashAlgorithm SHA256 ` +# -NotAfter (Get-Date).AddYears(1) + +foreach ($curve in $curves) { + + Write-Host "Generating certificate for curve: $curve" + + # Create certificate parameters and dynamically insert the curve + $params = @{ + Type = 'Custom' + Subject = 'CN=iama.tester@example.com' + TextExtension = @( + '2.5.29.37={text}1.3.6.1.5.5.7.3.2' + '2.5.29.17={text}upn=iama.tester@example.com' + ) + KeyUsage = 'DigitalSignature' + KeyAlgorithm = "ECDSA_$curve" # <-- dynamic! + CurveExport = 'CurveName' + CertStoreLocation = 'Cert:\CurrentUser\My' + } + + # 1. Create cert + $cert = New-SelfSignedCertificate @params + + # 2. Export DER + $derPath = Join-Path $certDir "iama.tester.$curve.der" + Export-Certificate -Cert $cert -FilePath $derPath -Type CERT + + # 3. Export PFX with password + $secret = ConvertTo-SecureString -String "password" -Force -AsPlainText + $pfxPath = Join-Path $privateDir "iama.tester.$curve.pfx" + Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $secret + + Write-Host "Finished: $curve`n" +} + +Write-Host "`n=== Generating RSA-2048 ===" + +$rsaParams = @{ + Type = 'Custom' + Subject = 'CN=iama.tester@example.com' + TextExtension = @( + '2.5.29.37={text}1.3.6.1.5.5.7.3.2' + '2.5.29.17={text}upn=iama.tester@example.com' + ) + KeyUsage = 'DigitalSignature' + KeyAlgorithm = 'RSA' + KeyLength = 2048 + CertStoreLocation = 'Cert:\CurrentUser\My' +} + +$rsaCert = New-SelfSignedCertificate @rsaParams + +Export-Certificate -Cert $rsaCert -FilePath (Join-Path $certDir "iama.tester.rsa.der") -Type CERT +Export-PfxCertificate -Cert $rsaCert -FilePath (Join-Path $privateDir "iama.tester.rsa.pfx") -Password $secret diff --git a/Applications/ConsoleReferenceServer/Program.cs b/Applications/ConsoleReferenceServer/Program.cs index 77fe414fd6..03d49ecb5c 100644 --- a/Applications/ConsoleReferenceServer/Program.cs +++ b/Applications/ConsoleReferenceServer/Program.cs @@ -61,9 +61,9 @@ public static async Task Main(string[] args) // command line options bool showHelp = false; - bool autoAccept = false; - bool logConsole = false; - bool appLog = false; + bool autoAccept = true; + bool logConsole = true; + bool appLog = true; bool fileLog = false; bool renewCertificate = false; bool shadowConfig = false; @@ -146,7 +146,13 @@ await server } // setup the logging - telemetry.ConfigureLogging(server.Configuration, applicationName, logConsole, fileLog, appLog, LogLevel.Information); + telemetry.ConfigureLogging( + server.Configuration, + applicationName, + logConsole, + fileLog, + appLog, + LogLevel.Warning); // check or renew the certificate logger.LogInformation("Check the certificate."); diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 16fd703f63..f1cfad96bf 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -14,35 +14,35 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - %LocalApplicationData%/OPC Foundation/pki/own + ./pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -51,17 +51,17 @@ Directory - %LocalApplicationData%/OPC Foundation/pki/issuer + ./pki/issuer Directory - %LocalApplicationData%/OPC Foundation/pki/trusted + ./pki/trusted Directory - %LocalApplicationData%/OPC Foundation/pki/rejected + ./pki/rejected 5 Directory - %LocalApplicationData%/OPC Foundation/pki/issuerUser + ./pki/issuerUser Directory - %LocalApplicationData%/OPC Foundation/pki/trustedUser + ./pki/trustedUser @@ -94,11 +94,11 @@ 4194304 65535 30000 - 3600000 + 30000 - opc.https://localhost:62540/Quickstarts/ReferenceServer + opc.tcp://localhost:62541/Quickstarts/ReferenceServer @@ -122,6 +122,7 @@ --> + Sign_2 - http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AES + + + + + + + + + + SignAndEncrypt_3 + + + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AES + + + + + + + + None_1 http://opcfoundation.org/UA/SecurityPolicy#None - 5 100 @@ -335,7 +347,7 @@ - %LocalApplicationData%/OPC Foundation/Logs/Quickstarts.ReferenceServer.log.txt + ./Logs/Quickstarts.ReferenceServer.log.txt true diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 891f2e0ed8..c8fbd97628 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -308,7 +308,7 @@ private void ValidateServerNonce( if (!Nonce.ValidateNonce( serverNonce, MessageSecurityMode.SignAndEncrypt, - (uint)m_configuration.SecurityConfiguration.NonceLength)) + m_configuration.SecurityConfiguration.NonceLength)) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) @@ -875,7 +875,7 @@ public virtual void Restore(SessionState state) public void Snapshot(out SessionConfiguration sessionConfiguration) { var serverNonce = Nonce.CreateNonce( - m_endpoint.Description?.SecurityPolicyUri, + SecurityPolicies.GetInfo(m_endpoint.Description?.SecurityPolicyUri), m_serverNonce); sessionConfiguration = new SessionConfiguration { @@ -1115,8 +1115,8 @@ await m_configuration } // create a nonce. - uint length = (uint)m_configuration.SecurityConfiguration.NonceLength; - byte[] clientNonce = Nonce.CreateRandomNonceData(length); + int length = m_configuration.SecurityConfiguration.NonceLength; + m_clientNonce = Nonce.CreateRandomNonceData(length); // send the application instance certificate for the client. BuildCertificateData( @@ -1144,10 +1144,10 @@ await m_configuration bool successCreateSession = false; CreateSessionResponse? response = null; - //if security none, first try to connect without certificate + // if security none, first try to connect without certificate if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { - //first try to connect with client certificate NULL + // first try to connect with client certificate NULL try { response = await base.CreateSessionAsync( @@ -1156,7 +1156,7 @@ await m_configuration m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), sessionName, - clientNonce, + m_clientNonce, null, sessionTimeout, maxMessageSize, @@ -1179,7 +1179,7 @@ await m_configuration m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl.ToString(), sessionName, - clientNonce, + m_clientNonce, clientCertificateChainData ?? clientCertificateData, sessionTimeout, maxMessageSize, @@ -1230,7 +1230,8 @@ await m_configuration serverSignature, clientCertificateData, clientCertificateChainData, - clientNonce); + m_clientNonce, + serverNonce); HandleSignedSoftwareCertificates(serverSoftwareCertificates); @@ -1238,10 +1239,20 @@ await m_configuration ProcessResponseAdditionalHeader(response.ResponseHeader, serverCertificate); // create the client signature. - byte[] dataToSign = Utils.Append(serverCertificate?.RawData, serverNonce); - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + // create the client signature. + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.SecureChannelHash, + serverNonce, + serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, + m_instanceCertificate, dataToSign); // select the security policy for the user token. @@ -1252,18 +1263,24 @@ await m_configuration tokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; } - // save previous nonce - byte[]? previousServerNonce = GetCurrentTokenServerNonce(); - // validate server nonce and security parameters for user identity. ValidateServerNonce( identity, serverNonce, tokenSecurityPolicyUri, - previousServerNonce, + m_previousServerNonce, m_endpoint.Description.SecurityMode); // sign data with user token. + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.SecureChannelHash, + serverNonce, + serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + SignatureData userTokenSignature = identityToken.Sign( dataToSign, tokenSecurityPolicyUri, @@ -1334,7 +1351,7 @@ SignedSoftwareCertificateCollection clientSoftwareCertificates // save nonces. m_sessionName = sessionName; m_identity = identity; - m_previousServerNonce = previousServerNonce; + m_previousServerNonce = m_serverNonce; m_serverNonce = serverNonce; m_serverCertificate = serverCertificate; @@ -1419,12 +1436,20 @@ public async Task UpdateSessionAsync( // get the identity token. string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, serverNonce); - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.SecureChannelHash, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, + m_instanceCertificate, dataToSign); // choose a default token. @@ -1469,6 +1494,16 @@ public async Task UpdateSessionAsync( // sign data with user token. UserIdentityToken identityToken = identity.GetIdentityToken(); identityToken.PolicyId = identityPolicy.PolicyId; + + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.SecureChannelHash, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + SignatureData userTokenSignature = identityToken.Sign( dataToSign, tokenSecurityPolicyUri, @@ -2279,12 +2314,23 @@ public async Task ReconnectAsync( // await LoadInstanceCertificateAsync(true, ct).ConfigureAwait(false); + string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, m_serverNonce); + byte[] dataToSign = securityPolicy.GetClientSignatureData( + TransportChannel.SecureChannelHash, + m_serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + EndpointDescription endpoint = m_endpoint.Description; - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( endpoint.SecurityPolicyUri, + m_instanceCertificate, dataToSign); // check that the user identity is supported by the endpoint. @@ -2323,6 +2369,16 @@ public async Task ReconnectAsync( // sign data with user token. UserIdentityToken identityToken = m_identity.GetIdentityToken(); identityToken.PolicyId = identityPolicy.PolicyId; + + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.SecureChannelHash, + m_serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); + SignatureData userTokenSignature = identityToken.Sign( dataToSign, tokenSecurityPolicyUri, @@ -3875,36 +3931,48 @@ private void ValidateServerSignature( SignatureData serverSignature, byte[]? clientCertificateData, byte[]? clientCertificateChainData, - byte[] clientNonce) + byte[] clientNonce, + byte[] serverNonce) { if (serverSignature == null || serverSignature.Signature == null) { m_logger.LogInformation("Server signature is null or empty."); - - //throw ServiceResultException.Create( - // StatusCodes.BadSecurityChecksFailed, - // "Server signature is null or empty."); + return; } // validate the server's signature. - byte[] dataToSign = Utils.Append(clientCertificateData, clientNonce); + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(m_endpoint.Description.SecurityPolicyUri); - if (!SecurityPolicies.Verify( - serverCertificate, + byte[] dataToSign = securityPolicy.GetServerSignatureData( + TransportChannel.SecureChannelHash, + clientNonce, + TransportChannel.ServerChannelCertificate, + clientCertificateData, + TransportChannel.ClientChannelCertificate, + serverNonce); + + if (!SecurityPolicies.VerifySignatureData( + serverSignature, m_endpoint.Description.SecurityPolicyUri, - dataToSign, - serverSignature)) + serverCertificate, + dataToSign)) { // validate the signature with complete chain if the check with leaf certificate failed. if (clientCertificateChainData != null) { - dataToSign = Utils.Append(clientCertificateChainData, clientNonce); - - if (!SecurityPolicies.Verify( - serverCertificate, - m_endpoint.Description.SecurityPolicyUri, - dataToSign, - serverSignature)) + dataToSign = securityPolicy.GetServerSignatureData( + TransportChannel.SecureChannelHash, + clientNonce, + TransportChannel.ServerChannelCertificate, + clientCertificateChainData, + TransportChannel.ClientChannelCertificate, + serverNonce); + + if (!SecurityPolicies.VerifySignatureData( + serverSignature, + m_endpoint.Description.SecurityPolicyUri, + serverCertificate, + dataToSign)) { throw ServiceResultException.Create( StatusCodes.BadApplicationSignatureInvalid, @@ -4166,15 +4234,6 @@ private static void UpdateDescription( return (result, error); } - /// - /// If available, returns the current nonce or null. - /// - private byte[]? GetCurrentTokenServerNonce() - { - ChannelToken? currentToken = (NullableTransportChannel as ISecureChannel)?.CurrentToken; - return currentToken?.ServerNonce; - } - /// /// Handles the validation of server software certificates and application callback. /// @@ -4807,11 +4866,11 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( } m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; - if (EccUtils.IsEccPolicy(userTokenSecurityPolicyUri)) + if (CryptoUtils.IsEccPolicy(userTokenSecurityPolicyUri)) { var parameters = new AdditionalParametersType(); parameters.Parameters.Add( - new KeyValuePair { Key = "ECDHPolicyUri", Value = userTokenSecurityPolicyUri }); + new KeyValuePair { Key = AdditionalParameterNames.ECDHPolicyUri, Value = userTokenSecurityPolicyUri }); requestHeader.AdditionalHeader = new ExtensionObject(parameters); } @@ -4839,7 +4898,25 @@ protected virtual void ProcessResponseAdditionalHeader( { foreach (KeyValuePair ii in parameters.Parameters) { - if (ii.Key == "ECDHKey") + if (ii.Key == AdditionalParameterNames.Padding) + { + if (ii.Value.TypeInfo != TypeInfo.Scalars.ByteString || ii.Value.Value is not byte[]) + { + m_logger.LogWarning( + "Server returned invalid message padding. Ignored."); + } + + if (ii.Value.Value is byte[] padding && padding.Length > 4096) + { + m_logger.LogWarning( + "Server returned a {Size}byte message padding that is too long. Ignored.", + padding.Length); + } + + continue; + } + + if (ii.Key == AdditionalParameterNames.ECDHKey) { if (ii.Value.TypeInfo == TypeInfo.Scalars.StatusCode) { @@ -4856,7 +4933,7 @@ protected virtual void ProcessResponseAdditionalHeader( "Server did not provide a valid ECDHKey. User authentication not possible."); } - if (!EccUtils.Verify( + if (!CryptoUtils.Verify( new ArraySegment(key.PublicKey), key.Signature, serverCertificate, @@ -4868,7 +4945,7 @@ protected virtual void ProcessResponseAdditionalHeader( } m_eccServerEphemeralKey = Nonce.CreateNonce( - m_userTokenSecurityPolicyUri, + SecurityPolicies.GetInfo(m_userTokenSecurityPolicyUri), key.PublicKey); } } @@ -4951,6 +5028,7 @@ protected virtual void ProcessResponseAdditionalHeader( private readonly NodeCache m_nodeCache; private readonly List m_identityHistory = []; private byte[]? m_serverNonce; + private byte[]? m_clientNonce; private byte[]? m_previousServerNonce; private X509Certificate2? m_serverCertificate; private uint m_publishCounter; diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 6d1dbb31e0..2529912cd9 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -943,7 +943,7 @@ await DeleteApplicationInstanceCertificateAsync(configuration, id, ct).Configure else { ECCurve? curve = - EccUtils.GetCurveFromCertificateTypeId(id.CertificateType) + CryptoUtils.GetCurveFromCertificateTypeId(id.CertificateType) ?? throw new ServiceResultException( StatusCodes.BadConfigurationError, "The Ecc certificate type is not supported."); diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 8e38212fd7..f34fb19d47 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -980,7 +980,7 @@ private X509Certificate2 GenerateTemporaryApplicationCertificate( else { ECCurve? curve = - EccUtils.GetCurveFromCertificateTypeId(certificateTypeId) + CryptoUtils.GetCurveFromCertificateTypeId(certificateTypeId) ?? throw new ServiceResultException( StatusCodes.BadNotSupported, "The Ecc certificate type is not supported."); diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index f0c3b4cf08..e5428998c8 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -537,10 +537,19 @@ X509Certificate2Collection clientCertificateChain // sign the client nonce (if provided). if (parsedClientCertificate != null && clientNonce != null) { - byte[] dataToSign = Utils.Append(parsedClientCertificate.RawData, clientNonce); - serverSignature = SecurityPolicies.Sign( - instanceCertificate, + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(context.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetServerSignatureData( + context.ChannelContext.SecureChannelHash, + clientNonce, + context.ChannelContext.ServerChannelCertificate, + parsedClientCertificate.RawData, + context.ChannelContext.ClientChannelCertificate, + serverNonce); + + serverSignature = SecurityPolicies.CreateSignatureData( context.SecurityPolicyUri, + instanceCertificate, dataToSign); } } @@ -640,18 +649,18 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet foreach (KeyValuePair ii in parameters.Parameters) { - if (ii.Key == "ECDHPolicyUri") + if (ii.Key == AdditionalParameterNames.ECDHPolicyUri) { string policyUri = ii.Value.ToString(); - if (EccUtils.IsEccPolicy(policyUri)) + if (CryptoUtils.IsEccPolicy(policyUri)) { session.SetEccUserTokenSecurityPolicy(policyUri); EphemeralKeyType key = session.GetNewEccKey(); response.Parameters.Add( new KeyValuePair { - Key = "ECDHKey", + Key = AdditionalParameterNames.ECDHKey, Value = new ExtensionObject(key) }); } @@ -660,7 +669,7 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet response.Parameters.Add( new KeyValuePair { - Key = "ECDHKey", + Key = AdditionalParameterNames.ECDHKey, Value = StatusCodes.BadSecurityPolicyRejected }); } @@ -689,7 +698,7 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam { response = new AdditionalParametersType(); response.Parameters - .Add(new KeyValuePair { Key = "ECDHKey", Value = new ExtensionObject(key) }); + .Add(new KeyValuePair { Key = AdditionalParameterNames.ECDHKey, Value = new ExtensionObject(key) }); } return response; diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index 0f6f5c0182..4420a0d698 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -312,7 +312,7 @@ public virtual EphemeralKeyType GetNewEccKey() var key = new EphemeralKeyType { PublicKey = m_eccUserTokenNonce.Data }; - key.Signature = EccUtils.Sign( + key.Signature = CryptoUtils.Sign( new ArraySegment(key.PublicKey), m_serverCertificate, m_eccUserTokenSecurityPolicyUri); @@ -473,15 +473,21 @@ public void ValidateBeforeActivate( StatusCodes.BadApplicationSignatureInvalid); } - byte[] dataToSign = Utils.Append( + var securityPolicy = SecurityPolicies.GetInfo(EndpointDescription.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetClientSignatureData( + context.ChannelContext.SecureChannelHash, + m_serverNonce.Data, m_serverCertificate.RawData, - m_serverNonce.Data); + context.ChannelContext.ServerChannelCertificate, + context.ChannelContext.ClientChannelCertificate, + ClientNonce); - if (!SecurityPolicies.Verify( - ClientCertificate, + if (!SecurityPolicies.VerifySignatureData( + clientSignature, EndpointDescription.SecurityPolicyUri, - dataToSign, - clientSignature)) + ClientCertificate, + dataToSign)) { // verify for certificate chain in endpoint. // validate the signature with complete chain if the check with leaf certificate failed. @@ -502,15 +508,19 @@ public void ValidateBeforeActivate( byte[] serverCertificateChainData = [.. serverCertificateChainList]; - dataToSign = Utils.Append( + dataToSign = securityPolicy.GetClientSignatureData( + context.ChannelContext.SecureChannelHash, + m_serverNonce.Data, serverCertificateChainData, - m_serverNonce.Data); - - if (!SecurityPolicies.Verify( - ClientCertificate, - EndpointDescription.SecurityPolicyUri, - dataToSign, - clientSignature)) + context.ChannelContext.ServerChannelCertificate, + context.ChannelContext.ClientChannelCertificate, + ClientNonce); + + if (!SecurityPolicies.VerifySignatureData( + clientSignature, + EndpointDescription.SecurityPolicyUri, + ClientCertificate, + dataToSign)) { throw new ServiceResultException( StatusCodes.BadApplicationSignatureInvalid); @@ -543,6 +553,7 @@ public void ValidateBeforeActivate( // validate the user identity token. identityToken = ValidateUserIdentityToken( + context, userIdentityToken, userTokenSignature, out userTokenPolicy); @@ -848,6 +859,7 @@ private ServiceResult OnUpdateSecurityDiagnostics( /// /// private UserIdentityToken ValidateUserIdentityToken( + OperationContext context, ExtensionObject identityToken, SignatureData userTokenSignature, out UserTokenPolicy policy) @@ -1040,9 +1052,16 @@ private UserIdentityToken ValidateUserIdentityToken( // verify the signature. if (securityPolicyUri != SecurityPolicies.None) { - byte[] dataToSign = Utils.Append( + var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); + + byte[] dataToSign = securityPolicy.GetUserTokenSignatureData( + context.ChannelContext.SecureChannelHash, + m_serverNonce.Data, m_serverCertificate.RawData, - m_serverNonce.Data); + context.ChannelContext.ServerChannelCertificate, + ClientCertificate?.RawData, + context.ChannelContext.ClientChannelCertificate, + ClientNonce ?? []); if (!token.Verify(dataToSign, userTokenSignature, securityPolicyUri, m_server.Telemetry)) { diff --git a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs index c66ec8217a..a471870e3d 100644 --- a/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs +++ b/Stack/Opc.Ua.Bindings.Https/Stack/Https/HttpsTransportListener.cs @@ -157,6 +157,8 @@ protected virtual void Dispose(bool disposing) /// public string ListenerId { get; private set; } + internal byte[] ServerChannelCertificate { get; set; } + /// /// Opens the listener and starts accepting connection. /// @@ -287,6 +289,8 @@ private void ConfigureWebHost(IWebHostBuilder webHostBuilder) m_logger.LogTrace("Copy of the private key for https was denied: {Message}", ce.Message); } #endif + // save the server certificate so it can be used in the secure channel context. + ServerChannelCertificate = serverCertificate.RawData; var httpsOptions = new HttpsConnectionAdapterOptions { @@ -469,10 +473,13 @@ await WriteServiceResponseAsync(context, serviceResponse, ct) return; } } + var secureChannelContext = new SecureChannelContext( - ListenerId, - endpoint, - RequestEncoding.Binary); + ListenerId, + endpoint, + RequestEncoding.Binary, + context.Connection.ClientCertificate?.RawData, + ServerChannelCertificate); IServiceResponse output = await m_callback.ProcessRequestAsync( diff --git a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs index 07a06bd575..9125c18515 100644 --- a/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs +++ b/Stack/Opc.Ua.Core/Schema/SecuredApplicationHelpers.cs @@ -357,8 +357,17 @@ public static byte CalculateSecurityLevel( result = 2; break; case SecurityPolicies.Basic256: - logger.LogWarning( - "Deprecated Security Policy Basic256 requested - Not rcommended."); + logger.LogWarning("Deprecated Security Policy Basic256 requested - Not recommended."); + result = 4; + break; + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP384: + logger.LogWarning("Deprecated Security Policy {PolicyUri} requested - Use ECC_nistP[256/384]_AES.", policyUri); + result = 4; + break; + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP384r1: + logger.LogWarning("Deprecated Security Policy {PolicyUri} requested - Use ECC_brainpoolP[256/384]r1_AES.", policyUri); result = 4; break; case SecurityPolicies.Basic256Sha256: @@ -370,16 +379,16 @@ public static byte CalculateSecurityLevel( case SecurityPolicies.Aes256_Sha256_RsaPss: result = 10; break; - case SecurityPolicies.ECC_brainpoolP256r1: - result = 11; - break; - case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_brainpoolP256r1_AES: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: + case SecurityPolicies.ECC_nistP256_AES: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: result = 12; break; - case SecurityPolicies.ECC_brainpoolP384r1: - result = 13; - break; - case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AES: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: + case SecurityPolicies.ECC_brainpoolP384r1_AES: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: result = 14; break; case SecurityPolicies.None: diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 7f0494e4e3..ce2f2d3193 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -691,7 +691,7 @@ public static NodeId GetCertificateType(X509Certificate2 certificate) case Oids.ECDsaWithSha384: case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: - return EccUtils.GetEccCertificateTypeId(certificate); + return CryptoUtils.GetEccCertificateTypeId(certificate); case Oids.RsaPkcs1Sha256: case Oids.RsaPkcs1Sha384: case Oids.RsaPkcs1Sha512: @@ -722,7 +722,7 @@ public static bool ValidateCertificateType( case Oids.ECDsaWithSha384: case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: - NodeId certType = EccUtils.GetEccCertificateTypeId(certificate); + NodeId certType = CryptoUtils.GetEccCertificateTypeId(certificate); if (certType.IsNullNodeId) { return false; @@ -778,32 +778,45 @@ public static IList MapSecurityPolicyToCertificateTypes(string securityP case SecurityPolicies.Basic256Sha256: case SecurityPolicies.Aes128_Sha256_RsaOaep: case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AES_GCM: + case SecurityPolicies.RSA_DH_ChaChaPoly: result.Add(ObjectTypeIds.RsaSha256ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP256_AES: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: result.Add(ObjectTypeIds.EccNistP256ApplicationCertificateType); goto case SecurityPolicies.ECC_nistP384; case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AES: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: result.Add(ObjectTypeIds.EccNistP384ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP256r1_AES: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: result.Add(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); goto case SecurityPolicies.ECC_brainpoolP384r1; case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_brainpoolP384r1_AES: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: result.Add(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve25519_AES: + case SecurityPolicies.ECC_curve25519_ChaChaPoly: result.Add(ObjectTypeIds.EccCurve25519ApplicationCertificateType); - goto default; + break; case SecurityPolicies.ECC_curve448: + case SecurityPolicies.ECC_curve448_AES: + case SecurityPolicies.ECC_curve448_ChaChaPoly: result.Add(ObjectTypeIds.EccCurve448ApplicationCertificateType); - goto default; + break; case SecurityPolicies.Https: result.Add(ObjectTypeIds.HttpsCertificateType); - goto default; - default: - return result; + break; } + return result; } /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs similarity index 83% rename from Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs rename to Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs index 93d7b07e29..262db18009 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/EccUtils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Opc.Ua.Bindings; using Opc.Ua.Security.Certificates; #if CURVE25519 using Org.BouncyCastle.Pkcs; @@ -32,7 +33,7 @@ namespace Opc.Ua /// /// Defines functions to implement ECC cryptography. /// - public static class EccUtils + public static class CryptoUtils { /// /// The name of the NIST P-256 curve. @@ -64,9 +65,11 @@ public static class EccUtils /// public static bool IsEccPolicy(string securityPolicyUri) { - if (securityPolicyUri != null) + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + if (info != null) { - return securityPolicyUri.Contains("#ECC_", StringComparison.Ordinal); + return info.CertificateKeyFamily == CertificateKeyFamily.ECC; } return false; @@ -332,8 +335,8 @@ public static byte[] Sign( X509Certificate2 signingCertificate, string securityPolicyUri) { - HashAlgorithmName algorithm = GetSignatureAlgorithmName(securityPolicyUri); - return Sign(dataToSign, signingCertificate, algorithm); + var info = SecurityPolicies.GetInfo(securityPolicyUri); + return Sign(dataToSign, signingCertificate, info.AsymmetricSignatureAlgorithm); } /// @@ -343,53 +346,23 @@ public static byte[] Sign( public static byte[] Sign( ArraySegment dataToSign, X509Certificate2 signingCertificate, - HashAlgorithmName algorithm) + AsymmetricSignatureAlgorithm algorithm) { -#if CURVE25519 - var publicKey = signingCertificate.BcCertificate.GetPublicKey(); + // get the algorithm used for the signature. + HashAlgorithmName hashAlgorithm; - if (publicKey is Ed25519PublicKeyParameters) + switch (algorithm) { - var signer = new Ed25519Signer(); - - signer.Init(true, signingCertificate.BcPrivateKey); - signer.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - byte[] signature = signer.GenerateSignature(); -#if DEBUG - var verifier = new Ed25519Signer(); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - - if (!verifier.VerifySignature(signature)) - { - throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Could not verify signature."); - } -#endif - return signature; + case AsymmetricSignatureAlgorithm.EcdsaSha384: + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case AsymmetricSignatureAlgorithm.EcdsaSha256: + hashAlgorithm = HashAlgorithmName.SHA256; + break; + default: + throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}"); } - if (publicKey is Ed448PublicKeyParameters) - { - var signer = new Ed448Signer(new byte[32]); - - signer.Init(true, signingCertificate.BcPrivateKey); - signer.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - byte[] signature = signer.GenerateSignature(); -#if DEBUG - var verifier = new Ed448Signer(new byte[32]); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToSign.Array, dataToSign.Offset, dataToSign.Count); - - if (!verifier.VerifySignature(signature)) - { - throw new ServiceResultException(StatusCodes.BadSecurityChecksFailed, "Could not verify signature."); - } -#endif - return signature; - } -#endif ECDsa senderPrivateKey = signingCertificate.GetECDsaPrivateKey() ?? throw new ServiceResultException( @@ -402,19 +375,7 @@ public static byte[] Sign( dataToSign.Array, dataToSign.Offset, dataToSign.Count, - algorithm); - -#if DEBUGxxx - using (ECDsa ecdsa = EccUtils.GetPublicKey(new X509Certificate2(signingCertificate.RawData))) - { - if (!ecdsa.VerifyData(dataToSign.Array, dataToSign.Offset, dataToSign.Count, signature, algorithm)) - { - throw new ServiceResultException( - StatusCodes.BadSecurityChecksFailed, - "Could not verify signature."); - } - } -#endif + hashAlgorithm); return signature; } @@ -429,11 +390,20 @@ public static bool Verify( X509Certificate2 signingCertificate, string securityPolicyUri) { + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + if (info == null) + { + throw new ServiceResultException( + StatusCodes.BadSecurityChecksFailed, + $"Unknown security policy: {securityPolicyUri}"); + } + return Verify( dataToVerify, signature, signingCertificate, - GetSignatureAlgorithmName(securityPolicyUri)); + info.AsymmetricSignatureAlgorithm); } /// @@ -443,48 +413,31 @@ public static bool Verify( ArraySegment dataToVerify, byte[] signature, X509Certificate2 signingCertificate, - HashAlgorithmName algorithm) + AsymmetricSignatureAlgorithm algorithm) { -#if CURVE25519 - var publicKey = signingCertificate.BcCertificate.GetPublicKey(); + // get the algorithm used for the signature. + HashAlgorithmName hashAlgorithm; - if (publicKey is Ed25519PublicKeyParameters) + switch (algorithm) { - var verifier = new Ed25519Signer(); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count); - - if (!verifier.VerifySignature(signature)) - { - return false; - } - - return true; + case AsymmetricSignatureAlgorithm.EcdsaSha384: + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case AsymmetricSignatureAlgorithm.EcdsaSha256: + hashAlgorithm = HashAlgorithmName.SHA256; + break; + default: + throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}."); } - if (publicKey is Ed448PublicKeyParameters) - { - var verifier = new Ed448Signer(new byte[32]); - - verifier.Init(false, signingCertificate.BcCertificate.GetPublicKey()); - verifier.BlockUpdate(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count); - - if (!verifier.VerifySignature(signature)) - { - return false; - } - - return true; - } -#endif using ECDsa ecdsa = GetPublicKey(signingCertificate); + return ecdsa.VerifyData( dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count, signature, - algorithm); + hashAlgorithm); } /// @@ -561,21 +514,15 @@ private static ArraySegment RemovePadding(ArraySegment data, int blo /// /// Encrypts the buffer using the algorithm specified by the security policy. /// - /// 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[] signingKey = null, - bool signOnly = false) + bool signOnly = false, + uint tokenId = 0, + uint lastSequenceNumber = 0) { SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; @@ -587,7 +534,7 @@ public static ArraySegment SymmetricEncryptAndSign( if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) { #if NET8_0_OR_GREATER - return EncryptWithAesGcm(encryptingKey, iv, signOnly, data); + return EncryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); #else throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); #endif @@ -601,7 +548,8 @@ public static ArraySegment SymmetricEncryptAndSign( encryptingKey, iv, signOnly, - true); + tokenId, + lastSequenceNumber); #else throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); #endif @@ -632,6 +580,7 @@ public static ArraySegment SymmetricEncryptAndSign( if (!signOnly) { +#pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; @@ -640,6 +589,7 @@ public static ArraySegment SymmetricEncryptAndSign( aes.IV = iv; using ICryptoTransform encryptor = aes.CreateEncryptor(); +#pragma warning restore CA5401 encryptor.TransformBlock( data.Array, @@ -653,6 +603,23 @@ public static ArraySegment SymmetricEncryptAndSign( } #if NET8_0_OR_GREATER + private static byte[] ApplyAeadMask(uint tokenId, uint lastSequenceNumber, byte[] iv) + { + var copy = new byte[iv.Length]; + Buffer.BlockCopy(iv, 0, copy, 0, iv.Length); + + copy[0] ^= (byte)((tokenId & 0x000000FF)); + copy[1] ^= (byte)((tokenId & 0x0000FF00) >> 8); + copy[2] ^= (byte)((tokenId & 0x00FF0000) >> 16); + copy[3] ^= (byte)((tokenId & 0xFF000000) >> 24); + copy[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); + copy[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); + copy[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); + copy[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + + return copy; + } + private const int kChaChaPolyIvLength = 12; private const int kChaChaPolyTagLength = 16; @@ -661,7 +628,8 @@ private static ArraySegment EncryptWithChaCha20Poly1305( byte[] encryptingKey, byte[] iv, bool signOnly, - bool noPadding) + uint tokenId, + uint lastSequenceNumber) { if (encryptingKey == null || encryptingKey.Length != 32) { @@ -673,11 +641,6 @@ private static ArraySegment EncryptWithChaCha20Poly1305( throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); } - if (!noPadding && !signOnly) - { - data = AddPadding(data, iv.Length); - } - byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; byte[] tag = new byte[kChaChaPolyTagLength]; // ChaCha20-Poly1305/AES-GCM uses 128-bit authentication tag @@ -688,6 +651,8 @@ private static ArraySegment EncryptWithChaCha20Poly1305( using var chacha = new ChaCha20Poly1305(encryptingKey); + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + chacha.Encrypt( iv, signOnly ? Array.Empty() : data, @@ -695,6 +660,13 @@ private static ArraySegment EncryptWithChaCha20Poly1305( tag, extraData); +#if xDEBUG + Console.WriteLine($"E.iv={TcpMessageType.KeyToString(iv)}"); + Console.WriteLine($"E.extraData={TcpMessageType.KeyToString(extraData.ToArray())}"); + Console.WriteLine($"E.tag={TcpMessageType.KeyToString(tag)}"); + Console.WriteLine($"E.ciphertext={TcpMessageType.KeyToString(ciphertext)}"); +#endif + // Return layout: [associated data | ciphertext | tag] if (!signOnly) { @@ -716,7 +688,8 @@ private static ArraySegment DecryptWithChaCha20Poly1305( byte[] encryptingKey, byte[] iv, bool signOnly, - bool noPadding) + uint tokenId, + uint lastSequenceNumber) { if (encryptingKey == null || encryptingKey.Length != 32) { @@ -752,6 +725,15 @@ private static ArraySegment DecryptWithChaCha20Poly1305( using var chacha = new ChaCha20Poly1305(encryptingKey); + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + +#if xDEBUG + Console.WriteLine($"D.iv={TcpMessageType.KeyToString(iv)}"); + Console.WriteLine($"D.extraData={TcpMessageType.KeyToString(extraData.ToArray())}"); + Console.WriteLine($"D.tag={TcpMessageType.KeyToString(tag.ToArray())}"); + Console.WriteLine($"D.ciphertext={TcpMessageType.KeyToString(encryptedData.ToArray())}"); +#endif + chacha.Decrypt( iv, encryptedData, @@ -765,11 +747,6 @@ private static ArraySegment DecryptWithChaCha20Poly1305( Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count); } - if (!noPadding && !signOnly) - { - return RemovePadding(new ArraySegment(data.Array, data.Offset, data.Count - kChaChaPolyTagLength), iv.Length); - } - return new ArraySegment(data.Array, 0, data.Offset + data.Count - kChaChaPolyTagLength); } #endif @@ -779,10 +756,12 @@ private static ArraySegment DecryptWithChaCha20Poly1305( private const int kAesGcmTagLength = 16; private static ArraySegment EncryptWithAesGcm( + ArraySegment data, byte[] encryptingKey, byte[] iv, bool signOnly, - ArraySegment data) + uint tokenId, + uint lastSequenceNumber) { if (encryptingKey == null) { @@ -794,11 +773,6 @@ private static ArraySegment EncryptWithAesGcm( throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); } - if (!signOnly) - { - data = AddPadding(data, iv.Length); - } - byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; byte[] tag = new byte[kAesGcmTagLength]; // AES-GCM uses 128-bit authentication tag @@ -809,6 +783,9 @@ private static ArraySegment EncryptWithAesGcm( using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); + //Console.WriteLine($"Prior={TcpMessageType.KeyToString(iv)} tokenId={tokenId} lastSequenceNumber={lastSequenceNumber}"); + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + aesGcm.Encrypt( iv, signOnly ? Array.Empty() : data, @@ -816,6 +793,13 @@ private static ArraySegment EncryptWithAesGcm( tag, extraData); +#if xDEBUG + Console.WriteLine($"E.iv={TcpMessageType.KeyToString(iv)}"); + Console.WriteLine($"E.extraData={TcpMessageType.KeyToString(extraData.ToArray())}"); + Console.WriteLine($"E.tag={TcpMessageType.KeyToString(tag)}"); + Console.WriteLine($"E.ciphertext={TcpMessageType.KeyToString(ciphertext)}"); +#endif + // Return layout: [associated data | ciphertext | tag] if (!signOnly) { @@ -836,7 +820,9 @@ private static ArraySegment DecryptWithAesGcm( ArraySegment data, byte[] encryptingKey, byte[] iv, - bool signOnly) + bool signOnly, + uint tokenId, + uint lastSequenceNumber) { if (encryptingKey == null) { @@ -872,6 +858,16 @@ private static ArraySegment DecryptWithAesGcm( using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); + //Console.WriteLine($"Prior={TcpMessageType.KeyToString(iv)} tokenId={tokenId} lastSequenceNumber={lastSequenceNumber}"); + iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); + +#if xDEBUG + Console.WriteLine($"D.iv={TcpMessageType.KeyToString(iv)}"); + Console.WriteLine($"D.extraData={TcpMessageType.KeyToString(extraData.ToArray())}"); + Console.WriteLine($"D.tag={TcpMessageType.KeyToString(tag.ToArray())}"); + Console.WriteLine($"D.ciphertext={TcpMessageType.KeyToString(encryptedData.ToArray())}"); +#endif + aesGcm.Decrypt( iv, encryptedData, @@ -885,27 +881,24 @@ private static ArraySegment DecryptWithAesGcm( Buffer.BlockCopy(plaintext, 0, data.Array, data.Offset, encryptedData.Count); } - if (!signOnly) - { - return RemovePadding(new ArraySegment(data.Array, data.Offset, data.Count - kAesGcmTagLength), iv.Length); - } - return new ArraySegment(data.Array, 0, data.Offset + data.Count - kAesGcmTagLength); } #endif - /// - /// Decrypts the buffer using the algorithm specified by the security policy. - /// - /// - /// + /// + /// 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) + bool signOnly = false, + uint tokenId = 0, + uint lastSequenceNumber = 0) { SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; @@ -917,7 +910,7 @@ public static ArraySegment SymmetricDecryptAndVerify( if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) { #if NET8_0_OR_GREATER - return DecryptWithAesGcm(data, encryptingKey, iv, signOnly); + return DecryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); #else throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); #endif @@ -931,7 +924,8 @@ public static ArraySegment SymmetricDecryptAndVerify( encryptingKey, iv, signOnly, - true); + tokenId, + lastSequenceNumber); #else throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); #endif diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs index 049c97af01..b7cc7f893d 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; #if CURVE25519 @@ -46,8 +47,13 @@ public EncryptedSecret( ReceiverNonce = receiverNonce; ReceiverCertificate = receiverCertificate; Validator = validator; - SecurityPolicyUri = securityPolicyUri; + SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); Context = context; + + if (SecurityPolicy == null) + { + throw new ArgumentException($"Cannot resolve SecurityPolicy '{securityPolicyUri}'.", nameof(securityPolicyUri)); + } } /// @@ -86,343 +92,62 @@ public EncryptedSecret( public CertificateValidator Validator { get; } /// - /// Gets or sets the security policy URI. + /// Gets or sets the security policy. /// - public string SecurityPolicyUri { get; private set; } + public SecurityPolicyInfo SecurityPolicy { 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"); + private static readonly byte[] s_secretLabel = 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, + SecurityPolicyInfo securityPolicy, + Nonce localNonce, + Nonce remoteNonce, 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; - } + int encryptingKeySize = securityPolicy.SymmetricEncryptionKeyLength; + int blockSize = securityPolicy.InitializationVectorLength; encryptingKey = new byte[encryptingKeySize]; iv = new byte[blockSize]; + byte[] secret = localNonce.GenerateSecret(remoteNonce, null); byte[] keyLength = BitConverter.GetBytes((ushort)(encryptingKeySize + blockSize)); - byte[] salt = Utils.Append(keyLength, s_label, senderNonce.Data, receiverNonce.Data); + + byte[] salt = (forDecryption) ? + Utils.Append(keyLength, s_secretLabel, remoteNonce.Data, localNonce.Data) : + Utils.Append(keyLength, s_secretLabel, localNonce.Data, remoteNonce.Data); + + System.Console.WriteLine( + $"LOCAL={Utils.ToHexString(localNonce.Data).Substring(0, 8)} " + + $"REMOTE={Utils.ToHexString(remoteNonce.Data).Substring(0, 8)} " + + $"SALT={Utils.ToHexString(salt).Substring(0, 8)} "); byte[] keyData; + if (forDecryption) { - keyData = receiverNonce.DeriveKey( - senderNonce, + keyData = remoteNonce.DeriveKey( + secret, salt, - algorithmName, + securityPolicy.KeyDerivationAlgorithm, encryptingKeySize + blockSize); } else { - keyData = senderNonce.DeriveKey( - receiverNonce, + keyData = localNonce.DeriveKey( + secret, salt, - algorithmName, + securityPolicy.KeyDerivationAlgorithm, encryptingKeySize + blockSize); } @@ -443,7 +168,7 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) byte[] message = null; int lengthPosition = 0; - int signatureLength = EccUtils.GetSignatureLength(SenderCertificate); + int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate); using (var encoder = new BinaryEncoder(Context)) { @@ -454,7 +179,7 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) lengthPosition = encoder.Position; encoder.WriteUInt32(null, 0); - encoder.WriteString(null, SecurityPolicyUri); + encoder.WriteString(null, SecurityPolicy.Uri); byte[] senderCertificate = null; @@ -498,24 +223,32 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) 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); + CreateKeysForEcc( + SecurityPolicy, + SenderNonce, + ReceiverNonce, + false, + out encryptingKey, + out iv); + + // reserves space for padding and tag that is added by SymmetricEncryptAndSign. + var dataToEncrypt = new byte[4096]; + using var stream = new MemoryStream(dataToEncrypt); + using var secretEncoder = new BinaryEncoder(stream, Context, false); + + secretEncoder.WriteByteString(null, nonce); + secretEncoder.WriteByteString(null, secret); + + var encryptedData = CryptoUtils.SymmetricEncryptAndSign( + new ArraySegment(dataToEncrypt, 0, secretEncoder.Position), + SecurityPolicy, + encryptingKey, + iv); // append encrypted secret. - for (int ii = 0; ii < encryptedData.Length; ii++) + for (int ii = encryptedData.Offset; ii < encryptedData.Offset + encryptedData.Count; ii++) { - encoder.WriteByte(null, encryptedData[ii]); + encoder.WriteByte(null, encryptedData.Array[ii]); } // save space for signature. @@ -534,27 +267,16 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) 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); + byte[] signature = CryptoUtils.Sign(dataToSign, SenderCertificate, SecurityPolicy.AsymmetricSignatureAlgorithm); + Buffer.BlockCopy( signature, 0, message, message.Length - signatureLength, signatureLength); + return message; } @@ -595,27 +317,13 @@ private ArraySegment VerifyHeaderForEcc( // get the start of data. int startOfData = decoder.Position + dataToDecrypt.Offset; - SecurityPolicyUri = decoder.ReadString(null); + SecurityPolicy = SecurityPolicies.GetInfo(decoder.ReadString(null)); - if (!EccUtils.IsEccPolicy(SecurityPolicyUri)) + if (SecurityPolicy.CertificateKeyFamily != CertificateKeyFamily.ECC) { 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); @@ -673,7 +381,7 @@ private ArraySegment VerifyHeaderForEcc( int startOfEncryption = decoder.Position; - SenderNonce = Nonce.CreateNonce(SecurityPolicyUri, senderPublicKey); + SenderNonce = Nonce.CreateNonce(SecurityPolicy, senderPublicKey); if (!Utils.IsEqual(receiverPublicKey, ReceiverNonce.Data)) { @@ -683,7 +391,7 @@ private ArraySegment VerifyHeaderForEcc( } // check the signature. - int signatureLength = EccUtils.GetSignatureLength(SenderCertificate); + int signatureLength = CryptoUtils.GetSignatureLength(SenderCertificate); if (signatureLength >= length) { @@ -703,7 +411,7 @@ private ArraySegment VerifyHeaderForEcc( 0, startOfData + (int)length - signatureLength); - if (!EccUtils.Verify(dataToSign, signature, SenderCertificate, signatureAlgorithm)) + if (!CryptoUtils.Verify(dataToSign, signature, SenderCertificate, SecurityPolicy.AsymmetricSignatureAlgorithm)) { throw new ServiceResultException( StatusCodes.BadSecurityChecksFailed, @@ -742,17 +450,19 @@ public byte[] Decrypt( telemetry); CreateKeysForEcc( - SecurityPolicyUri, - SenderNonce, + SecurityPolicy, ReceiverNonce, + SenderNonce, true, out byte[] encryptingKey, out byte[] iv); - ArraySegment plainText = DecryptSecret( - dataToDecrypt.Array, - dataToDecrypt.Offset, - dataToDecrypt.Count, + byte[] bytes = new byte[dataToDecrypt.Count]; + Buffer.BlockCopy(dataToDecrypt.Array, dataToDecrypt.Offset, bytes, 0, dataToDecrypt.Count); + + ArraySegment plainText = CryptoUtils.SymmetricDecryptAndVerify( + new ArraySegment(bytes), + SecurityPolicy, encryptingKey, iv); @@ -761,6 +471,7 @@ public byte[] Decrypt( plainText.Offset, plainText.Count, Context); + byte[] actualNonce = decoder.ReadByteString(null); if (expectedNonce != null && expectedNonce.Length > 0) @@ -781,4 +492,4 @@ public byte[] Decrypt( return decoder.ReadByteString(null); } } -} \ No newline at end of file +} diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs index 4cbbd64b57..0ce941cc9b 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -50,80 +50,69 @@ private Nonce() /// public byte[] Data { get; private set; } + internal byte[] GenerateSecret( + Nonce remoteNonce, + byte[] previousSecret) + { + byte[] ikm = null; + +#if NET8_0_OR_GREATER +#if xDEBUG + Span privateKey = stackalloc char[2048]; + + if (m_ecdh.TryExportECPrivateKeyPem(privateKey, out int charsWritten)) + { + Console.WriteLine($"Private Key PEM ({charsWritten} chars):"); + } +#endif + + ikm = m_ecdh.DeriveRawSecretAgreement(remoteNonce.m_ecdh.PublicKey); + + if (previousSecret != null) + { + for (int ii = 0; ii < ikm.Length && ii < previousSecret.Length; ii++) + { + ikm[ii] ^= previousSecret[ii]; + } + } +#endif + return ikm; + } + /// /// Derives a key from the remote nonce, using the specified salt, hash algorithm, and length. /// - /// The remote nonce to use in key derivation. + /// The secret to use in key derivation. /// The salt to use in key derivation. /// The hash algorithm to use in key derivation. /// The length of the derived key. /// The derived key. public byte[] DeriveKey( - Nonce remoteNonce, + byte[] secret, byte[] salt, - HashAlgorithmName algorithm, + KeyDerivationAlgorithm algorithm, int length) { -#if CURVE25519 - if (m_bcKeyPair != null) + if (m_ecdh != null) { - var localPublicKey = m_bcKeyPair.Public; - - if (localPublicKey is X25519PublicKeyParameters) + HMAC hmac = algorithm switch { - X25519Agreement agreement = new X25519Agreement(); - agreement.Init(m_bcKeyPair.Private); - - var key = new X25519PublicKeyParameters(remoteNonce.Data, 0); - byte[] secret = new byte[agreement.AgreementSize]; - agreement.CalculateAgreement(key, secret, 0); - - HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); - generator.Init(new HkdfParameters(secret, salt, salt)); - - byte[] output = new byte[length]; - generator.GenerateBytes(output, 0, output.Length); - return output; - } - - if (localPublicKey is X448PublicKeyParameters) - { - X448Agreement agreement = new X448Agreement(); - agreement.Init(m_bcKeyPair.Private); - - var key = new X448PublicKeyParameters(remoteNonce.Data, 0); - byte[] secret = new byte[agreement.AgreementSize]; - agreement.CalculateAgreement(key, secret, 0); - - HkdfBytesGenerator generator = new HkdfBytesGenerator(new Sha256Digest()); - generator.Init(new HkdfParameters(secret, salt, salt)); + KeyDerivationAlgorithm.HKDFSha256 => new HMACSHA256(salt), + KeyDerivationAlgorithm.HKDFSha384 => new HMACSHA384(salt), + _ => new HMACSHA256(salt) + }; - byte[] output = new byte[length]; - generator.GenerateBytes(output, 0, output.Length); - return output; - } + //byte[] secret2 = m_ecdh.DeriveKeyFromHmac( + // remoteNonce.m_ecdh.PublicKey, + // algorithm, + // salt, + // null, + // null); - throw new NotSupportedException(); - } -#endif - if (m_ecdh != null) - { - byte[] secret = m_ecdh.DeriveKeyFromHmac( - remoteNonce.m_ecdh.PublicKey, - algorithm, - salt, - null, - null); + //System.Console.WriteLine($"PRK2={Utils.ToHexString(secret2).Substring(0, 8)}"); byte[] output = new byte[length]; - HMAC hmac = algorithm.Name switch - { - "SHA256" => new HMACSHA256(secret), - "SHA384" => new HMACSHA384(secret), - _ => new HMACSHA256(secret) - }; - byte counter = 1; byte[] info = new byte[(hmac.HashSize / 8) + salt.Length + 1]; @@ -162,50 +151,45 @@ public byte[] DeriveKey( /// /// Creates a nonce for the specified security policy URI and nonce length. /// - /// The security policy URI. - /// A object containing the generated nonce. - /// is null. public static Nonce CreateNonce(string securityPolicyUri) { - if (securityPolicyUri == null) + var info = SecurityPolicies.GetInfo(securityPolicyUri); + return CreateNonce(info); + } + + /// + /// Creates a nonce for the specified security policy and nonce length. + /// + public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy) + { + if (securityPolicy == null) { - throw new ArgumentNullException(nameof(securityPolicyUri)); + throw new ArgumentNullException(nameof(securityPolicy)); } - switch (securityPolicyUri) + switch (securityPolicy.CertificateKeyAlgorithm) { - case SecurityPolicies.ECC_nistP256: + case CertificateKeyAlgorithm.NistP256: return CreateNonce(ECCurve.NamedCurves.nistP256); - case SecurityPolicies.ECC_nistP384: + case CertificateKeyAlgorithm.NistP384: return CreateNonce(ECCurve.NamedCurves.nistP384); - case SecurityPolicies.ECC_brainpoolP256r1: + case CertificateKeyAlgorithm.BrainpoolP256r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1); - case SecurityPolicies.ECC_brainpoolP384r1: + case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1); -#if CURVE25519 - case SecurityPolicies.ECC_curve25519: - return CreateNonceForCurve25519(); - case SecurityPolicies.ECC_curve448: - return CreateNonceForCurve448(); -#endif default: - uint rsaNonceLength = GetNonceLength(securityPolicyUri); - return new Nonce { Data = CreateRandomNonceData(rsaNonceLength) }; + return new Nonce { Data = CreateRandomNonceData(securityPolicy.SecureChannelNonceLength) }; } } /// /// Creates a new Nonce object for the specified security policy URI and nonce data. /// - /// The security policy URI. - /// The nonce data. - /// A new Nonce object. - /// is null. - public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) + public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy, byte[] nonceData) { - if (securityPolicyUri == null) + if (securityPolicy == null) { - throw new ArgumentNullException(nameof(securityPolicyUri)); + throw new ArgumentNullException(nameof(securityPolicy)); } if (nonceData == null) @@ -215,19 +199,19 @@ public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) var nonce = new Nonce { Data = nonceData }; - switch (securityPolicyUri) + switch (securityPolicy.CertificateKeyAlgorithm) { - case SecurityPolicies.ECC_nistP256: + case CertificateKeyAlgorithm.NistP256: return CreateNonce(ECCurve.NamedCurves.nistP256, nonceData); - case SecurityPolicies.ECC_nistP384: + case CertificateKeyAlgorithm.NistP384: return CreateNonce(ECCurve.NamedCurves.nistP384, nonceData); - case SecurityPolicies.ECC_brainpoolP256r1: + case CertificateKeyAlgorithm.BrainpoolP256r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP256r1, nonceData); - case SecurityPolicies.ECC_brainpoolP384r1: + case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1, nonceData); - case SecurityPolicies.ECC_curve25519: + case CertificateKeyAlgorithm.Curve25519: return CreateNonceForCurve25519(nonceData); - case SecurityPolicies.ECC_curve448: + case CertificateKeyAlgorithm.Curve448: return CreateNonceForCurve448(nonceData); default: return nonce; @@ -238,7 +222,7 @@ public static Nonce CreateNonce(string securityPolicyUri, byte[] nonceData) /// Generates a Nonce for cryptographic functions of a given length. /// /// The requested Nonce as a - public static byte[] CreateRandomNonceData(uint length) + public static byte[] CreateRandomNonceData(int length) { byte[] randomBytes = new byte[length]; s_rng.GetBytes(randomBytes); @@ -251,9 +235,9 @@ public static byte[] CreateRandomNonceData(uint length) public static bool ValidateNonce( byte[] nonce, MessageSecurityMode securityMode, - string securityPolicyUri) + SecurityPolicyInfo securityPolicy) { - return ValidateNonce(nonce, securityMode, GetNonceLength(securityPolicyUri)); + return ValidateNonce(nonce, securityMode, securityPolicy.SecureChannelNonceLength); } /// @@ -262,7 +246,7 @@ public static bool ValidateNonce( public static bool ValidateNonce( byte[] nonce, MessageSecurityMode securityMode, - uint minNonceLength) + int minNonceLength) { // no nonce needed for no security. if (securityMode == MessageSecurityMode.None) @@ -288,36 +272,6 @@ public static bool ValidateNonce( return false; } - /// - /// Returns the length of the symmetric encryption key for a security policy. - /// - public static uint GetNonceLength(string securityPolicyUri) - { - switch (securityPolicyUri) - { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_curve25519: - return 32; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - // Q.X + Q.Y = 32 + 32 = 64 - return 64; - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - // Q.X + Q.Y = 48 + 48 = 96 - return 96; - case SecurityPolicies.ECC_curve448: - // Q.X - return 56; - default: - // Minimum nonce length by default - return s_minNonceLength; - } - } - /// /// Compare Nonce for equality. /// diff --git a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs index 0b715fa2f8..c3459d09f0 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/X509Utils.cs @@ -606,7 +606,7 @@ public static bool IsECDsaSignature(X509Certificate2 cert) /// The certificate. public static string GetECDsaQualifier(X509Certificate2 certificate) { - return EccUtils.GetECDsaQualifier(certificate); + return CryptoUtils.GetECDsaQualifier(certificate); } /// diff --git a/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs b/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs new file mode 100644 index 0000000000..c939d8b92b --- /dev/null +++ b/Stack/Opc.Ua.Core/Security/Constants/AdditionalParameterNames.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Opc.Ua +{ + /// + /// The names of additional parameters used in security-related operations. + /// + public static class AdditionalParameterNames + { + /// + /// The algorith to use for the ephemeral key used to encrypt user identity tokens. + /// + public const string ECDHPolicyUri = "ECDHPolicyUri"; + + /// + /// An ephemeral key used to encrypt user identity tokens. + /// + public const string ECDHKey = "ECDHKey"; + + /// + /// Padding bytes added to randomize the length of messages. + /// + public const string Padding = "Padding"; + + /// + /// A token used to authenticate a transfer of a session to a new secure channel. + /// + public const string SessionTransferToken = "SessionTransferToken"; + } +} diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index a2920d6e80..7fac64d318 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -66,36 +66,106 @@ public static class SecurityPolicies /// public const string Aes256_Sha256_RsaPss = BaseUri + "Aes256_Sha256_RsaPss"; + /// + /// The URI for the RSA_DH_AES_GCM security policy. + /// + public const string RSA_DH_AES_GCM = BaseUri + "RSA_DH_AES_GCM"; + + /// + /// The URI for the RSA_DH_ChaChaPoly security policy. + /// + public const string RSA_DH_ChaChaPoly = BaseUri + "RSA_DH_ChaChaPoly"; + /// /// The URI for the ECC_nistP256 security policy. /// public const string ECC_nistP256 = BaseUri + "ECC_nistP256"; + /// + /// The URI for the ECC_nistP256 security policy with AES-GCM. + /// + public const string ECC_nistP256_AES = ECC_nistP256 + "_AES"; + + /// + /// The URI for the ECC_nistP256 security policy with ChaCha20Poly1305. + /// + public const string ECC_nistP256_ChaChaPoly = ECC_nistP256 + "_ChaChaPoly"; + /// /// The URI for the ECC_nistP384 security policy. /// public const string ECC_nistP384 = BaseUri + "ECC_nistP384"; + /// + /// The URI for the ECC_nistP384 security policy with AES-GCM. + /// + public const string ECC_nistP384_AES = ECC_nistP384 + "_AES"; + + /// + /// The URI for the ECC_nistP384 security policy with ChaCha20Poly1305. + /// + public const string ECC_nistP384_ChaChaPoly = ECC_nistP384 + "_ChaChaPoly"; + /// /// The URI for the ECC_brainpoolP256r1 security policy. /// public const string ECC_brainpoolP256r1 = BaseUri + "ECC_brainpoolP256r1"; + /// + /// The URI for the ECC_brainpoolP256r1 security policy with AES-GCM. + /// + public const string ECC_brainpoolP256r1_AES = ECC_brainpoolP256r1 + "_AES"; + + /// + /// The URI for the ECC_brainpoolP256r1 security policy with ChaCha20Poly1305. + /// + public const string ECC_brainpoolP256r1_ChaChaPoly = ECC_brainpoolP256r1 + "_ChaChaPoly"; + /// /// The URI for the ECC_brainpoolP384r1 security policy. /// public const string ECC_brainpoolP384r1 = BaseUri + "ECC_brainpoolP384r1"; + /// + /// The URI for the ECC_brainpoolP384r1 security policy with AES-GCM. + /// + public const string ECC_brainpoolP384r1_AES = ECC_brainpoolP384r1 + "_AES"; + + /// + /// The URI for the ECC_brainpoolP384r1 security policy with ChaCha20Poly1305. + /// + public const string ECC_brainpoolP384r1_ChaChaPoly = ECC_brainpoolP384r1 + "_ChaChaPoly"; + /// /// The URI for the ECC_curve25519 security policy. /// public const string ECC_curve25519 = BaseUri + "ECC_curve25519"; /// - /// The URI for the ECC_curve448 security policy. + /// The URI for the ECC_curve25519 security policy with AES-GCM. + /// + public const string ECC_curve25519_AES = ECC_curve25519 + "_AES"; + + /// + /// The URI for the ECC_curve25519 security policy with ChaCha20Poly1305. + /// + public const string ECC_curve25519_ChaChaPoly = ECC_curve25519 + "_ChaChaPoly"; + + /// + /// The URI for the ECC_curve448 deprecated security policy. /// public const string ECC_curve448 = BaseUri + "ECC_curve448"; + /// + /// The URI for the ECC_curve448 security policy with AES-GCM. + /// + public const string ECC_curve448_AES = ECC_curve448 + "_AES"; + + /// + /// The URI for the ECC_curve448 security policy with ChaCha20Poly1305. + /// + public const string ECC_curve448_ChaChaPoly = ECC_curve448 + "_ChaChaPoly"; + /// /// The URI for the Https security policy. /// @@ -114,7 +184,9 @@ private static bool IsPlatformSupportedName(string name) name.Equals(nameof(Basic256), StringComparison.Ordinal) || name.Equals(nameof(Basic128Rsa15), StringComparison.Ordinal) || name.Equals(nameof(Basic256Sha256), StringComparison.Ordinal) || - name.Equals(nameof(Aes128_Sha256_RsaOaep), StringComparison.Ordinal)) + name.Equals(nameof(Aes128_Sha256_RsaOaep), StringComparison.Ordinal) || + name.Equals(nameof(RSA_DH_AES_GCM), StringComparison.Ordinal) || + name.Equals(nameof(RSA_DH_ChaChaPoly), StringComparison.Ordinal)) { return true; } @@ -124,29 +196,41 @@ private static bool IsPlatformSupportedName(string name) { return true; } - if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP256_AES), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP256_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP256ApplicationCertificateType); } - if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP384_AES), StringComparison.Ordinal) || + name.Equals(nameof(ECC_nistP384_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP384ApplicationCertificateType); } - if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP256r1_AES), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP256r1_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); } - if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP384r1_AES), StringComparison.Ordinal) || + name.Equals(nameof(ECC_brainpoolP384r1_ChaChaPoly), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); } if (name.Equals(nameof(ECC_curve25519), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve448), StringComparison.Ordinal)) + name.Equals(nameof(ECC_curve25519_AES), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve25519_ChaChaPoly), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_AES), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_ChaChaPoly), StringComparison.Ordinal)) { #if CURVE25519 return true; @@ -161,6 +245,11 @@ private static bool IsPlatformSupportedName(string name) /// public static SecurityPolicyInfo GetInfo(string securityPolicyUri) { + if (String.IsNullOrEmpty(securityPolicyUri)) + { + return SecurityPolicyInfo.None; + } + // 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)) @@ -315,65 +404,65 @@ public static EncryptedData Encrypt( ReadOnlySpan plainText, ILogger logger) { - var encryptedData = new EncryptedData - { - Algorithm = null, - Data = plainText.IsEmpty ? null : plainText.ToArray() - }; + var encryptedData = new EncryptedData { Algorithm = null }; // check if nothing to do. - if (plainText.IsEmpty) + if (plainText.Length == 0 || String.IsNullOrEmpty(securityPolicyUri)) { + encryptedData.Data = plainText.ToArray(); return encryptedData; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - return encryptedData; + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); } - // encrypt data. - switch (securityPolicyUri) + // check if asymmetric encryption is possible. + if (info.AsymmetricEncryptionAlgorithm != AsymmetricEncryptionAlgorithm.None) { - case Basic256: - case Basic256Sha256: - case Aes128_Sha256_RsaOaep: - encryptedData.Algorithm = SecurityAlgorithms.RsaOaep; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.OaepSHA1, - logger); - break; - case Basic128Rsa15: - encryptedData.Algorithm = SecurityAlgorithms.Rsa15; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.Pkcs1, - logger); - break; - case Aes256_Sha256_RsaPss: - encryptedData.Algorithm = SecurityAlgorithms.RsaOaepSha256; - encryptedData.Data = RsaUtils.Encrypt( - plainText, - certificate, - RsaUtils.Padding.OaepSHA256, - logger); - break; - case ECC_nistP256: - case ECC_nistP384: - case ECC_brainpoolP256r1: - case ECC_brainpoolP384r1: - return encryptedData; - case None: - break; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + switch (info.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: + { + encryptedData.Algorithm = SecurityAlgorithms.RsaOaep; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.OaepSHA1, + logger); + break; + } + + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: + { + encryptedData.Algorithm = SecurityAlgorithms.Rsa15; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.Pkcs1, + logger); + break; + } + + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: + { + encryptedData.Algorithm = SecurityAlgorithms.RsaOaepSha256; + encryptedData.Data = RsaUtils.Encrypt( + plainText, + certificate, + RsaUtils.Padding.OaepSHA256, + logger); + break; + } + } } return encryptedData; @@ -401,56 +490,68 @@ public static byte[] Decrypt( return dataToDecrypt.Data; } - // decrypt data. - switch (securityPolicyUri) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - case Basic256: - case Basic256Sha256: - case Aes128_Sha256_RsaOaep: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaep) - { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.OaepSHA1, - logger); - } - break; - case Basic128Rsa15: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.Rsa15) + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); + } + + // check if asymmetric encryption is possible. + if (info.AsymmetricEncryptionAlgorithm != AsymmetricEncryptionAlgorithm.None) + { + switch (info.AsymmetricEncryptionAlgorithm) + { + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.Pkcs1, - logger); + if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaep) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.OaepSHA1, + logger); + } + break; } - break; - case Aes256_Sha256_RsaPss: - if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaepSha256) + + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: { - return RsaUtils.Decrypt( - new ArraySegment(dataToDecrypt.Data), - certificate, - RsaUtils.Padding.OaepSHA256, - logger); + if (dataToDecrypt.Algorithm == SecurityAlgorithms.Rsa15) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.Pkcs1, + logger); + } + break; } - break; - case ECC_nistP256: - case ECC_nistP384: - case ECC_brainpoolP256r1: - case ECC_brainpoolP384r1: - case None: - if (string.IsNullOrEmpty(dataToDecrypt.Algorithm)) + + default: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: { - return dataToDecrypt.Data; + if (dataToDecrypt.Algorithm == SecurityAlgorithms.RsaOaepSha256) + { + return RsaUtils.Decrypt( + new ArraySegment(dataToDecrypt.Data), + certificate, + RsaUtils.Padding.OaepSHA256, + logger); + } + break; } - break; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + } + } + + if (String.IsNullOrEmpty(dataToDecrypt.Algorithm)) + { + return dataToDecrypt.Data; } throw ServiceResultException.Create( @@ -460,74 +561,129 @@ public static byte[] Decrypt( } /// - /// Signs the data using the SecurityPolicyUri and returns the signature. + /// Creates a signature using the security enhancements if required by the SecurityPolicy. /// - /// - public static SignatureData Sign( - X509Certificate2 certificate, + public static SignatureData CreateSignatureData( string securityPolicyUri, - byte[] dataToSign) + X509Certificate2 signingCertificate, + byte[] secureChannelSecret, + byte[] remoteCertificate, + byte[] remoteChannelCertificate, + byte[] localChannelCertificate, + byte[] remoteNonce, + byte[] localNonce) { var signatureData = new SignatureData(); - // check if nothing to do. - if (dataToSign == null) + // nothing more to do if no encryption. + if (string.IsNullOrEmpty(securityPolicyUri)) { return signatureData; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) { - return signatureData; + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); } + System.Console.WriteLine( + $"CreateSignatureData\r\n" + + $"secureChannelSecret: {ToFragment(secureChannelSecret)}\r\n" + + $"remoteCertificate: {ToFragment(remoteCertificate)}\r\n" + + $"remoteChannelCertificate: {ToFragment(remoteChannelCertificate)}\r\n" + + $"localChannelCertificate: {ToFragment(localChannelCertificate)}\r\n" + + $"remoteNonce: {ToFragment(remoteNonce)}\r\n" + + $"localNonce: {ToFragment(localNonce)}" + ); + + // create the data to sign. + byte[] dataToSign = (info.SecureChannelEnhancements) + ? Utils.Append( + secureChannelSecret ?? Array.Empty(), + remoteCertificate ?? Array.Empty(), + remoteChannelCertificate ?? Array.Empty(), + localChannelCertificate ?? Array.Empty(), + remoteNonce ?? Array.Empty(), + localNonce ?? Array.Empty()) + : + Utils.Append( + remoteCertificate ?? Array.Empty(), + remoteNonce); + + return CreateSignatureData(info, signingCertificate, dataToSign); + } + + /// + /// Creates a signature on the data provided using the SecurityPolicy. + /// + public static SignatureData CreateSignatureData( + string securityPolicyUri, + X509Certificate2 localCertificate, + byte[] dataToSign) + { + var info = GetInfo(securityPolicyUri); + return CreateSignatureData(info, localCertificate, dataToSign); + } + + /// + /// Creates a signature on the data provided using the SecurityPolicy. + /// + public static SignatureData CreateSignatureData( + SecurityPolicyInfo securityPolicy, + X509Certificate2 localCertificate, + byte[] dataToSign) + { + var signatureData = new SignatureData(); + // sign data. - switch (securityPolicyUri) + switch (securityPolicy.AsymmetricSignatureAlgorithm) { - case Basic256: - case Basic128Rsa15: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: signatureData.Algorithm = SecurityAlgorithms.RsaSha1; signatureData.Signature = RsaUtils.Rsa_Sign( new ArraySegment(dataToSign), - certificate, + localCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); break; - case Aes128_Sha256_RsaOaep: - case Basic256Sha256: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: signatureData.Algorithm = SecurityAlgorithms.RsaSha256; signatureData.Signature = RsaUtils.Rsa_Sign( new ArraySegment(dataToSign), - certificate, + localCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); break; - case Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPssSha256: signatureData.Algorithm = SecurityAlgorithms.RsaPssSha256; signatureData.Signature = RsaUtils.Rsa_Sign( new ArraySegment(dataToSign), - certificate, + localCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); break; - case ECC_nistP256: - case ECC_brainpoolP256r1: + case AsymmetricSignatureAlgorithm.EcdsaSha256: signatureData.Algorithm = null; - signatureData.Signature = EccUtils.Sign( + signatureData.Signature = CryptoUtils.Sign( new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA256); + localCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); break; - case ECC_nistP384: - case ECC_brainpoolP384r1: + case AsymmetricSignatureAlgorithm.EcdsaSha384: signatureData.Algorithm = null; - signatureData.Signature = EccUtils.Sign( + signatureData.Signature = CryptoUtils.Sign( new ArraySegment(dataToSign), - certificate, - HashAlgorithmName.SHA384); + localCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); break; - case None: + case AsymmetricSignatureAlgorithm.None: signatureData.Algorithm = null; signatureData.Signature = null; break; @@ -535,110 +691,198 @@ public static SignatureData Sign( throw ServiceResultException.Create( StatusCodes.BadSecurityPolicyRejected, "Unsupported security policy: {0}", - securityPolicyUri); + securityPolicy.Uri); } return signatureData; } /// - /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// Creates a signature using the security enhancements if required by the SecurityPolicy. /// - /// - public static bool Verify( - X509Certificate2 certificate, + public static bool VerifySignatureData( + SignatureData signature, string securityPolicyUri, - byte[] dataToVerify, - SignatureData signature) + X509Certificate2 signingCertificate, + byte[] secureChannelSecret, + byte[] localCertificate, + byte[] localChannelCertificate, + byte[] remoteChannelCertificate, + byte[] localNonce, + byte[] remoteNonce) { - // check if nothing to do. - if (signature == null) + var signatureData = new SignatureData(); + + // nothing more to do if no encryption. + if (string.IsNullOrEmpty(securityPolicyUri)) { return true; } - // nothing more to do if no encryption. - if (string.IsNullOrEmpty(securityPolicyUri)) + // get the info object. + var info = GetInfo(securityPolicyUri); + + // unsupported policy. + if (info == null) + { + throw ServiceResultException.Create( + StatusCodes.BadSecurityPolicyRejected, + "Unsupported security policy: {0}", + securityPolicyUri); + } + + System.Console.WriteLine( + $"VerifySignatureData\r\n" + + $"secureChannelSecret: {ToFragment(secureChannelSecret)}\r\n" + + $"localCertificate: {ToFragment(localCertificate)}\r\n" + + $"localChannelCertificate: {ToFragment(localChannelCertificate)}\r\n" + + $"remoteChannelCertificate: {ToFragment(remoteChannelCertificate)}\r\n" + + $"localNonce: {ToFragment(localNonce)}\r\n" + + $"remoteNonce: {ToFragment(remoteNonce)}" + ); + + // create the data to sign. + byte[] dataToVerify = (info.SecureChannelEnhancements) + ? Utils.Append( + secureChannelSecret ?? Array.Empty(), + localCertificate ?? Array.Empty(), + localChannelCertificate ?? Array.Empty(), + remoteChannelCertificate ?? Array.Empty(), + localNonce ?? Array.Empty(), + remoteNonce ?? Array.Empty()) + : + Utils.Append( + localCertificate ?? Array.Empty(), + localNonce); + + return VerifySignatureData(signature, info, signingCertificate, dataToVerify); + } + + /// + /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// + public static bool VerifySignatureData( + SignatureData signature, + string securityPolicyUri, + X509Certificate2 signingCertificate, + byte[] dataToVerify) + { + var info = GetInfo(securityPolicyUri); + return VerifySignatureData(signature, info, signingCertificate, dataToVerify); + } + + /// + /// Verifies the signature using the SecurityPolicyUri and return true if valid. + /// + public static bool VerifySignatureData( + SignatureData signature, + SecurityPolicyInfo securityPolicy, + X509Certificate2 signingCertificate, + byte[] dataToVerify) + { + // check if nothing to do. + if (signature == null) { return true; } - // decrypt data. - switch (securityPolicyUri) + // sign data. + switch (securityPolicy.AsymmetricSignatureAlgorithm) { - case Basic256: - case Basic128Rsa15: + // always accept signatures if security is not used. + case AsymmetricSignatureAlgorithm.None: + return true; + + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + { if (signature.Algorithm == SecurityAlgorithms.RsaSha1) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Basic256/Basic128Rsa15: {0}\n" + - "Expected signature algorithm: {1}", - signature.Algorithm, - SecurityAlgorithms.RsaSha1); - case Aes128_Sha256_RsaOaep: - case Basic256Sha256: + break; + } + + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + { if (signature.Algorithm == SecurityAlgorithms.RsaSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Basic256Sha256/Aes128_Sha256_RsaOaep: {0}\n" + - "Expected signature algorithm: {1}", - signature.Algorithm, - SecurityAlgorithms.RsaSha256); - case Aes256_Sha256_RsaPss: + break; + } + + case AsymmetricSignatureAlgorithm.RsaPssSha256: + { if (signature.Algorithm == SecurityAlgorithms.RsaPssSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), signature.Signature, - certificate, + signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); } - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Unexpected signature algorithm for Aes256_Sha256_RsaPss: {0}\n" + - "Expected signature algorithm : {1}", - signature.Algorithm, - SecurityAlgorithms.RsaPssSha256); - case ECC_nistP256: - case ECC_brainpoolP256r1: - return EccUtils.Verify( - new ArraySegment(dataToVerify), - signature.Signature, - certificate, - HashAlgorithmName.SHA256); - case ECC_nistP384: - case ECC_brainpoolP384r1: - return EccUtils.Verify( - new ArraySegment(dataToVerify), - signature.Signature, - certificate, - HashAlgorithmName.SHA384); - // always accept signatures if security is not used. - case None: - return true; - default: - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - securityPolicyUri); + break; + } + + case AsymmetricSignatureAlgorithm.EcdsaSha256: + { + if (signature.Algorithm == null || signature.Algorithm == securityPolicy.Uri) + { + return CryptoUtils.Verify( + new ArraySegment(dataToVerify), + signature.Signature, + signingCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + } + + break; + } + + case AsymmetricSignatureAlgorithm.EcdsaSha384: + { + if (signature.Algorithm == null || signature.Algorithm == securityPolicy.Uri) + { + return CryptoUtils.Verify( + new ArraySegment(dataToVerify), + signature.Signature, + signingCertificate, + securityPolicy.AsymmetricSignatureAlgorithm); + } + + break; + } } + + throw ServiceResultException.Create( + StatusCodes.BadSecurityChecksFailed, + "Unexpected SignatureData algorithm: {0}", + signature.Algorithm); + } + + static string ToFragment(byte[] input) + { + if (input != null) + { + if (input.Length < 8) + { + return Utils.ToHexString(input); + } + + return Utils.ToHexString(input).Substring(0, 16); + } + + return "null"; } /// diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs index af75058c8f..0660d76119 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -11,6 +11,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; +using System.IO; using System.Security.Cryptography; namespace Opc.Ua @@ -97,6 +98,11 @@ public SecurityPolicyInfo(string uri, string name = null) /// public AsymmetricSignatureAlgorithm CertificateSignatureAlgorithm { get; private set; } + /// + /// Returns algorithm family used to create asymmetric key pairs used with Certificates. + /// + public CertificateKeyFamily CertificateKeyFamily { get; private set; } + /// /// The algorithm used to create asymmetric key pairs used with Certificates. /// @@ -133,10 +139,21 @@ public SecurityPolicyInfo(string uri, string name = null) /// public bool LegacySequenceNumbers { get; private set; } + /// + /// If TRUE, the enhancements to the SecureChannel are required for the SecurityPolicy. + /// • Channel-bound Signature calculations in CreateSession/ActivateSession; + /// • Session transfer tokens in ActivateSession; + /// • Chained symmetric key derivation when renewing SecureChannels. + /// • Allow padding when using Authenticated Encryption; + /// + public bool SecureChannelEnhancements { get; private set; } + /// /// Whether the padding is required with symmetric encryption. /// public bool NoSymmetricEncryptionPadding => + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.Aes256Gcm || + SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.Aes128Gcm || SymmetricEncryptionAlgorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305; /// @@ -151,11 +168,122 @@ public SecurityPolicyInfo(string uri, string name = null) public byte[] KeyDataLengthForEncryptedSecret => BitConverter.GetBytes(SymmetricEncryptionKeyLength + InitializationVectorLength); + /// + /// Returns the data to be signed by the server when creating a session. + /// + public byte[] GetUserTokenSignatureData( + byte[] secureChanneHash, + byte[] serverNonce, + byte[] serverCertificate, + byte[] serverChannelCertificate, + byte[] clientCertificate, + byte[] clientChannelCertificate, + byte[] clientNonce) + { + byte[] data = null; + + if (SecureChannelEnhancements) + { + data = Utils.Append( + secureChanneHash, + serverNonce, + serverCertificate, + serverChannelCertificate, + clientCertificate, + clientChannelCertificate, + clientNonce); + } + else + { + data = Utils.Append( + serverCertificate, + serverNonce); + } + + System.Console.WriteLine($"UserTokenSignatureData={Opc.Ua.Bindings.TcpMessageType.KeyToString(data)}"); + return data; + } + + /// + /// Returns the data to be signed by the server when creating a session. + /// + public byte[] GetServerSignatureData( + byte[] secureChanneHash, + byte[] clientNonce, + byte[] serverChannelCertificate, + byte[] clientCertificate, + byte[] clientChannelCertificate, + byte[] serverNonce) + { + byte[] data = null; + + if (SecureChannelEnhancements) + { + data = Utils.Append( + secureChanneHash, + clientNonce, + serverChannelCertificate, + clientChannelCertificate, + serverNonce); + + System.Console.WriteLine($"secureChanneHash={Opc.Ua.Bindings.TcpMessageType.KeyToString(secureChanneHash)}"); + System.Console.WriteLine($"clientNonce={Opc.Ua.Bindings.TcpMessageType.KeyToString(clientNonce)}"); + System.Console.WriteLine($"serverChannelCertificate={Opc.Ua.Bindings.TcpMessageType.KeyToString(serverChannelCertificate)}"); + System.Console.WriteLine($"clientChannelCertificate={Opc.Ua.Bindings.TcpMessageType.KeyToString(clientChannelCertificate)}"); + System.Console.WriteLine($"serverNonce={Opc.Ua.Bindings.TcpMessageType.KeyToString(serverNonce)}"); + + } + else + { + data = Utils.Append( + clientCertificate, + clientNonce); + } + + System.Console.WriteLine($"ServerSignatureData={Opc.Ua.Bindings.TcpMessageType.KeyToString(data)}"); + return data; + } + + /// + /// Returns the data to be signed by the client when creating a session. + /// + public byte[] GetClientSignatureData( + byte[] secureChannelHash, + byte[] serverNonce, + byte[] serverCertificate, + byte[] serverChannelCertificate, + byte[] clientChannelCertificate, + byte[] clientNonce) + { + byte[] data = null; + + if (SecureChannelEnhancements) + { + data = Utils.Append( + secureChannelHash, + serverNonce, + serverCertificate, + serverChannelCertificate, + clientChannelCertificate, + clientNonce); + } + else + { + data = Utils.Append( + serverCertificate, + serverNonce); + } + + System.Console.WriteLine($"ClientSignatureData={Opc.Ua.Bindings.TcpMessageType.KeyToString(data)}"); + return data; + } + /// /// Returns a HMAC based on the symmetric signature algorithm. /// public HMAC CreateSignatureHmac(byte[] signingKey) { +#pragma warning disable CA5350 // Do Not Use Weak Cryptographic Algorithms return SymmetricSignatureAlgorithm switch { SymmetricSignatureAlgorithm.HmacSha1 => new HMACSHA1(signingKey), @@ -163,6 +291,7 @@ public HMAC CreateSignatureHmac(byte[] signingKey) SymmetricSignatureAlgorithm.HmacSha384 => new HMACSHA384(signingKey), _ => null }; +#pragma warning restore CA5350 // Do Not Use Weak Cryptographic Algorithms } /// @@ -195,12 +324,14 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, + CertificateKeyFamily = CertificateKeyFamily.None, CertificateKeyAlgorithm = CertificateKeyAlgorithm.None, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, KeyDerivationAlgorithm = KeyDerivationAlgorithm.None, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.None, - SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.None + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.None, + SecureChannelEnhancements = false }; /// @@ -219,6 +350,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + CertificateKeyFamily = CertificateKeyFamily.RSA, CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, @@ -244,6 +376,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, + CertificateKeyFamily = CertificateKeyFamily.RSA, CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, @@ -268,6 +401,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, @@ -292,6 +426,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, @@ -314,6 +449,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha256, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPssSha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, @@ -326,9 +462,61 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() }; /// - /// ECC_curve25519 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// ECC curve25519 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// + public readonly static 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, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve25519_AES = new(SecurityPolicies.ECC_curve25519_AES) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 32, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// ECC curve25519 is a required minimum security policy. It uses ChaCha20Poly1305 for symmetric encryption. /// - public static readonly SecurityPolicyInfo ECC_curve25519 = new(SecurityPolicies.ECC_curve25519) + public readonly static SecurityPolicyInfo ECC_curve25519_ChaChaPoly = new(SecurityPolicies.ECC_curve25519_ChaChaPoly) { DerivedSignatureKeyLength = 0, SymmetricEncryptionKeyLength = 256 / 8, @@ -340,19 +528,73 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, + CertificateKeyFamily = CertificateKeyFamily.ECC, CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure25519, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve25519, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// ECC curve448 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// + public readonly static 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, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// ECC curve448 is a required minimum security policy. It uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_curve448_AES = new(SecurityPolicies.ECC_curve448_AES) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 456, + MaxAsymmetricKeyLength = 456, + SecureChannelNonceLength = 56, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, IsDeprecated = false }; /// - /// ECC_curve448 is a required minimum security policy. It uses ChaChaPoly and 256 bit encryption. + /// ECC Curve448 is a required minimum security policy. It uses ChaCha20Poly1305 for symmetric encryption. /// - public static readonly SecurityPolicyInfo ECC_curve448 = new(SecurityPolicies.ECC_curve448) + public readonly static SecurityPolicyInfo ECC_curve448_ChaChaPoly = new(SecurityPolicies.ECC_curve448_ChaChaPoly) { DerivedSignatureKeyLength = 0, SymmetricEncryptionKeyLength = 256 / 8, @@ -364,19 +606,21 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, + CertificateKeyFamily = CertificateKeyFamily.ECC, CertificateKeyAlgorithm = CertificateKeyAlgorithm.Curve448, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaPure448, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.Curve448, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, IsDeprecated = false }; /// - /// ECC nistP256 is a required minimum security policy. + /// The ECC nistP256 is a required minimum security policy. /// - public static readonly SecurityPolicyInfo ECC_nistP256 = new(SecurityPolicies.ECC_nistP256) + public readonly static SecurityPolicyInfo ECC_nistP256 = new(SecurityPolicies.ECC_nistP256) { DerivedSignatureKeyLength = 256 / 8, SymmetricEncryptionKeyLength = 128 / 8, @@ -388,19 +632,73 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC_nistP256_AES is an ECC nistP256 variant that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP256_AES = new(SecurityPolicies.ECC_nistP256_AES) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC_nistP256_AES is an ECC nistP256 variant that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP256_ChaChaPoly = new(SecurityPolicies.ECC_nistP256_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP256, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, IsDeprecated = false }; /// - /// ECC nistP384 is an optional high security policy. + /// The ECC nistP384 is an optional high security policy. /// - public static readonly SecurityPolicyInfo ECC_nistP384 = new(SecurityPolicies.ECC_nistP384) + public readonly static SecurityPolicyInfo ECC_nistP384 = new(SecurityPolicies.ECC_nistP384) { DerivedSignatureKeyLength = 384 / 8, SymmetricEncryptionKeyLength = 256 / 8, @@ -412,19 +710,73 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC nistP384 is an optional high security policy that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP384_AES = new(SecurityPolicies.ECC_nistP384_AES) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC nistP384 is an optional high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_nistP384_ChaChaPoly = new(SecurityPolicies.ECC_nistP384_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.NistP384, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, IsDeprecated = false }; /// - /// ECC brainpoolP256r1 is a required minimum security policy. + /// The ECC brainpoolP256r1 is a required minimum security policy. /// - public static readonly SecurityPolicyInfo ECC_brainpoolP256r1 = new(SecurityPolicies.ECC_brainpoolP256r1) + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1 = new(SecurityPolicies.ECC_brainpoolP256r1) { DerivedSignatureKeyLength = 256 / 8, SymmetricEncryptionKeyLength = 128 / 8, @@ -436,23 +788,77 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, CertificateKeyAlgorithm = CertificateKeyAlgorithm.NistP256, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC_brainpoolP256r1_AES is an ECC brainpoolP256 variant that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1_AES = new (SecurityPolicies.ECC_brainpoolP256r1_AES) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 128 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC_brainpoolP256_AES is an ECC brainpoolP256 variant that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP256r1_ChaChaPoly = new(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 256, + MaxAsymmetricKeyLength = 256, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP256r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, IsDeprecated = false }; /// - /// ECC brainpoolP384r1 is an optional high security policy. + /// The ECC brainpoolP384r1 is an optional high security policy. /// - public static readonly SecurityPolicyInfo ECC_brainpoolP384r1 = new(SecurityPolicies.ECC_brainpoolP384r1) + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1 = new(SecurityPolicies.ECC_brainpoolP384r1) { DerivedSignatureKeyLength = 384 / 8, SymmetricEncryptionKeyLength = 256 / 8, - InitializationVectorLength = 128 / 8, + InitializationVectorLength = 96 / 8, SymmetricSignatureLength = 384 / 8, MinAsymmetricKeyLength = 384, MaxAsymmetricKeyLength = 384, @@ -460,16 +866,143 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() LegacySequenceNumbers = false, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, + SecureChannelEnhancements = false, + IsDeprecated = false + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy that uses AES-GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1_AES = new(SecurityPolicies.ECC_brainpoolP384r1_AES) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes256Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The ECC brainpoolP384r1 is an optional high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo ECC_brainpoolP384r1_ChaChaPoly = new(SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 384, + MaxAsymmetricKeyLength = 384, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + CertificateKeyFamily = CertificateKeyFamily.ECC, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.EcdsaSha384, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.BrainpoolP384r1, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha384, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The RSA_DH_AES_GCM is an high security policy that uses AES GCM for symmetric encryption. + /// + public readonly static SecurityPolicyInfo RSA_DH_AES_GCM = new(SecurityPolicies.RSA_DH_AES_GCM) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 96, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPssSha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, + SecureChannelEnhancements = true, + IsDeprecated = false + }; + + /// + /// The RSA_DH_ChaChaPoly is an high security policy that uses ChaCha20Poly1305 for symmetric encryption. + /// + public readonly static SecurityPolicyInfo RSA_DH_ChaChaPoly = new(SecurityPolicies.RSA_DH_ChaChaPoly) + { + DerivedSignatureKeyLength = 0, + SymmetricEncryptionKeyLength = 256 / 8, + InitializationVectorLength = 96 / 8, + SymmetricSignatureLength = 128 / 8, + MinAsymmetricKeyLength = 2048, + MaxAsymmetricKeyLength = 4096, + SecureChannelNonceLength = 64, + LegacySequenceNumbers = false, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, + AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPssSha256, + CertificateKeyFamily = CertificateKeyFamily.RSA, + CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, + EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, + KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, + SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, + SecureChannelEnhancements = true, IsDeprecated = false }; } + /// + /// The algorithm family used to generate key pairs. + /// + public enum CertificateKeyFamily + { + /// + /// Does not apply. + /// + None, + + /// + /// The RSA algorithm. + /// + RSA, + + /// + /// Ellipic curve algorithms. + /// + ECC + } + /// /// The algorithm used to generate key pairs. /// @@ -655,9 +1188,14 @@ public enum SymmetricSignatureAlgorithm ChaCha20Poly1305, /// - /// AES GCM with 128 bit tag + /// AES GCM with 128 bit key /// - Aes128Gcm + Aes128Gcm, + + /// + /// AES GCM with 256 bit key + /// + Aes256Gcm } /// diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs index c3bc1d12a7..3d28cbe6a7 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientChannelManager.cs @@ -361,6 +361,18 @@ private sealed class ClientChannel : ITransportChannel /// public IServiceMessageContext MessageContext => m_channel.MessageContext; + /// + public byte[] SecureChannelHash => m_channel?.SecureChannelHash ?? []; + + /// + public byte[] SessionActivationSecret => m_channel?.SessionActivationSecret ?? []; + + /// + public byte[] ClientChannelCertificate => m_channel?.ClientChannelCertificate ?? []; + + /// + public byte[] ServerChannelCertificate => m_channel?.ServerChannelCertificate ?? []; + /// public int OperationTimeout { diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index aa8eb51d58..50edbb619d 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -89,12 +89,12 @@ public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurity else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } @@ -159,12 +159,12 @@ public UserTokenPolicy FindUserTokenPolicy( else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index 11cd9fd7a8..4d2770443d 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -118,17 +118,22 @@ public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); /// - public ChannelToken? CurrentToken => null; + public byte[] SecureChannelHash => []; + + /// + public byte[] SessionActivationSecret => []; + + /// + public byte[] ClientChannelCertificate { get; private set; } = []; + + /// + public byte[] ServerChannelCertificate { get; private set; } = []; /// public event ChannelTokenActivatedEventHandler OnTokenActivated { - add - { - } - remove - { - } + add {} + remove {} } /// @@ -395,6 +400,7 @@ private void CreateHttpClient() } #endif handler.ClientCertificates.Add(clientCertificate); + ClientChannelCertificate = clientCertificate.RawData; } Func< @@ -441,7 +447,7 @@ private void CreateHttpClient() } m_quotas.CertificateValidator?.ValidateAsync(validationChain, default).GetAwaiter().GetResult(); - + ServerChannelCertificate = cert.RawData; return true; } catch (Exception ex) @@ -469,6 +475,7 @@ private void CreateHttpClient() #pragma warning disable CA5400 // HttpClient is created without enabling CheckCertificateRevocationList m_client = new HttpClient(handler); #pragma warning restore CA5400 // HttpClient is created without enabling CheckCertificateRevocationList + } catch (Exception ex) { diff --git a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs index e55aff3a90..547dd89f32 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs @@ -26,14 +26,26 @@ public class SecureChannelContext /// The secure channel identifier. /// The endpoint description. /// The message encoding. + /// The unique hash for the secure channel calculated during channel creation. + /// A secret used to re-activate sessions on a new secure channel. + /// The client certificate used to establsih the secure channel. + /// The server certificate used to establsih the secure channel. public SecureChannelContext( string secureChannelId, EndpointDescription endpointDescription, - RequestEncoding messageEncoding) + RequestEncoding messageEncoding, + byte[] clientChannelCertificate, + byte[] serverChannelCertificate, + byte[] secureChannelHash = null, + byte[] sessionActivationSecret = null) { SecureChannelId = secureChannelId; EndpointDescription = endpointDescription; MessageEncoding = messageEncoding; + ClientChannelCertificate = clientChannelCertificate; + ServerChannelCertificate = serverChannelCertificate; + SecureChannelHash = secureChannelHash; + SessionActivationSecret = sessionActivationSecret; } /// @@ -54,6 +66,26 @@ public SecureChannelContext( /// The message encoding. public RequestEncoding MessageEncoding { get; } + /// + /// The unique hash for the secure channel calculated during channel creation. + /// + public byte[] SecureChannelHash { get; } + + /// + /// A secret used to re-activate sessions on a new secure channel. + /// + public byte[] SessionActivationSecret { get; } + + /// + /// The client certificate used to establsih the secure channel. + /// + public byte[] ClientChannelCertificate { get; } + + /// + /// The server certificate used to establsih the secure channel. + /// + public byte[] ServerChannelCertificate { get; } + /// /// The active secure channel for the thread. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs index e2e8cbf835..39d21c6234 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs @@ -101,6 +101,16 @@ public void Dispose() /// public SecurityPolicyInfo SecurityPolicy { get; set; } + /// + /// The secret used to compute the keys. + /// + internal byte[] Secret { get; set; } + + /// + /// The previous server nonce used to compute the keys. + /// + internal byte[] PreviousSecret { get; set; } + /// /// The nonce provided by the client. /// @@ -140,5 +150,10 @@ public void Dispose() /// The initialization vector by the server when encrypting a message. /// internal byte[] ServerInitializationVector { get; set; } + + /// + /// The secret used to re-activate sessions on a new secure channel. + /// + internal byte[] SessionActivationSecret { get; set; } } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs index a5cf6303b6..2d0f4f3ebb 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs @@ -10,6 +10,11 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +using System; +using System.Globalization; +using System.Text; +using Microsoft.Extensions.Logging; + namespace Opc.Ua.Bindings { /// @@ -141,6 +146,46 @@ public static bool IsValid(uint messageType) } } + + internal static string GetTypeAndSize(ArraySegment chunk) + { + StringBuilder sb = new StringBuilder(); + + for (int ii = 0; ii < 1; ii++) + { + uint size = BitConverter.ToUInt32(chunk.Array ?? [], 4); + sb.Append(Encoding.ASCII.GetString(chunk.Array ?? [], 0, 4)); + sb.Append("=>"); + sb.Append(BitConverter.ToUInt32(chunk.Array ?? [], 4)); + sb.Append((size != chunk.Count) ? " X " : " O "); + sb.Append(chunk.Count); + } + + return sb.ToString(); + } + + internal static string KeyToString(ArraySegment key) + { + byte[] bytes = new byte[key.Count]; + Buffer.BlockCopy(key.Array ?? [], key.Offset, bytes, 0, key.Count); + return KeyToString(bytes); + } + + internal static string KeyToString(byte[] key) + { + if (key == null || key.Length == 0) + { + return "0:---"; + } + + if (key.Length <= 16) + { + return key.Length.ToString(CultureInfo.InvariantCulture) + ":" + Utils.ToHexString(key); + } + + var text = Utils.ToHexString(key); + return $"{key.Length}:{text.Substring(0, 8)}...{text.Substring(text.Length-8, 8)}"; + } } /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index ca15fe824a..bb8185ffbf 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -13,6 +13,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -256,7 +257,7 @@ public override void Reconnect( State = TcpChannelState.Open; // send response. - SendOpenSecureChannelResponse(requestId, token, request); + SendOpenSecureChannelResponse(requestId, token, request, true); // send any queued responses. ResetQueuedResponses(OnChannelReconnected); @@ -286,6 +287,8 @@ protected override bool HandleIncomingMessage( { SetResponseRequired(true); + m_logger.LogWarning("IN:{Id}", TcpMessageType.GetTypeAndSize(messageChunk)); + try { // process a response. @@ -549,13 +552,26 @@ private bool ProcessOpenSecureChannelRequest( try { + m_oscRequestSignature = null; + byte[] signature; + messageBody = ReadAsymmetricMessage( messageChunk, ServerCertificate, out channelId, out clientCertificate, out requestId, - out sequenceNumber); + out sequenceNumber, + m_oscRequestSignature, + out signature); + + // don't keep signature if secure channel enhancements are not used. + m_oscRequestSignature = (SecurityPolicy.SecureChannelEnhancements) ? signature : null; + + Console.WriteLine($"OSC IN={TcpMessageType.KeyToString(messageBody)}"); + Console.WriteLine($"oscRequestSignature={TcpMessageType.KeyToString(m_oscRequestSignature)}"); + Console.WriteLine($"signatureHash={TcpMessageType.KeyToString(signature)}"); + Console.WriteLine($"State={State}"); // check for replay attacks. if (!VerifySequenceNumber(sequenceNumber, "ProcessOpenSecureChannelRequest")) @@ -664,6 +680,8 @@ or StatusCodes.BadCertificateIssuerRevocationUnknown token = CreateToken(); token.TokenId = GetNewTokenId(); token.ServerNonce = CreateNonce(ServerCertificate); + token.PreviousSecret = CurrentToken?.Secret; + // check the client nonce. token.ClientNonce = request.ClientNonce; if (!ValidateNonce(ClientCertificate, token.ClientNonce)) @@ -775,11 +793,11 @@ or StatusCodes.BadCertificateIssuerRevocationUnknown // send the response. if (requestType == SecurityTokenRequestType.Renew) { - SendOpenSecureChannelResponse(requestId, RenewedToken, request); + SendOpenSecureChannelResponse(requestId, RenewedToken, request, true); } else { - SendOpenSecureChannelResponse(requestId, CurrentToken, request); + SendOpenSecureChannelResponse(requestId, CurrentToken, request, false); } // notify reverse @@ -851,7 +869,8 @@ protected override void CompleteReverseHello(Exception e) private void SendOpenSecureChannelResponse( uint requestId, ChannelToken token, - OpenSecureChannelRequest request) + OpenSecureChannelRequest request, + bool renew) { m_logger.LogDebug("ChannelId {Id}: SendOpenSecureChannelResponse()", ChannelId); @@ -867,6 +886,7 @@ private void SendOpenSecureChannelResponse( response.ServerNonce = token.ServerNonce; byte[] buffer = BinaryEncoder.EncodeMessage(response, Quotas.MessageContext); + byte[] signature; BufferCollection chunksToSend = WriteAsymmetricMessage( TcpMessageType.Open, @@ -874,7 +894,14 @@ private void SendOpenSecureChannelResponse( ServerCertificate, ServerCertificateChain, ClientCertificate, - new ArraySegment(buffer, 0, buffer.Length)); + new ArraySegment(buffer, 0, buffer.Length), + m_oscRequestSignature, + out signature); + + if (!renew) + { + ComputeSecureChannelHash(signature); + } // write the message to the server. try @@ -1326,6 +1353,7 @@ private bool ValidateDiscoveryServiceCall( private readonly ILogger m_logger; private SortedDictionary m_queuedResponses; private ReverseConnectAsyncResult m_pendingReverseHello; + private byte[] m_oscRequestSignature; private static readonly string s_implementationString = ".NET Standard ServerChannel UA-TCP " + Utils.GetAssemblyBuildNumber(); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs index 844ab24ffe..8434a2e327 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpTransportListener.cs @@ -1012,7 +1012,11 @@ private async void OnRequestReceivedAsync( var context = new SecureChannelContext( channel.GlobalChannelId, channel.EndpointDescription, - RequestEncoding.Binary); + RequestEncoding.Binary, + channel.ClientCertificate?.RawData, + channel.ServerCertificate?.RawData, + channel.SecureChannelHash, + channel.SessionActivationSecret); IServiceResponse response = await m_callback.ProcessRequestAsync( context, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index b878de8d11..ec408fe0eb 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -51,7 +51,7 @@ protected set /// /// The certificate for the server. /// - protected X509Certificate2 ServerCertificate { get; private set; } + internal X509Certificate2 ServerCertificate { get; private set; } /// /// The server certificate chain. @@ -66,7 +66,20 @@ protected set /// /// The security policy used with the channel. /// - protected string SecurityPolicyUri { get; private set; } + protected string SecurityPolicyUri + { + get => SecurityPolicy.Uri; + + private set + { + SecurityPolicy = SecurityPolicies.GetInfo(value); + } + } + + /// + /// The security policy used with the channel. + /// + protected SecurityPolicyInfo SecurityPolicy { get; private set; } /// /// Whether the channel is restricted to discovery operations. @@ -76,7 +89,7 @@ protected set /// /// The certificate for the client. /// - protected X509Certificate2 ClientCertificate { get; set; } + internal X509Certificate2 ClientCertificate { get; set; } /// /// The client certificate chain. @@ -170,33 +183,16 @@ protected static void CompareCertificates( /// protected byte[] CreateNonce(X509Certificate2 certificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.CertificateKeyFamily) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - uint length = Nonce.GetNonceLength(SecurityPolicyUri); - - if (length > 0) - { - return Nonce.CreateRandomNonceData(length); - } - break; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - m_localNonce = Nonce.CreateNonce(SecurityPolicyUri); + case CertificateKeyFamily.RSA: + return Nonce.CreateRandomNonceData(SecurityPolicy.SecureChannelNonceLength); + case CertificateKeyFamily.ECC: + m_localNonce = Nonce.CreateNonce(SecurityPolicy); return m_localNonce.Data; default: return null; } - - return null; } /// @@ -211,18 +207,14 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) } // check the length. - if (nonce == null || nonce.Length != Nonce.GetNonceLength(SecurityPolicyUri)) + if (nonce == null || nonce.Length != SecurityPolicy.SecureChannelNonceLength) { return false; } - switch (SecurityPolicyUri) + switch (SecurityPolicy.CertificateKeyFamily) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: + case CertificateKeyFamily.RSA: // try to catch programming errors by rejecting nonces with all zeros. for (int ii = 0; ii < nonce.Length; ii++) { @@ -231,19 +223,13 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) return true; } } - - return false; - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - m_remoteNonce = Nonce.CreateNonce(SecurityPolicyUri, nonce); + break; + case CertificateKeyFamily.ECC: + m_remoteNonce = Nonce.CreateNonce(SecurityPolicy, nonce); return true; - default: - return false; } + + return false; } /// @@ -251,19 +237,17 @@ protected bool ValidateNonce(X509Certificate2 certificate, byte[] nonce) /// protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.OaepSHA1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.OaepSHA256); - case SecurityPolicies.Basic128Rsa15: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return RsaUtils.GetPlainTextBlockSize( receiverCertificate, RsaUtils.Padding.Pkcs1); @@ -277,13 +261,11 @@ protected int GetPlainTextBlockSize(X509Certificate2 receiverCertificate) /// protected int GetCipherTextBlockSize(X509Certificate2 receiverCertificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.AsymmetricEncryptionAlgorithm) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.Basic128Rsa15: + case AsymmetricEncryptionAlgorithm.RsaOaepSha1: + case AsymmetricEncryptionAlgorithm.RsaOaepSha256: + case AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1: return RsaUtils.GetCipherTextBlockSize(receiverCertificate); default: return 1; @@ -382,21 +364,17 @@ protected int GetAsymmetricHeaderSize( /// protected int GetAsymmetricSignatureSize(X509Certificate2 senderCertificate) { - switch (SecurityPolicyUri) + switch (SecurityPolicy.AsymmetricSignatureAlgorithm) { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: + case AsymmetricSignatureAlgorithm.RsaPssSha256: return RsaUtils.GetSignatureLength(senderCertificate); - 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 EccUtils.GetSignatureLength(senderCertificate); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + case AsymmetricSignatureAlgorithm.EcdsaPure25519: + case AsymmetricSignatureAlgorithm.EcdsaPure448: + return CryptoUtils.GetSignatureLength(senderCertificate); default: return 0; } @@ -534,13 +512,16 @@ protected BufferCollection WriteAsymmetricMessage( X509Certificate2 receiverCertificate, ArraySegment messageBody) { + byte[] unused = null; return WriteAsymmetricMessage( messageType, requestId, senderCertificate, null, receiverCertificate, - messageBody); + messageBody, + null, + out unused); } /// @@ -554,8 +535,12 @@ protected BufferCollection WriteAsymmetricMessage( X509Certificate2 senderCertificate, X509Certificate2Collection senderCertificateChain, X509Certificate2 receiverCertificate, - ArraySegment messageBody) + ArraySegment messageBody, + byte[] oscRequestSignature, + out byte[] signature) { + signature = null; + bool success = false; var chunksToSend = new BufferCollection(); @@ -704,10 +689,30 @@ protected BufferCollection WriteAsymmetricMessage( // put the message size after encryption into the header. UpdateMessageSize(buffer, 0, cipherTextSize + headerSize); + ArraySegment dataToSign; + + if (oscRequestSignature != null && SecurityPolicy.SecureChannelEnhancements) + { + // copy osc request signature if provided before verifying. + dataToSign = new ArraySegment( + buffer, + 0, + encoder.Position + oscRequestSignature.Length); + + Array.Copy( + oscRequestSignature, + 0, + buffer, + encoder.Position, + oscRequestSignature.Length); + } + else + { + dataToSign = new ArraySegment(buffer, 0, encoder.Position); + } + // write the signature. - byte[] signature = Sign( - new ArraySegment(buffer, 0, encoder.Position), - senderCertificate); + signature = Sign(dataToSign, senderCertificate); if (signature != null) { @@ -748,6 +753,10 @@ protected BufferCollection WriteAsymmetricMessage( // ensure the buffers don't get clean up on exit. success = true; + + Console.WriteLine($"OSC IN={TcpMessageType.KeyToString(messageBody)}"); + Console.WriteLine($"OSC OUT={TcpMessageType.KeyToString(chunksToSend[0])}"); + return chunksToSend; } catch (Exception ex) @@ -961,6 +970,25 @@ protected virtual bool SetEndpointUrl(string endpointUrl) return false; } + /// + /// Computes the SecureChannelHash. + /// + protected void ComputeSecureChannelHash(byte[] signature) + { + SecureChannelHash = null; + if (SecurityPolicy.SecureChannelEnhancements) + { + HashAlgorithm hash = SecurityPolicy.KeyDerivationAlgorithm switch + { + KeyDerivationAlgorithm.HKDFSha384 => SHA384.Create(), + _ => SHA256.Create() + }; + + Console.WriteLine($"signature={TcpMessageType.KeyToString(signature)}"); + SecureChannelHash = hash.ComputeHash(signature, 0, signature.Length); + } + } + /// /// Processes an OpenSecureChannel request message. /// @@ -971,8 +999,12 @@ protected ArraySegment ReadAsymmetricMessage( out uint channelId, out X509Certificate2 senderCertificate, out uint requestId, - out uint sequenceNumber) + out uint sequenceNumber, + byte[] oscRequestSignature, + out byte[] signature) { + signature = null; + int headerSize; using (var decoder = new BinaryDecoder(buffer, Quotas.MessageContext)) { @@ -1079,20 +1111,39 @@ protected ArraySegment ReadAsymmetricMessage( // extract signature. int signatureSize = GetAsymmetricSignatureSize(senderCertificate); - byte[] signature = new byte[signatureSize]; + signature = new byte[signatureSize]; for (int ii = 0; ii < signatureSize; ii++) { - signature[ii] = plainText.Array[ - plainText.Offset + plainText.Count - signatureSize + ii]; + signature[ii] = plainText.Array[plainText.Offset + plainText.Count - signatureSize + ii]; } - // verify the signature. - var dataToVerify = new ArraySegment( - plainText.Array, - plainText.Offset, - plainText.Count - signatureSize); + ArraySegment dataToVerify; + if (oscRequestSignature != null && SecurityPolicy.SecureChannelEnhancements) + { + // copy osc request signature if provided before verifying. + dataToVerify = new ArraySegment( + plainText.Array, + plainText.Offset, + plainText.Count - signatureSize + oscRequestSignature.Length); + + Array.Copy( + oscRequestSignature, + dataToVerify.Offset, + dataToVerify.Array, + dataToVerify.Count - oscRequestSignature.Length, + oscRequestSignature.Length); + } + else + { + dataToVerify = new ArraySegment( + plainText.Array, + plainText.Offset, + plainText.Count - signatureSize); + } + + // verify the signature. if (!Verify(dataToVerify, signature, senderCertificate)) { m_logger.LogWarning("Could not verify signature on message."); @@ -1177,38 +1228,35 @@ protected ArraySegment ReadAsymmetricMessage( /// protected byte[] Sign(ArraySegment dataToSign, X509Certificate2 senderCertificate) { - switch (SecurityPolicyUri) + var info = SecurityPolicies.GetInfo(SecurityPolicyUri); + + switch (info.AsymmetricSignatureAlgorithm) { - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic128Rsa15: + case AsymmetricSignatureAlgorithm.None: + return null; + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: return Rsa_Sign( dataToSign, senderCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: return Rsa_Sign( dataToSign, senderCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPssSha256: return Rsa_Sign( dataToSign, senderCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA256); - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return EccUtils.Sign(dataToSign, senderCertificate, HashAlgorithmName.SHA384); + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + return CryptoUtils.Sign(dataToSign, senderCertificate, info.AsymmetricSignatureAlgorithm); default: - return null; + throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); } } @@ -1225,50 +1273,40 @@ protected bool Verify( byte[] signature, X509Certificate2 senderCertificate) { - // verify signature. - switch (SecurityPolicyUri) + var info = SecurityPolicies.GetInfo(SecurityPolicyUri); + + switch (info.AsymmetricSignatureAlgorithm) { - case SecurityPolicies.None: + case AsymmetricSignatureAlgorithm.None: return true; - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: return Rsa_Verify( dataToVerify, signature, senderCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Basic256Sha256: + case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: return Rsa_Verify( dataToVerify, signature, senderCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - case SecurityPolicies.Aes256_Sha256_RsaPss: + case AsymmetricSignatureAlgorithm.RsaPssSha256: return Rsa_Verify( dataToVerify, signature, senderCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); - case SecurityPolicies.ECC_nistP256: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_curve25519: - case SecurityPolicies.ECC_curve448: - return EccUtils.Verify( + case AsymmetricSignatureAlgorithm.EcdsaSha256: + case AsymmetricSignatureAlgorithm.EcdsaSha384: + return CryptoUtils.Verify( dataToVerify, signature, senderCertificate, - HashAlgorithmName.SHA256); - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP384r1: - return EccUtils.Verify( - dataToVerify, - signature, - senderCertificate, - HashAlgorithmName.SHA384); + info.AsymmetricSignatureAlgorithm); default: return false; } @@ -1298,6 +1336,8 @@ protected ArraySegment Encrypt( receiverCertificate, RsaUtils.Padding.OaepSHA1); case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AES_GCM: + case SecurityPolicies.RSA_DH_ChaChaPoly: return Rsa_Encrypt( dataToEncrypt, headerToCopy, @@ -1309,6 +1349,25 @@ protected ArraySegment Encrypt( headerToCopy, receiverCertificate, RsaUtils.Padding.Pkcs1); + case SecurityPolicies.ECC_nistP256: + case SecurityPolicies.ECC_nistP256_AES: + case SecurityPolicies.ECC_nistP256_ChaChaPoly: + case SecurityPolicies.ECC_brainpoolP256r1: + case SecurityPolicies.ECC_brainpoolP256r1_AES: + case SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly: + case SecurityPolicies.ECC_curve25519: + case SecurityPolicies.ECC_curve25519_AES: + case SecurityPolicies.ECC_curve25519_ChaChaPoly: + case SecurityPolicies.ECC_curve448: + case SecurityPolicies.ECC_curve448_AES: + case SecurityPolicies.ECC_curve448_ChaChaPoly: + case SecurityPolicies.ECC_nistP384: + case SecurityPolicies.ECC_nistP384_AES: + case SecurityPolicies.ECC_nistP384_ChaChaPoly: + case SecurityPolicies.ECC_brainpoolP384r1: + case SecurityPolicies.ECC_brainpoolP384r1_AES: + case SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly: + goto default; default: byte[] encryptedBuffer = BufferManager.TakeBuffer(SendBufferSize, "Encrypt"); @@ -1355,6 +1414,8 @@ protected ArraySegment Decrypt( receiverCertificate, RsaUtils.Padding.OaepSHA1); case SecurityPolicies.Aes256_Sha256_RsaPss: + case SecurityPolicies.RSA_DH_AES_GCM: + case SecurityPolicies.RSA_DH_ChaChaPoly: return Rsa_Decrypt( dataToDecrypt, headerToCopy, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs index a03c2ba721..d4915b7286 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Rsa.cs @@ -42,12 +42,21 @@ private static byte[] Rsa_Sign( "No private key for certificate."); // create the signature. - return rsa.SignData( + var signature = rsa.SignData( dataToSign.Array, dataToSign.Offset, dataToSign.Count, algorithm, padding); + +#if xDEBUG + var data = new ReadOnlySpan(dataToSign.Array, dataToSign.Offset, dataToSign.Count).ToArray() + Console.WriteLine($"dataToSign={TcpMessageType.KeyToString(data)}"); + Console.WriteLine($"algorithm={algorithm} padding={padding}"); + Console.WriteLine($"signingCertificate={signingCertificate.Thumbprint}"); + Console.WriteLine($"signature={TcpMessageType.KeyToString(signature)}"); +#endif + return signature; } /// @@ -68,6 +77,13 @@ private bool Rsa_Verify( StatusCodes.BadSecurityChecksFailed, "No public key for certificate."); +#if xDEBUG + var data = new ReadOnlySpan(dataToVerify.Array, dataToVerify.Offset, dataToVerify.Count).ToArray() + Console.WriteLine($"dataToVerify={TcpMessageType.KeyToString(data)}"); + Console.WriteLine($"algorithm={algorithm} padding={padding}"); + Console.WriteLine($"signingCertificate={signingCertificate.Thumbprint}"); + Console.WriteLine($"signature={TcpMessageType.KeyToString(signature)}"); +#endif // verify signature. if (!rsa.VerifyData( dataToVerify.Array, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 562272fbf7..dec6ec6358 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -135,13 +135,7 @@ protected void DiscardTokens() /// protected void CalculateSymmetricKeySizes() { - var securityPolicyUri = SecurityPolicyUri; - if (securityPolicyUri.StartsWith(SecurityPolicies.BaseUri, StringComparison.Ordinal)) - { - securityPolicyUri = securityPolicyUri.Substring(SecurityPolicies.BaseUri.Length); - } - - SecurityPolicyInfo info = SecurityPolicies.GetInfo(securityPolicyUri) + SecurityPolicyInfo info = SecurityPolicies.GetInfo(SecurityPolicyUri) ?? throw ServiceResultException.Create( StatusCodes.BadSecurityPolicyRejected, "Unsupported security policy: {0}", @@ -160,9 +154,15 @@ private void DeriveKeysWithPSHA( ChannelToken token, bool isServer) { + using HMAC hmac = Utils.CreateHMAC(algorithmName, secret); + int length = m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize; - using HMAC hmac = Utils.CreateHMAC(algorithmName, secret); + if (!isServer && SecurityPolicy.SecureChannelEnhancements) + { + length += hmac.HashSize/8; + } + byte[] output = Utils.PSHA(hmac, null, seed, 0, length); byte[] signingKey = new byte[m_signatureKeySize]; @@ -184,6 +184,19 @@ private void DeriveKeysWithPSHA( token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.SessionActivationSecret = null; + + if (SecurityPolicy.SecureChannelEnhancements) + { + token.SessionActivationSecret = new byte[hmac.HashSize / 8]; + + Buffer.BlockCopy( + output, + m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize, + token.SessionActivationSecret, + 0, + token.SessionActivationSecret.Length); + } } } @@ -197,12 +210,32 @@ private void DeriveKeysWithHKDF( token.SecurityPolicy.SymmetricEncryptionKeyLength + token.SecurityPolicy.InitializationVectorLength; + int secretLength = 0; + + if (!isServer && SecurityPolicy.SecureChannelEnhancements) + { + secretLength = token.SecurityPolicy.KeyDerivationAlgorithm switch + { + KeyDerivationAlgorithm.HKDFSha256 => 32, + KeyDerivationAlgorithm.HKDFSha384 => 48, + _ => 32 + }; + } + + length += secretLength; + byte[] prk = m_localNonce.DeriveKey( - m_remoteNonce, + m_remoteNonce.Data, salt, - token.SecurityPolicy.GetKeyDerivationHashAlgorithmName(), + token.SecurityPolicy.KeyDerivationAlgorithm, length); +#if xDEBUG + m_logger.LogWarning("LocalNonce={LocalNonce}", TcpMessageType.KeyToString(m_localNonce.Data)); + m_logger.LogWarning("RemoteNonce={RemoteNonce}", TcpMessageType.KeyToString(m_remoteNonce.Data)); + m_logger.LogWarning("PRK={PRK}", TcpMessageType.KeyToString(prk)); +#endif + byte[] signingKey = new byte[m_signatureKeySize]; byte[] encryptingKey = new byte[m_encryptionKeySize]; byte[] iv = new byte[EncryptionBlockSize]; @@ -222,6 +255,19 @@ private void DeriveKeysWithHKDF( token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.SessionActivationSecret = null; + + if (SecurityPolicy.SecureChannelEnhancements) + { + token.SessionActivationSecret = new byte[secretLength]; + + Buffer.BlockCopy( + prk, + m_signatureKeySize + m_encryptionKeySize + EncryptionBlockSize, + token.SessionActivationSecret, + 0, + token.SessionActivationSecret.Length); + } } } @@ -230,22 +276,7 @@ private void DeriveKeysWithHKDF( /// protected void ComputeKeys(ChannelToken token) { - // Strip BaseUri prefix to get short name for dictionary lookup - var securityPolicyUri = SecurityPolicyUri; - if (securityPolicyUri.StartsWith(SecurityPolicies.BaseUri, StringComparison.Ordinal)) - { - securityPolicyUri = securityPolicyUri.Substring(SecurityPolicies.BaseUri.Length); - } - - token.SecurityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); - - if (token.SecurityPolicy == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityPolicyRejected, - "Unsupported security policy: {0}", - SecurityPolicyUri); - } + token.SecurityPolicy = SecurityPolicies.GetInfo(SecurityPolicyUri); if (SecurityMode == MessageSecurityMode.None) { @@ -255,45 +286,53 @@ protected void ComputeKeys(ChannelToken token) byte[] serverSecret = token.ServerNonce; byte[] clientSecret = token.ClientNonce; - m_logger?.LogInformation( - "[ComputeKeys] KeyDerivationAlgorithm: {Algo}", - token.SecurityPolicy.KeyDerivationAlgorithm); - - if (token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha1 || - token.SecurityPolicy.KeyDerivationAlgorithm == KeyDerivationAlgorithm.PSha256) + switch (token.SecurityPolicy.KeyDerivationAlgorithm) { - 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("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)); + case KeyDerivationAlgorithm.HKDFSha256: + case KeyDerivationAlgorithm.HKDFSha384: + { + token.Secret = m_localNonce.GenerateSecret(m_remoteNonce, token.PreviousSecret); + + byte[] length = token.SecurityPolicy.KeyDataLength; + + byte[] serverSalt = Utils.Append( + length, + s_hkdfServerLabel, + serverSecret, + clientSecret); + + byte[] clientSalt = Utils.Append( + length, + s_hkdfClientLabel, + clientSecret, + serverSecret); + + DeriveKeysWithHKDF(token, serverSalt, true); + DeriveKeysWithHKDF(token, clientSalt, false); + +#if xDEBUG + m_logger.LogWarning("ServerSecret={ServerSecret}", TcpMessageType.KeyToString(serverSecret)); + m_logger.LogWarning("ClientSecret={ClientSecret}", TcpMessageType.KeyToString(clientSecret)); + m_logger.LogWarning("ServerSalt={ServerSalt}", TcpMessageType.KeyToString(serverSalt)); + m_logger.LogWarning("ClientSalt={ClientSalt}", TcpMessageType.KeyToString(clientSalt)); + m_logger.LogWarning("ServerEncryptingKey={ServerEncryptingKey}", TcpMessageType.KeyToString(token.ServerEncryptingKey)); + m_logger.LogWarning("ServerInitializationVector={ServerIV}", TcpMessageType.KeyToString(token.ServerInitializationVector)); + m_logger.LogWarning("ClientEncryptingKey={ClientEncryptingKey}", TcpMessageType.KeyToString(token.ClientEncryptingKey)); + m_logger.LogWarning("ClientInitializationVector={ClientIV}", TcpMessageType.KeyToString(token.ClientInitializationVector)); #endif + break; + } - DeriveKeysWithHKDF(token, serverSalt, true); - DeriveKeysWithHKDF(token, clientSalt, false); + default: + case KeyDerivationAlgorithm.PSha1: + case KeyDerivationAlgorithm.PSha256: + HashAlgorithmName algorithmName = token.SecurityPolicy.GetKeyDerivationHashAlgorithmName(); + DeriveKeysWithPSHA(algorithmName, serverSecret, clientSecret, token, false); + DeriveKeysWithPSHA(algorithmName, clientSecret, serverSecret, token, true); + break; } + + SessionActivationSecret = token.SessionActivationSecret; } /// @@ -317,20 +356,21 @@ protected BufferCollection WriteSymmetricMessage( int maxCipherTextSize = SendBufferSize - TcpMessageLimits.SymmetricHeaderSize; int maxCipherBlocks = maxCipherTextSize / EncryptionBlockSize; int maxPlainTextSize = maxCipherBlocks * EncryptionBlockSize; + + int paddingCountSize = + (SecurityMode != MessageSecurityMode.SignAndEncrypt || token.SecurityPolicy.NoSymmetricEncryptionPadding) + ? 0 + : 1; + int maxPayloadSize = maxPlainTextSize - SymmetricSignatureSize - - 1 - - TcpMessageLimits.SequenceHeaderSize; + TcpMessageLimits.SequenceHeaderSize - + paddingCountSize; + const int headerSize = TcpMessageLimits.SymmetricHeaderSize + TcpMessageLimits.SequenceHeaderSize; - // no padding byte for authenticated encryption. - if (token.SecurityPolicy.NoSymmetricEncryptionPadding) - { - maxPayloadSize++; - } - // write the body to stream. var ostrm = new ArraySegmentStream( BufferManager, @@ -370,14 +410,13 @@ protected BufferCollection WriteSymmetricMessage( byte[] buffer = BufferManager.TakeBuffer( SendBufferSize, "WriteSymmetricMessage"); + chunksToProcess.Add(new ArraySegment(buffer, 0, 0)); } + //Console.WriteLine($"WriteSymmetricMessage:{chunksToProcess[0].Offset}:{chunksToProcess[0].Count}"); var chunksToSend = new BufferCollection(chunksToProcess.Capacity); -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - Span paddingBuffer = stackalloc byte[EncryptionBlockSize]; -#endif int messageSize = 0; for (int ii = 0; ii < chunksToProcess.Count; ii++) @@ -393,6 +432,7 @@ protected BufferCollection WriteSymmetricMessage( var strm = new MemoryStream(chunkToProcess.Array, 0, SendBufferSize); using var encoder = new BinaryEncoder(strm, Quotas.MessageContext, false); + // check if the message needs to be aborted. if (MessageLimitsExceeded( isRequest, @@ -423,6 +463,7 @@ protected BufferCollection WriteSymmetricMessage( limitsExceeded = true; } + // check if the message is complete. else if (ii == chunksToProcess.Count - 1) { @@ -438,28 +479,22 @@ protected BufferCollection WriteSymmetricMessage( count += TcpMessageLimits.SequenceHeaderSize; count += chunkToProcess.Count; - count += SymmetricSignatureSize; + count += paddingCountSize; - // calculate the padding. int padding = 0; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && - !token.SecurityPolicy.NoSymmetricEncryptionPadding) + if (paddingCountSize > 0) { - // reserve one byte for the padding size. - count++; + padding = EncryptionBlockSize - (count % EncryptionBlockSize); - // use padding as helper to calc the real padding - padding = count % EncryptionBlockSize; - if (padding != 0) + if (padding < EncryptionBlockSize) { - padding = EncryptionBlockSize - padding; + count += padding; } - - count += padding; } count += TcpMessageLimits.SymmetricHeaderSize; + count += SymmetricSignatureSize; encoder.WriteUInt32(null, (uint)count); encoder.WriteUInt32(null, ChannelId); @@ -467,7 +502,6 @@ protected BufferCollection WriteSymmetricMessage( uint sequenceNumber = GetNewSequenceNumber(); encoder.WriteUInt32(null, sequenceNumber); - encoder.WriteUInt32(null, requestId); // skip body. @@ -476,63 +510,27 @@ protected BufferCollection WriteSymmetricMessage( // update message size count. messageSize += chunkToProcess.Count; - // write padding. - if (SecurityMode == MessageSecurityMode.SignAndEncrypt && - !token.SecurityPolicy.NoSymmetricEncryptionPadding) - { -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - if (padding > 1) - { - Span buffer = paddingBuffer[..(padding + 1)]; - buffer.Fill((byte)padding); - encoder.WriteRawBytes(buffer); - } - else -#endif - { - for (int jj = 0; jj <= padding; jj++) - { - encoder.WriteByte(null, (byte)padding); - } - } - } + ArraySegment dataToSend; - // calculate and write signature. if (SecurityMode != MessageSecurityMode.None) { - if (token.SecurityPolicy.NoSymmetricEncryptionPadding) - { - strm.Seek(SymmetricSignatureSize, SeekOrigin.Current); - } - else - { - byte[] signature = Sign( - token, - new ArraySegment(chunkToProcess.Array, 0, encoder.Position), - isRequest); - - if (signature != null) - { - encoder.WriteRawBytes(signature, 0, signature.Length); - } - } - } - - if ((SecurityMode == MessageSecurityMode.SignAndEncrypt && - !token.SecurityPolicy.NoSymmetricEncryptionPadding) || - (SecurityMode != MessageSecurityMode.None && token.SecurityPolicy.NoSymmetricEncryptionPadding)) - { - // encrypt the data. - var dataToEncrypt = new ArraySegment( + dataToSend = new ArraySegment( chunkToProcess.Array, TcpMessageLimits.SymmetricHeaderSize, encoder.Position - TcpMessageLimits.SymmetricHeaderSize); - Encrypt(token, dataToEncrypt, isRequest); + + dataToSend = EncryptAndSign(token, dataToSend, isRequest); + } + else + { + dataToSend = new ArraySegment( + chunkToProcess.Array, + 0, + encoder.Position); } // add the header into chunk. - chunksToSend.Add( - new ArraySegment(chunkToProcess.Array, 0, encoder.Position)); + chunksToSend.Add(dataToSend); } // ensure the buffers don't get cleaned up on exit. @@ -547,6 +545,21 @@ protected BufferCollection WriteSymmetricMessage( } } } + private ArraySegment EncryptAndSign( + ChannelToken token, + ArraySegment dataToEncrypt, + bool useClientKeys) + { + return CryptoUtils.SymmetricEncryptAndSign( + dataToEncrypt, + token.SecurityPolicy, + useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, + useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, + useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + this.SecurityMode == MessageSecurityMode.Sign, + token.TokenId, + (uint)(m_localSequenceNumber - 1)); // already incremented to create this message. need the last one sent. + } /// /// Decrypts and verifies a message chunk. @@ -580,6 +593,7 @@ protected ArraySegment ReadSymmetricMessage( { ActivateToken(RenewedToken); } + // check if activation of the new token should be forced. else if (RenewedToken != null && CurrentToken.ActivationRequired) { @@ -633,700 +647,55 @@ protected ArraySegment ReadSymmetricMessage( int headerSize = decoder.Position; - int decryptedCount = buffer.Count - headerSize; - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // decrypt the message. - decryptedCount = Decrypt( - token, - new ArraySegment( - buffer.Array, - buffer.Offset + headerSize, - buffer.Count - headerSize), - isRequest); - } + var dataToProcess = new ArraySegment( + buffer.Array, + buffer.Offset, + buffer.Count); - int paddingCount = 0; - if (SecurityMode != MessageSecurityMode.None && - !token.SecurityPolicy.NoSymmetricEncryptionPadding) + if (SecurityMode != MessageSecurityMode.None) { - int signatureStart = - buffer.Offset + - headerSize + - decryptedCount - - SymmetricSignatureSize; - - // extract signature. - byte[] signature = new byte[SymmetricSignatureSize]; - Array.Copy(buffer.Array, signatureStart, signature, 0, signature.Length); - - // verify the signature. - if (!Verify( - token, - signature, - new ArraySegment( - buffer.Array, - buffer.Offset, - headerSize + decryptedCount - SymmetricSignatureSize), - isRequest)) - { - m_logger.LogError("ChannelId {Id}: Could not verify signature on message.", Id); - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Could not verify the signature on the message."); - } + dataToProcess = new ArraySegment( + buffer.Array, + buffer.Offset + headerSize, + buffer.Count - headerSize); - if (SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // verify padding. - int paddingStart = signatureStart - 1; - paddingCount = buffer.Array[paddingStart]; - - for (int ii = paddingStart - paddingCount; ii < paddingStart; ii++) - { - if (buffer.Array[ii] != paddingCount) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Could not verify the padding in the message."); - } - } - - // add byte for size. - paddingCount++; - } - } - else if (SecurityMode != MessageSecurityMode.None) - { - // AEAD algorithms are verified during decrypt. - paddingCount = 0; + dataToProcess = DecryptAndVerify( + token, + dataToProcess, + isRequest); } // extract request id and sequence number. sequenceNumber = decoder.ReadUInt32(null); requestId = decoder.ReadUInt32(null); - // return an the data contained in the message. - int startOfBody = - buffer.Offset + - TcpMessageLimits.SymmetricHeaderSize + - TcpMessageLimits.SequenceHeaderSize; - int sizeOfBody = - decryptedCount - - TcpMessageLimits.SequenceHeaderSize - - paddingCount - - (SecurityMode != MessageSecurityMode.None && - !token.SecurityPolicy.NoSymmetricEncryptionPadding - ? SymmetricSignatureSize - : 0); - - return new ArraySegment(buffer.Array, startOfBody, sizeOfBody); - } - - /// - /// Returns the symmetric signature for the data. - /// - protected byte[] Sign(ChannelToken token, ArraySegment dataToSign, bool useClientKeys) - { - switch (SecurityPolicyUri) - { - case SecurityPolicies.Basic128Rsa15: - case SecurityPolicies.Basic256: - case SecurityPolicies.Basic256Sha256: - case SecurityPolicies.Aes128_Sha256_RsaOaep: - case SecurityPolicies.Aes256_Sha256_RsaPss: - case SecurityPolicies.ECC_nistP384: - case SecurityPolicies.ECC_brainpoolP256r1: - case SecurityPolicies.ECC_brainpoolP384r1: - case SecurityPolicies.ECC_nistP256: - return SymmetricSign(token, dataToSign, useClientKeys); - default: - return null; - } - } + headerSize += TcpMessageLimits.SequenceHeaderSize; - /// - /// Returns the symmetric signature for the data. - /// - protected bool Verify( - ChannelToken token, - byte[] signature, - ArraySegment dataToVerify, - bool useClientKeys) - { - // verify signature. - switch (SecurityPolicyUri) - { - case SecurityPolicies.None: - return true; - 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: - return SymmetricVerify(token, signature, dataToVerify, useClientKeys); - default: - return false; - } + // return only the data contained in the message. + return new ArraySegment( + dataToProcess.Array, + dataToProcess.Offset + headerSize, + dataToProcess.Count - headerSize); } - /// - /// Encrypts and signs the data in a buffer using symmetric encryption. - /// - /// - protected void Encrypt( - ChannelToken token, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - 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) - { - return; - } - - // For CBC based policies the caller already applied padding and signatures. - if (token.SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc - or SymmetricEncryptionAlgorithm.Aes256Cbc) - { - if (signOnly) - { - return; - } - - 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 and verifies the data in a buffer using symmetric encryption. - /// - /// - protected int Decrypt( + private ArraySegment DecryptAndVerify( ChannelToken token, ArraySegment dataToDecrypt, bool useClientKeys) { - if (SecurityPolicyUri == SecurityPolicies.None) - { - return dataToDecrypt.Count; - } - - 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; - - // 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; - } - - 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( + return CryptoUtils.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 - /// - /// Signs the message using HMAC. - /// - private static byte[] SymmetricSign( - ChannelToken token, - ReadOnlySpan dataToSign, - bool useClientKeys) - { - byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey; - - using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); - - // compute hash. - int hashSizeInBytes = hmac.HashSize >> 3; - byte[] signature = new byte[hashSizeInBytes]; - bool result = hmac.TryComputeHash(dataToSign, signature, out int bytesWritten); - - // check result - if (!result || bytesWritten != hashSizeInBytes) - { - ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "The computed hash doesn't match the expected size."); - } - - // return signature. - return signature; - } -#else - /// - /// Signs the message using HMAC. - /// - private static byte[] SymmetricSign( - ChannelToken token, - ArraySegment dataToSign, - bool useClientKeys) - { - byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey; - - using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); - - // compute hash. - var istrm = new MemoryStream( - dataToSign.Array, - dataToSign.Offset, - dataToSign.Count, - false); - byte[] signature = hmac.ComputeHash(istrm); - istrm.Dispose(); - - // return signature. - return signature; - } -#endif - -#if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER - /// - /// Verifies a HMAC for a message. - /// - private bool SymmetricVerify( - ChannelToken token, - ReadOnlySpan signature, - ReadOnlySpan dataToVerify, - bool useClientKeys) - { - byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey; - - using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); - - // compute hash. - int hashSizeInBytes = hmac.HashSize >> 3; - Span computedSignature = stackalloc byte[hashSizeInBytes]; - bool result = hmac.TryComputeHash( - dataToVerify, - computedSignature, - out int bytesWritten); - System.Diagnostics.Debug.Assert(bytesWritten == hashSizeInBytes); - // compare signatures. - if (!result || !computedSignature.SequenceEqual(signature)) - { - string expectedSignature = Utils.ToHexString(computedSignature.ToArray()); - string messageType = Encoding.UTF8.GetString(dataToVerify[..4]); - int messageLength = BitConverter.ToInt32(dataToVerify[4..]); - string actualSignature = Utils.ToHexString(signature); -#else - /// - /// Verifies a HMAC for a message. - /// - private bool SymmetricVerify( - ChannelToken token, - byte[] signature, - ArraySegment dataToVerify, - bool useClientKeys) - { - byte[] signingKey = useClientKeys ? token.ClientSigningKey : token.ServerSigningKey; - - using HMAC hmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); - - var istrm = new MemoryStream( - dataToVerify.Array, - dataToVerify.Offset, - dataToVerify.Count, - false); - byte[] computedSignature = hmac.ComputeHash(istrm); - istrm.Dispose(); - // compare signatures. - if (!Utils.IsEqual(computedSignature, signature)) - { - string expectedSignature = Utils.ToHexString(computedSignature); - string messageType = Encoding.UTF8 - .GetString(dataToVerify.Array, dataToVerify.Offset, 4); - int messageLength = BitConverter.ToInt32( - dataToVerify.Array, - dataToVerify.Offset + 4); - string actualSignature = Utils.ToHexString(signature); -#endif - m_logger.LogError( - "Channel{Id}: Could not validate signature. ChannelId={ChannelId}, TokenId={TokenId}, MessageType={MessageType}, Length={Length} ExpectedSignature={ExpectedSignature} ActualSignature={ActualSignature}", - Id, - token.ChannelId, - token.TokenId, - messageType, - messageLength, - expectedSignature, - actualSignature); - - return false; - } - - return true; - } - - - -#if CURVE25519 - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricEncryptWithChaCha20Poly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var encryptingKey = (useClientKeys) ? token.ClientEncryptingKey : token.ServerEncryptingKey; - - if (encryptingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var iv = (useClientKeys) ? token.ClientInitializationVector : token.ServerInitializationVector; - - if (iv == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - // Utils.Trace($"EncryptKey={Utils.ToHexString(encryptingKey)}"); - // Utils.Trace($"EncryptIV1={Utils.ToHexString(iv)}"); - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, iv); - // Utils.Trace($"EncryptIV2={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - var plaintext = dataToEncrypt.Array; - int headerSize = dataToEncrypt.Offset; - int plainTextLength = dataToEncrypt.Offset + dataToEncrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 encryptor = new ChaCha20Poly1305(); - encryptor.Init(true, parameters); - encryptor.ProcessAadBytes(plaintext, 0, headerSize); - - byte[] ciphertext = new byte[encryptor.GetOutputSize(plainTextLength - headerSize) + headerSize]; - Buffer.BlockCopy(plaintext, 0, ciphertext, 0, headerSize); - int length = encryptor.ProcessBytes( - plaintext, - headerSize, - plainTextLength - headerSize, - ciphertext, - headerSize); - length += encryptor.DoFinal(ciphertext, length + headerSize); - - if (ciphertext.Length - headerSize != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Cipher text not the expected size. [{ciphertext.Length - headerSize} != {length}]"); - } - - Buffer.BlockCopy(ciphertext, 0, plaintext, 0, plainTextLength + signatureLength); - - // byte[] mac = new byte[16]; - // Buffer.BlockCopy(plaintext, plainTextLength, mac, 0, signatureLength); - // Utils.Trace($"EncryptMAC1={Utils.ToHexString(encryptor.GetMac())}"); - // Utils.Trace($"EncryptMAC2={Utils.ToHexString(mac)}"); - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricSignWithPoly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToEncrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, signingKey); - - using (var hash = SHA256.Create()) - { - signingKey = hash.ComputeHash(signingKey); - } - - // Utils.Trace($"SigningKey={Utils.ToHexString(signingKey)}"); - - int signatureLength = 16; - - var plaintext = dataToEncrypt.Array; - int headerSize = dataToEncrypt.Offset; - int plainTextLength = dataToEncrypt.Offset + dataToEncrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - Poly1305 poly = new Poly1305(); - - poly.Init(new KeyParameter(signingKey, 0, signingKey.Length)); - poly.BlockUpdate(plaintext, 0, plainTextLength); - int length = poly.DoFinal(plaintext, plainTextLength); - - if (signatureLength != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Signed data not the expected size. [{plainTextLength + signatureLength} != {length}]"); - } - } - - /// - /// Decrypts a message using a symmetric algorithm. - /// - private static void SymmetricDecryptWithChaCha20Poly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - var encryptingKey = (useClientKeys) ? token.ClientEncryptingKey : token.ServerEncryptingKey; - - if (encryptingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - var iv = (useClientKeys) ? token.ClientInitializationVector : token.ServerInitializationVector; - - if (iv == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - // Utils.Trace($"DecryptKey={Utils.ToHexString(encryptingKey)}"); - // Utils.Trace($"DecryptIV1={Utils.ToHexString(iv)}"); - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, iv); - // Utils.Trace($"DecryptIV2={Utils.ToHexString(iv)}"); - - int signatureLength = 16; - - var ciphertext = dataToDecrypt.Array; - int headerSize = dataToDecrypt.Offset; - int cipherTextLength = dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{cipherTextLength}|{signatureLength}|[{cipherTextLength + signatureLength}]"); - - byte[] mac = new byte[16]; - Buffer.BlockCopy(ciphertext, cipherTextLength, mac, 0, signatureLength); - // Utils.Trace($"DecryptMAC={Utils.ToHexString(mac)}"); - - AeadParameters parameters = new AeadParameters( - new KeyParameter(encryptingKey), - signatureLength * 8, - iv, - null); - - ChaCha20Poly1305 decryptor = new ChaCha20Poly1305(); - decryptor.Init(false, parameters); - decryptor.ProcessAadBytes(ciphertext, 0, headerSize); - - var plaintext = new byte[ - decryptor.GetOutputSize(cipherTextLength + signatureLength - headerSize) + headerSize - ]; - Buffer.BlockCopy(ciphertext, headerSize, plaintext, 0, headerSize); - - int length = decryptor.ProcessBytes( - ciphertext, - headerSize, - cipherTextLength + signatureLength - headerSize, - plaintext, - headerSize); - length += decryptor.DoFinal(plaintext, length + headerSize); - - if (plaintext.Length - headerSize != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Plain text not the expected size. [{plaintext.Length - headerSize} != {length}]"); - } - - Buffer.BlockCopy(plaintext, 0, ciphertext, 0, cipherTextLength); - } - - /// - /// Encrypts a message using a symmetric algorithm. - /// - private static void SymmetricVerifyWithPoly1305( - ChannelToken token, - uint lastSequenceNumber, - ArraySegment dataToDecrypt, - bool useClientKeys) - { - var signingKey = (useClientKeys) ? token.ClientSigningKey : token.ServerSigningKey; - - if (signingKey == null) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - "Token missing symmetric key object."); - } - - ApplyChaCha20Poly1305Mask(token, lastSequenceNumber, signingKey); - // Utils.Trace($"SigningKey={Utils.ToHexString(signingKey)}"); - - using (var hash = SHA256.Create()) - { - signingKey = hash.ComputeHash(signingKey); - } - - int signatureLength = 16; - - var plaintext = dataToDecrypt.Array; - int headerSize = dataToDecrypt.Offset; - int plainTextLength = dataToDecrypt.Offset + dataToDecrypt.Count - signatureLength; - - // Utils.Trace($"OUT={headerSize}|{plainTextLength}|{signatureLength}|[{plainTextLength + signatureLength}]"); - - Poly1305 poly = new Poly1305(); - - poly.Init(new KeyParameter(signingKey, 0, signingKey.Length)); - poly.BlockUpdate(plaintext, 0, plainTextLength); - - byte[] mac = new byte[poly.GetMacSize()]; - int length = poly.DoFinal(mac, 0); - - if (signatureLength != length) - { - throw ServiceResultException.Create( - StatusCodes.BadSecurityChecksFailed, - $"Signed data not the expected size. [{plainTextLength + signatureLength} != {length}]"); - } - - for (int ii = 0; ii < mac.Length; ii++) - { - if (mac[ii] != plaintext[plainTextLength + ii]) - { - throw ServiceResultException.Create(StatusCodes.BadSecurityChecksFailed, $"Invaid MAC on data."); - } - } - } - - private static void ApplyChaCha20Poly1305Mask(ChannelToken token, uint lastSequenceNumber, byte[] iv) - { - iv[0] ^= (byte)((token.TokenId & 0x000000FF)); - iv[1] ^= (byte)((token.TokenId & 0x0000FF00) >> 8); - iv[2] ^= (byte)((token.TokenId & 0x00FF0000) >> 16); - iv[3] ^= (byte)((token.TokenId & 0xFF000000) >> 24); - iv[4] ^= (byte)((lastSequenceNumber & 0x000000FF)); - iv[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); - iv[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); - iv[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); + useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, + useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, + useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + this.SecurityMode == MessageSecurityMode.Sign, + token.TokenId, + (uint)m_remoteSequenceNumber); } -#endif private static readonly byte[] s_hkdfClientLabel = Encoding.UTF8.GetBytes("opcua-client"); private static readonly byte[] s_hkdfServerLabel = Encoding.UTF8.GetBytes("opcua-server"); - private static readonly byte[] s_hkdfAes128SignOnlyKeyLength = BitConverter.GetBytes( - (ushort)32); - private static readonly byte[] s_hkdfAes256SignOnlyKeyLength = BitConverter.GetBytes( - (ushort)48); - private static readonly byte[] s_hkdfAes128SignAndEncryptKeyLength = BitConverter.GetBytes( - (ushort)64); - private static readonly byte[] s_hkdfAes256SignAndEncryptKeyLength = BitConverter.GetBytes( - (ushort)96); - private static readonly byte[] s_hkdfChaCha20Poly1305KeyLength = BitConverter.GetBytes( - (ushort)76); private int m_signatureKeySize; private int m_encryptionKeySize; } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs index 2e24e6729d..f396c3ad8b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.cs @@ -238,6 +238,18 @@ protected virtual void Dispose(bool disposing) /// public string GlobalChannelId { get; private set; } + /// + internal byte[] SecureChannelHash { get; set; } + + /// + internal byte[] SessionActivationSecret { get; set; } + + /// + public byte[] ClientChannelCertificate { get; protected set; } + + /// + public byte[] ServerChannelCertificate { get; protected set; } + /// /// Raised when the state of the channel changes. /// @@ -271,7 +283,7 @@ protected void ChannelStateChanged(TcpChannelState state, ServiceResult reason) /// protected uint GetNewSequenceNumber() { - bool isLegacy = !EccUtils.IsEccPolicy(SecurityPolicyUri); + bool isLegacy = !CryptoUtils.IsEccPolicy(SecurityPolicyUri); long newSeqNumber = Interlocked.Increment(ref m_sequenceNumber); bool maxValueOverflow = isLegacy @@ -320,8 +332,8 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // Accept the first sequence number depending on security policy if (m_firstReceivedSequenceNumber && ( - !EccUtils.IsEccPolicy(SecurityPolicyUri) || - (EccUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) + !CryptoUtils.IsEccPolicy(SecurityPolicyUri) || + (CryptoUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) { m_remoteSequenceNumber = sequenceNumber; m_firstReceivedSequenceNumber = false; @@ -342,8 +354,8 @@ protected bool VerifySequenceNumber(uint sequenceNumber, string context) // only one rollover per token is allowed and with valid values depending on security policy if (!m_sequenceRollover && ( - !EccUtils.IsEccPolicy(SecurityPolicyUri) || - (EccUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) + !CryptoUtils.IsEccPolicy(SecurityPolicyUri) || + (CryptoUtils.IsEccPolicy(SecurityPolicyUri) && (sequenceNumber == 0)))) { m_sequenceRollover = true; m_remoteSequenceNumber = sequenceNumber; @@ -617,6 +629,8 @@ protected void BeginWriteMessage(BufferCollection buffers, object state) try { + m_logger.LogWarning("OUT:{Id}", TcpMessageType.GetTypeAndSize(buffers[0])); + Interlocked.Increment(ref m_activeWriteRequests); args.BufferList = buffers; args.Completed += OnWriteComplete; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 355c2b44f0..453cbde83b 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -18,6 +18,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Diagnostics; using System.Globalization; using System.IO; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; @@ -535,6 +536,11 @@ private void SendOpenSecureChannelRequest(bool renew) ChannelToken token = CreateToken(); token.ClientNonce = CreateNonce(ClientCertificate); + if (renew) + { + token.PreviousSecret = CurrentToken?.Secret; + } + // construct the request. var request = new OpenSecureChannelRequest(); request.RequestHeader.Timestamp = DateTime.UtcNow; @@ -549,6 +555,11 @@ private void SendOpenSecureChannelRequest(bool renew) // encode the request. byte[] buffer = BinaryEncoder.EncodeMessage(request, Quotas.MessageContext); + ClientChannelCertificate = ClientCertificate?.RawData; + ServerChannelCertificate = ServerCertificate?.RawData; + + byte[] signature; + // write the asymmetric message. BufferCollection? chunksToSend = WriteAsymmetricMessage( TcpMessageType.Open, @@ -556,7 +567,12 @@ private void SendOpenSecureChannelRequest(bool renew) ClientCertificate, ClientCertificateChain, ServerCertificate, - new ArraySegment(buffer, 0, buffer.Length)); + new ArraySegment(buffer, 0, buffer.Length), + m_oscRequestSignature, + out signature); + + // don't keep signature if secure channel enhancements are not used. + m_oscRequestSignature = (SecurityPolicy.SecureChannelEnhancements) ? signature : null; // save token. m_requestedToken = token; @@ -611,13 +627,29 @@ private bool ProcessOpenSecureChannelResponse( uint sequenceNumber; try { + byte[] signature; + + Console.WriteLine($"OSC IN={TcpMessageType.KeyToString(messageChunk)}"); + messageBody = ReadAsymmetricMessage( messageChunk, ClientCertificate, out channelId, out serverCertificate, out requestId, - out sequenceNumber); + out sequenceNumber, + m_oscRequestSignature, + out signature); + + if (PreviousToken == null) + { + ComputeSecureChannelHash(signature); + } + + Console.WriteLine($"OSC OUT={TcpMessageType.KeyToString(messageBody)}"); + Console.WriteLine($"oscRequestSignature={TcpMessageType.KeyToString(m_oscRequestSignature)}"); + Console.WriteLine($"signature={TcpMessageType.KeyToString(signature)}"); + Console.WriteLine($"State={State}"); } catch (Exception e) { @@ -783,6 +815,8 @@ protected override bool HandleIncomingMessage( uint messageType, ArraySegment messageChunk) { + //m_logger.LogWarning("IN:{Size}", TcpMessageType.GetTypeAndSize(messageChunk)); + // process a response. if (TcpMessageType.IsType(messageType, TcpMessageType.Message)) { @@ -1748,5 +1782,6 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message private List? m_queuedOperations; private readonly ILogger m_logger; private readonly ITelemetryContext m_telemetry; + private byte[]? m_oscRequestSignature; } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index 0b84cfee04..fa3ad84191 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -104,7 +104,16 @@ public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); /// - public ChannelToken? CurrentToken => m_channel?.CurrentToken; + public byte[] SecureChannelHash => m_channel?.SecureChannelHash ?? []; + + /// + public byte[] SessionActivationSecret => m_channel?.SessionActivationSecret ?? []; + + /// + public byte[] ClientChannelCertificate => m_channel?.ClientChannelCertificate ?? []; + + /// + public byte[] ServerChannelCertificate => m_channel?.ServerChannelCertificate ?? []; /// public int OperationTimeout { get; set; } @@ -428,6 +437,8 @@ private UaSCUaBinaryClientChannel CreateChannel( } } + var id = Guid.NewGuid().ToString(); + // create the channel. var channel = new UaSCUaBinaryClientChannel( Guid.NewGuid().ToString(), diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index 9d59e71634..772ae91045 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -34,11 +34,6 @@ public delegate void ChannelTokenActivatedEventHandler( /// public interface ISecureChannel { - /// - /// Gets the channel's current security token. - /// - ChannelToken? CurrentToken { get; } - /// /// Register for token change events /// @@ -99,6 +94,26 @@ public interface ITransportChannel : IDisposable /// EndpointConfiguration EndpointConfiguration { get; } + /// + /// The unique identifier for the secure channel. + /// + byte[] SecureChannelHash { get; } + + /// + /// A secret used to re-activate sessions on a new secure channel. + /// + byte[] SessionActivationSecret { get; } + + /// + /// The client certificate used to establsih the secure channel. + /// + byte[] ClientChannelCertificate { get; } + + /// + /// The server certificate used to establsih the secure channel. + /// + byte[] ServerChannelCertificate { get; } + /// /// Gets the context used when serializing messages exchanged /// via the channel. Throws if the channel is not yet opened. diff --git a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs index 1e683d3be8..8f0029fb8f 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs @@ -41,8 +41,20 @@ public IServiceMessageContext MessageContext => throw Unexpected(nameof(MessageContext)); /// - public ChannelToken CurrentToken - => throw Unexpected(nameof(CurrentToken)); + public byte[] SecureChannelHash + => throw Unexpected(nameof(SecureChannelHash)); + + /// + public byte[] SessionActivationSecret + => throw Unexpected(nameof(SessionActivationSecret)); + + /// + public byte[] ClientChannelCertificate + => throw Unexpected(nameof(ClientChannelCertificate)); + + /// + public byte[] ServerChannelCertificate + => throw Unexpected(nameof(ServerChannelCertificate)); /// public int OperationTimeout { get; set; } diff --git a/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs b/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs index d7cfd9ba19..959660105d 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/UserNameIdentityToken.cs @@ -58,7 +58,7 @@ public override void Encrypt( } // handle RSA encryption. - if (!EccUtils.IsEccPolicy(securityPolicyUri)) + if (!CryptoUtils.IsEccPolicy(securityPolicyUri)) { byte[] dataToEncrypt = Utils.Append(DecryptedPassword, receiverNonce); @@ -98,7 +98,7 @@ public override void Encrypt( receiverCertificate, receiverEphemeralKey, senderCertificate, - Nonce.CreateNonce(securityPolicyUri), + Nonce.CreateNonce(SecurityPolicies.GetInfo(securityPolicyUri)), null, doNotEncodeSenderCertificate); @@ -138,7 +138,7 @@ public override void Decrypt( } // handle RSA encryption. - if (!EccUtils.IsEccPolicy(securityPolicyUri)) + if (!CryptoUtils.IsEccPolicy(securityPolicyUri)) { var encryptedData = new EncryptedData { diff --git a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs index 7dae91b6d1..e2e51ed695 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityToken.cs @@ -50,9 +50,11 @@ public override SignatureData Sign( X509Certificate2 certificate = Certificate ?? CertificateFactory.Create(m_certificateData); - SignatureData signatureData = SecurityPolicies.Sign( + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + SignatureData signatureData = SecurityPolicies.CreateSignatureData( + info, certificate, - securityPolicyUri, dataToSign); m_certificateData = certificate.RawData; @@ -75,11 +77,13 @@ public override bool Verify( X509Certificate2 certificate = Certificate ?? CertificateFactory.Create(m_certificateData); - bool valid = SecurityPolicies.Verify( + var info = SecurityPolicies.GetInfo(securityPolicyUri); + + bool valid = SecurityPolicies.VerifySignatureData( + signatureData, + info, certificate, - securityPolicyUri, - dataToVerify, - signatureData); + dataToVerify); m_certificateData = certificate.RawData; diff --git a/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs b/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs index 7cdbdadda8..0a9788e9da 100644 --- a/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs +++ b/Stack/Opc.Ua.Types/Diagnostics/TelemetryUtils.cs @@ -141,7 +141,7 @@ public void Dispose() [Conditional("DEBUG")] private static void DebugCheck() { - Debug.Fail("Using a NullLogger"); + //Debug.Fail("Using a NullLogger"); } } diff --git a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs index ef43b211dc..0d775566fa 100644 --- a/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Types/Encoders/BinaryDecoder.cs @@ -228,7 +228,8 @@ public IEncodeable DecodeMessage(Type expectedType) // lookup message type. Type actualType = Context.Factory.GetSystemType(absoluteId) - ?? throw ServiceResultException.Create( + ?? + throw ServiceResultException.Create( StatusCodes.BadDecodingError, "Cannot decode message with type id: {0}.", absoluteId); diff --git a/UA Reference.sln b/UA Reference.sln index 4ef54f8e4e..9915517938 100644 --- a/UA Reference.sln +++ b/UA Reference.sln @@ -70,6 +70,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stack", "Stack", "{2DC9F7F3 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Opc.Ua.Types", "Stack\Opc.Ua.Types\Opc.Ua.Types.csproj", "{1A3E53FF-C13F-78A9-22B0-2045136C1904}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecurityTestClient", "Applications\SecurityTestClient\SecurityTestClient.csproj", "{D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -213,6 +215,18 @@ Global {1A3E53FF-C13F-78A9-22B0-2045136C1904}.Release|x64.Build.0 = Release|Any CPU {1A3E53FF-C13F-78A9-22B0-2045136C1904}.Release|x86.ActiveCfg = Release|Any CPU {1A3E53FF-C13F-78A9-22B0-2045136C1904}.Release|x86.Build.0 = Release|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Debug|x64.Build.0 = Debug|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Debug|x86.Build.0 = Debug|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Release|Any CPU.Build.0 = Release|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Release|x64.ActiveCfg = Release|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Release|x64.Build.0 = Release|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Release|x86.ActiveCfg = Release|Any CPU + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -231,6 +245,7 @@ Global {4B72937F-5A57-4CEA-B9FE-B4C45CF7B284} = {ACBF012E-08A7-4939-88CF-D6B610E35AC5} {83CC0D9C-40CD-487A-BF45-C4A8C1B8BF69} = {07AA31A3-97A1-4A6F-9E88-97B5198997B7} {1A3E53FF-C13F-78A9-22B0-2045136C1904} = {2DC9F7F3-6698-4875-88A3-50678170A810} + {D1C054DC-3D5C-E7EF-6C08-6C22E5FA89F8} = {07AA31A3-97A1-4A6F-9E88-97B5198997B7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {33FA5BCB-C827-4D44-AECF-F51342DFE64A} From a579c625296a3bdf3c4783d780d65c9dba488647 Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Wed, 26 Nov 2025 19:36:59 -0800 Subject: [PATCH 05/42] Add support for SessionTransferToken. Removed obsolete SoftwareCertificate code. --- .../ConsoleReferenceClient/RunTest.cs | 10 +- .../generate_user_certificate.ps1 | 13 +- .../Quickstarts.ReferenceServer.Config.xml | 20 +-- Libraries/Opc.Ua.Client/Session/Session.cs | 140 +++++++----------- .../Opc.Ua.Server/Diagnostics/AuditEvents.cs | 19 --- .../Opc.Ua.Server/Server/StandardServer.cs | 110 ++++++-------- Libraries/Opc.Ua.Server/Session/ISession.cs | 7 + .../Opc.Ua.Server/Session/ISessionManager.cs | 11 ++ Libraries/Opc.Ua.Server/Session/Session.cs | 46 ++++++ .../Opc.Ua.Server/Session/SessionManager.cs | 28 ++++ .../Schema/SecuredApplicationHelpers.cs | 8 +- .../Certificates/CertificateIdentifier.cs | 12 +- .../Security/Certificates/CryptoUtils.cs | 4 +- .../Security/Certificates/Nonce.cs | 5 + .../Security/Constants/SecurityPolicies.cs | 26 ++-- .../Security/Constants/SecurityPolicyInfo.cs | 117 +++++++++++++-- Stack/Opc.Ua.Core/Stack/Tcp/TcpMessageType.cs | 15 +- .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 4 + .../Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs | 29 ++-- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 1 + .../Stack/Tcp/UaSCBinaryClientChannel.cs | 14 +- 21 files changed, 396 insertions(+), 243 deletions(-) diff --git a/Applications/ConsoleReferenceClient/RunTest.cs b/Applications/ConsoleReferenceClient/RunTest.cs index a129e6dbb6..469877b595 100644 --- a/Applications/ConsoleReferenceClient/RunTest.cs +++ b/Applications/ConsoleReferenceClient/RunTest.cs @@ -25,6 +25,7 @@ internal sealed class RunTest private ISession m_session; const string ServerUrl = "opc.tcp://localhost:62541"; + //const string ServerUrl = "opc.tcp://WhiteCat:4880/Softing/OpcUa/TestServer"; const int kMaxSearchDepth = 128; const int ReconnectPeriod = 1000; const int ReconnectPeriodExponentialBackoff = 15000; @@ -107,9 +108,12 @@ public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken c ServerUrl, ct).ConfigureAwait(false); + // endpoints = endpoints.Where(x => x.SecurityPolicyUri == SecurityPolicies.ECC_nistP256_AesGcm).ToList(); + var endpointConfiguration = EndpointConfiguration.Create(m_configuration); var sessionFactory = new DefaultSessionFactory(m_context); var userNameidentity = new UserIdentity("sysadmin", new UTF8Encoding(false).GetBytes("demo")); + // var userNameidentity = new UserIdentity("usr", new UTF8Encoding(false).GetBytes("pwd")); foreach (var ii in endpoints) { @@ -119,7 +123,7 @@ public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken c var certificateIdentity = await LoadUserCertificateAsync(thumbprint, "password", ct).ConfigureAwait(false); - foreach (var identity in new UserIdentity[] { userNameidentity, certificateIdentity }) + foreach (var identity in new UserIdentity[] { userNameidentity }) { try { @@ -144,7 +148,7 @@ public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken c m_logger.LogWarning("Waiting for SecureChannel renew"); - for (int count = 0; count < 10; count++) + for (int count = 0; count < 15; count++) { var result = await session.ReadAsync( null, @@ -167,6 +171,8 @@ public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken c await Task.Delay(5000, ct).ConfigureAwait(false); } + await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false); + await session.CloseAsync(true, ct: ct).ConfigureAwait(false); } catch (Exception e) diff --git a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 index bb3a12a470..3174d24855 100644 --- a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 +++ b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 @@ -29,6 +29,8 @@ foreach ($d in @($certDir, $privateDir)) { foreach ($curve in $curves) { Write-Host "Generating certificate for curve: $curve" + + $signatureAlgorithm = if ($curve -match 'P384') { 'SHA384' } else { 'SHA256' } # Create certificate parameters and dynamically insert the curve $params = @{ @@ -38,10 +40,11 @@ foreach ($curve in $curves) { '2.5.29.37={text}1.3.6.1.5.5.7.3.2' '2.5.29.17={text}upn=iama.tester@example.com' ) - KeyUsage = 'DigitalSignature' - KeyAlgorithm = "ECDSA_$curve" # <-- dynamic! - CurveExport = 'CurveName' - CertStoreLocation = 'Cert:\CurrentUser\My' + KeyUsage = @('DigitalSignature', 'NonRepudiation') + KeyAlgorithm = "ECDSA_$curve" # <-- dynamic! + CurveExport = 'CurveName' + HashAlgorithm = $signatureAlgorithm + CertStoreLocation = 'Cert:\CurrentUser\My' } # 1. Create cert @@ -68,7 +71,7 @@ $rsaParams = @{ '2.5.29.37={text}1.3.6.1.5.5.7.3.2' '2.5.29.17={text}upn=iama.tester@example.com' ) - KeyUsage = 'DigitalSignature' + KeyUsage = @('DigitalSignature','DataEncipherment','NonRepudiation','KeyEncipherment') KeyAlgorithm = 'RSA' KeyLength = 2048 CertStoreLocation = 'Cert:\CurrentUser\My' diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index f1cfad96bf..5ce4d8f4c5 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -140,27 +140,27 @@ Sign_2 - http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AES + - - + + - - + + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly SignAndEncrypt_3 - http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AES + - + - + - - + + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly - + - + + - - http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + + + http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + SignAndEncrypt_3 - + - + - - http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + + + http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + + @@ -148,12 +180,11 @@ - http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm - - - - - SignAndEncrypt_3 + + + @@ -164,9 +195,9 @@ - http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm - - + + Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -49,17 +49,17 @@ Directory - ./pki/issuer + ../../pki/issuer Directory - ./pki/trusted + ../../pki/trusted Directory - ./pki/rejected + ../../pki/rejected 5 Directory - ./pki/userIssuer + ../../pki/userIssuer Directory - ./pki/trustedUser + ../../pki/trustedUser diff --git a/Applications/ConsoleReferenceClient/RunTest.cs b/Applications/ConsoleReferenceClient/RunConnectAll.cs similarity index 99% rename from Applications/ConsoleReferenceClient/RunTest.cs rename to Applications/ConsoleReferenceClient/RunConnectAll.cs index 116b728d43..0d0c22281e 100644 --- a/Applications/ConsoleReferenceClient/RunTest.cs +++ b/Applications/ConsoleReferenceClient/RunConnectAll.cs @@ -15,7 +15,7 @@ namespace SecurityTestClient { - internal sealed class RunTest + internal sealed class RunConnectAll { private readonly Lock m_lock = new(); private SessionReconnectHandler m_reconnectHandler; @@ -30,7 +30,7 @@ internal sealed class RunTest const int ReconnectPeriod = 1000; const int ReconnectPeriodExponentialBackoff = 15000; - public RunTest(ApplicationConfiguration configuration, ITelemetryContext context) + public RunConnectAll(ApplicationConfiguration configuration, ITelemetryContext context) { m_context = context; m_configuration = configuration; @@ -118,7 +118,7 @@ public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken c foreach (var ii in endpoints) { var userCertificateFile = GetUserCertificateFile(ii.SecurityPolicyUri); - var x509 = X509CertificateLoader.LoadCertificateFromFile(Path.Combine(".\\pki\\trustedUser\\certs", userCertificateFile)); + var x509 = X509CertificateLoader.LoadCertificateFromFile(Path.Combine("..\\..\\pki\\trustedUser\\certs", userCertificateFile)); var thumbprint = x509.Thumbprint; var certificateIdentity = await LoadUserCertificateAsync(thumbprint, "password", ct).ConfigureAwait(false); diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 5de749c282..b612da440e 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -14,35 +14,35 @@ Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - ./pki/own + ../../pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -51,17 +51,17 @@ Directory - ./pki/issuer + ../../pki/issuer Directory - ./pki/trusted + ../../pki/trusted Directory - ./pki/rejected + ../../pki/rejected 5 Directory - ./pki/issuerUser + ../../pki/issuerUser Directory - ./pki/trustedUser + ../../pki/trustedUser @@ -135,7 +135,6 @@ SignAndEncrypt_3 - --> Sign_2 http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 @@ -144,6 +143,7 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + --> Sign_2 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm @@ -152,6 +152,7 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 6d7b91ce69..873635dcf3 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1527,7 +1527,6 @@ public async Task UpdateSessionAsync( // send the software certificates assigned to the client. SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); - // during debugging send the sesson transfer token on all activations. RequestHeader? requestHeader = CreateRequestHeaderForActivateSession( securityPolicy, tokenSecurityPolicyUri); diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs index 6301776680..cc6e55fa91 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -562,6 +562,7 @@ public static ArraySegment SymmetricEncryptAndSign( byte[] encryptingKey, byte[] iv, byte[] signingKey = null, + HMAC hmac = null, bool signOnly = false, uint tokenId = 0, uint lastSequenceNumber = 0) @@ -604,7 +605,6 @@ public static ArraySegment SymmetricEncryptAndSign( if (signingKey != null) { - using HMAC hmac = securityPolicy.CreateSignatureHmac(signingKey); byte[] hash = hmac.ComputeHash(data.Array, 0, data.Offset + data.Count); Buffer.BlockCopy( diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs index b497d2ea71..c1af365da7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/ChannelToken.cs @@ -28,6 +28,7 @@ * ======================================================================*/ using System; +using System.Security.Cryptography; namespace Opc.Ua.Bindings { @@ -52,6 +53,18 @@ private void Dispose(bool disposing) { if (!m_disposed) { + if (ServerHmac != null) + { + ServerHmac.Dispose(); + ServerHmac = null; + } + + if (ClientHmac != null) + { + ClientHmac.Dispose(); + ClientHmac = null; + } + m_disposed = true; } } @@ -167,5 +180,15 @@ public void Dispose() /// The initialization vector by the server when encrypting a message. /// internal byte[] ServerInitializationVector { get; set; } + + /// + /// A pre-allocated HMAC used to improve performance for SecurityPolicies that need it. + /// + internal HMAC ServerHmac { get; set; } + + /// + /// A pre-allocated HMAC used to improve performance for SecurityPolicies that need it. + /// + internal HMAC ClientHmac { get; set; } } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index 6061fbad68..f306901e0c 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -304,8 +304,6 @@ protected override bool HandleIncomingMessage( { SetResponseRequired(true); - //m_logger.LogWarning("IN:{Id}", TcpMessageType.GetTypeAndSize(messageChunk)); - try { // process a response. diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 910ea33e91..3445be72a9 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -45,12 +45,12 @@ public partial class UaSCUaBinaryChannel /// /// Returns the current security token. /// - protected ChannelToken PreviousToken { get; private set; } + protected internal ChannelToken PreviousToken { get; private set; } /// /// Returns the renewed but not yet activated token. /// - protected ChannelToken RenewedToken { get; private set; } + protected internal ChannelToken RenewedToken { get; private set; } /// /// Called when the token changes @@ -195,12 +195,14 @@ private void DeriveKeysWithPSHA( token.ServerSigningKey = signingKey; token.ServerEncryptingKey = encryptingKey; token.ServerInitializationVector = iv; + token.ServerHmac = SecurityPolicy.CreateSignatureHmac(signingKey); } else { token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.ClientHmac = SecurityPolicy.CreateSignatureHmac(signingKey); } } @@ -521,6 +523,7 @@ private ArraySegment EncryptAndSign( useClientKeys ? token.ClientEncryptingKey : token.ServerEncryptingKey, useClientKeys ? token.ClientInitializationVector : token.ServerInitializationVector, useClientKeys ? token.ClientSigningKey : token.ServerSigningKey, + useClientKeys ? token.ClientHmac : token.ServerHmac, this.SecurityMode == MessageSecurityMode.Sign, token.TokenId, (uint)(m_localSequenceNumber - 1)); // already incremented to create this message. need the last one sent. diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 1a75441c2d..fcbbcea5e7 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -842,8 +842,6 @@ protected override bool HandleIncomingMessage( uint messageType, ArraySegment messageChunk) { - //m_logger.LogWarning("IN:{Size}", TcpMessageType.GetTypeAndSize(messageChunk)); - // process a response. if (TcpMessageType.IsType(messageType, TcpMessageType.Message)) { From 387ad76b77de3085b2745bdd53e025bda37195f6 Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Wed, 17 Dec 2025 22:23:05 -0800 Subject: [PATCH 11/42] Fix CoPilot flagged spelling errors. --- Libraries/Opc.Ua.Server/Server/StandardServer.cs | 4 ++-- Libraries/Opc.Ua.Server/Session/ISession.cs | 2 +- Libraries/Opc.Ua.Server/Session/Session.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 74b7455913..17b6d0c582 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -659,7 +659,7 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { session.SetUserTokenSecurityPolicy(policyUri); - EphemeralKeyType key = session.GetNewEphmeralKey(); + EphemeralKeyType key = session.GetNewEphemeralKey(); response.Parameters.Add( new KeyValuePair { @@ -699,7 +699,7 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam { AdditionalParametersType response = null; - EphemeralKeyType key = session.GetNewEphmeralKey(); + EphemeralKeyType key = session.GetNewEphemeralKey(); if (key != null) { diff --git a/Libraries/Opc.Ua.Server/Session/ISession.cs b/Libraries/Opc.Ua.Server/Session/ISession.cs index 8b447b087a..3674cf4597 100644 --- a/Libraries/Opc.Ua.Server/Session/ISession.cs +++ b/Libraries/Opc.Ua.Server/Session/ISession.cs @@ -134,7 +134,7 @@ bool Activate( /// Create new ECC ephemeral key /// /// A new ephemeral key - EphemeralKeyType GetNewEphmeralKey(); + EphemeralKeyType GetNewEphemeralKey(); /// /// Checks if the secure channel is currently valid. diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index dc2ecd3880..e6501b3770 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -305,7 +305,7 @@ public virtual void SetUserTokenSecurityPolicy(string securityPolicyUri) /// Create new ECC ephemeral key /// /// A new ephemeral key - public virtual EphemeralKeyType GetNewEphmeralKey() + public virtual EphemeralKeyType GetNewEphemeralKey() { lock (m_lock) { From 61530986566d9b082866398cd3532436f24387a0 Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Wed, 17 Dec 2025 22:45:10 -0800 Subject: [PATCH 12/42] Rename EccUtils to CryptoUtils --- Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs | 2 +- Tests/Opc.Ua.Gds.Tests/PushTest.cs | 4 ++-- Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs index d23cd51d2c..a9969e985d 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/CertificateGroup.cs @@ -674,7 +674,7 @@ private static bool TryGetECCCurve(NodeId certificateType, out ECCurve curve) return false; } curve = - EccUtils.GetCurveFromCertificateTypeId(certificateType) + CryptoUtils.GetCurveFromCertificateTypeId(certificateType) ?? throw new ServiceResultException( StatusCodes.BadNotSupported, $"The certificate type {certificateType} is not supported."); diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index a4e3ee2807..deeef0b057 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -764,7 +764,7 @@ public async Task UpdateCertificateSelfSignedAsync(string keyFormat) X509Certificate2 newCert; - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { @@ -1286,7 +1286,7 @@ private async Task CreateCATestCertsAsync(string tempStorePath, ITelemetryContex var certificateStoreIdentifier = new CertificateStoreIdentifier(tempStorePath, false); Assert.IsTrue(EraseStore(certificateStoreIdentifier, telemetry)); const string subjectName = "CN=CA Test Cert, O=OPC Foundation"; - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs index d04e798ce6..1f668787e2 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/CRLTests.cs @@ -111,7 +111,7 @@ public CRLTests(string certificateTypeString, NodeId certificateType) [OneTimeSetUp] protected void OneTimeSetUp() { - ECCurve? curve = EccUtils.GetCurveFromCertificateTypeId(m_certificateType); + ECCurve? curve = CryptoUtils.GetCurveFromCertificateTypeId(m_certificateType); if (curve != null) { From e46ff9abd03cb5b1000e788b3559fc67161ef94e Mon Sep 17 00:00:00 2001 From: Suciu Mircea Adrian Date: Thu, 18 Dec 2025 09:36:37 +0200 Subject: [PATCH 13/42] Update version from 1.5.378-preview to 1.5.378 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2384e1c39b..2dc9e72bb2 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.5.378-preview", + "version": "1.5.378", "versionHeightOffset": 0, "nugetPackageVersion": { "semVer": 2 From dc0b110aa725d54c38b1f58583ea704cf512f4f8 Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Wed, 17 Dec 2025 23:57:21 -0800 Subject: [PATCH 14/42] Fix unit tests. --- .../Stack/Https/HttpsTransportChannel.cs | 3 +++ .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 3 +++ .../Stack/Transport/ITransportChannel.cs | 5 +++++ .../Opc.Ua.Core/Stack/Transport/NullChannel.cs | 4 ++++ .../Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs | 17 ++++++++++------- .../Pkcs10CertificationRequestTests.cs | 4 +++- Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs | 7 ++++++- 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index fb7a5b7bb5..52a4b36b93 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -134,6 +134,9 @@ public EndpointConfiguration EndpointConfiguration public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); + /// + public ChannelToken CurrentToken => new(); + /// public byte[] ChannelThumbprint => []; diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index 3e26468f47..639cd07c76 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -120,6 +120,9 @@ public EndpointConfiguration EndpointConfiguration public IServiceMessageContext MessageContext => m_quotas?.MessageContext ?? throw BadNotConnected(); + /// + public ChannelToken CurrentToken => m_channel?.CurrentToken ?? new(); + /// public byte[] ChannelThumbprint => m_channel?.ChannelThumbprint ?? []; diff --git a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs index eee03aa083..7de67cf0f1 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/ITransportChannel.cs @@ -51,6 +51,11 @@ public delegate void ChannelTokenActivatedEventHandler( /// public interface ISecureChannel { + /// + /// Gets the channel's current security token. + /// + ChannelToken? CurrentToken { get; } + /// /// Register for token change events /// diff --git a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs index 356c550ed6..b6787e56b5 100644 --- a/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Transport/NullChannel.cs @@ -41,6 +41,10 @@ namespace Opc.Ua /// internal sealed class NullChannel : ITransportChannel, ISecureChannel { + /// + public ChannelToken CurrentToken + => throw Unexpected(nameof(CurrentToken)); + /// public TransportChannelFeatures SupportedFeatures => throw Unexpected(nameof(SupportedFeatures)); diff --git a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs index 9517ceb487..5a0ad14145 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs @@ -58,7 +58,8 @@ public void ValidateCreateNoncePolicyLength(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; var nonce = Ua.Nonce.CreateNonce(securityPolicyUri); @@ -84,10 +85,11 @@ public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; var nonceByLen = Ua.Nonce.CreateNonce(securityPolicyUri); - var nonceByData = Ua.Nonce.CreateNonce(securityPolicyUri, nonceByLen.Data); + var nonceByData = Ua.Nonce.CreateNonce(info, nonceByLen.Data); Assert.IsNotNull(nonceByData); Assert.IsNotNull(nonceByData.Data); @@ -113,11 +115,12 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( { if (IsSupportedByPlatform(securityPolicyUri)) { - uint nonceLength = Ua.Nonce.GetNonceLength(securityPolicyUri); + var info = Ua.SecurityPolicies.GetInfo(securityPolicyUri); + var nonceLength = info.SecureChannelNonceLength; byte[] randomValue = Ua.Nonce.CreateRandomNonceData(nonceLength); - if (securityPolicyUri.Contains("ECC_", StringComparison.Ordinal)) + if (info.CertificateKeyFamily == CertificateKeyFamily.ECC) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && ( @@ -128,11 +131,11 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( .Ignore("No exception is thrown on OSX with NIST curves"); } NUnit.Framework.Assert.Throws(() => - Ua.Nonce.CreateNonce(securityPolicyUri, randomValue)); + Ua.Nonce.CreateNonce(info, randomValue)); } else { - var rsaNonce = Ua.Nonce.CreateNonce(securityPolicyUri, randomValue); + var rsaNonce = Ua.Nonce.CreateNonce(info, randomValue); Assert.AreEqual(rsaNonce.Data, randomValue); } } diff --git a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs index 8109e9663f..19626f70b0 100644 --- a/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs +++ b/Tests/Opc.Ua.Security.Certificates.Tests/Pkcs10CertificationRequestTests.cs @@ -293,6 +293,8 @@ public void GetCertificationRequestInfoReturnsValidData() Assert.Greater(requestInfo.Length, 0); } + static readonly string[] kHosts = new[] { "localhost" }; + /// /// Test parsing multiple CSRs in sequence. /// @@ -310,7 +312,7 @@ public void ParseMultipleCsrsInSequence() using X509Certificate2 certificate = CertificateBuilder.Create(subject) .SetNotBefore(DateTime.UtcNow.AddDays(-1)) .SetLifeTime(TimeSpan.FromDays(30)) - .AddExtension(new X509SubjectAltNameExtension(applicationUri, new[] { "localhost" })) + .AddExtension(new X509SubjectAltNameExtension(applicationUri, kHosts)) .CreateForRSA(); byte[] csrData = CertificateFactory.CreateSigningRequest(certificate); diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs b/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs index d453ac1244..ab28ead410 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixtureUtils.cs @@ -93,7 +93,12 @@ public static class ServerFixtureUtils // set security context var secureChannelContext - = new SecureChannelContext(sessionName, endpoint, RequestEncoding.Binary); + = new SecureChannelContext( + sessionName, + endpoint, RequestEncoding.Binary, + null, + null, + null); var requestHeader = new RequestHeader(); // Create session From e8befb73027a2d42322d6329c29b0c8e950685ef Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Thu, 15 Jan 2026 19:12:01 -0800 Subject: [PATCH 15/42] Allow SignatureData.Algorithm to be NULL or Empty. --- .../ConsoleReferenceClient/generate_user_certificate.ps1 | 4 ++-- .../Configuration/ConfigurationNodeManager.cs | 1 + Libraries/Opc.Ua.Server/Server/StandardServer.cs | 3 --- Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs | 8 ++++---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 index 3174d24855..ee938f5431 100644 --- a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 +++ b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 @@ -1,6 +1,6 @@ # 1. Ensure directories exist -$certDir = "./pki/trustedUser/certs" -$privateDir = "./pki/trustedUser/private" +$certDir = "./bin/pki/trustedUser/certs" +$privateDir = "./bin/pki/trustedUser/private" $curves = @( 'nistP256', diff --git a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index 7f380579cf..d26458d4a0 100644 --- a/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -391,6 +391,7 @@ public void HasApplicationSecureAdminAccess(ISystemContext context) } /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1725:Parameter names should match base declaration", Justification = "")] public void HasApplicationSecureAdminAccess( ISystemContext context, CertificateStoreIdentifier _) diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 2757f8417c..0154890748 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -525,9 +525,6 @@ X509Certificate2Collection clientCertificateChain // return the endpoints supported by the server. serverEndpoints = GetEndpointDescriptions(endpointUrl, BaseAddresses, null); - // return the software certificates assigned to the server. - serverSoftwareCertificates = new(); - // sign the nonce provided by the client. serverSignature = null; diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index ae0972f558..aab7821d92 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -787,7 +787,7 @@ public static bool VerifySignatureData( case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: { - if (signature.Algorithm == SecurityAlgorithms.RsaSha256) + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == SecurityAlgorithms.RsaSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), @@ -801,7 +801,7 @@ public static bool VerifySignatureData( case AsymmetricSignatureAlgorithm.RsaPssSha256: { - if (signature.Algorithm == SecurityAlgorithms.RsaPssSha256) + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == SecurityAlgorithms.RsaPssSha256) { return RsaUtils.Rsa_Verify( new ArraySegment(dataToVerify), @@ -815,7 +815,7 @@ public static bool VerifySignatureData( case AsymmetricSignatureAlgorithm.EcdsaSha256: { - if (signature.Algorithm == null || signature.Algorithm == securityPolicy.Uri) + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == securityPolicy.Uri) { return CryptoUtils.Verify( new ArraySegment(dataToVerify), @@ -829,7 +829,7 @@ public static bool VerifySignatureData( case AsymmetricSignatureAlgorithm.EcdsaSha384: { - if (signature.Algorithm == null || signature.Algorithm == securityPolicy.Uri) + if (String.IsNullOrEmpty(signature.Algorithm) || signature.Algorithm == securityPolicy.Uri) { return CryptoUtils.Verify( new ArraySegment(dataToVerify), From 9e8e144afc3c7e856776700c491170670a740792 Mon Sep 17 00:00:00 2001 From: Randy Armstrong Date: Wed, 28 Jan 2026 03:12:03 -0800 Subject: [PATCH 16/42] Fix issue with BrainPool_p256r1_ChaChaPoly --- .../Quickstarts.ReferenceServer.Config.xml | 12 ++++++++++-- .../Security/Constants/SecurityPolicyInfo.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index d41d6cce78..2bb229cfde 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -143,7 +143,6 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 - --> Sign_2 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm @@ -152,7 +151,16 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm - + + Sign_2 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly + + Sign_2 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly @@ -160,7 +159,6 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly - Sign_2 http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm @@ -177,6 +176,7 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm + diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs index 33fb1a31b4..c5122b7bae 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -978,7 +978,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() CertificateSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha256, EphemeralKeyAlgorithm = CertificateKeyAlgorithm.RSADH, KeyDerivationAlgorithm = KeyDerivationAlgorithm.HKDFSha256, - SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, + SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, IsDeprecated = false From f242325f23f7ce13f25f35bb0e12e4378c32001f Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 4 Feb 2026 15:29:52 +0200 Subject: [PATCH 18/42] Fix OSC/padding by deriving HMAC keys, tightening symmetric size math, and correcting ECC_brainpoolP384r1/Basic128Rsa15 IV, signature, padding, and nonce parameters. --- .../Security/Certificates/CryptoUtils.cs | 19 ++++++++++++++++--- .../Security/Constants/SecurityPolicyInfo.cs | 11 ++++++----- .../Stack/Tcp/UaSCBinaryChannel.Symmetric.cs | 16 ++++++++++++---- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs index cc6e55fa91..22968835ed 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -487,11 +487,12 @@ public static bool Verify( /// /// buffer with unencrypted data starting at 0; plaintext data starting at offset; no padding. /// + /// Additional bytes that will be appended after padding (e.g., HMAC) and must be considered for block alignment. /// Output: buffer with unencrypted data starting at 0; plaintext data starting at offset; padding added. - private static ArraySegment AddPadding(ArraySegment data, int blockSize) + private static ArraySegment AddPadding(ArraySegment data, int blockSize, int trailingBytes = 0) { int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1; - int paddingSize = blockSize - ((data.Count + paddingByteSize) % blockSize); + int paddingSize = blockSize - ((data.Count + paddingByteSize + trailingBytes) % blockSize); paddingSize %= blockSize; int endOfData = data.Offset + data.Count; @@ -598,9 +599,21 @@ public static ArraySegment SymmetricEncryptAndSign( #endif } + int hashLength = 0; + + if (signingKey != null) + { + if (hmac == null) + { + throw new CryptographicException("Missing HMAC for symmetric signing."); + } + + hashLength = hmac.HashSize / 8; + } + if (!signOnly) { - data = AddPadding(data, iv.Length); + data = AddPadding(data, iv.Length, hashLength); } if (signingKey != null) diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs index c5122b7bae..cd0609c706 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -368,16 +368,17 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() /// public static readonly SecurityPolicyInfo Basic128Rsa15 = new(SecurityPolicies.Basic128Rsa15) { - DerivedSignatureKeyLength = 128 / 8, + // HMAC-SHA1 requires a 160-bit derived signature key. + DerivedSignatureKeyLength = 160 / 8, SymmetricEncryptionKeyLength = 128 / 8, // HMAC-SHA1 produces a 160-bit MAC SymmetricSignatureLength = 160 / 8, InitializationVectorLength = 128 / 8, MinAsymmetricKeyLength = 1024, MaxAsymmetricKeyLength = 2048, - SecureChannelNonceLength = 16, + SecureChannelNonceLength = 32, LegacySequenceNumbers = true, - AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaOaepSha1, + AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, CertificateKeyFamily = CertificateKeyFamily.RSA, CertificateKeyAlgorithm = CertificateKeyAlgorithm.RSA, @@ -887,8 +888,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() { DerivedSignatureKeyLength = 384 / 8, SymmetricEncryptionKeyLength = 256 / 8, - InitializationVectorLength = 96 / 8, - SymmetricSignatureLength = 128 / 8, + InitializationVectorLength = 128 / 8, + SymmetricSignatureLength = 384 / 8, MinAsymmetricKeyLength = 384, MaxAsymmetricKeyLength = 384, SecureChannelNonceLength = 96, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs index 3445be72a9..9d8dbb24b5 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Symmetric.cs @@ -233,12 +233,14 @@ private void DeriveKeysWithHKDF( token.ServerSigningKey = signingKey; token.ServerEncryptingKey = encryptingKey; token.ServerInitializationVector = iv; + token.ServerHmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); } else { token.ClientSigningKey = signingKey; token.ClientEncryptingKey = encryptingKey; token.ClientInitializationVector = iv; + token.ClientHmac = token.SecurityPolicy.CreateSignatureHmac(signingKey); } } @@ -325,14 +327,15 @@ protected BufferCollection WriteSymmetricMessage( int maxCipherBlocks = maxCipherTextSize / EncryptionBlockSize; int maxPlainTextSize = maxCipherBlocks * EncryptionBlockSize; + int signatureSize = SymmetricSignatureSize; int paddingCountSize = (SecurityMode != MessageSecurityMode.SignAndEncrypt || token.SecurityPolicy.NoSymmetricEncryptionPadding) ? 0 - : 1; + : (EncryptionBlockSize > byte.MaxValue ? 2 : 1); int maxPayloadSize = maxPlainTextSize - - SymmetricSignatureSize - + signatureSize - TcpMessageLimits.SequenceHeaderSize - paddingCountSize; @@ -447,6 +450,7 @@ protected BufferCollection WriteSymmetricMessage( count += TcpMessageLimits.SequenceHeaderSize; count += chunkToProcess.Count; count += paddingCountSize; + count += signatureSize; int padding = 0; @@ -454,14 +458,18 @@ protected BufferCollection WriteSymmetricMessage( { padding = EncryptionBlockSize - (count % EncryptionBlockSize); - if (padding < EncryptionBlockSize) + if (padding == EncryptionBlockSize) + { + padding = 0; + } + + if (padding > 0) { count += padding; } } count += TcpMessageLimits.SymmetricHeaderSize; - count += SymmetricSignatureSize; encoder.WriteUInt32(null, (uint)count); encoder.WriteUInt32(null, ChannelId); From 1d065c2528cd0cbe62f27e3dc12b52b1b82e07dd Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 5 Feb 2026 17:21:29 +0200 Subject: [PATCH 19/42] Policies without asymmetric encryption (ECC) return the plaintext when encrypting user tokens; this prevents null payloads from causing BadIdentityTokenInvalid. --- Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index aab7821d92..e4707e4c72 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -481,6 +481,11 @@ public static EncryptedData Encrypt( } } } + else + { + // No asymmetric encryption is defined for this policy – return the plaintext. + encryptedData.Data = plainText.ToArray(); + } return encryptedData; } From 0adcdf99bff4bff4cbdc9794aa625d83be5d92fa Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 6 Feb 2026 15:58:59 +0200 Subject: [PATCH 20/42] Reserve outer CBC padding for avoiding SymetricEncryptAndSign->AddPadding to overwrite next fields (signature) --- .../Security/Certificates/EncryptedSecret.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs index 21c86173f8..da3c9829aa 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs @@ -262,6 +262,22 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) int endOfSecret = encoder.Position; + // reserve space for the outer padding that SymmetricEncryptAndSign will add (CBC only). + int outerPaddingSize = 0; + if (SecurityPolicy.SymmetricEncryptionAlgorithm is SymmetricEncryptionAlgorithm.Aes128Cbc or SymmetricEncryptionAlgorithm.Aes256Cbc) + { + int blockSize = SecurityPolicy.InitializationVectorLength; + int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1; + int paddingSize = blockSize - ((endOfSecret - startOfSecret + paddingByteSize) % blockSize); + paddingSize %= blockSize; + outerPaddingSize = paddingSize + paddingByteSize; + + for (int ii = 0; ii < outerPaddingSize; ii++) + { + encoder.WriteByte(null, 0xCD); + } + } + // save space for tag. for (int ii = 0; ii < tagLength; ii++) { @@ -300,7 +316,7 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) signature, 0, message, - endOfSecret + tagLength, + endOfSecret + outerPaddingSize + tagLength, signatureLength); return message; From 30c78f6f41a594496f3bd798911968758cf21ef0 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 6 Feb 2026 16:05:10 +0200 Subject: [PATCH 21/42] Nonce stored as byte array in SessionConfiguration; Sesion snapshot restores ServerNonce and reconstructs ECC ephemeral key Nonce from bytes and policy --- Libraries/Opc.Ua.Client/Session/Session.cs | 138 +++++++++++------- .../Session/SessionConfiguration.cs | 9 +- 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index f0768c6349..eabb04e45f 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -893,9 +893,10 @@ public virtual void Restore(SessionState state) /// public void Snapshot(out SessionConfiguration sessionConfiguration) { - var serverNonce = Nonce.CreateNonce( - SecurityPolicies.GetInfo(m_endpoint.Description?.SecurityPolicyUri), - m_serverNonce); + byte[]? serverNonce = m_serverNonce != null ? [.. m_serverNonce] : null; + byte[]? serverEccEphemeralKey = m_eccServerEphemeralKey?.Data != null + ? [.. m_eccServerEphemeralKey.Data] + : null; sessionConfiguration = new SessionConfiguration { SessionName = SessionName, @@ -905,7 +906,7 @@ public void Snapshot(out SessionConfiguration sessionConfiguration) ConfiguredEndpoint = ConfiguredEndpoint, CheckDomain = CheckDomain, ServerNonce = serverNonce, - ServerEccEphemeralKey = m_eccServerEphemeralKey, + ServerEccEphemeralKey = serverEccEphemeralKey, UserIdentityTokenPolicy = m_userTokenSecurityPolicyUri }; } @@ -922,9 +923,24 @@ public void Restore(SessionConfiguration sessionConfiguration) : null; m_identity = sessionConfiguration.Identity ?? new UserIdentity(); m_checkDomain = sessionConfiguration.CheckDomain; - m_serverNonce = sessionConfiguration.ServerNonce?.Data; + m_serverNonce = sessionConfiguration.ServerNonce != null + ? [.. sessionConfiguration.ServerNonce] + : null; m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; - m_eccServerEphemeralKey = sessionConfiguration.ServerEccEphemeralKey; + if (sessionConfiguration.ServerEccEphemeralKey?.Length > 0) + { + string ephemeralKeyPolicyUri = !string.IsNullOrEmpty(m_userTokenSecurityPolicyUri) + ? m_userTokenSecurityPolicyUri + : m_endpoint.Description?.SecurityPolicyUri ?? SecurityPolicies.None; + SecurityPolicyInfo ephemeralKeyPolicy = SecurityPolicies.GetInfo(ephemeralKeyPolicyUri); + m_eccServerEphemeralKey = Nonce.CreateNonce( + ephemeralKeyPolicy, + sessionConfiguration.ServerEccEphemeralKey); + } + else + { + m_eccServerEphemeralKey = null; + } lock (m_lock) { @@ -1286,6 +1302,9 @@ await m_configuration m_previousServerNonce, m_endpoint.Description.SecurityMode); + // remember the policy actually used to encrypt the user token + m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; + SignatureData? userTokenSignature = null; if (identityToken is X509IdentityToken) @@ -1311,7 +1330,7 @@ await m_configuration identityToken.Encrypt( serverCertificate, serverNonce, - m_userTokenSecurityPolicyUri, + tokenSecurityPolicyUri, MessageContext, m_eccServerEphemeralKey, m_instanceCertificate, @@ -2365,23 +2384,8 @@ public async Task ReconnectAsync( string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); - - // create the client signature. - byte[] dataToSign = securityPolicy.GetClientSignatureData( - TransportChannel.ChannelThumbprint, - m_serverNonce, - m_serverCertificate?.RawData, - TransportChannel.ServerChannelCertificate, - TransportChannel.ClientChannelCertificate, - m_clientNonce ?? []); - EndpointDescription endpoint = m_endpoint.Description; - SignatureData clientSignature = SecurityPolicies.CreateSignatureData( - endpoint.SecurityPolicyUri, - m_instanceCertificate, - dataToSign); - // check that the user identity is supported by the endpoint. UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy( m_identity.TokenType, @@ -2419,34 +2423,6 @@ public async Task ReconnectAsync( UserIdentityToken identityToken = m_identity.GetIdentityToken(); identityToken.PolicyId = identityPolicy.PolicyId; - dataToSign = securityPolicy.GetUserTokenSignatureData( - TransportChannel.ChannelThumbprint, - m_serverNonce, - m_serverCertificate?.RawData, - TransportChannel.ServerChannelCertificate, - m_instanceCertificate?.RawData, - TransportChannel.ClientChannelCertificate, - m_clientNonce ?? []); - - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri, - m_telemetry); - - // encrypt token. - identityToken.Encrypt( - m_serverCertificate, - m_serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); - - // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); - m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); if (connection != null) @@ -2511,6 +2487,68 @@ public async Task ReconnectAsync( } } + ITransportChannel activeChannel = TransportChannel; + + byte[] channelThumbprint = activeChannel.ChannelThumbprint; + byte[] serverChannelCertificate = activeChannel.ServerChannelCertificate; + byte[] clientChannelCertificate = activeChannel.ClientChannelCertificate; + + // HTTPS channels populate the server TLS certificate lazily after the first request. + // Reconnect signs before that, so use the server certificate from the session state. + if ((serverChannelCertificate == null || serverChannelCertificate.Length == 0) && + m_serverCertificate != null) + { + serverChannelCertificate = m_serverCertificate.RawData; + } + + if ((clientChannelCertificate == null || clientChannelCertificate.Length == 0) && + m_instanceCertificate != null) + { + clientChannelCertificate = m_instanceCertificate.RawData; + } + + // create the client signature. + byte[] dataToSign = securityPolicy.GetClientSignatureData( + channelThumbprint, + m_serverNonce, + m_serverCertificate?.RawData, + serverChannelCertificate, + clientChannelCertificate, + m_clientNonce ?? []); + + SignatureData clientSignature = SecurityPolicies.CreateSignatureData( + endpoint.SecurityPolicyUri, + m_instanceCertificate, + dataToSign); + + dataToSign = securityPolicy.GetUserTokenSignatureData( + channelThumbprint, + m_serverNonce, + m_serverCertificate?.RawData, + serverChannelCertificate, + m_instanceCertificate?.RawData, + clientChannelCertificate, + m_clientNonce ?? []); + + SignatureData userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri, + m_telemetry); + + // encrypt token. + identityToken.Encrypt( + m_serverCertificate, + m_serverNonce, + m_userTokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + + // send the software certificates assigned to the client. + SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); + m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); var header = CreateRequestHeaderForActivateSession( diff --git a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs index 1412b32844..82dabe508b 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs @@ -123,10 +123,11 @@ public SessionState(SessionOptions options) public NodeId AuthenticationToken { get; init; } = NodeId.Null; /// - /// The last server nonce received. + /// The raw bytes of the last server nonce received. + /// Persisting bytes avoids object-serialization ambiguity for Nonce internals. /// [DataMember(IsRequired = true, Order = 80)] - public Nonce? ServerNonce { get; init; } + public byte[]? ServerNonce { get; init; } /// /// The user identity token policy which was used to create the session. @@ -135,10 +136,10 @@ public SessionState(SessionOptions options) public string? UserIdentityTokenPolicy { get; init; } /// - /// The last server ecc ephemeral key received. + /// The raw bytes of the last server ECC ephemeral key received. /// [DataMember(IsRequired = false, Order = 100)] - public Nonce? ServerEccEphemeralKey { get; init; } + public byte[]? ServerEccEphemeralKey { get; init; } /// /// Allows the list of subscriptions to be saved/restored From dc99f61c22b354dbb99acfb3b6c572f14bd9b74f Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Tue, 10 Feb 2026 13:14:37 +0200 Subject: [PATCH 22/42] Added _AesGcm and _ChaChaPoly variants to BuildSupportedSecurityPolicies() for all six ECC certificate types (nistP256, nistP384, brainpoolP256r1, brainpoolP384r1, curve25519, curve448) to SecurityConfiguration. Added Sign and SignAndEncrypt endpoints for the four ECC AesGcm/ChaChaPoly policy pairs (nistP256, nistP384, brainpoolP256r1, brainpoolP384r1) to ServerFixture. --- .../Certificates/SecurityConfiguration.cs | 20 +++++++++++++++++ Tests/Opc.Ua.Server.Tests/ServerFixture.cs | 22 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs b/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs index 87f1c1f0a6..d728742955 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs @@ -215,6 +215,8 @@ private StringCollection BuildSupportedSecurityPolicies() securityPolicies.Add(SecurityPolicies.Basic256Sha256); securityPolicies.Add(SecurityPolicies.Aes128_Sha256_RsaOaep); securityPolicies.Add(SecurityPolicies.Aes256_Sha256_RsaPss); + securityPolicies.Add(SecurityPolicies.RSA_DH_AesGcm); + securityPolicies.Add(SecurityPolicies.RSA_DH_ChaChaPoly); continue; } if (applicationCertificate.CertificateType.Identifier is uint identifier) @@ -223,23 +225,39 @@ private StringCollection BuildSupportedSecurityPolicies() { case ObjectTypes.EccNistP256ApplicationCertificateType: securityPolicies.Add(SecurityPolicies.ECC_nistP256); + securityPolicies.Add(SecurityPolicies.ECC_nistP256_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_nistP256_ChaChaPoly); break; case ObjectTypes.EccNistP384ApplicationCertificateType: securityPolicies.Add(SecurityPolicies.ECC_nistP256); + securityPolicies.Add(SecurityPolicies.ECC_nistP256_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_nistP256_ChaChaPoly); securityPolicies.Add(SecurityPolicies.ECC_nistP384); + securityPolicies.Add(SecurityPolicies.ECC_nistP384_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_nistP384_ChaChaPoly); break; case ObjectTypes.EccBrainpoolP256r1ApplicationCertificateType: securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); break; case ObjectTypes.EccBrainpoolP384r1ApplicationCertificateType: securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); break; case ObjectTypes.EccCurve25519ApplicationCertificateType: securityPolicies.Add(SecurityPolicies.ECC_curve25519); + securityPolicies.Add(SecurityPolicies.ECC_curve25519_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_curve25519_ChaChaPoly); break; case ObjectTypes.EccCurve448ApplicationCertificateType: securityPolicies.Add(SecurityPolicies.ECC_curve448); + securityPolicies.Add(SecurityPolicies.ECC_curve448_AesGcm); + securityPolicies.Add(SecurityPolicies.ECC_curve448_ChaChaPoly); break; case ObjectTypes.RsaMinApplicationCertificateType: securityPolicies.Add(SecurityPolicies.Basic128Rsa15); @@ -250,6 +268,8 @@ private StringCollection BuildSupportedSecurityPolicies() securityPolicies.Add(SecurityPolicies.Basic256Sha256); securityPolicies.Add(SecurityPolicies.Aes128_Sha256_RsaOaep); securityPolicies.Add(SecurityPolicies.Aes256_Sha256_RsaPss); + securityPolicies.Add(SecurityPolicies.RSA_DH_AesGcm); + securityPolicies.Add(SecurityPolicies.RSA_DH_ChaChaPoly); goto case ObjectTypes.RsaMinApplicationCertificateType; } } diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs index 555bda63a0..d851f8f308 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs @@ -127,8 +127,28 @@ public async Task LoadConfigurationAsync(string pkiRoot = null) .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256) .AddSignPolicies() .AddSignAndEncryptPolicies() + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.RSA_DH_AesGcm) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.RSA_DH_AesGcm) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.RSA_DH_ChaChaPoly) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.RSA_DH_ChaChaPoly) .AddEccSignPolicies() - .AddEccSignAndEncryptPolicies(); + .AddEccSignAndEncryptPolicies() + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP256_AesGcm) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP256_AesGcm) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP256_ChaChaPoly) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP256_ChaChaPoly) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP384_AesGcm) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP384_AesGcm) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP384_ChaChaPoly) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP384_ChaChaPoly) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP256r1_AesGcm) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP256r1_AesGcm) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP384r1_AesGcm) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP384r1_AesGcm) + .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly) + .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); } if (OperationLimits) From 0e83966ef40aabd925da85076662a2d794a668bf Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Tue, 10 Feb 2026 16:36:27 +0200 Subject: [PATCH 23/42] make ephemeralKeyPolicyUri nullable --- Libraries/Opc.Ua.Client/Session/Session.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index eabb04e45f..46b8b6e820 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -929,7 +929,7 @@ public void Restore(SessionConfiguration sessionConfiguration) m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; if (sessionConfiguration.ServerEccEphemeralKey?.Length > 0) { - string ephemeralKeyPolicyUri = !string.IsNullOrEmpty(m_userTokenSecurityPolicyUri) + string? ephemeralKeyPolicyUri = !string.IsNullOrEmpty(m_userTokenSecurityPolicyUri) ? m_userTokenSecurityPolicyUri : m_endpoint.Description?.SecurityPolicyUri ?? SecurityPolicies.None; SecurityPolicyInfo ephemeralKeyPolicy = SecurityPolicies.GetInfo(ephemeralKeyPolicyUri); From 908398229bdcba869341d278091046f7b4b5922a Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 12 Feb 2026 17:29:43 +0200 Subject: [PATCH 24/42] GenerateSecret also for NET7 and NET8 --- .../Opc.Ua.Core/Security/Certificates/Nonce.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs index 0900287541..fffa3aaa73 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -83,7 +83,7 @@ internal byte[] GenerateSecret( #if xDEBUG Span privateKey = stackalloc char[2048]; - if (m_ecdh.TryExportECPrivateKeyPem(privateKey, out int charsWritten)) + if (m_ecdh != null && m_ecdh.TryExportECPrivateKeyPem(privateKey, out int charsWritten)) { CryptoTrace.WriteLine($"Private Key PEM ({charsWritten} chars):"); } @@ -92,28 +92,35 @@ internal byte[] GenerateSecret( if (m_ecdh != null) { ikm = m_ecdh.DeriveRawSecretAgreement(remoteNonce.m_ecdh.PublicKey); - } else if (m_rsadh != null) { ikm = m_rsadh.DeriveRawSecretAgreement(remoteNonce.m_rsadh); } +#else // !NET8_0_OR_GREATER (NET78 and NET80) + if (m_ecdh != null) + { + ikm = m_ecdh.DeriveKeyMaterial(remoteNonce.m_ecdh.PublicKey); + } + else if (m_rsadh != null) + { + ikm = m_rsadh.DeriveRawSecretAgreement(remoteNonce.m_rsadh); + } +#endif CryptoTrace.WriteLine($"IKM-Raw={CryptoTrace.KeyToString(ikm)}"); CryptoTrace.WriteLine($"Previous-IKM={CryptoTrace.KeyToString(previousSecret)}"); - if (previousSecret != null) + if (ikm != null && previousSecret != null) { for (int ii = 0; ii < ikm.Length && ii < previousSecret.Length; ii++) { ikm[ii] ^= previousSecret[ii]; } } - CryptoTrace.WriteLine($"IKM-XOR={CryptoTrace.KeyToString(ikm)}"); CryptoTrace.Finish("GenerateSecret"); -#endif return ikm; } From 6b60fbff3a3c0631d9a35e914c29596e307d62ef Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 13 Feb 2026 20:11:04 +0200 Subject: [PATCH 25/42] Adjust Basic128Rsa15 policy properties for backword compatibility --- Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs | 10 +++++++++- .../Security/Constants/SecurityPolicyInfo.cs | 5 ++--- .../Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs | 9 ++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs index fffa3aaa73..aa982a2bfc 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -308,7 +308,15 @@ public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy, byte[] nonceD /// The requested Nonce as a public static byte[] CreateRandomNonceData(int length) { - if (length < s_minNonceLength) + return CreateRandomNonceData(length, true); + } + + /// + /// Generates nonce data with optional minimum length enforcement. + /// + public static byte[] CreateRandomNonceData(int length, bool enforceMinimumLength) + { + if (enforceMinimumLength && length < s_minNonceLength) { length = (int)s_minNonceLength; } diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs index cd0609c706..2de16f5526 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -368,15 +368,14 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() /// public static readonly SecurityPolicyInfo Basic128Rsa15 = new(SecurityPolicies.Basic128Rsa15) { - // HMAC-SHA1 requires a 160-bit derived signature key. - DerivedSignatureKeyLength = 160 / 8, + DerivedSignatureKeyLength = 128 / 8, SymmetricEncryptionKeyLength = 128 / 8, // HMAC-SHA1 produces a 160-bit MAC SymmetricSignatureLength = 160 / 8, InitializationVectorLength = 128 / 8, MinAsymmetricKeyLength = 1024, MaxAsymmetricKeyLength = 2048, - SecureChannelNonceLength = 32, + SecureChannelNonceLength = 16, LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.RsaPkcs15Sha1, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.RsaPkcs15Sha1, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index db7a7f19bb..c5b8c76073 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -208,7 +208,14 @@ protected byte[] CreateNonce(X509Certificate2 certificate) m_localNonce = Nonce.CreateNonce(SecurityPolicy); return m_localNonce.Data; } - return Nonce.CreateRandomNonceData(SecurityPolicy.SecureChannelNonceLength); + // Basic128Rsa15 is the only RSA based security policy that allows nonces + // with a length less than 32 bytes for compatibility reasons. + bool enforceMinimumLength = !SecurityPolicy.Uri.Equals( + SecurityPolicies.Basic128Rsa15, + StringComparison.Ordinal); + return Nonce.CreateRandomNonceData( + SecurityPolicy.SecureChannelNonceLength, + enforceMinimumLength); case CertificateKeyFamily.ECC: m_localNonce = Nonce.CreateNonce(SecurityPolicy); return m_localNonce.Data; From be88c47e1f8662a99435547e70ec61d9b89665fe Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 13 Feb 2026 20:45:32 +0200 Subject: [PATCH 26/42] Exclude unsuported AEAD policies from .NET Framework client tests --- Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs | 15 ++++++++++++++- .../SecurityPolicyBenchmarks.cs | 5 ++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index abc7536339..dcc0188b48 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -105,9 +105,22 @@ public void InitializeSession(ISession session) [DatapointSource] public static readonly string[] Policies = [ - .. SecurityPolicies.GetDisplayNames().Select(SecurityPolicies.GetUri) + .. GetPolicyUrisForTests() ]; + private static IEnumerable GetPolicyUrisForTests() + { + IEnumerable displayNames = SecurityPolicies.GetDisplayNames(); + +#if NETFRAMEWORK + displayNames = displayNames.Where(name => + !name.EndsWith("_AesGcm", StringComparison.Ordinal) && + !name.EndsWith("_ChaChaPoly", StringComparison.Ordinal)); +#endif + + return displayNames.Select(SecurityPolicies.GetUri); + } + /// /// Set up a Server and a Client instance. /// diff --git a/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs b/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs index a639930c67..b6bb0e83a9 100644 --- a/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs +++ b/Tests/Opc.Ua.Client.Tests/SecurityPolicyBenchmarks.cs @@ -146,13 +146,12 @@ public SecurityPolicyBenchmarks() /// /// Override to exclude None policy from benchmarks to avoid CI test failures. + /// Uses the base policy list so target-specific filtering is preserved. /// public new IEnumerable BenchPolicies() { - // Return all security policies except None - foreach (string displayName in SecurityPolicies.GetDisplayNames()) + foreach (string policyUri in Policies) { - string policyUri = SecurityPolicies.GetUri(displayName); if (policyUri != SecurityPolicies.None) { yield return policyUri; From 8ca68a88aca9a638a0441459a1fa66cb518361f1 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Sat, 14 Feb 2026 10:45:52 +0200 Subject: [PATCH 27/42] Addapt to new changes (still build fail) --- Libraries/Opc.Ua.Client/Session/Session.cs | 7 +++---- .../Certificates/CertificateIdentifier.cs | 2 +- .../Stack/Configuration/EndpointDescription.cs | 16 ++++++++-------- .../Stack/Types/X509IdentityTokenHandler.cs | 12 ++++++------ 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 001450366d..f6cae85fbb 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1326,8 +1326,7 @@ await m_configuration userTokenSignature = identityToken.Sign( dataToSign, - tokenSecurityPolicyUri, - m_telemetry); + tokenSecurityPolicyUri); } else { @@ -4009,8 +4008,8 @@ private void ValidateServerSignature( SignatureData serverSignature, byte[]? clientCertificateData, byte[]? clientCertificateChainData, - byte[] clientNonce, - byte[] serverNonce) + byte[]? clientNonce, + byte[]? serverNonce) { if (serverSignature == null || serverSignature.Signature == null) { diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 6491713a48..096e64a794 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -740,7 +740,7 @@ public static bool ValidateCertificateType( case Oids.ECDsaWithSha256: case Oids.ECDsaWithSha512: NodeId certType = CryptoUtils.GetEccCertificateTypeId(certificate); - if (certType.IsNullNodeId) + if (certType.IsNull) { return false; } diff --git a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs index 7af466800e..df561dfd8b 100644 --- a/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs +++ b/Stack/Opc.Ua.Core/Stack/Configuration/EndpointDescription.cs @@ -80,12 +80,12 @@ public UserTokenPolicy FindUserTokenPolicy(string policyId, string tokenSecurity else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } @@ -153,12 +153,12 @@ public UserTokenPolicy FindUserTokenPolicy( else if (( policy.SecurityPolicyUri != null && tokenSecurityPolicyUri != null && - EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - EccUtils.IsEccPolicy(tokenSecurityPolicyUri) + CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri) ) || ( - !EccUtils.IsEccPolicy(policy.SecurityPolicyUri) && - !EccUtils.IsEccPolicy(tokenSecurityPolicyUri))) + !CryptoUtils.IsEccPolicy(policy.SecurityPolicyUri) && + !CryptoUtils.IsEccPolicy(tokenSecurityPolicyUri))) { sameEncryptionAlgorithm ??= policy; } diff --git a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs index 45817b6cc8..beffa7ce0b 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs @@ -137,9 +137,9 @@ public SignatureData Sign( X509Certificate2 certificate = Certificate ?? CertificateFactory.Create(m_token.CertificateData); - SignatureData signatureData = SecurityPolicies.Sign( - certificate, + SignatureData signatureData = SecurityPolicies.CreateSignatureData( securityPolicyUri, + certificate, dataToSign); m_token.CertificateData = certificate.RawData; @@ -158,11 +158,11 @@ public bool Verify( X509Certificate2 certificate = Certificate ?? CertificateFactory.Create(m_token.CertificateData); - bool valid = SecurityPolicies.Verify( - certificate, + bool valid = SecurityPolicies.VerifySignatureData( + signatureData, securityPolicyUri, - dataToVerify, - signatureData); + certificate, + dataToVerify); m_token.CertificateData = certificate.RawData; From ffa9af530b02e54b58702e11701986ec04666aa3 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Sat, 14 Feb 2026 11:34:42 +0200 Subject: [PATCH 28/42] Addapt code to make it compile --- .../ConsoleReferenceClient/RunConnectAll.cs | 8 +- Libraries/Opc.Ua.Client/Session/Session.cs | 180 ++++++++++-------- .../Opc.Ua.Server/Server/StandardServer.cs | 10 +- Libraries/Opc.Ua.Server/Session/Session.cs | 1 + 4 files changed, 112 insertions(+), 87 deletions(-) diff --git a/Applications/ConsoleReferenceClient/RunConnectAll.cs b/Applications/ConsoleReferenceClient/RunConnectAll.cs index 0d0c22281e..3d296ed6eb 100644 --- a/Applications/ConsoleReferenceClient/RunConnectAll.cs +++ b/Applications/ConsoleReferenceClient/RunConnectAll.cs @@ -418,7 +418,7 @@ private void Client_ReconnectComplete(object sender, EventArgs e) private async Task BrowseFullAddressSpaceAsync( ISession session, - NodeId startingNode = null, + NodeId? startingNode = default, BrowseDescription browseDescription = null, CancellationToken ct = default) { @@ -492,7 +492,7 @@ BrowseDescriptionCollection browseDescriptionCollection { Parameters = new KeyValuePairCollection([ new Opc.Ua.KeyValuePair() { - Key = AdditionalParameterNames.Padding, + Key = QualifiedName.From(AdditionalParameterNames.Padding), Value = new Variant(padding) } ]) @@ -540,8 +540,8 @@ BrowseDescriptionCollection browseDescriptionCollection } catch (ServiceResultException sre) { - if (sre.StatusCode is StatusCodes.BadEncodingLimitsExceeded or StatusCodes - .BadResponseTooLarge) + if (sre.StatusCode == StatusCodes.BadEncodingLimitsExceeded || + sre.StatusCode == StatusCodes.BadResponseTooLarge) { // try to address by overriding operation limit maxNodesPerBrowse = diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index f6cae85fbb..7463a086c4 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1117,7 +1117,7 @@ public async Task OpenAsync( out bool requireEncryption); // validate the server certificate /certificate chain. - IUserIdentityTokenHandler identityToken = identity.TokenHandler; + using IUserIdentityTokenHandler identityToken = identity.TokenHandler.Copy(); X509Certificate2? serverCertificate = null; byte[]? certificateData = m_endpoint.Description.ServerCertificate; @@ -1292,12 +1292,9 @@ await m_configuration dataToSign); // select the security policy for the user token. - string? tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) - { - tokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; - } + string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + ? m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None + : identityPolicy.SecurityPolicyUri; // validate server nonce and security parameters for user identity. ValidateServerNonce( @@ -1312,7 +1309,7 @@ await m_configuration SignatureData? userTokenSignature = null; - if (identityToken is X509IdentityToken) + if (identityToken.Token is X509IdentityToken) { // sign data with user token. dataToSign = securityPolicy.GetUserTokenSignatureData( @@ -1354,9 +1351,9 @@ await m_configuration ActivateSessionResponse activateResponse = await ActivateSessionAsync( header, clientSignature, - null, + [], m_preferredLocales, - new ExtensionObject(identityToken), + new ExtensionObject(identityToken.Token), userTokenSignature, ct) .ConfigureAwait(false); @@ -1472,7 +1469,8 @@ public async Task UpdateSessionAsync( } // get the identity token. - string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + string securityPolicyUri = + m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); // create the client signature. @@ -1495,23 +1493,29 @@ public async Task UpdateSessionAsync( // check that the user identity is supported by the endpoint. UserTokenPolicy identityPolicy = m_endpoint.Description.FindUserTokenPolicy( - identity.TokenType, - identity.IssuedTokenType, + identity.TokenHandler.Token.PolicyId, securityPolicyUri); if (identityPolicy == null) { - throw ServiceResultException.Create( - StatusCodes.BadIdentityTokenRejected, - "Endpoint does not support the user identity type provided."); + identityPolicy = + m_endpoint.Description.FindUserTokenPolicy( + identity.TokenType, + identity.IssuedTokenType, + securityPolicyUri); + + if (identityPolicy == null) + { + throw ServiceResultException.Create( + StatusCodes.BadIdentityTokenRejected, + "Endpoint does not support the user identity type provided."); + } } // select the security policy for the user token. - string? tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) - { - tokenSecurityPolicyUri = securityPolicyUri; - } + string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + ? securityPolicyUri + : identityPolicy.SecurityPolicyUri; bool requireEncryption = tokenSecurityPolicyUri != SecurityPolicies.None; @@ -1533,38 +1537,42 @@ await m_configuration.CertificateValidator.ValidateAsync( m_previousServerNonce, m_endpoint.Description.SecurityMode); - // sign data with user token. - UserIdentityToken identityToken = identity.GetIdentityToken(); - identityToken.PolicyId = identityPolicy.PolicyId; + // sign/encrypt with a disposable token handler copy to avoid mutating stored credentials. + using IUserIdentityTokenHandler identityToken = identity.TokenHandler.Copy(); + identityToken.UpdatePolicy(identityPolicy); - dataToSign = securityPolicy.GetUserTokenSignatureData( - TransportChannel.ChannelThumbprint, - serverNonce, - m_serverCertificate?.RawData, - TransportChannel.ServerChannelCertificate, - m_instanceCertificate?.RawData, - TransportChannel.ClientChannelCertificate, - m_clientNonce ?? []); + SignatureData? userTokenSignature = null; - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri); - - m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; + if (identityToken.Token is X509IdentityToken) + { + dataToSign = securityPolicy.GetUserTokenSignatureData( + TransportChannel.ChannelThumbprint, + serverNonce, + m_serverCertificate?.RawData, + TransportChannel.ServerChannelCertificate, + m_instanceCertificate?.RawData, + TransportChannel.ClientChannelCertificate, + m_clientNonce ?? []); - // encrypt token. - identityToken.Encrypt( - m_serverCertificate, - serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri); + } + else + { + // encrypt token. + identityToken.Encrypt( + m_serverCertificate, + serverNonce, + tokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + } - // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); + m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; RequestHeader? requestHeader = CreateRequestHeaderForActivateSession( securityPolicy, @@ -2386,7 +2394,7 @@ public async Task ReconnectAsync( // await LoadInstanceCertificateAsync(true, ct).ConfigureAwait(false); - string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); EndpointDescription endpoint = m_endpoint.Description; @@ -2407,11 +2415,9 @@ public async Task ReconnectAsync( } // select the security policy for the user token. - string? tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) - { - tokenSecurityPolicyUri = endpoint.SecurityPolicyUri; - } + string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + ? endpoint.SecurityPolicyUri ?? SecurityPolicies.None + : identityPolicy.SecurityPolicyUri; m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; // validate server nonce and security parameters for user identity. @@ -2422,9 +2428,9 @@ public async Task ReconnectAsync( m_previousServerNonce, m_endpoint.Description.SecurityMode); - // sign data with user token. - UserIdentityToken identityToken = m_identity.GetIdentityToken(); - identityToken.PolicyId = identityPolicy.PolicyId; + // sign/encrypt with a disposable token handler copy to avoid mutating stored credentials. + using IUserIdentityTokenHandler identityToken = m_identity.TokenHandler.Copy(); + identityToken.UpdatePolicy(identityPolicy); m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); @@ -2533,24 +2539,27 @@ public async Task ReconnectAsync( clientChannelCertificate, m_clientNonce ?? []); - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri, - m_telemetry); - - // encrypt token. - identityToken.Encrypt( - m_serverCertificate, - m_serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + SignatureData? userTokenSignature = null; - // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates = new(); + if (identityToken.Token is X509IdentityToken) + { + userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri); + } + else + { + // encrypt token. + identityToken.Encrypt( + m_serverCertificate, + m_serverNonce, + tokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); + } m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); @@ -4902,10 +4911,10 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( string? endpointSecurityPolicyUri) { var requestHeader = new RequestHeader(); - string? userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri; + string userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri ?? string.Empty; if (string.IsNullOrEmpty(userTokenSecurityPolicyUri)) { - userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri; + userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; } m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; @@ -4916,7 +4925,11 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( { var parameters = new AdditionalParametersType(); parameters.Parameters.Add( - new KeyValuePair { Key = AdditionalParameterNames.ECDHPolicyUri, Value = userTokenSecurityPolicyUri }); + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri), + Value = userTokenSecurityPolicyUri + }); requestHeader.AdditionalHeader = new ExtensionObject(parameters); m_logger.LogWarning("Request EphemeralKey for {Policy}.", userTokenSecurityPolicyUri); @@ -4932,7 +4945,7 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( var requestHeader = new RequestHeader(); var parameters = new AdditionalParametersType(); - if (userTokenSecurityPolicyUri != null) + if (!string.IsNullOrEmpty(userTokenSecurityPolicyUri)) { var userTokenSecurityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); @@ -4941,7 +4954,7 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( parameters.Parameters.Add( new KeyValuePair { - Key = AdditionalParameterNames.ECDHPolicyUri, + Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri), Value = userTokenSecurityPolicyUri }); @@ -5019,6 +5032,13 @@ protected virtual void ProcessResponseAdditionalHeader( "Server did not provide a valid ECDHKey. User authentication not possible."); } + if (key.PublicKey == null || key.Signature == null) + { + throw new ServiceResultException( + StatusCodes.BadDecodingError, + "Server did not provide a valid ECDHKey. User authentication not possible."); + } + if (!CryptoUtils.Verify( new ArraySegment(key.PublicKey), key.Signature, diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 31ee0c948d..d7ad4f29cf 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -619,7 +619,7 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet response.Parameters.Add( new KeyValuePair { - Key = AdditionalParameterNames.ECDHKey, + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), Value = new ExtensionObject(key) }); @@ -630,7 +630,7 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet response.Parameters.Add( new KeyValuePair { - Key = AdditionalParameterNames.ECDHKey, + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), Value = StatusCodes.BadSecurityPolicyRejected }); @@ -661,7 +661,11 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam { response = new AdditionalParametersType(); response.Parameters - .Add(new KeyValuePair { Key = AdditionalParameterNames.ECDHKey, Value = new ExtensionObject(key) }); + .Add(new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = new ExtensionObject(key) + }); m_logger.LogWarning("Returning new EphmeralKey: {PublicKey}.", CryptoTrace.KeyToString(key.PublicKey)); } diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index 4af2ec4025..f2a9e4bf90 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -843,6 +843,7 @@ private ServiceResult OnUpdateSecurityDiagnostics( /// /// private IUserIdentityTokenHandler ValidateUserIdentityToken( + OperationContext context, ExtensionObject identityToken, SignatureData userTokenSignature, out UserTokenPolicy policy) From 24b041d89337a15ff13ff4b6b7839aa397e8de1b Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Mon, 16 Feb 2026 13:16:08 +0200 Subject: [PATCH 29/42] Fixed failing build on net48 (by ignoring potential null ref which is not reported on other newer targets) --- Libraries/Opc.Ua.Client/Session/Session.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 7463a086c4..0c714d0f45 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -1292,7 +1292,7 @@ await m_configuration dataToSign); // select the security policy for the user token. - string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + string? tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) ? m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None : identityPolicy.SecurityPolicyUri; @@ -1345,7 +1345,7 @@ await m_configuration m_preferredLocales = [.. preferredLocales]; } - var header = CreateRequestHeaderForActivateSession(securityPolicy, tokenSecurityPolicyUri); + var header = CreateRequestHeaderForActivateSession(securityPolicy, tokenSecurityPolicyUri!); // activate session. ActivateSessionResponse activateResponse = await ActivateSessionAsync( @@ -1513,7 +1513,7 @@ public async Task UpdateSessionAsync( } // select the security policy for the user token. - string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + string? tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) ? securityPolicyUri : identityPolicy.SecurityPolicyUri; @@ -1576,7 +1576,7 @@ await m_configuration.CertificateValidator.ValidateAsync( RequestHeader? requestHeader = CreateRequestHeaderForActivateSession( securityPolicy, - tokenSecurityPolicyUri); + tokenSecurityPolicyUri!); ActivateSessionResponse response = await ActivateSessionAsync( requestHeader, @@ -2415,7 +2415,7 @@ public async Task ReconnectAsync( } // select the security policy for the user token. - string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) + string? tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) ? endpoint.SecurityPolicyUri ?? SecurityPolicies.None : identityPolicy.SecurityPolicyUri; m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; @@ -2565,7 +2565,7 @@ public async Task ReconnectAsync( var header = CreateRequestHeaderForActivateSession( securityPolicy, - tokenSecurityPolicyUri); + tokenSecurityPolicyUri!); if (header == null) { From 8d28128e5e969dbd3f3df42982afcee83b6b040a Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Mon, 16 Feb 2026 15:35:45 +0200 Subject: [PATCH 30/42] Preserve the certificate reference in Clone() so copied handlers can still sign --- .../Stack/Types/X509IdentityTokenHandler.cs | 4 +- .../Types/X509IdentityTokenHandlerTests.cs | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs diff --git a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs index beffa7ce0b..a57f45b229 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs @@ -189,7 +189,9 @@ public object Clone() { return new X509IdentityTokenHandler(Utils.Clone(m_token)) { - // TODO: m_certificate = m_certificate + // Keep the in-memory certificate instance so private key operations + // continue to work when cloned handlers are used for signing. + Certificate = m_certificate }; } diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs new file mode 100644 index 0000000000..7563695e09 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Stack/Types/X509IdentityTokenHandlerTests.cs @@ -0,0 +1,66 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.Security.Cryptography.X509Certificates; +using NUnit.Framework; +using Opc.Ua.Security.Certificates; +using Assert = NUnit.Framework.Legacy.ClassicAssert; + +namespace Opc.Ua.Core.Tests.Stack.Types +{ + [TestFixture] + [Category("X509IdentityTokenHandler")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [Parallelizable] + public class X509IdentityTokenHandlerTests + { + [Test] + public void CopyPreservesPrivateKeyForSigning() + { + using X509Certificate2 cert = CertificateBuilder + .Create("CN=User Identity Test Subject, O=OPC Foundation") + .SetRSAKeySize(2048) + .CreateForRSA(); + + using var tokenHandler = new X509IdentityTokenHandler(cert); + using X509IdentityTokenHandler copy = tokenHandler.Copy(); + + Assert.IsTrue(copy.Certificate.HasPrivateKey); + + SignatureData signature = copy.Sign( + [0x01, 0x02, 0x03, 0x04], + SecurityPolicies.Basic256Sha256); + + Assert.NotNull(signature); + Assert.NotNull(signature.Signature); + Assert.Greater(signature.Signature.Length, 0); + } + } +} From aae135fb007395c8ef5c5002ac80309820b21459 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Mon, 16 Feb 2026 20:38:31 +0200 Subject: [PATCH 31/42] Remove extra code and addapt to existing master configuration settings --- .../ConsoleReferenceClient/Program.cs | 29 +- .../Quickstarts.ReferenceClient.Config.xml | 24 +- .../ConsoleReferenceClient/RunConnectAll.cs | 666 ------------------ .../generate_user_certificate.ps1 | 83 --- .../ConsoleReferenceServer/Program.cs | 14 +- .../Quickstarts.ReferenceServer.Config.xml | 114 ++- .../ReferenceServer/ReferenceServer.cs | 30 +- 7 files changed, 78 insertions(+), 882 deletions(-) delete mode 100644 Applications/ConsoleReferenceClient/RunConnectAll.cs delete mode 100644 Applications/ConsoleReferenceClient/generate_user_certificate.ps1 diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs index ae5a06c258..de85fe079d 100644 --- a/Applications/ConsoleReferenceClient/Program.cs +++ b/Applications/ConsoleReferenceClient/Program.cs @@ -75,8 +75,8 @@ public static async Task Main(string[] args) byte[] userpassword = null; string userCertificateThumbprint = null; byte[] userCertificatePassword = null; - bool logConsole = true; - bool appLog = true; + bool logConsole = false; + bool appLog = false; bool fileLog = false; bool renewCertificate = false; bool loadTypes = false; @@ -94,7 +94,6 @@ public static async Task Main(string[] args) bool leakChannels = false; bool forever = false; bool enableDurableSubscriptions = false; - bool connectAllEndpointDescriptions = true; bool exportNodes = false; var options = new Mono.Options.OptionSet @@ -277,17 +276,6 @@ public static async Task Main(string[] args) enableDurableSubscriptions = true; } } - }, - { - "ca|connectall", - "Connects using all published EndpointDescriptions.", - ca => - { - if (ca != null) - { - connectAllEndpointDescriptions = true; - } - } } }; @@ -357,7 +345,7 @@ public static async Task Main(string[] args) logConsole, fileLog, appLog, - LogLevel.Warning); + LogLevel.Information); // delete old certificate if (renewCertificate) @@ -392,17 +380,6 @@ await application.DeleteApplicationInstanceCertificateAsync() CancellationToken ct = quitCTS.Token; ManualResetEvent quitEvent = ConsoleUtils.CtrlCHandler(quitCTS); - // handle connect all endpoints test. - if (connectAllEndpointDescriptions) - { - var tester = new SecurityTestClient.RunConnectAll(config, telemetry); - - if (await tester.RunAsync(quitEvent, ct).ConfigureAwait(false)) - { - return; - } - } - var userIdentity = new UserIdentity(); // set user identity of type username/pw diff --git a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml index f067984d1d..aa3a8ae264 100644 --- a/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml +++ b/Applications/ConsoleReferenceClient/Quickstarts.ReferenceClient.Config.xml @@ -13,35 +13,35 @@ Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference client, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Client, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -49,17 +49,17 @@ Directory - ../../pki/issuer + %LocalApplicationData%/OPC Foundation/pki/issuer Directory - ../../pki/trusted + %LocalApplicationData%/OPC Foundation/pki/trusted Directory - ../../pki/rejected + %LocalApplicationData%/OPC Foundation/pki/rejected 5 Directory - ../../pki/userIssuer + %LocalApplicationData%/OPC Foundation/pki/userIssuer Directory - ../../pki/trustedUser + %LocalApplicationData%/OPC Foundation/pki/trustedUser @@ -92,7 +92,7 @@ 4194304 65535 300000 - 30000 + 3600000 60000 @@ -120,7 +120,7 @@ - ./Logs/Quickstarts.ReferenceClient.log.txt + %LocalApplicationData%/OPC Foundation/Logs/Quickstarts.ReferenceClient.log.txt true diff --git a/Applications/ConsoleReferenceClient/RunConnectAll.cs b/Applications/ConsoleReferenceClient/RunConnectAll.cs deleted file mode 100644 index 3d296ed6eb..0000000000 --- a/Applications/ConsoleReferenceClient/RunConnectAll.cs +++ /dev/null @@ -1,666 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Opc.Ua; -using Opc.Ua.Client; -using Opc.Ua.Configuration; - -namespace SecurityTestClient -{ - internal sealed class RunConnectAll - { - private readonly Lock m_lock = new(); - private SessionReconnectHandler m_reconnectHandler; - private ILogger m_logger; - private ITelemetryContext m_context; - private ApplicationConfiguration m_configuration; - private ISession m_session; - - const string ServerUrl = "opc.tcp://localhost:62541"; - //const string ServerUrl = "opc.tcp://WhiteCat:4880/Softing/OpcUa/TestServer"; - const int kMaxSearchDepth = 128; - const int ReconnectPeriod = 1000; - const int ReconnectPeriodExponentialBackoff = 15000; - - public RunConnectAll(ApplicationConfiguration configuration, ITelemetryContext context) - { - m_context = context; - m_configuration = configuration; - m_logger = context.CreateLogger("Test"); - - m_reconnectHandler = new SessionReconnectHandler( - context, - true, - ReconnectPeriodExponentialBackoff); - } - - private string GetUserCertificateFile(string securityPolicyUri) - { - var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); - - switch (securityPolicy.CertificateKeyAlgorithm) - { - default: - case CertificateKeyAlgorithm.RSA: - case CertificateKeyAlgorithm.RSADH: - return $"iama.tester.rsa.der"; - case CertificateKeyAlgorithm.BrainpoolP256r1: - return $"iama.tester.brainpoolP256r1.der"; - case CertificateKeyAlgorithm.BrainpoolP384r1: - return $"iama.tester.brainpoolP384r1.der"; - case CertificateKeyAlgorithm.NistP256: - return $"iama.tester.nistP256.der"; - case CertificateKeyAlgorithm.NistP384: - return $"iama.tester.nistP384.der"; - } - } - - public async Task RunAsync(ManualResetEvent quitEvent, CancellationToken ct) - { - try - { - m_logger.LogInformation("OPC UA Security Test Client"); - - // The application name and config file names - const string applicationName = "ConsoleReferenceClient"; - const string configSectionName = "Quickstarts.ReferenceClient"; - - // Define the UA Client application - var passwordProvider = new CertificatePasswordProvider([]); - - var application = new ApplicationInstance(m_context) - { - ApplicationName = applicationName, - ApplicationType = ApplicationType.Client, - ConfigSectionName = configSectionName, - CertificatePasswordProvider = passwordProvider - }; - - // load the application configuration. - var configuration = m_configuration = await application - .LoadApplicationConfigurationAsync(silent: false, ct: ct) - .ConfigureAwait(false); - - m_configuration.CertificateValidator.CertificateValidation += CertificateValidation; - - // check the application certificate. - bool haveAppCertificate = await application - .CheckApplicationInstanceCertificatesAsync(false, ct: ct) - .ConfigureAwait(false); - - if (!haveAppCertificate) - { - throw new InvalidOperationException("Application instance certificate invalid!"); - } - - m_logger.LogInformation("Connecting to... {ServerUrl}", ServerUrl); - - var endpoints = await GetEndpoints( - m_configuration, - ServerUrl, - ct).ConfigureAwait(false); - - //endpoints = endpoints.Where(x => x.SecurityPolicyUri == SecurityPolicies.RSA_DH_ChaChaPoly).ToList(); - - var endpointConfiguration = EndpointConfiguration.Create(m_configuration); - var sessionFactory = new DefaultSessionFactory(m_context); - var userNameidentity = new UserIdentity("sysadmin", new UTF8Encoding(false).GetBytes("demo")); - //var userNameidentity = new UserIdentity("usr", new UTF8Encoding(false).GetBytes("pwd")); - - foreach (var ii in endpoints) - { - var userCertificateFile = GetUserCertificateFile(ii.SecurityPolicyUri); - var x509 = X509CertificateLoader.LoadCertificateFromFile(Path.Combine("..\\..\\pki\\trustedUser\\certs", userCertificateFile)); - var thumbprint = x509.Thumbprint; - - var certificateIdentity = await LoadUserCertificateAsync(thumbprint, "password", ct).ConfigureAwait(false); - - foreach (var identity in new UserIdentity[] { userNameidentity, certificateIdentity }) - { - try - { - m_logger.LogWarning("{Line}", new string('=', 80)); - - m_logger.LogWarning( - "SECURITY-POLICY={SecurityPolicyUri} {SecurityMode}", - SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri), - ii.SecurityMode); - - m_logger.LogWarning( - "IDENTITY={DisplayName} {TokenType}", - identity.DisplayName, - identity.TokenType); - - ISession session = await RunTestAsync( - endpointConfiguration, - sessionFactory, - ii, - identity, - ct).ConfigureAwait(false); - - m_logger.LogWarning("Waiting for SecureChannel renew"); - await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false); - - for (int count = 0; count < 8; count++) - { - var result = await session.ReadAsync( - null, - 0, - TimestampsToReturn.Neither, - new ReadValueIdCollection() - { - new ReadValueId() - { - NodeId = Opc.Ua.VariableIds.Server_ServerStatus_CurrentTime, - AttributeId = Attributes.Value - } - }, - ct).ConfigureAwait(false); - - m_logger.LogWarning( - "CurrentTime: {CurrentTime}", - result.Results[0].GetValueOrDefault().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture)); - - await Task.Delay(5000, ct).ConfigureAwait(false); - } - - await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false); - - await session.CloseAsync(true, ct: ct).ConfigureAwait(false); - } - catch (Exception e) - { - Console.WriteLine("Exception: {0}", e.Message); - Console.WriteLine("StackTrace: {0}", e.StackTrace); - quitEvent.WaitOne(20000); - } - - m_logger.LogWarning( - "TEST COMPLETE: {SecurityPolicyUri} {SecurityMode}", - SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri), - ii.SecurityMode); - - m_logger.LogWarning("{Line}", new string('=', 80)); - //break; - } - - //break; - } - - Console.WriteLine("Ctrl-C to stop."); - quitEvent.WaitOne(); - } - catch (Exception e) - { - m_logger.LogError("Exception: {Message}", e.Message); - m_logger.LogTrace("StackTrace: {StackTrace}", e.StackTrace); - } - - return true; - } - - private async Task LoadUserCertificateAsync(string thumbprint, string password, CancellationToken ct) - { -#if NET8_0_OR_GREATER - var store = m_configuration.SecurityConfiguration.TrustedUserCertificates; - - // get user certificate with matching thumbprint - var hit = ( - await store.GetCertificatesAsync(m_context, ct).ConfigureAwait(false) - ).Find(X509FindType.FindByThumbprint, thumbprint, false).FirstOrDefault(); - - // create Certificate Identifier - var cid = new CertificateIdentifier(hit) - { - StorePath = store.StorePath, - StoreType = store.StoreType - }; - - return await UserIdentity.CreateAsync( - cid, - new CertificatePasswordProvider(new UTF8Encoding(false).GetBytes(password)), - m_context, - ct - ).ConfigureAwait(false); -#else - await Task.Delay(1, ct).ConfigureAwait(false); - throw new NotSupportedException("User certificate identity is only supported on .NET 8 or greater."); -#endif - } - - public async Task RunTestAsync( - EndpointConfiguration endpointConfiguration, - DefaultSessionFactory sessionFactory, - EndpointDescription endpointDescription, - UserIdentity identity, - CancellationToken ct) - { - var endpoint = new ConfiguredEndpoint( - null, - endpointDescription, - endpointConfiguration); - - // Create the session - ISession session = await sessionFactory - .CreateAsync( - m_configuration, - endpoint, - false, - false, - m_configuration.ApplicationName, - 600000, - //new UserIdentity(), - (endpointDescription.SecurityMode != MessageSecurityMode.None) ? identity : new UserIdentity(), - null, - ct - ) - .ConfigureAwait(false); - - // Assign the created session - if (session == null || !session.Connected) - { - throw new InvalidOperationException("Could not connect to server at " + ServerUrl); - } - - session.KeepAliveInterval = 10000; - session.KeepAlive += Session_KeepAlive; - - var nodes = await BrowseFullAddressSpaceAsync( - session, - ObjectIds.ObjectsFolder, - null, - ct).ConfigureAwait(false); - - return session; - } - private async ValueTask> GetEndpoints( - ApplicationConfiguration application, - string discoveryUrl, - CancellationToken ct = default) - { - var endpointConfiguration = EndpointConfiguration.Create(application); - - using var client = await DiscoveryClient.CreateAsync( - application, - new Uri(discoveryUrl), - endpointConfiguration, - ct: ct).ConfigureAwait(false); - - return await client.GetEndpointsAsync(null, ct).ConfigureAwait(false); - } - - private void CertificateValidation( - CertificateValidator sender, - CertificateValidationEventArgs e) - { - bool certificateAccepted = false; - - // **** - // Implement a custom logic to decide if the certificate should be - // accepted or not and set certificateAccepted flag accordingly. - // The certificate can be retrieved from the e.Certificate field - // *** - - ServiceResult error = e.Error; - m_logger.LogInformation("{ServiceResult}", error); - if (error.StatusCode == StatusCodes.BadCertificateUntrusted) - { - certificateAccepted = true; - } - - if (certificateAccepted) - { - m_logger.LogInformation( - "Untrusted Certificate accepted. Subject = {Subject}", - e.Certificate.Subject); - e.Accept = true; - } - else - { - m_logger.LogInformation( - "Untrusted Certificate rejected. Subject = {Subject}", - e.Certificate.Subject); - } - } - - private void Session_KeepAlive(ISession session, KeepAliveEventArgs e) - { - try - { - // check for events from discarded sessions. - if (m_session == null || !m_session.Equals(session)) - { - return; - } - - // start reconnect sequence on communication error. - if (ServiceResult.IsBad(e.Status)) - { - SessionReconnectHandler.ReconnectState state = m_reconnectHandler - .BeginReconnect( - m_session, - null, - ReconnectPeriod, - Client_ReconnectComplete); - - if (state == SessionReconnectHandler.ReconnectState.Triggered) - { - m_logger.LogInformation( - "KeepAlive status {Status}, reconnect status {State}, reconnect period {ReconnectPeriod}ms.", - e.Status, - state, - ReconnectPeriod - ); - } - else - { - m_logger.LogInformation( - "KeepAlive status {Status}, reconnect status {State}.", - e.Status, - state); - } - - // cancel sending a new keep alive request, because reconnect is triggered. - e.CancelKeepAlive = true; - } - } - catch (Exception exception) - { - m_logger.LogError(exception, "Error in OnKeepAlive."); - } - } - private void Client_ReconnectComplete(object sender, EventArgs e) - { - // ignore callbacks from discarded objects. - if (!ReferenceEquals(sender, m_reconnectHandler)) - { - return; - } - - lock (m_lock) - { - // if session recovered, Session property is null - if (m_reconnectHandler.Session != null) - { - // ensure only a new instance is disposed - // after reactivate, the same session instance may be returned - if (!ReferenceEquals(m_session, m_reconnectHandler.Session)) - { - m_logger.LogInformation( - "--- RECONNECTED TO NEW SESSION --- {SessionId}", - m_reconnectHandler.Session.SessionId - ); - ISession session = m_session; - m_session = m_reconnectHandler.Session; - Utils.SilentDispose(session); - } - else - { - m_logger.LogInformation( - "--- REACTIVATED SESSION --- {SessionId}", - m_reconnectHandler.Session.SessionId); - } - } - else - { - m_logger.LogInformation("--- RECONNECT KeepAlive recovered ---"); - } - } - } - - private async Task BrowseFullAddressSpaceAsync( - ISession session, - NodeId? startingNode = default, - BrowseDescription browseDescription = null, - CancellationToken ct = default) - { - var stopWatch = new Stopwatch(); - stopWatch.Start(); - - // Browse template - const int kMaxReferencesPerNode = 1000; - BrowseDescription browseTemplate = - browseDescription - ?? new BrowseDescription - { - NodeId = startingNode ?? ObjectIds.RootFolder, - BrowseDirection = BrowseDirection.Forward, - ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, - IncludeSubtypes = true, - NodeClassMask = 0, - ResultMask = (uint)BrowseResultMask.All - }; - BrowseDescriptionCollection browseDescriptionCollection - = CreateBrowseDescriptionCollectionFromNodeId( - [.. new NodeId[] { startingNode ?? ObjectIds.RootFolder }], - browseTemplate); - - // Browse - var referenceDescriptions = new Dictionary(); - var random = new Random(11211); // use a fixed seed for test reproducibility - - int searchDepth = 0; - uint maxNodesPerBrowse = session.OperationLimits.MaxNodesPerBrowse; - while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth) - { - searchDepth++; - m_logger.LogInformation( - "{SearchDepth}: Browse {Count} nodes after {ElapsedMilliseconds}ms", - searchDepth, - browseDescriptionCollection.Count, - stopWatch.ElapsedMilliseconds); - - var allBrowseResults = new BrowseResultCollection(); - bool repeatBrowse; - var browseResultCollection = new BrowseResultCollection(); - var unprocessedOperations = new BrowseDescriptionCollection(); - DiagnosticInfoCollection diagnosticsInfoCollection; - do - { - BrowseDescriptionCollection browseCollection = - maxNodesPerBrowse == 0 - ? browseDescriptionCollection - : browseDescriptionCollection.Take((int)maxNodesPerBrowse).ToArray(); - repeatBrowse = false; - try - { - RequestHeader requestHeader = null; - - // a random pattern to obscure the message size - // (only useful for application running over untrusted networks). - if (session.ConfiguredEndpoint.Description.SecurityMode == MessageSecurityMode.SignAndEncrypt) - { - // a real application needs to use secure randomness -#pragma warning disable CA5394 // Do not use insecure randomness - var padding = new byte[random.Next() % 128]; - random.NextBytes(padding); -#pragma warning restore CA5394 // Do not use insecure randomness - - m_logger.LogWarning("Sending Padding with {Length} Bytes", padding.Length); - - requestHeader = new RequestHeader - { - AdditionalHeader = new ExtensionObject(new Opc.Ua.AdditionalParametersType() - { - Parameters = new KeyValuePairCollection([ - new Opc.Ua.KeyValuePair() { - Key = QualifiedName.From(AdditionalParameterNames.Padding), - Value = new Variant(padding) - } - ]) - }) - }; - } - - BrowseResponse browseResponse = await - session.BrowseAsync( - requestHeader, - null, - kMaxReferencesPerNode, - browseCollection, - ct) - .ConfigureAwait(false); - browseResultCollection = browseResponse.Results; - diagnosticsInfoCollection = browseResponse.DiagnosticInfos; - ClientBase.ValidateResponse(browseResultCollection, browseCollection); - ClientBase.ValidateDiagnosticInfos( - diagnosticsInfoCollection, - browseCollection); - - // separate unprocessed nodes for later - int ii = 0; - foreach (BrowseResult browseResult in browseResultCollection) - { - // check for error. - StatusCode statusCode = browseResult.StatusCode; - if (StatusCode.IsBad(statusCode)) - { - // this error indicates that the server does not have enough simultaneously active - // continuation points. This request will need to be resent after the other operations - // have been completed and their continuation points released. - if (statusCode == StatusCodes.BadNoContinuationPoints) - { - unprocessedOperations.Add(browseCollection[ii++]); - continue; - } - } - - // save results. - allBrowseResults.Add(browseResult); - ii++; - } - } - catch (ServiceResultException sre) - { - if (sre.StatusCode == StatusCodes.BadEncodingLimitsExceeded || - sre.StatusCode == StatusCodes.BadResponseTooLarge) - { - // try to address by overriding operation limit - maxNodesPerBrowse = - maxNodesPerBrowse == 0 - ? (uint)browseCollection.Count / 2 - : maxNodesPerBrowse / 2; - repeatBrowse = true; - } - else - { - m_logger.LogError("Browse error: {Message}", sre.Message); - throw; - } - } - } while (repeatBrowse); - - if (maxNodesPerBrowse == 0) - { - browseDescriptionCollection.Clear(); - } - else - { - browseDescriptionCollection = browseDescriptionCollection - .Skip(browseResultCollection.Count) - .ToArray(); - } - - // Browse next - ByteStringCollection continuationPoints = PrepareBrowseNext(browseResultCollection); - while (continuationPoints.Count > 0) - { - m_logger.LogInformation("BrowseNext {Count} continuation points.", continuationPoints.Count); - BrowseNextResponse browseNextResult = await - session.BrowseNextAsync(null, false, continuationPoints, ct) - .ConfigureAwait(false); - BrowseResultCollection browseNextResultCollection = browseNextResult.Results; - diagnosticsInfoCollection = browseNextResult.DiagnosticInfos; - ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints); - ClientBase.ValidateDiagnosticInfos( - diagnosticsInfoCollection, - continuationPoints); - allBrowseResults.AddRange(browseNextResultCollection); - continuationPoints = PrepareBrowseNext(browseNextResultCollection); - } - - // Build browse request for next level - var browseTable = new NodeIdCollection(); - int duplicates = 0; - foreach (BrowseResult browseResult in allBrowseResults) - { - foreach (ReferenceDescription reference in browseResult.References) - { - if (!referenceDescriptions.ContainsKey(reference.NodeId)) - { - referenceDescriptions[reference.NodeId] = reference; - if (reference.ReferenceTypeId != ReferenceTypeIds.HasProperty) - { - browseTable.Add( - ExpandedNodeId.ToNodeId( - reference.NodeId, - session.NamespaceUris)); - } - } - else - { - duplicates++; - } - } - } - if (duplicates > 0) - { - m_logger.LogInformation("Browse Result {Duplicates} duplicate nodes were ignored.", duplicates); - } - browseDescriptionCollection.AddRange( - CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate)); - - // add unprocessed nodes if any - browseDescriptionCollection.AddRange(unprocessedOperations); - } - - stopWatch.Stop(); - - var result = new ReferenceDescriptionCollection(referenceDescriptions.Values); - result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); - - m_logger.LogWarning( - "BrowseFullAddressSpace found {Count} references on server in {ElapsedMilliseconds}ms.", - referenceDescriptions.Count, - stopWatch.ElapsedMilliseconds); - - return result; - } - - private static BrowseDescriptionCollection CreateBrowseDescriptionCollectionFromNodeId( - NodeIdCollection nodeIdCollection, - BrowseDescription template) - { - var browseDescriptionCollection = new BrowseDescriptionCollection(); - foreach (NodeId nodeId in nodeIdCollection) - { - var browseDescription = (BrowseDescription)template.MemberwiseClone(); - browseDescription.NodeId = nodeId; - browseDescriptionCollection.Add(browseDescription); - } - return browseDescriptionCollection; - } - - private static ByteStringCollection PrepareBrowseNext( - BrowseResultCollection browseResultCollection) - { - var continuationPoints = new ByteStringCollection(); - foreach (BrowseResult browseResult in browseResultCollection) - { - if (browseResult.ContinuationPoint != null) - { - continuationPoints.Add(browseResult.ContinuationPoint); - } - } - return continuationPoints; - } - } -} diff --git a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 b/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 deleted file mode 100644 index ee938f5431..0000000000 --- a/Applications/ConsoleReferenceClient/generate_user_certificate.ps1 +++ /dev/null @@ -1,83 +0,0 @@ -# 1. Ensure directories exist -$certDir = "./bin/pki/trustedUser/certs" -$privateDir = "./bin/pki/trustedUser/private" - -$curves = @( - 'nistP256', - 'nistP384', - 'brainpoolP256r1', - 'brainpoolP384r1' -) - -foreach ($d in @($certDir, $privateDir)) { - if (-not (Test-Path $d)) { - New-Item -ItemType Directory -Path $d -Force | Out-Null - } -} - -# 2. Create a self-signed ECC certificate (NIST P-256) -# $cert = New-SelfSignedCertificate ` -# -Subject "CN=iama.tester@example.com" ` -# -CertStoreLocation "Cert:\CurrentUser\My" ` -# -KeyExportPolicy Exportable ` -# -KeySpec Signature ` -# -KeyAlgorithm ECDSA_nistP256 ` -# -Curve 'CurveName' ` -# -HashAlgorithm SHA256 ` -# -NotAfter (Get-Date).AddYears(1) - -foreach ($curve in $curves) { - - Write-Host "Generating certificate for curve: $curve" - - $signatureAlgorithm = if ($curve -match 'P384') { 'SHA384' } else { 'SHA256' } - - # Create certificate parameters and dynamically insert the curve - $params = @{ - Type = 'Custom' - Subject = 'CN=iama.tester@example.com' - TextExtension = @( - '2.5.29.37={text}1.3.6.1.5.5.7.3.2' - '2.5.29.17={text}upn=iama.tester@example.com' - ) - KeyUsage = @('DigitalSignature', 'NonRepudiation') - KeyAlgorithm = "ECDSA_$curve" # <-- dynamic! - CurveExport = 'CurveName' - HashAlgorithm = $signatureAlgorithm - CertStoreLocation = 'Cert:\CurrentUser\My' - } - - # 1. Create cert - $cert = New-SelfSignedCertificate @params - - # 2. Export DER - $derPath = Join-Path $certDir "iama.tester.$curve.der" - Export-Certificate -Cert $cert -FilePath $derPath -Type CERT - - # 3. Export PFX with password - $secret = ConvertTo-SecureString -String "password" -Force -AsPlainText - $pfxPath = Join-Path $privateDir "iama.tester.$curve.pfx" - Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $secret - - Write-Host "Finished: $curve`n" -} - -Write-Host "`n=== Generating RSA-2048 ===" - -$rsaParams = @{ - Type = 'Custom' - Subject = 'CN=iama.tester@example.com' - TextExtension = @( - '2.5.29.37={text}1.3.6.1.5.5.7.3.2' - '2.5.29.17={text}upn=iama.tester@example.com' - ) - KeyUsage = @('DigitalSignature','DataEncipherment','NonRepudiation','KeyEncipherment') - KeyAlgorithm = 'RSA' - KeyLength = 2048 - CertStoreLocation = 'Cert:\CurrentUser\My' -} - -$rsaCert = New-SelfSignedCertificate @rsaParams - -Export-Certificate -Cert $rsaCert -FilePath (Join-Path $certDir "iama.tester.rsa.der") -Type CERT -Export-PfxCertificate -Cert $rsaCert -FilePath (Join-Path $privateDir "iama.tester.rsa.pfx") -Password $secret diff --git a/Applications/ConsoleReferenceServer/Program.cs b/Applications/ConsoleReferenceServer/Program.cs index 5a5dd41f1d..a33e7577c7 100644 --- a/Applications/ConsoleReferenceServer/Program.cs +++ b/Applications/ConsoleReferenceServer/Program.cs @@ -62,9 +62,9 @@ public static async Task Main(string[] args) // command line options bool showHelp = false; - bool autoAccept = true; - bool logConsole = true; - bool appLog = true; + bool autoAccept = false; + bool logConsole = false; + bool appLog = false; bool fileLog = false; bool renewCertificate = false; bool shadowConfig = false; @@ -160,13 +160,7 @@ await server } // setup the logging - telemetry.ConfigureLogging( - server.Configuration, - applicationName, - logConsole, - fileLog, - appLog, - LogLevel.Warning); + telemetry.ConfigureLogging(server.Configuration, applicationName, logConsole, fileLog, appLog, LogLevel.Information); // check or renew the certificate Console.WriteLine("Check the certificate."); diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 2f60e9cb95..158c6a601e 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -14,35 +14,35 @@ Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost RsaSha256 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP256 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost NistP384 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP256r1 Directory - ../../pki/own + %LocalApplicationData%/OPC Foundation/pki/own CN=Quickstart Reference Server, C=US, S=Arizona, O=OPC Foundation, DC=localhost BrainpoolP384r1 @@ -51,17 +51,17 @@ Directory - ../../pki/issuer + %LocalApplicationData%/OPC Foundation/pki/issuer Directory - ../../pki/trusted + %LocalApplicationData%/OPC Foundation/pki/trusted Directory - ../../pki/rejected + %LocalApplicationData%/OPC Foundation/pki/rejected 5 Directory - ../../pki/issuerUser + %LocalApplicationData%/OPC Foundation/pki/issuerUser Directory - ../../pki/trustedUser + %LocalApplicationData%/OPC Foundation/pki/trustedUser @@ -94,11 +94,11 @@ 4194304 65535 30000 - 30000 + 3600000 - + opc.https://localhost:62540/Quickstarts/ReferenceServer opc.tcp://localhost:62541/Quickstarts/ReferenceServer @@ -122,7 +122,6 @@ --> - Sign_2 http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm @@ -176,68 +198,20 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#RSA_DH_AesGcm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - None_1 - http://opcfoundation.org/UA/SecurityPolicy#None - 5 100 @@ -396,7 +370,7 @@ - ./Logs/Quickstarts.ReferenceServer.log.txt + %LocalApplicationData%/OPC Foundation/Logs/Quickstarts.ReferenceServer.log.txt true diff --git a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs index 2089c14718..538760be6d 100644 --- a/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs +++ b/Applications/Quickstarts.Servers/ReferenceServer/ReferenceServer.cs @@ -254,21 +254,21 @@ public override UserTokenPolicyCollection GetUserTokenPolicies( } // sample how to modify default user token policies - //if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss && - // description.SecurityMode == MessageSecurityMode.SignAndEncrypt) - //{ - // return [.. policies.Where(u => u.TokenType != UserTokenType.Certificate)]; - //} - //else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && - // description.SecurityMode == MessageSecurityMode.Sign) - //{ - // return [.. policies.Where(u => u.TokenType != UserTokenType.Anonymous)]; - //} - //else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && - // description.SecurityMode == MessageSecurityMode.SignAndEncrypt) - //{ - // return [.. policies.Where(u => u.TokenType != UserTokenType.UserName)]; - //} + if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss && + description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + return [.. policies.Where(u => u.TokenType != UserTokenType.Certificate)]; + } + else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && + description.SecurityMode == MessageSecurityMode.Sign) + { + return [.. policies.Where(u => u.TokenType != UserTokenType.Anonymous)]; + } + else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && + description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + return [.. policies.Where(u => u.TokenType != UserTokenType.UserName)]; + } return policies; } From da52de840c514ba283172b746241926e60b87b39 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Mon, 16 Feb 2026 21:02:03 +0200 Subject: [PATCH 32/42] Filter *_AesGcm and *_ChaChaPoly security policies based on actual runtime support, so unsupported AEAD policies are not advertised or selected on older frameworks --- .../Security/Constants/SecurityPolicies.cs | 108 ++++++++++++++---- 1 file changed, 88 insertions(+), 20 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index e4707e4c72..fea313c1c7 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -188,6 +188,24 @@ public static class SecurityPolicies /// public const string Https = BaseUri + "Https"; + private static bool SupportsAesGcmPolicy() + { +#if NET8_0_OR_GREATER + return AesGcm.IsSupported; +#else + return false; +#endif + } + + private static bool SupportsChaCha20Poly1305Policy() + { +#if NET8_0_OR_GREATER + return ChaCha20Poly1305.IsSupported; +#else + return false; +#endif + } + private static bool IsPlatformSupportedName(string name) { // If name contains BaseUri trim the BaseUri part @@ -201,56 +219,106 @@ private static bool IsPlatformSupportedName(string name) name.Equals(nameof(Basic256), StringComparison.Ordinal) || name.Equals(nameof(Basic128Rsa15), StringComparison.Ordinal) || name.Equals(nameof(Basic256Sha256), StringComparison.Ordinal) || - name.Equals(nameof(Aes128_Sha256_RsaOaep), StringComparison.Ordinal) || - name.Equals(nameof(RSA_DH_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(RSA_DH_ChaChaPoly), StringComparison.Ordinal)) + name.Equals(nameof(Aes128_Sha256_RsaOaep), StringComparison.Ordinal)) { return true; } + if (name.Equals(nameof(RSA_DH_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy(); + } + + if (name.Equals(nameof(RSA_DH_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy(); + } + if (name.Equals(nameof(Aes256_Sha256_RsaPss), StringComparison.Ordinal) && RsaUtils.IsSupportingRSAPssSign.Value) { return true; } - if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal) || - name.Equals(nameof(ECC_nistP256_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(ECC_nistP256_ChaChaPoly), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP256), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP256ApplicationCertificateType); } - if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal) || - name.Equals(nameof(ECC_nistP384_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(ECC_nistP384_ChaChaPoly), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP256_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP256ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_nistP256_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP256ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_nistP384), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccNistP384ApplicationCertificateType); } - if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal) || - name.Equals(nameof(ECC_brainpoolP256r1_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(ECC_brainpoolP256r1_ChaChaPoly), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_nistP384_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP384ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_nistP384_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccNistP384ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP256r1), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); } - if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal) || - name.Equals(nameof(ECC_brainpoolP384r1_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(ECC_brainpoolP384r1_ChaChaPoly), StringComparison.Ordinal)) + if (name.Equals(nameof(ECC_brainpoolP256r1_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP256r1_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP384r1), StringComparison.Ordinal)) { return Utils.IsSupportedCertificateType( ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); } + if (name.Equals(nameof(ECC_brainpoolP384r1_AesGcm), StringComparison.Ordinal)) + { + return SupportsAesGcmPolicy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + } + if (name.Equals(nameof(ECC_brainpoolP384r1_ChaChaPoly), StringComparison.Ordinal)) + { + return SupportsChaCha20Poly1305Policy() && + Utils.IsSupportedCertificateType(ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + } if (name.Equals(nameof(ECC_curve25519), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve25519_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve25519_ChaChaPoly), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve448), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve448_AesGcm), StringComparison.Ordinal) || - name.Equals(nameof(ECC_curve448_ChaChaPoly), StringComparison.Ordinal)) + name.Equals(nameof(ECC_curve448), StringComparison.Ordinal)) { #if CURVE25519 return true; +#endif + } + if (name.Equals(nameof(ECC_curve25519_AesGcm), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_AesGcm), StringComparison.Ordinal)) + { +#if CURVE25519 + return SupportsAesGcmPolicy(); +#endif + } + if (name.Equals(nameof(ECC_curve25519_ChaChaPoly), StringComparison.Ordinal) || + name.Equals(nameof(ECC_curve448_ChaChaPoly), StringComparison.Ordinal)) + { +#if CURVE25519 + return SupportsChaCha20Poly1305Policy(); #endif } return false; From 6637a82b9c9cbf04cdc44faeff0a23bb2aa2b096 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 18 Feb 2026 12:21:45 +0200 Subject: [PATCH 33/42] Expand ECC/RSA policy test coverage and keep Basic128Rsa15 nonce length backward-compatible --- .../Security/Certificates/Nonce.cs | 12 +- Tests/Opc.Ua.Client.Tests/ClientTest.cs | 212 ++++++++++-------- .../ClientTestFramework.cs | 109 +++------ Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs | 32 ++- .../Types/Nonce/NonceTests.cs | 71 ++---- Tests/Opc.Ua.Gds.Tests/PushTest.cs | 84 ++++++- 6 files changed, 294 insertions(+), 226 deletions(-) diff --git a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs index aa982a2bfc..53e4b220d3 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/Nonce.cs @@ -245,7 +245,17 @@ public static Nonce CreateNonce(SecurityPolicyInfo securityPolicy) case CertificateKeyAlgorithm.BrainpoolP384r1: return CreateNonce(ECCurve.NamedCurves.brainpoolP384r1); default: - return new Nonce { Data = CreateRandomNonceData(securityPolicy.SecureChannelNonceLength) }; + // Basic128Rsa15 keeps the legacy 16-byte nonce for compatibility. + bool enforceMinimumLength = !string.Equals( + securityPolicy.Uri, + SecurityPolicies.Basic128Rsa15, + StringComparison.Ordinal); + return new Nonce + { + Data = CreateRandomNonceData( + securityPolicy.SecureChannelNonceLength, + enforceMinimumLength) + }; } } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 815c63a970..2ebee15454 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -79,6 +79,31 @@ public ClientTest(string uriScheme = Utils.UriSchemeOpcTcp) ObjectIds.XmlSchema_TypeSystem ]; + public static readonly string[] SupportedEccPolicies = + [ + .. GetSupportedEccPolicyUris(includeCurvePolicies: false) + ]; + + public static readonly string[] SupportedEccX509Policies = + [ + .. SupportedEccPolicies.Where(policyUri => + { + CertificateKeyAlgorithm certificateKeyAlgorithm = + SecurityPolicies.GetInfo(policyUri).CertificateKeyAlgorithm; + return certificateKeyAlgorithm != CertificateKeyAlgorithm.Curve25519 && + certificateKeyAlgorithm != CertificateKeyAlgorithm.Curve448; + }) + ]; + + public static IEnumerable ReconnectSessionOnAlternateChannelWithSavedSessionSecretsEccTestCases() + { + foreach (string securityPolicy in SupportedEccPolicies) + { + yield return new TestCaseData(securityPolicy, true); + yield return new TestCaseData(securityPolicy, false); + } + } + /// /// Set up a Server and a Client instance. /// @@ -841,18 +866,17 @@ await session1.ReadValueAsync( [TestCase(SecurityPolicies.None, false)] [TestCase(SecurityPolicies.Basic256Sha256, true)] [TestCase(SecurityPolicies.Basic256Sha256, false)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1, true)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1, false)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1, true)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1, false)] - [TestCase(SecurityPolicies.ECC_nistP256, true)] - [TestCase(SecurityPolicies.ECC_nistP256, false)] - [TestCase(SecurityPolicies.ECC_nistP384, true)] - [TestCase(SecurityPolicies.ECC_nistP384, false)] + [TestCase(SecurityPolicies.RSA_DH_AesGcm, true)] + [TestCase(SecurityPolicies.RSA_DH_AesGcm, false)] + [TestCase(SecurityPolicies.RSA_DH_ChaChaPoly, true)] + [TestCase(SecurityPolicies.RSA_DH_ChaChaPoly, false)] + [TestCaseSource(nameof(ReconnectSessionOnAlternateChannelWithSavedSessionSecretsEccTestCases))] public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecretsAsync( string securityPolicy, bool anonymous) { + IgnoreIfPolicyNotAdvertised(securityPolicy); + ServiceResultException sre; UserIdentity userIdentity = anonymous @@ -1830,46 +1854,39 @@ public async Task ReadBuildInfoAsync() [Combinatorial] [Order(10100)] public async Task OpenSessionECCUserNamePwdIdentityTokenAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] string securityPolicy) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || - (securityPolicy != SecurityPolicies.ECC_brainpoolP256r1 && - securityPolicy != SecurityPolicies.ECC_brainpoolP384r1)) - { - var userIdentity = new UserIdentity("user1", "password"u8); - - // the first channel determines the endpoint - ConfiguredEndpoint endpoint = await ClientFixture - .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) - .ConfigureAwait(false); - Assert.NotNull(endpoint); + [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy) + { + IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); + IgnoreIfPolicyNotAdvertised(securityPolicy); - UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( - userIdentity.TokenType, - userIdentity.IssuedTokenType, - endpoint.Description.SecurityPolicyUri); - if (identityPolicy == null) - { - NUnit.Framework.Assert.Ignore( - $"No UserTokenPolicy found for {userIdentity.TokenType}" + - $" / {userIdentity.IssuedTokenType}"); - } + var userIdentity = new UserIdentity("user1", "password"u8); - // the active channel - ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) - .ConfigureAwait(false); - Assert.NotNull(session1); + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture + .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) + .ConfigureAwait(false); + Assert.NotNull(endpoint); - ServerStatusDataType value1 = - await session1.ReadValueAsync( - VariableIds.Server_ServerStatus).ConfigureAwait(false); - Assert.NotNull(value1); + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( + userIdentity.TokenType, + userIdentity.IssuedTokenType, + endpoint.Description.SecurityPolicyUri); + if (identityPolicy == null) + { + NUnit.Framework.Assert.Ignore( + $"No UserTokenPolicy found for {userIdentity.TokenType}" + + $" / {userIdentity.IssuedTokenType}"); } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) + .ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = + await session1.ReadValueAsync( + VariableIds.Server_ServerStatus).ConfigureAwait(false); + Assert.NotNull(value1); } /// @@ -1879,51 +1896,44 @@ await session1.ReadValueAsync( [Combinatorial] [Order(10200)] public async Task OpenSessionECCIssuedIdentityTokenAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] string securityPolicy) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || - (securityPolicy != SecurityPolicies.ECC_brainpoolP256r1 && - securityPolicy != SecurityPolicies.ECC_brainpoolP384r1)) - { - const string identityToken = "fakeTokenString"; - - using var issuedToken = new IssuedIdentityTokenHandler( - Profiles.JwtUserToken, - Encoding.UTF8.GetBytes(identityToken)); - var userIdentity = new UserIdentity(issuedToken); - - // the first channel determines the endpoint - ConfiguredEndpoint endpoint = await ClientFixture - .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) - .ConfigureAwait(false); - Assert.NotNull(endpoint); + [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy) + { + IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); + IgnoreIfPolicyNotAdvertised(securityPolicy); - UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( - userIdentity.TokenType, - userIdentity.IssuedTokenType, - securityPolicy); + const string identityToken = "fakeTokenString"; - if (identityPolicy == null) - { - NUnit.Framework.Assert.Ignore( - $"No UserTokenPolicy found for {userIdentity.TokenType}" + - $" / {userIdentity.IssuedTokenType}"); - } + using var issuedToken = new IssuedIdentityTokenHandler( + Profiles.JwtUserToken, + Encoding.UTF8.GetBytes(identityToken)); + var userIdentity = new UserIdentity(issuedToken); - // the active channel - ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) - .ConfigureAwait(false); - Assert.NotNull(session1); + // the first channel determines the endpoint + ConfiguredEndpoint endpoint = await ClientFixture + .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) + .ConfigureAwait(false); + Assert.NotNull(endpoint); - ServerStatusDataType value1 = await session1.ReadValueAsync( - VariableIds.Server_ServerStatus).ConfigureAwait(false); - Assert.NotNull(value1); + UserTokenPolicy identityPolicy = endpoint.Description.FindUserTokenPolicy( + userIdentity.TokenType, + userIdentity.IssuedTokenType, + securityPolicy); + + if (identityPolicy == null) + { + NUnit.Framework.Assert.Ignore( + $"No UserTokenPolicy found for {userIdentity.TokenType}" + + $" / {userIdentity.IssuedTokenType}"); } + + // the active channel + ISession session1 = await ClientFixture.ConnectAsync(endpoint, userIdentity) + .ConfigureAwait(false); + Assert.NotNull(session1); + + ServerStatusDataType value1 = await session1.ReadValueAsync( + VariableIds.Server_ServerStatus).ConfigureAwait(false); + Assert.NotNull(value1); } /// @@ -1933,14 +1943,11 @@ public async Task OpenSessionECCIssuedIdentityTokenAsync( [Combinatorial] [Order(10300)] public async Task OpenSessionECCUserCertIdentityTokenAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] - string securityPolicy) + [ValueSource(nameof(SupportedEccX509Policies))] string securityPolicy) { + IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); + IgnoreIfPolicyNotAdvertised(securityPolicy); + var eccCurveHashPairs = new ECCurveHashPairCollection { { ECCurve.NamedCurves.nistP256, HashAlgorithmName.SHA256 }, @@ -2326,5 +2333,28 @@ private static void ValidateOperationLimit(uint serverLimit, uint clientLimit) Assert.NotZero(clientLimit); } } + + private void IgnoreIfPolicyNotAdvertised(string securityPolicyUri) + { + if (Endpoints?.Any(endpoint => + string.Equals( + endpoint.SecurityPolicyUri, + securityPolicyUri, + StringComparison.Ordinal)) != true) + { + NUnit.Framework.Assert.Ignore( + $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server."); + } + } + + private static void IgnoreUnsupportedBrainpoolOnMacOs(string securityPolicyUri) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && + (securityPolicyUri.Contains("ECC_brainpoolP256r1", StringComparison.Ordinal) || + securityPolicyUri.Contains("ECC_brainpoolP384r1", StringComparison.Ordinal))) + { + NUnit.Framework.Assert.Ignore("Brainpool curve is not supported on Mac OS."); + } + } } } diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index bc7fd8dba2..99b957f4b2 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -108,6 +108,21 @@ public void InitializeSession(ISession session) .. GetPolicyUrisForTests() ]; + protected static IEnumerable GetSupportedEccPolicyUris( + bool includeCurvePolicies = true) + { + IEnumerable displayNames = SecurityPolicies.GetDisplayNames() + .Where(name => name.StartsWith("ECC_", StringComparison.Ordinal)); + + if (!includeCurvePolicies) + { + displayNames = displayNames + .Where(name => !name.StartsWith("ECC_curve", StringComparison.Ordinal)); + } + + return displayNames.Select(SecurityPolicies.GetUri); + } + private static IEnumerable GetPolicyUrisForTests() { IEnumerable displayNames = SecurityPolicies.GetDisplayNames(); @@ -253,82 +268,28 @@ public virtual async Task CreateReferenceServerFixtureAsync( IssuedTokenType = Profiles.JwtUserToken }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.UserName) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" - }); + foreach (string securityPolicyUri in GetSupportedEccPolicyUris()) + { + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.UserName) + { + SecurityPolicyUri = securityPolicyUri + }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.Certificate) - { - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" - }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.Certificate) + { + SecurityPolicyUri = securityPolicyUri + }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri - = "http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" - }); - ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( - new UserTokenPolicy(UserTokenType.IssuedToken) - { - IssuedTokenType = Profiles.JwtUserToken, - PolicyId = Profiles.JwtUserToken, - SecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384" - }); + ServerFixture.Config.ServerConfiguration.UserTokenPolicies.Add( + new UserTokenPolicy(UserTokenType.IssuedToken) + { + IssuedTokenType = Profiles.JwtUserToken, + PolicyId = Profiles.JwtUserToken, + SecurityPolicyUri = securityPolicyUri + }); + } ServerFixture.Config.ServerConfiguration.MaxChannelCount = MaxChannelCount; ServerFixture.Config.ServerConfiguration.MaxSubscriptionCount = 1000; diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index a543f99c9e..f9d3471ad8 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -50,6 +50,11 @@ namespace Opc.Ua.Client.Tests [SetUICulture("en-us")] public class SubscriptionTest : ClientTestFramework { + public static readonly string[] SupportedEccPolicies = + [ + .. GetSupportedEccPolicyUris(includeCurvePolicies: false) + ]; + private readonly string m_subscriptionTestXml = Path.Combine( Path.GetTempPath(), "SubscriptionTest.xml"); @@ -449,13 +454,7 @@ await Session.SetPublishingModeAsync( [Order(351)] [Explicit] public Task ReconnectWithSavedSessionSecretsOnlyECCAsync( - [Values( - SecurityPolicies.ECC_nistP256, - SecurityPolicies.ECC_nistP384, - SecurityPolicies.ECC_brainpoolP256r1, - SecurityPolicies.ECC_brainpoolP384r1 - )] - string securityPolicy, + [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, [Values(true, false)] bool sendInitialValues) @@ -478,7 +477,9 @@ public Task ReconnectWithSavedSessionSecretsOnlyECCAsync( public Task ReconnectWithSavedSessionSecretsOnlyAsync( [Values(SecurityPolicies.None, SecurityPolicies.ECC_nistP256, - SecurityPolicies.Basic256Sha256)] + SecurityPolicies.Basic256Sha256, + SecurityPolicies.RSA_DH_AesGcm, + SecurityPolicies.RSA_DH_ChaChaPoly)] string securityPolicy, [Values(true, false)] bool anonymous, [Values(true, false)] bool sequentialPublishing, @@ -497,6 +498,8 @@ public async Task ReconnectWithSavedSessionSecretsAsync( bool sequentialPublishing, bool sendInitialValues) { + IgnoreIfPolicyNotAdvertised(securityPolicy); + const int kTestSubscriptions = 5; const int kDelay = 2_000; const int kQueueSize = 10; @@ -1629,5 +1632,18 @@ public async Task ConcurrentCreateItemsNoDuplicatesAsync() // Clean up await subscription.DeleteAsync(true, CancellationToken.None).ConfigureAwait(false); } + + private void IgnoreIfPolicyNotAdvertised(string securityPolicyUri) + { + if (Endpoints?.Any(endpoint => + string.Equals( + endpoint.SecurityPolicyUri, + securityPolicyUri, + StringComparison.Ordinal)) != true) + { + NUnit.Framework.Assert.Ignore( + $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server."); + } + } } } diff --git a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs index 5a0ad14145..f7534c9f6f 100644 --- a/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs +++ b/Tests/Opc.Ua.Core.Tests/Types/Nonce/NonceTests.cs @@ -28,6 +28,8 @@ * ======================================================================*/ using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; @@ -42,18 +44,23 @@ namespace Opc.Ua.Core.Tests.Types.Nonce [Parallelizable] public class NonceTests { + public static readonly string[] SupportedNoncePolicies = + [ + .. SecurityPolicies.GetDisplayNames() + .Where(name => !name.Equals(nameof(SecurityPolicies.None), StringComparison.Ordinal)) + .Select(SecurityPolicies.GetUri) + ]; + + private static readonly HashSet s_supportedPolicyUris = + [ + .. SecurityPolicies.GetDisplayNames().Select(SecurityPolicies.GetUri) + ]; + /// /// Test the CreateNonce - securitypolicy and valid nonceLength /// [Theory] - [TestCase(SecurityPolicies.ECC_nistP256)] - [TestCase(SecurityPolicies.ECC_nistP384)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] - [TestCase(SecurityPolicies.Basic256)] - [TestCase(SecurityPolicies.Basic256Sha256)] - [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] - [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + [TestCaseSource(nameof(SupportedNoncePolicies))] public void ValidateCreateNoncePolicyLength(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) @@ -73,14 +80,7 @@ public void ValidateCreateNoncePolicyLength(string securityPolicyUri) /// Test the CreateEccNonce - securitypolicy and nonceData /// [Theory] - [TestCase(SecurityPolicies.ECC_nistP256)] - [TestCase(SecurityPolicies.ECC_nistP384)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] - [TestCase(SecurityPolicies.Basic256)] - [TestCase(SecurityPolicies.Basic256Sha256)] - [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] - [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + [TestCaseSource(nameof(SupportedNoncePolicies))] public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) { if (IsSupportedByPlatform(securityPolicyUri)) @@ -102,14 +102,7 @@ public void ValidateCreateNoncePolicyNonceData(string securityPolicyUri) /// Test the CreateEccNonce - securitypolicy and invalid nonceData /// [Theory] - [TestCase(SecurityPolicies.ECC_nistP256)] - [TestCase(SecurityPolicies.ECC_nistP384)] - [TestCase(SecurityPolicies.ECC_brainpoolP256r1)] - [TestCase(SecurityPolicies.ECC_brainpoolP384r1)] - [TestCase(SecurityPolicies.Basic256)] - [TestCase(SecurityPolicies.Basic256Sha256)] - [TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)] - [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] + [TestCaseSource(nameof(SupportedNoncePolicies))] public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( string securityPolicyUri) { @@ -124,8 +117,8 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && ( - securityPolicyUri == SecurityPolicies.ECC_nistP256 || - securityPolicyUri == SecurityPolicies.ECC_nistP384)) + securityPolicyUri.Contains("ECC_nistP256", StringComparison.Ordinal) || + securityPolicyUri.Contains("ECC_nistP384", StringComparison.Ordinal))) { NUnit.Framework.Assert .Ignore("No exception is thrown on OSX with NIST curves"); @@ -146,31 +139,11 @@ public void ValidateCreateEccNoncePolicyInvalidNonceDataCorrectLength( /// private static bool IsSupportedByPlatform(string securityPolicyUri) { - if (securityPolicyUri.Equals(SecurityPolicies.ECC_nistP256, StringComparison.Ordinal)) - { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccNistP256ApplicationCertificateType); - } - else if (securityPolicyUri.Equals( - SecurityPolicies.ECC_nistP384, - StringComparison.Ordinal)) - { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccNistP384ApplicationCertificateType); - } - else if (securityPolicyUri.Equals( - SecurityPolicies.ECC_brainpoolP256r1, - StringComparison.Ordinal)) - { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType); - } - else if (securityPolicyUri.Equals( - SecurityPolicies.ECC_brainpoolP384r1, + if (securityPolicyUri.StartsWith( + SecurityPolicies.BaseUri, StringComparison.Ordinal)) { - return Utils.IsSupportedCertificateType( - ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType); + return s_supportedPolicyUris.Contains(securityPolicyUri); } return true; diff --git a/Tests/Opc.Ua.Gds.Tests/PushTest.cs b/Tests/Opc.Ua.Gds.Tests/PushTest.cs index c51bb71ff2..b8b632a57d 100644 --- a/Tests/Opc.Ua.Gds.Tests/PushTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/PushTest.cs @@ -28,6 +28,7 @@ * ======================================================================*/ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; @@ -57,6 +58,11 @@ namespace Opc.Ua.Gds.Tests [NonParallelizable] public class PushTest { + private static readonly HashSet s_supportedPolicyUris = + [ + .. SecurityPolicies.GetDisplayNames().Select(SecurityPolicies.GetUri) + ]; + /// /// CertificateTypes to run the Test with. /// For ECC types, the additional fourth element is the expected curve friendly name. @@ -78,6 +84,20 @@ public class PushTest ECCurve.NamedCurves.nistP256 }, new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType, + SecurityPolicies.ECC_nistP256_AesGcm, + ECCurve.NamedCurves.nistP256 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP256ApplicationCertificateType, + SecurityPolicies.ECC_nistP256_ChaChaPoly, + ECCurve.NamedCurves.nistP256 + }, + new object[] { nameof(OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType), OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType, @@ -85,6 +105,20 @@ public class PushTest ECCurve.NamedCurves.nistP384 }, new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType, + SecurityPolicies.ECC_nistP384_AesGcm, + ECCurve.NamedCurves.nistP384 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccNistP384ApplicationCertificateType, + SecurityPolicies.ECC_nistP384_ChaChaPoly, + ECCurve.NamedCurves.nistP384 + }, + new object[] { nameof(OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType), OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType, @@ -92,16 +126,50 @@ public class PushTest ECCurve.NamedCurves.brainpoolP256r1 }, new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP256r1_AesGcm, + ECCurve.NamedCurves.brainpoolP256r1 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly, + ECCurve.NamedCurves.brainpoolP256r1 + }, + new object[] { nameof(OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType), OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType, SecurityPolicies.ECC_brainpoolP384r1, ECCurve.NamedCurves.brainpoolP384r1 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP384r1_AesGcm, + ECCurve.NamedCurves.brainpoolP384r1 + }, + new object[] + { + nameof(OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType), + OpcUa.ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType, + SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly, + ECCurve.NamedCurves.brainpoolP384r1 } ]; public PushTest(string certificateTypeString, NodeId certificateType, string securityPolicyUri, ECCurve? curve) { + if (!s_supportedPolicyUris.Contains(securityPolicyUri)) + { + NUnit.Framework.Assert.Ignore( + $"Security policy {securityPolicyUri} is not supported on this runtime."); + } + if (!Utils.IsSupportedCertificateType(certificateType)) { NUnit.Framework.Assert.Ignore( @@ -110,7 +178,8 @@ public PushTest(string certificateTypeString, NodeId certificateType, string sec // Skip brainpool curves on Mac OS if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && - (securityPolicyUri == SecurityPolicies.ECC_brainpoolP256r1 || securityPolicyUri == SecurityPolicies.ECC_brainpoolP384r1)) + (securityPolicyUri.Contains("ECC_brainpoolP256r1", StringComparison.Ordinal) || + securityPolicyUri.Contains("ECC_brainpoolP384r1", StringComparison.Ordinal))) { NUnit.Framework.Assert.Ignore("Brainpool curve is not supported on Mac OS."); } @@ -179,8 +248,17 @@ await m_pushClient.LoadClientConfigurationAsync(m_server.BasePort) await m_gdsClient.GDSClient.ConnectAsync(m_gdsClient.GDSClient.EndpointUrl) .ConfigureAwait(false); - await m_pushClient.ConnectAsync(m_securityPolicyUri) - .ConfigureAwait(false); + try + { + await m_pushClient.ConnectAsync(m_securityPolicyUri) + .ConfigureAwait(false); + } + catch (ArgumentException ex) when ( + ex.Message.Contains("No endpoint found for SecurityPolicyUri", StringComparison.Ordinal)) + { + NUnit.Framework.Assert.Ignore( + $"Security policy {m_securityPolicyUri} is not advertised by the GDS test server."); + } await ConnectGDSClientAsync(true).ConfigureAwait(false); From a7d5fb77f0d1d1274746a591daa8baad0f7492d6 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 18 Feb 2026 14:14:16 +0200 Subject: [PATCH 34/42] Fix ReconnectSessionOnAlternateChannel _AES and _ChaCha policies --- Libraries/Opc.Ua.Client/Session/Session.cs | 5 ++ .../Session/SessionConfiguration.cs | 7 +++ Tests/Opc.Ua.Client.Tests/SessionTests.cs | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 0c714d0f45..18962700f3 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -894,6 +894,7 @@ public virtual void Restore(SessionState state) public void Snapshot(out SessionConfiguration sessionConfiguration) { byte[]? serverNonce = m_serverNonce != null ? [.. m_serverNonce] : null; + byte[]? clientNonce = m_clientNonce != null ? [.. m_clientNonce] : null; byte[]? serverEccEphemeralKey = m_eccServerEphemeralKey?.Data != null ? [.. m_eccServerEphemeralKey.Data] : null; @@ -906,6 +907,7 @@ public void Snapshot(out SessionConfiguration sessionConfiguration) ConfiguredEndpoint = ConfiguredEndpoint, CheckDomain = CheckDomain, ServerNonce = serverNonce, + ClientNonce = clientNonce, ServerEccEphemeralKey = serverEccEphemeralKey, UserIdentityTokenPolicy = m_userTokenSecurityPolicyUri }; @@ -926,6 +928,9 @@ public void Restore(SessionConfiguration sessionConfiguration) m_serverNonce = sessionConfiguration.ServerNonce != null ? [.. sessionConfiguration.ServerNonce] : null; + m_clientNonce = sessionConfiguration.ClientNonce != null + ? [.. sessionConfiguration.ClientNonce] + : null; m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; if (sessionConfiguration.ServerEccEphemeralKey?.Length > 0) { diff --git a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs index d1b15ccb02..bcf0fb779c 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs @@ -129,6 +129,13 @@ public SessionState(SessionOptions options) [DataMember(IsRequired = true, Order = 80)] public byte[]? ServerNonce { get; init; } + /// + /// The raw bytes of the client nonce used when the session was created. + /// Required for enhanced-policy activate signatures during reconnect. + /// + [DataMember(IsRequired = false, Order = 85)] + public byte[]? ClientNonce { get; init; } + /// /// The user identity token policy which was used to create the session. /// diff --git a/Tests/Opc.Ua.Client.Tests/SessionTests.cs b/Tests/Opc.Ua.Client.Tests/SessionTests.cs index 703b983d25..c5c19ff9e5 100644 --- a/Tests/Opc.Ua.Client.Tests/SessionTests.cs +++ b/Tests/Opc.Ua.Client.Tests/SessionTests.cs @@ -1570,5 +1570,54 @@ public void SaveShouldOnlySaveSpecifiedSubscriptions() Assert.That(loadedSubscriptions.Count, Is.EqualTo(2), "Only the specified subscriptions should be saved"); Assert.That(loadedSubscriptions.Select(s => s.DisplayName), Is.EquivalentTo(["Subscription1", "Subscription3"])); } + + [Test] + public void SaveAndApplySessionConfigurationShouldPersistClientNonce() + { + var source = SessionMock.Create(); + source.SetConnected(); + + byte[] clientNonce = [1, 3, 5, 7, 9]; + byte[] serverNonce = [2, 4, 6, 8]; + + SetPrivateByteArrayField(source, "m_clientNonce", clientNonce); + SetPrivateByteArrayField(source, "m_serverNonce", serverNonce); + + SessionConfiguration configuration = source.SaveSessionConfiguration(); + + Assert.That(configuration.ClientNonce, Is.EquivalentTo(clientNonce)); + Assert.That(configuration.ServerNonce, Is.EquivalentTo(serverNonce)); + + var target = SessionMock.Create(); + + bool success = target.ApplySessionConfiguration(configuration); + + Assert.That(success, Is.True); + Assert.That(GetPrivateByteArrayField(target, "m_clientNonce"), Is.EquivalentTo(clientNonce)); + Assert.That(GetPrivateByteArrayField(target, "m_serverNonce"), Is.EquivalentTo(serverNonce)); + } + + private static byte[] GetPrivateByteArrayField(Session session, string fieldName) + { + return (byte[])typeof(Session) + .GetField( + fieldName, + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance)? + .GetValue(session); + } + + private static void SetPrivateByteArrayField( + Session session, + string fieldName, + byte[] value) + { + typeof(Session) + .GetField( + fieldName, + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance)? + .SetValue(session, value); + } } } From e9e4c63c401d95499d031a130a5100eab3e54b0c Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 18 Feb 2026 15:03:55 +0200 Subject: [PATCH 35/42] Fixed IgnoreIfPolicyNotAdvertised so it now fetches endpoints on-demand (instead of relying on Endpoints being preloaded by earlier tests), which avoids false ignores when running tests directlyy --- Tests/Opc.Ua.Client.Tests/ClientTest.cs | 23 ++++++++++++++----- Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs | 17 +++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 2ebee15454..d4b2516a06 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -875,7 +875,7 @@ public async Task ReconnectSessionOnAlternateChannelWithSavedSessionSecretsAsync string securityPolicy, bool anonymous) { - IgnoreIfPolicyNotAdvertised(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); ServiceResultException sre; @@ -1857,7 +1857,7 @@ public async Task OpenSessionECCUserNamePwdIdentityTokenAsync( [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy) { IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); - IgnoreIfPolicyNotAdvertised(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); var userIdentity = new UserIdentity("user1", "password"u8); @@ -1899,7 +1899,7 @@ public async Task OpenSessionECCIssuedIdentityTokenAsync( [ValueSource(nameof(SupportedEccPolicies))] string securityPolicy) { IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); - IgnoreIfPolicyNotAdvertised(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); const string identityToken = "fakeTokenString"; @@ -1946,7 +1946,7 @@ public async Task OpenSessionECCUserCertIdentityTokenAsync( [ValueSource(nameof(SupportedEccX509Policies))] string securityPolicy) { IgnoreUnsupportedBrainpoolOnMacOs(securityPolicy); - IgnoreIfPolicyNotAdvertised(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); var eccCurveHashPairs = new ECCurveHashPairCollection { @@ -2334,16 +2334,27 @@ private static void ValidateOperationLimit(uint serverLimit, uint clientLimit) } } - private void IgnoreIfPolicyNotAdvertised(string securityPolicyUri) + private async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri) { + Endpoints ??= await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false); if (Endpoints?.Any(endpoint => string.Equals( endpoint.SecurityPolicyUri, securityPolicyUri, StringComparison.Ordinal)) != true) { + string advertisedPolicies = Endpoints == null + ? "" + : string.Join( + ", ", + Endpoints + .Select(endpoint => endpoint.SecurityPolicyUri) + .Where(policy => !string.IsNullOrEmpty(policy)) + .Distinct() + .OrderBy(policy => policy, StringComparer.Ordinal)); NUnit.Framework.Assert.Ignore( - $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server."); + $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " + + $"Advertised: {advertisedPolicies}"); } } diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index f9d3471ad8..d65b8d1415 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -498,7 +498,7 @@ public async Task ReconnectWithSavedSessionSecretsAsync( bool sequentialPublishing, bool sendInitialValues) { - IgnoreIfPolicyNotAdvertised(securityPolicy); + await IgnoreIfPolicyNotAdvertisedAsync(securityPolicy).ConfigureAwait(false); const int kTestSubscriptions = 5; const int kDelay = 2_000; @@ -1633,16 +1633,27 @@ public async Task ConcurrentCreateItemsNoDuplicatesAsync() await subscription.DeleteAsync(true, CancellationToken.None).ConfigureAwait(false); } - private void IgnoreIfPolicyNotAdvertised(string securityPolicyUri) + private async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri) { + Endpoints ??= await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false); if (Endpoints?.Any(endpoint => string.Equals( endpoint.SecurityPolicyUri, securityPolicyUri, StringComparison.Ordinal)) != true) { + string advertisedPolicies = Endpoints == null + ? "" + : string.Join( + ", ", + Endpoints + .Select(endpoint => endpoint.SecurityPolicyUri) + .Where(policy => !string.IsNullOrEmpty(policy)) + .Distinct() + .OrderBy(policy => policy, StringComparer.Ordinal)); NUnit.Framework.Assert.Ignore( - $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server."); + $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " + + $"Advertised: {advertisedPolicies}"); } } } From 905d4ca0f060f3402cb61cfd834f5ea6515a5c2e Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 19 Feb 2026 12:20:10 +0200 Subject: [PATCH 36/42] Merge with commit 5e627f2ca1cf from secure-channel-enhancements-2025-11 branch --- Libraries/Opc.Ua.Client/Session/Session.cs | 2 +- .../Opc.Ua.Server/Server/StandardServer.cs | 6 +- .../Security/Certificates/CryptoUtils.cs | 93 ++++++---- .../Security/Certificates/EncryptedSecret.cs | 24 ++- .../Security/Constants/SecurityPolicies.cs | 5 + .../Security/Constants/SecurityPolicyInfo.cs | 165 ++++++++++++++---- .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 6 + 7 files changed, 224 insertions(+), 77 deletions(-) diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index 18962700f3..3f28d1f2a0 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -5059,7 +5059,7 @@ protected virtual void ProcessResponseAdditionalHeader( SecurityPolicies.GetInfo(m_userTokenSecurityPolicyUri), key.PublicKey); - m_logger.LogWarning("Updating ServerEphemeralKey: {Key}", CryptoTrace.KeyToString(m_eccServerEphemeralKey.Data)); + m_logger.LogWarning("Updating ServerEphemeralKey: {Key} bytes", m_eccServerEphemeralKey.Data?.Length ?? 0); } } } diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index d7ad4f29cf..838fd1d591 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -623,7 +623,7 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet Value = new ExtensionObject(key) }); - m_logger.LogWarning("Returning new EphmeralKey: {PublicKey}.", CryptoTrace.KeyToString(key.PublicKey)); + m_logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); } else { @@ -634,7 +634,7 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet Value = StatusCodes.BadSecurityPolicyRejected }); - m_logger.LogWarning("Rejecting request for new EphmeralKey using {SecurityPolicyUri}.", policyUri); + m_logger.LogWarning("Rejecting request for new EphemeralKey using {SecurityPolicyUri}.", policyUri); } } } @@ -667,7 +667,7 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam Value = new ExtensionObject(key) }); - m_logger.LogWarning("Returning new EphmeralKey: {PublicKey}.", CryptoTrace.KeyToString(key.PublicKey)); + m_logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); } return response; diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs index 22968835ed..8e00d00599 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -1228,16 +1228,24 @@ static byte[] HkdfSha256(byte[] ikm, byte[] salt, byte[] info, int outLen) /// public static class CryptoTrace { + /// + /// Enabled crypto traces to stdout. + /// + public static bool Enabled { get; set; } = true; + /// /// Starts a trace block. /// public static void Start(ConsoleColor color, string format, params object[] args) { #if DEBUG - Console.ForegroundColor = color; - Console.Write("============ "); - Console.Write(format, args); - Console.WriteLine(" ============"); + if (Enabled) + { + Console.ForegroundColor = color; + Console.Write("============ "); + Console.Write(format, args); + Console.WriteLine(" ============"); + } #endif } @@ -1247,10 +1255,13 @@ public static void Start(ConsoleColor color, string format, params object[] args public static void Finish(string format, params object[] args) { #if DEBUG - Console.Write("============ "); - Console.Write(format, args); - Console.WriteLine(" Finished ============"); - Console.ForegroundColor = ConsoleColor.White; + if (Enabled) + { + Console.Write("============ "); + Console.Write(format, args); + Console.WriteLine(" Finished ============"); + Console.ForegroundColor = ConsoleColor.White; + } #endif } @@ -1260,7 +1271,10 @@ public static void Finish(string format, params object[] args) public static void Write(string format, params object[] args) { #if DEBUG - Console.Write(format, args); + if (Enabled) + { + Console.Write(format, args); + } #endif } @@ -1270,7 +1284,10 @@ public static void Write(string format, params object[] args) public static void WriteLine(string format, params object[] args) { #if DEBUG - Console.WriteLine(format, args); + if (Enabled) + { + Console.WriteLine(format, args); + } #endif } @@ -1280,13 +1297,20 @@ public static void WriteLine(string format, params object[] args) public static string KeyToString(ArraySegment key) { #if DEBUG - byte[] bytes = new byte[key.Count]; - Buffer.BlockCopy(key.Array ?? [], key.Offset, bytes, 0, key.Count); - return KeyToString(bytes); + if (Enabled) + { + byte[] bytes = new byte[key.Count]; + Buffer.BlockCopy(key.Array ?? [], key.Offset, bytes, 0, key.Count); + return KeyToString(bytes); + } + else + { + return String.Empty; + } #else return String.Empty; #endif - } + } /// /// Returns a debug string for a key. @@ -1294,29 +1318,36 @@ public static string KeyToString(ArraySegment key) public static string KeyToString(byte[] key) { #if DEBUG - if (key == null || key.Length == 0) + if (Enabled) { - return "Len=0:---"; - } + if (key == null || key.Length == 0) + { + return "Len=0:---"; + } - byte checksum = 0; + byte checksum = 0; - foreach (var item in key) - { - checksum ^= item; - } + foreach (var item in key) + { + checksum ^= item; + } - if (key.Length <= 16) + if (key.Length <= 16) + { + return "Len=" + key.Length.ToString(CultureInfo.InvariantCulture) + + ":" + + Utils.ToHexString(key) + + "=>XOR=" + + checksum.ToString(CultureInfo.InvariantCulture); + } + + var text = Utils.ToHexString(key); + return $"Len={key.Length}:{text.Substring(0, 8)}...{text.Substring(text.Length - 8, 8)}=>XOR={checksum}"; + } + else { - return "Len=" + key.Length.ToString(CultureInfo.InvariantCulture) + - ":" + - Utils.ToHexString(key) + - "=>XOR=" + - checksum.ToString(CultureInfo.InvariantCulture); + return String.Empty; } - - var text = Utils.ToHexString(key); - return $"Len={key.Length}:{text.Substring(0, 8)}...{text.Substring(text.Length - 8, 8)}=>XOR={checksum}"; #else return String.Empty; #endif diff --git a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs index da3c9829aa..fbda7398d5 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs @@ -210,6 +210,13 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) encoder.WriteByteString(null, senderCertificate); encoder.WriteDateTime(null, DateTime.UtcNow); + if (ReceiverNonce?.Data == null) + { + throw new ServiceResultException( + StatusCodes.BadArgumentsMissing, + $"The receiver did not provide an ephemeral key."); + } + byte[] senderNonce = SenderNonce.Data; byte[] receiverNonce = ReceiverNonce.Data; @@ -241,13 +248,13 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) { case SymmetricEncryptionAlgorithm.Aes128Cbc: case SymmetricEncryptionAlgorithm.Aes256Cbc: - paddingCount = GetPaddingCount(SecurityPolicy.InitializationVectorLength, encoder.Position - startOfSecret); + paddingCount = GetPaddingCount(SecurityPolicy.InitializationVectorLength, secret.Length, encoder.Position - startOfSecret); tagLength = 0; break; case SymmetricEncryptionAlgorithm.Aes128Gcm: case SymmetricEncryptionAlgorithm.Aes256Gcm: case SymmetricEncryptionAlgorithm.ChaCha20Poly1305: - paddingCount = GetPaddingCount(encryptingKey.Length, encoder.Position - startOfSecret); + paddingCount = GetPaddingCount(16, secret.Length, encoder.Position - startOfSecret); tagLength = SecurityPolicy.SymmetricSignatureLength; break; } @@ -322,13 +329,18 @@ public byte[] Encrypt(byte[] secret, byte[] nonce) return message; } - private int GetPaddingCount(int blockSize, int dataLength) + private int GetPaddingCount(int blockSize, int secretLength, int dataLength) { - int paddingCount = blockSize - ((dataLength + 2) % blockSize); + dataLength += 2; // add padding size + + int paddingCount = + dataLength % blockSize == 0 + ? 0 + : blockSize - dataLength % blockSize; - if (paddingCount == blockSize) + if (paddingCount + secretLength < blockSize) { - paddingCount = 0; + paddingCount += blockSize; } return paddingCount; diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs index fea313c1c7..867a17ae28 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicies.cs @@ -750,6 +750,11 @@ public static SignatureData CreateSignatureData( securityPolicy.Uri); } + if (securityPolicy.SecureChannelEnhancements) + { + signatureData.Signature = null; + } + signatureData.Signature = CryptoUtils.Sign( new ArraySegment(dataToSign), localCertificate, diff --git a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs index 2de16f5526..786f714230 100644 --- a/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs +++ b/Stack/Opc.Ua.Core/Security/Constants/SecurityPolicyInfo.cs @@ -113,6 +113,11 @@ public SecurityPolicyInfo(string uri, string name = null) /// public CertificateKeyAlgorithm EphemeralKeyAlgorithm { get; private set; } + /// + /// The algorithm used to calculate the thumbprint of the certificate. + /// + public CertificateThumbprintAlgorithm CertificateThumbprintAlgorithm { get; private set; } + /// /// The length, in bytes, of the Nonces used when opening a SecureChannel. /// @@ -194,13 +199,26 @@ public byte[] GetUserTokenSignatureData( CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + using HashAlgorithm hash = CertificateThumbprintAlgorithm switch + { + CertificateThumbprintAlgorithm.SHA256 => SHA256.Create(), + CertificateThumbprintAlgorithm.SHA384 => SHA384.Create(), + CertificateThumbprintAlgorithm.SHA512 => SHA512.Create(), + _ => throw new NotSupportedException() + }; + + var serverCertificateHash = serverCertificate != null ? hash.ComputeHash(serverCertificate) : null; + var serverChannelCertificateHash = serverChannelCertificate != null ? hash.ComputeHash(serverChannelCertificate) : null; + var clientCertificateHash = clientCertificate != null ? hash.ComputeHash(clientCertificate) : null; + var clientChannelCertificateHash = clientChannelCertificate != null ? hash.ComputeHash(clientChannelCertificate) : null; + data = Utils.Append( channelThumbprint, serverNonce, - serverCertificate, - serverChannelCertificate, - clientCertificate, - clientChannelCertificate, + serverCertificateHash, + serverChannelCertificateHash, + clientCertificateHash, + clientChannelCertificateHash, clientNonce); } else @@ -240,11 +258,22 @@ public byte[] GetServerSignatureData( CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); CryptoTrace.WriteLine($"ServerNonce={CryptoTrace.KeyToString(serverNonce)}"); + using HashAlgorithm hash = CertificateThumbprintAlgorithm switch + { + CertificateThumbprintAlgorithm.SHA256 => SHA256.Create(), + CertificateThumbprintAlgorithm.SHA384 => SHA384.Create(), + CertificateThumbprintAlgorithm.SHA512 => SHA512.Create(), + _ => throw new NotSupportedException() + }; + + var serverChannelCertificateHash = serverChannelCertificate != null ? hash.ComputeHash(serverChannelCertificate) : null; + var clientChannelCertificateHash = clientChannelCertificate != null ? hash.ComputeHash(clientChannelCertificate) : null; + data = Utils.Append( channelThumbprint, clientNonce, - serverChannelCertificate, - clientChannelCertificate, + serverChannelCertificateHash, + clientChannelCertificateHash, serverNonce); } else @@ -285,12 +314,24 @@ public byte[] GetClientSignatureData( CryptoTrace.WriteLine($"ClientChannelCertificate={CryptoTrace.KeyToString(clientChannelCertificate)}"); CryptoTrace.WriteLine($"ClientNonce={CryptoTrace.KeyToString(clientNonce)}"); + using HashAlgorithm hash = CertificateThumbprintAlgorithm switch + { + CertificateThumbprintAlgorithm.SHA256 => SHA256.Create(), + CertificateThumbprintAlgorithm.SHA384 => SHA384.Create(), + CertificateThumbprintAlgorithm.SHA512 => SHA512.Create(), + _ => throw new NotSupportedException() + }; + + var serverCertificateHash = serverCertificate != null ? hash.ComputeHash(serverCertificate) : null; + var serverChannelCertificateHash = serverChannelCertificate != null ? hash.ComputeHash(serverChannelCertificate) : null; + var clientChannelCertificateHash = clientChannelCertificate != null ? hash.ComputeHash(clientChannelCertificate) : null; + data = Utils.Append( channelThumbprint, serverNonce, - serverCertificate, - serverChannelCertificate, - clientChannelCertificate, + serverCertificateHash, + serverChannelCertificateHash, + clientChannelCertificateHash, clientNonce); } else @@ -350,7 +391,7 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() MinAsymmetricKeyLength = 0, MaxAsymmetricKeyLength = 0, SecureChannelNonceLength = 32, - LegacySequenceNumbers = false, + LegacySequenceNumbers = true, AsymmetricEncryptionAlgorithm = AsymmetricEncryptionAlgorithm.None, AsymmetricSignatureAlgorithm = AsymmetricSignatureAlgorithm.None, CertificateKeyFamily = CertificateKeyFamily.None, @@ -360,7 +401,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() KeyDerivationAlgorithm = KeyDerivationAlgorithm.None, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.None, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.None, - SecureChannelEnhancements = false + SecureChannelEnhancements = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 }; /// @@ -386,7 +428,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1, - IsDeprecated = true + IsDeprecated = true, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 }; /// @@ -412,7 +455,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha1, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha1, - IsDeprecated = true + IsDeprecated = true, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 }; /// @@ -437,7 +481,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() KeyDerivationAlgorithm = KeyDerivationAlgorithm.PSha256, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 }; /// @@ -462,7 +507,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() EphemeralKeyAlgorithm = CertificateKeyAlgorithm.None, SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 }; /// @@ -487,7 +533,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, InitializationVectorLength = 128 / 8, SymmetricSignatureLength = 256 / 8, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA1 }; /// @@ -513,7 +560,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = false, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -539,7 +587,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -565,7 +614,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -591,7 +641,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = false, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -617,7 +668,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -643,7 +695,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -669,7 +722,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, SecureChannelEnhancements = false, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -695,7 +749,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -721,7 +776,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -747,7 +803,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, SecureChannelEnhancements = false, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -773,7 +830,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -799,7 +857,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -825,7 +884,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha256, SecureChannelEnhancements = false, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -851,7 +911,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -877,7 +938,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -903,7 +965,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Cbc, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.HmacSha384, SecureChannelEnhancements = false, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -929,7 +992,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes256Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes256Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -955,7 +1019,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA384 }; /// @@ -981,7 +1046,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.Aes128Gcm, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.Aes128Gcm, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; /// @@ -1007,7 +1073,8 @@ public HashAlgorithmName GetKeyDerivationHashAlgorithmName() SymmetricEncryptionAlgorithm = SymmetricEncryptionAlgorithm.ChaCha20Poly1305, SymmetricSignatureAlgorithm = SymmetricSignatureAlgorithm.ChaCha20Poly1305, SecureChannelEnhancements = true, - IsDeprecated = false + IsDeprecated = false, + CertificateThumbprintAlgorithm = CertificateThumbprintAlgorithm.SHA256 }; } @@ -1272,4 +1339,30 @@ public enum SymmetricEncryptionAlgorithm /// Aes256Gcm } + + /// + /// The algorithm used to generate certificate thumbprints. + /// + public enum CertificateThumbprintAlgorithm + { + /// + /// The SHA1 algorithm. This algorithm is considered insecure. + /// + SHA1, + + /// + /// The SHA256 algorithm. + /// + SHA256, + + /// + /// The SHA384 algorithm. + /// + SHA384, + + /// + /// The SHA512 algorithm. + /// + SHA512 + } } diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index ab4af6cd19..a52b9fb8c8 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -696,6 +696,12 @@ const string errorSecurityChecksFailed token = CreateToken(); token.TokenId = GetNewTokenId(); token.ServerNonce = CreateNonce(ServerCertificate); + + CryptoTrace.Start(ConsoleColor.Red, $"PreviousSecret"); + CryptoTrace.WriteLine($"PreviousSecret={CryptoTrace.KeyToString(token.PreviousSecret)}"); + CryptoTrace.WriteLine($"CurrentSecret={CryptoTrace.KeyToString(CurrentToken?.Secret)}"); + CryptoTrace.Finish($"PreviousSecret"); + token.PreviousSecret = CurrentToken?.Secret; // check the client nonce. From 67a8f9d15bf2b555f7cebbff3b6b7b5d06ec0beb Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 19 Feb 2026 12:22:17 +0200 Subject: [PATCH 37/42] minor log mesatge formating --- Applications/ConsoleReferenceClient/ClientSamples.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Applications/ConsoleReferenceClient/ClientSamples.cs b/Applications/ConsoleReferenceClient/ClientSamples.cs index 776f133bf5..b8cbb7866b 100644 --- a/Applications/ConsoleReferenceClient/ClientSamples.cs +++ b/Applications/ConsoleReferenceClient/ClientSamples.cs @@ -862,7 +862,7 @@ await uaClient result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); m_logger.LogInformation( - "ManagedBrowseFullAddressSpace found {Count} references on server in {Duration}ms.", + "ManagedBrowseFullAddressSpace found {Count} references on server in {Duration} ms.", result.Count, stopWatch.ElapsedMilliseconds); @@ -1083,7 +1083,7 @@ BrowseDescriptionCollection browseDescriptionCollection result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); m_logger.LogInformation( - "BrowseFullAddressSpace found {Count} references on server in {Duration}ms.", + "BrowseFullAddressSpace found {Count} references on server in {Duration} ms.", referenceDescriptions.Count, stopWatch.ElapsedMilliseconds); From b6230cf2beaee86decc64a41c11ec496d6ee15a0 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 19 Feb 2026 15:46:32 +0200 Subject: [PATCH 38/42] Fix ClientLockoutTests --- .../Opc.Ua.Server.Tests/ClientLockoutTests.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs b/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs index 15dff4d53b..fba58efb08 100644 --- a/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs +++ b/Tests/Opc.Ua.Server.Tests/ClientLockoutTests.cs @@ -66,10 +66,7 @@ public async Task FailedAuthenticationAttemptsAreTrackedAsync() EndpointDescriptionCollection endpoints = m_server.GetEndpoints(); EndpointDescription endpoint = FindTcpEndpoint(endpoints); - var secureChannelContext = new SecureChannelContext( - sessionName, - endpoint, - RequestEncoding.Binary); + SecureChannelContext secureChannelContext = CreateSecureChannelContext(sessionName, endpoint); var requestHeader = new RequestHeader(); CreateSessionResponse createResponse = await m_server.CreateSessionAsync( @@ -130,10 +127,7 @@ public async Task ClientIsLockedOutAfterFiveFailedAttemptsAsync() EndpointDescriptionCollection endpoints = m_server.GetEndpoints(); EndpointDescription endpoint = FindTcpEndpoint(endpoints); - var secureChannelContext = new SecureChannelContext( - sessionName, - endpoint, - RequestEncoding.Binary); + SecureChannelContext secureChannelContext = CreateSecureChannelContext(sessionName, endpoint); var requestHeader = new RequestHeader(); CreateSessionResponse createResponse = await m_server.CreateSessionAsync( @@ -228,10 +222,7 @@ public async Task SuccessfulAuthenticationClearsFailedAttemptsAsync() EndpointDescriptionCollection endpoints = m_server.GetEndpoints(); EndpointDescription endpoint = FindTcpEndpoint(endpoints); - var secureChannelContext = new SecureChannelContext( - sessionName, - endpoint, - RequestEncoding.Binary); + SecureChannelContext secureChannelContext = CreateSecureChannelContext(sessionName, endpoint); var requestHeader = new RequestHeader(); CreateSessionResponse createResponse = await m_server.CreateSessionAsync( @@ -337,5 +328,16 @@ private static EndpointDescription FindTcpEndpoint(EndpointDescriptionCollection endpoint.SecurityPolicyUri = SecurityPolicies.None; return endpoint; } + + private static SecureChannelContext CreateSecureChannelContext(string sessionName, EndpointDescription endpoint) + { + return new SecureChannelContext( + sessionName, + endpoint, + RequestEncoding.Binary, + clientChannelCertificate: null, + serverChannelCertificate: null, + channelThumbprint: null); + } } } From fd2b9fc2ff1d81b4a1bfab88d128a9215e82236e Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 20 Feb 2026 16:41:20 +0200 Subject: [PATCH 39/42] Add ServerFixture policies upfront only if framework and runtime capability supports them --- Tests/Opc.Ua.Server.Tests/ServerFixture.cs | 136 +++++++++++++++++---- 1 file changed, 111 insertions(+), 25 deletions(-) diff --git a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs index 6880fcd363..0aa119eaaa 100644 --- a/Tests/Opc.Ua.Server.Tests/ServerFixture.cs +++ b/Tests/Opc.Ua.Server.Tests/ServerFixture.cs @@ -124,34 +124,109 @@ public async Task LoadConfigurationAsync(string pkiRoot = null) { // add deprecated policies for opc.tcp tests serverConfig - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.Basic256) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256) .AddSignPolicies() .AddSignAndEncryptPolicies() - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.RSA_DH_AesGcm) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.RSA_DH_AesGcm) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.RSA_DH_ChaChaPoly) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.RSA_DH_ChaChaPoly) .AddEccSignPolicies() - .AddEccSignAndEncryptPolicies() - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP256_AesGcm) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP256_AesGcm) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP256_ChaChaPoly) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP256_ChaChaPoly) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP384_AesGcm) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP384_AesGcm) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP384_ChaChaPoly) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP384_ChaChaPoly) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP256r1_AesGcm) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP256r1_AesGcm) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP384r1_AesGcm) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP384r1_AesGcm) - .AddPolicy(MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly) - .AddPolicy(MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); + .AddEccSignAndEncryptPolicies(); + + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.Basic128Rsa15); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Basic128Rsa15); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.Basic256); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.Basic256); + + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.RSA_DH_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.RSA_DH_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.RSA_DH_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.RSA_DH_ChaChaPoly); + + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP256_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP256_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP256_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP256_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP384_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP384_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_nistP384_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_nistP384_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP256r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP256r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP384r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP384r1_AesGcm); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.Sign, + SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); + AddPolicyIfSupported( + serverConfig, + MessageSecurityMode.SignAndEncrypt, + SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); } if (OperationLimits) @@ -362,6 +437,17 @@ public async Task StopAsync() await Task.Delay(100).ConfigureAwait(false); } + private static void AddPolicyIfSupported( + IApplicationConfigurationBuilderServerSelected serverConfig, + MessageSecurityMode securityMode, + string securityPolicyUri) + { + if (SecurityPolicies.GetInfo(securityPolicyUri) != null) + { + serverConfig.AddPolicy(securityMode, securityPolicyUri); + } + } + private readonly Func m_factory; private readonly ITelemetryContext m_telemetry; private readonly ILogger m_logger; From 911c20281fab2b99d02606134bdd6e0a1bb2e592 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 25 Feb 2026 17:14:48 +0200 Subject: [PATCH 40/42] A few cosmetic/config changes as review sugested --- Libraries/Opc.Ua.Server/Session/ISession.cs | 2 +- Libraries/Opc.Ua.Server/Session/Session.cs | 2 +- .../Security/Certificates/CryptoUtils.cs | 8 ++--- .../Stack/Server/SecureChannelContext.cs | 4 +-- .../Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs | 9 +++++- .../Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs | 8 +++++ .../Stack/Tcp/UaSCBinaryTransportChannel.cs | 2 +- Tests/Opc.Ua.Client.Tests/ClientTest.cs | 24 -------------- .../ClientTestFramework.cs | 24 ++++++++++++++ Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs | 23 ------------- ...pc.Ua.GlobalDiscoveryTestServer.Config.xml | 32 +++++++++++++++++++ 11 files changed, 81 insertions(+), 57 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Session/ISession.cs b/Libraries/Opc.Ua.Server/Session/ISession.cs index ce22644641..91d47cbd7d 100644 --- a/Libraries/Opc.Ua.Server/Session/ISession.cs +++ b/Libraries/Opc.Ua.Server/Session/ISession.cs @@ -43,7 +43,7 @@ public interface ISession : IDisposable bool Activated { get; } /// - /// The application instance certificate associated with the server. + /// The server application instance certificate used by this session. /// X509Certificate2 ServerCertificate { get; } diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs index b341b73c12..d7d48a5313 100644 --- a/Libraries/Opc.Ua.Server/Session/Session.cs +++ b/Libraries/Opc.Ua.Server/Session/Session.cs @@ -243,7 +243,7 @@ protected virtual void Dispose(bool disposing) public byte[] ClientNonce { get; } /// - /// The application instance certificate associated with the server. + /// The server application instance certificate used by this session. /// public X509Certificate2 ServerCertificate => m_serverCertificate; diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs index 8e00d00599..c247383166 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs @@ -309,7 +309,7 @@ public static int GetSignatureLength(X509Certificate2 signingCertificate) } /// - /// Computes an ECDSA signature. + /// Computes a signature. /// public static byte[] Sign( ArraySegment dataToSign, @@ -321,7 +321,7 @@ public static byte[] Sign( } /// - /// Computes an signature. + /// Computes a signature. /// /// public static byte[] Sign( @@ -392,7 +392,7 @@ public static byte[] Sign( } /// - /// Verifies a ECDsa signature. + /// Verifies a signature. /// public static bool Verify( ArraySegment dataToVerify, @@ -417,7 +417,7 @@ public static bool Verify( } /// - /// Verifies a ECDsa signature. + /// Verifies a signature. /// public static bool Verify( ArraySegment dataToVerify, diff --git a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs index 6e407db4c9..e393444af1 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/SecureChannelContext.cs @@ -44,8 +44,8 @@ public class SecureChannelContext /// The endpoint description. /// The message encoding. /// The unique hash for the secure channel calculated during channel creation. - /// The client certificate used to establsih the secure channel. - /// The server certificate used to establsih the secure channel. + /// The client certificate used to establish the secure channel. + /// The server certificate used to establish the secure channel. public SecureChannelContext( string secureChannelId, EndpointDescription endpointDescription, diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs index a52b9fb8c8..df83e166d2 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/TcpServerChannel.cs @@ -962,6 +962,13 @@ protected void SendServiceFault(uint requestId, bool renew, ServiceResult fault) /// /// Sends an OpenSecureChannel response. /// + /// The request identifier. + /// The security token to return in the response. + /// The OpenSecureChannel request being answered. + /// + /// true when answering a token renewal request. Renewal keeps the same channel, but issues a new + /// security token (new token id and server nonce), false for the initial open. + /// private void SendOpenSecureChannelResponse( uint requestId, ChannelToken token, @@ -1007,7 +1014,7 @@ private void SendOpenSecureChannelResponse( CryptoTrace.WriteLine($"ChannelThumbprint={CryptoTrace.KeyToString(ChannelThumbprint)}"); CryptoTrace.Finish("SendOpenSecureChannelResponse"); - // write the message to the server. + // write the response to the client. try { BeginWriteMessage(chunksToSend, null); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs index c5b8c76073..e42f4e0024 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryChannel.Asymmetric.cs @@ -574,6 +574,14 @@ protected BufferCollection WriteAsymmetricMessage( /// /// Sends a OpenSecureChannel request. /// + /// The UA TCP message type (for example, Open or OpenFinal). + /// The request identifier used in the sequence header. + /// The certificate used to sign the asymmetric message. + /// The optional sender certificate chain to include in the message header. + /// The receiver certificate used for asymmetric encryption. + /// The encoded message body to send. + /// The signature from the OpenSecureChannel request. + /// Returns the signature generated for the message being written. /// /// protected BufferCollection WriteAsymmetricMessage( diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index 639cd07c76..4ee76828fb 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -458,7 +458,7 @@ private UaSCUaBinaryClientChannel CreateChannel( // create the channel. var channel = new UaSCUaBinaryClientChannel( - Guid.NewGuid().ToString(), + id, m_bufferManager, m_messageSocketFactory, m_quotas, diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index d4b2516a06..2544345e32 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -2334,30 +2334,6 @@ private static void ValidateOperationLimit(uint serverLimit, uint clientLimit) } } - private async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri) - { - Endpoints ??= await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false); - if (Endpoints?.Any(endpoint => - string.Equals( - endpoint.SecurityPolicyUri, - securityPolicyUri, - StringComparison.Ordinal)) != true) - { - string advertisedPolicies = Endpoints == null - ? "" - : string.Join( - ", ", - Endpoints - .Select(endpoint => endpoint.SecurityPolicyUri) - .Where(policy => !string.IsNullOrEmpty(policy)) - .Distinct() - .OrderBy(policy => policy, StringComparer.Ordinal)); - NUnit.Framework.Assert.Ignore( - $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " + - $"Advertised: {advertisedPolicies}"); - } - } - private static void IgnoreUnsupportedBrainpoolOnMacOs(string securityPolicyUri) { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs index 99b957f4b2..271841716d 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestFramework.cs @@ -136,6 +136,30 @@ private static IEnumerable GetPolicyUrisForTests() return displayNames.Select(SecurityPolicies.GetUri); } + protected async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri) + { + Endpoints ??= await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false); + if (Endpoints?.Any(endpoint => + string.Equals( + endpoint.SecurityPolicyUri, + securityPolicyUri, + StringComparison.Ordinal)) != true) + { + string advertisedPolicies = Endpoints == null + ? "" + : string.Join( + ", ", + Endpoints + .Select(endpoint => endpoint.SecurityPolicyUri) + .Where(policy => !string.IsNullOrEmpty(policy)) + .Distinct() + .OrderBy(policy => policy, StringComparer.Ordinal)); + NUnit.Framework.Assert.Ignore( + $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " + + $"Advertised: {advertisedPolicies}"); + } + } + /// /// Set up a Server and a Client instance. /// diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index d65b8d1415..3d67660776 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -1633,28 +1633,5 @@ public async Task ConcurrentCreateItemsNoDuplicatesAsync() await subscription.DeleteAsync(true, CancellationToken.None).ConfigureAwait(false); } - private async Task IgnoreIfPolicyNotAdvertisedAsync(string securityPolicyUri) - { - Endpoints ??= await ClientFixture.GetEndpointsAsync(ServerUrl).ConfigureAwait(false); - if (Endpoints?.Any(endpoint => - string.Equals( - endpoint.SecurityPolicyUri, - securityPolicyUri, - StringComparison.Ordinal)) != true) - { - string advertisedPolicies = Endpoints == null - ? "" - : string.Join( - ", ", - Endpoints - .Select(endpoint => endpoint.SecurityPolicyUri) - .Where(policy => !string.IsNullOrEmpty(policy)) - .Distinct() - .OrderBy(policy => policy, StringComparer.Ordinal)); - NUnit.Framework.Assert.Ignore( - $"SecurityPolicy '{securityPolicyUri}' is not advertised by the server. " + - $"Advertised: {advertisedPolicies}"); - } - } } } diff --git a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml index a8e65fb8b9..ce7b462efd 100644 --- a/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml +++ b/Tests/Opc.Ua.Gds.Tests/Opc.Ua.GlobalDiscoveryTestServer.Config.xml @@ -90,18 +90,50 @@ SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256_ChaChaPoly + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP384_ChaChaPoly + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1_ChaChaPoly + SignAndEncrypt_3 http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1 + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_AesGcm + + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1_ChaChaPoly + From 05a069fac3a2e8ec84603b86f5f8e22be42a5482 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Fri, 27 Feb 2026 19:22:44 +0200 Subject: [PATCH 41/42] Moved the session/security handling from StandardServer into a dedicated helper --- .../Opc.Ua.Server/Server/StandardServer.cs | 170 ++++++++---------- .../Session/SessionSecurityPolicyHelper.cs | 161 +++++++++++++++++ 2 files changed, 237 insertions(+), 94 deletions(-) create mode 100644 Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs diff --git a/Libraries/Opc.Ua.Server/Server/StandardServer.cs b/Libraries/Opc.Ua.Server/Server/StandardServer.cs index 4f37fe381a..a6d172371e 100644 --- a/Libraries/Opc.Ua.Server/Server/StandardServer.cs +++ b/Libraries/Opc.Ua.Server/Server/StandardServer.cs @@ -462,14 +462,9 @@ X509Certificate2Collection clientCertificateChain } } - var parameters = - ExtensionObject.ToEncodeable( - requestHeader.AdditionalHeader) as AdditionalParametersType; - - if (parameters != null) - { - parameters = CreateSessionProcessAdditionalParameters(session, parameters); - } + AdditionalParametersType parameters = CreateSessionProcessAdditionalParameters( + session, + requestHeader.AdditionalHeader); await m_semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); try @@ -494,26 +489,12 @@ X509Certificate2Collection clientCertificateChain serverEndpoints = GetEndpointDescriptions(endpointUrl, BaseAddresses, null); // sign the nonce provided by the client. - serverSignature = null; - - // sign the client nonce (if provided). - if (parsedClientCertificate != null && clientNonce != null) - { - SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(context.SecurityPolicyUri); - - byte[] dataToSign = securityPolicy.GetServerSignatureData( - context.ChannelContext.ChannelThumbprint, - clientNonce, - context.ChannelContext.ServerChannelCertificate, - parsedClientCertificate.RawData, - context.ChannelContext.ClientChannelCertificate, - serverNonce); - - serverSignature = SecurityPolicies.CreateSignatureData( - context.SecurityPolicyUri, - instanceCertificate, - dataToSign); - } + serverSignature = CreateSessionServerSignature( + context, + instanceCertificate, + parsedClientCertificate, + clientNonce, + serverNonce); } finally { @@ -593,7 +574,47 @@ X509Certificate2Collection clientCertificateChain } /// - /// Process additional parameters during the ECC session creation and set the session's UserToken security policy + /// Creates the server signature returned during CreateSession. + /// + /// The request context. + /// The instance certificate used to sign data. + /// The client certificate supplied in the request. + /// The client nonce supplied in the request. + /// The server nonce generated for the response. + /// The server signature or null when signing is not required. + protected virtual SignatureData CreateSessionServerSignature( + OperationContext context, + X509Certificate2 instanceCertificate, + X509Certificate2 parsedClientCertificate, + byte[] clientNonce, + byte[] serverNonce) + { + return SessionSecurityPolicyHelper.CreateServerSignature( + context, + instanceCertificate, + parsedClientCertificate, + clientNonce, + serverNonce); + } + + /// + /// Process additional header values during session creation for ephemeral-key user token policies. + /// + /// The session. + /// The additional request header. + /// An AdditionalParametersType object containing the processed parameters. + protected virtual AdditionalParametersType CreateSessionProcessAdditionalParameters( + ISession session, + ExtensionObject additionalHeader) + { + AdditionalParametersType parameters = SessionSecurityPolicyHelper + .DecodeAdditionalParameters(additionalHeader); + + return CreateSessionProcessAdditionalParameters(session, parameters); + } + + /// + /// Process additional parameters during session creation and set the session's user token security policy. /// /// The session /// The additional parameters for the session @@ -602,54 +623,30 @@ protected virtual AdditionalParametersType CreateSessionProcessAdditionalParamet ISession session, AdditionalParametersType parameters) { - AdditionalParametersType response = null; - - if (parameters != null && parameters.Parameters != null) - { - response = new AdditionalParametersType(); - - foreach (KeyValuePair ii in parameters.Parameters) - { - if (ii.Key == AdditionalParameterNames.ECDHPolicyUri) - { - string policyUri = ii.Value.ToString(); - m_logger.LogWarning("Received request for new EphmeralKey using {SecurityPolicyUri}.", policyUri); - - var securityPolicy = SecurityPolicies.GetInfo(policyUri); - - if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) - { - session.SetUserTokenSecurityPolicy(policyUri); - EphemeralKeyType key = session.GetNewEphemeralKey(); - response.Parameters.Add( - new KeyValuePair - { - Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), - Value = new ExtensionObject(key) - }); - - m_logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); - } - else - { - response.Parameters.Add( - new KeyValuePair - { - Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), - Value = StatusCodes.BadSecurityPolicyRejected - }); + return SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters( + session, + parameters, + m_logger); + } - m_logger.LogWarning("Rejecting request for new EphemeralKey using {SecurityPolicyUri}.", policyUri); - } - } - } - } + /// + /// Process additional header values during session activation for ephemeral-key user token policies. + /// + /// The session. + /// The additional request header. + /// An AdditionalParametersType object containing the processed parameters. + protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParameters( + ISession session, + ExtensionObject additionalHeader) + { + AdditionalParametersType parameters = SessionSecurityPolicyHelper + .DecodeAdditionalParameters(additionalHeader); - return response; + return ActivateSessionProcessAdditionalParameters(session, parameters); } /// - /// Process additional parameters during ECC session activation + /// Process additional parameters during session activation for ephemeral-key user token policies. /// /// The session /// The additional parameters for the session @@ -658,24 +655,10 @@ protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParam ISession session, AdditionalParametersType parameters) { - AdditionalParametersType response = null; - - EphemeralKeyType key = session.GetNewEphemeralKey(); - - if (key != null) - { - response = new AdditionalParametersType(); - response.Parameters - .Add(new KeyValuePair - { - Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), - Value = new ExtensionObject(key) - }); - - m_logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); - } - - return response; + return SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters( + session, + parameters, + m_logger); } /// @@ -716,10 +699,9 @@ public override async ValueTask ActivateSessionAsync( ISession session = ServerInternal.SessionManager .GetSession(requestHeader.AuthenticationToken); - var parameters = - ExtensionObject.ToEncodeable( - requestHeader.AdditionalHeader) as AdditionalParametersType; - parameters = ActivateSessionProcessAdditionalParameters(session, parameters); + AdditionalParametersType parameters = ActivateSessionProcessAdditionalParameters( + session, + requestHeader.AdditionalHeader); m_logger.LogInformation("Server - SESSION ACTIVATED."); diff --git a/Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs b/Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs new file mode 100644 index 0000000000..ab97e323aa --- /dev/null +++ b/Libraries/Opc.Ua.Server/Session/SessionSecurityPolicyHelper.cs @@ -0,0 +1,161 @@ +/* ======================================================================== + * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. + * + * OPC Foundation MIT License 1.00 + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * The complete license agreement can be found here: + * http://opcfoundation.org/License/MIT/1.00/ + * ======================================================================*/ + +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +namespace Opc.Ua.Server +{ + /// + /// Encapsulates session service security-policy specific processing. + /// + internal static class SessionSecurityPolicyHelper + { + /// + /// Decodes additional request parameters from an additional header. + /// + public static AdditionalParametersType DecodeAdditionalParameters( + ExtensionObject additionalHeader) + { + return ExtensionObject.ToEncodeable(additionalHeader) as AdditionalParametersType; + } + + /// + /// Creates the signature returned by CreateSession. + /// + public static SignatureData CreateServerSignature( + OperationContext context, + X509Certificate2 instanceCertificate, + X509Certificate2 parsedClientCertificate, + byte[] clientNonce, + byte[] serverNonce) + { + if (parsedClientCertificate == null || clientNonce == null) + { + return null; + } + + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(context.SecurityPolicyUri); + + byte[] dataToSign = securityPolicy.GetServerSignatureData( + context.ChannelContext.ChannelThumbprint, + clientNonce, + context.ChannelContext.ServerChannelCertificate, + parsedClientCertificate.RawData, + context.ChannelContext.ClientChannelCertificate, + serverNonce); + + return SecurityPolicies.CreateSignatureData( + context.SecurityPolicyUri, + instanceCertificate, + dataToSign); + } + + /// + /// Processes additional request parameters during CreateSession. + /// + public static AdditionalParametersType ProcessCreateSessionAdditionalParameters( + ISession session, + AdditionalParametersType parameters, + ILogger logger) + { + AdditionalParametersType response = null; + + if (parameters != null && parameters.Parameters != null) + { + response = new AdditionalParametersType(); + + foreach (KeyValuePair ii in parameters.Parameters) + { + if (ii.Key == AdditionalParameterNames.ECDHPolicyUri) + { + string policyUri = ii.Value.ToString(); + logger.LogWarning("Received request for new EphmeralKey using {SecurityPolicyUri}.", policyUri); + + SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(policyUri); + + if (securityPolicy.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) + { + session.SetUserTokenSecurityPolicy(policyUri); + EphemeralKeyType key = session.GetNewEphemeralKey(); + response.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = new ExtensionObject(key) + }); + + logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); + } + else + { + response.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = StatusCodes.BadSecurityPolicyRejected + }); + + logger.LogWarning("Rejecting request for new EphemeralKey using {SecurityPolicyUri}.", policyUri); + } + } + } + } + + return response; + } + + /// + /// Processes additional request parameters during ActivateSession. + /// + public static AdditionalParametersType ProcessActivateSessionAdditionalParameters( + ISession session, + AdditionalParametersType parameters, + ILogger logger) + { + AdditionalParametersType response = null; + EphemeralKeyType key = session.GetNewEphemeralKey(); + + if (key != null) + { + response = new AdditionalParametersType(); + response.Parameters.Add( + new KeyValuePair + { + Key = QualifiedName.From(AdditionalParameterNames.ECDHKey), + Value = new ExtensionObject(key) + }); + + logger.LogWarning("Returning new EphemeralKey: {PublicKey} bytes.", key.PublicKey?.Length ?? 0); + } + + return response; + } + } +} From a2b008e0a4f0e446053be65891d95f59ad3ca8a6 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Mon, 2 Mar 2026 10:52:05 +0200 Subject: [PATCH 42/42] use X509IdentityTokenHandler from secure-channel-enhancements-2025-11 --- .../Stack/Types/X509IdentityTokenHandler.cs | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs index a57f45b229..a72bcd00c4 100644 --- a/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs +++ b/Stack/Opc.Ua.Core/Stack/Types/X509IdentityTokenHandler.cs @@ -43,6 +43,13 @@ public sealed class X509IdentityTokenHandler : IUserIdentityTokenHandler public X509IdentityTokenHandler(X509IdentityToken token) { m_token = token; + + if (m_token.CertificateData != null) + { + m_certificate = CertificateFactory.Create(m_token.CertificateData); + } + + m_ownsCertificate = true; } /// @@ -66,12 +73,29 @@ public X509IdentityTokenHandler(X509Certificate2 certificate) } Certificate = certificate; + m_ownsCertificate = true; m_token = new X509IdentityToken { CertificateData = certificate.RawData }; } + /// + /// Private constructor for . The cloned handler + /// shares the certificate reference but does not own it, so it will + /// not dispose the certificate. This is necessary because the + /// certificate's private key may reside in protected storage and + /// cannot be deep-copied. + /// + private X509IdentityTokenHandler( + X509IdentityToken token, + X509Certificate2 certificate) + { + m_token = token; + m_certificate = certificate; + m_ownsCertificate = false; + } + /// /// The certificate associated with the token. /// @@ -134,16 +158,13 @@ public SignatureData Sign( byte[] dataToSign, string securityPolicyUri) { - X509Certificate2 certificate = Certificate ?? - CertificateFactory.Create(m_token.CertificateData); + var info = SecurityPolicies.GetInfo(securityPolicyUri); - SignatureData signatureData = SecurityPolicies.CreateSignatureData( - securityPolicyUri, - certificate, + var signatureData = SecurityPolicies.CreateSignatureData( + info, + m_certificate, dataToSign); - m_token.CertificateData = certificate.RawData; - return signatureData; } @@ -155,17 +176,14 @@ public bool Verify( { try { - X509Certificate2 certificate = Certificate ?? - CertificateFactory.Create(m_token.CertificateData); + var info = SecurityPolicies.GetInfo(securityPolicyUri); bool valid = SecurityPolicies.VerifySignatureData( signatureData, - securityPolicyUri, - certificate, + info, + m_certificate, dataToVerify); - m_token.CertificateData = certificate.RawData; - return valid; } catch (Exception e) @@ -180,19 +198,19 @@ public bool Verify( /// public void Dispose() { - // TODOL Utils.SilentDispose(m_certificate); + if (m_ownsCertificate) + { + Utils.SilentDispose(m_certificate); + } m_certificate = null; } /// public object Clone() { - return new X509IdentityTokenHandler(Utils.Clone(m_token)) - { - // Keep the in-memory certificate instance so private key operations - // continue to work when cloned handlers are used for signing. - Certificate = m_certificate - }; + return new X509IdentityTokenHandler( + Utils.Clone(m_token), + m_certificate); } /// @@ -206,6 +224,7 @@ public bool Equals(IUserIdentityTokenHandler other) } private readonly X509IdentityToken m_token; + private readonly bool m_ownsCertificate; private X509Certificate2 m_certificate; } }