diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs
new file mode 100644
index 0000000..14a6499
--- /dev/null
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs
@@ -0,0 +1,213 @@
+// Copyright 2012-2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0
+
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text.Json;
+using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
+
+namespace ZB.MOM.NatsNet.Server;
+
+///
+/// OCSP helper functions mapped from server/ocsp.go package-level helpers.
+///
+internal static class OcspHandler
+{
+ private const string CertPemLabel = "CERTIFICATE";
+
+ internal static (List? certificates, Exception? error) ParseCertPEM(string name)
+ {
+ try
+ {
+ var text = File.ReadAllText(name);
+ var span = text.AsSpan();
+ var certificates = new List();
+
+ while (PemEncoding.TryFind(span, out var fields))
+ {
+ var label = span[fields.Label].ToString();
+ if (!string.Equals(label, CertPemLabel, StringComparison.Ordinal))
+ {
+ return (null, new InvalidOperationException($"unexpected PEM certificate type: {label}"));
+ }
+
+ var derBytes = Convert.FromBase64String(span[fields.Base64Data].ToString());
+ certificates.Add(new X509Certificate2(derBytes));
+ span = span[fields.Location.End..];
+ }
+
+ if (certificates.Count == 0)
+ {
+ return (null, new InvalidOperationException("failed to parse certificate pem"));
+ }
+
+ return (certificates, null);
+ }
+ catch (Exception ex)
+ {
+ return (null, ex);
+ }
+ }
+
+ internal static (X509Certificate2? issuer, Exception? error) GetOCSPIssuerLocally(
+ IReadOnlyList trustedCAs,
+ IReadOnlyList certBundle)
+ {
+ if (certBundle.Count == 0)
+ {
+ return (null, new InvalidOperationException("invalid ocsp ca configuration"));
+ }
+
+ var leaf = certBundle[0];
+ if (certBundle.Count > 1)
+ {
+ var issuerCandidate = certBundle[1];
+ if (!string.Equals(leaf.Issuer, issuerCandidate.Subject, StringComparison.Ordinal))
+ {
+ return (null, new InvalidOperationException("invalid issuer configuration: issuer subject mismatch"));
+ }
+ return (issuerCandidate, null);
+ }
+
+ using var chain = new X509Chain();
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
+
+ if (trustedCAs.Count > 0)
+ {
+ chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
+ foreach (var ca in trustedCAs)
+ {
+ chain.ChainPolicy.CustomTrustStore.Add(ca);
+ }
+ }
+
+ if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
+ {
+ return (null, null);
+ }
+
+ return (chain.ChainElements[1].Certificate, null);
+ }
+
+ internal static (X509Certificate2? issuer, Exception? error) GetOCSPIssuer(string caFile, IReadOnlyList chain)
+ {
+ var trustedCAs = new List();
+ if (!string.IsNullOrEmpty(caFile))
+ {
+ var (parsed, parseError) = ParseCertPEM(caFile);
+ if (parseError != null)
+ {
+ return (null, new InvalidOperationException($"failed to parse ca_file: {parseError.Message}", parseError));
+ }
+
+ trustedCAs.AddRange(parsed!);
+ }
+
+ var certBundle = new List(chain.Count);
+ foreach (var certBytes in chain)
+ {
+ try
+ {
+ certBundle.Add(new X509Certificate2(certBytes));
+ }
+ catch (Exception ex)
+ {
+ return (null, new InvalidOperationException($"failed to parse cert: {ex.Message}", ex));
+ }
+ }
+
+ var (issuer, issuerError) = GetOCSPIssuerLocally(trustedCAs, certBundle);
+ if (issuerError != null || issuer == null)
+ {
+ return (null, new InvalidOperationException("no issuers found"));
+ }
+
+ if (!IsCertificateAuthority(issuer))
+ {
+ return (null, new InvalidOperationException(
+ string.Create(CultureInfo.InvariantCulture, $"{issuer.Subject} invalid ca basic constraints: is not ca")));
+ }
+
+ return (issuer, null);
+ }
+
+ internal static string OcspStatusString(int status) => status switch
+ {
+ 0 => "good",
+ 1 => "revoked",
+ _ => "unknown",
+ };
+
+ internal static Exception? ValidOCSPResponse(OcspResponse response, DateTime? nowUtc = null)
+ {
+ var now = nowUtc ?? DateTime.UtcNow;
+
+ if (response.NextUpdate != DateTime.MinValue && response.NextUpdate < now)
+ {
+ var t = response.NextUpdate.ToString("O", CultureInfo.InvariantCulture);
+ return new InvalidOperationException($"invalid ocsp NextUpdate, is past time: {t}");
+ }
+
+ if (response.ThisUpdate > now)
+ {
+ var t = response.ThisUpdate.ToString("O", CultureInfo.InvariantCulture);
+ return new InvalidOperationException($"invalid ocsp ThisUpdate, is future time: {t}");
+ }
+
+ return null;
+ }
+
+ internal static (OcspResponse? response, Exception? error) ParseOcspResponse(byte[] raw)
+ {
+ try
+ {
+ var parsed = JsonSerializer.Deserialize(raw);
+ if (parsed == null)
+ {
+ return (null, new InvalidOperationException("failed to parse OCSP response"));
+ }
+
+ var status = parsed.Status switch
+ {
+ 0 => OcspStatusAssertion.Good,
+ 1 => OcspStatusAssertion.Revoked,
+ _ => OcspStatusAssertion.Unknown,
+ };
+
+ var response = new OcspResponse
+ {
+ Status = status,
+ ThisUpdate = parsed.ThisUpdate,
+ NextUpdate = parsed.NextUpdate ?? DateTime.MinValue,
+ };
+
+ return (response, null);
+ }
+ catch (Exception ex)
+ {
+ return (null, ex);
+ }
+ }
+
+ private static bool IsCertificateAuthority(X509Certificate2 cert)
+ {
+ foreach (var extension in cert.Extensions)
+ {
+ if (extension is X509BasicConstraintsExtension basicConstraints)
+ {
+ return basicConstraints.CertificateAuthority;
+ }
+ }
+
+ return false;
+ }
+
+ private sealed class SerializedOcspResponse
+ {
+ public int Status { get; set; }
+ public DateTime ThisUpdate { get; set; }
+ public DateTime? NextUpdate { get; set; }
+ }
+}
diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs
index fe6742a..7a078e8 100644
--- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs
+++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs
@@ -16,7 +16,9 @@
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
+using System.Net.Http;
using System.Text;
+using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
namespace ZB.MOM.NatsNet.Server.Auth.Ocsp;
@@ -71,9 +73,15 @@ internal sealed class OcspStaple
///
internal sealed class OcspMonitor
{
+ private const string DefaultOcspStoreDir = "ocsp";
+ private static readonly TimeSpan DefaultOcspCheckInterval = TimeSpan.FromHours(24);
+ private static readonly TimeSpan MinOcspCheckInterval = TimeSpan.FromMinutes(2);
+
private readonly Lock _mu = new();
private Timer? _timer;
private readonly OcspStaple _staple = new();
+ private byte[]? _raw;
+ private OcspResponse? _response;
/// Path to the TLS certificate file being monitored.
public string? CertFile { get; set; }
@@ -93,9 +101,229 @@ internal sealed class OcspMonitor
/// The owning server instance.
public NatsServer? Server { get; set; }
+ /// The monitored certificate leaf.
+ public X509Certificate2? Leaf { get; set; }
+
+ /// The monitored certificate issuer.
+ public X509Certificate2? Issuer { get; set; }
+
+ /// HTTP client for remote OCSP fetch attempts.
+ public HttpClient? HttpClient { get; set; }
+
+ /// When true, monitor exits on revoked/unknown status.
+ public bool ShutdownOnRevoke { get; set; }
+
/// The synchronisation lock for this monitor's mutable state.
public Lock Mu => _mu;
+ ///
+ /// Calculates the next polling delay based on .
+ /// Mirrors Go OCSPMonitor.getNextRun.
+ ///
+ internal TimeSpan GetNextRun()
+ {
+ DateTime nextUpdate;
+ lock (_mu)
+ {
+ nextUpdate = _response?.NextUpdate ?? DateTime.MinValue;
+ }
+
+ if (nextUpdate == DateTime.MinValue)
+ {
+ return DefaultOcspCheckInterval;
+ }
+
+ var duration = (nextUpdate - DateTime.UtcNow) / 2;
+ if (duration < TimeSpan.Zero)
+ {
+ return MinOcspCheckInterval;
+ }
+
+ return duration;
+ }
+
+ ///
+ /// Returns currently cached OCSP raw bytes and parsed response.
+ /// Mirrors Go OCSPMonitor.getCacheStatus.
+ ///
+ internal (byte[]? raw, OcspResponse? response) GetCacheStatus()
+ {
+ lock (_mu)
+ {
+ return (_raw is null ? null : [.. _raw], _response);
+ }
+ }
+
+ ///
+ /// Resolves OCSP status from cache, local store, then remote fetch fallback.
+ /// Mirrors Go OCSPMonitor.getStatus.
+ ///
+ internal (byte[]? raw, OcspResponse? response, Exception? error) GetStatus()
+ {
+ var (cachedRaw, cachedResponse) = GetCacheStatus();
+ if (cachedRaw is { Length: > 0 } && cachedResponse != null)
+ {
+ var validityError = OcspHandler.ValidOCSPResponse(cachedResponse);
+ if (validityError == null)
+ {
+ return (cachedRaw, cachedResponse, null);
+ }
+ }
+
+ var (localRaw, localResponse, localError) = GetLocalStatus();
+ if (localError == null)
+ {
+ return (localRaw, localResponse, null);
+ }
+
+ return GetRemoteStatus();
+ }
+
+ ///
+ /// Loads and validates an OCSP response from local store_dir cache.
+ /// Mirrors Go OCSPMonitor.getLocalStatus.
+ ///
+ internal (byte[]? raw, OcspResponse? response, Exception? error) GetLocalStatus()
+ {
+ var storeDir = Server?.Options.StoreDir ?? string.Empty;
+ if (string.IsNullOrEmpty(storeDir))
+ {
+ return (null, null, new InvalidOperationException("store_dir not set"));
+ }
+
+ if (Leaf == null)
+ {
+ return (null, null, new InvalidOperationException("leaf certificate not set"));
+ }
+
+ var key = Convert.ToHexString(SHA256.HashData(Leaf.RawData)).ToLowerInvariant();
+ var path = Path.Combine(storeDir, DefaultOcspStoreDir, key);
+
+ byte[] raw;
+ try
+ {
+ lock (_mu)
+ {
+ raw = File.ReadAllBytes(path);
+ }
+ }
+ catch (Exception ex)
+ {
+ return (null, null, ex);
+ }
+
+ var (response, parseError) = OcspHandler.ParseOcspResponse(raw);
+ if (parseError != null)
+ {
+ return (null, null, new InvalidOperationException($"failed to get local status: {parseError.Message}", parseError));
+ }
+
+ var validityError = OcspHandler.ValidOCSPResponse(response!);
+ if (validityError != null)
+ {
+ return (null, null, validityError);
+ }
+
+ lock (_mu)
+ {
+ _raw = [.. raw];
+ _response = response;
+ _staple.Response = [.. raw];
+ _staple.NextUpdate = response!.NextUpdate;
+ }
+
+ return (raw, response, null);
+ }
+
+ ///
+ /// Attempts to fetch OCSP status remotely from configured responders.
+ /// Mirrors Go OCSPMonitor.getRemoteStatus.
+ ///
+ internal (byte[]? raw, OcspResponse? response, Exception? error) GetRemoteStatus()
+ {
+ var responders = Server?.Options.OcspConfig?.OverrideUrls ?? [];
+ if (responders.Count == 0)
+ {
+ return (null, null, new InvalidOperationException("no available ocsp servers"));
+ }
+
+ return (null, null, new InvalidOperationException("remote OCSP fetching is not implemented"));
+ }
+
+ ///
+ /// Monitor loop that periodically refreshes OCSP status.
+ /// Mirrors Go OCSPMonitor.run.
+ ///
+ internal async Task Run(CancellationToken cancellationToken = default)
+ {
+ var (_, response, error) = GetStatus();
+ if (error != null || response == null)
+ {
+ return;
+ }
+
+ var nextRun = response.Status == OcspStatusAssertion.Good
+ ? GetNextRun()
+ : MinOcspCheckInterval;
+
+ if (response.Status != OcspStatusAssertion.Good && ShutdownOnRevoke)
+ {
+ return;
+ }
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(nextRun, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ var (_, updated, updateError) = GetRemoteStatus();
+ if (updateError != null || updated == null)
+ {
+ nextRun = GetNextRun();
+ continue;
+ }
+
+ if (updated.Status != OcspStatusAssertion.Good)
+ {
+ return;
+ }
+
+ nextRun = GetNextRun();
+ }
+ }
+
+ ///
+ /// Writes OCSP bytes to a temporary file and atomically renames to target.
+ /// Mirrors Go OCSPMonitor.writeOCSPStatus.
+ ///
+ internal Exception? WriteOCSPStatus(string storeDir, string file, byte[] data)
+ {
+ try
+ {
+ var ocspDir = Path.Combine(storeDir, DefaultOcspStoreDir);
+ Directory.CreateDirectory(ocspDir);
+
+ var tempPath = Path.Combine(ocspDir, $"tmp-cert-status-{Path.GetRandomFileName()}");
+ File.WriteAllBytes(tempPath, data);
+
+ lock (_mu)
+ {
+ File.Move(tempPath, Path.Combine(ocspDir, file), overwrite: true);
+ }
+ return null;
+ }
+ catch (Exception ex)
+ {
+ return ex;
+ }
+ }
+
/// Starts the background OCSP refresh timer.
public void Start()
{
@@ -109,7 +337,12 @@ internal sealed class OcspMonitor
lock (_mu)
{
if (!string.IsNullOrEmpty(OcspStapleFile) && File.Exists(OcspStapleFile))
+ {
_staple.Response = File.ReadAllBytes(OcspStapleFile);
+ _raw = [.. _staple.Response];
+ var (response, _) = OcspHandler.ParseOcspResponse(_raw);
+ _response = response;
+ }
_staple.NextUpdate = DateTime.UtcNow + CheckInterval;
}
}, null, TimeSpan.Zero, CheckInterval);
diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspFoundationTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspFoundationTests.cs
new file mode 100644
index 0000000..266bde6
--- /dev/null
+++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspFoundationTests.cs
@@ -0,0 +1,287 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text.Json;
+using Shouldly;
+using ZB.MOM.NatsNet.Server;
+using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
+using ZB.MOM.NatsNet.Server.Auth.Ocsp;
+
+namespace ZB.MOM.NatsNet.Server.Tests.Auth;
+
+public sealed class OcspFoundationTests : IDisposable
+{
+ private readonly List _tempDirs = [];
+ private readonly List _certs = [];
+
+ [Fact]
+ public void GetNextRun_NoCachedResponse_ReturnsDefaultInterval()
+ {
+ var monitor = new OcspMonitor();
+
+ monitor.GetNextRun().ShouldBe(TimeSpan.FromHours(24));
+ }
+
+ [Fact]
+ public void GetLocalStatus_StoreDirMissing_ReturnsError()
+ {
+ var monitor = new OcspMonitor
+ {
+ Server = NewServer(new ServerOptions()),
+ Leaf = CreateSelfSignedCertificate("CN=leaf"),
+ Issuer = CreateSelfSignedCertificate("CN=issuer"),
+ };
+
+ var (_, _, err) = monitor.GetLocalStatus();
+
+ err.ShouldNotBeNull();
+ err!.Message.ShouldContain("store_dir");
+ }
+
+ [Fact]
+ public void GetStatus_UsesLocalStatus_WhenCacheIsEmpty()
+ {
+ var dir = MakeTempDir();
+ var monitor = NewMonitorWithStore(dir);
+ var key = GetLeafKey(monitor.Leaf!);
+ var responseBytes = SerializeResponse(
+ status: (int)OcspStatusAssertion.Good,
+ thisUpdate: DateTime.UtcNow.AddMinutes(-1),
+ nextUpdate: DateTime.UtcNow.AddHours(1));
+
+ Directory.CreateDirectory(Path.Combine(dir, "ocsp"));
+ File.WriteAllBytes(Path.Combine(dir, "ocsp", key), responseBytes);
+
+ var (raw, response, err) = monitor.GetStatus();
+
+ err.ShouldBeNull();
+ raw.ShouldNotBeNull();
+ response.ShouldNotBeNull();
+ response.Status.ShouldBe(OcspStatusAssertion.Good);
+ }
+
+ [Fact]
+ public void GetRemoteStatus_NoResponders_ReturnsError()
+ {
+ var monitor = NewMonitorWithStore(MakeTempDir());
+
+ var (_, _, err) = monitor.GetRemoteStatus();
+
+ err.ShouldNotBeNull();
+ err!.Message.ShouldContain("no available ocsp servers");
+ }
+
+ [Fact]
+ public void WriteOcspStatus_ValidPath_WritesFile()
+ {
+ var dir = MakeTempDir();
+ Directory.CreateDirectory(Path.Combine(dir, "ocsp"));
+ var monitor = NewMonitorWithStore(dir);
+
+ var err = monitor.WriteOCSPStatus(dir, "status.bin", [1, 2, 3, 4]);
+
+ err.ShouldBeNull();
+ var path = Path.Combine(dir, "ocsp", "status.bin");
+ File.Exists(path).ShouldBeTrue();
+ File.ReadAllBytes(path).ShouldBe([1, 2, 3, 4]);
+ }
+
+ [Fact]
+ public async Task Run_CancelledToken_Completes()
+ {
+ var monitor = new OcspMonitor();
+ using var cts = new CancellationTokenSource();
+ cts.Cancel();
+
+ await monitor.Run(cts.Token);
+ }
+
+ [Fact]
+ public void ParseCertPem_CertificateFile_ReturnsCertificate()
+ {
+ var cert = CreateSelfSignedCertificate("CN=parsecert");
+ var path = Path.Combine(MakeTempDir(), "ca.pem");
+ File.WriteAllText(path, cert.ExportCertificatePem());
+
+ var (certs, err) = OcspHandler.ParseCertPEM(path);
+
+ err.ShouldBeNull();
+ certs.ShouldNotBeNull();
+ certs.Count.ShouldBe(1);
+ }
+
+ [Fact]
+ public void GetOcspIssuerLocally_LeafAndIssuerBundle_ReturnsIssuer()
+ {
+ var (leaf, issuer) = CreateLeafAndIssuer();
+
+ var (resolvedIssuer, err) = OcspHandler.GetOCSPIssuerLocally([], [leaf, issuer]);
+
+ err.ShouldBeNull();
+ resolvedIssuer.ShouldNotBeNull();
+ resolvedIssuer!.Subject.ShouldBe(issuer.Subject);
+ }
+
+ [Fact]
+ public void GetOcspIssuer_WithChain_ReturnsIssuer()
+ {
+ var (leaf, issuer) = CreateLeafAndIssuer();
+ var chain = new[] { leaf.RawData, issuer.RawData };
+
+ var (resolvedIssuer, err) = OcspHandler.GetOCSPIssuer(string.Empty, chain);
+
+ err.ShouldBeNull();
+ resolvedIssuer.ShouldNotBeNull();
+ resolvedIssuer!.Subject.ShouldBe(issuer.Subject);
+ }
+
+ [Theory]
+ [InlineData(0, "good")]
+ [InlineData(1, "revoked")]
+ [InlineData(99, "unknown")]
+ public void OcspStatusString_AnyStatus_ReturnsExpectedString(int status, string expected)
+ {
+ OcspHandler.OcspStatusString(status).ShouldBe(expected);
+ }
+
+ [Fact]
+ public void ValidOcspResponse_ExpiredNextUpdate_ReturnsError()
+ {
+ var response = new OcspResponse
+ {
+ Status = OcspStatusAssertion.Good,
+ ThisUpdate = DateTime.UtcNow.AddMinutes(-2),
+ NextUpdate = DateTime.UtcNow.AddMinutes(-1),
+ };
+
+ var err = OcspHandler.ValidOCSPResponse(response);
+
+ err.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public void ValidOcspResponse_CurrentResponse_ReturnsNull()
+ {
+ var response = new OcspResponse
+ {
+ Status = OcspStatusAssertion.Good,
+ ThisUpdate = DateTime.UtcNow.AddMinutes(-1),
+ NextUpdate = DateTime.UtcNow.AddMinutes(10),
+ };
+
+ var err = OcspHandler.ValidOCSPResponse(response);
+
+ err.ShouldBeNull();
+ }
+
+ public void Dispose()
+ {
+ foreach (var cert in _certs)
+ {
+ cert.Dispose();
+ }
+
+ foreach (var dir in _tempDirs)
+ {
+ try { Directory.Delete(dir, recursive: true); } catch { }
+ }
+ }
+
+ private NatsServer NewServer(ServerOptions options)
+ {
+ var (server, err) = NatsServer.NewServer(options);
+ err.ShouldBeNull();
+ return server!;
+ }
+
+ private OcspMonitor NewMonitorWithStore(string storeDir)
+ {
+ var opts = new ServerOptions
+ {
+ StoreDir = storeDir,
+ OcspConfig = new OcspConfig(),
+ };
+
+ return new OcspMonitor
+ {
+ Server = NewServer(opts),
+ Leaf = CreateSelfSignedCertificate("CN=leaf"),
+ Issuer = CreateSelfSignedCertificate("CN=issuer"),
+ };
+ }
+
+ private string MakeTempDir()
+ {
+ var path = Path.Combine(Path.GetTempPath(), "ocsp-foundation-" + Path.GetRandomFileName());
+ Directory.CreateDirectory(path);
+ _tempDirs.Add(path);
+ return path;
+ }
+
+ private X509Certificate2 CreateSelfSignedCertificate(string subjectName)
+ {
+ var request = new CertificateRequest(
+ subjectName,
+ RSA.Create(2048),
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+
+ var cert = request.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(30));
+
+ _certs.Add(cert);
+ return cert;
+ }
+
+ private (X509Certificate2 leaf, X509Certificate2 issuer) CreateLeafAndIssuer()
+ {
+ var issuerRequest = new CertificateRequest(
+ "CN=issuer",
+ RSA.Create(2048),
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+ issuerRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
+ issuerRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(issuerRequest.PublicKey, false));
+
+ var issuer = issuerRequest.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-2),
+ DateTimeOffset.UtcNow.AddDays(365));
+
+ var leafRequest = new CertificateRequest(
+ "CN=leaf",
+ RSA.Create(2048),
+ HashAlgorithmName.SHA256,
+ RSASignaturePadding.Pkcs1);
+ leafRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
+ leafRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(leafRequest.PublicKey, false));
+
+ var serial = new byte[16];
+ RandomNumberGenerator.Fill(serial);
+ var leaf = leafRequest.Create(
+ issuer,
+ DateTimeOffset.UtcNow.AddDays(-1),
+ DateTimeOffset.UtcNow.AddDays(90),
+ serial);
+
+ _certs.Add(issuer);
+ _certs.Add(leaf);
+ return (leaf, issuer);
+ }
+
+ private static byte[] SerializeResponse(int status, DateTime thisUpdate, DateTime nextUpdate)
+ {
+ var payload = new
+ {
+ Status = status,
+ ThisUpdate = thisUpdate,
+ NextUpdate = nextUpdate,
+ };
+ return JsonSerializer.SerializeToUtf8Bytes(payload);
+ }
+
+ private static string GetLeafKey(X509Certificate2 cert)
+ {
+ var hash = SHA256.HashData(cert.RawData);
+ return Convert.ToHexString(hash).ToLowerInvariant();
+ }
+}
diff --git a/porting.db b/porting.db
index dc3392b..6aa7eea 100644
Binary files a/porting.db and b/porting.db differ