feat(batch10): task3 implement ocsp cache core stats and compression

This commit is contained in:
Joseph Doherty
2026-02-28 12:59:26 -05:00
parent 4b2875141c
commit 98cf350383
7 changed files with 534 additions and 64 deletions

View File

@@ -103,6 +103,8 @@ public sealed class OcspResponse
public DateTime ThisUpdate { get; init; }
/// <summary><see cref="DateTime.MinValue"/> means "not set" (CA did not supply NextUpdate).</summary>
public DateTime NextUpdate { get; init; }
/// <summary>Raw OCSP response payload bytes when available.</summary>
public byte[]? Raw { get; init; }
/// <summary>Optional delegated signer certificate (RFC 6960 §4.2.2.2).</summary>
public X509Certificate2? Certificate { get; init; }
}

View File

@@ -19,6 +19,21 @@ internal static class OcspHandler
private const string TlsFeaturesOid = "1.3.6.1.5.5.7.1.24";
private const int StatusRequestExtension = 5;
internal const string OcspResponseCacheDefaultDir = "_rc_";
internal const string OcspResponseCacheTypeNone = "none";
internal const string OcspResponseCacheTypeLocal = "local";
internal static readonly TimeSpan OcspResponseCacheMinimumSaveInterval = TimeSpan.FromSeconds(1);
internal static readonly TimeSpan OcspResponseCacheDefaultSaveInterval = TimeSpan.FromMinutes(5);
internal static OcspResponseCacheConfig NewOCSPResponseCacheConfig() =>
new()
{
Type = OcspResponseCacheTypeLocal,
LocalStore = OcspResponseCacheDefaultDir,
PreserveRevoked = false,
SaveInterval = OcspResponseCacheDefaultSaveInterval.TotalSeconds,
};
internal static (List<X509Certificate2>? certificates, Exception? error) ParseCertPEM(string name)
{
try

View File

@@ -16,6 +16,7 @@
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.IO.Compression;
using System.Net.Http;
using System.Text;
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
@@ -403,6 +404,15 @@ public sealed class OcspResponseCacheStats
public long Unknowns { get; set; }
}
internal sealed class OcspResponseCacheItem
{
public string Subject { get; set; } = string.Empty;
public DateTime CachedAt { get; set; }
public OcspStatusAssertion RespStatus { get; set; }
public DateTime RespExpires { get; set; }
public byte[] Resp { get; set; } = [];
}
/// <summary>
/// A no-op OCSP cache that never stores anything.
/// Mirrors Go <c>NoOpCache</c> in server/ocsp_responsecache.go.
@@ -424,9 +434,17 @@ internal sealed class NoOpCache : IOcspResponseCache
_config = config;
}
public byte[]? Get(string key) => null;
public byte[]? Get(string key)
{
_ = key;
return null;
}
public void Put(string key, byte[] response) { }
public void Put(string key, byte[] response)
{
_ = key;
_ = response;
}
public void Remove(string key) => Delete(key);
@@ -497,42 +515,415 @@ internal sealed class NoOpCache : IOcspResponseCache
/// </summary>
internal sealed class LocalDirCache : IOcspResponseCache
{
private readonly string _dir;
private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.NoRecursion);
private readonly OcspResponseCacheConfig _config;
private readonly Dictionary<string, OcspResponseCacheItem> _cache = new(StringComparer.Ordinal);
private OcspResponseCacheStats? _stats;
private bool _online;
private bool _dirty;
private readonly TimeSpan _saveInterval;
private Timer? _timer;
public LocalDirCache(string dir)
: this(new OcspResponseCacheConfig
{
Type = OcspHandler.OcspResponseCacheTypeLocal,
LocalStore = dir,
PreserveRevoked = false,
SaveInterval = OcspHandler.OcspResponseCacheDefaultSaveInterval.TotalSeconds,
})
{
_dir = dir;
}
public byte[]? Get(string key)
public LocalDirCache(OcspResponseCacheConfig config)
{
var file = CacheFilePath(key);
if (!File.Exists(file))
return null;
return File.ReadAllBytes(file);
_config = config ?? OcspHandler.NewOCSPResponseCacheConfig();
if (string.IsNullOrEmpty(_config.Type))
{
_config.Type = OcspHandler.OcspResponseCacheTypeLocal;
}
if (string.IsNullOrEmpty(_config.LocalStore))
{
_config.LocalStore = OcspHandler.OcspResponseCacheDefaultDir;
}
var saveSeconds = _config.SaveInterval <= 0
? OcspHandler.OcspResponseCacheDefaultSaveInterval.TotalSeconds
: _config.SaveInterval;
var configuredInterval = TimeSpan.FromSeconds(saveSeconds);
_saveInterval = configuredInterval < OcspHandler.OcspResponseCacheMinimumSaveInterval
? OcspHandler.OcspResponseCacheMinimumSaveInterval
: configuredInterval;
}
public byte[]? Get(string key) => Get(key, log: null);
public byte[]? Get(string key, OcspLog? log)
{
_mu.EnterReadLock();
try
{
if (!_online || string.IsNullOrEmpty(key))
{
return null;
}
if (!_cache.TryGetValue(key, out var item))
{
if (_stats is not null)
{
_stats.Misses++;
}
return null;
}
if (_stats is not null)
{
_stats.Hits++;
}
var (decompressed, error) = Decompress(item.Resp);
if (error != null)
{
log?.Errorf?.Invoke(OcspMessages.ErrResponseDecompressFail, [key, error]);
return null;
}
return decompressed;
}
finally
{
_mu.ExitReadLock();
}
}
public void Put(string key, byte[] response)
{
ArgumentException.ThrowIfNullOrEmpty(key);
ArgumentNullException.ThrowIfNull(response);
var status = OcspStatusAssertion.Unknown;
var (parsed, error) = OcspHandler.ParseOcspResponse(response);
if (error == null && parsed != null)
{
status = parsed.Status;
}
Directory.CreateDirectory(_dir);
File.WriteAllBytes(CacheFilePath(key), response);
var ocspResponse = new OcspResponse
{
Status = status,
ThisUpdate = DateTime.UtcNow,
NextUpdate = DateTime.MinValue,
Raw = [.. response],
};
Put(key, ocspResponse, string.Empty);
}
public void Remove(string key)
public void Put(string key, OcspResponse response, string subject, OcspLog? log = null)
{
var file = CacheFilePath(key);
if (File.Exists(file))
File.Delete(file);
if (response == null)
{
return;
}
_mu.EnterReadLock();
try
{
if (!_online || string.IsNullOrEmpty(key))
{
return;
}
}
finally
{
_mu.ExitReadLock();
}
var raw = response.Raw ?? [];
var (compressed, error) = Compress(raw);
if (error != null || compressed == null)
{
log?.Errorf?.Invoke(OcspMessages.ErrResponseCompressFail, [key, error ?? new InvalidOperationException("compression failed")]);
return;
}
_mu.EnterWriteLock();
try
{
if (_cache.TryGetValue(key, out var existing))
{
AdjustStats(-1, existing.RespStatus);
}
var item = new OcspResponseCacheItem
{
Subject = subject,
CachedAt = DateTime.UtcNow,
RespStatus = response.Status,
RespExpires = response.NextUpdate,
Resp = compressed,
};
_cache[key] = item;
AdjustStats(1, item.RespStatus);
_dirty = true;
}
finally
{
_mu.ExitWriteLock();
}
}
internal void AdjustStatsHitToMiss()
{
if (_stats is null)
{
return;
}
_stats.Misses++;
_stats.Hits--;
}
internal void AdjustStats(long delta, OcspStatusAssertion status)
{
if (delta == 0 || _stats is null)
{
return;
}
_stats.Responses += delta;
switch (status)
{
case OcspStatusAssertion.Good:
_stats.Goods += delta;
break;
case OcspStatusAssertion.Revoked:
_stats.Revokes += delta;
break;
case OcspStatusAssertion.Unknown:
_stats.Unknowns += delta;
break;
}
}
public void Remove(string key) => Delete(key, wasMiss: false, log: null);
public void Delete(string key, bool wasMiss = false, OcspLog? log = null)
{
_mu.EnterWriteLock();
try
{
if (!_online || string.IsNullOrEmpty(key))
{
return;
}
if (!_cache.TryGetValue(key, out var item))
{
return;
}
if (item.RespStatus == OcspStatusAssertion.Revoked && _config.PreserveRevoked)
{
if (wasMiss)
{
AdjustStatsHitToMiss();
}
return;
}
_cache.Remove(key);
AdjustStats(-1, item.RespStatus);
if (wasMiss)
{
AdjustStatsHitToMiss();
}
_dirty = true;
}
finally
{
_mu.ExitWriteLock();
}
}
public void Start(NatsServer? server = null)
{
_ = server;
InitStats();
_mu.EnterWriteLock();
try
{
_online = true;
_timer ??= new Timer(_ => { }, null, _saveInterval, _saveInterval);
}
finally
{
_mu.ExitWriteLock();
}
}
public void Stop(NatsServer? server = null)
{
_ = server;
_mu.EnterWriteLock();
try
{
_online = false;
if (_dirty)
{
_dirty = false;
}
_timer?.Dispose();
_timer = null;
}
finally
{
_mu.ExitWriteLock();
}
}
public bool Online()
{
_mu.EnterReadLock();
try
{
return _online;
}
finally
{
_mu.ExitReadLock();
}
}
public string Type()
{
_mu.EnterReadLock();
try
{
return _config.Type;
}
finally
{
_mu.ExitReadLock();
}
}
public OcspResponseCacheConfig Config()
{
_mu.EnterReadLock();
try
{
return _config;
}
finally
{
_mu.ExitReadLock();
}
}
public OcspResponseCacheStats? Stats()
{
_mu.EnterReadLock();
try
{
if (_stats == null)
{
return null;
}
return new OcspResponseCacheStats
{
Responses = _stats.Responses,
Hits = _stats.Hits,
Misses = _stats.Misses,
Revokes = _stats.Revokes,
Goods = _stats.Goods,
Unknowns = _stats.Unknowns,
};
}
finally
{
_mu.ExitReadLock();
}
}
internal void InitStats()
{
_mu.EnterWriteLock();
try
{
_stats = new OcspResponseCacheStats
{
Responses = _cache.Count,
};
foreach (var entry in _cache.Values)
{
switch (entry.RespStatus)
{
case OcspStatusAssertion.Good:
_stats.Goods++;
break;
case OcspStatusAssertion.Revoked:
_stats.Revokes++;
break;
case OcspStatusAssertion.Unknown:
_stats.Unknowns++;
break;
}
}
}
finally
{
_mu.ExitWriteLock();
}
}
public (byte[]? compressed, Exception? error) Compress(ReadOnlySpan<byte> buffer)
{
try
{
using var output = new MemoryStream();
using (var writer = new BrotliStream(output, CompressionLevel.Fastest, leaveOpen: true))
{
writer.Write(buffer);
}
return (output.ToArray(), null);
}
catch (Exception ex)
{
return (null, ex);
}
}
public (byte[]? decompressed, Exception? error) Decompress(ReadOnlySpan<byte> buffer)
{
try
{
using var input = new MemoryStream(buffer.ToArray());
using var reader = new BrotliStream(input, System.IO.Compression.CompressionMode.Decompress);
using var output = new MemoryStream();
reader.CopyTo(output);
return (output.ToArray(), null);
}
catch (Exception ex)
{
return (null, ex);
}
}
private string CacheFilePath(string key)
{
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key));
var file = Convert.ToHexString(hash).ToLowerInvariant();
return Path.Combine(_dir, $"{file}.ocsp");
return Path.Combine(_config.LocalStore, $"{file}.ocsp");
}
}