@page "/certificates" @attribute [Microsoft.AspNetCore.Authorization.Authorize(Roles = AdminRoles.FleetAdmin)] @using ZB.MOM.WW.OtOpcUa.Admin.Services @inject CertTrustService Certs @inject AuthenticationStateProvider AuthState @inject ILogger Log

Certificate trust

PKI store root @Certs.PkiStoreRoot. Trusting a rejected cert moves the file into the trusted store — the OPC UA server picks up the change on the next client handshake, so operators should retry the rejected client's connection after trusting.
@if (_status is not null) {
@_status
}

Rejected (@_rejected.Count)

@if (_rejected.Count == 0) {

No rejected certificates. Clients that fail to handshake with an untrusted cert land here.

} else { @foreach (var c in _rejected) { }
SubjectIssuerThumbprintValidActions
@c.Subject @c.Issuer @c.Thumbprint @c.NotBefore.ToString("yyyy-MM-dd") → @c.NotAfter.ToString("yyyy-MM-dd")
}

Trusted (@_trusted.Count)

@if (_trusted.Count == 0) {

No client certs have been explicitly trusted. The server's own application cert lives in own/ and is not listed here.

} else { @foreach (var c in _trusted) { }
SubjectIssuerThumbprintValidActions
@c.Subject @c.Issuer @c.Thumbprint @c.NotBefore.ToString("yyyy-MM-dd") → @c.NotAfter.ToString("yyyy-MM-dd")
} @code { private IReadOnlyList _rejected = []; private IReadOnlyList _trusted = []; private string? _status; private string _statusKind = "success"; protected override void OnInitialized() => Reload(); private void Reload() { _rejected = Certs.ListRejected(); _trusted = Certs.ListTrusted(); } private async Task TrustAsync(CertInfo c) { if (Certs.TrustRejected(c.Thumbprint)) { await LogActionAsync("cert.trust", c); Set($"Trusted cert {c.Subject} ({Short(c.Thumbprint)}).", "success"); } else { Set($"Could not trust {Short(c.Thumbprint)} — file missing; another admin may have already handled it.", "warning"); } Reload(); } private async Task DeleteRejectedAsync(CertInfo c) { if (Certs.DeleteRejected(c.Thumbprint)) { await LogActionAsync("cert.delete.rejected", c); Set($"Deleted rejected cert {c.Subject} ({Short(c.Thumbprint)}).", "success"); } else { Set($"Could not delete {Short(c.Thumbprint)} — file missing.", "warning"); } Reload(); } private async Task UntrustAsync(CertInfo c) { if (Certs.UntrustCert(c.Thumbprint)) { await LogActionAsync("cert.untrust", c); Set($"Revoked trust for {c.Subject} ({Short(c.Thumbprint)}).", "success"); } else { Set($"Could not revoke {Short(c.Thumbprint)} — file missing.", "warning"); } Reload(); } private async Task LogActionAsync(string action, CertInfo c) { // Cert trust changes are operator-initiated and security-sensitive — Serilog captures the // user + thumbprint trail. CertTrustService also logs at Information on each filesystem // move/delete; this line ties the action to the authenticated admin user so the two logs // correlate. DB-level ConfigAuditLog persistence is deferred — its schema is // cluster-scoped and cert actions are cluster-agnostic. var state = await AuthState.GetAuthenticationStateAsync(); var user = state.User.Identity?.Name ?? "(anonymous)"; Log.LogInformation("Admin cert action: user={User} action={Action} thumbprint={Thumbprint} subject={Subject}", user, action, c.Thumbprint, c.Subject); } private void Set(string message, string kind) { _status = message; _statusKind = kind; } private void ClearStatus() => _status = null; private static string Short(string thumbprint) => thumbprint.Length > 12 ? thumbprint[..12] + "…" : thumbprint; }