From a0c9c0094c4c6d0271c91040e222caadfb33913f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 17:49:13 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20session=20B=20=E2=80=94=20Go-faithful=20?= =?UTF-8?q?auth=20error=20states,=20NKey=20padding,=20permissions,=20signa?= =?UTF-8?q?l=20disposal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs | 17 +++++---- .../ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs | 18 +++++++++ .../Auth/JwtProcessor.cs | 23 ++---------- .../ZB.MOM.NatsNet.Server/ClientConnection.cs | 4 +- .../src/ZB.MOM.NatsNet.Server/ClientTypes.cs | 1 - .../ZB.MOM.NatsNet.Server/NatsServer.Auth.cs | 10 +++-- .../NatsServer.Lifecycle.cs | 2 + .../NatsServer.Signals.cs | 2 +- .../Auth/AuthImplementationTests.cs | 23 ++++++++---- .../Auth/JwtProcessorTests.cs | 4 +- reports/current.md | 2 +- reports/report_8c380e7.md | 37 +++++++++++++++++++ 12 files changed, 97 insertions(+), 46 deletions(-) create mode 100644 reports/report_8c380e7.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs index b8bbe96..963057e 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthHandler.cs @@ -278,16 +278,19 @@ public static partial class AuthHandler /// public static ClosedState GetAuthErrClosedState(Exception? err) { - if (err == null) return ClosedState.AuthenticationTimeout; - var msg = err.Message; - if (msg.Contains("expired", StringComparison.OrdinalIgnoreCase)) return ClosedState.AuthenticationExpired; - if (msg.Contains("revoked", StringComparison.OrdinalIgnoreCase)) return ClosedState.AuthRevoked; - return ClosedState.AuthenticationViolation; + return err switch + { + AuthProxyNotTrustedException => ClosedState.ProxyNotTrusted, + AuthProxyRequiredException => ClosedState.ProxyRequired, + _ => ClosedState.AuthenticationViolation, + }; } /// - /// Validates proxy configuration entries in options. - /// Mirrors Go validateProxies in server/auth.go. + /// Validates that proxy protocol configuration is consistent. + /// If is set, must also be enabled. + /// Note: Full NKey-format validation of trusted proxy keys is deferred until proxy auth is fully implemented. + /// Partially mirrors Go validateProxies in server/auth.go. /// public static Exception? ValidateProxies(ServerOptions opts) { diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs index d1ddb19..7d5716f 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/AuthTypes.cs @@ -170,3 +170,21 @@ public class RoutePermissions // Account stub removed — full implementation is in Accounts/Account.cs // in the ZB.MOM.NatsNet.Server namespace. + +/// +/// Sentinel exception representing a proxy-auth "not trusted" error. +/// Mirrors Go ErrAuthProxyNotTrusted in server/auth.go. +/// +public sealed class AuthProxyNotTrustedException : InvalidOperationException +{ + public AuthProxyNotTrustedException() : base("proxy not trusted") { } +} + +/// +/// Sentinel exception representing a proxy-auth "required" error. +/// Mirrors Go ErrAuthProxyRequired in server/auth.go. +/// +public sealed class AuthProxyRequiredException : InvalidOperationException +{ + public AuthProxyRequiredException() : base("proxy required") { } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs index d124c99..afb79e5 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/JwtProcessor.cs @@ -31,15 +31,6 @@ public static class JwtProcessor /// public const string JwtPrefix = "eyJ"; - /// - /// Wipes a byte slice by filling with 'x', for clearing nkey seed data. - /// Mirrors Go wipeSlice. - /// - public static void WipeSlice(Span buf) - { - buf.Fill((byte)'x'); - } - /// /// Validates that the given IP host address is allowed by the user claims source CIDRs. /// Returns true if the host is within any of the allowed CIDRs, or if no CIDRs are specified. @@ -227,17 +218,9 @@ public static class JwtProcessor if (opts.TrustedOperators == null || opts.TrustedOperators.Count == 0) return null; - // Each operator should be a well-formed JWT. - foreach (var op in opts.TrustedOperators) - { - var jwtStr = op?.ToString() ?? string.Empty; - var (_, err) = ReadOperatorJwtInternal(jwtStr); - // Allow the "not implemented" case through — structure validated up to prefix check. - if (err is FormatException fe && fe.Message.Contains("not fully implemented")) - continue; - if (err is ArgumentException) - return new InvalidOperationException($"invalid trusted operator JWT: {err.Message}"); - } + // TODO: Full trusted operator JWT validation requires a NATS JWT library. + // Each operator JWT should be decoded and its signing key chain verified. + // For now, we accept any non-empty operator list and validate at connect time. return null; } } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs index 822d8a2..8918f0c 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs @@ -872,12 +872,10 @@ public sealed partial class ClientConnection internal void SetPermissions(Auth.Permissions? perms) { - // Full permission installation deferred to later session. - // Store in Perms for now. lock (_mu) { if (perms != null) - Perms ??= new ClientPermissions(); + Perms = BuildPermissions(perms); } } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientTypes.cs index b0b5d22..06c25a1 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientTypes.cs @@ -166,7 +166,6 @@ public enum ClosedState Kicked, ProxyNotTrusted, ProxyRequired, - AuthRevoked, } // ============================================================================ diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Auth.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Auth.cs index 0a88c02..af78aba 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Auth.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Auth.cs @@ -169,9 +169,13 @@ public sealed partial class NatsServer { try { - var kp = NATS.NKeys.KeyPair.FromPublicKey(nkeyPub.AsSpan()); - // Sig is base64url-encoded; nonce is raw bytes. - var sigBytes = Convert.FromBase64String(sig.Replace('-', '+').Replace('_', '/')); + var kp = NATS.NKeys.KeyPair.FromPublicKey(nkeyPub.AsSpan()); + // Sig is raw URL-safe base64; convert to standard base64 with padding. + var padded = sig.Replace('-', '+').Replace('_', '/'); + var rem = padded.Length % 4; + if (rem == 2) padded += "=="; + else if (rem == 3) padded += "="; + var sigBytes = Convert.FromBase64String(padded); var verified = kp.Verify( new ReadOnlyMemory(nonce), new ReadOnlyMemory(sigBytes)); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs index 093d773..bc101de 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs @@ -141,6 +141,8 @@ public sealed partial class NatsServer if (_ocsprc != null) { /* stub — stop OCSP cache in session 23 */ } + DisposeSignalHandlers(); + _shutdownComplete.TrySetResult(); } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Signals.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Signals.cs index f9abe78..7449ee1 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Signals.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Signals.cs @@ -73,7 +73,7 @@ public sealed partial class NatsServer }); } - private void DisposeSignalHandlers() + internal void DisposeSignalHandlers() { _sigHup?.Dispose(); _sigTerm?.Dispose(); diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthImplementationTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthImplementationTests.cs index 1c0cc7c..8b15f92 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthImplementationTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/AuthImplementationTests.cs @@ -27,23 +27,30 @@ public class AuthHandlerExtendedTests } [Fact] - public void GetAuthErrClosedState_ExpiredMessage_ReturnsExpiredState() + public void GetAuthErrClosedState_ProxyNotTrusted_ReturnsProxyNotTrusted() { - var err = new InvalidOperationException("token is expired"); - AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthenticationExpired); + var err = new AuthProxyNotTrustedException(); + AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.ProxyNotTrusted); } [Fact] - public void GetAuthErrClosedState_NullError_ReturnsTimeout() + public void GetAuthErrClosedState_ProxyRequired_ReturnsProxyRequired() { - AuthHandler.GetAuthErrClosedState(null).ShouldBe(ClosedState.AuthenticationTimeout); + var err = new AuthProxyRequiredException(); + AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.ProxyRequired); } [Fact] - public void GetAuthErrClosedState_RevokedMessage_ReturnsRevoked() + public void GetAuthErrClosedState_OtherError_ReturnsAuthenticationViolation() { - var err = new InvalidOperationException("credential was revoked"); - AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthRevoked); + var err = new InvalidOperationException("bad credentials"); + AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthenticationViolation); + } + + [Fact] + public void GetAuthErrClosedState_NullError_ReturnsAuthenticationViolation() + { + AuthHandler.GetAuthErrClosedState(null).ShouldBe(ClosedState.AuthenticationViolation); } [Fact] diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs index 5ec132c..5db8321 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/JwtProcessorTests.cs @@ -40,7 +40,7 @@ public class JwtProcessorTests public void WipeSlice_FillsWithX() { var buf = new byte[] { 0x01, 0x02, 0x03 }; - JwtProcessor.WipeSlice(buf); + AuthHandler.WipeSlice(buf); buf.ShouldAllBe(b => b == (byte)'x'); } @@ -48,7 +48,7 @@ public class JwtProcessorTests public void WipeSlice_EmptyBuffer_NoOp() { var buf = Array.Empty(); - JwtProcessor.WipeSlice(buf); + AuthHandler.WipeSlice(buf); } // ========================================================================= diff --git a/reports/current.md b/reports/current.md index 72b4ab6..ebd499f 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 22:38:47 UTC +Generated: 2026-02-26 22:49:14 UTC ## Modules (12 total) diff --git a/reports/report_8c380e7.md b/reports/report_8c380e7.md new file mode 100644 index 0000000..ebd499f --- /dev/null +++ b/reports/report_8c380e7.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 22:49:14 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 3596 | +| n_a | 77 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 319 | +| n_a | 181 | +| not_started | 2533 | +| stub | 224 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**4184/6942 items complete (60.3%)**