feat(batch9): implement f1 auth and dirstore foundations
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user