From 688e2b892ca54accc4da19b80599ebcbf4f1539f Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 19 Nov 2025 16:23:30 +0200 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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),