diff --git a/src/NATS.Server/Events/EventJsonContext.cs b/src/NATS.Server/Events/EventJsonContext.cs
index d601bff..7998a16 100644
--- a/src/NATS.Server/Events/EventJsonContext.cs
+++ b/src/NATS.Server/Events/EventJsonContext.cs
@@ -11,4 +11,5 @@ namespace NATS.Server.Events;
[JsonSerializable(typeof(LameDuckEventMsg))]
[JsonSerializable(typeof(AuthErrorEventMsg))]
[JsonSerializable(typeof(OcspPeerRejectEventMsg))]
+[JsonSerializable(typeof(OcspChainValidationEvent))]
internal partial class EventJsonContext : JsonSerializerContext;
diff --git a/src/NATS.Server/Events/EventSubjects.cs b/src/NATS.Server/Events/EventSubjects.cs
index 81401c6..7b6d35e 100644
--- a/src/NATS.Server/Events/EventSubjects.cs
+++ b/src/NATS.Server/Events/EventSubjects.cs
@@ -40,6 +40,11 @@ public static class EventSubjects
// Inbox for responses
public const string InboxResponse = "$SYS._INBOX_.{0}";
+ // OCSP advisory events
+ // Go reference: ocsp.go — OCSP peer reject and chain validation subjects.
+ public const string OcspPeerReject = "$SYS.SERVER.{0}.OCSP.PEER.REJECT";
+ public const string OcspChainValidation = "$SYS.SERVER.{0}.OCSP.CHAIN.VALIDATION";
+
// JetStream advisory events
// Go reference: jetstream_api.go advisory subjects
public const string JsAdvisoryStreamCreated = "$JS.EVENT.ADVISORY.STREAM.CREATED.{0}";
diff --git a/src/NATS.Server/Events/EventTypes.cs b/src/NATS.Server/Events/EventTypes.cs
index d5a1f7b..5106123 100644
--- a/src/NATS.Server/Events/EventTypes.cs
+++ b/src/NATS.Server/Events/EventTypes.cs
@@ -540,6 +540,119 @@ public sealed class OcspPeerRejectEventMsg
public string Reason { get; set; } = string.Empty;
}
+///
+/// OCSP chain validation advisory, published when a certificate's OCSP status
+/// is checked during TLS handshake.
+/// Go reference: ocsp.go — OCSP peer verification advisory.
+///
+public sealed class OcspChainValidationEvent
+{
+ public const string EventType = "io.nats.server.advisory.v1.ocsp_chain_validation";
+
+ [JsonPropertyName("type")]
+ public string Type { get; init; } = EventType;
+
+ [JsonPropertyName("id")]
+ public string Id { get; init; } = Guid.NewGuid().ToString("N");
+
+ [JsonPropertyName("time")]
+ public string Time { get; init; } = DateTime.UtcNow.ToString("O");
+
+ [JsonPropertyName("server")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public EventServerInfo? Server { get; set; }
+
+ [JsonPropertyName("cert_subject")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? CertSubject { get; set; }
+
+ [JsonPropertyName("cert_issuer")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? CertIssuer { get; set; }
+
+ [JsonPropertyName("serial_number")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? SerialNumber { get; set; }
+
+ /// OCSP status string: "good", "revoked", or "unknown".
+ [JsonPropertyName("ocsp_status")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? OcspStatus { get; set; }
+
+ [JsonPropertyName("checked_at")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public DateTime? CheckedAt { get; set; }
+
+ [JsonPropertyName("error")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? Error { get; set; }
+}
+
+///
+/// OCSP status values matching the Go OCSP response status strings.
+/// Go reference: ocsp.go — ocspStatusGood, ocspStatusRevoked, ocspStatusUnknown.
+///
+public enum OcspStatus
+{
+ Unknown,
+ Good,
+ Revoked,
+}
+
+///
+/// Factory helpers for constructing OCSP advisory event messages.
+/// Go reference: ocsp.go — OCSP peer reject and chain validation advisory publishing.
+///
+public static class OcspEventBuilder
+{
+ ///
+ /// Build an OcspPeerRejectEventMsg for a rejected peer certificate.
+ /// Go reference: ocsp.go — postOCSPPeerRejectEvent.
+ ///
+ public static OcspPeerRejectEventMsg BuildPeerReject(
+ string serverId, string serverName,
+ string kind, string reason) =>
+ new()
+ {
+ Id = EventBuilder.GenerateEventId(),
+ Time = DateTime.UtcNow,
+ Server = new EventServerInfo { Id = serverId, Name = serverName },
+ Kind = kind,
+ Reason = reason,
+ };
+
+ ///
+ /// Build an OcspChainValidationEvent for a certificate OCSP check.
+ /// Go reference: ocsp.go — OCSP chain validation advisory.
+ ///
+ public static OcspChainValidationEvent BuildChainValidation(
+ string serverId, string serverName,
+ string certSubject, string certIssuer, string serialNumber,
+ string ocspStatus, string? error = null) =>
+ new()
+ {
+ Server = new EventServerInfo { Id = serverId, Name = serverName },
+ CertSubject = certSubject,
+ CertIssuer = certIssuer,
+ SerialNumber = serialNumber,
+ OcspStatus = ocspStatus,
+ CheckedAt = DateTime.UtcNow,
+ Error = error,
+ };
+
+ ///
+ /// Parse an OCSP status string into the enum.
+ /// Go reference: ocsp.go — ocspStatusGood / ocspStatusRevoked string constants.
+ ///
+ public static OcspStatus ParseStatus(string? status) =>
+ status?.ToLowerInvariant() switch
+ {
+ "good" => OcspStatus.Good,
+ "revoked" => OcspStatus.Revoked,
+ _ => OcspStatus.Unknown,
+ };
+}
+
///
/// Remote server shutdown advisory.
/// Go reference: events.go — remote server lifecycle.
diff --git a/tests/NATS.Server.Tests/Events/OcspEventTests.cs b/tests/NATS.Server.Tests/Events/OcspEventTests.cs
new file mode 100644
index 0000000..43f07ec
--- /dev/null
+++ b/tests/NATS.Server.Tests/Events/OcspEventTests.cs
@@ -0,0 +1,266 @@
+// Port of Go server/ocsp_test.go — OCSP peer reject and chain validation advisory tests.
+// Go reference: golang/nats-server/server/ocsp.go — postOCSPPeerRejectEvent,
+// OCSP chain validation advisory publishing (Gap 10.10).
+//
+// Tests cover: OcspPeerRejectEventMsg, OcspChainValidationEvent, OcspEventBuilder,
+// OcspStatus enum, subject constants, and JSON serialisation round-trip.
+
+using System.Text.Json;
+using NATS.Server.Events;
+
+namespace NATS.Server.Tests.Events;
+
+///
+/// Tests for , ,
+/// , and the enum.
+/// Go reference: ocsp.go — OCSP peer verification advisory events (Gap 10.10).
+///
+public class OcspEventTests
+{
+ // ========================================================================
+ // OcspPeerRejectEventMsg
+ // Go reference: ocsp.go postOCSPPeerRejectEvent
+ // ========================================================================
+
+ ///
+ /// EventType constant must match the Go advisory type string.
+ /// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_peer_reject".
+ ///
+ [Fact]
+ public void OcspPeerRejectEventMsg_HasCorrectEventType()
+ {
+ // Go reference: ocsp.go — advisory type constant.
+ OcspPeerRejectEventMsg.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_peer_reject");
+ var ev = new OcspPeerRejectEventMsg();
+ ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
+ }
+
+ ///
+ /// Each OcspPeerRejectEventMsg gets its own unique non-empty Id.
+ /// Go reference: ocsp.go — nuid.Next() generates a unique event ID per advisory.
+ ///
+ [Fact]
+ public void OcspPeerRejectEventMsg_GeneratesUniqueId()
+ {
+ // Go reference: ocsp.go — each advisory is assigned a unique ID.
+ var ev1 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
+ var ev2 = new OcspPeerRejectEventMsg { Id = EventBuilder.GenerateEventId() };
+ ev1.Id.ShouldNotBeNullOrEmpty();
+ ev2.Id.ShouldNotBeNullOrEmpty();
+ ev1.Id.ShouldNotBe(ev2.Id);
+ }
+
+ // ========================================================================
+ // OcspChainValidationEvent
+ // Go reference: ocsp.go — OCSP chain validation advisory
+ // ========================================================================
+
+ ///
+ /// EventType constant must match the advisory type string.
+ /// Go reference: ocsp.go — "io.nats.server.advisory.v1.ocsp_chain_validation".
+ ///
+ [Fact]
+ public void OcspChainValidationEvent_HasCorrectEventType()
+ {
+ // Go reference: ocsp.go — chain validation advisory type constant.
+ OcspChainValidationEvent.EventType.ShouldBe("io.nats.server.advisory.v1.ocsp_chain_validation");
+ var ev = new OcspChainValidationEvent();
+ ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
+ }
+
+ ///
+ /// All optional fields on OcspChainValidationEvent can be assigned.
+ /// Go reference: ocsp.go — chain validation advisory carries cert metadata.
+ ///
+ [Fact]
+ public void OcspChainValidationEvent_AllFieldsSettable()
+ {
+ // Go reference: ocsp.go — OCSP advisory fields: subject, issuer, serial, status.
+ var server = new EventServerInfo { Id = "srv1", Name = "test-server" };
+ var checkedAt = new DateTime(2026, 2, 25, 12, 0, 0, DateTimeKind.Utc);
+ var ev = new OcspChainValidationEvent
+ {
+ Server = server,
+ CertSubject = "CN=leaf.example.com",
+ CertIssuer = "CN=Intermediate CA",
+ SerialNumber = "0123456789abcdef",
+ OcspStatus = "good",
+ CheckedAt = checkedAt,
+ Error = null,
+ };
+
+ ev.Server.ShouldBeSameAs(server);
+ ev.CertSubject.ShouldBe("CN=leaf.example.com");
+ ev.CertIssuer.ShouldBe("CN=Intermediate CA");
+ ev.SerialNumber.ShouldBe("0123456789abcdef");
+ ev.OcspStatus.ShouldBe("good");
+ ev.CheckedAt.ShouldBe(checkedAt);
+ ev.Error.ShouldBeNull();
+ }
+
+ // ========================================================================
+ // OcspEventBuilder
+ // Go reference: ocsp.go — postOCSPPeerRejectEvent, chain validation advisory
+ // ========================================================================
+
+ ///
+ /// BuildPeerReject populates all required fields of OcspPeerRejectEventMsg.
+ /// Go reference: ocsp.go postOCSPPeerRejectEvent — kind and reason always set.
+ ///
+ [Fact]
+ public void BuildPeerReject_PopulatesAllFields()
+ {
+ // Go reference: ocsp.go — peer reject advisory carries kind + reason.
+ var ev = OcspEventBuilder.BuildPeerReject(
+ serverId: "srv-abc",
+ serverName: "my-server",
+ kind: "client",
+ reason: "certificate revoked");
+
+ ev.Id.ShouldNotBeNullOrEmpty();
+ ev.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
+ ev.Server.ShouldNotBeNull();
+ ev.Server.Id.ShouldBe("srv-abc");
+ ev.Server.Name.ShouldBe("my-server");
+ ev.Kind.ShouldBe("client");
+ ev.Reason.ShouldBe("certificate revoked");
+ ev.Time.ShouldNotBe(default);
+ }
+
+ ///
+ /// BuildChainValidation populates all fields of OcspChainValidationEvent.
+ /// Go reference: ocsp.go — chain validation advisory carries full cert metadata.
+ ///
+ [Fact]
+ public void BuildChainValidation_PopulatesAllFields()
+ {
+ // Go reference: ocsp.go — chain validation advisory fields.
+ var before = DateTime.UtcNow;
+ var ev = OcspEventBuilder.BuildChainValidation(
+ serverId: "srv-xyz",
+ serverName: "nats-1",
+ certSubject: "CN=client.example.com",
+ certIssuer: "CN=Root CA",
+ serialNumber: "deadbeef",
+ ocspStatus: "good",
+ error: null);
+ var after = DateTime.UtcNow;
+
+ ev.Id.ShouldNotBeNullOrEmpty();
+ ev.Type.ShouldBe(OcspChainValidationEvent.EventType);
+ ev.Server.ShouldNotBeNull();
+ ev.Server!.Id.ShouldBe("srv-xyz");
+ ev.Server.Name.ShouldBe("nats-1");
+ ev.CertSubject.ShouldBe("CN=client.example.com");
+ ev.CertIssuer.ShouldBe("CN=Root CA");
+ ev.SerialNumber.ShouldBe("deadbeef");
+ ev.OcspStatus.ShouldBe("good");
+ ev.CheckedAt.ShouldNotBeNull();
+ ev.CheckedAt!.Value.ShouldBeInRange(before, after);
+ ev.Error.ShouldBeNull();
+ }
+
+ // ========================================================================
+ // OcspStatus enum — ParseStatus
+ // Go reference: ocsp.go — ocspStatusGood, ocspStatusRevoked constants
+ // ========================================================================
+
+ ///
+ /// ParseStatus("good") returns OcspStatus.Good.
+ /// Go reference: ocsp.go — ocspStatusGood = "good".
+ ///
+ [Fact]
+ public void ParseStatus_Good()
+ {
+ // Go reference: ocsp.go — ocspStatusGood.
+ OcspEventBuilder.ParseStatus("good").ShouldBe(OcspStatus.Good);
+ OcspEventBuilder.ParseStatus("Good").ShouldBe(OcspStatus.Good);
+ OcspEventBuilder.ParseStatus("GOOD").ShouldBe(OcspStatus.Good);
+ }
+
+ ///
+ /// ParseStatus("revoked") returns OcspStatus.Revoked.
+ /// Go reference: ocsp.go — ocspStatusRevoked = "revoked".
+ ///
+ [Fact]
+ public void ParseStatus_Revoked()
+ {
+ // Go reference: ocsp.go — ocspStatusRevoked.
+ OcspEventBuilder.ParseStatus("revoked").ShouldBe(OcspStatus.Revoked);
+ OcspEventBuilder.ParseStatus("Revoked").ShouldBe(OcspStatus.Revoked);
+ OcspEventBuilder.ParseStatus("REVOKED").ShouldBe(OcspStatus.Revoked);
+ }
+
+ ///
+ /// ParseStatus with null or unrecognised string returns OcspStatus.Unknown.
+ /// Go reference: ocsp.go — unknown/unrecognised OCSP status treated as unknown.
+ ///
+ [Fact]
+ public void ParseStatus_Unknown()
+ {
+ // Go reference: ocsp.go — default case maps to unknown status.
+ OcspEventBuilder.ParseStatus(null).ShouldBe(OcspStatus.Unknown);
+ OcspEventBuilder.ParseStatus("unknown").ShouldBe(OcspStatus.Unknown);
+ OcspEventBuilder.ParseStatus("").ShouldBe(OcspStatus.Unknown);
+ OcspEventBuilder.ParseStatus("invalid-status").ShouldBe(OcspStatus.Unknown);
+ }
+
+ // ========================================================================
+ // JSON serialisation round-trip
+ // Go reference: ocsp.go — advisories are published as JSON payloads
+ // ========================================================================
+
+ ///
+ /// OcspPeerRejectEventMsg and OcspChainValidationEvent round-trip through JSON
+ /// preserving all set fields.
+ /// Go reference: ocsp.go — OCSP advisories serialised as JSON before publishing.
+ ///
+ [Fact]
+ public void OcspEvents_SerializeToJson()
+ {
+ // Go reference: ocsp.go — advisories are JSON-encoded for publishing.
+ var peerReject = OcspEventBuilder.BuildPeerReject(
+ serverId: "srv-1",
+ serverName: "node-a",
+ kind: "route",
+ reason: "OCSP status revoked");
+
+ var chainValidation = OcspEventBuilder.BuildChainValidation(
+ serverId: "srv-2",
+ serverName: "node-b",
+ certSubject: "CN=server.nats.io",
+ certIssuer: "CN=NATS CA",
+ serialNumber: "cafebabe",
+ ocspStatus: "revoked",
+ error: "certificate has been revoked");
+
+ // Serialise OcspPeerRejectEventMsg
+ var rejectJson = JsonSerializer.Serialize(peerReject);
+ rejectJson.ShouldContain("io.nats.server.advisory.v1.ocsp_peer_reject");
+ rejectJson.ShouldContain("route");
+ rejectJson.ShouldContain("OCSP status revoked");
+
+ var rejectDeserialized = JsonSerializer.Deserialize(rejectJson);
+ rejectDeserialized.ShouldNotBeNull();
+ rejectDeserialized!.Type.ShouldBe(OcspPeerRejectEventMsg.EventType);
+ rejectDeserialized.Kind.ShouldBe("route");
+ rejectDeserialized.Reason.ShouldBe("OCSP status revoked");
+ rejectDeserialized.Server.Id.ShouldBe("srv-1");
+ rejectDeserialized.Server.Name.ShouldBe("node-a");
+
+ // Serialise OcspChainValidationEvent
+ var chainJson = JsonSerializer.Serialize(chainValidation);
+ chainJson.ShouldContain("io.nats.server.advisory.v1.ocsp_chain_validation");
+ chainJson.ShouldContain("CN=server.nats.io");
+ chainJson.ShouldContain("revoked");
+
+ var chainDeserialized = JsonSerializer.Deserialize(chainJson);
+ chainDeserialized.ShouldNotBeNull();
+ chainDeserialized!.Type.ShouldBe(OcspChainValidationEvent.EventType);
+ chainDeserialized.CertSubject.ShouldBe("CN=server.nats.io");
+ chainDeserialized.CertIssuer.ShouldBe("CN=NATS CA");
+ chainDeserialized.SerialNumber.ShouldBe("cafebabe");
+ chainDeserialized.OcspStatus.ShouldBe("revoked");
+ chainDeserialized.Error.ShouldBe("certificate has been revoked");
+ }
+}