fix: session B — Go-faithful auth error states, NKey padding, permissions, signal disposal
This commit is contained in:
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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") { }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ public enum ClosedState
|
|||||||
Kicked,
|
Kicked,
|
||||||
ProxyNotTrusted,
|
ProxyNotTrusted,
|
||||||
ProxyRequired,
|
ProxyRequired,
|
||||||
AuthRevoked,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public sealed partial class NatsServer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeSignalHandlers()
|
internal void DisposeSignalHandlers()
|
||||||
{
|
{
|
||||||
_sigHup?.Dispose();
|
_sigHup?.Dispose();
|
||||||
_sigTerm?.Dispose();
|
_sigTerm?.Dispose();
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -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
37
reports/report_8c380e7.md
Normal 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%)**
|
||||||
Reference in New Issue
Block a user