fix: session B — Go-faithful auth error states, NKey padding, permissions, signal disposal

This commit is contained in:
Joseph Doherty
2026-02-26 17:49:13 -05:00
parent 8c380e7ca6
commit a0c9c0094c
12 changed files with 97 additions and 46 deletions

View File

@@ -278,16 +278,19 @@ public static partial class AuthHandler
/// </summary> /// </summary>
public static ClosedState GetAuthErrClosedState(Exception? err) public static ClosedState GetAuthErrClosedState(Exception? err)
{ {
if (err == null) return ClosedState.AuthenticationTimeout; return err switch
var msg = err.Message; {
if (msg.Contains("expired", StringComparison.OrdinalIgnoreCase)) return ClosedState.AuthenticationExpired; AuthProxyNotTrustedException => ClosedState.ProxyNotTrusted,
if (msg.Contains("revoked", StringComparison.OrdinalIgnoreCase)) return ClosedState.AuthRevoked; AuthProxyRequiredException => ClosedState.ProxyRequired,
return ClosedState.AuthenticationViolation; _ => ClosedState.AuthenticationViolation,
};
} }
/// <summary> /// <summary>
/// Validates proxy configuration entries in options. /// Validates that proxy protocol configuration is consistent.
/// Mirrors Go <c>validateProxies</c> in server/auth.go. /// If <see cref="ServerOptions.ProxyRequired"/> is set, <see cref="ServerOptions.ProxyProtocol"/> must also be enabled.
/// Note: Full NKey-format validation of trusted proxy keys is deferred until proxy auth is fully implemented.
/// Partially mirrors Go <c>validateProxies</c> in server/auth.go.
/// </summary> /// </summary>
public static Exception? ValidateProxies(ServerOptions opts) public static Exception? ValidateProxies(ServerOptions opts)
{ {

View File

@@ -170,3 +170,21 @@ public class RoutePermissions
// Account stub removed — full implementation is in Accounts/Account.cs // Account stub removed — full implementation is in Accounts/Account.cs
// in the ZB.MOM.NatsNet.Server namespace. // in the ZB.MOM.NatsNet.Server namespace.
/// <summary>
/// Sentinel exception representing a proxy-auth "not trusted" error.
/// Mirrors Go <c>ErrAuthProxyNotTrusted</c> in server/auth.go.
/// </summary>
public sealed class AuthProxyNotTrustedException : InvalidOperationException
{
public AuthProxyNotTrustedException() : base("proxy not trusted") { }
}
/// <summary>
/// Sentinel exception representing a proxy-auth "required" error.
/// Mirrors Go <c>ErrAuthProxyRequired</c> in server/auth.go.
/// </summary>
public sealed class AuthProxyRequiredException : InvalidOperationException
{
public AuthProxyRequiredException() : base("proxy required") { }
}

View File

@@ -31,15 +31,6 @@ public static class JwtProcessor
/// </summary> /// </summary>
public const string JwtPrefix = "eyJ"; public const string JwtPrefix = "eyJ";
/// <summary>
/// Wipes a byte slice by filling with 'x', for clearing nkey seed data.
/// Mirrors Go <c>wipeSlice</c>.
/// </summary>
public static void WipeSlice(Span<byte> buf)
{
buf.Fill((byte)'x');
}
/// <summary> /// <summary>
/// Validates that the given IP host address is allowed by the user claims source CIDRs. /// 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. /// 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) if (opts.TrustedOperators == null || opts.TrustedOperators.Count == 0)
return null; return null;
// Each operator should be a well-formed JWT. // TODO: Full trusted operator JWT validation requires a NATS JWT library.
foreach (var op in opts.TrustedOperators) // 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.
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}");
}
return null; return null;
} }
} }

View File

@@ -872,12 +872,10 @@ public sealed partial class ClientConnection
internal void SetPermissions(Auth.Permissions? perms) internal void SetPermissions(Auth.Permissions? perms)
{ {
// Full permission installation deferred to later session.
// Store in Perms for now.
lock (_mu) lock (_mu)
{ {
if (perms != null) if (perms != null)
Perms ??= new ClientPermissions(); Perms = BuildPermissions(perms);
} }
} }

View File

@@ -166,7 +166,6 @@ public enum ClosedState
Kicked, Kicked,
ProxyNotTrusted, ProxyNotTrusted,
ProxyRequired, ProxyRequired,
AuthRevoked,
} }
// ============================================================================ // ============================================================================

View File

@@ -169,9 +169,13 @@ public sealed partial class NatsServer
{ {
try try
{ {
var kp = NATS.NKeys.KeyPair.FromPublicKey(nkeyPub.AsSpan()); var kp = NATS.NKeys.KeyPair.FromPublicKey(nkeyPub.AsSpan());
// Sig is base64url-encoded; nonce is raw bytes. // Sig is raw URL-safe base64; convert to standard base64 with padding.
var sigBytes = Convert.FromBase64String(sig.Replace('-', '+').Replace('_', '/')); 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( var verified = kp.Verify(
new ReadOnlyMemory<byte>(nonce), new ReadOnlyMemory<byte>(nonce),
new ReadOnlyMemory<byte>(sigBytes)); new ReadOnlyMemory<byte>(sigBytes));

View File

@@ -141,6 +141,8 @@ public sealed partial class NatsServer
if (_ocsprc != null) { /* stub — stop OCSP cache in session 23 */ } if (_ocsprc != null) { /* stub — stop OCSP cache in session 23 */ }
DisposeSignalHandlers();
_shutdownComplete.TrySetResult(); _shutdownComplete.TrySetResult();
} }

View File

@@ -73,7 +73,7 @@ public sealed partial class NatsServer
}); });
} }
private void DisposeSignalHandlers() internal void DisposeSignalHandlers()
{ {
_sigHup?.Dispose(); _sigHup?.Dispose();
_sigTerm?.Dispose(); _sigTerm?.Dispose();

View File

@@ -27,23 +27,30 @@ public class AuthHandlerExtendedTests
} }
[Fact] [Fact]
public void GetAuthErrClosedState_ExpiredMessage_ReturnsExpiredState() public void GetAuthErrClosedState_ProxyNotTrusted_ReturnsProxyNotTrusted()
{ {
var err = new InvalidOperationException("token is expired"); var err = new AuthProxyNotTrustedException();
AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthenticationExpired); AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.ProxyNotTrusted);
} }
[Fact] [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] [Fact]
public void GetAuthErrClosedState_RevokedMessage_ReturnsRevoked() public void GetAuthErrClosedState_OtherError_ReturnsAuthenticationViolation()
{ {
var err = new InvalidOperationException("credential was revoked"); var err = new InvalidOperationException("bad credentials");
AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthRevoked); AuthHandler.GetAuthErrClosedState(err).ShouldBe(ClosedState.AuthenticationViolation);
}
[Fact]
public void GetAuthErrClosedState_NullError_ReturnsAuthenticationViolation()
{
AuthHandler.GetAuthErrClosedState(null).ShouldBe(ClosedState.AuthenticationViolation);
} }
[Fact] [Fact]

View File

@@ -40,7 +40,7 @@ public class JwtProcessorTests
public void WipeSlice_FillsWithX() public void WipeSlice_FillsWithX()
{ {
var buf = new byte[] { 0x01, 0x02, 0x03 }; var buf = new byte[] { 0x01, 0x02, 0x03 };
JwtProcessor.WipeSlice(buf); AuthHandler.WipeSlice(buf);
buf.ShouldAllBe(b => b == (byte)'x'); buf.ShouldAllBe(b => b == (byte)'x');
} }
@@ -48,7 +48,7 @@ public class JwtProcessorTests
public void WipeSlice_EmptyBuffer_NoOp() public void WipeSlice_EmptyBuffer_NoOp()
{ {
var buf = Array.Empty<byte>(); var buf = Array.Empty<byte>();
JwtProcessor.WipeSlice(buf); AuthHandler.WipeSlice(buf);
} }
// ========================================================================= // =========================================================================

View File

@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report # NATS .NET Porting Status Report
Generated: 2026-02-26 22:38:47 UTC Generated: 2026-02-26 22:49:14 UTC
## Modules (12 total) ## Modules (12 total)

37
reports/report_8c380e7.md Normal file
View File

@@ -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%)**