/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Security.Cryptography; using System.Threading.Tasks; using Moq; using NUnit.Framework; using Opc.Ua.Identity; using Opc.Ua.Security.Certificates; using Opc.Ua.Tests; namespace Opc.Ua.Server.Tests { [TestFixture] [Category("Session")] [Category("Identity")] public sealed class SessionAuthenticationErrorRegressionTests { /// /// Verifies padding, plaintext length and policy errors are indistinguishable at ActivateSession. /// [TestCase("padding")] [TestCase("length")] [TestCase("algorithm")] public Task SessionDecryptFailuresReturnIdentityTokenInvalidAsync(string failure) { return AssertActivationRejectionAsync( new ServerIdentityRegistry(), StatusCodes.BadIdentityTokenInvalid, "Could not decrypt identity token.", failure); } /// /// Verifies an RSAEncryptedSecret whose KeyData fails RSA unpadding and one whose KeyData /// unpads but does not hold the expected keys produce the same error, with no inner result /// a client asking for inner diagnostics could use to tell them apart (OPC 10000-4 7.40.2.1). /// [TestCase(SecurityPolicies.Basic128Rsa15)] [TestCase(SecurityPolicies.Basic256Sha256)] [TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)] public async Task RsaEncryptedSecretKeyDataFailuresAreIndistinguishableAsync(string policyUri) { ServiceResultException badPadding = await ActivateWithRsaEncryptedSecretAsync( policyUri, wellFormedPadding: false).ConfigureAwait(false); ServiceResultException badLayout = await ActivateWithRsaEncryptedSecretAsync( policyUri, wellFormedPadding: true).ConfigureAwait(false); foreach (ServiceResultException error in new[] { badPadding, badLayout }) { Assert.That(error.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenInvalid)); Assert.That(error.InnerResult, Is.Null); } Assert.That(badLayout.Result.LocalizedText, Is.EqualTo(badPadding.Result.LocalizedText)); Assert.That(badLayout.AdditionalInfo, Is.EqualTo(badPadding.AdditionalInfo)); } [TestCaseSource(nameof(s_rejectionCodes))] public Task AuthenticatorRejectionPreservesStatusAndMessageAsync(StatusCode statusCode) { var error = new ServiceResult(statusCode, new LocalizedText("Authenticator rejection must survive.")); var registry = new ServerIdentityRegistry(new UserNamePasswordAuthenticator( (_, _) => throw new ServiceResultException(error))); return AssertActivationRejectionAsync(registry, statusCode, error.LocalizedText.Text!); } [Test] public Task UnhandledNonAnonymousTokenStillFailsClosedAsync() { return AssertActivationRejectionAsync( new ServerIdentityRegistry(), StatusCodes.BadIdentityTokenRejected, null!); } private static async Task AssertActivationRejectionAsync( ServerIdentityRegistry registry, StatusCode expectedStatus, string expectedMessage, string? decryptionFailure = null) { ServiceResultException error = await ActivateAsync( registry, decryptionFailure == null ? SecurityPolicies.None : SecurityPolicies.Basic128Rsa15, (certificate, _) => { var token = new UserNameIdentityToken { PolicyId = "username", UserName = "test-user", Password = ByteString.From([1]) }; if (decryptionFailure != null) { using RSA rsa = certificate.GetRSAPublicKey()!; token.Password = decryptionFailure == "padding" ? ByteString.From(new byte[rsa!.KeySize / 8]) : ByteString.From(rsa!.Encrypt( [0xFF, 0xFF, 0xFF, 0x7F], RSAEncryptionPadding.Pkcs1)); token.EncryptionAlgorithm = decryptionFailure == "algorithm" ? SecurityAlgorithms.RsaOaep : SecurityAlgorithms.Rsa15; } return token; }).ConfigureAwait(false); Assert.That(error.StatusCode, Is.EqualTo(expectedStatus)); if (expectedMessage != null) { Assert.That(error.Result.LocalizedText.Text, Is.EqualTo(expectedMessage)); } if (decryptionFailure != null) { Assert.That(error.InnerResult, Is.Null); } } /// /// Activates with a UserName token whose password is an RSAEncryptedSecret carrying a /// KeyData block that either fails RSA unpadding or unpads to a KeyData of the wrong layout. /// private static Task ActivateWithRsaEncryptedSecretAsync( string policyUri, bool wellFormedPadding) { return ActivateAsync( new ServerIdentityRegistry(), policyUri, (certificate, serverNonce) => { IServiceMessageContext messageContext = ServiceMessageContext.CreateEmpty(NUnitTelemetryContext.Create()); using var encryptor = EncryptedSecret.CreateForRsa(messageContext, policyUri, certificate); byte[] encoded = encryptor.EncryptRsa([1, 2, 3, 4], serverNonce.ToArray()); // locate the KeyData behind the fixed RSAEncryptedSecret header. using var decoder = new BinaryDecoder(encoded, messageContext); decoder.ReadNodeId(null); decoder.ReadByte(null); decoder.ReadUInt32(null); decoder.ReadString(null); decoder.ReadByteString(null); decoder.ReadDateTime(null); int keyDataLength = decoder.ReadUInt16(null); int keyDataStart = decoder.Position; using RSA rsa = certificate.GetRSAPublicKey()!; byte[] keyData = wellFormedPadding ? rsa!.Encrypt( [0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03], policyUri switch { SecurityPolicies.Basic128Rsa15 => RSAEncryptionPadding.Pkcs1, SecurityPolicies.Aes256_Sha256_RsaPss => RSAEncryptionPadding.OaepSHA256, _ => RSAEncryptionPadding.OaepSHA1 }) : new byte[rsa!.KeySize / 8]; Assert.That(keyData, Has.Length.EqualTo(keyDataLength)); Buffer.BlockCopy(keyData, 0, encoded, keyDataStart, keyDataLength); return new UserNameIdentityToken { PolicyId = "username", UserName = "test-user", Password = ByteString.From(encoded) }; }); } private static async Task ActivateAsync( ServerIdentityRegistry registry, string tokenSecurityPolicyUri, Func createToken) { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); var server = new Mock(); server.Setup(value => value.Telemetry).Returns(telemetry); server.Setup(value => value.NamespaceUris).Returns(new NamespaceTable()); server.Setup(value => value.MessageContext).Returns(ServiceMessageContext.CreateEmpty(telemetry)); server.Setup(value => value.IdentityRegistry).Returns(registry); server.Setup(value => value.DiagnosticsNodeManager).Returns(Mock.Of()); server.Setup(value => value.DefaultSystemContext).Returns(new ServerSystemContext(server.Object)); var configuration = new ApplicationConfiguration { ServerConfiguration = new ServerConfiguration { MinSessionTimeout = 1000, MaxSessionTimeout = 60000, MaxSessionCount = 10 } }; using Certificate certificate = CertificateBuilder.Create("CN=Authentication Error Regression") .SetRSAKeySize(2048).CreateForRSA(); using var manager = new SessionManager(server.Object, configuration); server.Setup(value => value.SessionManager).Returns(manager); var endpoint = new EndpointDescription { EndpointUrl = "opc.tcp://localhost/authentication-error", SecurityPolicyUri = SecurityPolicies.None, SecurityMode = MessageSecurityMode.None, UserIdentityTokens = [ new UserTokenPolicy { PolicyId = "username", TokenType = UserTokenType.UserName, SecurityPolicyUri = tokenSecurityPolicyUri } ] }; using var context = new OperationContext( new RequestHeader(), new SecureChannelContext("authentication-error", endpoint, RequestEncoding.Binary), RequestType.ActivateSession, RequestLifetime.None); CreateSessionResult created = await manager.CreateSessionAsync( context, certificate, "authentication-error", default, new ApplicationDescription(), endpoint.EndpointUrl, null, [], 60000, 64 * 1024, default).ConfigureAwait(false); try { UserIdentityToken token = createToken(certificate, created.ServerNonce); ServiceResultException error = Assert.ThrowsAsync(async () => await manager.ActivateSessionAsync( context, created.AuthenticationToken, new SignatureData(), new ExtensionObject(token), new SignatureData(), [], default).ConfigureAwait(false)); Assert.That(created.Session.Activated, Is.False); return error; } finally { await manager.CloseSessionAsync(created.SessionId).ConfigureAwait(false); } } private static readonly StatusCode[] s_rejectionCodes = [ StatusCodes.BadIdentityTokenInvalid, StatusCodes.BadUserAccessDenied, StatusCodes.BadIdentityTokenRejected, StatusCodes.BadCertificateUntrusted ]; } }