@page "/certificates" @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using System.Security.Cryptography.X509Certificates @using Microsoft.Extensions.Configuration @using Microsoft.AspNetCore.Components.Authorization @using ZB.MOM.WW.OtOpcUa.AdminUI.Certificates @inject IConfiguration Config @inject CertificateStoreManager CertManager @implements IDisposable

OPC UA certificates

PKI store layout: {PkiStoreRoot}/own (this server's identity), issuer / trusted (peers we accept), rejected (peers we've turned away). F13a wires SDK auto-creation so the own-store self-signs on first boot.
@if (_rows is null) {

Loading…

} else { @if (_statusMsg is not null) {
@_statusMsg
} @if (_pending is { } p) {
Confirm @p.Verb of @p.Subject?
} @foreach (var store in _rows) {
@store.Label · @store.Certificates.Count entry@(store.Certificates.Count == 1 ? "" : "s")
@if (string.IsNullOrEmpty(store.Path)) {
No path configured.
} else if (!Directory.Exists(store.Path)) {
@store.Path doesn't exist yet. It will be created on first boot.
} else if (store.Certificates.Count == 0) {
No certificates in @store.Path.
} else {
@if (store.Kind is StoreKind.Trusted or StoreKind.Rejected) { } @foreach (var c in store.Certificates) { @if (store.Kind is StoreKind.Trusted or StoreKind.Rejected) { } }
Subject Issuer Thumbprint Not before Not afterActions
@c.Subject @c.Issuer @c.Thumbprint[..16]… @c.NotBefore.ToString("u") @c.NotAfter.ToString("u") @if (store.Kind == StoreKind.Rejected) { } else { }
}
} } @code { private List? _rows; private enum StoreKind { Own, Trusted, Issuer, Rejected } private sealed record StoreView(string Label, StoreKind Kind, string Path, List Certificates); private (StoreKind Kind, string Thumbprint, string Subject, string Verb)? _pending; private string? _statusMsg; private bool _statusError; protected override void OnInitialized() => LoadAll(); private void LoadAll() { DisposeRows(); var pkiRoot = Config.GetValue("OpcUa:PkiStoreRoot") ?? "pki"; _rows = new() { LoadStore("Own", StoreKind.Own, Path.Combine(pkiRoot, "own", "certs")), LoadStore("Trusted peers", StoreKind.Trusted, Path.Combine(pkiRoot, "trusted", "certs")), LoadStore("Trusted issuers", StoreKind.Issuer, Path.Combine(pkiRoot, "issuer", "certs")), LoadStore("Rejected", StoreKind.Rejected, Path.Combine(pkiRoot, "rejected", "certs")), }; _pending = null; } private void DisposeRows() { if (_rows is null) return; foreach (var store in _rows) foreach (var c in store.Certificates) c.Dispose(); } public void Dispose() => DisposeRows(); private static StoreView LoadStore(string label, StoreKind kind, string path) { var view = new StoreView(label, kind, path, new List()); if (!Directory.Exists(path)) return view; foreach (var file in Directory.EnumerateFiles(path).Where(IsCertFile)) { try { view.Certificates.Add(X509CertificateLoader.LoadCertificateFromFile(file)); } catch { /* ignore unreadable entries */ } } return view; } private static bool IsCertFile(string path) { var ext = Path.GetExtension(path); return ext.Equals(".der", StringComparison.OrdinalIgnoreCase) || ext.Equals(".cer", StringComparison.OrdinalIgnoreCase) || ext.Equals(".crt", StringComparison.OrdinalIgnoreCase); } private void RequestAction(StoreKind kind, X509Certificate2 cert, string verb) { _pending = (kind, cert.Thumbprint!, cert.Subject, verb); _statusMsg = null; } private void CancelAction() => _pending = null; private void ConfirmAction() { if (_pending is not { } p) return; var result = p.Verb switch { "trust" => CertManager.Trust(p.Thumbprint), "untrust" => CertManager.Untrust(p.Thumbprint), "delete" => CertManager.Delete(p.Kind == StoreKind.Trusted ? "trusted" : "rejected", p.Thumbprint), _ => CertActionResult.Fail("unknown action"), }; _statusError = !result.Success; _statusMsg = result.Success ? $"{char.ToUpper(p.Verb[0])}{p.Verb[1..]} of {p.Subject} succeeded." : $"{p.Verb} failed: {result.Error}"; LoadAll(); // clears _pending } }