feat(batch9): implement f1 auth and dirstore foundations

This commit is contained in:
Joseph Doherty
2026-02-28 12:12:50 -05:00
parent 26e4729e8b
commit 78d222a86d
6 changed files with 212 additions and 38 deletions

View File

@@ -833,6 +833,58 @@ public sealed class DirJwtStore : IDisposable
// Private static helpers
// ---------------------------------------------------------------------------
/// <summary>
/// Validates the supplied path exists, and enforces whether it must be a
/// directory or regular file.
/// Mirrors Go <c>validatePathExists</c>.
/// </summary>
internal static string ValidatePathExists(string path, bool dir)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("path is not specified", nameof(path));
}
string absolutePath;
try
{
absolutePath = Path.GetFullPath(path);
}
catch (Exception ex)
{
throw new InvalidOperationException($"error parsing path [{path}]: {ex.Message}", ex);
}
if (!File.Exists(absolutePath) && !Directory.Exists(absolutePath))
{
throw new InvalidOperationException($"the path [{absolutePath}] doesn't exist");
}
var attributes = File.GetAttributes(absolutePath);
var isDirectory = (attributes & FileAttributes.Directory) == FileAttributes.Directory;
if (dir && !isDirectory)
{
throw new InvalidOperationException($"the path [{absolutePath}] is not a directory");
}
if (!dir && isDirectory)
{
throw new InvalidOperationException($"the path [{absolutePath}] is not a file");
}
return absolutePath;
}
/// <summary>
/// Validates the supplied path exists and is a directory.
/// Mirrors Go <c>validateDirPath</c>.
/// </summary>
internal static string ValidateDirPath(string path)
{
return ValidatePathExists(path, dir: true);
}
/// <summary>
/// Validates that <paramref name="dirPath"/> exists and is a directory, optionally
/// creating it when <paramref name="create"/> is true.
@@ -841,31 +893,18 @@ public sealed class DirJwtStore : IDisposable
/// </summary>
private static string NewDir(string dirPath, bool create)
{
if (string.IsNullOrEmpty(dirPath))
if (Directory.Exists(dirPath) || File.Exists(dirPath))
{
throw new ArgumentException("Path is not specified", nameof(dirPath));
}
if (Directory.Exists(dirPath))
{
return Path.GetFullPath(dirPath);
return ValidateDirPath(dirPath);
}
if (!create)
{
throw new DirectoryNotFoundException(
$"The path [{dirPath}] doesn't exist");
return ValidateDirPath(dirPath);
}
Directory.CreateDirectory(dirPath);
if (!Directory.Exists(dirPath))
{
throw new DirectoryNotFoundException(
$"Failed to create directory [{dirPath}]");
}
return Path.GetFullPath(dirPath);
return ValidateDirPath(dirPath);
}
/// <summary>
@@ -1044,6 +1083,7 @@ internal sealed class ExpirationTracker
{
// Min-heap ordered by expiration (Unix nanoseconds stored as ticks for TimeSpan compatibility).
private readonly PriorityQueue<JwtItem, long> _heap;
private readonly List<JwtItem> _compatHeap;
// Index from publicKey to JwtItem for O(1) lookup and hash tracking.
private readonly Dictionary<string, JwtItem> _idx;
@@ -1068,6 +1108,7 @@ internal sealed class ExpirationTracker
EvictOnLimit = evictOnLimit;
Ttl = ttl;
_heap = new PriorityQueue<JwtItem, long>();
_compatHeap = [];
_idx = new Dictionary<string, JwtItem>(StringComparer.Ordinal);
_lru = new LinkedList<string>();
_hash = new byte[SHA256.HashSizeInBytes];
@@ -1075,6 +1116,55 @@ internal sealed class ExpirationTracker
internal void SetTimer(Timer timer) => _timer = timer;
/// <summary>Returns the number of items in the compatibility heap.</summary>
/// <remarks>Mirrors Go <c>expirationTracker.Len</c>.</remarks>
internal int Len() => _compatHeap.Count;
/// <summary>Returns true when item <paramref name="i"/> expires before <paramref name="j"/>.</summary>
/// <remarks>Mirrors Go <c>expirationTracker.Less</c>.</remarks>
internal bool Less(int i, int j)
{
return _compatHeap[i].Expiration < _compatHeap[j].Expiration;
}
/// <summary>Swaps two compatibility heap items and updates their indexes.</summary>
/// <remarks>Mirrors Go <c>expirationTracker.Swap</c>.</remarks>
internal void Swap(int i, int j)
{
(_compatHeap[i], _compatHeap[j]) = (_compatHeap[j], _compatHeap[i]);
_compatHeap[i].Index = i;
_compatHeap[j].Index = j;
}
/// <summary>Adds an item to the compatibility heap and index maps.</summary>
/// <remarks>Mirrors Go <c>expirationTracker.Push</c>.</remarks>
internal void Push(JwtItem item)
{
item.Index = _compatHeap.Count;
_compatHeap.Add(item);
_idx[item.PublicKey] = item;
_lru.AddLast(item.PublicKey);
}
/// <summary>Removes and returns the last compatibility heap item.</summary>
/// <remarks>Mirrors Go <c>expirationTracker.Pop</c>.</remarks>
internal JwtItem Pop()
{
var n = _compatHeap.Count;
var item = _compatHeap[n - 1];
_compatHeap.RemoveAt(n - 1);
item.Index = -1;
var node = _lru.Find(item.PublicKey);
if (node != null)
{
_lru.Remove(node);
}
_idx.Remove(item.PublicKey);
return item;
}
/// <summary>
/// Adds or updates tracking for <paramref name="publicKey"/>.
/// When an entry already exists its expiration and hash are updated.
@@ -1259,6 +1349,7 @@ internal sealed class ExpirationTracker
internal void Reset()
{
_heap.Clear();
_compatHeap.Clear();
_idx.Clear();
_lru.Clear();
Array.Clear(_hash);
@@ -1352,6 +1443,7 @@ internal sealed class ExpirationTracker
/// </summary>
internal sealed class JwtItem
{
internal int Index { get; set; }
internal string PublicKey { get; }
internal long Expiration { get; set; }
internal byte[] Hash { get; set; }
@@ -1367,6 +1459,7 @@ internal sealed class JwtItem
internal JwtItem(string publicKey, long expiration, byte[] hash)
{
Index = -1;
PublicKey = publicKey;
Expiration = expiration;
Hash = hash;

View File

@@ -17,6 +17,7 @@ using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
@@ -875,6 +876,47 @@ public sealed partial class ClientConnection
}
}
/// <summary>
/// Returns true when the current TLS peer certificate matches one of the pinned
/// SPKI SHA-256 key identifiers.
/// Mirrors Go <c>client.matchesPinnedCert</c>.
/// </summary>
internal bool MatchesPinnedCert(PinnedCertSet? tlsPinnedCerts)
{
if (tlsPinnedCerts == null)
{
return true;
}
var certificate = GetTlsCertificate();
if (certificate == null)
{
Debugf("Failed pinned cert test as client did not provide a certificate");
return false;
}
byte[] subjectPublicKeyInfo;
try
{
subjectPublicKeyInfo = certificate.PublicKey.ExportSubjectPublicKeyInfo();
}
catch
{
subjectPublicKeyInfo = certificate.GetPublicKey();
}
var sha = SHA256.HashData(subjectPublicKeyInfo);
var keyId = Convert.ToHexString(sha).ToLowerInvariant();
if (!tlsPinnedCerts.Contains(keyId))
{
Debugf("Failed pinned cert test for key id: {0}", keyId);
return false;
}
return true;
}
internal void SetAccount(INatsAccount? acc)
{
lock (_mu) { Account = acc; }

View File

@@ -14,7 +14,6 @@
// Adapted from server/reload.go in the NATS server Go source.
using System.Reflection;
using System.Security.Cryptography;
using System.Text.Json;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
@@ -1331,26 +1330,7 @@ public sealed partial class NatsServer
private static bool MatchesPinnedCert(ClientConnection client, PinnedCertSet? pinnedCerts)
{
if (pinnedCerts == null || pinnedCerts.Count == 0)
return true;
var certificate = client.GetTlsCertificate();
if (certificate == null)
return false;
byte[] keyBytes;
try
{
keyBytes = certificate.PublicKey.ExportSubjectPublicKeyInfo();
}
catch
{
keyBytes = certificate.GetPublicKey();
}
var hash = SHA256.HashData(keyBytes);
var hex = Convert.ToHexString(hash).ToLowerInvariant();
return pinnedCerts.Contains(hex);
return client.MatchesPinnedCert(pinnedCerts);
}
}